diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java')
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java | 2122 |
1 files changed, 1615 insertions, 507 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java index c60590dda4..41ab8acf05 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -1,90 +1,98 @@ /* - * Copyright (C) 2008-2010, Google Inc. - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2008, 2022 Google Inc. and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.transport; -import static org.eclipse.jgit.lib.RefDatabase.ALL; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableMap; +import static java.util.Objects.requireNonNull; +import static org.eclipse.jgit.lib.Constants.R_TAGS; +import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_REF_IN_WANT; +import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SERVER_OPTION; +import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_FETCH; +import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_LS_REFS; +import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_OBJECT_INFO; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_ALLOW_REACHABLE_SHA1_IN_WANT; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_ALLOW_TIP_SHA1_IN_WANT; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_DEEPEN_RELATIVE; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_FILTER; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_INCLUDE_TAG; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_MULTI_ACK; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_MULTI_ACK_DETAILED; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_NO_DONE; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_NO_PROGRESS; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_OFS_DELTA; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SESSION_ID; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SHALLOW; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDEBAND_ALL; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND_64K; import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_THIN_PACK; - +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_WAIT_FOR_DONE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ACK; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_DONE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_ERR; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_HAVE; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_SHALLOW; +import static org.eclipse.jgit.transport.GitProtocolConstants.PACKET_UNSHALLOW; +import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2_REQUEST; +import static org.eclipse.jgit.util.RefMap.toRefMap; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.text.MessageFormat; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; - +import java.util.TreeMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.pack.CachedPackUriProvider; import org.eclipse.jgit.internal.storage.pack.PackWriter; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.AsyncRevObjectQueue; import org.eclipse.jgit.revwalk.DepthWalk; +import org.eclipse.jgit.revwalk.ObjectReachabilityChecker; import org.eclipse.jgit.revwalk.ObjectWalk; +import org.eclipse.jgit.revwalk.ReachabilityChecker; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevFlagSet; @@ -93,8 +101,10 @@ import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter; import org.eclipse.jgit.storage.pack.PackConfig; +import org.eclipse.jgit.storage.pack.PackStatistics; import org.eclipse.jgit.transport.GitProtocolConstants.MultiAck; import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser; +import org.eclipse.jgit.transport.TransferConfig.ProtocolVersion; import org.eclipse.jgit.util.io.InterruptTimer; import org.eclipse.jgit.util.io.NullOutputStream; import org.eclipse.jgit.util.io.TimeoutInputStream; @@ -103,17 +113,17 @@ import org.eclipse.jgit.util.io.TimeoutOutputStream; /** * Implements the server side of a fetch connection, transmitting objects. */ -public class UploadPack { +public class UploadPack implements Closeable { /** Policy the server uses to validate client requests */ - public static enum RequestPolicy { + public enum RequestPolicy { /** Client may only ask for objects the server advertised a reference for. */ - ADVERTISED, + ADVERTISED(0x08), /** * Client may ask for any commit reachable from a reference advertised by * the server. */ - REACHABLE_COMMIT, + REACHABLE_COMMIT(0x02), /** * Client may ask for objects that are the tip of any reference, even if not @@ -123,18 +133,36 @@ public class UploadPack { * * @since 3.1 */ - TIP, + TIP(0x01), /** * Client may ask for any commit reachable from any reference, even if that - * reference wasn't advertised. + * reference wasn't advertised, implies REACHABLE_COMMIT and TIP. * * @since 3.1 */ - REACHABLE_COMMIT_TIP, + REACHABLE_COMMIT_TIP(0x03), + + /** Client may ask for any SHA-1 in the repository, implies REACHABLE_COMMIT_TIP. */ + ANY(0x07); + + private final int bitmask; - /** Client may ask for any SHA-1 in the repository. */ - ANY; + RequestPolicy(int bitmask) { + this.bitmask = bitmask; + } + + /** + * Check if the current policy implies another, based on its bitmask. + * + * @param implied + * the implied policy based on its bitmask. + * @return true if the policy is implied. + * @since 6.10.1 + */ + public boolean implies(RequestPolicy implied) { + return (bitmask & implied.bitmask) != 0; + } } /** @@ -161,42 +189,13 @@ public class UploadPack { throws PackProtocolException, IOException; } - /** Data in the first line of a request, the line itself plus options. */ - public static class FirstLine { - private final String line; - private final Set<String> options; - - /** - * Parse the first line of a receive-pack request. - * - * @param line - * line from the client. - */ - public FirstLine(String line) { - if (line.length() > 45) { - final HashSet<String> opts = new HashSet<String>(); - String opt = line.substring(45); - if (opt.startsWith(" ")) //$NON-NLS-1$ - opt = opt.substring(1); - for (String c : opt.split(" ")) //$NON-NLS-1$ - opts.add(c); - this.line = line.substring(0, 45); - this.options = Collections.unmodifiableSet(opts); - } else { - this.line = line; - this.options = Collections.emptySet(); - } - } - - /** @return non-capabilities part of the line. */ - public String getLine() { - return line; - } - - /** @return options parsed from the line. */ - public Set<String> getOptions() { - return options; - } + /* + * {@link java.util.function.Consumer} doesn't allow throwing checked + * exceptions. Define our own to propagate IOExceptions. + */ + @FunctionalInterface + private static interface IOConsumer<R> { + void accept(R t) throws IOException; } /** Database we read the objects from. */ @@ -230,49 +229,57 @@ public class UploadPack { /** Timer to manage {@link #timeout}. */ private InterruptTimer timer; + /** + * Whether the client requested to use protocol V2 through a side + * channel (such as the Git-Protocol HTTP header). + */ + private boolean clientRequestedV2; + private InputStream rawIn; - private OutputStream rawOut; + private ResponseBufferedOutputStream rawOut; private PacketLineIn pckIn; - private PacketLineOut pckOut; - private OutputStream msgOut = NullOutputStream.INSTANCE; - /** The refs we advertised as existing at the start of the connection. */ + private ErrorWriter errOut = new PackProtocolErrorWriter(); + + /** + * Refs eligible for advertising to the client, set using + * {@link #setAdvertisedRefs}. + */ private Map<String, Ref> refs; + /** Hook used while processing Git protocol v2 requests. */ + private ProtocolV2Hook protocolV2Hook = ProtocolV2Hook.DEFAULT; + /** Hook used while advertising the refs to the client. */ private AdvertiseRefsHook advertiseRefsHook = AdvertiseRefsHook.DEFAULT; + /** Whether the {@link #advertiseRefsHook} has been invoked. */ + private boolean advertiseRefsHookCalled; + /** Filter used while advertising the refs to the client. */ private RefFilter refFilter = RefFilter.DEFAULT; /** Hook handling the various upload phases. */ private PreUploadHook preUploadHook = PreUploadHook.NULL; - /** Capabilities requested by the client. */ - private Set<String> options; + /** Hook for taking post upload actions. */ + private PostUploadHook postUploadHook = PostUploadHook.NULL; + + /** Caller user agent */ String userAgent; /** Raw ObjectIds the client has asked for, before validating them. */ - private final Set<ObjectId> wantIds = new HashSet<ObjectId>(); + private Set<ObjectId> wantIds = new HashSet<>(); /** Objects the client wants to obtain. */ - private final Set<RevObject> wantAll = new HashSet<RevObject>(); + private final Set<RevObject> wantAll = new HashSet<>(); /** Objects on both sides, these don't have to be sent. */ - private final Set<RevObject> commonBase = new HashSet<RevObject>(); - - /** Shallow commits the client already has. */ - private final Set<ObjectId> clientShallowCommits = new HashSet<ObjectId>(); - - /** Shallow commits on the client which are now becoming unshallow */ - private final List<ObjectId> unshallowCommits = new ArrayList<ObjectId>(); - - /** Desired depth from the client on a shallow request. */ - private int depth; + private final Set<RevObject> commonBase = new HashSet<>(); /** Commit time of the oldest common commit, in seconds. */ private int oldestTime; @@ -282,7 +289,7 @@ public class UploadPack { private boolean sentReady; - /** Objects we sent in our advertisement list, clients can ask for these. */ + /** Objects we sent in our advertisement list. */ private Set<ObjectId> advertised; /** Marked on objects the client has asked us to give them. */ @@ -305,9 +312,18 @@ public class UploadPack { private boolean noDone; - private PackWriter.Statistics statistics; + private PackStatistics statistics; - private UploadPackLogger logger = UploadPackLogger.NULL; + /** + * Request this instance is handling. + * + * We need to keep a reference to it for {@link PreUploadHook pre upload + * hooks}. They receive a reference this instance and invoke methods like + * getDepth() to get information about the request. + */ + private FetchRequest currentRequest; + + private CachedPackUriProvider cachedPackUriProvider; /** * Create a new pack upload for an open repository. @@ -315,7 +331,7 @@ public class UploadPack { * @param copyFrom * the source repository. */ - public UploadPack(final Repository copyFrom) { + public UploadPack(Repository copyFrom) { db = copyFrom; walk = new RevWalk(db); walk.setRetainBody(false); @@ -335,12 +351,20 @@ public class UploadPack { setTransferConfig(null); } - /** @return the repository this upload is reading from. */ + /** + * Get the repository this upload is reading from. + * + * @return the repository this upload is reading from. + */ public final Repository getRepository() { return db; } - /** @return the RevWalk instance used by this connection. */ + /** + * Get the RevWalk instance used by this connection. + * + * @return the RevWalk instance used by this connection. + */ public final RevWalk getRevWalk() { return walk; } @@ -348,8 +372,10 @@ public class UploadPack { /** * Get refs which were advertised to the client. * - * @return all refs which were advertised to the client, or null if - * {@link #setAdvertisedRefs(Map)} has not been called yet. + * @return all refs which were advertised to the client. Only valid during + * the negotiation phase. Will return {@code null} if + * {@link #setAdvertisedRefs(Map)} has not been called yet or if + * {@code #sendPack()} has been called. */ public final Map<String, Ref> getAdvertisedRefs() { return refs; @@ -358,27 +384,34 @@ public class UploadPack { /** * Set the refs advertised by this UploadPack. * <p> - * Intended to be called from a {@link PreUploadHook}. + * Intended to be called from a + * {@link org.eclipse.jgit.transport.PreUploadHook}. * * @param allRefs * explicit set of references to claim as advertised by this - * UploadPack instance. This overrides any references that - * may exist in the source repository. The map is passed - * to the configured {@link #getRefFilter()}. If null, assumes - * all refs were advertised. + * UploadPack instance. This overrides any references that may + * exist in the source repository. The map is passed to the + * configured {@link #getRefFilter()}. If null, assumes all refs + * were advertised. */ - public void setAdvertisedRefs(Map<String, Ref> allRefs) { - if (allRefs != null) + public void setAdvertisedRefs(@Nullable Map<String, Ref> allRefs) { + if (allRefs != null) { refs = allRefs; - else - refs = db.getAllRefs(); - if (refFilter == RefFilter.DEFAULT) + } else { + refs = getAllRefs(); + } + if (refFilter == RefFilter.DEFAULT) { refs = transferConfig.getRefFilter().filter(refs); - else + } else { refs = refFilter.filter(refs); + } } - /** @return timeout (in seconds) before aborting an IO operation. */ + /** + * Get timeout (in seconds) before aborting an IO operation. + * + * @return timeout (in seconds) before aborting an IO operation. + */ public int getTimeout() { return timeout; } @@ -391,11 +424,14 @@ public class UploadPack { * before aborting an IO read or write operation with the * connected client. */ - public void setTimeout(final int seconds) { + public void setTimeout(int seconds) { timeout = seconds; } /** + * Whether this class expects a bi-directional pipe opened between the + * client and itself. + * * @return true if this class expects a bi-directional pipe opened between * the client and itself. The default is true. */ @@ -404,6 +440,9 @@ public class UploadPack { } /** + * Set whether this class will assume the socket is a fully bidirectional + * pipe between the two peers + * * @param twoWay * if true, this class will assume the socket is a fully * bidirectional pipe between the two peers and takes advantage @@ -412,13 +451,15 @@ public class UploadPack { * commands before writing output and does not perform the * initial advertising. */ - public void setBiDirectionalPipe(final boolean twoWay) { + public void setBiDirectionalPipe(boolean twoWay) { biDirectionalPipe = twoWay; } /** - * @return policy used by the service to validate client requests, or null for - * a custom request validator. + * Get policy used by the service to validate client requests + * + * @return policy used by the service to validate client requests, or null + * for a custom request validator. */ public RequestPolicy getRequestPolicy() { if (requestValidator instanceof AdvertisedRequestValidator) @@ -435,15 +476,21 @@ public class UploadPack { } /** + * Set the policy used to enforce validation of a client's want list. + * * @param policy * the policy used to enforce validation of a client's want list. - * By default the policy is {@link RequestPolicy#ADVERTISED}, + * By default the policy is + * {@link org.eclipse.jgit.transport.UploadPack.RequestPolicy#ADVERTISED}, * which is the Git default requiring clients to only ask for an - * object that a reference directly points to. This may be relaxed - * to {@link RequestPolicy#REACHABLE_COMMIT} or - * {@link RequestPolicy#REACHABLE_COMMIT_TIP} when callers have - * {@link #setBiDirectionalPipe(boolean)} set to false. - * Overrides any policy specified in a {@link TransferConfig}. + * object that a reference directly points to. This may be + * relaxed to + * {@link org.eclipse.jgit.transport.UploadPack.RequestPolicy#REACHABLE_COMMIT} + * or + * {@link org.eclipse.jgit.transport.UploadPack.RequestPolicy#REACHABLE_COMMIT_TIP} + * when callers have {@link #setBiDirectionalPipe(boolean)} set + * to false. Overrides any policy specified in a + * {@link org.eclipse.jgit.transport.TransferConfig}. */ public void setRequestPolicy(RequestPolicy policy) { switch (policy) { @@ -467,21 +514,31 @@ public class UploadPack { } /** + * Set custom validator for client want list. + * * @param validator * custom validator for client want list. * @since 3.1 */ - public void setRequestValidator(RequestValidator validator) { + public void setRequestValidator(@Nullable RequestValidator validator) { requestValidator = validator != null ? validator : new AdvertisedRequestValidator(); } - /** @return the hook used while advertising the refs to the client */ + /** + * Get the hook used while advertising the refs to the client. + * + * @return the hook used while advertising the refs to the client. + */ public AdvertiseRefsHook getAdvertiseRefsHook() { return advertiseRefsHook; } - /** @return the filter used while advertising the refs to the client */ + /** + * Get the filter used while advertising the refs to the client. + * + * @return the filter used while advertising the refs to the client. + */ public RefFilter getRefFilter() { return refFilter; } @@ -489,36 +546,64 @@ public class UploadPack { /** * Set the hook used while advertising the refs to the client. * <p> - * If the {@link AdvertiseRefsHook} chooses to call - * {@link #setAdvertisedRefs(Map)}, only refs set by this hook <em>and</em> - * selected by the {@link RefFilter} will be shown to the client. + * If the {@link org.eclipse.jgit.transport.AdvertiseRefsHook} chooses to + * call {@link #setAdvertisedRefs(Map)}, only refs set by this hook + * <em>and</em> selected by the {@link org.eclipse.jgit.transport.RefFilter} + * will be shown to the client. * * @param advertiseRefsHook * the hook; may be null to show all refs. */ - public void setAdvertiseRefsHook(final AdvertiseRefsHook advertiseRefsHook) { - if (advertiseRefsHook != null) - this.advertiseRefsHook = advertiseRefsHook; - else - this.advertiseRefsHook = AdvertiseRefsHook.DEFAULT; + public void setAdvertiseRefsHook( + @Nullable AdvertiseRefsHook advertiseRefsHook) { + this.advertiseRefsHook = advertiseRefsHook != null ? advertiseRefsHook + : AdvertiseRefsHook.DEFAULT; + } + + /** + * Set the protocol V2 hook. + * + * @param hook + * the hook; if null no special actions are taken. + * @since 5.1 + */ + public void setProtocolV2Hook(@Nullable ProtocolV2Hook hook) { + this.protocolV2Hook = hook != null ? hook : ProtocolV2Hook.DEFAULT; + } + + /** + * Get the currently installed protocol v2 hook. + * + * @return the hook or a default implementation if none installed. + * + * @since 5.5 + */ + public ProtocolV2Hook getProtocolV2Hook() { + return this.protocolV2Hook != null ? this.protocolV2Hook + : ProtocolV2Hook.DEFAULT; } /** * Set the filter used while advertising the refs to the client. * <p> - * Only refs allowed by this filter will be sent to the client. - * The filter is run against the refs specified by the - * {@link AdvertiseRefsHook} (if applicable). If null or not set, uses the - * filter implied by the {@link TransferConfig}. + * Only refs allowed by this filter will be sent to the client. The filter + * is run against the refs specified by the + * {@link org.eclipse.jgit.transport.AdvertiseRefsHook} (if applicable). If + * null or not set, uses the filter implied by the + * {@link org.eclipse.jgit.transport.TransferConfig}. * * @param refFilter * the filter; may be null to show all refs. */ - public void setRefFilter(final RefFilter refFilter) { + public void setRefFilter(@Nullable RefFilter refFilter) { this.refFilter = refFilter != null ? refFilter : RefFilter.DEFAULT; } - /** @return the configured upload hook. */ + /** + * Get the configured pre upload hook. + * + * @return the configured pre upload hook. + */ public PreUploadHook getPreUploadHook() { return preUploadHook; } @@ -529,46 +614,63 @@ public class UploadPack { * @param hook * the hook; if null no special actions are taken. */ - public void setPreUploadHook(PreUploadHook hook) { + public void setPreUploadHook(@Nullable PreUploadHook hook) { preUploadHook = hook != null ? hook : PreUploadHook.NULL; } /** + * Get the configured post upload hook. + * + * @return the configured post upload hook. + * @since 4.1 + */ + public PostUploadHook getPostUploadHook() { + return postUploadHook; + } + + /** + * Set the hook for post upload actions (logging, repacking). + * + * @param hook + * the hook; if null no special actions are taken. + * @since 4.1 + */ + public void setPostUploadHook(@Nullable PostUploadHook hook) { + postUploadHook = hook != null ? hook : PostUploadHook.NULL; + } + + /** * Set the configuration used by the pack generator. * * @param pc * configuration controlling packing parameters. If null the * source repository's settings will be used. */ - public void setPackConfig(PackConfig pc) { + public void setPackConfig(@Nullable PackConfig pc) { this.packConfig = pc; } /** + * Set configuration controlling transfer options. + * * @param tc * configuration controlling transfer options. If null the source * repository's settings will be used. * @since 3.1 */ - public void setTransferConfig(TransferConfig tc) { + public void setTransferConfig(@Nullable TransferConfig tc) { this.transferConfig = tc != null ? tc : new TransferConfig(db); - setRequestPolicy(transferConfig.isAllowTipSha1InWant() - ? RequestPolicy.TIP : RequestPolicy.ADVERTISED); - } - - /** @return the configured logger. */ - public UploadPackLogger getLogger() { - return logger; - } - - /** - * Set the logger. - * - * @param logger - * the logger instance. If null, no logging occurs. - */ - public void setLogger(UploadPackLogger logger) { - this.logger = logger; + if (transferConfig.isAllowAnySha1InWant()) { + setRequestPolicy(RequestPolicy.ANY); + return; + } + if (transferConfig.isAllowTipSha1InWant()) { + setRequestPolicy(transferConfig.isAllowReachableSha1InWant() + ? RequestPolicy.REACHABLE_COMMIT_TIP : RequestPolicy.TIP); + } else { + setRequestPolicy(transferConfig.isAllowReachableSha1InWant() + ? RequestPolicy.REACHABLE_COMMIT : RequestPolicy.ADVERTISED); + } } /** @@ -576,22 +678,134 @@ public class UploadPack { * * @return true if the client has advertised a side-band capability, false * otherwise. - * @throws RequestNotYetReadException + * @throws org.eclipse.jgit.transport.RequestNotYetReadException * if the client's request has not yet been read from the wire, so * we do not know if they expect side-band. Note that the client * may have already written the request, it just has not been * read. */ public boolean isSideBand() throws RequestNotYetReadException { - if (options == null) + if (currentRequest == null) { throw new RequestNotYetReadException(); - return (options.contains(OPTION_SIDE_BAND) - || options.contains(OPTION_SIDE_BAND_64K)); + } + Set<String> caps = currentRequest.getClientCapabilities(); + return caps.contains(OPTION_SIDE_BAND) + || caps.contains(OPTION_SIDE_BAND_64K); + } + + /** + * Set the Extra Parameters provided by the client. + * + * <p>These are parameters passed by the client through a side channel + * such as the Git-Protocol HTTP header, to allow a client to request + * a newer response format while remaining compatible with older servers + * that do not understand different request formats. + * + * @param params + * parameters supplied by the client, split at colons or NUL + * bytes. + * @since 5.0 + */ + public void setExtraParameters(Collection<String> params) { + this.clientRequestedV2 = params.contains(VERSION_2_REQUEST); + } + + /** + * Set provider of cached pack URIs + * + * @param p + * provider of URIs corresponding to cached packs (to support the + * packfile URIs feature) + * @since 5.5 + */ + public void setCachedPackUriProvider(@Nullable CachedPackUriProvider p) { + cachedPackUriProvider = p; + } + + private boolean useProtocolV2() { + return (transferConfig.protocolVersion == null + || ProtocolVersion.V2.equals(transferConfig.protocolVersion)) + && clientRequestedV2; + } + + @Override + public void close() { + if (timer != null) { + try { + timer.terminate(); + } finally { + timer = null; + } + } + } + + /** + * Execute the upload task on the socket. + * + * <p> + * Same as {@link #uploadWithExceptionPropagation} except that the thrown + * exceptions are handled in the method, and the error messages are sent to + * the clients. + * + * <p> + * Call this method if the caller does not have an error handling mechanism. + * Call {@link #uploadWithExceptionPropagation} if the caller wants to have + * its own error handling mechanism. + * + * @param input + * input stream + * @param output + * output stream + * @param messages + * stream for messages + * @throws java.io.IOException + * if an IO error occurred + */ + public void upload(InputStream input, OutputStream output, + @Nullable OutputStream messages) throws IOException { + try { + uploadWithExceptionPropagation(input, output, messages); + } catch (ServiceMayNotContinueException err) { + if (!err.isOutput() && err.getMessage() != null) { + try { + errOut.writeError(err.getMessage()); + } catch (IOException e) { + err.addSuppressed(e); + throw err; + } + err.setOutput(); + } + throw err; + } catch (IOException | RuntimeException | Error err) { + if (rawOut != null) { + String msg = err instanceof PackProtocolException + ? err.getMessage() + : JGitText.get().internalServerError; + try { + errOut.writeError(msg); + } catch (IOException e) { + err.addSuppressed(e); + throw err; + } + throw new UploadPackInternalServerErrorException(err); + } + throw err; + } finally { + close(); + } } /** * Execute the upload task on the socket. * + * <p> + * If the client passed extra parameters (e.g., "version=2") through a side + * channel, the caller must call setExtraParameters first to supply them. + * Callers of this method should call {@link #close()} to terminate the + * internal interrupt timer thread. If the caller fails to terminate the + * thread, it will (eventually) terminate itself when the InterruptTimer + * instance is garbage collected. + * * @param input * raw input to read client commands from. Caller must ensure the * input is buffered, otherwise read performance may suffer. @@ -604,40 +818,49 @@ public class UploadPack { * through. When run over SSH this should be tied back to the * standard error channel of the command execution. For most * other network connections this should be null. + * @throws ServiceMayNotContinueException + * thrown if one of the hooks throws this. * @throws IOException + * thrown if the server or the client I/O fails, or there's an + * internal server error. + * @since 5.6 */ - public void upload(final InputStream input, final OutputStream output, - final OutputStream messages) throws IOException { + public void uploadWithExceptionPropagation(InputStream input, + OutputStream output, @Nullable OutputStream messages) + throws ServiceMayNotContinueException, IOException { try { rawIn = input; - rawOut = output; - if (messages != null) + if (messages != null) { msgOut = messages; + } if (timeout > 0) { final Thread caller = Thread.currentThread(); timer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$ TimeoutInputStream i = new TimeoutInputStream(rawIn, timer); - TimeoutOutputStream o = new TimeoutOutputStream(rawOut, timer); + @SuppressWarnings("resource") + TimeoutOutputStream o = new TimeoutOutputStream(output, timer); i.setTimeout(timeout * 1000); o.setTimeout(timeout * 1000); rawIn = i; - rawOut = o; + output = o; + } + + rawOut = new ResponseBufferedOutputStream(output); + if (biDirectionalPipe) { + rawOut.stopBuffering(); } pckIn = new PacketLineIn(rawIn); - pckOut = new PacketLineOut(rawOut); - service(); + PacketLineOut pckOut = new PacketLineOut(rawOut); + if (useProtocolV2()) { + serviceV2(pckOut); + } else { + service(pckOut); + } } finally { msgOut = NullOutputStream.INSTANCE; walk.close(); - if (timer != null) { - try { - timer.terminate(); - } finally { - timer = null; - } - } } } @@ -645,103 +868,604 @@ public class UploadPack { * Get the PackWriter's statistics if a pack was sent to the client. * * @return statistics about pack output, if a pack was sent. Null if no pack - * was sent, such as during the negotation phase of a smart HTTP + * was sent, such as during the negotiation phase of a smart HTTP * connection, or if the client was already up-to-date. - * @since 3.0 + * @since 4.1 */ - public PackWriter.Statistics getPackStatistics() { + public PackStatistics getStatistics() { return statistics; } - private Map<String, Ref> getAdvertisedOrDefaultRefs() { - if (refs == null) - setAdvertisedRefs(null); + /** + * Extract the full list of refs from the ref-db. + * + * @return Map of all refname/ref + */ + private Map<String, Ref> getAllRefs() { + try { + return db.getRefDatabase().getRefs().stream().collect( + Collectors.toMap(Ref::getName, Function.identity())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private Map<String, Ref> getAdvertisedOrDefaultRefs() throws IOException { + if (refs != null) { + return refs; + } + + if (!advertiseRefsHookCalled) { + advertiseRefsHook.advertiseRefs(this); + advertiseRefsHookCalled = true; + } + if (refs == null) { + // Fall back to all refs. + setAdvertisedRefs( + db.getRefDatabase().getRefs().stream() + .collect(toRefMap((a, b) -> b))); + } return refs; } - private void service() throws IOException { - if (biDirectionalPipe) - sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut)); - else if (requestValidator instanceof AnyRequestValidator) - advertised = Collections.emptySet(); - else - advertised = refIdSet(getAdvertisedOrDefaultRefs().values()); + private Map<String, Ref> getFilteredRefs(Collection<String> refPrefixes) + throws IOException { + if (refPrefixes.isEmpty()) { + return getAdvertisedOrDefaultRefs(); + } + if (refs == null && !advertiseRefsHookCalled) { + advertiseRefsHook.advertiseRefs(this); + advertiseRefsHookCalled = true; + } + if (refs == null) { + // Fast path: the advertised refs hook did not set advertised refs. + String[] prefixes = refPrefixes.toArray(new String[0]); + Map<String, Ref> rs = + db.getRefDatabase().getRefsByPrefix(prefixes).stream() + .collect(toRefMap((a, b) -> b)); + if (refFilter != RefFilter.DEFAULT) { + return refFilter.filter(rs); + } + return transferConfig.getRefFilter().filter(rs); + } + + // Slow path: filter the refs provided by the advertised refs hook. + // refFilter has already been applied to refs. + return refs.values().stream() + .filter(ref -> refPrefixes.stream() + .anyMatch(ref.getName()::startsWith)) + .collect(toRefMap((a, b) -> b)); + } + + /** + * Returns the specified references. + * <p> + * This produces an immutable map containing whatever subset of the + * refs named by the caller are present in the supplied {@code refs} + * map. + * + * @param refs + * Map to search for refs to return. + * @param names + * which refs to search for in {@code refs}. + * @return the requested Refs, omitting any that are null or missing. + */ + @NonNull + private static Map<String, Ref> mapRefs( + Map<String, Ref> refs, List<String> names) { + return unmodifiableMap( + names.stream() + .map(refs::get) + .filter(Objects::nonNull) + .collect(toRefMap((a, b) -> b))); + } + + /** + * Read refs on behalf of the client. + * <p> + * This checks whether the refs are present in the ref advertisement + * since otherwise the client might not be supposed to be able to + * read them. + * + * @param names + * unabbreviated names of references. + * @return the requested Refs, omitting any that are not visible or + * do not exist. + * @throws java.io.IOException + * on failure to read a ref or check it for visibility. + */ + @NonNull + private Map<String, Ref> exactRefs(List<String> names) throws IOException { + if (refs != null) { + return mapRefs(refs, names); + } + if (!advertiseRefsHookCalled) { + advertiseRefsHook.advertiseRefs(this); + advertiseRefsHookCalled = true; + } + if (refs == null && + refFilter == RefFilter.DEFAULT && + transferConfig.hasDefaultRefFilter()) { + // Fast path: no ref filtering is needed. + String[] ns = names.toArray(new String[0]); + return unmodifiableMap(db.getRefDatabase().exactRef(ns)); + } + return mapRefs(getAdvertisedOrDefaultRefs(), names); + } + + /** + * Find a ref in the usual search path on behalf of the client. + * <p> + * This checks that the ref is present in the ref advertisement since + * otherwise the client might not be supposed to be able to read it. + * + * @param name + * short name of the ref to find, e.g. "master" to find + * "refs/heads/master". + * @return the requested Ref, or {@code null} if it is not visible or + * does not exist. + * @throws java.io.IOException + * on failure to read the ref or check it for visibility. + */ + @Nullable + private Ref findRef(String name) throws IOException { + if (refs != null) { + return RefDatabase.findRef(refs, name); + } + if (!advertiseRefsHookCalled) { + advertiseRefsHook.advertiseRefs(this); + advertiseRefsHookCalled = true; + } + if (refs == null && + refFilter == RefFilter.DEFAULT && + transferConfig.hasDefaultRefFilter()) { + // Fast path: no ref filtering is needed. + return db.getRefDatabase().findRef(name); + } + return RefDatabase.findRef(getAdvertisedOrDefaultRefs(), name); + } - boolean sendPack; + private void service(PacketLineOut pckOut) throws IOException { + boolean sendPack = false; + // If it's a non-bidi request, we need to read the entire request before + // writing a response. Buffer the response until then. + PackStatistics.Accumulator accumulator = new PackStatistics.Accumulator(); + List<ObjectId> unshallowCommits = new ArrayList<>(); + List<ObjectId> deepenNots = emptyList(); + FetchRequest req; try { - recvWants(); - if (wantIds.isEmpty()) { - preUploadHook.onBeginNegotiateRound(this, wantIds, 0); - preUploadHook.onEndNegotiateRound(this, wantIds, 0, 0, false); + if (biDirectionalPipe) + sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut)); + else if (requestValidator instanceof AnyRequestValidator) + advertised = Collections.emptySet(); + else + advertised = refIdSet(getAdvertisedOrDefaultRefs().values()); + + Instant negotiateStart = Instant.now(); + accumulator.advertised = advertised.size(); + + ProtocolV0Parser parser = new ProtocolV0Parser(transferConfig); + req = parser.recvWants(pckIn); + currentRequest = req; + + wantIds = req.getWantIds(); + + if (req.getWantIds().isEmpty()) { + preUploadHook.onBeginNegotiateRound(this, req.getWantIds(), 0); + preUploadHook.onEndNegotiateRound(this, req.getWantIds(), 0, 0, + false); return; } + accumulator.wants = req.getWantIds().size(); - if (options.contains(OPTION_MULTI_ACK_DETAILED)) { + if (req.getClientCapabilities().contains(OPTION_MULTI_ACK_DETAILED)) { multiAck = MultiAck.DETAILED; - noDone = options.contains(OPTION_NO_DONE); - } else if (options.contains(OPTION_MULTI_ACK)) + noDone = req.getClientCapabilities().contains(OPTION_NO_DONE); + } else if (req.getClientCapabilities().contains(OPTION_MULTI_ACK)) multiAck = MultiAck.CONTINUE; else multiAck = MultiAck.OFF; - if (depth != 0) - processShallow(); - if (!clientShallowCommits.isEmpty()) - walk.assumeShallow(clientShallowCommits); - sendPack = negotiate(); - } catch (PackProtocolException err) { - reportErrorDuringNegotiate(err.getMessage()); - throw err; + if (!req.getClientShallowCommits().isEmpty()) { + verifyClientShallow(req.getClientShallowCommits()); + } - } catch (ServiceMayNotContinueException err) { - if (!err.isOutput() && err.getMessage() != null) { - try { - pckOut.writeString("ERR " + err.getMessage() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ - err.setOutput(); - } catch (Throwable err2) { - // Ignore this secondary failure (and not mark output). + deepenNots = parseDeepenNots(req.getDeepenNots()); + if (req.getDepth() != 0 || req.getDeepenSince() != 0 || !req.getDeepenNots().isEmpty()) { + computeShallowsAndUnshallows(req, shallow -> { + pckOut.writeString(PACKET_SHALLOW + shallow.name() + '\n'); + }, unshallow -> { + pckOut.writeString( + PACKET_UNSHALLOW + unshallow.name() + '\n'); + unshallowCommits.add(unshallow); + }, deepenNots); + pckOut.end(); + } + + if (!req.getClientShallowCommits().isEmpty()) + walk.assumeShallow(req.getClientShallowCommits()); + sendPack = negotiate(req, accumulator, pckOut); + accumulator.timeNegotiating = Duration + .between(negotiateStart, Instant.now()).toMillis(); + + if (sendPack && !biDirectionalPipe) { + // Ensure the request was fully consumed. Any remaining input must + // be a protocol error. If we aren't at EOF the implementation is broken. + int eof = rawIn.read(); + if (0 <= eof) { + sendPack = false; + throw new CorruptObjectException(MessageFormat.format( + JGitText.get().expectedEOFReceived, + "\\x" + Integer.toHexString(eof))); //$NON-NLS-1$ } } - throw err; + } finally { + if (!sendPack && !biDirectionalPipe) { + while (0 < rawIn.skip(2048) || 0 <= rawIn.read()) { + // Discard until EOF. + } + } + rawOut.stopBuffering(); + } - } catch (IOException err) { - reportErrorDuringNegotiate(JGitText.get().internalServerError); - throw err; - } catch (RuntimeException err) { - reportErrorDuringNegotiate(JGitText.get().internalServerError); - throw err; - } catch (Error err) { - reportErrorDuringNegotiate(JGitText.get().internalServerError); - throw err; + if (sendPack) { + sendPack(accumulator, req, refs == null ? null : refs.values(), + unshallowCommits, deepenNots, pckOut); + } + } + + private void lsRefsV2(PacketLineOut pckOut) throws IOException { + ProtocolV2Parser parser = new ProtocolV2Parser(transferConfig); + LsRefsV2Request req = parser.parseLsRefsRequest(pckIn); + protocolV2Hook.onLsRefs(req); + + rawOut.stopBuffering(); + PacketLineOutRefAdvertiser adv = new PacketLineOutRefAdvertiser(pckOut); + adv.init(db); + adv.setUseProtocolV2(true); + if (req.getPeel()) { + adv.setDerefTags(true); + } + Map<String, Ref> refsToSend = getFilteredRefs(req.getRefPrefixes()); + if (req.getSymrefs()) { + findSymrefs(adv, refsToSend); + } + + adv.send(refsToSend.values()); + adv.end(); + } + + // Resolves ref names from the request's want-ref lines to + // object ids, throwing PackProtocolException if any are missing. + private Map<String, ObjectId> wantedRefs(FetchV2Request req) + throws IOException { + Map<String, ObjectId> result = new TreeMap<>(); + + List<String> wanted = req.getWantedRefs(); + Map<String, Ref> resolved = exactRefs(wanted); + + for (String refName : wanted) { + Ref ref = resolved.get(refName); + if (ref == null) { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().invalidRefName, refName)); + } + ObjectId oid = ref.getObjectId(); + if (oid == null) { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().invalidRefName, refName)); + } + result.put(refName, oid); + } + return result; + } + + private void fetchV2(PacketLineOut pckOut) throws IOException { + ProtocolV2Parser parser = new ProtocolV2Parser(transferConfig); + FetchV2Request req = parser.parseFetchRequest(pckIn); + currentRequest = req; + Map<String, ObjectId> wantedRefs = wantedRefs(req); + + // Depending on the requestValidator, #processHaveLines may + // require that advertised be set. Set it only in the required + // circumstances (to avoid a full ref lookup in the case that + // we don't need it). + if (requestValidator instanceof TipRequestValidator || + requestValidator instanceof ReachableCommitTipRequestValidator || + requestValidator instanceof AnyRequestValidator) { + advertised = Collections.emptySet(); + } else { + if (req.wantIds.isEmpty()) { + // Only refs-in-wants in request. These ref-in-wants where used as + // filters already in the ls-refs, there is no need to use a full + // advertisement now in fetch. This improves performance and also + // accuracy: when the ref db prioritize and truncates the returned + // refs (e.g. Gerrit hides too old refs), applying a filter can + // return different results than a plain listing. + advertised = refIdSet(getFilteredRefs(wantedRefs.keySet()).values()); + } else { + // At least one SHA1 in wants, so we need to take the full + // advertisement as base for a reachability check. + advertised = refIdSet(getAdvertisedOrDefaultRefs().values()); + } + } + + PackStatistics.Accumulator accumulator = new PackStatistics.Accumulator(); + Instant negotiateStart = Instant.now(); + accumulator.advertised = advertised.size(); + + rawOut.stopBuffering(); + + protocolV2Hook.onFetch(req); + + if (req.getSidebandAll()) { + pckOut.setUsingSideband(true); + } + + // TODO(ifrade): Refactor to pass around the Request object, instead of + // copying data back to class fields + List<ObjectId> deepenNots = parseDeepenNots(req.getDeepenNots()); + + // TODO(ifrade): Avoid mutating the parsed request. + req.getWantIds().addAll(wantedRefs.values()); + wantIds = req.getWantIds(); + accumulator.wants = wantIds.size(); + + boolean sectionSent = false; + boolean mayHaveShallow = req.getDepth() != 0 + || req.getDeepenSince() != 0 + || !req.getDeepenNots().isEmpty(); + List<ObjectId> shallowCommits = new ArrayList<>(); + List<ObjectId> unshallowCommits = new ArrayList<>(); + + if (!req.getClientShallowCommits().isEmpty()) { + verifyClientShallow(req.getClientShallowCommits()); + } + if (mayHaveShallow) { + computeShallowsAndUnshallows(req, + shallowCommit -> shallowCommits.add(shallowCommit), + unshallowCommit -> unshallowCommits.add(unshallowCommit), + deepenNots); + } + if (!req.getClientShallowCommits().isEmpty()) + walk.assumeShallow(req.getClientShallowCommits()); + + if (req.wasDoneReceived()) { + processHaveLines( + req.getPeerHas(), ObjectId.zeroId(), + new PacketLineOut(NullOutputStream.INSTANCE, false), + accumulator, req.wasWaitForDoneReceived() ? Option.WAIT_FOR_DONE : Option.NONE); + } else { + pckOut.writeString( + GitProtocolConstants.SECTION_ACKNOWLEDGMENTS + '\n'); + for (ObjectId id : req.getPeerHas()) { + if (walk.getObjectReader().has(id)) { + pckOut.writeString(PACKET_ACK + id.getName() + '\n'); + } + } + processHaveLines(req.getPeerHas(), ObjectId.zeroId(), + new PacketLineOut(NullOutputStream.INSTANCE, false), + accumulator, Option.NONE); + if (!req.wasWaitForDoneReceived() && okToGiveUp()) { + pckOut.writeString("ready\n"); //$NON-NLS-1$ + } else if (commonBase.isEmpty()) { + pckOut.writeString("NAK\n"); //$NON-NLS-1$ + } + sectionSent = true; + } + + if (req.wasDoneReceived() || (!req.wasWaitForDoneReceived() && okToGiveUp())) { + if (mayHaveShallow) { + if (sectionSent) + pckOut.writeDelim(); + pckOut.writeString( + GitProtocolConstants.SECTION_SHALLOW_INFO + '\n'); + for (ObjectId o : shallowCommits) { + pckOut.writeString(PACKET_SHALLOW + o.getName() + '\n'); + } + for (ObjectId o : unshallowCommits) { + pckOut.writeString(PACKET_UNSHALLOW + o.getName() + '\n'); + } + sectionSent = true; + } + + if (!wantedRefs.isEmpty()) { + if (sectionSent) { + pckOut.writeDelim(); + } + pckOut.writeString("wanted-refs\n"); //$NON-NLS-1$ + for (Map.Entry<String, ObjectId> entry : + wantedRefs.entrySet()) { + pckOut.writeString(entry.getValue().getName() + ' ' + + entry.getKey() + '\n'); + } + sectionSent = true; + } + + if (sectionSent) + pckOut.writeDelim(); + if (!pckOut.isUsingSideband()) { + // sendPack will write "packfile\n" for us if sideband-all is used. + // But sideband-all is not used, so we have to write it ourselves. + pckOut.writeString( + GitProtocolConstants.SECTION_PACKFILE + '\n'); + } + + accumulator.timeNegotiating = Duration + .between(negotiateStart, Instant.now()).toMillis(); + + sendPack(accumulator, + req, + req.getClientCapabilities().contains(OPTION_INCLUDE_TAG) + ? db.getRefDatabase().getRefsByPrefix(R_TAGS) + : null, + unshallowCommits, deepenNots, pckOut); + // sendPack invokes pckOut.end() for us, so we do not + // need to invoke it here. + } else { + // Invoke pckOut.end() by ourselves. + pckOut.end(); + } + } + + private void objectInfo(PacketLineOut pckOut) throws IOException { + ProtocolV2Parser parser = new ProtocolV2Parser(transferConfig); + ObjectInfoRequest req = parser.parseObjectInfoRequest(pckIn); + + protocolV2Hook.onObjectInfo(req); + + ObjectReader or = getRepository().newObjectReader(); + + // Size is the only attribute currently supported. + pckOut.writeString("size"); //$NON-NLS-1$ + + for (ObjectId oid : req.getObjectIDs()) { + long size; + try { + size = or.getObjectSize(oid, ObjectReader.OBJ_ANY); + } catch (MissingObjectException e) { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().missingObject, oid.name()), e); + } + + pckOut.writeString(oid.getName() + ' ' + size); + } + + pckOut.end(); + } + + /* + * Returns true if this is the last command and we should tear down the + * connection. + */ + private boolean serveOneCommandV2(PacketLineOut pckOut) throws IOException { + String command; + try { + command = pckIn.readString(); + } catch (EOFException eof) { + /* EOF when awaiting command is fine */ + return true; + } + if (PacketLineIn.isEnd(command)) { + // A blank request is valid according + // to the protocol; do nothing in this + // case. + return true; + } + if (command.equals("command=" + COMMAND_LS_REFS)) { //$NON-NLS-1$ + lsRefsV2(pckOut); + return false; + } + if (command.equals("command=" + COMMAND_FETCH)) { //$NON-NLS-1$ + fetchV2(pckOut); + return false; + } + if (command.equals("command=" + COMMAND_OBJECT_INFO)) { //$NON-NLS-1$ + objectInfo(pckOut); + return false; + } + throw new PackProtocolException(MessageFormat + .format(JGitText.get().unknownTransportCommand, command)); + } + + @SuppressWarnings("nls") + private List<String> getV2CapabilityAdvertisement() { + ArrayList<String> caps = new ArrayList<>(); + caps.add("version 2"); + caps.add(COMMAND_LS_REFS); + boolean advertiseRefInWant = transferConfig.isAllowRefInWant() + && db.getConfig().getBoolean("uploadpack", null, + "advertiserefinwant", true); + caps.add(COMMAND_FETCH + '=' + + (transferConfig.isAllowFilter() ? OPTION_FILTER + ' ' : "") + + (advertiseRefInWant ? CAPABILITY_REF_IN_WANT + ' ' : "") + + (transferConfig.isAdvertiseSidebandAll() + ? OPTION_SIDEBAND_ALL + ' ' + : "") + + (cachedPackUriProvider != null ? "packfile-uris " : "") + + (transferConfig.isAdvertiseWaitForDone() + ? OPTION_WAIT_FOR_DONE + ' ' + : "") + + OPTION_SHALLOW); + caps.add(CAPABILITY_SERVER_OPTION); + if (transferConfig.isAllowReceiveClientSID()) { + caps.add(OPTION_SESSION_ID); + } + if (transferConfig.isAdvertiseObjectInfo()) { + caps.add(COMMAND_OBJECT_INFO); } + caps.add(OPTION_AGENT + "=" + UserAgent.get()); - if (sendPack) - sendPack(); + return caps; + } + + private void serviceV2(PacketLineOut pckOut) throws IOException { + if (biDirectionalPipe) { + // Just like in service(), the capability advertisement + // is sent only if this is a bidirectional pipe. (If + // not, the client is expected to call + // sendAdvertisedRefs() on its own.) + protocolV2Hook + .onCapabilities(CapabilitiesV2Request.builder().build()); + for (String s : getV2CapabilityAdvertisement()) { + pckOut.writeString(s + '\n'); + } + pckOut.end(); + + while (!serveOneCommandV2(pckOut)) { + // Repeat until an empty command or EOF. + } + return; + } + + try { + serveOneCommandV2(pckOut); + } finally { + while (0 < rawIn.skip(2048) || 0 <= rawIn.read()) { + // Discard until EOF. + } + rawOut.stopBuffering(); + } } private static Set<ObjectId> refIdSet(Collection<Ref> refs) { - Set<ObjectId> ids = new HashSet<ObjectId>(refs.size()); + Set<ObjectId> ids = new HashSet<>(refs.size()); for (Ref ref : refs) { - if (ref.getObjectId() != null) - ids.add(ref.getObjectId()); + ObjectId id = ref.getObjectId(); + if (id != null) { + ids.add(id); + } + id = ref.getPeeledObjectId(); + if (id != null) { + ids.add(id); + } } return ids; } - private void reportErrorDuringNegotiate(String msg) { - try { - pckOut.writeString("ERR " + msg + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ - } catch (Throwable err) { - // Ignore this secondary failure. + /* + * Determines what object ids must be marked as shallow or unshallow for the + * client. + */ + private void computeShallowsAndUnshallows(FetchRequest req, + IOConsumer<ObjectId> shallowFunc, + IOConsumer<ObjectId> unshallowFunc, + List<ObjectId> deepenNots) + throws IOException { + if (req.getClientCapabilities().contains(OPTION_DEEPEN_RELATIVE)) { + // TODO(jonathantanmy): Implement deepen-relative + throw new UnsupportedOperationException(); } - } - private void processShallow() throws IOException { + int walkDepth = req.getDepth() == 0 ? Integer.MAX_VALUE + : req.getDepth() - 1; try (DepthWalk.RevWalk depthWalk = new DepthWalk.RevWalk( - walk.getObjectReader(), depth)) { + walk.getObjectReader(), walkDepth)) { + + depthWalk.setDeepenSince(req.getDeepenSince()); // Find all the commits which will be shallow - for (ObjectId o : wantIds) { + for (ObjectId o : req.getWantIds()) { try { depthWalk.markRoot(depthWalk.parseCommit(o)); } catch (IncorrectObjectTypeException notCommit) { @@ -749,24 +1473,67 @@ public class UploadPack { } } + depthWalk.setDeepenNots(deepenNots); + RevCommit o; + boolean atLeastOne = false; while ((o = depthWalk.next()) != null) { DepthWalk.Commit c = (DepthWalk.Commit) o; + atLeastOne = true; + + boolean isBoundary = (c.getDepth() == walkDepth) || c.isBoundary(); // Commits at the boundary which aren't already shallow in // the client need to be marked as such - if (c.getDepth() == depth && !clientShallowCommits.contains(c)) - pckOut.writeString("shallow " + o.name()); //$NON-NLS-1$ + if (isBoundary && !req.getClientShallowCommits().contains(c)) { + shallowFunc.accept(c.copy()); + } // Commits not on the boundary which are shallow in the client // need to become unshallowed - if (c.getDepth() < depth && clientShallowCommits.remove(c)) { - unshallowCommits.add(c.copy()); - pckOut.writeString("unshallow " + c.name()); //$NON-NLS-1$ + if (!isBoundary && req.getClientShallowCommits().remove(c)) { + unshallowFunc.accept(c.copy()); } } + if (!atLeastOne) { + throw new PackProtocolException( + JGitText.get().noCommitsSelectedForShallow); + } + } + } + + /* + * Verify all shallow lines refer to commits + * + * It can mutate the input set (removing missing object ids from it) + */ + private void verifyClientShallow(Set<ObjectId> shallowCommits) + throws IOException, PackProtocolException { + AsyncRevObjectQueue q = walk.parseAny(shallowCommits, true); + try { + for (;;) { + try { + // Shallow objects named by the client must be commits. + RevObject o = q.next(); + if (o == null) { + break; + } + if (!(o instanceof RevCommit)) { + throw new PackProtocolException( + MessageFormat.format( + JGitText.get().invalidShallowObject, + o.name())); + } + } catch (MissingObjectException notCommit) { + // shallow objects not known at the server are ignored + // by git-core upload-pack, match that behavior. + shallowCommits.remove(notCommit.getObjectId()); + continue; + } + } + } finally { + q.release(); } - pckOut.end(); } /** @@ -774,23 +1541,53 @@ public class UploadPack { * * @param adv * the advertisement formatter. - * @throws IOException - * the formatter failed to write an advertisement. - * @throws ServiceMayNotContinueException - * the hook denied advertisement. + * @throws java.io.IOException + * the formatter failed to write an advertisement. + * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException + * the hook denied advertisement. */ - public void sendAdvertisedRefs(final RefAdvertiser adv) throws IOException, + public void sendAdvertisedRefs(RefAdvertiser adv) throws IOException, ServiceMayNotContinueException { - try { - advertiseRefsHook.advertiseRefs(this); - } catch (ServiceMayNotContinueException fail) { - if (fail.getMessage() != null) { - adv.writeOne("ERR " + fail.getMessage()); //$NON-NLS-1$ - fail.setOutput(); + sendAdvertisedRefs(adv, null); + } + + /** + * Generate an advertisement of available refs and capabilities. + * + * @param adv + * the advertisement formatter. + * @param serviceName + * if not null, also output "# service=serviceName" followed by a + * flush packet before the advertisement. This is required + * in v0 of the HTTP protocol, described in Git's + * Documentation/technical/http-protocol.txt. + * @throws java.io.IOException + * the formatter failed to write an advertisement. + * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException + * the hook denied advertisement. + * @since 5.0 + */ + public void sendAdvertisedRefs(RefAdvertiser adv, + @Nullable String serviceName) throws IOException, + ServiceMayNotContinueException { + if (useProtocolV2()) { + // The equivalent in v2 is only the capabilities + // advertisement. + protocolV2Hook + .onCapabilities(CapabilitiesV2Request.builder().build()); + for (String s : getV2CapabilityAdvertisement()) { + adv.writeOne(s); } - throw fail; + adv.end(); + return; } + Map<String, Ref> advertisedOrDefaultRefs = getAdvertisedOrDefaultRefs(); + + if (serviceName != null) { + adv.writeOne("# service=" + serviceName + '\n'); //$NON-NLS-1$ + adv.end(); + } adv.init(db); adv.advertiseCapability(OPTION_INCLUDE_TAG); adv.advertiseCapability(OPTION_MULTI_ACK_DETAILED); @@ -804,15 +1601,18 @@ public class UploadPack { if (!biDirectionalPipe) adv.advertiseCapability(OPTION_NO_DONE); RequestPolicy policy = getRequestPolicy(); - if (policy == RequestPolicy.TIP - || policy == RequestPolicy.REACHABLE_COMMIT_TIP - || policy == null) + if (policy == null || policy.implies(RequestPolicy.TIP)) adv.advertiseCapability(OPTION_ALLOW_TIP_SHA1_IN_WANT); + if (policy == null || policy.implies(RequestPolicy.REACHABLE_COMMIT)) + adv.advertiseCapability(OPTION_ALLOW_REACHABLE_SHA1_IN_WANT); adv.advertiseCapability(OPTION_AGENT, UserAgent.get()); + if (transferConfig.isAllowFilter()) { + adv.advertiseCapability(OPTION_FILTER); + } adv.setDerefTags(true); - Map<String, Ref> refs = getAdvertisedOrDefaultRefs(); - findSymrefs(adv, refs); - advertised = adv.send(refs); + findSymrefs(adv, advertisedOrDefaultRefs); + advertised = adv.send(advertisedOrDefaultRefs.values()); + if (adv.isEmpty()) adv.advertiseId(ObjectId.zeroId(), "capabilities^{}"); //$NON-NLS-1$ adv.end(); @@ -831,13 +1631,15 @@ public class UploadPack { */ public void sendMessage(String what) { try { - msgOut.write(Constants.encode(what + "\n")); //$NON-NLS-1$ + msgOut.write(Constants.encode(what + '\n')); } catch (IOException e) { // Ignore write failures. } } /** + * Get an underlying stream for sending messages to the client + * * @return an underlying stream for sending messages to the client, or null. * @since 3.1 */ @@ -845,58 +1647,32 @@ public class UploadPack { return msgOut; } - private void recvWants() throws IOException { - boolean isFirst = true; - for (;;) { - String line; - try { - line = pckIn.readString(); - } catch (EOFException eof) { - if (isFirst) - break; - throw eof; - } - - if (line == PacketLineIn.END) - break; - - if (line.startsWith("deepen ")) { //$NON-NLS-1$ - depth = Integer.parseInt(line.substring(7)); - continue; - } - - if (line.startsWith("shallow ")) { //$NON-NLS-1$ - clientShallowCommits.add(ObjectId.fromString(line.substring(8))); - continue; - } - - if (!line.startsWith("want ") || line.length() < 45) //$NON-NLS-1$ - throw new PackProtocolException(MessageFormat.format(JGitText.get().expectedGot, "want", line)); //$NON-NLS-1$ - - if (isFirst) { - if (line.length() > 45) { - FirstLine firstLine = new FirstLine(line); - options = firstLine.getOptions(); - line = firstLine.getLine(); - } else - options = Collections.emptySet(); - } - - wantIds.add(ObjectId.fromString(line.substring(5))); - isFirst = false; - } - } - /** - * Returns the clone/fetch depth. Valid only after calling recvWants(). + * Returns the clone/fetch depth. Valid only after calling recvWants(). A + * depth of 1 means return only the wants. * * @return the depth requested by the client, or 0 if unbounded. * @since 4.0 */ public int getDepth() { - if (options == null) + if (currentRequest == null) + throw new RequestNotYetReadException(); + return currentRequest.getDepth(); + } + + /** + * Returns the filter spec for the current request. Valid only after + * calling recvWants(). This may be a no-op filter spec, but it won't be + * null. + * + * @return filter requested by the client + * @since 5.4 + */ + public final FilterSpec getFilterSpec() { + if (currentRequest == null) { throw new RequestNotYetReadException(); - return depth; + } + return currentRequest.getFilterSpec(); } /** @@ -915,14 +1691,36 @@ public class UploadPack { * @since 4.0 */ public String getPeerUserAgent() { - return UserAgent.getAgent(options, userAgent); + if (currentRequest != null && currentRequest.getAgent() != null) { + return currentRequest.getAgent(); + } + + return userAgent; + } + + /** + * Get the session ID if received from the client. + * + * @return The session ID if it has been received from the client. + * @since 6.4 + */ + @Nullable + public String getClientSID() { + if (currentRequest == null) { + return null; + } + + return currentRequest.getClientSID(); } - private boolean negotiate() throws IOException { + private boolean negotiate(FetchRequest req, + PackStatistics.Accumulator accumulator, + PacketLineOut pckOut) + throws IOException { okToGiveUp = Boolean.FALSE; ObjectId last = ObjectId.zeroId(); - List<ObjectId> peerHas = new ArrayList<ObjectId>(64); + List<ObjectId> peerHas = new ArrayList<>(64); for (;;) { String line; try { @@ -933,34 +1731,35 @@ public class UploadPack { // disconnected, and will try another request with actual want/have. // Don't report the EOF here, its a bug in the protocol that the client // just disconnects without sending an END. - if (!biDirectionalPipe && depth > 0) + if (!biDirectionalPipe && req.getDepth() > 0) return false; throw eof; } - if (line == PacketLineIn.END) { - last = processHaveLines(peerHas, last); + if (PacketLineIn.isEnd(line)) { + last = processHaveLines(peerHas, last, pckOut, accumulator, Option.NONE); if (commonBase.isEmpty() || multiAck != MultiAck.OFF) pckOut.writeString("NAK\n"); //$NON-NLS-1$ if (noDone && sentReady) { - pckOut.writeString("ACK " + last.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + pckOut.writeString(PACKET_ACK + last.name() + '\n'); return true; } if (!biDirectionalPipe) return false; pckOut.flush(); - } else if (line.startsWith("have ") && line.length() == 45) { //$NON-NLS-1$ - peerHas.add(ObjectId.fromString(line.substring(5))); - - } else if (line.equals("done")) { //$NON-NLS-1$ - last = processHaveLines(peerHas, last); + } else if (line.startsWith(PACKET_HAVE) + && line.length() == PACKET_HAVE.length() + 40) { + peerHas.add(ObjectId + .fromString(line.substring(PACKET_HAVE.length()))); + } else if (line.equals(PACKET_DONE)) { + last = processHaveLines(peerHas, last, pckOut, accumulator, Option.NONE); if (commonBase.isEmpty()) pckOut.writeString("NAK\n"); //$NON-NLS-1$ else if (multiAck != MultiAck.OFF) - pckOut.writeString("ACK " + last.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + pckOut.writeString(PACKET_ACK + last.name() + '\n'); return true; @@ -970,13 +1769,21 @@ public class UploadPack { } } - private ObjectId processHaveLines(List<ObjectId> peerHas, ObjectId last) + private enum Option { + WAIT_FOR_DONE, + NONE; + } + + private ObjectId processHaveLines(List<ObjectId> peerHas, ObjectId last, + PacketLineOut out, PackStatistics.Accumulator accumulator, + Option option) throws IOException { preUploadHook.onBeginNegotiateRound(this, wantIds, peerHas.size()); if (wantAll.isEmpty() && !wantIds.isEmpty()) - parseWants(); + parseWants(accumulator); if (peerHas.isEmpty()) return last; + accumulator.haves += peerHas.size(); sentReady = false; int haveCnt = 0; @@ -1014,14 +1821,15 @@ public class UploadPack { // switch (multiAck) { case OFF: - if (commonBase.size() == 1) - pckOut.writeString("ACK " + obj.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + if (commonBase.size() == 1) { + out.writeString(PACKET_ACK + obj.name() + '\n'); + } break; case CONTINUE: - pckOut.writeString("ACK " + obj.name() + " continue\n"); //$NON-NLS-1$ //$NON-NLS-2$ + out.writeString(PACKET_ACK + obj.name() + " continue\n"); //$NON-NLS-1$ break; case DETAILED: - pckOut.writeString("ACK " + obj.name() + " common\n"); //$NON-NLS-1$ //$NON-NLS-2$ + out.writeString(PACKET_ACK + obj.name() + " common\n"); //$NON-NLS-1$ break; } } @@ -1036,6 +1844,18 @@ public class UploadPack { // create a pack at this point, let the client know so it stops // telling us about its history. // + if (option != Option.WAIT_FOR_DONE) { + sentReady = shouldGiveUp(peerHas, out, missCnt); + } + + preUploadHook.onEndNegotiateRound(this, wantAll, haveCnt, missCnt, sentReady); + peerHas.clear(); + return last; + } + + private boolean shouldGiveUp(List<ObjectId> peerHas, PacketLineOut out, int missCnt) + throws IOException { + boolean readySent = false; boolean didOkToGiveUp = false; if (0 < missCnt) { for (int i = peerHas.size() - 1; i >= 0; i--) { @@ -1047,11 +1867,13 @@ public class UploadPack { case OFF: break; case CONTINUE: - pckOut.writeString("ACK " + id.name() + " continue\n"); //$NON-NLS-1$ //$NON-NLS-2$ + out.writeString( + PACKET_ACK + id.name() + " continue\n"); //$NON-NLS-1$ break; case DETAILED: - pckOut.writeString("ACK " + id.name() + " ready\n"); //$NON-NLS-1$ //$NON-NLS-2$ - sentReady = true; + out.writeString( + PACKET_ACK + id.name() + " ready\n"); //$NON-NLS-1$ + readySent = true; break; } } @@ -1060,30 +1882,37 @@ public class UploadPack { } } - if (multiAck == MultiAck.DETAILED && !didOkToGiveUp && okToGiveUp()) { + if (multiAck == MultiAck.DETAILED && !didOkToGiveUp + && okToGiveUp()) { ObjectId id = peerHas.get(peerHas.size() - 1); - sentReady = true; - pckOut.writeString("ACK " + id.name() + " ready\n"); //$NON-NLS-1$ //$NON-NLS-2$ - sentReady = true; + out.writeString(PACKET_ACK + id.name() + " ready\n"); //$NON-NLS-1$ + readySent = true; } - preUploadHook.onEndNegotiateRound(this, wantAll, haveCnt, missCnt, sentReady); - peerHas.clear(); - return last; + return readySent; } - private void parseWants() throws IOException { + private void parseWants(PackStatistics.Accumulator accumulator) throws IOException { List<ObjectId> notAdvertisedWants = null; for (ObjectId obj : wantIds) { if (!advertised.contains(obj)) { if (notAdvertisedWants == null) - notAdvertisedWants = new ArrayList<ObjectId>(); + notAdvertisedWants = new ArrayList<>(); notAdvertisedWants.add(obj); } } - if (notAdvertisedWants != null) + if (notAdvertisedWants != null) { + accumulator.notAdvertisedWants = notAdvertisedWants.size(); + + Instant startReachabilityChecking = Instant.now(); + requestValidator.checkWants(this, notAdvertisedWants); + accumulator.reachabilityCheckDuration = Duration + .between(startReachabilityChecking, Instant.now()) + .toMillis(); + } + AsyncRevObjectQueue q = walk.parseAny(wantIds, true); try { RevObject obj; @@ -1100,9 +1929,7 @@ public class UploadPack { } wantIds.clear(); } catch (MissingObjectException notFound) { - ObjectId id = notFound.getObjectId(); - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, id.name()), notFound); + throw new WantNotValidException(notFound.getObjectId(), notFound); } finally { q.release(); } @@ -1122,13 +1949,12 @@ public class UploadPack { */ public static final class AdvertisedRequestValidator implements RequestValidator { + @Override public void checkWants(UploadPack up, List<ObjectId> wants) throws PackProtocolException, IOException { - if (!up.isBiDirectionalPipe()) + if (!up.isBiDirectionalPipe() || !wants.isEmpty()) { new ReachableCommitRequestValidator().checkWants(up, wants); - else if (!wants.isEmpty()) - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, wants.iterator().next().name())); + } } } @@ -1139,10 +1965,10 @@ public class UploadPack { */ public static final class ReachableCommitRequestValidator implements RequestValidator { + @Override public void checkWants(UploadPack up, List<ObjectId> wants) throws PackProtocolException, IOException { - checkNotAdvertisedWants(up.getRevWalk(), wants, - refIdSet(up.getAdvertisedRefs().values())); + checkNotAdvertisedWants(up, wants, up.getAdvertisedRefs().values()); } } @@ -1152,17 +1978,17 @@ public class UploadPack { * @since 3.1 */ public static final class TipRequestValidator implements RequestValidator { + @Override public void checkWants(UploadPack up, List<ObjectId> wants) throws PackProtocolException, IOException { if (!up.isBiDirectionalPipe()) new ReachableCommitTipRequestValidator().checkWants(up, wants); else if (!wants.isEmpty()) { Set<ObjectId> refIds = - refIdSet(up.getRepository().getRefDatabase().getRefs(ALL).values()); + refIdSet(up.getRepository().getRefDatabase().getRefs()); for (ObjectId obj : wants) { if (!refIds.contains(obj)) - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, obj.name())); + throw new WantNotValidException(obj); } } } @@ -1175,10 +2001,11 @@ public class UploadPack { */ public static final class ReachableCommitTipRequestValidator implements RequestValidator { + @Override public void checkWants(UploadPack up, List<ObjectId> wants) throws PackProtocolException, IOException { - checkNotAdvertisedWants(up.getRevWalk(), wants, - refIdSet(up.getRepository().getRefDatabase().getRefs(ALL).values())); + checkNotAdvertisedWants(up, wants, + up.getRepository().getRefDatabase().getRefs()); } } @@ -1188,55 +2015,177 @@ public class UploadPack { * @since 3.1 */ public static final class AnyRequestValidator implements RequestValidator { + @Override public void checkWants(UploadPack up, List<ObjectId> wants) throws PackProtocolException, IOException { // All requests are valid. } } - private static void checkNotAdvertisedWants(RevWalk walk, - List<ObjectId> notAdvertisedWants, Set<ObjectId> reachableFrom) - throws MissingObjectException, IncorrectObjectTypeException, IOException { - // Walk the requested commits back to the provided set of commits. If any - // commit exists, a branch was deleted or rewound and the repository owner - // no longer exports that requested item. If the requested commit is merged - // into an advertised branch it will be marked UNINTERESTING and no commits - // return. + private static void checkNotAdvertisedWants(UploadPack up, + List<ObjectId> notAdvertisedWants, Collection<Ref> visibleRefs) + throws IOException { + + ObjectReader reader = up.getRevWalk().getObjectReader(); + Set<ObjectId> directlyVisibleObjects = refIdSet(visibleRefs); + List<ObjectId> nonTipWants = notAdvertisedWants.stream() + .filter(not(directlyVisibleObjects::contains)) + .collect(Collectors.toList()); + + try (RevWalk walk = new RevWalk(reader)) { + walk.setRetainBody(false); + // Missing "wants" throw exception here + List<RevObject> wantsAsObjs = objectIdsToRevObjects(walk, + nonTipWants); + List<RevCommit> wantsAsCommits = wantsAsObjs.stream() + .filter(obj -> obj instanceof RevCommit) + .map(obj -> (RevCommit) obj) + .collect(Collectors.toList()); + boolean allWantsAreCommits = wantsAsObjs.size() == wantsAsCommits + .size(); + boolean repoHasBitmaps = reader.getBitmapIndex() != null; + + if (!allWantsAreCommits) { + if (!repoHasBitmaps && !up.transferConfig.isAllowFilter()) { + // Checking unadvertised non-commits without bitmaps + // requires an expensive manual walk. Use allowFilter as an + // indication that the server operator is willing to pay + // this cost. Reject the request otherwise. + RevObject nonCommit = wantsAsObjs + .stream() + .filter(obj -> !(obj instanceof RevCommit)) + .limit(1) + .collect(Collectors.toList()).get(0); + throw new WantNotValidException(nonCommit, + new Exception("Cannot walk without bitmaps")); //$NON-NLS-1$ + } - AsyncRevObjectQueue q = walk.parseAny(notAdvertisedWants, true); - try { - RevObject obj; - while ((obj = q.next()) != null) { - if (!(obj instanceof RevCommit)) - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, obj.name())); - walk.markStart((RevCommit) obj); + try (ObjectWalk objWalk = walk.toObjectWalkWithSameObjects()) { + Stream<RevObject> startersAsObjs = importantRefsFirst(visibleRefs) + .map(UploadPack::refToObjectId) + .map(objId -> objectIdToRevObject(objWalk, objId)) + .filter(Objects::nonNull); // Ignore missing tips + + ObjectReachabilityChecker reachabilityChecker = reader + .createObjectReachabilityChecker(objWalk); + Optional<RevObject> unreachable = reachabilityChecker + .areAllReachable(wantsAsObjs, startersAsObjs); + if (unreachable.isPresent()) { + if (!repoHasBitmaps) { + throw new WantNotValidException( + unreachable.get(), new Exception( + "Retry with bitmaps enabled")); //$NON-NLS-1$ + } + throw new WantNotValidException(unreachable.get()); + } + } + return; + } + + // All wants are commits, we can use ReachabilityChecker + ReachabilityChecker reachabilityChecker = reader + .createReachabilityChecker(walk); + + Stream<RevCommit> reachableCommits = importantRefsFirst(visibleRefs) + .map(UploadPack::refToObjectId) + .map(objId -> objectIdToRevCommit(walk, objId)) + .filter(Objects::nonNull); // Ignore missing tips + + Optional<RevCommit> unreachable = reachabilityChecker + .areAllReachable(wantsAsCommits, reachableCommits); + if (unreachable.isPresent()) { + throw new WantNotValidException(unreachable.get()); } + } catch (MissingObjectException notFound) { - ObjectId id = notFound.getObjectId(); - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, id.name()), notFound); - } finally { - q.release(); + throw new WantNotValidException(notFound.getObjectId(), notFound); } - for (ObjectId id : reachableFrom) { - try { - walk.markUninteresting(walk.parseCommit(id)); - } catch (IncorrectObjectTypeException notCommit) { - continue; - } + } + + private static <T> Predicate<T> not(Predicate<T> t) { + return t.negate(); + } + + static Stream<Ref> importantRefsFirst( + Collection<Ref> visibleRefs) { + Predicate<Ref> startsWithRefsHeads = ref -> ref.getName() + .startsWith(Constants.R_HEADS); + Predicate<Ref> startsWithRefsTags = ref -> ref.getName() + .startsWith(Constants.R_TAGS); + Predicate<Ref> allOther = ref -> !startsWithRefsHeads.test(ref) + && !startsWithRefsTags.test(ref); + + return Stream.concat( + visibleRefs.stream().filter(startsWithRefsHeads), + Stream.concat( + visibleRefs.stream().filter(startsWithRefsTags), + visibleRefs.stream().filter(allOther))); + } + + private static ObjectId refToObjectId(Ref ref) { + return ref.getObjectId() != null ? ref.getObjectId() + : ref.getPeeledObjectId(); + } + + /** + * Translate an object id to a RevCommit. + * + * @param walk + * walk on the relevant object storae + * @param objectId + * Object Id + * @return RevCommit instance or null if the object is missing + */ + @Nullable + private static RevCommit objectIdToRevCommit(RevWalk walk, + ObjectId objectId) { + if (objectId == null) { + return null; + } + + try { + return walk.parseCommit(objectId); + } catch (IOException e) { + return null; + } + } + + /** + * Translate an object id to a RevObject. + * + * @param walk + * walk on the relevant object storage + * @param objectId + * Object Id + * @return RevObject instance or null if the object is missing + */ + @Nullable + private static RevObject objectIdToRevObject(RevWalk walk, + ObjectId objectId) { + if (objectId == null) { + return null; } - RevCommit bad = walk.next(); - if (bad != null) { - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, - bad.name())); + try { + return walk.parseAny(objectId); + } catch (IOException e) { + return null; + } + } + + // Resolve the ObjectIds into RevObjects. Any missing object raises an + // exception + private static List<RevObject> objectIdsToRevObjects(RevWalk walk, + Iterable<ObjectId> objectIds) + throws MissingObjectException, IOException { + List<RevObject> result = new ArrayList<>(); + for (ObjectId objectId : objectIds) { + result.add(walk.parseAny(objectId)); } - walk.reset(); + return result; } - private void addCommonBase(final RevObject o) { + private void addCommonBase(RevObject o) { if (!o.has(COMMON)) { o.add(COMMON); commonBase.add(o); @@ -1265,14 +2214,19 @@ public class UploadPack { } } - private boolean wantSatisfied(final RevObject want) throws IOException { + private boolean wantSatisfied(RevObject want) throws IOException { if (want.has(SATISFIED)) return true; + if (((RevCommit) want).getParentCount() == 0) { + want.add(SATISFIED); + return true; + } + walk.resetRetain(SAVE); walk.markStart((RevCommit) want); if (oldestTime != 0) - walk.setRevFilter(CommitTimeRevFilter.after(oldestTime * 1000L)); + walk.setRevFilter(CommitTimeRevFilter.after(Instant.ofEpochSecond(oldestTime))); for (;;) { final RevCommit c = walk.next(); if (c == null) @@ -1286,116 +2240,128 @@ public class UploadPack { return false; } - private void sendPack() throws IOException { - final boolean sideband = options.contains(OPTION_SIDE_BAND) - || options.contains(OPTION_SIDE_BAND_64K); - - if (!biDirectionalPipe) { - // Ensure the request was fully consumed. Any remaining input must - // be a protocol error. If we aren't at EOF the implementation is broken. - int eof = rawIn.read(); - if (0 <= eof) - throw new CorruptObjectException(MessageFormat.format( - JGitText.get().expectedEOFReceived, - "\\x" + Integer.toHexString(eof))); //$NON-NLS-1$ - } + /** + * Send the requested objects to the client. + * + * @param accumulator + * where to write statistics about the content of the pack. + * @param req + * request in process + * @param allTags + * refs to search for annotated tags to include in the pack if + * the {@link GitProtocolConstants#OPTION_INCLUDE_TAG} capability + * was requested. + * @param unshallowCommits + * shallow commits on the client that are now becoming unshallow + * @param deepenNots + * objects that the client specified using --shallow-exclude + * @param pckOut + * output writer + * @throws IOException + * if an error occurred while generating or writing the pack. + */ + private void sendPack(PackStatistics.Accumulator accumulator, + FetchRequest req, + @Nullable Collection<Ref> allTags, + List<ObjectId> unshallowCommits, + List<ObjectId> deepenNots, + PacketLineOut pckOut) throws IOException { + Set<String> caps = req.getClientCapabilities(); + boolean sideband = caps.contains(OPTION_SIDE_BAND) + || caps.contains(OPTION_SIDE_BAND_64K); if (sideband) { - try { - sendPack(true); - } catch (ServiceMayNotContinueException noPack) { - // This was already reported on (below). - throw noPack; - } catch (IOException err) { - if (reportInternalServerErrorOverSideband()) - throw new UploadPackInternalServerErrorException(err); - else - throw err; - } catch (RuntimeException err) { - if (reportInternalServerErrorOverSideband()) - throw new UploadPackInternalServerErrorException(err); - else - throw err; - } catch (Error err) { - if (reportInternalServerErrorOverSideband()) - throw new UploadPackInternalServerErrorException(err); - else - throw err; - } - } else { - sendPack(false); - } - } - - private boolean reportInternalServerErrorOverSideband() { - try { - @SuppressWarnings("resource" /* java 7 */) - SideBandOutputStream err = new SideBandOutputStream( - SideBandOutputStream.CH_ERROR, - SideBandOutputStream.SMALL_BUF, - rawOut); - err.write(Constants.encode(JGitText.get().internalServerError)); - err.flush(); - return true; - } catch (Throwable cannotReport) { - // Ignore the reason. This is a secondary failure. - return false; - } - } + errOut = new SideBandErrorWriter(); - private void sendPack(final boolean sideband) throws IOException { - ProgressMonitor pm = NullProgressMonitor.INSTANCE; - OutputStream packOut = rawOut; - - if (sideband) { int bufsz = SideBandOutputStream.SMALL_BUF; - if (options.contains(OPTION_SIDE_BAND_64K)) + if (req.getClientCapabilities().contains(OPTION_SIDE_BAND_64K)) { bufsz = SideBandOutputStream.MAX_BUF; + } + OutputStream packOut = new SideBandOutputStream( + SideBandOutputStream.CH_DATA, bufsz, rawOut); - packOut = new SideBandOutputStream(SideBandOutputStream.CH_DATA, - bufsz, rawOut); - if (!options.contains(OPTION_NO_PROGRESS)) { + ProgressMonitor pm = NullProgressMonitor.INSTANCE; + if (!req.getClientCapabilities().contains(OPTION_NO_PROGRESS)) { msgOut = new SideBandOutputStream( SideBandOutputStream.CH_PROGRESS, bufsz, rawOut); pm = new SideBandProgressMonitor(msgOut); } + + sendPack(pm, pckOut, packOut, req, accumulator, allTags, + unshallowCommits, deepenNots); + pckOut.end(); + } else { + sendPack(NullProgressMonitor.INSTANCE, pckOut, rawOut, req, + accumulator, allTags, unshallowCommits, deepenNots); } + } - try { - if (wantAll.isEmpty()) { - preUploadHook.onSendPack(this, wantIds, commonBase); - } else { - preUploadHook.onSendPack(this, wantAll, commonBase); - } - msgOut.flush(); - } catch (ServiceMayNotContinueException noPack) { - if (sideband && noPack.getMessage() != null) { - noPack.setOutput(); - @SuppressWarnings("resource" /* java 7 */) - SideBandOutputStream err = new SideBandOutputStream( - SideBandOutputStream.CH_ERROR, - SideBandOutputStream.SMALL_BUF, rawOut); - err.write(Constants.encode(noPack.getMessage())); - err.flush(); - } - throw noPack; + /** + * Send the requested objects to the client. + * + * @param pm + * progress monitor + * @param pckOut + * PacketLineOut that shares the output with packOut + * @param packOut + * packfile output + * @param req + * request being processed + * @param accumulator + * where to write statistics about the content of the pack. + * @param allTags + * refs to search for annotated tags to include in the pack if + * the {@link GitProtocolConstants#OPTION_INCLUDE_TAG} capability + * was requested. + * @param unshallowCommits + * shallow commits on the client that are now becoming unshallow + * @param deepenNots + * objects that the client specified using --shallow-exclude + * @throws IOException + * if an error occurred while generating or writing the pack. + */ + private void sendPack(ProgressMonitor pm, PacketLineOut pckOut, + OutputStream packOut, FetchRequest req, + PackStatistics.Accumulator accumulator, + @Nullable Collection<Ref> allTags, List<ObjectId> unshallowCommits, + List<ObjectId> deepenNots) throws IOException { + if (wantAll.isEmpty()) { + preUploadHook.onSendPack(this, wantIds, commonBase); + } else { + preUploadHook.onSendPack(this, wantAll, commonBase); } + msgOut.flush(); PackConfig cfg = packConfig; if (cfg == null) cfg = new PackConfig(db); - final PackWriter pw = new PackWriter(cfg, walk.getObjectReader()); + @SuppressWarnings("resource") // PackWriter is referenced in the finally + // block, and is closed there + final PackWriter pw = new PackWriter(cfg, walk.getObjectReader(), + accumulator); try { pw.setIndexDisabled(true); - pw.setUseCachedPacks(true); - pw.setUseBitmaps(depth == 0 && clientShallowCommits.isEmpty()); + if (req.getFilterSpec().isNoOp()) { + pw.setUseCachedPacks(true); + } else { + pw.setFilterSpec(req.getFilterSpec()); + pw.setUseCachedPacks(false); + } + pw.setUseBitmaps( + req.getDepth() == 0 + && req.getClientShallowCommits().isEmpty() + && req.getFilterSpec().getTreeDepthLimit() == -1); + pw.setClientShallowCommits(req.getClientShallowCommits()); pw.setReuseDeltaCommits(true); - pw.setDeltaBaseAsOffset(options.contains(OPTION_OFS_DELTA)); - pw.setThin(options.contains(OPTION_THIN_PACK)); + pw.setDeltaBaseAsOffset( + req.getClientCapabilities().contains(OPTION_OFS_DELTA)); + pw.setThin(req.getClientCapabilities().contains(OPTION_THIN_PACK)); pw.setReuseValidatingObjects(false); + // Objects named directly by references go at the beginning + // of the pack. if (commonBase.isEmpty() && refs != null) { - Set<ObjectId> tagTargets = new HashSet<ObjectId>(); + Set<ObjectId> tagTargets = new HashSet<>(); for (Ref ref : refs.values()) { if (ref.getPeeledObjectId() != null) tagTargets.add(ref.getPeeledObjectId()); @@ -1407,23 +2373,46 @@ public class UploadPack { pw.setTagTargets(tagTargets); } - if (depth > 0) - pw.setShallowPack(depth, unshallowCommits); + // Advertised objects and refs are not used from here on and can be + // cleared. + advertised = null; + refs = null; RevWalk rw = walk; + if (req.getDepth() > 0 || req.getDeepenSince() != 0 || !deepenNots.isEmpty()) { + int walkDepth = req.getDepth() == 0 ? Integer.MAX_VALUE + : req.getDepth() - 1; + pw.setShallowPack(req.getDepth(), unshallowCommits); + + // dw borrows the reader from walk which is closed by #close + @SuppressWarnings("resource") + DepthWalk.RevWalk dw = new DepthWalk.RevWalk( + walk.getObjectReader(), walkDepth); + dw.setDeepenSince(req.getDeepenSince()); + dw.setDeepenNots(deepenNots); + dw.assumeShallow(req.getClientShallowCommits()); + rw = dw; + } + if (wantAll.isEmpty()) { - pw.preparePack(pm, wantIds, commonBase); + pw.preparePack(pm, wantIds, commonBase, + req.getClientShallowCommits()); } else { walk.reset(); - ObjectWalk ow = walk.toObjectWalkWithSameObjects(); - pw.preparePack(pm, ow, wantAll, commonBase); + ObjectWalk ow = rw.toObjectWalkWithSameObjects(); + pw.preparePack(pm, ow, wantAll, commonBase, PackWriter.NONE); rw = ow; } - if (options.contains(OPTION_INCLUDE_TAG) && refs != null) { - for (Ref ref : refs.values()) { + if (req.getClientCapabilities().contains(OPTION_INCLUDE_TAG) + && allTags != null) { + for (Ref ref : allTags) { ObjectId objectId = ref.getObjectId(); + if (objectId == null) { + // skip unborn branch + continue; + } // If the object was already requested, skip it. if (wantAll.isEmpty()) { @@ -1436,18 +2425,41 @@ public class UploadPack { } if (!ref.isPeeled()) - ref = db.peel(ref); + ref = db.getRefDatabase().peel(ref); ObjectId peeledId = ref.getPeeledObjectId(); - if (peeledId == null) + objectId = ref.getObjectId(); + if (peeledId == null || objectId == null) continue; - objectId = ref.getObjectId(); - if (pw.willInclude(peeledId) && !pw.willInclude(objectId)) - pw.addObject(rw.parseAny(objectId)); + if (pw.willInclude(peeledId)) { + // We don't need to handle parseTag throwing an + // IncorrectObjectTypeException as we only reach + // here when ref is an annotated tag + addTagChain(rw.parseTag(objectId), pw); + } } } + if (pckOut.isUsingSideband()) { + if (req instanceof FetchV2Request && + cachedPackUriProvider != null && + !((FetchV2Request) req).getPackfileUriProtocols().isEmpty()) { + FetchV2Request reqV2 = (FetchV2Request) req; + pw.setPackfileUriConfig(new PackWriter.PackfileUriConfig( + pckOut, + reqV2.getPackfileUriProtocols(), + cachedPackUriProvider)); + } else { + // PackWriter will write "packfile-uris\n" and "packfile\n" + // for us if provided a PackfileUriConfig. In this case, we + // are not providing a PackfileUriConfig, so we have to + // write this line ourselves. + pckOut.writeString( + GitProtocolConstants.SECTION_PACKFILE + '\n'); + } + } + pw.enableSearchForReuseTimeout(); pw.writePack(pm, NullProgressMonitor.INSTANCE, packOut); if (msgOut != NullOutputStream.INSTANCE) { @@ -1458,20 +2470,116 @@ public class UploadPack { } finally { statistics = pw.getStatistics(); - if (statistics != null) - logger.onPackStatistics(statistics); + if (statistics != null) { + postUploadHook.onPostUpload(statistics); + } pw.close(); } - - if (sideband) - pckOut.end(); } - private void findSymrefs( + private static void findSymrefs( final RefAdvertiser adv, final Map<String, Ref> refs) { Ref head = refs.get(Constants.HEAD); if (head != null && head.isSymbolic()) { adv.addSymref(Constants.HEAD, head.getLeaf().getName()); } } + + private void addTagChain( + RevTag tag, PackWriter pw) throws IOException { + RevObject o = tag; + do { + tag = (RevTag) o; + walk.parseBody(tag); + if (!pw.willInclude(tag.getId())) { + pw.addObject(tag); + } + o = tag.getObject(); + } while (Constants.OBJ_TAG == o.getType()); + } + + private List<ObjectId> parseDeepenNots(List<String> deepenNots) + throws IOException { + List<ObjectId> result = new ArrayList<>(); + for (String s : deepenNots) { + if (ObjectId.isId(s)) { + result.add(ObjectId.fromString(s)); + } else { + Ref ref = findRef(s); + if (ref == null) { + throw new PackProtocolException(MessageFormat + .format(JGitText.get().invalidRefName, s)); + } + result.add(ref.getObjectId()); + } + } + return result; + } + + private static class ResponseBufferedOutputStream extends OutputStream { + private final OutputStream rawOut; + + private OutputStream out; + + ResponseBufferedOutputStream(OutputStream rawOut) { + this.rawOut = rawOut; + this.out = new ByteArrayOutputStream(); + } + + @Override + public void write(int b) throws IOException { + out.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + out.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void close() throws IOException { + out.close(); + } + + void stopBuffering() throws IOException { + if (out != rawOut) { + ((ByteArrayOutputStream) out).writeTo(rawOut); + out = rawOut; + } + } + } + + private interface ErrorWriter { + void writeError(String message) throws IOException; + } + + private class SideBandErrorWriter implements ErrorWriter { + @Override + public void writeError(String message) throws IOException { + @SuppressWarnings("resource" /* java 7 */) + SideBandOutputStream err = new SideBandOutputStream( + SideBandOutputStream.CH_ERROR, + SideBandOutputStream.SMALL_BUF, requireNonNull(rawOut)); + err.write(Constants.encode(message)); + err.flush(); + } + } + + private class PackProtocolErrorWriter implements ErrorWriter { + @Override + public void writeError(String message) throws IOException { + new PacketLineOut(requireNonNull(rawOut)) + .writeString(PACKET_ERR + message + '\n'); + } + } } |