Package org.eclipse.jgit.transport

Source Code of org.eclipse.jgit.transport.ReceivePack

/*
* 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.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 java.util.concurrent.TimeUnit;

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.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor;
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.RevSort;
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 OutputStream msgOut;

  private PacketLineIn pckIn;

  private PacketLineOut pckOut;

  private PackParser parser;

  /** The refs we advertised as existing at the start of the connection. */
  private Map<String, Ref> refs;

  /** All SHA-1s shown to the client, which can be possible edges. */
  private Set<ObjectId> advertisedHaves;

  /** 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;

  /** Git object size limit */
  private long maxObjectSizeLimit;

  /**
   * 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;
    advertisedHaves = new HashSet<ObjectId>();
  }

  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() {
    if (refs == null) {
      refs = refFilter.filter(db.getAllRefs());

      Ref head = refs.get(Constants.HEAD);
      if (head != null && head.isSymbolic())
        refs.remove(Constants.HEAD);

      for (Ref ref : refs.values()) {
        if (ref.getObjectId() != null)
          advertisedHaves.add(ref.getObjectId());
      }
      advertisedHaves.addAll(db.getAdditionalHaves());
    }
    return refs;
  }

  /** @return the set of objects advertised as present in this repository. */
  public final Set<ObjectId> getAdvertisedObjects() {
    getAdvertisedRefs();
    return advertisedHaves;
  }

  /**
   * @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;
  }

  /**
   * Set the maximum allowed Git object size.
   * <p>
   * If an object is larger than the given size the pack-parsing will throw an
   * exception aborting the receive-pack operation.
   *
   * @param limit
   *            the Git object size limit. If zero then there is not limit.
   */
  public void setMaxObjectSizeLimit(final long limit) {
    maxObjectSizeLimit = limit;
  }

  /** @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 (msgOut != null)
          msgOut.write(Constants.encode("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 (msgOut != null)
        msgOut.write(Constants.encode(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;
      msgOut = messages;

      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);
      pckOut.setFlushOnEnd(false);

      enabledCapablities = new HashSet<String>();
      commands = new ArrayList<ReceiveCommand>();

      service();
    } finally {
      walk.release();
      try {
        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.
          //
          ((SideBandOutputStream) msgOut).flushBuffer();
          ((SideBandOutputStream) rawOut).flushBuffer();

          PacketLineOut plo = new PacketLineOut(output);
          plo.setFlushOnEnd(false);
          plo.end();
        }

        if (biDirectionalPipe) {
          // If this was a native git connection, flush the pipe for
          // the caller. For smart HTTP we don't do this flush and
          // instead let the higher level HTTP servlet code do it.
          //
          if (!sideBand && msgOut != null)
            msgOut.flush();
          rawOut.flush();
        }
      } finally {
        unlockPack();
        timeoutIn = null;
        rawIn = null;
        rawOut = null;
        msgOut = null;
        pckIn = null;
        pckOut = 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));
      pckOut.flush();
    } else
      getAdvertisedRefs();
    if (advertiseError != null)
      return;
    recvCommands();
    if (!commands.isEmpty()) {
      enableCapabilities();

      if (needPack()) {
        try {
          receivePack();
          if (needCheckConnectivity())
            checkConnectivity();
          parser = 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 (msgOut != null) {
        sendStatusReport(false, new Reporter() {
          void sendString(final String s) throws IOException {
            msgOut.write(Constants.encode(s + "\n"));
          }
        });
      }

      postReceive.onPostReceive(this, filterCommands(Result.OK));

      if (unpackError != null)
        throw new UnpackException(unpackError);
    }
  }

  private void unlockPack() throws IOException {
    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;
    }

    adv.init(db);
    adv.advertiseCapability(CAPABILITY_SIDE_BAND_64K);
    adv.advertiseCapability(CAPABILITY_DELETE_REFS);
    adv.advertiseCapability(CAPABILITY_REPORT_STATUS);
    if (allowOfsDelta)
      adv.advertiseCapability(CAPABILITY_OFS_DELTA);
    adv.send(getAdvertisedRefs());
    for (ObjectId obj : advertisedHaves)
      adv.advertiseHave(obj);
    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);
      msgOut = new SideBandOutputStream(CH_PROGRESS, MAX_BUF, out);

      pckOut = new PacketLineOut(rawOut);
      pckOut.setFlushOnEnd(false);
    }
  }

  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);

    ProgressMonitor receiving = NullProgressMonitor.INSTANCE;
    ProgressMonitor resolving = NullProgressMonitor.INSTANCE;
    if (sideBand)
      resolving = new SideBandProgressMonitor(msgOut);

    ObjectInserter ins = db.newObjectInserter();
    try {
      String lockMsg = "jgit receive-pack";
      if (getRefLogIdent() != null)
        lockMsg += " from " + getRefLogIdent().toExternalString();

      parser = ins.newPackParser(rawIn);
      parser.setAllowThin(true);
      parser.setNeedNewObjectIds(checkReferencedIsReachable);
      parser.setNeedBaseObjectIds(checkReferencedIsReachable);
      parser.setCheckEofAfterPackFooter(!biDirectionalPipe);
      parser.setObjectChecking(isCheckReceivedObjects());
      parser.setLockMessage(lockMsg);
      parser.setMaxObjectSizeLimit(maxObjectSizeLimit);
      packLock = parser.parse(receiving, resolving);
      ins.flush();
    } finally {
      ins.release();
    }

    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 = parser.getBaseObjectIds();
      providedObjects = parser.getNewObjectIds();
    }
    parser = null;

    final ObjectWalk ow = new ObjectWalk(db);
    ow.setRetainBody(false);
    if (checkReferencedIsReachable) {
      ow.sort(RevSort.TOPO);
      if (!baseObjects.isEmpty())
        ow.sort(RevSort.BOUNDARY, true);
    }

    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 ObjectId have : advertisedHaves) {
      RevObject o = ow.parseAny(have);
      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);
      }
    }

    RevCommit c;
    while ((c = ow.next()) != null) {
      if (checkReferencedIsReachable //
          && !c.has(RevFlag.UNINTERESTING) //
          && !providedObjects.contains(c))
        throw new MissingObjectException(c, Constants.TYPE_COMMIT);
    }

    RevObject o;
    while ((o = ow.nextObject()) != null) {
      if (o.has(RevFlag.UNINTERESTING))
        continue;

      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);
    }

    if (checkReferencedIsReachable) {
      for (ObjectId id : baseObjects) {
        o = ow.parseAny(id);
        if (!o.has(RevFlag.UNINTERESTING))
          throw new MissingObjectException(o, o.getType());
      }
    }
  }

  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, MessageFormat
              .format(JGitText.get().refAlreadyExists, ref));
          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));

    List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED);
    ProgressMonitor updating = NullProgressMonitor.INSTANCE;
    if (sideBand) {
      SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut);
      pm.setDelayStart(250, TimeUnit.MILLISECONDS);
      updating = pm;
    }
    updating.beginTask(JGitText.get().updatingReferences, toApply.size());
    for (ReceiveCommand cmd : toApply) {
      updating.update(1);
      execute(cmd);
    }
    updating.endTask();
  }

  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;
  }
}
TOP

Related Classes of org.eclipse.jgit.transport.ReceivePack

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.