From 1c43af8b9794abcad7a4ac77c352626063aa1f05 Mon Sep 17 00:00:00 2001 From: Markus Duft Date: Tue, 5 Dec 2017 09:30:48 +0100 Subject: [PATCH] Progress reporting for checkout The reason for the change is LFS: when using a lot of LFS files, checkout can take quite some time on larger repositories. To avoid "hanging" UI, provide progress reporting. Also implement (partial) progress reporting for cherry-pick, reset, revert which are using checkout internally. The feature is also useful without LFS, so it is independent of it. Change-Id: I021e764241f3c107eaf2771f6b5785245b146b42 Signed-off-by: Markus Duft Signed-off-by: Matthias Sohn --- .../src/org/eclipse/jgit/pgm/Checkout.java | 4 +- .../eclipse/jgit/internal/JGitText.properties | 1 + .../org/eclipse/jgit/api/CheckoutCommand.java | 19 +++++++++ .../eclipse/jgit/api/CherryPickCommand.java | 23 +++++++++++ .../org/eclipse/jgit/api/CloneCommand.java | 1 + .../org/eclipse/jgit/api/MergeCommand.java | 3 ++ .../org/eclipse/jgit/api/RebaseCommand.java | 3 ++ .../org/eclipse/jgit/api/ResetCommand.java | 23 +++++++++++ .../org/eclipse/jgit/api/RevertCommand.java | 23 +++++++++++ .../jgit/api/SubmoduleUpdateCommand.java | 1 + .../jgit/dircache/DirCacheCheckout.java | 41 ++++++++++++++++++- .../org/eclipse/jgit/internal/JGitText.java | 1 + 12 files changed, 141 insertions(+), 2 deletions(-) diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java index 1f80301f6..6ff39fab0 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java @@ -57,6 +57,7 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.pgm.internal.CLIText; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; @@ -90,7 +91,8 @@ protected void run() throws Exception { } try (Git git = new Git(db)) { - CheckoutCommand command = git.checkout(); + CheckoutCommand command = git.checkout() + .setProgressMonitor(new TextProgressMonitor(errw)); if (paths.size() > 0) { command.setStartPoint(name); if (paths.size() == 1 && paths.get(0).equals(".")) { //$NON-NLS-1$ diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 3dd72c3ca..74be24665 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -117,6 +117,7 @@ cantFindObjectInReversePackIndexForTheSpecifiedOffset=Can''t find object in (rev cantPassMeATree=Can't pass me a tree! channelMustBeInRange1_255=channel {0} must be in range [1, 255] characterClassIsNotSupported=The character class {0} is not supported. +checkingOutFiles=Checking out files checkoutConflictWithFile=Checkout conflict with file: {0} checkoutConflictWithFiles=Checkout conflict with files: {0} checkoutUnexpectedResult=Checkout returned unexpected result {0} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java index aa9939edd..11edb1089 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java @@ -76,8 +76,10 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; @@ -182,6 +184,8 @@ private Stage(int number) { private Set actuallyModifiedPaths; + private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; + /** * Constructor for CheckoutCommand * @@ -266,6 +270,7 @@ public Ref call() throws GitAPIException, RefAlreadyExistsException, dco = new DirCacheCheckout(repo, headTree, dc, newCommit.getTree()); dco.setFailOnConflict(true); + dco.setProgressMonitor(monitor); try { dco.checkout(); } catch (org.eclipse.jgit.errors.CheckoutConflictException e) { @@ -346,6 +351,20 @@ private String getShortBranchName(Ref headRef) { return id.getName(); } + /** + * @param monitor + * a progress monitor + * @return this instance + * @since 4.11 + */ + public CheckoutCommand setProgressMonitor(ProgressMonitor monitor) { + if (monitor == null) { + monitor = NullProgressMonitor.INSTANCE; + } + this.monitor = monitor; + return this; + } + /** * Add a single slash-separated path to the list of paths to check out. To * check out all paths, use {@link #setAllPaths(boolean)}. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java index 771798a50..f45e39e06 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java @@ -60,8 +60,10 @@ import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.lib.Repository; @@ -95,6 +97,8 @@ public class CherryPickCommand extends GitCommand { private boolean noCommit = false; + private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; + /** * Constructor for CherryPickCommand * @@ -160,6 +164,7 @@ public CherryPickResult call() throws GitAPIException, NoMessageException, newHead.getTree(), repo.lockDirCache(), merger.getResultTreeId()); dco.setFailOnConflict(true); + dco.setProgressMonitor(monitor); dco.checkout(); if (!noCommit) newHead = new Git(getRepository()).commit() @@ -332,6 +337,24 @@ public CherryPickCommand setNoCommit(boolean noCommit) { return this; } + /** + * The progress monitor associated with the cherry-pick operation. By + * default, this is set to NullProgressMonitor + * + * @see NullProgressMonitor + * @param monitor + * a {@link org.eclipse.jgit.lib.ProgressMonitor} + * @return {@code this} + * @since 4.11 + */ + public CherryPickCommand setProgressMonitor(ProgressMonitor monitor) { + if (monitor == null) { + monitor = NullProgressMonitor.INSTANCE; + } + this.monitor = monitor; + return this; + } + private String calculateOurName(Ref headRef) { if (ourCommitName != null) return ourCommitName; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java index 6d3afc619..79b0efbe6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java @@ -361,6 +361,7 @@ private void checkout(Repository clonedRepo, FetchResult result) DirCache dc = clonedRepo.lockDirCache(); DirCacheCheckout co = new DirCacheCheckout(clonedRepo, dc, commit.getTree()); + co.setProgressMonitor(monitor); co.checkout(); if (cloneSubmodules) cloneSubmodules(clonedRepo); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java index 44ff18fcc..2eced1e70 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java @@ -268,6 +268,7 @@ public MergeResult call() throws GitAPIException, NoHeadException, dco = new DirCacheCheckout(repo, repo.lockDirCache(), srcCommit.getTree()); dco.setFailOnConflict(true); + dco.setProgressMonitor(monitor); dco.checkout(); RefUpdate refUpdate = repo .updateRef(head.getTarget().getName()); @@ -298,6 +299,7 @@ public MergeResult call() throws GitAPIException, NoHeadException, dco = new DirCacheCheckout(repo, headCommit.getTree(), repo.lockDirCache(), srcCommit.getTree()); + dco.setProgressMonitor(monitor); dco.setFailOnConflict(true); dco.checkout(); String msg = null; @@ -376,6 +378,7 @@ public MergeResult call() throws GitAPIException, NoHeadException, headCommit.getTree(), repo.lockDirCache(), merger.getResultTreeId()); dco.setFailOnConflict(true); + dco.setProgressMonitor(monitor); dco.checkout(); String msg = null; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index b86a2fdf8..da1ff06ae 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -932,6 +932,7 @@ private RevCommit checkoutCurrentHead() throws IOException, NoHeadException { try { DirCacheCheckout dco = new DirCacheCheckout(repo, dc, headTree); dco.setFailOnConflict(false); + dco.setProgressMonitor(monitor); boolean needsDeleteFiles = dco.checkout(); if (needsDeleteFiles) { List fileList = dco.getToBeDeleted(); @@ -1265,6 +1266,7 @@ private RevCommit tryFastForward(String headName, RevCommit oldCommit, CheckoutCommand co = new CheckoutCommand(repo); try { + co.setProgressMonitor(monitor); co.setName(newCommit.name()).call(); if (headName.startsWith(Constants.R_HEADS)) { RefUpdate rup = repo.updateRef(headName); @@ -1407,6 +1409,7 @@ private boolean checkoutCommit(String headName, RevCommit commit) DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(), repo.lockDirCache(), commit.getTree()); dco.setFailOnConflict(true); + dco.setProgressMonitor(monitor); try { dco.checkout(); } catch (org.eclipse.jgit.errors.CheckoutConflictException cce) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java index 86a69b019..be446f944 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java @@ -58,7 +58,9 @@ import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; @@ -125,6 +127,8 @@ public enum ResetType { private boolean isReflogDisabled; + private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; + /** *

* Constructor for ResetCommand. @@ -336,6 +340,24 @@ private String getRefOrHEAD() { return Constants.HEAD; } + /** + * The progress monitor associated with the reset operation. By default, + * this is set to NullProgressMonitor + * + * @see NullProgressMonitor + * @param monitor + * a {@link org.eclipse.jgit.lib.ProgressMonitor} + * @return {@code this} + * @since 4.11 + */ + public ResetCommand setProgressMonitor(ProgressMonitor monitor) { + if (monitor == null) { + monitor = NullProgressMonitor.INSTANCE; + } + this.monitor = monitor; + return this; + } + private void resetIndexForPaths(ObjectId commitTree) { DirCache dc = null; try (final TreeWalk tw = new TreeWalk(repo)) { @@ -420,6 +442,7 @@ private void checkoutIndex(ObjectId commitTree) throws IOException, DirCacheCheckout checkout = new DirCacheCheckout(repo, dc, commitTree); checkout.setFailOnConflict(false); + checkout.setProgressMonitor(monitor); try { checkout.checkout(); } catch (org.eclipse.jgit.errors.CheckoutConflictException cce) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java index fa0d4c488..46e0df726 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java @@ -61,8 +61,10 @@ import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.lib.Repository; @@ -97,6 +99,8 @@ public class RevertCommand extends GitCommand { private MergeStrategy strategy = MergeStrategy.RECURSIVE; + private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; + /** *

* Constructor for RevertCommand. @@ -178,6 +182,7 @@ public RevCommit call() throws NoMessageException, UnmergedPathsException, headCommit.getTree(), repo.lockDirCache(), merger.getResultTreeId()); dco.setFailOnConflict(true); + dco.setProgressMonitor(monitor); dco.checkout(); try (Git git = new Git(getRepository())) { newHead = git.commit().setMessage(newMessage) @@ -325,4 +330,22 @@ public RevertCommand setStrategy(MergeStrategy strategy) { this.strategy = strategy; return this; } + + /** + * The progress monitor associated with the revert operation. By default, + * this is set to NullProgressMonitor + * + * @see NullProgressMonitor + * @param monitor + * a {@link org.eclipse.jgit.lib.ProgressMonitor} + * @return {@code this} + * @since 4.11 + */ + public RevertCommand setProgressMonitor(ProgressMonitor monitor) { + if (monitor == null) { + monitor = NullProgressMonitor.INSTANCE; + } + this.monitor = monitor; + return this; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java index 4b4e18c13..3362d46f1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java @@ -226,6 +226,7 @@ public Collection call() throws InvalidConfigurationException, submoduleRepo, submoduleRepo.lockDirCache(), commit.getTree()); co.setFailOnConflict(true); + co.setProgressMonitor(monitor); co.checkout(); RefUpdate refUpdate = submoduleRepo.updateRef( Constants.HEAD, true); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java index d41a1f57f..ba8bfe3e4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -56,6 +56,7 @@ import java.util.List; import java.util.Map; +import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.FilterFailedException; import org.eclipse.jgit.attributes.FilterCommand; import org.eclipse.jgit.attributes.FilterCommandRegistry; @@ -76,6 +77,7 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; @@ -158,6 +160,8 @@ public CheckoutMetadata(EolStreamType eolStreamType, private boolean performingCheckout; + private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; + /** * Get list of updated paths and smudgeFilterCommands * @@ -287,6 +291,18 @@ public DirCacheCheckout(Repository repo, DirCache dc, this(repo, null, dc, mergeCommitTree, new FileTreeIterator(repo)); } + /** + * Set a progress monitor which can be passed to built-in filter commands, + * providing progress information for long running tasks. + * + * @param monitor + * the {@link ProgressMonitor} + * @since 4.11 + */ + public void setProgressMonitor(ProgressMonitor monitor) { + this.monitor = monitor != null ? monitor : NullProgressMonitor.INSTANCE; + } + /** * Scan head, index and merge tree. Used during normal checkout or merge * operations. @@ -465,6 +481,10 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i, public boolean checkout() throws IOException { try { return doCheckout(); + } catch (CanceledException ce) { + // should actually be propagated, but this would change a LOT of + // APIs + throw new IOException(ce); } finally { try { dc.unlock(); @@ -482,7 +502,7 @@ public boolean checkout() throws IOException { private boolean doCheckout() throws CorruptObjectException, IOException, MissingObjectException, IncorrectObjectTypeException, - CheckoutConflictException, IndexWriteException { + CheckoutConflictException, IndexWriteException, CanceledException { toBeDeleted.clear(); try (ObjectReader objectReader = repo.getObjectDatabase().newReader()) { if (headCommitTree != null) @@ -500,6 +520,10 @@ private boolean doCheckout() throws CorruptObjectException, IOException, // update our index builder.finish(); + // init progress reporting + int numTotal = removed.size() + updated.size(); + monitor.beginTask(JGitText.get().checkingOutFiles, numTotal); + performingCheckout = true; File file = null; String last = null; @@ -525,6 +549,12 @@ private boolean doCheckout() throws CorruptObjectException, IOException, removeEmptyParents(new File(repo.getWorkTree(), last)); last = r; } + monitor.update(1); + if (monitor.isCancelled()) { + throw new CanceledException(MessageFormat.format( + JGitText.get().operationCanceled, + JGitText.get().checkingOutFiles)); + } } if (file != null) { removeEmptyParents(file); @@ -544,6 +574,13 @@ private boolean doCheckout() throws CorruptObjectException, IOException, checkoutEntry(repo, entry, objectReader, false, meta); } e = null; + + monitor.update(1); + if (monitor.isCancelled()) { + throw new CanceledException(MessageFormat.format( + JGitText.get().operationCanceled, + JGitText.get().checkingOutFiles)); + } } } catch (Exception ex) { // We didn't actually modify the current entry nor any that @@ -557,6 +594,8 @@ private boolean doCheckout() throws CorruptObjectException, IOException, } throw ex; } + monitor.endTask(); + // commit the index builder - a new index is persisted if (!builder.commit()) throw new IndexWriteException(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 0545a4eca..7f91b30a9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -178,6 +178,7 @@ public static JGitText get() { /***/ public String cantPassMeATree; /***/ public String channelMustBeInRange1_255; /***/ public String characterClassIsNotSupported; + /***/ public String checkingOutFiles; /***/ public String checkoutConflictWithFile; /***/ public String checkoutConflictWithFiles; /***/ public String checkoutUnexpectedResult;