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)}
211 * </ul>
212 *
213 * @param deref
214 * true to show the dereferenced value of a tag as the special
215 * ref <code>$tag^{}</code> ; false to omit it from the output.
216 */
217 public void setDerefTags(boolean deref) {
218 derefTags = deref;
219 }
220
221 /**
222 * Add one protocol capability to the initial advertisement.
223 * <p>
224 * This method must be invoked prior to any of the following:
225 * <ul>
226 * <li>{@link #send(Map)}
227 * <li>{@link #advertiseHave(AnyObjectId)}
228 * </ul>
229 *
230 * @param name
231 * the name of a single protocol capability supported by the
232 * caller. The set of capabilities are sent to the client in the
233 * advertisement, allowing the client to later selectively enable
234 * features it recognizes.
235 */
236 public void advertiseCapability(String name) {
237 capablities.add(name);
238 }
239
240 /**
241 * Add one protocol capability with a value ({@code "name=value"}).
242 *
243 * @param name
244 * name of the capability.
245 * @param value
246 * value. If null the capability will not be added.
247 * @since 4.0
248 */
249 public void advertiseCapability(String name, String value) {
250 if (value != null) {
251 capablities.add(name + '=' + value);
252 }
253 }
254
255 /**
256 * Add a symbolic ref to capabilities.
257 * <p>
258 * This method must be invoked prior to any of the following:
259 * <ul>
260 * <li>{@link #send(Map)}
261 * <li>{@link #advertiseHave(AnyObjectId)}
262 * </ul>
263 *
264 * @param from
265 * The symbolic ref, e.g. "HEAD"
266 * @param to
267 * The real ref it points to, e.g. "refs/heads/master"
268 * @since 3.6
269 */
270 public void addSymref(String from, String to) {
271 if (useProtocolV2) {
272 symrefs.put(from, to);
273 } else {
274 advertiseCapability(OPTION_SYMREF, from + ':' + to);
275 }
276 }
277
278 /**
279 * Format an advertisement for the supplied refs.
280 *
281 * @param refs
282 * zero or more refs to format for the client. The collection is
283 * sorted before display if necessary, and therefore may appear
284 * in any order.
285 * @return set of ObjectIds that were advertised to the client.
286 * @throws java.io.IOException
287 * the underlying output stream failed to write out an
288 * advertisement record.
289 * @deprecated use {@link #send(Collection)} instead.
290 */
291 @Deprecated
292 public Set<ObjectId> send(Map<String, Ref> refs) throws IOException {
293 return send(refs.values());
294 }
295
296 /**
297 * Format an advertisement for the supplied refs.
298 *
299 * @param refs
300 * zero or more refs to format for the client. The collection is
301 * sorted before display if necessary, and therefore may appear
302 * in any order.
303 * @return set of ObjectIds that were advertised to the client.
304 * @throws java.io.IOException
305 * the underlying output stream failed to write out an
306 * advertisement record.
307 * @since 5.0
308 */
309 public Set<ObjectId> send(Collection<Ref> refs) throws IOException {
310 for (Ref ref : RefComparator.sort(refs)) {
311 // TODO(jrn) revive the SortedMap optimization e.g. by introducing
312 // SortedList
313 ObjectId objectId = ref.getObjectId();
314 if (objectId == null) {
315 continue;
316 }
317
318 if (useProtocolV2) {
319 String symrefPart = symrefs.containsKey(ref.getName())
320 ? (" symref-target:" + symrefs.get(ref.getName())) //$NON-NLS-1$
321 : ""; //$NON-NLS-1$
322 String peelPart = ""; //$NON-NLS-1$
323 if (derefTags) {
324 if (!ref.isPeeled() && repository != null) {
325 ref = repository.getRefDatabase().peel(ref);
326 }
327 ObjectId peeledObjectId = ref.getPeeledObjectId();
328 if (peeledObjectId != null) {
329 peelPart = " peeled:" + peeledObjectId.getName(); //$NON-NLS-1$
330 }
331 }
332 writeOne(objectId.getName() + " " + ref.getName() + symrefPart //$NON-NLS-1$
333 + peelPart + "\n"); //$NON-NLS-1$
334 continue;
335 }
336
337 advertiseAny(objectId, ref.getName());
338
339 if (!derefTags)
340 continue;
341
342 if (!ref.isPeeled()) {
343 if (repository == null)
344 continue;
345 ref = repository.getRefDatabase().peel(ref);
346 }
347
348 if (ref.getPeeledObjectId() != null)
349 advertiseAny(ref.getPeeledObjectId(), ref.getName() + "^{}"); //$NON-NLS-1$
350 }
351 return sent;
352 }
353
354 /**
355 * Advertise one object is available using the magic {@code .have}.
356 * <p>
357 * The magic {@code .have} advertisement is not available for fetching by a
358 * client, but can be used by a client when considering a delta base
359 * candidate before transferring data in a push. Within the record created
360 * by this method the ref name is simply the invalid string {@code .have}.
361 *
362 * @param id
363 * identity of the object that is assumed to exist.
364 * @throws java.io.IOException
365 * the underlying output stream failed to write out an
366 * advertisement record.
367 */
368 public void advertiseHave(AnyObjectId id) throws IOException {
369 advertiseAnyOnce(id, ".have"); //$NON-NLS-1$
370 }
371
372 /**
373 * Whether no advertisements have been sent yet.
374 *
375 * @return true if no advertisements have been sent yet.
376 */
377 public boolean isEmpty() {
378 return first;
379 }
380
381 private void advertiseAnyOnce(AnyObjectId obj, String refName)
382 throws IOException {
383 if (!sent.contains(obj))
384 advertiseAny(obj, refName);
385 }
386
387 private void advertiseAny(AnyObjectId obj, String refName)
388 throws IOException {
389 sent.add(obj.toObjectId());
390 advertiseId(obj, refName);
391 }
392
393 /**
394 * Advertise one object under a specific name.
395 * <p>
396 * If the advertised object is a tag, this method does not advertise the
397 * peeled version of it.
398 *
399 * @param id
400 * the object to advertise.
401 * @param refName
402 * name of the reference to advertise the object as, can be any
403 * string not including the NUL byte.
404 * @throws java.io.IOException
405 * the underlying output stream failed to write out an
406 * advertisement record.
407 */
408 public void advertiseId(AnyObjectId id, String refName)
409 throws IOException {
410 tmpLine.setLength(0);
411 id.copyTo(tmpId, tmpLine);
412 tmpLine.append(' ');
413 tmpLine.append(refName);
414 if (first) {
415 first = false;
416 if (!capablities.isEmpty()) {
417 tmpLine.append('\0');
418 for (String capName : capablities) {
419 tmpLine.append(' ');
420 tmpLine.append(capName);
421 }
422 tmpLine.append(' ');
423 }
424 }
425 tmpLine.append('\n');
426 writeOne(tmpLine);
427 }
428
429 /**
430 * Write a single advertisement line.
431 *
432 * @param line
433 * the advertisement line to be written. The line always ends
434 * with LF. Never null or the empty string.
435 * @throws java.io.IOException
436 * the underlying output stream failed to write out an
437 * advertisement record.
438 */
439 protected abstract void writeOne(CharSequence line) throws IOException;
440
441 /**
442 * Mark the end of the advertisements.
443 *
444 * @throws java.io.IOException
445 * the underlying output stream failed to write out an
446 * advertisement record.
447 */
448 protected abstract void end() throws IOException;
449 }