BasePackPushConnection.java

  1. /*
  2.  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  3.  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */

  11. package org.eclipse.jgit.transport;

  12. import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_ATOMIC;

  13. import java.io.IOException;
  14. import java.io.InputStream;
  15. import java.io.OutputStream;
  16. import java.text.MessageFormat;
  17. import java.util.Collection;
  18. import java.util.HashSet;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.Set;

  22. import org.eclipse.jgit.errors.NoRemoteRepositoryException;
  23. import org.eclipse.jgit.errors.NotSupportedException;
  24. import org.eclipse.jgit.errors.PackProtocolException;
  25. import org.eclipse.jgit.errors.TooLargeObjectInPackException;
  26. import org.eclipse.jgit.errors.TooLargePackException;
  27. import org.eclipse.jgit.errors.TransportException;
  28. import org.eclipse.jgit.internal.JGitText;
  29. import org.eclipse.jgit.internal.storage.pack.PackWriter;
  30. import org.eclipse.jgit.lib.ObjectId;
  31. import org.eclipse.jgit.lib.ProgressMonitor;
  32. import org.eclipse.jgit.lib.Ref;
  33. import org.eclipse.jgit.transport.RemoteRefUpdate.Status;

  34. /**
  35.  * Push implementation using the native Git pack transfer service.
  36.  * <p>
  37.  * This is the canonical implementation for transferring objects to the remote
  38.  * repository from the local repository by talking to the 'git-receive-pack'
  39.  * service. Objects are packed on the local side into a pack file and then sent
  40.  * to the remote repository.
  41.  * <p>
  42.  * This connection requires only a bi-directional pipe or socket, and thus is
  43.  * easily wrapped up into a local process pipe, anonymous TCP socket, or a
  44.  * command executed through an SSH tunnel.
  45.  * <p>
  46.  * This implementation honors
  47.  * {@link org.eclipse.jgit.transport.Transport#isPushThin()} option.
  48.  * <p>
  49.  * Concrete implementations should just call
  50.  * {@link #init(java.io.InputStream, java.io.OutputStream)} and
  51.  * {@link #readAdvertisedRefs()} methods in constructor or before any use. They
  52.  * should also handle resources releasing in {@link #close()} method if needed.
  53.  */
  54. public abstract class BasePackPushConnection extends BasePackConnection implements
  55.         PushConnection {
  56.     /**
  57.      * The client expects a status report after the server processes the pack.
  58.      * @since 2.0
  59.      */
  60.     public static final String CAPABILITY_REPORT_STATUS = GitProtocolConstants.CAPABILITY_REPORT_STATUS;

  61.     /**
  62.      * The server supports deleting refs.
  63.      * @since 2.0
  64.      */
  65.     public static final String CAPABILITY_DELETE_REFS = GitProtocolConstants.CAPABILITY_DELETE_REFS;

  66.     /**
  67.      * The server supports packs with OFS deltas.
  68.      * @since 2.0
  69.      */
  70.     public static final String CAPABILITY_OFS_DELTA = GitProtocolConstants.CAPABILITY_OFS_DELTA;

  71.     /**
  72.      * The client supports using the 64K side-band for progress messages.
  73.      * @since 2.0
  74.      */
  75.     public static final String CAPABILITY_SIDE_BAND_64K = GitProtocolConstants.CAPABILITY_SIDE_BAND_64K;

  76.     /**
  77.      * The server supports the receiving of push options.
  78.      * @since 4.5
  79.      */
  80.     public static final String CAPABILITY_PUSH_OPTIONS = GitProtocolConstants.CAPABILITY_PUSH_OPTIONS;

  81.     private final boolean thinPack;
  82.     private final boolean atomic;

  83.     /** A list of option strings associated with this push. */
  84.     private List<String> pushOptions;

  85.     private boolean capableAtomic;
  86.     private boolean capableDeleteRefs;
  87.     private boolean capableReport;
  88.     private boolean capableSideBand;
  89.     private boolean capableOfsDelta;
  90.     private boolean capablePushOptions;

  91.     private boolean sentCommand;
  92.     private boolean writePack;

  93.     /** Time in milliseconds spent transferring the pack data. */
  94.     private long packTransferTime;

  95.     /**
  96.      * Create a new connection to push using the native git transport.
  97.      *
  98.      * @param packTransport
  99.      *            the transport.
  100.      */
  101.     public BasePackPushConnection(PackTransport packTransport) {
  102.         super(packTransport);
  103.         thinPack = transport.isPushThin();
  104.         atomic = transport.isPushAtomic();
  105.         pushOptions = transport.getPushOptions();
  106.     }

  107.     /** {@inheritDoc} */
  108.     @Override
  109.     public void push(final ProgressMonitor monitor,
  110.             final Map<String, RemoteRefUpdate> refUpdates)
  111.             throws TransportException {
  112.         push(monitor, refUpdates, null);
  113.     }

  114.     /** {@inheritDoc} */
  115.     @Override
  116.     public void push(final ProgressMonitor monitor,
  117.             final Map<String, RemoteRefUpdate> refUpdates, OutputStream outputStream)
  118.             throws TransportException {
  119.         markStartedOperation();
  120.         doPush(monitor, refUpdates, outputStream);
  121.     }

  122.     /** {@inheritDoc} */
  123.     @Override
  124.     protected TransportException noRepository() {
  125.         // Sadly we cannot tell the "invalid URI" case from "push not allowed".
  126.         // Opening a fetch connection can help us tell the difference, as any
  127.         // useful repository is going to support fetch if it also would allow
  128.         // push. So if fetch throws NoRemoteRepositoryException we know the
  129.         // URI is wrong. Otherwise we can correctly state push isn't allowed
  130.         // as the fetch connection opened successfully.
  131.         //
  132.         try {
  133.             transport.openFetch().close();
  134.         } catch (NotSupportedException e) {
  135.             // Fall through.
  136.         } catch (NoRemoteRepositoryException e) {
  137.             // Fetch concluded the repository doesn't exist.
  138.             //
  139.             return e;
  140.         } catch (TransportException e) {
  141.             // Fall through.
  142.         }
  143.         return new TransportException(uri, JGitText.get().pushNotPermitted);
  144.     }

  145.     /**
  146.      * Push one or more objects and update the remote repository.
  147.      *
  148.      * @param monitor
  149.      *            progress monitor to receive status updates.
  150.      * @param refUpdates
  151.      *            update commands to be applied to the remote repository.
  152.      * @param outputStream
  153.      *            output stream to write sideband messages to
  154.      * @throws org.eclipse.jgit.errors.TransportException
  155.      *             if any exception occurs.
  156.      * @since 3.0
  157.      */
  158.     protected void doPush(final ProgressMonitor monitor,
  159.             final Map<String, RemoteRefUpdate> refUpdates,
  160.             OutputStream outputStream) throws TransportException {
  161.         try {
  162.             writeCommands(refUpdates.values(), monitor, outputStream);

  163.             if (pushOptions != null && capablePushOptions)
  164.                 transmitOptions();
  165.             if (writePack)
  166.                 writePack(refUpdates, monitor);
  167.             if (sentCommand) {
  168.                 if (capableReport)
  169.                     readStatusReport(refUpdates);
  170.                 if (capableSideBand) {
  171.                     // Ensure the data channel is at EOF, so we know we have
  172.                     // read all side-band data from all channels and have a
  173.                     // complete copy of the messages (if any) buffered from
  174.                     // the other data channels.
  175.                     //
  176.                     int b = in.read();
  177.                     if (0 <= b)
  178.                         throw new TransportException(uri, MessageFormat.format(
  179.                                 JGitText.get().expectedEOFReceived,
  180.                                 Character.valueOf((char) b)));
  181.                 }
  182.             }
  183.         } catch (TransportException e) {
  184.             throw e;
  185.         } catch (Exception e) {
  186.             throw new TransportException(uri, e.getMessage(), e);
  187.         } finally {
  188.             close();
  189.         }
  190.     }

  191.     private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
  192.             final ProgressMonitor monitor, OutputStream outputStream) throws IOException {
  193.         final String capabilities = enableCapabilities(monitor, outputStream);
  194.         if (atomic && !capableAtomic) {
  195.             throw new TransportException(uri,
  196.                     JGitText.get().atomicPushNotSupported);
  197.         }

  198.         if (pushOptions != null && !capablePushOptions) {
  199.             throw new TransportException(uri,
  200.                     MessageFormat.format(JGitText.get().pushOptionsNotSupported,
  201.                             pushOptions.toString()));
  202.         }

  203.         for (RemoteRefUpdate rru : refUpdates) {
  204.             if (!capableDeleteRefs && rru.isDelete()) {
  205.                 rru.setStatus(Status.REJECTED_NODELETE);
  206.                 continue;
  207.             }

  208.             final StringBuilder sb = new StringBuilder();
  209.             ObjectId oldId = rru.getExpectedOldObjectId();
  210.             if (oldId == null) {
  211.                 final Ref advertised = getRef(rru.getRemoteName());
  212.                 oldId = advertised != null ? advertised.getObjectId() : null;
  213.                 if (oldId == null) {
  214.                     oldId = ObjectId.zeroId();
  215.                 }
  216.             }
  217.             sb.append(oldId.name());
  218.             sb.append(' ');
  219.             sb.append(rru.getNewObjectId().name());
  220.             sb.append(' ');
  221.             sb.append(rru.getRemoteName());
  222.             if (!sentCommand) {
  223.                 sentCommand = true;
  224.                 sb.append(capabilities);
  225.             }

  226.             pckOut.writeString(sb.toString());
  227.             rru.setStatus(Status.AWAITING_REPORT);
  228.             if (!rru.isDelete())
  229.                 writePack = true;
  230.         }

  231.         if (monitor.isCancelled())
  232.             throw new TransportException(uri, JGitText.get().pushCancelled);
  233.         pckOut.end();
  234.         outNeedsEnd = false;
  235.     }

  236.     private void transmitOptions() throws IOException {
  237.         for (String pushOption : pushOptions) {
  238.             pckOut.writeString(pushOption);
  239.         }

  240.         pckOut.end();
  241.     }

  242.     private String enableCapabilities(final ProgressMonitor monitor,
  243.             OutputStream outputStream) {
  244.         final StringBuilder line = new StringBuilder();
  245.         if (atomic)
  246.             capableAtomic = wantCapability(line, CAPABILITY_ATOMIC);
  247.         capableReport = wantCapability(line, CAPABILITY_REPORT_STATUS);
  248.         capableDeleteRefs = wantCapability(line, CAPABILITY_DELETE_REFS);
  249.         capableOfsDelta = wantCapability(line, CAPABILITY_OFS_DELTA);

  250.         if (pushOptions != null) {
  251.             capablePushOptions = wantCapability(line, CAPABILITY_PUSH_OPTIONS);
  252.         }

  253.         capableSideBand = wantCapability(line, CAPABILITY_SIDE_BAND_64K);
  254.         if (capableSideBand) {
  255.             in = new SideBandInputStream(in, monitor, getMessageWriter(),
  256.                     outputStream);
  257.             pckIn = new PacketLineIn(in);
  258.         }
  259.         addUserAgentCapability(line);

  260.         if (line.length() > 0)
  261.             line.setCharAt(0, '\0');
  262.         return line.toString();
  263.     }

  264.     private void writePack(final Map<String, RemoteRefUpdate> refUpdates,
  265.             final ProgressMonitor monitor) throws IOException {
  266.         Set<ObjectId> remoteObjects = new HashSet<>();
  267.         Set<ObjectId> newObjects = new HashSet<>();

  268.         try (PackWriter writer = new PackWriter(transport.getPackConfig(),
  269.                 local.newObjectReader())) {

  270.             for (Ref r : getRefs()) {
  271.                 // only add objects that we actually have
  272.                 ObjectId oid = r.getObjectId();
  273.                 if (local.getObjectDatabase().has(oid))
  274.                     remoteObjects.add(oid);
  275.             }
  276.             remoteObjects.addAll(additionalHaves);
  277.             for (RemoteRefUpdate r : refUpdates.values()) {
  278.                 if (!ObjectId.zeroId().equals(r.getNewObjectId()))
  279.                     newObjects.add(r.getNewObjectId());
  280.             }

  281.             writer.setIndexDisabled(true);
  282.             writer.setUseCachedPacks(true);
  283.             writer.setUseBitmaps(true);
  284.             writer.setThin(thinPack);
  285.             writer.setReuseValidatingObjects(false);
  286.             writer.setDeltaBaseAsOffset(capableOfsDelta);
  287.             writer.preparePack(monitor, newObjects, remoteObjects);

  288.             OutputStream packOut = out;
  289.             if (capableSideBand) {
  290.                 packOut = new CheckingSideBandOutputStream(in, out);
  291.             }
  292.             writer.writePack(monitor, monitor, packOut);

  293.             packTransferTime = writer.getStatistics().getTimeWriting();
  294.         }
  295.     }

  296.     private void readStatusReport(Map<String, RemoteRefUpdate> refUpdates)
  297.             throws IOException {
  298.         final String unpackLine = readStringLongTimeout();
  299.         if (!unpackLine.startsWith("unpack ")) //$NON-NLS-1$
  300.             throw new PackProtocolException(uri, MessageFormat
  301.                     .format(JGitText.get().unexpectedReportLine, unpackLine));
  302.         final String unpackStatus = unpackLine.substring("unpack ".length()); //$NON-NLS-1$
  303.         if (unpackStatus.startsWith("error Pack exceeds the limit of")) {//$NON-NLS-1$
  304.             throw new TooLargePackException(uri,
  305.                     unpackStatus.substring("error ".length())); //$NON-NLS-1$
  306.         } else if (unpackStatus.startsWith("error Object too large")) {//$NON-NLS-1$
  307.             throw new TooLargeObjectInPackException(uri,
  308.                     unpackStatus.substring("error ".length())); //$NON-NLS-1$
  309.         } else if (!unpackStatus.equals("ok")) { //$NON-NLS-1$
  310.             throw new TransportException(uri, MessageFormat.format(
  311.                     JGitText.get().errorOccurredDuringUnpackingOnTheRemoteEnd, unpackStatus));
  312.         }

  313.         for (String refLine : pckIn.readStrings()) {
  314.             boolean ok = false;
  315.             int refNameEnd = -1;
  316.             if (refLine.startsWith("ok ")) { //$NON-NLS-1$
  317.                 ok = true;
  318.                 refNameEnd = refLine.length();
  319.             } else if (refLine.startsWith("ng ")) { //$NON-NLS-1$
  320.                 ok = false;
  321.                 refNameEnd = refLine.indexOf(' ', 3);
  322.             }
  323.             if (refNameEnd == -1)
  324.                 throw new PackProtocolException(MessageFormat.format(JGitText.get().unexpectedReportLine2
  325.                         , uri, refLine));
  326.             final String refName = refLine.substring(3, refNameEnd);
  327.             final String message = (ok ? null : refLine
  328.                     .substring(refNameEnd + 1));

  329.             final RemoteRefUpdate rru = refUpdates.get(refName);
  330.             if (rru == null)
  331.                 throw new PackProtocolException(MessageFormat.format(JGitText.get().unexpectedRefReport, uri, refName));
  332.             if (ok) {
  333.                 rru.setStatus(Status.OK);
  334.             } else {
  335.                 rru.setStatus(Status.REJECTED_OTHER_REASON);
  336.                 rru.setMessage(message);
  337.             }
  338.         }
  339.         for (RemoteRefUpdate rru : refUpdates.values()) {
  340.             if (rru.getStatus() == Status.AWAITING_REPORT)
  341.                 throw new PackProtocolException(MessageFormat.format(
  342.                         JGitText.get().expectedReportForRefNotReceived , uri, rru.getRemoteName()));
  343.         }
  344.     }

  345.     private String readStringLongTimeout() throws IOException {
  346.         if (timeoutIn == null)
  347.             return pckIn.readString();

  348.         // The remote side may need a lot of time to choke down the pack
  349.         // we just sent them. There may be many deltas that need to be
  350.         // resolved by the remote. Its hard to say how long the other
  351.         // end is going to be silent. Taking 10x the configured timeout
  352.         // or the time spent transferring the pack, whichever is larger,
  353.         // gives the other side some reasonable window to process the data,
  354.         // but this is just a wild guess.
  355.         //
  356.         final int oldTimeout = timeoutIn.getTimeout();
  357.         final int sendTime = (int) Math.min(packTransferTime, 28800000L);
  358.         try {
  359.             int timeout = 10 * Math.max(sendTime, oldTimeout);
  360.             timeoutIn.setTimeout((timeout < 0) ? Integer.MAX_VALUE : timeout);
  361.             return pckIn.readString();
  362.         } finally {
  363.             timeoutIn.setTimeout(oldTimeout);
  364.         }
  365.     }

  366.     /**
  367.      * Gets the list of option strings associated with this push.
  368.      *
  369.      * @return pushOptions
  370.      * @since 4.5
  371.      */
  372.     public List<String> getPushOptions() {
  373.         return pushOptions;
  374.     }

  375.     private static class CheckingSideBandOutputStream extends OutputStream {
  376.         private final InputStream in;
  377.         private final OutputStream out;

  378.         CheckingSideBandOutputStream(InputStream in, OutputStream out) {
  379.             this.in = in;
  380.             this.out = out;
  381.         }

  382.         @Override
  383.         public void write(int b) throws IOException {
  384.             write(new byte[] { (byte) b });
  385.         }

  386.         @Override
  387.         public void write(byte[] buf, int ptr, int cnt) throws IOException {
  388.             try {
  389.                 out.write(buf, ptr, cnt);
  390.             } catch (IOException e) {
  391.                 throw checkError(e);
  392.             }
  393.         }

  394.         @Override
  395.         public void flush() throws IOException {
  396.             try {
  397.                 out.flush();
  398.             } catch (IOException e) {
  399.                 throw checkError(e);
  400.             }
  401.         }

  402.         private IOException checkError(IOException e1) {
  403.             try {
  404.                 in.read();
  405.             } catch (TransportException e2) {
  406.                 return e2;
  407.             } catch (IOException e2) {
  408.                 return e1;
  409.             }
  410.             return e1;
  411.         }
  412.     }
  413. }