View Javadoc
1   /*
2    * Copyright (C) 2008-2010, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.transport;
45  
46  import static java.nio.charset.StandardCharsets.UTF_8;
47  import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
48  import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SYMREF;
49  
50  import java.io.IOException;
51  import java.nio.ByteBuffer;
52  import java.nio.CharBuffer;
53  import java.nio.charset.CharacterCodingException;
54  import java.nio.charset.CharsetEncoder;
55  import java.nio.charset.CoderResult;
56  import java.util.HashSet;
57  import java.util.LinkedHashSet;
58  import java.util.Map;
59  import java.util.Set;
60  import java.util.SortedMap;
61  
62  import org.eclipse.jgit.lib.AnyObjectId;
63  import org.eclipse.jgit.lib.Constants;
64  import org.eclipse.jgit.lib.ObjectId;
65  import org.eclipse.jgit.lib.Ref;
66  import org.eclipse.jgit.lib.RefComparator;
67  import org.eclipse.jgit.lib.Repository;
68  import org.eclipse.jgit.util.RefMap;
69  
70  /** Support for the start of {@link UploadPack} and {@link ReceivePack}. */
71  public abstract class RefAdvertiser {
72  	/** Advertiser which frames lines in a {@link PacketLineOut} format. */
73  	public static class PacketLineOutRefAdvertiser extends RefAdvertiser {
74  		private final CharsetEncoder utf8 = UTF_8.newEncoder();
75  		private final PacketLineOut pckOut;
76  
77  		private byte[] binArr = new byte[256];
78  		private ByteBuffer binBuf = ByteBuffer.wrap(binArr);
79  
80  		private char[] chArr = new char[256];
81  		private CharBuffer chBuf = CharBuffer.wrap(chArr);
82  
83  		/**
84  		 * Create a new advertiser for the supplied stream.
85  		 *
86  		 * @param out
87  		 *            the output stream.
88  		 */
89  		public PacketLineOutRefAdvertiser(PacketLineOut out) {
90  			pckOut = out;
91  		}
92  
93  		@Override
94  		public void advertiseId(AnyObjectId id, String refName)
95  				throws IOException {
96  			id.copyTo(binArr, 0);
97  			binArr[OBJECT_ID_STRING_LENGTH] = ' ';
98  			binBuf.position(OBJECT_ID_STRING_LENGTH + 1);
99  			append(refName);
100 			if (first) {
101 				first = false;
102 				if (!capablities.isEmpty()) {
103 					append('\0');
104 					for (String cap : capablities) {
105 						append(' ');
106 						append(cap);
107 					}
108 				}
109 			}
110 			append('\n');
111 			pckOut.writePacket(binArr, 0, binBuf.position());
112 		}
113 
114 		private void append(String str) throws CharacterCodingException {
115 			int n = str.length();
116 			if (n > chArr.length) {
117 				chArr = new char[n + 256];
118 				chBuf = CharBuffer.wrap(chArr);
119 			}
120 			str.getChars(0, n, chArr, 0);
121 			chBuf.position(0).limit(n);
122 			utf8.reset();
123 			for (;;) {
124 				CoderResult cr = utf8.encode(chBuf, binBuf, true);
125 				if (cr.isOverflow()) {
126 					grow();
127 				} else if (cr.isUnderflow()) {
128 					break;
129 				} else {
130 					cr.throwException();
131 				}
132 			}
133 		}
134 
135 		private void append(int b) {
136 			if (!binBuf.hasRemaining()) {
137 				grow();
138 			}
139 			binBuf.put((byte) b);
140 		}
141 
142 		private void grow() {
143 			int cnt = binBuf.position();
144 			byte[] tmp = new byte[binArr.length << 1];
145 			System.arraycopy(binArr, 0, tmp, 0, cnt);
146 			binArr = tmp;
147 			binBuf = ByteBuffer.wrap(binArr);
148 			binBuf.position(cnt);
149 		}
150 
151 		@Override
152 		protected void writeOne(final CharSequence line) throws IOException {
153 			pckOut.writeString(line.toString());
154 		}
155 
156 		@Override
157 		protected void end() throws IOException {
158 			pckOut.end();
159 		}
160 	}
161 
162 	private final StringBuilder tmpLine = new StringBuilder(100);
163 
164 	private final char[] tmpId = new char[Constants.OBJECT_ID_STRING_LENGTH];
165 
166 	final Set<String> capablities = new LinkedHashSet<>();
167 
168 	private final Set<ObjectId> sent = new HashSet<>();
169 
170 	private Repository repository;
171 
172 	private boolean derefTags;
173 
174 	boolean first = true;
175 
176 	/**
177 	 * Initialize this advertiser with a repository for peeling tags.
178 	 *
179 	 * @param src
180 	 *            the repository to read from.
181 	 */
182 	public void init(Repository src) {
183 		repository = src;
184 	}
185 
186 	/**
187 	 * Toggle tag peeling.
188 	 * <p>
189 	 * <p>
190 	 * This method must be invoked prior to any of the following:
191 	 * <ul>
192 	 * <li>{@link #send(Map)}
193 	 * </ul>
194 	 *
195 	 * @param deref
196 	 *            true to show the dereferenced value of a tag as the special
197 	 *            ref <code>$tag^{}</code> ; false to omit it from the output.
198 	 */
199 	public void setDerefTags(final boolean deref) {
200 		derefTags = deref;
201 	}
202 
203 	/**
204 	 * Add one protocol capability to the initial advertisement.
205 	 * <p>
206 	 * This method must be invoked prior to any of the following:
207 	 * <ul>
208 	 * <li>{@link #send(Map)}
209 	 * <li>{@link #advertiseHave(AnyObjectId)}
210 	 * </ul>
211 	 *
212 	 * @param name
213 	 *            the name of a single protocol capability supported by the
214 	 *            caller. The set of capabilities are sent to the client in the
215 	 *            advertisement, allowing the client to later selectively enable
216 	 *            features it recognizes.
217 	 */
218 	public void advertiseCapability(String name) {
219 		capablities.add(name);
220 	}
221 
222 	/**
223 	 * Add one protocol capability with a value ({@code "name=value"}).
224 	 *
225 	 * @param name
226 	 *            name of the capability.
227 	 * @param value
228 	 *            value. If null the capability will not be added.
229 	 * @since 4.0
230 	 */
231 	public void advertiseCapability(String name, String value) {
232 		if (value != null) {
233 			capablities.add(name + '=' + value);
234 		}
235 	}
236 
237 	/**
238 	 * Add a symbolic ref to capabilities.
239 	 * <p>
240 	 * This method must be invoked prior to any of the following:
241 	 * <ul>
242 	 * <li>{@link #send(Map)}
243 	 * <li>{@link #advertiseHave(AnyObjectId)}
244 	 * </ul>
245 	 *
246 	 * @param from
247 	 *            The symbolic ref, e.g. "HEAD"
248 	 * @param to
249 	 *            The real ref it points to, e.g. "refs/heads/master"
250 	 *
251 	 * @since 3.6
252 	 */
253 	public void addSymref(String from, String to) {
254 		advertiseCapability(OPTION_SYMREF, from + ':' + to);
255 	}
256 
257 	/**
258 	 * Format an advertisement for the supplied refs.
259 	 *
260 	 * @param refs
261 	 *            zero or more refs to format for the client. The collection is
262 	 *            sorted before display if necessary, and therefore may appear
263 	 *            in any order.
264 	 * @return set of ObjectIds that were advertised to the client.
265 	 * @throws IOException
266 	 *             the underlying output stream failed to write out an
267 	 *             advertisement record.
268 	 */
269 	public Set<ObjectId> send(Map<String, Ref> refs) throws IOException {
270 		for (Ref ref : getSortedRefs(refs)) {
271 			if (ref.getObjectId() == null)
272 				continue;
273 
274 			advertiseAny(ref.getObjectId(), ref.getName());
275 
276 			if (!derefTags)
277 				continue;
278 
279 			if (!ref.isPeeled()) {
280 				if (repository == null)
281 					continue;
282 				ref = repository.peel(ref);
283 			}
284 
285 			if (ref.getPeeledObjectId() != null)
286 				advertiseAny(ref.getPeeledObjectId(), ref.getName() + "^{}"); //$NON-NLS-1$
287 		}
288 		return sent;
289 	}
290 
291 	private Iterable<Ref> getSortedRefs(Map<String, Ref> all) {
292 		if (all instanceof RefMap
293 				|| (all instanceof SortedMap && ((SortedMap) all).comparator() == null))
294 			return all.values();
295 		return RefComparator.sort(all.values());
296 	}
297 
298 	/**
299 	 * Advertise one object is available using the magic {@code .have}.
300 	 * <p>
301 	 * The magic {@code .have} advertisement is not available for fetching by a
302 	 * client, but can be used by a client when considering a delta base
303 	 * candidate before transferring data in a push. Within the record created
304 	 * by this method the ref name is simply the invalid string {@code .have}.
305 	 *
306 	 * @param id
307 	 *            identity of the object that is assumed to exist.
308 	 * @throws IOException
309 	 *             the underlying output stream failed to write out an
310 	 *             advertisement record.
311 	 */
312 	public void advertiseHave(AnyObjectId id) throws IOException {
313 		advertiseAnyOnce(id, ".have"); //$NON-NLS-1$
314 	}
315 
316 	/** @return true if no advertisements have been sent yet. */
317 	public boolean isEmpty() {
318 		return first;
319 	}
320 
321 	private void advertiseAnyOnce(AnyObjectId obj, final String refName)
322 			throws IOException {
323 		if (!sent.contains(obj))
324 			advertiseAny(obj, refName);
325 	}
326 
327 	private void advertiseAny(AnyObjectId obj, final String refName)
328 			throws IOException {
329 		sent.add(obj.toObjectId());
330 		advertiseId(obj, refName);
331 	}
332 
333 	/**
334 	 * Advertise one object under a specific name.
335 	 * <p>
336 	 * If the advertised object is a tag, this method does not advertise the
337 	 * peeled version of it.
338 	 *
339 	 * @param id
340 	 *            the object to advertise.
341 	 * @param refName
342 	 *            name of the reference to advertise the object as, can be any
343 	 *            string not including the NUL byte.
344 	 * @throws IOException
345 	 *             the underlying output stream failed to write out an
346 	 *             advertisement record.
347 	 */
348 	public void advertiseId(final AnyObjectId id, final String refName)
349 			throws IOException {
350 		tmpLine.setLength(0);
351 		id.copyTo(tmpId, tmpLine);
352 		tmpLine.append(' ');
353 		tmpLine.append(refName);
354 		if (first) {
355 			first = false;
356 			if (!capablities.isEmpty()) {
357 				tmpLine.append('\0');
358 				for (final String capName : capablities) {
359 					tmpLine.append(' ');
360 					tmpLine.append(capName);
361 				}
362 				tmpLine.append(' ');
363 			}
364 		}
365 		tmpLine.append('\n');
366 		writeOne(tmpLine);
367 	}
368 
369 	/**
370 	 * Write a single advertisement line.
371 	 *
372 	 * @param line
373 	 *            the advertisement line to be written. The line always ends
374 	 *            with LF. Never null or the empty string.
375 	 * @throws IOException
376 	 *             the underlying output stream failed to write out an
377 	 *             advertisement record.
378 	 */
379 	protected abstract void writeOne(CharSequence line) throws IOException;
380 
381 	/**
382 	 * Mark the end of the advertisements.
383 	 *
384 	 * @throws IOException
385 	 *             the underlying output stream failed to write out an
386 	 *             advertisement record.
387 	 */
388 	protected abstract void end() throws IOException;
389 }