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 /**
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(final 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 /**
180 * Initialize this advertiser with a repository for peeling tags.
181 *
182 * @param src
183 * the repository to read from.
184 */
185 public void init(Repository src) {
186 repository = src;
187 }
188
189 /**
190 * Toggle tag peeling.
191 * <p>
192 * <p>
193 * This method must be invoked prior to any of the following:
194 * <ul>
195 * <li>{@link #send(Map)}
196 * </ul>
197 *
198 * @param deref
199 * true to show the dereferenced value of a tag as the special
200 * ref <code>$tag^{}</code> ; false to omit it from the output.
201 */
202 public void setDerefTags(final boolean deref) {
203 derefTags = deref;
204 }
205
206 /**
207 * Add one protocol capability to the initial advertisement.
208 * <p>
209 * This method must be invoked prior to any of the following:
210 * <ul>
211 * <li>{@link #send(Map)}
212 * <li>{@link #advertiseHave(AnyObjectId)}
213 * </ul>
214 *
215 * @param name
216 * the name of a single protocol capability supported by the
217 * caller. The set of capabilities are sent to the client in the
218 * advertisement, allowing the client to later selectively enable
219 * features it recognizes.
220 */
221 public void advertiseCapability(String name) {
222 capablities.add(name);
223 }
224
225 /**
226 * Add one protocol capability with a value ({@code "name=value"}).
227 *
228 * @param name
229 * name of the capability.
230 * @param value
231 * value. If null the capability will not be added.
232 * @since 4.0
233 */
234 public void advertiseCapability(String name, String value) {
235 if (value != null) {
236 capablities.add(name + '=' + value);
237 }
238 }
239
240 /**
241 * Add a symbolic ref to capabilities.
242 * <p>
243 * This method must be invoked prior to any of the following:
244 * <ul>
245 * <li>{@link #send(Map)}
246 * <li>{@link #advertiseHave(AnyObjectId)}
247 * </ul>
248 *
249 * @param from
250 * The symbolic ref, e.g. "HEAD"
251 * @param to
252 * The real ref it points to, e.g. "refs/heads/master"
253 * @since 3.6
254 */
255 public void addSymref(String from, String to) {
256 advertiseCapability(OPTION_SYMREF, from + ':' + to);
257 }
258
259 /**
260 * Format an advertisement for the supplied refs.
261 *
262 * @param refs
263 * zero or more refs to format for the client. The collection is
264 * sorted before display if necessary, and therefore may appear
265 * in any order.
266 * @return set of ObjectIds that were advertised to the client.
267 * @throws java.io.IOException
268 * the underlying output stream failed to write out an
269 * advertisement record.
270 */
271 public Set<ObjectId> send(Map<String, Ref> refs) throws IOException {
272 for (Ref ref : getSortedRefs(refs)) {
273 if (ref.getObjectId() == null)
274 continue;
275
276 advertiseAny(ref.getObjectId(), ref.getName());
277
278 if (!derefTags)
279 continue;
280
281 if (!ref.isPeeled()) {
282 if (repository == null)
283 continue;
284 ref = repository.peel(ref);
285 }
286
287 if (ref.getPeeledObjectId() != null)
288 advertiseAny(ref.getPeeledObjectId(), ref.getName() + "^{}"); //$NON-NLS-1$
289 }
290 return sent;
291 }
292
293 private Iterable<Ref> getSortedRefs(Map<String, Ref> all) {
294 if (all instanceof RefMap
295 || (all instanceof SortedMap && ((SortedMap) all).comparator() == null))
296 return all.values();
297 return RefComparator.sort(all.values());
298 }
299
300 /**
301 * Advertise one object is available using the magic {@code .have}.
302 * <p>
303 * The magic {@code .have} advertisement is not available for fetching by a
304 * client, but can be used by a client when considering a delta base
305 * candidate before transferring data in a push. Within the record created
306 * by this method the ref name is simply the invalid string {@code .have}.
307 *
308 * @param id
309 * identity of the object that is assumed to exist.
310 * @throws java.io.IOException
311 * the underlying output stream failed to write out an
312 * advertisement record.
313 */
314 public void advertiseHave(AnyObjectId id) throws IOException {
315 advertiseAnyOnce(id, ".have"); //$NON-NLS-1$
316 }
317
318 /**
319 * Whether no advertisements have been sent yet.
320 *
321 * @return true if no advertisements have been sent yet.
322 */
323 public boolean isEmpty() {
324 return first;
325 }
326
327 private void advertiseAnyOnce(AnyObjectId obj, final String refName)
328 throws IOException {
329 if (!sent.contains(obj))
330 advertiseAny(obj, refName);
331 }
332
333 private void advertiseAny(AnyObjectId obj, final String refName)
334 throws IOException {
335 sent.add(obj.toObjectId());
336 advertiseId(obj, refName);
337 }
338
339 /**
340 * Advertise one object under a specific name.
341 * <p>
342 * If the advertised object is a tag, this method does not advertise the
343 * peeled version of it.
344 *
345 * @param id
346 * the object to advertise.
347 * @param refName
348 * name of the reference to advertise the object as, can be any
349 * string not including the NUL byte.
350 * @throws java.io.IOException
351 * the underlying output stream failed to write out an
352 * advertisement record.
353 */
354 public void advertiseId(final AnyObjectId id, final String refName)
355 throws IOException {
356 tmpLine.setLength(0);
357 id.copyTo(tmpId, tmpLine);
358 tmpLine.append(' ');
359 tmpLine.append(refName);
360 if (first) {
361 first = false;
362 if (!capablities.isEmpty()) {
363 tmpLine.append('\0');
364 for (final String capName : capablities) {
365 tmpLine.append(' ');
366 tmpLine.append(capName);
367 }
368 tmpLine.append(' ');
369 }
370 }
371 tmpLine.append('\n');
372 writeOne(tmpLine);
373 }
374
375 /**
376 * Write a single advertisement line.
377 *
378 * @param line
379 * the advertisement line to be written. The line always ends
380 * with LF. Never null or the empty string.
381 * @throws java.io.IOException
382 * the underlying output stream failed to write out an
383 * advertisement record.
384 */
385 protected abstract void writeOne(CharSequence line) throws IOException;
386
387 /**
388 * Mark the end of the advertisements.
389 *
390 * @throws java.io.IOException
391 * the underlying output stream failed to write out an
392 * advertisement record.
393 */
394 protected abstract void end() throws IOException;
395 }