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