![Shawn O. Pearce](https://secure.gravatar.com/avatar/a4611f1fb34714fc54ceec3859c490f7?d=identicon) Rewrite reference handling to be abstract and accurate
This commit actually does three major changes to the way references
are handled within JGit. Unfortunately they were easier to do as
a single massive commit than to break them up into smaller units.
Disambiguate symbolic references:
---------------------------------
Reporting a symbolic reference such as HEAD as though it were
any other normal reference like refs/heads/master causes subtle
programming errors. We have been bitten by this error on several
occasions, as have some downstream applications written by myself.
Instead of reporting HEAD as a reference whose name differs from
its "original name", report it as an actual SymbolicRef object
that the application can test the type and examine the target of.
With this change, Ref is now an abstract type with different
subclasses for the different types.
In the classical example of "HEAD" being a symbolic reference to
branch "refs/heads/master", the Repository.getAllRefs() method
will now return:
Map<String, Ref> all = repository.getAllRefs();
SymbolicRef HEAD = (SymbolicRef) all.get("HEAD");
ObjectIdRef master = (ObjectIdRef) all.get("refs/heads/master");
assertSame(master, HEAD.getTarget());
assertSame(master.getObjectId(), HEAD.getObjectId());
assertEquals("HEAD", HEAD.getName());
assertEquals("refs/heads/master", master.getName());
A nice side-effect of this change is the storage type of the
symbolic reference is no longer ambiguous with the storge type
of the underlying reference it targets. In the above example,
if master was only available in the packed-refs file, then the
following is also true:
assertSame(Ref.Storage.LOOSE, HEAD.getStorage());
assertSame(Ref.Storage.PACKED, master.getStorage());
(Prior to this change we returned the ambiguous storage of
LOOSE_PACKED for HEAD, which was confusing since it wasn't
actually true on disk).
Another nice side-effect of this change is all intermediate
symbolic references are preserved, and are therefore visible
to the application when they walk the target chain. We can
now correctly inspect chains of symbolic references.
As a result of this change the Ref.getOrigName() method has been
removed from the API. Applications should identify a symbolic
reference by testing for isSymbolic() and not by using an arcane
string comparsion between properties.
Abstract the RefDatabase storage:
---------------------------------
RefDatabase is now abstract, similar to ObjectDatabase, and a
new concrete implementation called RefDirectory is used for the
traditional on-disk storage layout. In the future we plan to
support additional implementations, such as a pure in-memory
RefDatabase for unit testing purposes.
Optimize RefDirectory:
----------------------
The implementation of the in-memory reference cache, reading, and
update routines has been completely rewritten. Much of the code
was heavily borrowed or cribbed from the prior implementation,
so copyright notices have been left intact as much as possible.
The RefDirectory cache no longer confuses symbolic references
with normal references. This permits the cache to resolve the
value of a symbolic reference as late as possible, ensuring it
is always current, without needing to maintain reverse pointers.
The cache is now 2 sorted RefLists, rather than 3 HashMaps.
Using sorted lists allows the implementation to reduce the
in-memory footprint when storing many refs. Using specialized
types for the elements allows the code to avoid additional map
lookups for auxiliary stat information.
To improve scan time during getRefs(), the lists are returned via
a copy-on-write contract. Most callers of getRefs() do not modify
the returned collections, so the copy-on-write semantics improves
access on repositories with a large number of packed references.
Iterator traversals of the returned Map<String,Ref> are performed
using a simple merge-join of the two cache lists, ensuring we can
perform the entire traversal in linear time as a function of the
number of references: O(PackedRefs + LooseRefs).
Scans of the loose reference space to update the cache run in
O(LooseRefs log LooseRefs) time, as the directory contents
are sorted before being merged against the in-memory cache.
Since the majority of stable references are kept packed, there
typically are only a handful of reference names to be sorted,
so the sorting cost should not be very high.
Locking is reduced during getRefs() by taking advantage of the
copy-on-write semantics of the improved cache data structure.
This permits concurrent readers to pull back references without
blocking each other. If there is contention updating the cache
during a scan, one or more updates are simply skipped and will
get picked up again in a future scan.
Writing to the $GIT_DIR/packed-refs during reference delete is
now fully atomic. The file is locked, reparsed fresh, and written
back out if a change is necessary. This avoids all race conditions
with concurrent external updates of the packed-refs file.
The RefLogWriter class has been fully folded into RefDirectory
and is therefore deleted. Maintaining the reference's log is
the responsiblity of the database implementation, and not all
implementations will use java.io for access.
Future work still remains to be done to abstract the ReflogReader
class away from local disk IO.
Change-Id: I26b9287c45a4b2d2be35ba2849daa316f5eec85d
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
14 years ago |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131 |
- /*
- * Copyright (C) 2008-2010, Google Inc.
- * and other copyright owners as documented in the project's IP log.
- *
- * 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
- *
- * 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.
- */
-
- package org.eclipse.jgit.transport;
-
- import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_DELETE_REFS;
- import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_OFS_DELTA;
- import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_REPORT_STATUS;
- import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_SIDE_BAND_64K;
- import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA;
- import static org.eclipse.jgit.transport.SideBandOutputStream.CH_PROGRESS;
- import static org.eclipse.jgit.transport.SideBandOutputStream.MAX_BUF;
-
- import java.io.EOFException;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.io.OutputStreamWriter;
- import java.io.Writer;
- import java.text.MessageFormat;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.HashSet;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
-
- import org.eclipse.jgit.JGitText;
- import org.eclipse.jgit.errors.MissingObjectException;
- import org.eclipse.jgit.errors.PackProtocolException;
- import org.eclipse.jgit.errors.UnpackException;
- import org.eclipse.jgit.lib.Config;
- import org.eclipse.jgit.lib.Constants;
- import org.eclipse.jgit.lib.NullProgressMonitor;
- import org.eclipse.jgit.lib.ObjectId;
- import org.eclipse.jgit.lib.ObjectIdSubclassMap;
- import org.eclipse.jgit.lib.PersonIdent;
- import org.eclipse.jgit.lib.Ref;
- import org.eclipse.jgit.lib.RefUpdate;
- import org.eclipse.jgit.lib.Repository;
- import org.eclipse.jgit.lib.Config.SectionParser;
- import org.eclipse.jgit.revwalk.ObjectWalk;
- import org.eclipse.jgit.revwalk.RevBlob;
- import org.eclipse.jgit.revwalk.RevCommit;
- import org.eclipse.jgit.revwalk.RevFlag;
- import org.eclipse.jgit.revwalk.RevObject;
- import org.eclipse.jgit.revwalk.RevTree;
- import org.eclipse.jgit.revwalk.RevWalk;
- import org.eclipse.jgit.storage.file.PackLock;
- import org.eclipse.jgit.transport.ReceiveCommand.Result;
- import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser;
- import org.eclipse.jgit.util.io.InterruptTimer;
- import org.eclipse.jgit.util.io.TimeoutInputStream;
- import org.eclipse.jgit.util.io.TimeoutOutputStream;
-
- /**
- * Implements the server side of a push connection, receiving objects.
- */
- public class ReceivePack {
- /** Database we write the stored objects into. */
- private final Repository db;
-
- /** Revision traversal support over {@link #db}. */
- private final RevWalk walk;
-
- /**
- * Is the client connection a bi-directional socket or pipe?
- * <p>
- * If true, this class assumes it can perform multiple read and write cycles
- * with the client over the input and output streams. This matches the
- * functionality available with a standard TCP/IP connection, or a local
- * operating system or in-memory pipe.
- * <p>
- * If false, this class runs in a read everything then output results mode,
- * making it suitable for single round-trip systems RPCs such as HTTP.
- */
- private boolean biDirectionalPipe = true;
-
- /** Should an incoming transfer validate objects? */
- private boolean checkReceivedObjects;
-
- /** Should an incoming transfer permit create requests? */
- private boolean allowCreates;
-
- /** Should an incoming transfer permit delete requests? */
- private boolean allowDeletes;
-
- /** Should an incoming transfer permit non-fast-forward requests? */
- private boolean allowNonFastForwards;
-
- private boolean allowOfsDelta;
-
- /** Identity to record action as within the reflog. */
- private PersonIdent refLogIdent;
-
- /** Filter used while advertising the refs to the client. */
- private RefFilter refFilter;
-
- /** Hook to validate the update commands before execution. */
- private PreReceiveHook preReceive;
-
- /** Hook to report on the commands after execution. */
- private PostReceiveHook postReceive;
-
- /** Timeout in seconds to wait for client interaction. */
- private int timeout;
-
- /** Timer to manage {@link #timeout}. */
- private InterruptTimer timer;
-
- private TimeoutInputStream timeoutIn;
-
- private InputStream rawIn;
-
- private OutputStream rawOut;
-
- private PacketLineIn pckIn;
-
- private PacketLineOut pckOut;
-
- private Writer msgs;
-
- private IndexPack ip;
-
- /** The refs we advertised as existing at the start of the connection. */
- private Map<String, Ref> refs;
-
- /** Capabilities requested by the client. */
- private Set<String> enabledCapablities;
-
- /** Commands to execute, as received by the client. */
- private List<ReceiveCommand> commands;
-
- /** Error to display instead of advertising the references. */
- private StringBuilder advertiseError;
-
- /** An exception caught while unpacking and fsck'ing the objects. */
- private Throwable unpackError;
-
- /** If {@link BasePackPushConnection#CAPABILITY_REPORT_STATUS} is enabled. */
- private boolean reportStatus;
-
- /** If {@link BasePackPushConnection#CAPABILITY_SIDE_BAND_64K} is enabled. */
- private boolean sideBand;
-
- /** Lock around the received pack file, while updating refs. */
- private PackLock packLock;
-
- private boolean checkReferencedIsReachable;
-
- /**
- * Create a new pack receive for an open repository.
- *
- * @param into
- * the destination repository.
- */
- public ReceivePack(final Repository into) {
- db = into;
- walk = new RevWalk(db);
-
- final ReceiveConfig cfg = db.getConfig().get(ReceiveConfig.KEY);
- checkReceivedObjects = cfg.checkReceivedObjects;
- allowCreates = cfg.allowCreates;
- allowDeletes = cfg.allowDeletes;
- allowNonFastForwards = cfg.allowNonFastForwards;
- allowOfsDelta = cfg.allowOfsDelta;
- refFilter = RefFilter.DEFAULT;
- preReceive = PreReceiveHook.NULL;
- postReceive = PostReceiveHook.NULL;
- }
-
- private static class ReceiveConfig {
- static final SectionParser<ReceiveConfig> KEY = new SectionParser<ReceiveConfig>() {
- public ReceiveConfig parse(final Config cfg) {
- return new ReceiveConfig(cfg);
- }
- };
-
- final boolean checkReceivedObjects;
-
- final boolean allowCreates;
-
- final boolean allowDeletes;
-
- final boolean allowNonFastForwards;
-
- final boolean allowOfsDelta;
-
- ReceiveConfig(final Config config) {
- checkReceivedObjects = config.getBoolean("receive", "fsckobjects",
- false);
- allowCreates = true;
- allowDeletes = !config.getBoolean("receive", "denydeletes", false);
- allowNonFastForwards = !config.getBoolean("receive",
- "denynonfastforwards", false);
- allowOfsDelta = config.getBoolean("repack", "usedeltabaseoffset",
- true);
- }
- }
-
- /** @return the repository this receive completes into. */
- public final Repository getRepository() {
- return db;
- }
-
- /** @return the RevWalk instance used by this connection. */
- public final RevWalk getRevWalk() {
- return walk;
- }
-
- /** @return all refs which were advertised to the client. */
- public final Map<String, Ref> getAdvertisedRefs() {
- return refs;
- }
-
- /**
- * @return true if this instance will validate all referenced, but not
- * supplied by the client, objects are reachable from another
- * reference.
- */
- public boolean isCheckReferencedObjectsAreReachable() {
- return checkReferencedIsReachable;
- }
-
- /**
- * Validate all referenced but not supplied objects are reachable.
- * <p>
- * If enabled, this instance will verify that references to objects not
- * contained within the received pack are already reachable through at least
- * one other reference selected by the {@link #getRefFilter()} and displayed
- * as part of {@link #getAdvertisedRefs()}.
- * <p>
- * This feature is useful when the application doesn't trust the client to
- * not provide a forged SHA-1 reference to an object, in an attempt to
- * access parts of the DAG that they aren't allowed to see and which have
- * been hidden from them via the configured {@link RefFilter}.
- * <p>
- * Enabling this feature may imply at least some, if not all, of the same
- * functionality performed by {@link #setCheckReceivedObjects(boolean)}.
- * Applications are encouraged to enable both features, if desired.
- *
- * @param b
- * {@code true} to enable the additional check.
- */
- public void setCheckReferencedObjectsAreReachable(boolean b) {
- this.checkReferencedIsReachable = b;
- }
-
- /**
- * @return true if this class expects a bi-directional pipe opened between
- * the client and itself. The default is true.
- */
- public boolean isBiDirectionalPipe() {
- return biDirectionalPipe;
- }
-
- /**
- * @param twoWay
- * if true, this class will assume the socket is a fully
- * bidirectional pipe between the two peers and takes advantage
- * of that by first transmitting the known refs, then waiting to
- * read commands. If false, this class assumes it must read the
- * commands before writing output and does not perform the
- * initial advertising.
- */
- public void setBiDirectionalPipe(final boolean twoWay) {
- biDirectionalPipe = twoWay;
- }
-
- /**
- * @return true if this instance will verify received objects are formatted
- * correctly. Validating objects requires more CPU time on this side
- * of the connection.
- */
- public boolean isCheckReceivedObjects() {
- return checkReceivedObjects;
- }
-
- /**
- * @param check
- * true to enable checking received objects; false to assume all
- * received objects are valid.
- */
- public void setCheckReceivedObjects(final boolean check) {
- checkReceivedObjects = check;
- }
-
- /** @return true if the client can request refs to be created. */
- public boolean isAllowCreates() {
- return allowCreates;
- }
-
- /**
- * @param canCreate
- * true to permit create ref commands to be processed.
- */
- public void setAllowCreates(final boolean canCreate) {
- allowCreates = canCreate;
- }
-
- /** @return true if the client can request refs to be deleted. */
- public boolean isAllowDeletes() {
- return allowDeletes;
- }
-
- /**
- * @param canDelete
- * true to permit delete ref commands to be processed.
- */
- public void setAllowDeletes(final boolean canDelete) {
- allowDeletes = canDelete;
- }
-
- /**
- * @return true if the client can request non-fast-forward updates of a ref,
- * possibly making objects unreachable.
- */
- public boolean isAllowNonFastForwards() {
- return allowNonFastForwards;
- }
-
- /**
- * @param canRewind
- * true to permit the client to ask for non-fast-forward updates
- * of an existing ref.
- */
- public void setAllowNonFastForwards(final boolean canRewind) {
- allowNonFastForwards = canRewind;
- }
-
- /** @return identity of the user making the changes in the reflog. */
- public PersonIdent getRefLogIdent() {
- return refLogIdent;
- }
-
- /**
- * Set the identity of the user appearing in the affected reflogs.
- * <p>
- * The timestamp portion of the identity is ignored. A new identity with the
- * current timestamp will be created automatically when the updates occur
- * and the log records are written.
- *
- * @param pi
- * identity of the user. If null the identity will be
- * automatically determined based on the repository
- * configuration.
- */
- public void setRefLogIdent(final PersonIdent pi) {
- refLogIdent = pi;
- }
-
- /** @return the filter used while advertising the refs to the client */
- public RefFilter getRefFilter() {
- return refFilter;
- }
-
- /**
- * Set the filter used while advertising the refs to the client.
- * <p>
- * Only refs allowed by this filter will be shown to the client.
- * Clients may still attempt to create or update a reference hidden
- * by the configured {@link RefFilter}. These attempts should be
- * rejected by a matching {@link PreReceiveHook}.
- *
- * @param refFilter
- * the filter; may be null to show all refs.
- */
- public void setRefFilter(final RefFilter refFilter) {
- this.refFilter = refFilter != null ? refFilter : RefFilter.DEFAULT;
- }
-
- /** @return get the hook invoked before updates occur. */
- public PreReceiveHook getPreReceiveHook() {
- return preReceive;
- }
-
- /**
- * Set the hook which is invoked prior to commands being executed.
- * <p>
- * Only valid commands (those which have no obvious errors according to the
- * received input and this instance's configuration) are passed into the
- * hook. The hook may mark a command with a result of any value other than
- * {@link Result#NOT_ATTEMPTED} to block its execution.
- * <p>
- * The hook may be called with an empty command collection if the current
- * set is completely invalid.
- *
- * @param h
- * the hook instance; may be null to disable the hook.
- */
- public void setPreReceiveHook(final PreReceiveHook h) {
- preReceive = h != null ? h : PreReceiveHook.NULL;
- }
-
- /** @return get the hook invoked after updates occur. */
- public PostReceiveHook getPostReceiveHook() {
- return postReceive;
- }
-
- /**
- * Set the hook which is invoked after commands are executed.
- * <p>
- * Only successful commands (type is {@link Result#OK}) are passed into the
- * hook. The hook may be called with an empty command collection if the
- * current set all resulted in an error.
- *
- * @param h
- * the hook instance; may be null to disable the hook.
- */
- public void setPostReceiveHook(final PostReceiveHook h) {
- postReceive = h != null ? h : PostReceiveHook.NULL;
- }
-
- /** @return timeout (in seconds) before aborting an IO operation. */
- public int getTimeout() {
- return timeout;
- }
-
- /**
- * Set the timeout before willing to abort an IO call.
- *
- * @param seconds
- * number of seconds to wait (with no data transfer occurring)
- * before aborting an IO read or write operation with the
- * connected client.
- */
- public void setTimeout(final int seconds) {
- timeout = seconds;
- }
-
- /** @return all of the command received by the current request. */
- public List<ReceiveCommand> getAllCommands() {
- return Collections.unmodifiableList(commands);
- }
-
- /**
- * Send an error message to the client.
- * <p>
- * If any error messages are sent before the references are advertised to
- * the client, the errors will be sent instead of the advertisement and the
- * receive operation will be aborted. All clients should receive and display
- * such early stage errors.
- * <p>
- * If the reference advertisements have already been sent, messages are sent
- * in a side channel. If the client doesn't support receiving messages, the
- * message will be discarded, with no other indication to the caller or to
- * the client.
- * <p>
- * {@link PreReceiveHook}s should always try to use
- * {@link ReceiveCommand#setResult(Result, String)} with a result status of
- * {@link Result#REJECTED_OTHER_REASON} to indicate any reasons for
- * rejecting an update. Messages attached to a command are much more likely
- * to be returned to the client.
- *
- * @param what
- * string describing the problem identified by the hook. The
- * string must not end with an LF, and must not contain an LF.
- */
- public void sendError(final String what) {
- if (refs == null) {
- if (advertiseError == null)
- advertiseError = new StringBuilder();
- advertiseError.append(what).append('\n');
- } else {
- try {
- if (msgs != null)
- msgs.write("error: " + what + "\n");
- } catch (IOException e) {
- // Ignore write failures.
- }
- }
- }
-
- /**
- * Send a message to the client, if it supports receiving them.
- * <p>
- * If the client doesn't support receiving messages, the message will be
- * discarded, with no other indication to the caller or to the client.
- *
- * @param what
- * string describing the problem identified by the hook. The
- * string must not end with an LF, and must not contain an LF.
- */
- public void sendMessage(final String what) {
- try {
- if (msgs != null)
- msgs.write(what + "\n");
- } catch (IOException e) {
- // Ignore write failures.
- }
- }
-
- /**
- * Execute the receive task on the socket.
- *
- * @param input
- * raw input to read client commands and pack data from. Caller
- * must ensure the input is buffered, otherwise read performance
- * may suffer.
- * @param output
- * response back to the Git network client. Caller must ensure
- * the output is buffered, otherwise write performance may
- * suffer.
- * @param messages
- * secondary "notice" channel to send additional messages out
- * 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 IOException
- */
- public void receive(final InputStream input, final OutputStream output,
- final OutputStream messages) throws IOException {
- try {
- rawIn = input;
- rawOut = output;
-
- if (timeout > 0) {
- final Thread caller = Thread.currentThread();
- timer = new InterruptTimer(caller.getName() + "-Timer");
- timeoutIn = new TimeoutInputStream(rawIn, timer);
- TimeoutOutputStream o = new TimeoutOutputStream(rawOut, timer);
- timeoutIn.setTimeout(timeout * 1000);
- o.setTimeout(timeout * 1000);
- rawIn = timeoutIn;
- rawOut = o;
- }
-
- pckIn = new PacketLineIn(rawIn);
- pckOut = new PacketLineOut(rawOut);
- if (messages != null)
- msgs = new OutputStreamWriter(messages, Constants.CHARSET);
-
- enabledCapablities = new HashSet<String>();
- commands = new ArrayList<ReceiveCommand>();
-
- service();
- } finally {
- walk.release();
- try {
- if (pckOut != null)
- pckOut.flush();
- if (msgs != null)
- msgs.flush();
-
- if (sideBand) {
- // If we are using side band, we need to send a final
- // flush-pkt to tell the remote peer the side band is
- // complete and it should stop decoding. We need to
- // use the original output stream as rawOut is now the
- // side band data channel.
- //
- new PacketLineOut(output).end();
- }
- } finally {
- unlockPack();
- timeoutIn = null;
- rawIn = null;
- rawOut = null;
- pckIn = null;
- pckOut = null;
- msgs = null;
- refs = null;
- enabledCapablities = null;
- commands = null;
- if (timer != null) {
- try {
- timer.terminate();
- } finally {
- timer = null;
- }
- }
- }
- }
- }
-
- private void service() throws IOException {
- if (biDirectionalPipe)
- sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut));
- else
- refs = refFilter.filter(db.getAllRefs());
- if (advertiseError != null)
- return;
- recvCommands();
- if (!commands.isEmpty()) {
- enableCapabilities();
-
- if (needPack()) {
- try {
- receivePack();
- if (needCheckConnectivity())
- checkConnectivity();
- ip = null;
- unpackError = null;
- } catch (IOException err) {
- unpackError = err;
- } catch (RuntimeException err) {
- unpackError = err;
- } catch (Error err) {
- unpackError = err;
- }
- }
-
- if (unpackError == null) {
- validateCommands();
- executeCommands();
- }
- unlockPack();
-
- if (reportStatus) {
- sendStatusReport(true, new Reporter() {
- void sendString(final String s) throws IOException {
- pckOut.writeString(s + "\n");
- }
- });
- pckOut.end();
- } else if (msgs != null) {
- sendStatusReport(false, new Reporter() {
- void sendString(final String s) throws IOException {
- msgs.write(s + "\n");
- }
- });
- }
-
- postReceive.onPostReceive(this, filterCommands(Result.OK));
-
- if (unpackError != null)
- throw new UnpackException(unpackError);
- }
- }
-
- private void unlockPack() {
- if (packLock != null) {
- packLock.unlock();
- packLock = null;
- }
- }
-
- /**
- * Generate an advertisement of available refs and capabilities.
- *
- * @param adv
- * the advertisement formatter.
- * @throws IOException
- * the formatter failed to write an advertisement.
- */
- public void sendAdvertisedRefs(final RefAdvertiser adv) throws IOException {
- if (advertiseError != null) {
- adv.writeOne("ERR " + advertiseError);
- return;
- }
-
- final RevFlag advertised = walk.newFlag("ADVERTISED");
- adv.init(walk, advertised);
- adv.advertiseCapability(CAPABILITY_SIDE_BAND_64K);
- adv.advertiseCapability(CAPABILITY_DELETE_REFS);
- adv.advertiseCapability(CAPABILITY_REPORT_STATUS);
- if (allowOfsDelta)
- adv.advertiseCapability(CAPABILITY_OFS_DELTA);
- refs = refFilter.filter(db.getAllRefs());
- final Ref head = refs.remove(Constants.HEAD);
- adv.send(refs);
- if (head != null && !head.isSymbolic())
- adv.advertiseHave(head.getObjectId());
- adv.includeAdditionalHaves(db);
- if (adv.isEmpty())
- adv.advertiseId(ObjectId.zeroId(), "capabilities^{}");
- adv.end();
- }
-
- private void recvCommands() throws IOException {
- for (;;) {
- String line;
- try {
- line = pckIn.readStringRaw();
- } catch (EOFException eof) {
- if (commands.isEmpty())
- return;
- throw eof;
- }
- if (line == PacketLineIn.END)
- break;
-
- if (commands.isEmpty()) {
- final int nul = line.indexOf('\0');
- if (nul >= 0) {
- for (String c : line.substring(nul + 1).split(" "))
- enabledCapablities.add(c);
- line = line.substring(0, nul);
- }
- }
-
- if (line.length() < 83) {
- final String m = JGitText.get().errorInvalidProtocolWantedOldNewRef;
- sendError(m);
- throw new PackProtocolException(m);
- }
-
- final ObjectId oldId = ObjectId.fromString(line.substring(0, 40));
- final ObjectId newId = ObjectId.fromString(line.substring(41, 81));
- final String name = line.substring(82);
- final ReceiveCommand cmd = new ReceiveCommand(oldId, newId, name);
- if (name.equals(Constants.HEAD)) {
- cmd.setResult(Result.REJECTED_CURRENT_BRANCH);
- } else {
- cmd.setRef(refs.get(cmd.getRefName()));
- }
- commands.add(cmd);
- }
- }
-
- private void enableCapabilities() {
- reportStatus = enabledCapablities.contains(CAPABILITY_REPORT_STATUS);
-
- sideBand = enabledCapablities.contains(CAPABILITY_SIDE_BAND_64K);
- if (sideBand) {
- OutputStream out = rawOut;
-
- rawOut = new SideBandOutputStream(CH_DATA, MAX_BUF, out);
- pckOut = new PacketLineOut(rawOut);
- msgs = new OutputStreamWriter(new SideBandOutputStream(CH_PROGRESS,
- MAX_BUF, out), Constants.CHARSET);
- }
- }
-
- private boolean needPack() {
- for (final ReceiveCommand cmd : commands) {
- if (cmd.getType() != ReceiveCommand.Type.DELETE)
- return true;
- }
- return false;
- }
-
- private void receivePack() throws IOException {
- // It might take the client a while to pack the objects it needs
- // to send to us. We should increase our timeout so we don't
- // abort while the client is computing.
- //
- if (timeoutIn != null)
- timeoutIn.setTimeout(10 * timeout * 1000);
-
- ip = IndexPack.create(db, rawIn);
- ip.setFixThin(true);
- ip.setNeedNewObjectIds(checkReferencedIsReachable);
- ip.setNeedBaseObjectIds(checkReferencedIsReachable);
- ip.setObjectChecking(isCheckReceivedObjects());
- ip.index(NullProgressMonitor.INSTANCE);
-
- String lockMsg = "jgit receive-pack";
- if (getRefLogIdent() != null)
- lockMsg += " from " + getRefLogIdent().toExternalString();
- packLock = ip.renameAndOpenPack(lockMsg);
-
- if (timeoutIn != null)
- timeoutIn.setTimeout(timeout * 1000);
- }
-
- private boolean needCheckConnectivity() {
- return isCheckReceivedObjects()
- || isCheckReferencedObjectsAreReachable();
- }
-
- private void checkConnectivity() throws IOException {
- ObjectIdSubclassMap<ObjectId> baseObjects = null;
- ObjectIdSubclassMap<ObjectId> providedObjects = null;
-
- if (checkReferencedIsReachable) {
- baseObjects = ip.getBaseObjectIds();
- providedObjects = ip.getNewObjectIds();
- }
- ip = null;
-
- final ObjectWalk ow = new ObjectWalk(db);
- for (final ReceiveCommand cmd : commands) {
- if (cmd.getResult() != Result.NOT_ATTEMPTED)
- continue;
- if (cmd.getType() == ReceiveCommand.Type.DELETE)
- continue;
- ow.markStart(ow.parseAny(cmd.getNewId()));
- }
- for (final Ref ref : refs.values()) {
- RevObject o = ow.parseAny(ref.getObjectId());
- ow.markUninteresting(o);
-
- if (checkReferencedIsReachable && !baseObjects.isEmpty()) {
- o = ow.peel(o);
- if (o instanceof RevCommit)
- o = ((RevCommit) o).getTree();
- if (o instanceof RevTree)
- ow.markUninteresting(o);
- }
- }
-
- if (checkReferencedIsReachable) {
- for (ObjectId id : baseObjects) {
- RevObject b = ow.lookupAny(id, Constants.OBJ_BLOB);
- if (!b.has(RevFlag.UNINTERESTING))
- throw new MissingObjectException(b, b.getType());
- }
- }
-
- RevCommit c;
- while ((c = ow.next()) != null) {
- if (checkReferencedIsReachable && !providedObjects.contains(c))
- throw new MissingObjectException(c, Constants.TYPE_COMMIT);
- }
-
- RevObject o;
- while ((o = ow.nextObject()) != null) {
- if (checkReferencedIsReachable) {
- if (providedObjects.contains(o))
- continue;
- else
- throw new MissingObjectException(o, o.getType());
- }
-
- if (o instanceof RevBlob && !db.hasObject(o))
- throw new MissingObjectException(o, Constants.TYPE_BLOB);
- }
- }
-
- private void validateCommands() {
- for (final ReceiveCommand cmd : commands) {
- final Ref ref = cmd.getRef();
- if (cmd.getResult() != Result.NOT_ATTEMPTED)
- continue;
-
- if (cmd.getType() == ReceiveCommand.Type.DELETE
- && !isAllowDeletes()) {
- // Deletes are not supported on this repository.
- //
- cmd.setResult(Result.REJECTED_NODELETE);
- continue;
- }
-
- if (cmd.getType() == ReceiveCommand.Type.CREATE) {
- if (!isAllowCreates()) {
- cmd.setResult(Result.REJECTED_NOCREATE);
- continue;
- }
-
- if (ref != null && !isAllowNonFastForwards()) {
- // Creation over an existing ref is certainly not going
- // to be a fast-forward update. We can reject it early.
- //
- cmd.setResult(Result.REJECTED_NONFASTFORWARD);
- continue;
- }
-
- if (ref != null) {
- // A well behaved client shouldn't have sent us a
- // create command for a ref we advertised to it.
- //
- cmd.setResult(Result.REJECTED_OTHER_REASON, "ref exists");
- continue;
- }
- }
-
- if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null
- && !ObjectId.zeroId().equals(cmd.getOldId())
- && !ref.getObjectId().equals(cmd.getOldId())) {
- // Delete commands can be sent with the old id matching our
- // advertised value, *OR* with the old id being 0{40}. Any
- // other requested old id is invalid.
- //
- cmd.setResult(Result.REJECTED_OTHER_REASON,
- JGitText.get().invalidOldIdSent);
- continue;
- }
-
- if (cmd.getType() == ReceiveCommand.Type.UPDATE) {
- if (ref == null) {
- // The ref must have been advertised in order to be updated.
- //
- cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().noSuchRef);
- continue;
- }
-
- if (!ref.getObjectId().equals(cmd.getOldId())) {
- // A properly functioning client will send the same
- // object id we advertised.
- //
- cmd.setResult(Result.REJECTED_OTHER_REASON,
- JGitText.get().invalidOldIdSent);
- continue;
- }
-
- // Is this possibly a non-fast-forward style update?
- //
- RevObject oldObj, newObj;
- try {
- oldObj = walk.parseAny(cmd.getOldId());
- } catch (IOException e) {
- cmd.setResult(Result.REJECTED_MISSING_OBJECT, cmd
- .getOldId().name());
- continue;
- }
-
- try {
- newObj = walk.parseAny(cmd.getNewId());
- } catch (IOException e) {
- cmd.setResult(Result.REJECTED_MISSING_OBJECT, cmd
- .getNewId().name());
- continue;
- }
-
- if (oldObj instanceof RevCommit && newObj instanceof RevCommit) {
- try {
- if (!walk.isMergedInto((RevCommit) oldObj,
- (RevCommit) newObj)) {
- cmd
- .setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD);
- }
- } catch (MissingObjectException e) {
- cmd.setResult(Result.REJECTED_MISSING_OBJECT, e
- .getMessage());
- } catch (IOException e) {
- cmd.setResult(Result.REJECTED_OTHER_REASON);
- }
- } else {
- cmd.setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD);
- }
- }
-
- if (!cmd.getRefName().startsWith(Constants.R_REFS)
- || !Repository.isValidRefName(cmd.getRefName())) {
- cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().funnyRefname);
- }
- }
- }
-
- private void executeCommands() {
- preReceive.onPreReceive(this, filterCommands(Result.NOT_ATTEMPTED));
- for (final ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED))
- execute(cmd);
- }
-
- private void execute(final ReceiveCommand cmd) {
- try {
- final RefUpdate ru = db.updateRef(cmd.getRefName());
- ru.setRefLogIdent(getRefLogIdent());
- switch (cmd.getType()) {
- case DELETE:
- if (!ObjectId.zeroId().equals(cmd.getOldId())) {
- // We can only do a CAS style delete if the client
- // didn't bork its delete request by sending the
- // wrong zero id rather than the advertised one.
- //
- ru.setExpectedOldObjectId(cmd.getOldId());
- }
- ru.setForceUpdate(true);
- status(cmd, ru.delete(walk));
- break;
-
- case CREATE:
- case UPDATE:
- case UPDATE_NONFASTFORWARD:
- ru.setForceUpdate(isAllowNonFastForwards());
- ru.setExpectedOldObjectId(cmd.getOldId());
- ru.setNewObjectId(cmd.getNewId());
- ru.setRefLogMessage("push", true);
- status(cmd, ru.update(walk));
- break;
- }
- } catch (IOException err) {
- cmd.setResult(Result.REJECTED_OTHER_REASON, MessageFormat.format(
- JGitText.get().lockError, err.getMessage()));
- }
- }
-
- private void status(final ReceiveCommand cmd, final RefUpdate.Result result) {
- switch (result) {
- case NOT_ATTEMPTED:
- cmd.setResult(Result.NOT_ATTEMPTED);
- break;
-
- case LOCK_FAILURE:
- case IO_FAILURE:
- cmd.setResult(Result.LOCK_FAILURE);
- break;
-
- case NO_CHANGE:
- case NEW:
- case FORCED:
- case FAST_FORWARD:
- cmd.setResult(Result.OK);
- break;
-
- case REJECTED:
- cmd.setResult(Result.REJECTED_NONFASTFORWARD);
- break;
-
- case REJECTED_CURRENT_BRANCH:
- cmd.setResult(Result.REJECTED_CURRENT_BRANCH);
- break;
-
- default:
- cmd.setResult(Result.REJECTED_OTHER_REASON, result.name());
- break;
- }
- }
-
- private List<ReceiveCommand> filterCommands(final Result want) {
- final List<ReceiveCommand> r = new ArrayList<ReceiveCommand>(commands
- .size());
- for (final ReceiveCommand cmd : commands) {
- if (cmd.getResult() == want)
- r.add(cmd);
- }
- return r;
- }
-
- private void sendStatusReport(final boolean forClient, final Reporter out)
- throws IOException {
- if (unpackError != null) {
- out.sendString("unpack error " + unpackError.getMessage());
- if (forClient) {
- for (final ReceiveCommand cmd : commands) {
- out.sendString("ng " + cmd.getRefName()
- + " n/a (unpacker error)");
- }
- }
- return;
- }
-
- if (forClient)
- out.sendString("unpack ok");
- for (final ReceiveCommand cmd : commands) {
- if (cmd.getResult() == Result.OK) {
- if (forClient)
- out.sendString("ok " + cmd.getRefName());
- continue;
- }
-
- final StringBuilder r = new StringBuilder();
- r.append("ng ");
- r.append(cmd.getRefName());
- r.append(" ");
-
- switch (cmd.getResult()) {
- case NOT_ATTEMPTED:
- r.append("server bug; ref not processed");
- break;
-
- case REJECTED_NOCREATE:
- r.append("creation prohibited");
- break;
-
- case REJECTED_NODELETE:
- r.append("deletion prohibited");
- break;
-
- case REJECTED_NONFASTFORWARD:
- r.append("non-fast forward");
- break;
-
- case REJECTED_CURRENT_BRANCH:
- r.append("branch is currently checked out");
- break;
-
- case REJECTED_MISSING_OBJECT:
- if (cmd.getMessage() == null)
- r.append("missing object(s)");
- else if (cmd.getMessage().length() == Constants.OBJECT_ID_STRING_LENGTH)
- r.append("object " + cmd.getMessage() + " missing");
- else
- r.append(cmd.getMessage());
- break;
-
- case REJECTED_OTHER_REASON:
- if (cmd.getMessage() == null)
- r.append("unspecified reason");
- else
- r.append(cmd.getMessage());
- break;
-
- case LOCK_FAILURE:
- r.append("failed to lock");
- break;
-
- case OK:
- // We shouldn't have reached this case (see 'ok' case above).
- continue;
- }
- out.sendString(r.toString());
- }
- }
-
- static abstract class Reporter {
- abstract void sendString(String s) throws IOException;
- }
- }
|