FetchProcess.java

  1. /*
  2.  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
  3.  * Copyright (C) 2008, 2022 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 java.util.stream.Collectors;

  32. import org.eclipse.jgit.errors.MissingObjectException;
  33. import org.eclipse.jgit.errors.NotSupportedException;
  34. import org.eclipse.jgit.errors.TransportException;
  35. import org.eclipse.jgit.internal.JGitText;
  36. import org.eclipse.jgit.internal.storage.file.LockFile;
  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.     /**
  54.      * List of things we don't want to fetch from the remote repository or to
  55.      * the local repository.
  56.      */
  57.     private final Collection<RefSpec> negativeRefSpecs;

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

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

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

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

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

  67.     private FetchConnection conn;

  68.     private Map<String, Ref> localRefs;

  69.     FetchProcess(Transport t, Collection<RefSpec> refSpecs) {
  70.         transport = t;
  71.         toFetch = refSpecs.stream().filter(refSpec -> !refSpec.isNegative())
  72.                 .collect(Collectors.toList());
  73.         negativeRefSpecs = refSpecs.stream().filter(RefSpec::isNegative)
  74.                 .collect(Collectors.toList());
  75.     }

  76.     void execute(ProgressMonitor monitor, FetchResult result,
  77.             String initialBranch)
  78.             throws NotSupportedException, TransportException {
  79.         askFor.clear();
  80.         localUpdates.clear();
  81.         fetchHeadUpdates.clear();
  82.         packLocks.clear();
  83.         localRefs = null;

  84.         Throwable e1 = null;
  85.         try {
  86.             executeImp(monitor, result, initialBranch);
  87.         } catch (NotSupportedException | TransportException err) {
  88.             e1 = err;
  89.             throw err;
  90.         } finally {
  91.             try {
  92.                 for (PackLock lock : packLocks) {
  93.                     lock.unlock();
  94.                 }
  95.             } catch (IOException e) {
  96.                 if (e1 != null) {
  97.                     e.addSuppressed(e1);
  98.                 }
  99.                 throw new TransportException(e.getMessage(), e);
  100.             }
  101.         }
  102.     }

  103.     private boolean isInitialBranchMissing(Map<String, Ref> refsMap,
  104.             String initialBranch) {
  105.         if (StringUtils.isEmptyOrNull(initialBranch) || refsMap.isEmpty()) {
  106.             return false;
  107.         }
  108.         if (refsMap.containsKey(initialBranch)
  109.                 || refsMap.containsKey(Constants.R_HEADS + initialBranch)
  110.                 || refsMap.containsKey(Constants.R_TAGS + initialBranch)) {
  111.             return false;
  112.         }
  113.         return true;
  114.     }

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

  146.                 if (spec.isWildcard())
  147.                     expandWildcard(spec, matched);
  148.                 else
  149.                     expandSingle(spec, matched);
  150.             }

  151.             Collection<Ref> additionalTags = Collections.<Ref> emptyList();
  152.             if (tagopt == TagOpt.AUTO_FOLLOW)
  153.                 additionalTags = expandAutoFollowTags();
  154.             else if (tagopt == TagOpt.FETCH_TAGS)
  155.                 expandFetchTags();

  156.             final boolean includedTags;
  157.             if (!askFor.isEmpty() && !askForIsComplete()) {
  158.                 fetchObjects(monitor);
  159.                 includedTags = conn.didFetchIncludeTags();

  160.                 // Connection was used for object transfer. If we
  161.                 // do another fetch we must open a new connection.
  162.                 //
  163.                 closeConnection(result);
  164.             } else {
  165.                 includedTags = false;
  166.             }

  167.             if (tagopt == TagOpt.AUTO_FOLLOW && !additionalTags.isEmpty()) {
  168.                 // There are more tags that we want to follow, but
  169.                 // not all were asked for on the initial request.
  170.                 //
  171.                 have.addAll(askFor.keySet());
  172.                 askFor.clear();
  173.                 for (Ref r : additionalTags) {
  174.                     ObjectId id = r.getPeeledObjectId();
  175.                     if (id == null)
  176.                         id = r.getObjectId();
  177.                     if (localHasObject(id))
  178.                         wantTag(r);
  179.                 }

  180.                 if (!askFor.isEmpty() && (!includedTags || !askForIsComplete())) {
  181.                     reopenConnection();
  182.                     if (!askFor.isEmpty())
  183.                         fetchObjects(monitor);
  184.                 }
  185.             }
  186.         } finally {
  187.             closeConnection(result);
  188.         }

  189.         BatchRefUpdate batch = transport.local.getRefDatabase()
  190.                 .newBatchUpdate()
  191.                 .setAllowNonFastForwards(true);

  192.         // Generate reflog only when fetching updates and not at the first clone
  193.         if (initialBranch == null) {
  194.             batch.setRefLogMessage("fetch", true); //$NON-NLS-1$
  195.         }

  196.         try (RevWalk walk = new RevWalk(transport.local)) {
  197.             walk.setRetainBody(false);
  198.             if (monitor instanceof BatchingProgressMonitor) {
  199.                 ((BatchingProgressMonitor) monitor).setDelayStart(
  200.                         250, TimeUnit.MILLISECONDS);
  201.             }
  202.             if (transport.isRemoveDeletedRefs()) {
  203.                 deleteStaleTrackingRefs(result, batch);
  204.             }
  205.             addUpdateBatchCommands(result, batch);
  206.             for (ReceiveCommand cmd : batch.getCommands()) {
  207.                 cmd.updateType(walk);
  208.                 if (cmd.getType() == UPDATE_NONFASTFORWARD
  209.                         && cmd instanceof TrackingRefUpdate.Command
  210.                         && !((TrackingRefUpdate.Command) cmd).canForceUpdate())
  211.                     cmd.setResult(REJECTED_NONFASTFORWARD);
  212.             }
  213.             if (transport.isDryRun()) {
  214.                 for (ReceiveCommand cmd : batch.getCommands()) {
  215.                     if (cmd.getResult() == NOT_ATTEMPTED)
  216.                         cmd.setResult(OK);
  217.                 }
  218.             } else {
  219.                 batch.execute(walk, monitor);
  220.             }
  221.         } catch (TransportException e) {
  222.             throw e;
  223.         } catch (IOException err) {
  224.             throw new TransportException(MessageFormat.format(
  225.                     JGitText.get().failureUpdatingTrackingRef,
  226.                     getFirstFailedRefName(batch), err.getMessage()), err);
  227.         }

  228.         if (!fetchHeadUpdates.isEmpty()) {
  229.             try {
  230.                 updateFETCH_HEAD(result);
  231.             } catch (IOException err) {
  232.                 throw new TransportException(MessageFormat.format(
  233.                         JGitText.get().failureUpdatingFETCH_HEAD, err.getMessage()), err);
  234.             }
  235.         }
  236.     }

  237.     private void addUpdateBatchCommands(FetchResult result,
  238.             BatchRefUpdate batch) throws TransportException {
  239.         Map<String, ObjectId> refs = new HashMap<>();
  240.         for (TrackingRefUpdate u : localUpdates) {
  241.             // Try to skip duplicates if they'd update to the same object ID
  242.             ObjectId existing = refs.get(u.getLocalName());
  243.             if (existing == null) {
  244.                 refs.put(u.getLocalName(), u.getNewObjectId());
  245.                 result.add(u);
  246.                 batch.addCommand(u.asReceiveCommand());
  247.             } else if (!existing.equals(u.getNewObjectId())) {
  248.                 throw new TransportException(MessageFormat
  249.                         .format(JGitText.get().duplicateRef, u.getLocalName()));
  250.             }
  251.         }
  252.     }

  253.     private void fetchObjects(ProgressMonitor monitor)
  254.             throws TransportException {
  255.         try {
  256.             conn.setPackLockMessage("jgit fetch " + transport.uri); //$NON-NLS-1$
  257.             conn.fetch(monitor, askFor.values(), have);
  258.         } finally {
  259.             packLocks.addAll(conn.getPackLocks());
  260.         }
  261.         if (transport.isCheckFetchedObjects()
  262.                 && !conn.didFetchTestConnectivity() && !askForIsComplete())
  263.             throw new TransportException(transport.getURI(),
  264.                     JGitText.get().peerDidNotSupplyACompleteObjectGraph);
  265.     }

  266.     private void closeConnection(FetchResult result) {
  267.         if (conn != null) {
  268.             conn.close();
  269.             result.addMessages(conn.getMessages());
  270.             conn = null;
  271.         }
  272.     }

  273.     private void reopenConnection() throws NotSupportedException,
  274.             TransportException {
  275.         if (conn != null)
  276.             return;

  277.         // Build prefixes
  278.         Set<String> prefixes = new HashSet<>();
  279.         for (Ref toGet : askFor.values()) {
  280.             String src = toGet.getName();
  281.             prefixes.add(src);
  282.             prefixes.add(Constants.R_REFS + src);
  283.             prefixes.add(Constants.R_HEADS + src);
  284.             prefixes.add(Constants.R_TAGS + src);
  285.         }
  286.         conn = transport.openFetch(Collections.emptyList(),
  287.                 prefixes.toArray(new String[0]));

  288.         // Since we opened a new connection we cannot be certain
  289.         // that the system we connected to has the same exact set
  290.         // of objects available (think round-robin DNS and mirrors
  291.         // that aren't updated at the same time).
  292.         //
  293.         // We rebuild our askFor list using only the refs that the
  294.         // new connection has offered to us.
  295.         //
  296.         final HashMap<ObjectId, Ref> avail = new HashMap<>();
  297.         for (Ref r : conn.getRefs())
  298.             avail.put(r.getObjectId(), r);

  299.         final Collection<Ref> wants = new ArrayList<>(askFor.values());
  300.         askFor.clear();
  301.         for (Ref want : wants) {
  302.             final Ref newRef = avail.get(want.getObjectId());
  303.             if (newRef != null) {
  304.                 askFor.put(newRef.getObjectId(), newRef);
  305.             } else {
  306.                 removeFetchHeadRecord(want.getObjectId());
  307.                 removeTrackingRefUpdate(want.getObjectId());
  308.             }
  309.         }
  310.     }

  311.     private void removeTrackingRefUpdate(ObjectId want) {
  312.         final Iterator<TrackingRefUpdate> i = localUpdates.iterator();
  313.         while (i.hasNext()) {
  314.             final TrackingRefUpdate u = i.next();
  315.             if (u.getNewObjectId().equals(want))
  316.                 i.remove();
  317.         }
  318.     }

  319.     private void removeFetchHeadRecord(ObjectId want) {
  320.         final Iterator<FetchHeadRecord> i = fetchHeadUpdates.iterator();
  321.         while (i.hasNext()) {
  322.             final FetchHeadRecord fh = i.next();
  323.             if (fh.newValue.equals(want))
  324.                 i.remove();
  325.         }
  326.     }

  327.     private void updateFETCH_HEAD(FetchResult result) throws IOException {
  328.         File meta = transport.local.getDirectory();
  329.         if (meta == null)
  330.             return;
  331.         final LockFile lock = new LockFile(new File(meta, "FETCH_HEAD")); //$NON-NLS-1$
  332.         try {
  333.             if (lock.lock()) {
  334.                 try (Writer w = new OutputStreamWriter(
  335.                         lock.getOutputStream(), UTF_8)) {
  336.                     for (FetchHeadRecord h : fetchHeadUpdates) {
  337.                         h.write(w);
  338.                         result.add(h);
  339.                     }
  340.                 }
  341.                 lock.commit();
  342.             }
  343.         } finally {
  344.             lock.unlock();
  345.         }
  346.     }

  347.     private boolean askForIsComplete() throws TransportException {
  348.         try {
  349.             try (ObjectWalk ow = new ObjectWalk(transport.local)) {
  350.                 for (ObjectId want : askFor.keySet())
  351.                     ow.markStart(ow.parseAny(want));
  352.                 for (Ref ref : localRefs().values())
  353.                     ow.markUninteresting(ow.parseAny(ref.getObjectId()));
  354.                 ow.checkConnectivity();
  355.             }
  356.             return transport.getDepth() == null; // if depth is set we need to request objects that are already available
  357.         } catch (MissingObjectException e) {
  358.             return false;
  359.         } catch (IOException e) {
  360.             throw new TransportException(JGitText.get().unableToCheckConnectivity, e);
  361.         }
  362.     }

  363.     private void expandWildcard(RefSpec spec, Set<Ref> matched)
  364.             throws TransportException {
  365.         for (Ref src : conn.getRefs()) {
  366.             if (spec.matchSource(src)) {
  367.                 RefSpec expandedRefSpec = spec.expandFromSource(src);
  368.                 if (!matchNegativeRefSpec(expandedRefSpec)
  369.                         && matched.add(src)) {
  370.                     want(src, expandedRefSpec);
  371.                 }
  372.             }
  373.         }
  374.     }

  375.     private void expandSingle(RefSpec spec, Set<Ref> matched)
  376.             throws TransportException {
  377.         String want = spec.getSource();
  378.         if (ObjectId.isId(want)) {
  379.             want(ObjectId.fromString(want));
  380.             return;
  381.         }

  382.         Ref src = conn.getRef(want);
  383.         if (src == null) {
  384.             throw new TransportException(MessageFormat.format(JGitText.get().remoteDoesNotHaveSpec, want));
  385.         }
  386.         if (!matchNegativeRefSpec(spec) && matched.add(src)) {
  387.             want(src, spec);
  388.         }
  389.     }

  390.     private boolean matchNegativeRefSpec(RefSpec spec) {
  391.         for (RefSpec negativeRefSpec : negativeRefSpecs) {
  392.             if (negativeRefSpec.getSource() != null && spec.getSource() != null
  393.                     && negativeRefSpec.matchSource(spec.getSource())) {
  394.                 return true;
  395.             }

  396.             if (negativeRefSpec.getDestination() != null
  397.                     && spec.getDestination() != null && negativeRefSpec
  398.                             .matchDestination(spec.getDestination())) {
  399.                 return true;
  400.             }
  401.         }
  402.         return false;
  403.     }

  404.     private boolean localHasObject(ObjectId id) throws TransportException {
  405.         try {
  406.             return transport.local.getObjectDatabase().has(id);
  407.         } catch (IOException err) {
  408.             throw new TransportException(
  409.                     MessageFormat.format(
  410.                             JGitText.get().readingObjectsFromLocalRepositoryFailed,
  411.                             err.getMessage()),
  412.                     err);
  413.         }
  414.     }

  415.     private Collection<Ref> expandAutoFollowTags() throws TransportException {
  416.         final Collection<Ref> additionalTags = new ArrayList<>();
  417.         final Map<String, Ref> haveRefs = localRefs();
  418.         for (Ref r : conn.getRefs()) {
  419.             if (!isTag(r))
  420.                 continue;

  421.             Ref local = haveRefs.get(r.getName());
  422.             if (local != null)
  423.                 // We already have a tag with this name, don't fetch it (even if
  424.                 // the local is different).
  425.                 continue;

  426.             ObjectId obj = r.getPeeledObjectId();
  427.             if (obj == null)
  428.                 obj = r.getObjectId();

  429.             if (askFor.containsKey(obj) || localHasObject(obj))
  430.                 wantTag(r);
  431.             else
  432.                 additionalTags.add(r);
  433.         }
  434.         return additionalTags;
  435.     }

  436.     private void expandFetchTags() throws TransportException {
  437.         final Map<String, Ref> haveRefs = localRefs();
  438.         for (Ref r : conn.getRefs()) {
  439.             if (!isTag(r)) {
  440.                 continue;
  441.             }
  442.             ObjectId id = r.getObjectId();
  443.             if (id == null) {
  444.                 continue;
  445.             }
  446.             final Ref local = haveRefs.get(r.getName());
  447.             if (local == null || !id.equals(local.getObjectId())) {
  448.                 wantTag(r);
  449.             }
  450.         }
  451.     }

  452.     private void wantTag(Ref r) throws TransportException {
  453.         want(r, new RefSpec().setSource(r.getName())
  454.                 .setDestination(r.getName()).setForceUpdate(true));
  455.     }

  456.     private void want(Ref src, RefSpec spec)
  457.             throws TransportException {
  458.         final ObjectId newId = src.getObjectId();
  459.         if (newId == null) {
  460.             throw new NullPointerException(MessageFormat.format(
  461.                     JGitText.get().transportProvidedRefWithNoObjectId,
  462.                     src.getName()));
  463.         }
  464.         if (spec.getDestination() != null) {
  465.             final TrackingRefUpdate tru = createUpdate(spec, newId);
  466.             // if depth is set we need to update the ref
  467.             if (newId.equals(tru.getOldObjectId()) && transport.getDepth() == null) {
  468.                 return;
  469.             }
  470.             localUpdates.add(tru);
  471.         }

  472.         askFor.put(newId, src);

  473.         final FetchHeadRecord fhr = new FetchHeadRecord();
  474.         fhr.newValue = newId;
  475.         fhr.notForMerge = spec.getDestination() != null;
  476.         fhr.sourceName = src.getName();
  477.         fhr.sourceURI = transport.getURI();
  478.         fetchHeadUpdates.add(fhr);
  479.     }

  480.     private void want(ObjectId id) {
  481.         askFor.put(id,
  482.                 new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, id.name(), id));
  483.     }

  484.     private TrackingRefUpdate createUpdate(RefSpec spec, ObjectId newId)
  485.             throws TransportException {
  486.         Ref ref = localRefs().get(spec.getDestination());
  487.         ObjectId oldId = ref != null && ref.getObjectId() != null
  488.                 ? ref.getObjectId()
  489.                 : ObjectId.zeroId();
  490.         return new TrackingRefUpdate(
  491.                 spec.isForceUpdate(),
  492.                 spec.getSource(),
  493.                 spec.getDestination(),
  494.                 oldId,
  495.                 newId);
  496.     }

  497.     private Map<String, Ref> localRefs() throws TransportException {
  498.         if (localRefs == null) {
  499.             try {
  500.                 localRefs = transport.local.getRefDatabase()
  501.                         .getRefs(RefDatabase.ALL);
  502.             } catch (IOException err) {
  503.                 throw new TransportException(JGitText.get().cannotListRefs, err);
  504.             }
  505.         }
  506.         return localRefs;
  507.     }

  508.     private void deleteStaleTrackingRefs(FetchResult result,
  509.             BatchRefUpdate batch) throws IOException {
  510.         Set<Ref> processed = new HashSet<>();
  511.         for (Ref ref : localRefs().values()) {
  512.             if (ref.isSymbolic()) {
  513.                 continue;
  514.             }
  515.             String refname = ref.getName();
  516.             for (RefSpec spec : toFetch) {
  517.                 if (spec.matchDestination(refname)) {
  518.                     RefSpec s = spec.expandFromDestination(refname);
  519.                     if (result.getAdvertisedRef(s.getSource()) == null
  520.                             && processed.add(ref)) {
  521.                         deleteTrackingRef(result, batch, s, ref);
  522.                     }
  523.                 }
  524.             }
  525.         }
  526.     }

  527.     private void deleteTrackingRef(final FetchResult result,
  528.             final BatchRefUpdate batch, final RefSpec spec, final Ref localRef) {
  529.         if (localRef.getObjectId() == null)
  530.             return;
  531.         TrackingRefUpdate update = new TrackingRefUpdate(
  532.                 true,
  533.                 spec.getSource(),
  534.                 localRef.getName(),
  535.                 localRef.getObjectId(),
  536.                 ObjectId.zeroId());
  537.         result.add(update);
  538.         batch.addCommand(update.asReceiveCommand());
  539.     }

  540.     private static boolean isTag(Ref r) {
  541.         return isTag(r.getName());
  542.     }

  543.     private static boolean isTag(String name) {
  544.         return name.startsWith(Constants.R_TAGS);
  545.     }

  546.     private static String getFirstFailedRefName(BatchRefUpdate batch) {
  547.         for (ReceiveCommand cmd : batch.getCommands()) {
  548.             if (cmd.getResult() != ReceiveCommand.Result.OK)
  549.                 return cmd.getRefName();
  550.         }
  551.         return ""; //$NON-NLS-1$
  552.     }
  553. }