View Javadoc
1   /*
2    * Copyright (C) 2008, 2010 Google Inc.
3    * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
4    * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
5    * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
6    *
7    * This program and the accompanying materials are made available under the
8    * terms of the Eclipse Distribution License v. 1.0 which is available at
9    * https://www.eclipse.org/org/documents/edl-v10.php.
10   *
11   * SPDX-License-Identifier: BSD-3-Clause
12   */
13  
14  package org.eclipse.jgit.transport;
15  
16  import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_LS_REFS;
17  import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
18  import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_PEELED;
19  import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_SYMREF_TARGET;
20  import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_1;
21  import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2;
22  
23  import java.io.EOFException;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.text.MessageFormat;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.Iterator;
34  import java.util.LinkedHashMap;
35  import java.util.Map;
36  import java.util.Objects;
37  import java.util.Set;
38  
39  import org.eclipse.jgit.annotations.NonNull;
40  import org.eclipse.jgit.errors.InvalidObjectIdException;
41  import org.eclipse.jgit.errors.NoRemoteRepositoryException;
42  import org.eclipse.jgit.errors.PackProtocolException;
43  import org.eclipse.jgit.errors.RemoteRepositoryException;
44  import org.eclipse.jgit.errors.TransportException;
45  import org.eclipse.jgit.internal.JGitText;
46  import org.eclipse.jgit.lib.Constants;
47  import org.eclipse.jgit.lib.ObjectId;
48  import org.eclipse.jgit.lib.ObjectIdRef;
49  import org.eclipse.jgit.lib.Ref;
50  import org.eclipse.jgit.lib.Repository;
51  import org.eclipse.jgit.lib.SymbolicRef;
52  import org.eclipse.jgit.util.StringUtils;
53  import org.eclipse.jgit.util.io.InterruptTimer;
54  import org.eclipse.jgit.util.io.TimeoutInputStream;
55  import org.eclipse.jgit.util.io.TimeoutOutputStream;
56  
57  /**
58   * Base helper class for pack-based operations implementations. Provides partial
59   * implementation of pack-protocol - refs advertising and capabilities support,
60   * and some other helper methods.
61   *
62   * @see BasePackFetchConnection
63   * @see BasePackPushConnection
64   */
65  abstract class BasePackConnection extends BaseConnection {
66  
67  	protected static final String CAPABILITY_SYMREF_PREFIX = "symref="; //$NON-NLS-1$
68  
69  	/** The repository this transport fetches into, or pushes out of. */
70  	protected final Repository local;
71  
72  	/** Remote repository location. */
73  	protected final URIish uri;
74  
75  	/** A transport connected to {@link #uri}. */
76  	protected final Transport transport;
77  
78  	/** Low-level input stream, if a timeout was configured. */
79  	protected TimeoutInputStream timeoutIn;
80  
81  	/** Low-level output stream, if a timeout was configured. */
82  	protected TimeoutOutputStream timeoutOut;
83  
84  	/** Timer to manage {@link #timeoutIn} and {@link #timeoutOut}. */
85  	private InterruptTimer myTimer;
86  
87  	/** Input stream reading from the remote. */
88  	protected InputStream in;
89  
90  	/** Output stream sending to the remote. */
91  	protected OutputStream out;
92  
93  	/** Packet line decoder around {@link #in}. */
94  	protected PacketLineIn pckIn;
95  
96  	/** Packet line encoder around {@link #out}. */
97  	protected PacketLineOut pckOut;
98  
99  	/** Send {@link PacketLineOut#end()} before closing {@link #out}? */
100 	protected boolean outNeedsEnd;
101 
102 	/** True if this is a stateless RPC connection. */
103 	protected boolean statelessRPC;
104 
105 	/** Capability tokens advertised by the remote side. */
106 	private final Map<String, String> remoteCapabilities = new HashMap<>();
107 
108 	/** Extra objects the remote has, but which aren't offered as refs. */
109 	protected final Set<ObjectId> additionalHaves = new HashSet<>();
110 
111 	private TransferConfig.ProtocolVersion protocol = TransferConfig.ProtocolVersion.V0;
112 
113 	BasePackConnection(PackTransport packTransport) {
114 		transport = (Transport) packTransport;
115 		local = transport.local;
116 		uri = transport.uri;
117 	}
118 
119 	TransferConfig.ProtocolVersion getProtocolVersion() {
120 		return protocol;
121 	}
122 
123 	void setProtocolVersion(@NonNull TransferConfig.ProtocolVersion protocol) {
124 		this.protocol = protocol;
125 	}
126 
127 	/**
128 	 * Configure this connection with the directional pipes.
129 	 *
130 	 * @param myIn
131 	 *            input stream to receive data from the peer. Caller must ensure
132 	 *            the input is buffered, otherwise read performance may suffer.
133 	 * @param myOut
134 	 *            output stream to transmit data to the peer. Caller must ensure
135 	 *            the output is buffered, otherwise write performance may
136 	 *            suffer.
137 	 */
138 	protected final void init(InputStream myIn, OutputStream myOut) {
139 		final int timeout = transport.getTimeout();
140 		if (timeout > 0) {
141 			final Thread caller = Thread.currentThread();
142 			if (myTimer == null) {
143 				myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$
144 			}
145 			timeoutIn = new TimeoutInputStream(myIn, myTimer);
146 			timeoutOut = new TimeoutOutputStream(myOut, myTimer);
147 			timeoutIn.setTimeout(timeout * 1000);
148 			timeoutOut.setTimeout(timeout * 1000);
149 			myIn = timeoutIn;
150 			myOut = timeoutOut;
151 		}
152 
153 		in = myIn;
154 		out = myOut;
155 
156 		pckIn = new PacketLineIn(in);
157 		pckOut = new PacketLineOut(out);
158 		outNeedsEnd = true;
159 	}
160 
161 	/**
162 	 * Reads the advertised references through the initialized stream.
163 	 * <p>
164 	 * Subclass implementations may call this method only after setting up the
165 	 * input and output streams with {@link #init(InputStream, OutputStream)}.
166 	 * <p>
167 	 * If any errors occur, this connection is automatically closed by invoking
168 	 * {@link #close()} and the exception is wrapped (if necessary) and thrown
169 	 * as a {@link org.eclipse.jgit.errors.TransportException}.
170 	 *
171 	 * @return {@code true} if the refs were read; {@code false} otherwise
172 	 *         indicating that {@link #lsRefs} must be called
173 	 *
174 	 * @throws org.eclipse.jgit.errors.TransportException
175 	 *             the reference list could not be scanned.
176 	 */
177 	protected boolean readAdvertisedRefs() throws TransportException {
178 		try {
179 			return readAdvertisedRefsImpl();
180 		} catch (TransportException err) {
181 			close();
182 			throw err;
183 		} catch (IOException | RuntimeException err) {
184 			close();
185 			throw new TransportException(err.getMessage(), err);
186 		}
187 	}
188 
189 	private String readLine() throws IOException {
190 		String line = pckIn.readString();
191 		if (PacketLineIn.isEnd(line)) {
192 			return null;
193 		}
194 		if (line.startsWith("ERR ")) { //$NON-NLS-1$
195 			// This is a customized remote service error.
196 			// Users should be informed about it.
197 			throw new RemoteRepositoryException(uri, line.substring(4));
198 		}
199 		return line;
200 	}
201 
202 	private boolean readAdvertisedRefsImpl() throws IOException {
203 		final Map<String, Ref> avail = new LinkedHashMap<>();
204 		final Map<String, String> symRefs = new LinkedHashMap<>();
205 		for (boolean first = true;; first = false) {
206 			String line;
207 
208 			if (first) {
209 				boolean isV1 = false;
210 				try {
211 					line = readLine();
212 				} catch (EOFException e) {
213 					TransportException noRepo = noRepository();
214 					noRepo.initCause(e);
215 					throw noRepo;
216 				}
217 				if (line != null && VERSION_1.equals(line)) {
218 					// Same as V0, except for this extra line. We shouldn't get
219 					// it since we never request V1.
220 					setProtocolVersion(TransferConfig.ProtocolVersion.V0);
221 					isV1 = true;
222 					line = readLine();
223 				}
224 				if (line == null) {
225 					break;
226 				}
227 				final int nul = line.indexOf('\0');
228 				if (nul >= 0) {
229 					// Protocol V0: The first line (if any) may contain
230 					// "hidden" capability values after a NUL byte.
231 					for (String capability : line.substring(nul + 1)
232 							.split(" ")) { //$NON-NLS-1$
233 						if (capability.startsWith(CAPABILITY_SYMREF_PREFIX)) {
234 							String[] parts = capability
235 									.substring(
236 											CAPABILITY_SYMREF_PREFIX.length())
237 									.split(":", 2); //$NON-NLS-1$
238 							if (parts.length == 2) {
239 								symRefs.put(parts[0], parts[1]);
240 							}
241 						} else {
242 							addCapability(capability);
243 						}
244 					}
245 					line = line.substring(0, nul);
246 					setProtocolVersion(TransferConfig.ProtocolVersion.V0);
247 				} else if (!isV1 && VERSION_2.equals(line)) {
248 					// Protocol V2: remaining lines are capabilities as
249 					// key=value pairs
250 					setProtocolVersion(TransferConfig.ProtocolVersion.V2);
251 					readCapabilitiesV2();
252 					// Break out here so that stateless RPC transports get a
253 					// chance to set up the output stream.
254 					return false;
255 				} else {
256 					setProtocolVersion(TransferConfig.ProtocolVersion.V0);
257 				}
258 			} else {
259 				line = readLine();
260 				if (line == null) {
261 					break;
262 				}
263 			}
264 
265 			// Expecting to get a line in the form "sha1 refname"
266 			if (line.length() < 41 || line.charAt(40) != ' ') {
267 				throw invalidRefAdvertisementLine(line);
268 			}
269 			String name = line.substring(41, line.length());
270 			if (first && name.equals("capabilities^{}")) { //$NON-NLS-1$
271 				// special line from git-receive-pack (protocol V0) to show
272 				// capabilities when there are no refs to advertise
273 				continue;
274 			}
275 
276 			final ObjectId id = toId(line, line.substring(0, 40));
277 			if (name.equals(".have")) { //$NON-NLS-1$
278 				additionalHaves.add(id);
279 			} else {
280 				processLineV1(name, id, avail);
281 			}
282 		}
283 		updateWithSymRefs(avail, symRefs);
284 		available(avail);
285 		return true;
286 	}
287 
288 	/**
289 	 * Issue a protocol V2 ls-refs command and read its response.
290 	 *
291 	 * @param refSpecs
292 	 *            to produce ref prefixes from if the server supports git
293 	 *            protocol V2
294 	 * @param additionalPatterns
295 	 *            to use for ref prefixes if the server supports git protocol V2
296 	 * @throws TransportException
297 	 *             if the command could not be run or its output not be read
298 	 */
299 	protected void lsRefs(Collection<RefSpec> refSpecs,
300 			String... additionalPatterns) throws TransportException {
301 		try {
302 			lsRefsImpl(refSpecs, additionalPatterns);
303 		} catch (TransportException err) {
304 			close();
305 			throw err;
306 		} catch (IOException | RuntimeException err) {
307 			close();
308 			throw new TransportException(err.getMessage(), err);
309 		}
310 	}
311 
312 	private void lsRefsImpl(Collection<RefSpec> refSpecs,
313 			String... additionalPatterns) throws IOException {
314 		pckOut.writeString("command=" + COMMAND_LS_REFS); //$NON-NLS-1$
315 		// Add the user-agent
316 		String agent = UserAgent.get();
317 		if (agent != null && isCapableOf(OPTION_AGENT)) {
318 			pckOut.writeString(OPTION_AGENT + '=' + agent);
319 		}
320 		pckOut.writeDelim();
321 		pckOut.writeString("peel"); //$NON-NLS-1$
322 		pckOut.writeString("symrefs"); //$NON-NLS-1$
323 		for (String refPrefix : getRefPrefixes(refSpecs, additionalPatterns)) {
324 			pckOut.writeString("ref-prefix " + refPrefix); //$NON-NLS-1$
325 		}
326 		pckOut.end();
327 		final Map<String, Ref> avail = new LinkedHashMap<>();
328 		final Map<String, String> symRefs = new LinkedHashMap<>();
329 		for (;;) {
330 			String line = readLine();
331 			if (line == null) {
332 				break;
333 			}
334 			// Expecting to get a line in the form "sha1 refname"
335 			if (line.length() < 41 || line.charAt(40) != ' ') {
336 				throw invalidRefAdvertisementLine(line);
337 			}
338 			String name = line.substring(41, line.length());
339 			final ObjectId id = toId(line, line.substring(0, 40));
340 			if (name.equals(".have")) { //$NON-NLS-1$
341 				additionalHaves.add(id);
342 			} else {
343 				processLineV2(line, id, name, avail, symRefs);
344 			}
345 		}
346 		updateWithSymRefs(avail, symRefs);
347 		available(avail);
348 	}
349 
350 	private Collection<String> getRefPrefixes(Collection<RefSpec> refSpecs,
351 			String... additionalPatterns) {
352 		if (refSpecs.isEmpty() && (additionalPatterns == null
353 				|| additionalPatterns.length == 0)) {
354 			return Collections.emptyList();
355 		}
356 		Set<String> patterns = new HashSet<>();
357 		if (additionalPatterns != null) {
358 			Arrays.stream(additionalPatterns).filter(Objects::nonNull)
359 					.forEach(patterns::add);
360 		}
361 		for (RefSpec spec : refSpecs) {
362 			// TODO: for now we only do protocol V2 for fetch. For push
363 			// RefSpecs, the logic would need to be different. (At the
364 			// minimum, take spec.getDestination().)
365 			String src = spec.getSource();
366 			if (ObjectId.isId(src)) {
367 				continue;
368 			}
369 			if (spec.isWildcard()) {
370 				patterns.add(src.substring(0, src.indexOf('*')));
371 			} else {
372 				patterns.add(src);
373 				patterns.add(Constants.R_REFS + src);
374 				patterns.add(Constants.R_HEADS + src);
375 				patterns.add(Constants.R_TAGS + src);
376 			}
377 		}
378 		return patterns;
379 	}
380 
381 	private void readCapabilitiesV2() throws IOException {
382 		// In git protocol V2, capabilities are different. If it's a key-value
383 		// pair, the key may be a command name, and the value a space-separated
384 		// list of capabilities for that command. We still store it in the same
385 		// map as for protocol v0/v1. Protocol v2 code has to account for this.
386 		for (;;) {
387 			String line = readLine();
388 			if (line == null) {
389 				break;
390 			}
391 			addCapability(line);
392 		}
393 	}
394 
395 	private void addCapability(String capability) {
396 		String parts[] = capability.split("=", 2); //$NON-NLS-1$
397 		if (parts.length == 2) {
398 			remoteCapabilities.put(parts[0], parts[1]);
399 		}
400 		remoteCapabilities.put(capability, null);
401 	}
402 
403 	private ObjectId toId(String line, String value)
404 			throws PackProtocolException {
405 		try {
406 			return ObjectId.fromString(value);
407 		} catch (InvalidObjectIdException e) {
408 			PackProtocolException ppe = invalidRefAdvertisementLine(line);
409 			ppe.initCause(e);
410 			throw ppe;
411 		}
412 	}
413 
414 	private void processLineV1(String name, ObjectId id, Map<String, Ref> avail)
415 			throws IOException {
416 		if (name.endsWith("^{}")) { //$NON-NLS-1$
417 			name = name.substring(0, name.length() - 3);
418 			final Ref prior = avail.get(name);
419 			if (prior == null) {
420 				throw new PackProtocolException(uri, MessageFormat.format(
421 						JGitText.get().advertisementCameBefore, name, name));
422 			}
423 			if (prior.getPeeledObjectId() != null) {
424 				throw duplicateAdvertisement(name + "^{}"); //$NON-NLS-1$
425 			}
426 			avail.put(name, new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name,
427 					prior.getObjectId(), id));
428 		} else {
429 			final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag(
430 					Ref.Storage.NETWORK, name, id));
431 			if (prior != null) {
432 				throw duplicateAdvertisement(name);
433 			}
434 		}
435 	}
436 
437 	private void processLineV2(String line, ObjectId id, String rest,
438 			Map<String, Ref> avail, Map<String, String> symRefs)
439 			throws IOException {
440 		String[] parts = rest.split(" "); //$NON-NLS-1$
441 		String name = parts[0];
442 		// Two attributes possible, symref-target or peeled
443 		String symRefTarget = null;
444 		String peeled = null;
445 		for (int i = 1; i < parts.length; i++) {
446 			if (parts[i].startsWith(REF_ATTR_SYMREF_TARGET)) {
447 				if (symRefTarget != null) {
448 					throw new PackProtocolException(uri, MessageFormat.format(
449 							JGitText.get().duplicateRefAttribute, line));
450 				}
451 				symRefTarget = parts[i]
452 						.substring(REF_ATTR_SYMREF_TARGET.length());
453 			} else if (parts[i].startsWith(REF_ATTR_PEELED)) {
454 				if (peeled != null) {
455 					throw new PackProtocolException(uri, MessageFormat.format(
456 							JGitText.get().duplicateRefAttribute, line));
457 				}
458 				peeled = parts[i].substring(REF_ATTR_PEELED.length());
459 			}
460 			if (peeled != null && symRefTarget != null) {
461 				break;
462 			}
463 		}
464 		Ref idRef;
465 		if (peeled != null) {
466 			idRef = new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name, id,
467 					toId(line, peeled));
468 		} else {
469 			idRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK, name, id);
470 		}
471 		Ref prior = avail.put(name, idRef);
472 		if (prior != null) {
473 			throw duplicateAdvertisement(name);
474 		}
475 		if (!StringUtils.isEmptyOrNull(symRefTarget)) {
476 			symRefs.put(name, symRefTarget);
477 		}
478 	}
479 
480 	/**
481 	 * Updates the given refMap with {@link SymbolicRef}s defined by the given
482 	 * symRefs.
483 	 * <p>
484 	 * For each entry, symRef, in symRefs, whose value is a key in refMap, adds
485 	 * a new entry to refMap with that same key and value of a new
486 	 * {@link SymbolicRef} with source=symRef.key and
487 	 * target=refMap.get(symRef.value), then removes that entry from symRefs.
488 	 * <p>
489 	 * If refMap already contains an entry for symRef.key, it is replaced.
490 	 * </p>
491 	 * </p>
492 	 * <p>
493 	 * For example, given:
494 	 * </p>
495 	 *
496 	 * <pre>
497 	 * refMap.put("refs/heads/main", ref);
498 	 * symRefs.put("HEAD", "refs/heads/main");
499 	 * </pre>
500 	 *
501 	 * then:
502 	 *
503 	 * <pre>
504 	 * updateWithSymRefs(refMap, symRefs);
505 	 * </pre>
506 	 *
507 	 * has the <em>effect</em> of:
508 	 *
509 	 * <pre>
510 	 * refMap.put("HEAD",
511 	 * 		new SymbolicRef("HEAD", refMap.get(symRefs.remove("HEAD"))))
512 	 * </pre>
513 	 * <p>
514 	 * Any entry in symRefs whose value is not a key in refMap is ignored. Any
515 	 * circular symRefs are ignored.
516 	 * </p>
517 	 * <p>
518 	 * Upon completion, symRefs will contain only any unresolvable entries.
519 	 * </p>
520 	 *
521 	 * @param refMap
522 	 *            a non-null, modifiable, Map to update, and the provider of
523 	 *            symref targets.
524 	 * @param symRefs
525 	 *            a non-null, modifiable, Map of symrefs.
526 	 * @throws NullPointerException
527 	 *             if refMap or symRefs is null
528 	 */
529 	static void updateWithSymRefs(Map<String, Ref> refMap, Map<String, String> symRefs) {
530 		boolean haveNewRefMapEntries = !refMap.isEmpty();
531 		while (!symRefs.isEmpty() && haveNewRefMapEntries) {
532 			haveNewRefMapEntries = false;
533 			final Iterator<Map.Entry<String, String>> iterator = symRefs.entrySet().iterator();
534 			while (iterator.hasNext()) {
535 				final Map.Entry<String, String> symRef = iterator.next();
536 				if (!symRefs.containsKey(symRef.getValue())) { // defer forward reference
537 					final Ref r = refMap.get(symRef.getValue());
538 					if (r != null) {
539 						refMap.put(symRef.getKey(), new SymbolicRef(symRef.getKey(), r));
540 						haveNewRefMapEntries = true;
541 						iterator.remove();
542 					}
543 				}
544 			}
545 		}
546 		// If HEAD is still in the symRefs map here, the real ref was not
547 		// reported, but we know it must point to the object reported for HEAD.
548 		// So fill it in in the refMap.
549 		String headRefName = symRefs.get(Constants.HEAD);
550 		if (headRefName != null && !refMap.containsKey(headRefName)) {
551 			Ref headRef = refMap.get(Constants.HEAD);
552 			if (headRef != null) {
553 				ObjectId headObj = headRef.getObjectId();
554 				headRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK,
555 						headRefName, headObj);
556 				refMap.put(headRefName, headRef);
557 				headRef = new SymbolicRef(Constants.HEAD, headRef);
558 				refMap.put(Constants.HEAD, headRef);
559 				symRefs.remove(Constants.HEAD);
560 			}
561 		}
562 	}
563 
564 	/**
565 	 * Create an exception to indicate problems finding a remote repository. The
566 	 * caller is expected to throw the returned exception.
567 	 *
568 	 * Subclasses may override this method to provide better diagnostics.
569 	 *
570 	 * @return a TransportException saying a repository cannot be found and
571 	 *         possibly why.
572 	 */
573 	protected TransportException noRepository() {
574 		return new NoRemoteRepositoryException(uri, JGitText.get().notFound);
575 	}
576 
577 	/**
578 	 * Whether this option is supported
579 	 *
580 	 * @param option
581 	 *            option string
582 	 * @return whether this option is supported
583 	 */
584 	protected boolean isCapableOf(String option) {
585 		return remoteCapabilities.containsKey(option);
586 	}
587 
588 	/**
589 	 * Request capability
590 	 *
591 	 * @param b
592 	 *            buffer
593 	 * @param option
594 	 *            option we want
595 	 * @return {@code true} if the requested option is supported
596 	 */
597 	protected boolean wantCapability(StringBuilder b, String option) {
598 		if (!isCapableOf(option))
599 			return false;
600 		b.append(' ');
601 		b.append(option);
602 		return true;
603 	}
604 
605 	/**
606 	 * Return a capability value.
607 	 *
608 	 * @param option
609 	 *            to get
610 	 * @return the value stored, if any.
611 	 */
612 	protected String getCapability(String option) {
613 		return remoteCapabilities.get(option);
614 	}
615 
616 	/**
617 	 * Add user agent capability
618 	 *
619 	 * @param b
620 	 *            a {@link java.lang.StringBuilder} object.
621 	 */
622 	protected void addUserAgentCapability(StringBuilder b) {
623 		String a = UserAgent.get();
624 		if (a != null && remoteCapabilities.get(OPTION_AGENT) != null) {
625 			b.append(' ').append(OPTION_AGENT).append('=').append(a);
626 		}
627 	}
628 
629 	/** {@inheritDoc} */
630 	@Override
631 	public String getPeerUserAgent() {
632 		String agent = remoteCapabilities.get(OPTION_AGENT);
633 		return agent != null ? agent : super.getPeerUserAgent();
634 	}
635 
636 	private PackProtocolException duplicateAdvertisement(String name) {
637 		return new PackProtocolException(uri, MessageFormat.format(JGitText.get().duplicateAdvertisementsOf, name));
638 	}
639 
640 	private PackProtocolException invalidRefAdvertisementLine(String line) {
641 		return new PackProtocolException(uri, MessageFormat.format(JGitText.get().invalidRefAdvertisementLine, line));
642 	}
643 
644 	/** {@inheritDoc} */
645 	@Override
646 	public void close() {
647 		if (out != null) {
648 			try {
649 				if (outNeedsEnd) {
650 					outNeedsEnd = false;
651 					pckOut.end();
652 				}
653 				out.close();
654 			} catch (IOException err) {
655 				// Ignore any close errors.
656 			} finally {
657 				out = null;
658 				pckOut = null;
659 			}
660 		}
661 
662 		if (in != null) {
663 			try {
664 				in.close();
665 			} catch (IOException err) {
666 				// Ignore any close errors.
667 			} finally {
668 				in = null;
669 				pckIn = null;
670 			}
671 		}
672 
673 		if (myTimer != null) {
674 			try {
675 				myTimer.terminate();
676 			} finally {
677 				myTimer = null;
678 				timeoutIn = null;
679 				timeoutOut = null;
680 			}
681 		}
682 	}
683 
684 	/**
685 	 * Tell the peer we are disconnecting, if it cares to know.
686 	 */
687 	protected void endOut() {
688 		if (outNeedsEnd && out != null) {
689 			try {
690 				outNeedsEnd = false;
691 				pckOut.end();
692 			} catch (IOException e) {
693 				try {
694 					out.close();
695 				} catch (IOException err) {
696 					// Ignore any close errors.
697 				} finally {
698 					out = null;
699 					pckOut = null;
700 				}
701 			}
702 		}
703 	}
704 }