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