FetchProcess.java

  1. /*
  2.  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
  3.  * Copyright (C) 2008, 2020 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 java.nio.charset.StandardCharsets.UTF_8;
  13. import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
  14. import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
  15. import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
  16. import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;

  17. import java.io.File;
  18. import java.io.IOException;
  19. import java.io.OutputStreamWriter;
  20. import java.io.Writer;
  21. import java.text.MessageFormat;
  22. import java.util.ArrayList;
  23. import java.util.Collection;
  24. import java.util.Collections;
  25. import java.util.HashMap;
  26. import java.util.HashSet;
  27. import java.util.Iterator;
  28. import java.util.Map;
  29. import java.util.Set;
  30. import java.util.concurrent.TimeUnit;

  31. import org.eclipse.jgit.errors.MissingObjectException;
  32. import org.eclipse.jgit.errors.NotSupportedException;
  33. import org.eclipse.jgit.errors.TransportException;
  34. import org.eclipse.jgit.internal.JGitText;
  35. import org.eclipse.jgit.internal.storage.file.LockFile;
  36. import org.eclipse.jgit.internal.storage.file.PackLock;
  37. import org.eclipse.jgit.lib.BatchRefUpdate;
  38. import org.eclipse.jgit.lib.BatchingProgressMonitor;
  39. import org.eclipse.jgit.lib.Constants;
  40. import org.eclipse.jgit.lib.ObjectId;
  41. import org.eclipse.jgit.lib.ObjectIdRef;
  42. import org.eclipse.jgit.lib.ProgressMonitor;
  43. import org.eclipse.jgit.lib.Ref;
  44. import org.eclipse.jgit.lib.RefDatabase;
  45. import org.eclipse.jgit.revwalk.ObjectWalk;
  46. import org.eclipse.jgit.revwalk.RevWalk;
  47. import org.eclipse.jgit.util.StringUtils;

  48. class FetchProcess {
  49.     /** Transport we will fetch over. */
  50.     private final Transport transport;

  51.     /** List of things we want to fetch from the remote repository. */
  52.     private final Collection<RefSpec> toFetch;

  53.     /** Set of refs we will actually wind up asking to obtain. */
  54.     private final HashMap<ObjectId, Ref> askFor = new HashMap<>();

  55.     /** Objects we know we have locally. */
  56.     private final HashSet<ObjectId> have = new HashSet<>();

  57.     /** Updates to local tracking branches (if any). */
  58.     private final ArrayList<TrackingRefUpdate> localUpdates = new ArrayList<>();

  59.     /** Records to be recorded into FETCH_HEAD. */
  60.     private final ArrayList<FetchHeadRecord> fetchHeadUpdates = new ArrayList<>();

  61.     private final ArrayList<PackLock> packLocks = new ArrayList<>();

  62.     private FetchConnection conn;

  63.     private Map<String, Ref> localRefs;

  64.     FetchProcess(Transport t, Collection<RefSpec> f) {
  65.         transport = t;
  66.         toFetch = f;
  67.     }

  68.     void execute(ProgressMonitor monitor, FetchResult result,
  69.             String initialBranch)
  70.             throws NotSupportedException, TransportException {
  71.         askFor.clear();
  72.         localUpdates.clear();
  73.         fetchHeadUpdates.clear();
  74.         packLocks.clear();
  75.         localRefs = null;

  76.         Throwable e1 = null;
  77.         try {
  78.             executeImp(monitor, result, initialBranch);
  79.         } catch (NotSupportedException | TransportException err) {
  80.             e1 = err;
  81.             throw err;
  82.         } finally {
  83.             try {
  84.                 for (PackLock lock : packLocks) {
  85.                     lock.unlock();
  86.                 }
  87.             } catch (IOException e) {
  88.                 if (e1 != null) {
  89.                     e.addSuppressed(e1);
  90.                 }
  91.                 throw new TransportException(e.getMessage(), e);
  92.             }
  93.         }
  94.     }

  95.     private boolean isInitialBranchMissing(Map<String, Ref> refsMap,
  96.             String initialBranch) {
  97.         if (StringUtils.isEmptyOrNull(initialBranch) || refsMap.isEmpty()) {
  98.             return false;
  99.         }
  100.         if (refsMap.containsKey(initialBranch)
  101.                 || refsMap.containsKey(Constants.R_HEADS + initialBranch)
  102.                 || refsMap.containsKey(Constants.R_TAGS + initialBranch)) {
  103.             return false;
  104.         }
  105.         return true;
  106.     }

  107.     private void executeImp(final ProgressMonitor monitor,
  108.             final FetchResult result, String initialBranch)
  109.             throws NotSupportedException, TransportException {
  110.         final TagOpt tagopt = transport.getTagOpt();
  111.         String getTags = (tagopt == TagOpt.NO_TAGS) ? null : Constants.R_TAGS;
  112.         String getHead = null;
  113.         try {
  114.             // If we don't have a HEAD yet, we're cloning and need to get the
  115.             // upstream HEAD, too.
  116.             Ref head = transport.local.exactRef(Constants.HEAD);
  117.             ObjectId id = head != null ? head.getObjectId() : null;
  118.             if (id == null || id.equals(ObjectId.zeroId())) {
  119.                 getHead = Constants.HEAD;
  120.             }
  121.         } catch (IOException e) {
  122.             // Ignore
  123.         }
  124.         conn = transport.openFetch(toFetch, getTags, getHead);
  125.         try {
  126.             Map<String, Ref> refsMap = conn.getRefsMap();
  127.             if (isInitialBranchMissing(refsMap, initialBranch)) {
  128.                 throw new TransportException(MessageFormat.format(
  129.                         JGitText.get().remoteBranchNotFound, initialBranch));
  130.             }
  131.             result.setAdvertisedRefs(transport.getURI(), refsMap);
  132.             result.peerUserAgent = conn.getPeerUserAgent();
  133.             final Set<Ref> matched = new HashSet<>();
  134.             for (RefSpec spec : toFetch) {
  135.                 if (spec.getSource() == null)
  136.                     throw new TransportException(MessageFormat.format(
  137.                             JGitText.get().sourceRefNotSpecifiedForRefspec, spec));

  138.                 if (spec.isWildcard())
  139.                     expandWildcard(spec, matched);
  140.                 else
  141.                     expandSingle(spec, matched);
  142.             }

  143.             Collection<Ref> additionalTags = Collections.<Ref> emptyList();
  144.             if (tagopt == TagOpt.AUTO_FOLLOW)
  145.                 additionalTags = expandAutoFollowTags();
  146.             else if (tagopt == TagOpt.FETCH_TAGS)
  147.                 expandFetchTags();

  148.             final boolean includedTags;
  149.             if (!askFor.isEmpty() && !askForIsComplete()) {
  150.                 fetchObjects(monitor);
  151.                 includedTags = conn.didFetchIncludeTags();

  152.                 // Connection was used for object transfer. If we
  153.                 // do another fetch we must open a new connection.
  154.                 //
  155.                 closeConnection(result);
  156.             } else {
  157.                 includedTags = false;
  158.             }

  159.             if (tagopt == TagOpt.AUTO_FOLLOW && !additionalTags.isEmpty()) {
  160.                 // There are more tags that we want to follow, but
  161.                 // not all were asked for on the initial request.
  162.                 //
  163.                 have.addAll(askFor.keySet());
  164.                 askFor.clear();
  165.                 for (Ref r : additionalTags) {
  166.                     ObjectId id = r.getPeeledObjectId();
  167.                     if (id == null)
  168.                         id = r.getObjectId();
  169.                     if (localHasObject(id))
  170.                         wantTag(r);
  171.                 }

  172.                 if (!askFor.isEmpty() && (!includedTags || !askForIsComplete())) {
  173.                     reopenConnection();
  174.                     if (!askFor.isEmpty())
  175.                         fetchObjects(monitor);
  176.                 }
  177.             }
  178.         } finally {
  179.             closeConnection(result);
  180.         }

  181.         BatchRefUpdate batch = transport.local.getRefDatabase()
  182.                 .newBatchUpdate()
  183.                 .setAllowNonFastForwards(true)
  184.                 .setRefLogMessage("fetch", true); //$NON-NLS-1$
  185.         try (RevWalk walk = new RevWalk(transport.local)) {
  186.             walk.setRetainBody(false);
  187.             if (monitor instanceof BatchingProgressMonitor) {
  188.                 ((BatchingProgressMonitor) monitor).setDelayStart(
  189.                         250, TimeUnit.MILLISECONDS);
  190.             }
  191.             if (transport.isRemoveDeletedRefs()) {
  192.                 deleteStaleTrackingRefs(result, batch);
  193.             }
  194.             addUpdateBatchCommands(result, batch);
  195.             for (ReceiveCommand cmd : batch.getCommands()) {
  196.                 cmd.updateType(walk);
  197.                 if (cmd.getType() == UPDATE_NONFASTFORWARD
  198.                         && cmd instanceof TrackingRefUpdate.Command
  199.                         && !((TrackingRefUpdate.Command) cmd).canForceUpdate())
  200.                     cmd.setResult(REJECTED_NONFASTFORWARD);
  201.             }
  202.             if (transport.isDryRun()) {
  203.                 for (ReceiveCommand cmd : batch.getCommands()) {
  204.                     if (cmd.getResult() == NOT_ATTEMPTED)
  205.                         cmd.setResult(OK);
  206.                 }
  207.             } else {
  208.                 batch.execute(walk, monitor);
  209.             }
  210.         } catch (TransportException e) {
  211.             throw e;
  212.         } catch (IOException err) {
  213.             throw new TransportException(MessageFormat.format(
  214.                     JGitText.get().failureUpdatingTrackingRef,
  215.                     getFirstFailedRefName(batch), err.getMessage()), err);
  216.         }

  217.         if (!fetchHeadUpdates.isEmpty()) {
  218.             try {
  219.                 updateFETCH_HEAD(result);
  220.             } catch (IOException err) {
  221.                 throw new TransportException(MessageFormat.format(
  222.                         JGitText.get().failureUpdatingFETCH_HEAD, err.getMessage()), err);
  223.             }
  224.         }
  225.     }

  226.     private void addUpdateBatchCommands(FetchResult result,
  227.             BatchRefUpdate batch) throws TransportException {
  228.         Map<String, ObjectId> refs = new HashMap<>();
  229.         for (TrackingRefUpdate u : localUpdates) {
  230.             // Try to skip duplicates if they'd update to the same object ID
  231.             ObjectId existing = refs.get(u.getLocalName());
  232.             if (existing == null) {
  233.                 refs.put(u.getLocalName(), u.getNewObjectId());
  234.                 result.add(u);
  235.                 batch.addCommand(u.asReceiveCommand());
  236.             } else if (!existing.equals(u.getNewObjectId())) {
  237.                 throw new TransportException(MessageFormat
  238.                         .format(JGitText.get().duplicateRef, u.getLocalName()));
  239.             }
  240.         }
  241.     }

  242.     private void fetchObjects(ProgressMonitor monitor)
  243.             throws TransportException {
  244.         try {
  245.             conn.setPackLockMessage("jgit fetch " + transport.uri); //$NON-NLS-1$
  246.             conn.fetch(monitor, askFor.values(), have);
  247.         } finally {
  248.             packLocks.addAll(conn.getPackLocks());
  249.         }
  250.         if (transport.isCheckFetchedObjects()
  251.                 && !conn.didFetchTestConnectivity() && !askForIsComplete())
  252.             throw new TransportException(transport.getURI(),
  253.                     JGitText.get().peerDidNotSupplyACompleteObjectGraph);
  254.     }

  255.     private void closeConnection(FetchResult result) {
  256.         if (conn != null) {
  257.             conn.close();
  258.             result.addMessages(conn.getMessages());
  259.             conn = null;
  260.         }
  261.     }

  262.     private void reopenConnection() throws NotSupportedException,
  263.             TransportException {
  264.         if (conn != null)
  265.             return;

  266.         // Build prefixes
  267.         Set<String> prefixes = new HashSet<>();
  268.         for (Ref toGet : askFor.values()) {
  269.             String src = toGet.getName();
  270.             prefixes.add(src);
  271.             prefixes.add(Constants.R_REFS + src);
  272.             prefixes.add(Constants.R_HEADS + src);
  273.             prefixes.add(Constants.R_TAGS + src);
  274.         }
  275.         conn = transport.openFetch(Collections.emptyList(),
  276.                 prefixes.toArray(new String[0]));

  277.         // Since we opened a new connection we cannot be certain
  278.         // that the system we connected to has the same exact set
  279.         // of objects available (think round-robin DNS and mirrors
  280.         // that aren't updated at the same time).
  281.         //
  282.         // We rebuild our askFor list using only the refs that the
  283.         // new connection has offered to us.
  284.         //
  285.         final HashMap<ObjectId, Ref> avail = new HashMap<>();
  286.         for (Ref r : conn.getRefs())
  287.             avail.put(r.getObjectId(), r);

  288.         final Collection<Ref> wants = new ArrayList<>(askFor.values());
  289.         askFor.clear();
  290.         for (Ref want : wants) {
  291.             final Ref newRef = avail.get(want.getObjectId());
  292.             if (newRef != null) {
  293.                 askFor.put(newRef.getObjectId(), newRef);
  294.             } else {
  295.                 removeFetchHeadRecord(want.getObjectId());
  296.                 removeTrackingRefUpdate(want.getObjectId());
  297.             }
  298.         }
  299.     }

  300.     private void removeTrackingRefUpdate(ObjectId want) {
  301.         final Iterator<TrackingRefUpdate> i = localUpdates.iterator();
  302.         while (i.hasNext()) {
  303.             final TrackingRefUpdate u = i.next();
  304.             if (u.getNewObjectId().equals(want))
  305.                 i.remove();
  306.         }
  307.     }

  308.     private void removeFetchHeadRecord(ObjectId want) {
  309.         final Iterator<FetchHeadRecord> i = fetchHeadUpdates.iterator();
  310.         while (i.hasNext()) {
  311.             final FetchHeadRecord fh = i.next();
  312.             if (fh.newValue.equals(want))
  313.                 i.remove();
  314.         }
  315.     }

  316.     private void updateFETCH_HEAD(FetchResult result) throws IOException {
  317.         File meta = transport.local.getDirectory();
  318.         if (meta == null)
  319.             return;
  320.         final LockFile lock = new LockFile(new File(meta, "FETCH_HEAD")); //$NON-NLS-1$
  321.         try {
  322.             if (lock.lock()) {
  323.                 try (Writer w = new OutputStreamWriter(
  324.                         lock.getOutputStream(), UTF_8)) {
  325.                     for (FetchHeadRecord h : fetchHeadUpdates) {
  326.                         h.write(w);
  327.                         result.add(h);
  328.                     }
  329.                 }
  330.                 lock.commit();
  331.             }
  332.         } finally {
  333.             lock.unlock();
  334.         }
  335.     }

  336.     private boolean askForIsComplete() throws TransportException {
  337.         try {
  338.             try (ObjectWalk ow = new ObjectWalk(transport.local)) {
  339.                 for (ObjectId want : askFor.keySet())
  340.                     ow.markStart(ow.parseAny(want));
  341.                 for (Ref ref : localRefs().values())
  342.                     ow.markUninteresting(ow.parseAny(ref.getObjectId()));
  343.                 ow.checkConnectivity();
  344.             }
  345.             return true;
  346.         } catch (MissingObjectException e) {
  347.             return false;
  348.         } catch (IOException e) {
  349.             throw new TransportException(JGitText.get().unableToCheckConnectivity, e);
  350.         }
  351.     }

  352.     private void expandWildcard(RefSpec spec, Set<Ref> matched)
  353.             throws TransportException {
  354.         for (Ref src : conn.getRefs()) {
  355.             if (spec.matchSource(src) && matched.add(src))
  356.                 want(src, spec.expandFromSource(src));
  357.         }
  358.     }

  359.     private void expandSingle(RefSpec spec, Set<Ref> matched)
  360.             throws TransportException {
  361.         String want = spec.getSource();
  362.         if (ObjectId.isId(want)) {
  363.             want(ObjectId.fromString(want));
  364.             return;
  365.         }

  366.         Ref src = conn.getRef(want);
  367.         if (src == null) {
  368.             throw new TransportException(MessageFormat.format(JGitText.get().remoteDoesNotHaveSpec, want));
  369.         }
  370.         if (matched.add(src)) {
  371.             want(src, spec);
  372.         }
  373.     }

  374.     private boolean localHasObject(ObjectId id) throws TransportException {
  375.         try {
  376.             return transport.local.getObjectDatabase().has(id);
  377.         } catch (IOException err) {
  378.             throw new TransportException(
  379.                     MessageFormat.format(
  380.                             JGitText.get().readingObjectsFromLocalRepositoryFailed,
  381.                             err.getMessage()),
  382.                     err);
  383.         }
  384.     }

  385.     private Collection<Ref> expandAutoFollowTags() throws TransportException {
  386.         final Collection<Ref> additionalTags = new ArrayList<>();
  387.         final Map<String, Ref> haveRefs = localRefs();
  388.         for (Ref r : conn.getRefs()) {
  389.             if (!isTag(r))
  390.                 continue;

  391.             Ref local = haveRefs.get(r.getName());
  392.             if (local != null)
  393.                 // We already have a tag with this name, don't fetch it (even if
  394.                 // the local is different).
  395.                 continue;

  396.             ObjectId obj = r.getPeeledObjectId();
  397.             if (obj == null)
  398.                 obj = r.getObjectId();

  399.             if (askFor.containsKey(obj) || localHasObject(obj))
  400.                 wantTag(r);
  401.             else
  402.                 additionalTags.add(r);
  403.         }
  404.         return additionalTags;
  405.     }

  406.     private void expandFetchTags() throws TransportException {
  407.         final Map<String, Ref> haveRefs = localRefs();
  408.         for (Ref r : conn.getRefs()) {
  409.             if (!isTag(r)) {
  410.                 continue;
  411.             }
  412.             ObjectId id = r.getObjectId();
  413.             if (id == null) {
  414.                 continue;
  415.             }
  416.             final Ref local = haveRefs.get(r.getName());
  417.             if (local == null || !id.equals(local.getObjectId())) {
  418.                 wantTag(r);
  419.             }
  420.         }
  421.     }

  422.     private void wantTag(Ref r) throws TransportException {
  423.         want(r, new RefSpec().setSource(r.getName())
  424.                 .setDestination(r.getName()).setForceUpdate(true));
  425.     }

  426.     private void want(Ref src, RefSpec spec)
  427.             throws TransportException {
  428.         final ObjectId newId = src.getObjectId();
  429.         if (newId == null) {
  430.             throw new NullPointerException(MessageFormat.format(
  431.                     JGitText.get().transportProvidedRefWithNoObjectId,
  432.                     src.getName()));
  433.         }
  434.         if (spec.getDestination() != null) {
  435.             final TrackingRefUpdate tru = createUpdate(spec, newId);
  436.             if (newId.equals(tru.getOldObjectId()))
  437.                 return;
  438.             localUpdates.add(tru);
  439.         }

  440.         askFor.put(newId, src);

  441.         final FetchHeadRecord fhr = new FetchHeadRecord();
  442.         fhr.newValue = newId;
  443.         fhr.notForMerge = spec.getDestination() != null;
  444.         fhr.sourceName = src.getName();
  445.         fhr.sourceURI = transport.getURI();
  446.         fetchHeadUpdates.add(fhr);
  447.     }

  448.     private void want(ObjectId id) {
  449.         askFor.put(id,
  450.                 new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, id.name(), id));
  451.     }

  452.     private TrackingRefUpdate createUpdate(RefSpec spec, ObjectId newId)
  453.             throws TransportException {
  454.         Ref ref = localRefs().get(spec.getDestination());
  455.         ObjectId oldId = ref != null && ref.getObjectId() != null
  456.                 ? ref.getObjectId()
  457.                 : ObjectId.zeroId();
  458.         return new TrackingRefUpdate(
  459.                 spec.isForceUpdate(),
  460.                 spec.getSource(),
  461.                 spec.getDestination(),
  462.                 oldId,
  463.                 newId);
  464.     }

  465.     private Map<String, Ref> localRefs() throws TransportException {
  466.         if (localRefs == null) {
  467.             try {
  468.                 localRefs = transport.local.getRefDatabase()
  469.                         .getRefs(RefDatabase.ALL);
  470.             } catch (IOException err) {
  471.                 throw new TransportException(JGitText.get().cannotListRefs, err);
  472.             }
  473.         }
  474.         return localRefs;
  475.     }

  476.     private void deleteStaleTrackingRefs(FetchResult result,
  477.             BatchRefUpdate batch) throws IOException {
  478.         Set<Ref> processed = new HashSet<>();
  479.         for (Ref ref : localRefs().values()) {
  480.             if (ref.isSymbolic()) {
  481.                 continue;
  482.             }
  483.             String refname = ref.getName();
  484.             for (RefSpec spec : toFetch) {
  485.                 if (spec.matchDestination(refname)) {
  486.                     RefSpec s = spec.expandFromDestination(refname);
  487.                     if (result.getAdvertisedRef(s.getSource()) == null
  488.                             && processed.add(ref)) {
  489.                         deleteTrackingRef(result, batch, s, ref);
  490.                     }
  491.                 }
  492.             }
  493.         }
  494.     }

  495.     private void deleteTrackingRef(final FetchResult result,
  496.             final BatchRefUpdate batch, final RefSpec spec, final Ref localRef) {
  497.         if (localRef.getObjectId() == null)
  498.             return;
  499.         TrackingRefUpdate update = new TrackingRefUpdate(
  500.                 true,
  501.                 spec.getSource(),
  502.                 localRef.getName(),
  503.                 localRef.getObjectId(),
  504.                 ObjectId.zeroId());
  505.         result.add(update);
  506.         batch.addCommand(update.asReceiveCommand());
  507.     }

  508.     private static boolean isTag(Ref r) {
  509.         return isTag(r.getName());
  510.     }

  511.     private static boolean isTag(String name) {
  512.         return name.startsWith(Constants.R_TAGS);
  513.     }

  514.     private static String getFirstFailedRefName(BatchRefUpdate batch) {
  515.         for (ReceiveCommand cmd : batch.getCommands()) {
  516.             if (cmd.getResult() != ReceiveCommand.Result.OK)
  517.                 return cmd.getRefName();
  518.         }
  519.         return ""; //$NON-NLS-1$
  520.     }
  521. }