View Javadoc
1   /*
2    * Copyright (C) 2008-2010, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.transport;
12  
13  import static java.nio.charset.StandardCharsets.UTF_8;
14  import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
15  import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SYMREF;
16  
17  import java.io.IOException;
18  import java.nio.ByteBuffer;
19  import java.nio.CharBuffer;
20  import java.nio.charset.CharacterCodingException;
21  import java.nio.charset.CharsetEncoder;
22  import java.nio.charset.CoderResult;
23  import java.util.Collection;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.LinkedHashSet;
27  import java.util.Map;
28  import java.util.Set;
29  
30  import org.eclipse.jgit.lib.AnyObjectId;
31  import org.eclipse.jgit.lib.Constants;
32  import org.eclipse.jgit.lib.ObjectId;
33  import org.eclipse.jgit.lib.Ref;
34  import org.eclipse.jgit.lib.RefComparator;
35  import org.eclipse.jgit.lib.Repository;
36  
37  /**
38   * Support for the start of {@link org.eclipse.jgit.transport.UploadPack} and
39   * {@link org.eclipse.jgit.transport.ReceivePack}.
40   */
41  public abstract class RefAdvertiser {
42  	/** Advertiser which frames lines in a {@link PacketLineOut} format. */
43  	public static class PacketLineOutRefAdvertiser extends RefAdvertiser {
44  		private final CharsetEncoder utf8 = UTF_8.newEncoder();
45  		private final PacketLineOut pckOut;
46  
47  		private byte[] binArr = new byte[256];
48  		private ByteBuffer binBuf = ByteBuffer.wrap(binArr);
49  
50  		private char[] chArr = new char[256];
51  		private CharBuffer chBuf = CharBuffer.wrap(chArr);
52  
53  		/**
54  		 * Create a new advertiser for the supplied stream.
55  		 *
56  		 * @param out
57  		 *            the output stream.
58  		 */
59  		public PacketLineOutRefAdvertiser(PacketLineOut out) {
60  			pckOut = out;
61  		}
62  
63  		@Override
64  		public void advertiseId(AnyObjectId id, String refName)
65  				throws IOException {
66  			id.copyTo(binArr, 0);
67  			binArr[OBJECT_ID_STRING_LENGTH] = ' ';
68  			binBuf.position(OBJECT_ID_STRING_LENGTH + 1);
69  			append(refName);
70  			if (first) {
71  				first = false;
72  				if (!capablities.isEmpty()) {
73  					append('\0');
74  					for (String cap : capablities) {
75  						append(' ');
76  						append(cap);
77  					}
78  				}
79  			}
80  			append('\n');
81  			pckOut.writePacket(binArr, 0, binBuf.position());
82  		}
83  
84  		private void append(String str) throws CharacterCodingException {
85  			int n = str.length();
86  			if (n > chArr.length) {
87  				chArr = new char[n + 256];
88  				chBuf = CharBuffer.wrap(chArr);
89  			}
90  			str.getChars(0, n, chArr, 0);
91  			chBuf.position(0).limit(n);
92  			utf8.reset();
93  			for (;;) {
94  				CoderResult cr = utf8.encode(chBuf, binBuf, true);
95  				if (cr.isOverflow()) {
96  					grow();
97  				} else if (cr.isUnderflow()) {
98  					break;
99  				} else {
100 					cr.throwException();
101 				}
102 			}
103 		}
104 
105 		private void append(int b) {
106 			if (!binBuf.hasRemaining()) {
107 				grow();
108 			}
109 			binBuf.put((byte) b);
110 		}
111 
112 		private void grow() {
113 			int cnt = binBuf.position();
114 			byte[] tmp = new byte[binArr.length << 1];
115 			System.arraycopy(binArr, 0, tmp, 0, cnt);
116 			binArr = tmp;
117 			binBuf = ByteBuffer.wrap(binArr);
118 			binBuf.position(cnt);
119 		}
120 
121 		@Override
122 		protected void writeOne(CharSequence line) throws IOException {
123 			pckOut.writeString(line.toString());
124 		}
125 
126 		@Override
127 		protected void end() throws IOException {
128 			pckOut.end();
129 		}
130 	}
131 
132 	private final StringBuilder tmpLine = new StringBuilder(100);
133 
134 	private final char[] tmpId = new char[Constants.OBJECT_ID_STRING_LENGTH];
135 
136 	final Set<String> capablities = new LinkedHashSet<>();
137 
138 	private final Set<ObjectId> sent = new HashSet<>();
139 
140 	private Repository repository;
141 
142 	private boolean derefTags;
143 
144 	boolean first = true;
145 
146 	private boolean useProtocolV2;
147 
148 	/* only used in protocol v2 */
149 	private final Map<String, String> symrefs = new HashMap<>();
150 
151 	/**
152 	 * Initialize this advertiser with a repository for peeling tags.
153 	 *
154 	 * @param src
155 	 *            the repository to read from.
156 	 */
157 	public void init(Repository src) {
158 		repository = src;
159 	}
160 
161 	/**
162 	 * @param b
163 	 *              true if this advertiser should advertise using the protocol
164 	 *              v2 format, false otherwise
165 	 * @since 5.0
166 	 */
167 	public void setUseProtocolV2(boolean b) {
168 		useProtocolV2 = b;
169 	}
170 
171 	/**
172 	 * Toggle tag peeling.
173 	 * <p>
174 	 * <p>
175 	 * This method must be invoked prior to any of the following:
176 	 * <ul>
177 	 * <li>{@link #send(Map)}</li>
178 	 * <li>{@link #send(Collection)}</li>
179 	 * </ul>
180 	 *
181 	 * @param deref
182 	 *            true to show the dereferenced value of a tag as the special
183 	 *            ref <code>$tag^{}</code> ; false to omit it from the output.
184 	 */
185 	public void setDerefTags(boolean deref) {
186 		derefTags = deref;
187 	}
188 
189 	/**
190 	 * Add one protocol capability to the initial advertisement.
191 	 * <p>
192 	 * This method must be invoked prior to any of the following:
193 	 * <ul>
194 	 * <li>{@link #send(Map)}</li>
195 	 * <li>{@link #send(Collection)}</li>
196 	 * <li>{@link #advertiseHave(AnyObjectId)}</li>
197 	 * </ul>
198 	 *
199 	 * @param name
200 	 *            the name of a single protocol capability supported by the
201 	 *            caller. The set of capabilities are sent to the client in the
202 	 *            advertisement, allowing the client to later selectively enable
203 	 *            features it recognizes.
204 	 */
205 	public void advertiseCapability(String name) {
206 		capablities.add(name);
207 	}
208 
209 	/**
210 	 * Add one protocol capability with a value ({@code "name=value"}).
211 	 *
212 	 * @param name
213 	 *            name of the capability.
214 	 * @param value
215 	 *            value. If null the capability will not be added.
216 	 * @since 4.0
217 	 */
218 	public void advertiseCapability(String name, String value) {
219 		if (value != null) {
220 			capablities.add(name + '=' + value);
221 		}
222 	}
223 
224 	/**
225 	 * Add a symbolic ref to capabilities.
226 	 * <p>
227 	 * This method must be invoked prior to any of the following:
228 	 * <ul>
229 	 * <li>{@link #send(Map)}</li>
230 	 * <li>{@link #send(Collection)}</li>
231 	 * <li>{@link #advertiseHave(AnyObjectId)}</li>
232 	 * </ul>
233 	 *
234 	 * @param from
235 	 *            The symbolic ref, e.g. "HEAD"
236 	 * @param to
237 	 *            The real ref it points to, e.g. "refs/heads/master"
238 	 * @since 3.6
239 	 */
240 	public void addSymref(String from, String to) {
241 		if (useProtocolV2) {
242 			symrefs.put(from, to);
243 		} else {
244 			advertiseCapability(OPTION_SYMREF, from + ':' + to);
245 		}
246 	}
247 
248 	/**
249 	 * Format an advertisement for the supplied refs.
250 	 *
251 	 * @param refs
252 	 *            zero or more refs to format for the client. The collection is
253 	 *            sorted before display if necessary, and therefore may appear
254 	 *            in any order.
255 	 * @return set of ObjectIds that were advertised to the client.
256 	 * @throws java.io.IOException
257 	 *             the underlying output stream failed to write out an
258 	 *             advertisement record.
259 	 * @deprecated use {@link #send(Collection)} instead.
260 	 */
261 	@Deprecated
262 	public Set<ObjectId> send(Map<String, Ref> refs) throws IOException {
263 		return send(refs.values());
264 	}
265 
266 	/**
267 	 * Format an advertisement for the supplied refs.
268 	 *
269 	 * @param refs
270 	 *            zero or more refs to format for the client. The collection is
271 	 *            sorted before display if necessary, and therefore may appear
272 	 *            in any order.
273 	 * @return set of ObjectIds that were advertised to the client.
274 	 * @throws java.io.IOException
275 	 *             the underlying output stream failed to write out an
276 	 *             advertisement record.
277 	 * @since 5.0
278 	 */
279 	public Set<ObjectId> send(Collection<Ref> refs) throws IOException {
280 		for (Ref ref : RefComparator.sort(refs)) {
281 			// TODO(jrn) revive the SortedMap optimization e.g. by introducing
282 			// SortedList
283 			ObjectId objectId = ref.getObjectId();
284 			if (objectId == null) {
285 				continue;
286 			}
287 
288 			if (useProtocolV2) {
289 				String symrefPart = symrefs.containsKey(ref.getName())
290 						? (" symref-target:" + symrefs.get(ref.getName())) //$NON-NLS-1$
291 						: ""; //$NON-NLS-1$
292 				String peelPart = ""; //$NON-NLS-1$
293 				if (derefTags) {
294 					if (!ref.isPeeled() && repository != null) {
295 						ref = repository.getRefDatabase().peel(ref);
296 					}
297 					ObjectId peeledObjectId = ref.getPeeledObjectId();
298 					if (peeledObjectId != null) {
299 						peelPart = " peeled:" + peeledObjectId.getName(); //$NON-NLS-1$
300 					}
301 				}
302 				writeOne(objectId.getName() + " " + ref.getName() + symrefPart //$NON-NLS-1$
303 						+ peelPart + "\n"); //$NON-NLS-1$
304 				continue;
305 			}
306 
307 			advertiseAny(objectId, ref.getName());
308 
309 			if (!derefTags)
310 				continue;
311 
312 			if (!ref.isPeeled()) {
313 				if (repository == null)
314 					continue;
315 				ref = repository.getRefDatabase().peel(ref);
316 			}
317 
318 			if (ref.getPeeledObjectId() != null)
319 				advertiseAny(ref.getPeeledObjectId(), ref.getName() + "^{}"); //$NON-NLS-1$
320 		}
321 		return sent;
322 	}
323 
324 	/**
325 	 * Advertise one object is available using the magic {@code .have}.
326 	 * <p>
327 	 * The magic {@code .have} advertisement is not available for fetching by a
328 	 * client, but can be used by a client when considering a delta base
329 	 * candidate before transferring data in a push. Within the record created
330 	 * by this method the ref name is simply the invalid string {@code .have}.
331 	 *
332 	 * @param id
333 	 *            identity of the object that is assumed to exist.
334 	 * @throws java.io.IOException
335 	 *             the underlying output stream failed to write out an
336 	 *             advertisement record.
337 	 */
338 	public void advertiseHave(AnyObjectId id) throws IOException {
339 		advertiseAnyOnce(id, ".have"); //$NON-NLS-1$
340 	}
341 
342 	/**
343 	 * Whether no advertisements have been sent yet.
344 	 *
345 	 * @return true if no advertisements have been sent yet.
346 	 */
347 	public boolean isEmpty() {
348 		return first;
349 	}
350 
351 	private void advertiseAnyOnce(AnyObjectId obj, String refName)
352 			throws IOException {
353 		if (!sent.contains(obj))
354 			advertiseAny(obj, refName);
355 	}
356 
357 	private void advertiseAny(AnyObjectId obj, String refName)
358 			throws IOException {
359 		sent.add(obj.toObjectId());
360 		advertiseId(obj, refName);
361 	}
362 
363 	/**
364 	 * Advertise one object under a specific name.
365 	 * <p>
366 	 * If the advertised object is a tag, this method does not advertise the
367 	 * peeled version of it.
368 	 *
369 	 * @param id
370 	 *            the object to advertise.
371 	 * @param refName
372 	 *            name of the reference to advertise the object as, can be any
373 	 *            string not including the NUL byte.
374 	 * @throws java.io.IOException
375 	 *             the underlying output stream failed to write out an
376 	 *             advertisement record.
377 	 */
378 	public void advertiseId(AnyObjectId id, String refName)
379 			throws IOException {
380 		tmpLine.setLength(0);
381 		id.copyTo(tmpId, tmpLine);
382 		tmpLine.append(' ');
383 		tmpLine.append(refName);
384 		if (first) {
385 			first = false;
386 			if (!capablities.isEmpty()) {
387 				tmpLine.append('\0');
388 				for (String capName : capablities) {
389 					tmpLine.append(' ');
390 					tmpLine.append(capName);
391 				}
392 				tmpLine.append(' ');
393 			}
394 		}
395 		tmpLine.append('\n');
396 		writeOne(tmpLine);
397 	}
398 
399 	/**
400 	 * Write a single advertisement line.
401 	 *
402 	 * @param line
403 	 *            the advertisement line to be written. The line always ends
404 	 *            with LF. Never null or the empty string.
405 	 * @throws java.io.IOException
406 	 *             the underlying output stream failed to write out an
407 	 *             advertisement record.
408 	 */
409 	protected abstract void writeOne(CharSequence line) throws IOException;
410 
411 	/**
412 	 * Mark the end of the advertisements.
413 	 *
414 	 * @throws java.io.IOException
415 	 *             the underlying output stream failed to write out an
416 	 *             advertisement record.
417 	 */
418 	protected abstract void end() throws IOException;
419 }