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