BasePackConnection.java

  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. package org.eclipse.jgit.transport;

  14. import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_LS_REFS;
  15. import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
  16. import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_PEELED;
  17. import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_SYMREF_TARGET;
  18. import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_1;
  19. import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2;

  20. import java.io.EOFException;
  21. import java.io.IOException;
  22. import java.io.InputStream;
  23. import java.io.OutputStream;
  24. import java.text.MessageFormat;
  25. import java.util.Arrays;
  26. import java.util.Collection;
  27. import java.util.Collections;
  28. import java.util.HashMap;
  29. import java.util.HashSet;
  30. import java.util.Iterator;
  31. import java.util.LinkedHashMap;
  32. import java.util.Map;
  33. import java.util.Objects;
  34. import java.util.Set;

  35. import org.eclipse.jgit.annotations.NonNull;
  36. import org.eclipse.jgit.errors.InvalidObjectIdException;
  37. import org.eclipse.jgit.errors.NoRemoteRepositoryException;
  38. import org.eclipse.jgit.errors.PackProtocolException;
  39. import org.eclipse.jgit.errors.RemoteRepositoryException;
  40. import org.eclipse.jgit.errors.TransportException;
  41. import org.eclipse.jgit.internal.JGitText;
  42. import org.eclipse.jgit.lib.Constants;
  43. import org.eclipse.jgit.lib.ObjectId;
  44. import org.eclipse.jgit.lib.ObjectIdRef;
  45. import org.eclipse.jgit.lib.Ref;
  46. import org.eclipse.jgit.lib.Repository;
  47. import org.eclipse.jgit.lib.SymbolicRef;
  48. import org.eclipse.jgit.util.StringUtils;
  49. import org.eclipse.jgit.util.io.InterruptTimer;
  50. import org.eclipse.jgit.util.io.TimeoutInputStream;
  51. import org.eclipse.jgit.util.io.TimeoutOutputStream;

  52. /**
  53.  * Base helper class for pack-based operations implementations. Provides partial
  54.  * implementation of pack-protocol - refs advertising and capabilities support,
  55.  * and some other helper methods.
  56.  *
  57.  * @see BasePackFetchConnection
  58.  * @see BasePackPushConnection
  59.  */
  60. abstract class BasePackConnection extends BaseConnection {

  61.     protected static final String CAPABILITY_SYMREF_PREFIX = "symref="; //$NON-NLS-1$

  62.     /** The repository this transport fetches into, or pushes out of. */
  63.     protected final Repository local;

  64.     /** Remote repository location. */
  65.     protected final URIish uri;

  66.     /** A transport connected to {@link #uri}. */
  67.     protected final Transport transport;

  68.     /** Low-level input stream, if a timeout was configured. */
  69.     protected TimeoutInputStream timeoutIn;

  70.     /** Low-level output stream, if a timeout was configured. */
  71.     protected TimeoutOutputStream timeoutOut;

  72.     /** Timer to manage {@link #timeoutIn} and {@link #timeoutOut}. */
  73.     private InterruptTimer myTimer;

  74.     /** Input stream reading from the remote. */
  75.     protected InputStream in;

  76.     /** Output stream sending to the remote. */
  77.     protected OutputStream out;

  78.     /** Packet line decoder around {@link #in}. */
  79.     protected PacketLineIn pckIn;

  80.     /** Packet line encoder around {@link #out}. */
  81.     protected PacketLineOut pckOut;

  82.     /** Send {@link PacketLineOut#end()} before closing {@link #out}? */
  83.     protected boolean outNeedsEnd;

  84.     /** True if this is a stateless RPC connection. */
  85.     protected boolean statelessRPC;

  86.     /** Capability tokens advertised by the remote side. */
  87.     private final Map<String, String> remoteCapabilities = new HashMap<>();

  88.     /** Extra objects the remote has, but which aren't offered as refs. */
  89.     protected final Set<ObjectId> additionalHaves = new HashSet<>();

  90.     private TransferConfig.ProtocolVersion protocol = TransferConfig.ProtocolVersion.V0;

  91.     BasePackConnection(PackTransport packTransport) {
  92.         transport = (Transport) packTransport;
  93.         local = transport.local;
  94.         uri = transport.uri;
  95.     }

  96.     TransferConfig.ProtocolVersion getProtocolVersion() {
  97.         return protocol;
  98.     }

  99.     void setProtocolVersion(@NonNull TransferConfig.ProtocolVersion protocol) {
  100.         this.protocol = protocol;
  101.     }

  102.     /**
  103.      * Configure this connection with the directional pipes.
  104.      *
  105.      * @param myIn
  106.      *            input stream to receive data from the peer. Caller must ensure
  107.      *            the input is buffered, otherwise read performance may suffer.
  108.      * @param myOut
  109.      *            output stream to transmit data to the peer. Caller must ensure
  110.      *            the output is buffered, otherwise write performance may
  111.      *            suffer.
  112.      */
  113.     protected final void init(InputStream myIn, OutputStream myOut) {
  114.         final int timeout = transport.getTimeout();
  115.         if (timeout > 0) {
  116.             final Thread caller = Thread.currentThread();
  117.             if (myTimer == null) {
  118.                 myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$
  119.             }
  120.             timeoutIn = new TimeoutInputStream(myIn, myTimer);
  121.             timeoutOut = new TimeoutOutputStream(myOut, myTimer);
  122.             timeoutIn.setTimeout(timeout * 1000);
  123.             timeoutOut.setTimeout(timeout * 1000);
  124.             myIn = timeoutIn;
  125.             myOut = timeoutOut;
  126.         }

  127.         in = myIn;
  128.         out = myOut;

  129.         pckIn = new PacketLineIn(in);
  130.         pckOut = new PacketLineOut(out);
  131.         outNeedsEnd = true;
  132.     }

  133.     /**
  134.      * Reads the advertised references through the initialized stream.
  135.      * <p>
  136.      * Subclass implementations may call this method only after setting up the
  137.      * input and output streams with {@link #init(InputStream, OutputStream)}.
  138.      * <p>
  139.      * If any errors occur, this connection is automatically closed by invoking
  140.      * {@link #close()} and the exception is wrapped (if necessary) and thrown
  141.      * as a {@link org.eclipse.jgit.errors.TransportException}.
  142.      *
  143.      * @return {@code true} if the refs were read; {@code false} otherwise
  144.      *         indicating that {@link #lsRefs} must be called
  145.      *
  146.      * @throws org.eclipse.jgit.errors.TransportException
  147.      *             the reference list could not be scanned.
  148.      */
  149.     protected boolean readAdvertisedRefs() throws TransportException {
  150.         try {
  151.             return readAdvertisedRefsImpl();
  152.         } catch (TransportException err) {
  153.             close();
  154.             throw err;
  155.         } catch (IOException | RuntimeException err) {
  156.             close();
  157.             throw new TransportException(err.getMessage(), err);
  158.         }
  159.     }

  160.     private String readLine() throws IOException {
  161.         String line = pckIn.readString();
  162.         if (PacketLineIn.isEnd(line)) {
  163.             return null;
  164.         }
  165.         if (line.startsWith("ERR ")) { //$NON-NLS-1$
  166.             // This is a customized remote service error.
  167.             // Users should be informed about it.
  168.             throw new RemoteRepositoryException(uri, line.substring(4));
  169.         }
  170.         return line;
  171.     }

  172.     private boolean readAdvertisedRefsImpl() throws IOException {
  173.         final Map<String, Ref> avail = new LinkedHashMap<>();
  174.         final Map<String, String> symRefs = new LinkedHashMap<>();
  175.         for (boolean first = true;; first = false) {
  176.             String line;

  177.             if (first) {
  178.                 boolean isV1 = false;
  179.                 try {
  180.                     line = readLine();
  181.                 } catch (EOFException e) {
  182.                     TransportException noRepo = noRepository();
  183.                     noRepo.initCause(e);
  184.                     throw noRepo;
  185.                 }
  186.                 if (line != null && VERSION_1.equals(line)) {
  187.                     // Same as V0, except for this extra line. We shouldn't get
  188.                     // it since we never request V1.
  189.                     setProtocolVersion(TransferConfig.ProtocolVersion.V0);
  190.                     isV1 = true;
  191.                     line = readLine();
  192.                 }
  193.                 if (line == null) {
  194.                     break;
  195.                 }
  196.                 final int nul = line.indexOf('\0');
  197.                 if (nul >= 0) {
  198.                     // Protocol V0: The first line (if any) may contain
  199.                     // "hidden" capability values after a NUL byte.
  200.                     for (String capability : line.substring(nul + 1)
  201.                             .split(" ")) { //$NON-NLS-1$
  202.                         if (capability.startsWith(CAPABILITY_SYMREF_PREFIX)) {
  203.                             String[] parts = capability
  204.                                     .substring(
  205.                                             CAPABILITY_SYMREF_PREFIX.length())
  206.                                     .split(":", 2); //$NON-NLS-1$
  207.                             if (parts.length == 2) {
  208.                                 symRefs.put(parts[0], parts[1]);
  209.                             }
  210.                         } else {
  211.                             addCapability(capability);
  212.                         }
  213.                     }
  214.                     line = line.substring(0, nul);
  215.                     setProtocolVersion(TransferConfig.ProtocolVersion.V0);
  216.                 } else if (!isV1 && VERSION_2.equals(line)) {
  217.                     // Protocol V2: remaining lines are capabilities as
  218.                     // key=value pairs
  219.                     setProtocolVersion(TransferConfig.ProtocolVersion.V2);
  220.                     readCapabilitiesV2();
  221.                     // Break out here so that stateless RPC transports get a
  222.                     // chance to set up the output stream.
  223.                     return false;
  224.                 } else {
  225.                     setProtocolVersion(TransferConfig.ProtocolVersion.V0);
  226.                 }
  227.             } else {
  228.                 line = readLine();
  229.                 if (line == null) {
  230.                     break;
  231.                 }
  232.             }

  233.             // Expecting to get a line in the form "sha1 refname"
  234.             if (line.length() < 41 || line.charAt(40) != ' ') {
  235.                 throw invalidRefAdvertisementLine(line);
  236.             }
  237.             String name = line.substring(41, line.length());
  238.             if (first && name.equals("capabilities^{}")) { //$NON-NLS-1$
  239.                 // special line from git-receive-pack (protocol V0) to show
  240.                 // capabilities when there are no refs to advertise
  241.                 continue;
  242.             }

  243.             final ObjectId id = toId(line, line.substring(0, 40));
  244.             if (name.equals(".have")) { //$NON-NLS-1$
  245.                 additionalHaves.add(id);
  246.             } else {
  247.                 processLineV1(name, id, avail);
  248.             }
  249.         }
  250.         updateWithSymRefs(avail, symRefs);
  251.         available(avail);
  252.         return true;
  253.     }

  254.     /**
  255.      * Issue a protocol V2 ls-refs command and read its response.
  256.      *
  257.      * @param refSpecs
  258.      *            to produce ref prefixes from if the server supports git
  259.      *            protocol V2
  260.      * @param additionalPatterns
  261.      *            to use for ref prefixes if the server supports git protocol V2
  262.      * @throws TransportException
  263.      *             if the command could not be run or its output not be read
  264.      */
  265.     protected void lsRefs(Collection<RefSpec> refSpecs,
  266.             String... additionalPatterns) throws TransportException {
  267.         try {
  268.             lsRefsImpl(refSpecs, additionalPatterns);
  269.         } catch (TransportException err) {
  270.             close();
  271.             throw err;
  272.         } catch (IOException | RuntimeException err) {
  273.             close();
  274.             throw new TransportException(err.getMessage(), err);
  275.         }
  276.     }

  277.     private void lsRefsImpl(Collection<RefSpec> refSpecs,
  278.             String... additionalPatterns) throws IOException {
  279.         pckOut.writeString("command=" + COMMAND_LS_REFS); //$NON-NLS-1$
  280.         // Add the user-agent
  281.         String agent = UserAgent.get();
  282.         if (agent != null && isCapableOf(OPTION_AGENT)) {
  283.             pckOut.writeString(OPTION_AGENT + '=' + agent);
  284.         }
  285.         pckOut.writeDelim();
  286.         pckOut.writeString("peel"); //$NON-NLS-1$
  287.         pckOut.writeString("symrefs"); //$NON-NLS-1$
  288.         for (String refPrefix : getRefPrefixes(refSpecs, additionalPatterns)) {
  289.             pckOut.writeString("ref-prefix " + refPrefix); //$NON-NLS-1$
  290.         }
  291.         pckOut.end();
  292.         final Map<String, Ref> avail = new LinkedHashMap<>();
  293.         final Map<String, String> symRefs = new LinkedHashMap<>();
  294.         for (;;) {
  295.             String line = readLine();
  296.             if (line == null) {
  297.                 break;
  298.             }
  299.             // Expecting to get a line in the form "sha1 refname"
  300.             if (line.length() < 41 || line.charAt(40) != ' ') {
  301.                 throw invalidRefAdvertisementLine(line);
  302.             }
  303.             String name = line.substring(41, line.length());
  304.             final ObjectId id = toId(line, line.substring(0, 40));
  305.             if (name.equals(".have")) { //$NON-NLS-1$
  306.                 additionalHaves.add(id);
  307.             } else {
  308.                 processLineV2(line, id, name, avail, symRefs);
  309.             }
  310.         }
  311.         updateWithSymRefs(avail, symRefs);
  312.         available(avail);
  313.     }

  314.     private Collection<String> getRefPrefixes(Collection<RefSpec> refSpecs,
  315.             String... additionalPatterns) {
  316.         if (refSpecs.isEmpty() && (additionalPatterns == null
  317.                 || additionalPatterns.length == 0)) {
  318.             return Collections.emptyList();
  319.         }
  320.         Set<String> patterns = new HashSet<>();
  321.         if (additionalPatterns != null) {
  322.             Arrays.stream(additionalPatterns).filter(Objects::nonNull)
  323.                     .forEach(patterns::add);
  324.         }
  325.         for (RefSpec spec : refSpecs) {
  326.             // TODO: for now we only do protocol V2 for fetch. For push
  327.             // RefSpecs, the logic would need to be different. (At the
  328.             // minimum, take spec.getDestination().)
  329.             String src = spec.getSource();
  330.             if (ObjectId.isId(src)) {
  331.                 continue;
  332.             }
  333.             if (spec.isWildcard()) {
  334.                 patterns.add(src.substring(0, src.indexOf('*')));
  335.             } else {
  336.                 patterns.add(src);
  337.                 patterns.add(Constants.R_REFS + src);
  338.                 patterns.add(Constants.R_HEADS + src);
  339.                 patterns.add(Constants.R_TAGS + src);
  340.             }
  341.         }
  342.         return patterns;
  343.     }

  344.     private void readCapabilitiesV2() throws IOException {
  345.         // In git protocol V2, capabilities are different. If it's a key-value
  346.         // pair, the key may be a command name, and the value a space-separated
  347.         // list of capabilities for that command. We still store it in the same
  348.         // map as for protocol v0/v1. Protocol v2 code has to account for this.
  349.         for (;;) {
  350.             String line = readLine();
  351.             if (line == null) {
  352.                 break;
  353.             }
  354.             addCapability(line);
  355.         }
  356.     }

  357.     private void addCapability(String capability) {
  358.         String parts[] = capability.split("=", 2); //$NON-NLS-1$
  359.         if (parts.length == 2) {
  360.             remoteCapabilities.put(parts[0], parts[1]);
  361.         }
  362.         remoteCapabilities.put(capability, null);
  363.     }

  364.     private ObjectId toId(String line, String value)
  365.             throws PackProtocolException {
  366.         try {
  367.             return ObjectId.fromString(value);
  368.         } catch (InvalidObjectIdException e) {
  369.             PackProtocolException ppe = invalidRefAdvertisementLine(line);
  370.             ppe.initCause(e);
  371.             throw ppe;
  372.         }
  373.     }

  374.     private void processLineV1(String name, ObjectId id, Map<String, Ref> avail)
  375.             throws IOException {
  376.         if (name.endsWith("^{}")) { //$NON-NLS-1$
  377.             name = name.substring(0, name.length() - 3);
  378.             final Ref prior = avail.get(name);
  379.             if (prior == null) {
  380.                 throw new PackProtocolException(uri, MessageFormat.format(
  381.                         JGitText.get().advertisementCameBefore, name, name));
  382.             }
  383.             if (prior.getPeeledObjectId() != null) {
  384.                 throw duplicateAdvertisement(name + "^{}"); //$NON-NLS-1$
  385.             }
  386.             avail.put(name, new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name,
  387.                     prior.getObjectId(), id));
  388.         } else {
  389.             final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag(
  390.                     Ref.Storage.NETWORK, name, id));
  391.             if (prior != null) {
  392.                 throw duplicateAdvertisement(name);
  393.             }
  394.         }
  395.     }

  396.     private void processLineV2(String line, ObjectId id, String rest,
  397.             Map<String, Ref> avail, Map<String, String> symRefs)
  398.             throws IOException {
  399.         String[] parts = rest.split(" "); //$NON-NLS-1$
  400.         String name = parts[0];
  401.         // Two attributes possible, symref-target or peeled
  402.         String symRefTarget = null;
  403.         String peeled = null;
  404.         for (int i = 1; i < parts.length; i++) {
  405.             if (parts[i].startsWith(REF_ATTR_SYMREF_TARGET)) {
  406.                 if (symRefTarget != null) {
  407.                     throw new PackProtocolException(uri, MessageFormat.format(
  408.                             JGitText.get().duplicateRefAttribute, line));
  409.                 }
  410.                 symRefTarget = parts[i]
  411.                         .substring(REF_ATTR_SYMREF_TARGET.length());
  412.             } else if (parts[i].startsWith(REF_ATTR_PEELED)) {
  413.                 if (peeled != null) {
  414.                     throw new PackProtocolException(uri, MessageFormat.format(
  415.                             JGitText.get().duplicateRefAttribute, line));
  416.                 }
  417.                 peeled = parts[i].substring(REF_ATTR_PEELED.length());
  418.             }
  419.             if (peeled != null && symRefTarget != null) {
  420.                 break;
  421.             }
  422.         }
  423.         Ref idRef;
  424.         if (peeled != null) {
  425.             idRef = new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name, id,
  426.                     toId(line, peeled));
  427.         } else {
  428.             idRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK, name, id);
  429.         }
  430.         Ref prior = avail.put(name, idRef);
  431.         if (prior != null) {
  432.             throw duplicateAdvertisement(name);
  433.         }
  434.         if (!StringUtils.isEmptyOrNull(symRefTarget)) {
  435.             symRefs.put(name, symRefTarget);
  436.         }
  437.     }

  438.     /**
  439.      * Updates the given refMap with {@link SymbolicRef}s defined by the given
  440.      * symRefs.
  441.      * <p>
  442.      * For each entry, symRef, in symRefs, whose value is a key in refMap, adds
  443.      * a new entry to refMap with that same key and value of a new
  444.      * {@link SymbolicRef} with source=symRef.key and
  445.      * target=refMap.get(symRef.value), then removes that entry from symRefs.
  446.      * <p>
  447.      * If refMap already contains an entry for symRef.key, it is replaced.
  448.      * </p>
  449.      * </p>
  450.      * <p>
  451.      * For example, given:
  452.      * </p>
  453.      *
  454.      * <pre>
  455.      * refMap.put("refs/heads/main", ref);
  456.      * symRefs.put("HEAD", "refs/heads/main");
  457.      * </pre>
  458.      *
  459.      * then:
  460.      *
  461.      * <pre>
  462.      * updateWithSymRefs(refMap, symRefs);
  463.      * </pre>
  464.      *
  465.      * has the <em>effect</em> of:
  466.      *
  467.      * <pre>
  468.      * refMap.put("HEAD",
  469.      *      new SymbolicRef("HEAD", refMap.get(symRefs.remove("HEAD"))))
  470.      * </pre>
  471.      * <p>
  472.      * Any entry in symRefs whose value is not a key in refMap is ignored. Any
  473.      * circular symRefs are ignored.
  474.      * </p>
  475.      * <p>
  476.      * Upon completion, symRefs will contain only any unresolvable entries.
  477.      * </p>
  478.      *
  479.      * @param refMap
  480.      *            a non-null, modifiable, Map to update, and the provider of
  481.      *            symref targets.
  482.      * @param symRefs
  483.      *            a non-null, modifiable, Map of symrefs.
  484.      * @throws NullPointerException
  485.      *             if refMap or symRefs is null
  486.      */
  487.     static void updateWithSymRefs(Map<String, Ref> refMap, Map<String, String> symRefs) {
  488.         boolean haveNewRefMapEntries = !refMap.isEmpty();
  489.         while (!symRefs.isEmpty() && haveNewRefMapEntries) {
  490.             haveNewRefMapEntries = false;
  491.             final Iterator<Map.Entry<String, String>> iterator = symRefs.entrySet().iterator();
  492.             while (iterator.hasNext()) {
  493.                 final Map.Entry<String, String> symRef = iterator.next();
  494.                 if (!symRefs.containsKey(symRef.getValue())) { // defer forward reference
  495.                     final Ref r = refMap.get(symRef.getValue());
  496.                     if (r != null) {
  497.                         refMap.put(symRef.getKey(), new SymbolicRef(symRef.getKey(), r));
  498.                         haveNewRefMapEntries = true;
  499.                         iterator.remove();
  500.                     }
  501.                 }
  502.             }
  503.         }
  504.         // If HEAD is still in the symRefs map here, the real ref was not
  505.         // reported, but we know it must point to the object reported for HEAD.
  506.         // So fill it in in the refMap.
  507.         String headRefName = symRefs.get(Constants.HEAD);
  508.         if (headRefName != null && !refMap.containsKey(headRefName)) {
  509.             Ref headRef = refMap.get(Constants.HEAD);
  510.             if (headRef != null) {
  511.                 ObjectId headObj = headRef.getObjectId();
  512.                 headRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK,
  513.                         headRefName, headObj);
  514.                 refMap.put(headRefName, headRef);
  515.                 headRef = new SymbolicRef(Constants.HEAD, headRef);
  516.                 refMap.put(Constants.HEAD, headRef);
  517.                 symRefs.remove(Constants.HEAD);
  518.             }
  519.         }
  520.     }

  521.     /**
  522.      * Create an exception to indicate problems finding a remote repository. The
  523.      * caller is expected to throw the returned exception.
  524.      *
  525.      * Subclasses may override this method to provide better diagnostics.
  526.      *
  527.      * @return a TransportException saying a repository cannot be found and
  528.      *         possibly why.
  529.      */
  530.     protected TransportException noRepository() {
  531.         return new NoRemoteRepositoryException(uri, JGitText.get().notFound);
  532.     }

  533.     /**
  534.      * Whether this option is supported
  535.      *
  536.      * @param option
  537.      *            option string
  538.      * @return whether this option is supported
  539.      */
  540.     protected boolean isCapableOf(String option) {
  541.         return remoteCapabilities.containsKey(option);
  542.     }

  543.     /**
  544.      * Request capability
  545.      *
  546.      * @param b
  547.      *            buffer
  548.      * @param option
  549.      *            option we want
  550.      * @return {@code true} if the requested option is supported
  551.      */
  552.     protected boolean wantCapability(StringBuilder b, String option) {
  553.         if (!isCapableOf(option))
  554.             return false;
  555.         b.append(' ');
  556.         b.append(option);
  557.         return true;
  558.     }

  559.     /**
  560.      * Return a capability value.
  561.      *
  562.      * @param option
  563.      *            to get
  564.      * @return the value stored, if any.
  565.      */
  566.     protected String getCapability(String option) {
  567.         return remoteCapabilities.get(option);
  568.     }

  569.     /**
  570.      * Add user agent capability
  571.      *
  572.      * @param b
  573.      *            a {@link java.lang.StringBuilder} object.
  574.      */
  575.     protected void addUserAgentCapability(StringBuilder b) {
  576.         String a = UserAgent.get();
  577.         if (a != null && remoteCapabilities.get(OPTION_AGENT) != null) {
  578.             b.append(' ').append(OPTION_AGENT).append('=').append(a);
  579.         }
  580.     }

  581.     /** {@inheritDoc} */
  582.     @Override
  583.     public String getPeerUserAgent() {
  584.         String agent = remoteCapabilities.get(OPTION_AGENT);
  585.         return agent != null ? agent : super.getPeerUserAgent();
  586.     }

  587.     private PackProtocolException duplicateAdvertisement(String name) {
  588.         return new PackProtocolException(uri, MessageFormat.format(JGitText.get().duplicateAdvertisementsOf, name));
  589.     }

  590.     private PackProtocolException invalidRefAdvertisementLine(String line) {
  591.         return new PackProtocolException(uri, MessageFormat.format(JGitText.get().invalidRefAdvertisementLine, line));
  592.     }

  593.     /** {@inheritDoc} */
  594.     @Override
  595.     public void close() {
  596.         if (out != null) {
  597.             try {
  598.                 if (outNeedsEnd) {
  599.                     outNeedsEnd = false;
  600.                     pckOut.end();
  601.                 }
  602.                 out.close();
  603.             } catch (IOException err) {
  604.                 // Ignore any close errors.
  605.             } finally {
  606.                 out = null;
  607.                 pckOut = null;
  608.             }
  609.         }

  610.         if (in != null) {
  611.             try {
  612.                 in.close();
  613.             } catch (IOException err) {
  614.                 // Ignore any close errors.
  615.             } finally {
  616.                 in = null;
  617.                 pckIn = null;
  618.             }
  619.         }

  620.         if (myTimer != null) {
  621.             try {
  622.                 myTimer.terminate();
  623.             } finally {
  624.                 myTimer = null;
  625.                 timeoutIn = null;
  626.                 timeoutOut = null;
  627.             }
  628.         }
  629.     }

  630.     /**
  631.      * Tell the peer we are disconnecting, if it cares to know.
  632.      */
  633.     protected void endOut() {
  634.         if (outNeedsEnd && out != null) {
  635.             try {
  636.                 outNeedsEnd = false;
  637.                 pckOut.end();
  638.             } catch (IOException e) {
  639.                 try {
  640.                     out.close();
  641.                 } catch (IOException err) {
  642.                     // Ignore any close errors.
  643.                 } finally {
  644.                     out = null;
  645.                     pckOut = null;
  646.                 }
  647.             }
  648.         }
  649.     }
  650. }