diff options
12 files changed, 720 insertions, 100 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/NameRevCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/NameRevCommandTest.java new file mode 100644 index 0000000000..82839e3bc1 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/NameRevCommandTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2013, 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.api; + +import static org.junit.Assert.assertEquals; + +import java.util.Map; + +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Before; +import org.junit.Test; + +public class NameRevCommandTest extends RepositoryTestCase { + private TestRepository<Repository> tr; + private Git git; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + tr = new TestRepository<Repository>(db); + git = new Git(db); + } + + @Test + public void nameExact() throws Exception { + RevCommit c = tr.commit().create(); + tr.update("master", c); + assertOneResult("master", c); + } + + @Test + public void prefix() throws Exception { + RevCommit c = tr.commit().create(); + tr.update("refs/heads/master", c); + tr.update("refs/tags/tag", c); + assertOneResult("master", c); + assertOneResult("master", + git.nameRev().addPrefix("refs/heads/").addPrefix("refs/tags/"), + c); + assertOneResult("tag", + git.nameRev().addPrefix("refs/tags/").addPrefix("refs/heads/"), + c); + } + + @Test + public void simpleAncestor() throws Exception { + // 0--1--2 + RevCommit c0 = tr.commit().create(); + RevCommit c1 = tr.commit().parent(c0).create(); + RevCommit c2 = tr.commit().parent(c1).create(); + tr.update("master", c2); + Map<ObjectId, String> result = git.nameRev().add(c0).add(c1).add(c2).call(); + assertEquals(3, result.size()); + assertEquals("master~2", result.get(c0)); + assertEquals("master~1", result.get(c1)); + assertEquals("master", result.get(c2)); + } + + @Test + public void multiplePathsNoMerge() throws Exception { + // 0--1 <- master + // \-2--3 <- branch + RevCommit c0 = tr.commit().create(); + RevCommit c1 = tr.commit().parent(c0).create(); + RevCommit c2 = tr.commit().parent(c0).create(); + RevCommit c3 = tr.commit().parent(c2).create(); + tr.update("master", c1); + tr.update("branch", c3); + assertOneResult("master~1", c0); + } + + @Test + public void onePathMerge() throws Exception { + // 0--1--3 + // \-2-/ + RevCommit c0 = tr.commit().create(); + RevCommit c1 = tr.commit().parent(c0).create(); + RevCommit c2 = tr.commit().parent(c0).create(); + RevCommit c3 = tr.commit().parent(c1).parent(c2).create(); + tr.update("master", c3); + assertOneResult("master^1~1", c0); + } + + @Test + public void oneMergeDifferentLengths() throws Exception { + // 0--1--2--4 + // \--3---/ + RevCommit c0 = tr.commit().create(); + RevCommit c1 = tr.commit().parent(c0).create(); + RevCommit c2 = tr.commit().parent(c1).create(); + RevCommit c3 = tr.commit().parent(c0).create(); + RevCommit c4 = tr.commit().parent(c2).parent(c3).create(); + tr.update("master", c4); + assertOneResult("master^2~1", c0); + } + + @Test + public void longerPathWithoutMerge() throws Exception { + // 0--1--2--4 <- master + // \ \-3-/ + // \--5--6--7--8--9 <- branch + RevCommit c0 = tr.commit().create(); + RevCommit c1 = tr.commit().parent(c0).create(); + RevCommit c2 = tr.commit().parent(c1).create(); + RevCommit c3 = tr.commit().parent(c1).create(); + RevCommit c4 = tr.commit().parent(c2).parent(c3).create(); + RevCommit c5 = tr.commit().parent(c0).create(); + RevCommit c6 = tr.commit().parent(c5).create(); + RevCommit c7 = tr.commit().parent(c6).create(); + RevCommit c8 = tr.commit().parent(c7).create(); + RevCommit c9 = tr.commit().parent(c8).create(); + tr.update("master", c4); + tr.update("branch", c9); + assertOneResult("branch~5", c0); + } + + private static void assertOneResult(String expected, NameRevCommand nameRev, + ObjectId id) throws Exception { + Map<ObjectId, String> result = nameRev.add(id).call(); + assertEquals(1, result.size()); + assertEquals(expected, result.get(id)); + } + + private void assertOneResult(String expected, ObjectId id) throws Exception { + assertOneResult(expected, git.nameRev(), id); + } +} 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 18f33147ce..677f735cec 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -385,7 +385,7 @@ pushIsNotSupportedForBundleTransport=Push is not supported for bundle transport pushNotPermitted=push not permitted rawLogMessageDoesNotParseAsLogEntry=Raw log message does not parse as log entry readingObjectsFromLocalRepositoryFailed=reading objects from local repository failed: {0} -readTimedOut=Read timed out +readTimedOut=Read timed out after {0} ms receivePackObjectTooLarge1=Object too large, rejecting the pack. Max object size limit is {0} bytes. receivePackObjectTooLarge2=Object too large ({0} bytes), rejecting the pack. Max object size limit is {1} bytes. receivingObjects=Receiving objects @@ -541,7 +541,7 @@ weeksAgo={0} weeks ago windowSizeMustBeLesserThanLimit=Window size must be < limit windowSizeMustBePowerOf2=Window size must be power of 2 writerAlreadyInitialized=Writer already initialized -writeTimedOut=Write timed out +writeTimedOut=Write timed out after {0} ms writingNotPermitted=Writing not permitted writingNotSupported=Writing {0} not supported. writingObjects=Writing objects diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java index 77f951e16f..d36b9bede4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java @@ -633,6 +633,16 @@ public class Git { } /** + * Returns a command object to find human-readable names of revisions. + * + * @return a {@link NameRevCommand}. + * @since 2.3 + */ + public NameRevCommand nameRev() { + return new NameRevCommand(repo); + } + + /** * @return the git repository this class is interacting with */ public Repository getRepository() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/NameRevCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/NameRevCommand.java new file mode 100644 index 0000000000..dfae50a725 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/NameRevCommand.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2013, 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.api; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.FIFORevQueue; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Command to find human-readable names of revisions. + * + * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-name-rev.html" + * >Git documentation about name-rev</a> + */ +public class NameRevCommand extends GitCommand<Map<ObjectId, String>> { + /** Amount of slop to allow walking past the earliest requested commit. */ + private static final int COMMIT_TIME_SLOP = 60 * 60 * 24; + + /** Cost of traversing a merge commit compared to a linear history. */ + private static final int MERGE_COST = 65535; + + private static class NameRevCommit extends RevCommit { + private String tip; + private int distance; + private long cost; + + private NameRevCommit(AnyObjectId id) { + super(id); + } + + private StringBuilder format() { + StringBuilder sb = new StringBuilder(tip); + if (distance > 0) + sb.append('~').append(distance); + return sb; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()) + .append('['); + if (tip != null) + sb.append(format()); + else + sb.append(String.valueOf(null)); + sb.append(',').append(cost).append(']').append(' ') + .append(super.toString()).toString(); + return sb.toString(); + } + } + + private final RevWalk walk; + private final List<String> prefixes; + private final List<ObjectId> revs; + + /** + * Create a new name-rev command. + * + * @param repo + */ + protected NameRevCommand(Repository repo) { + super(repo); + prefixes = new ArrayList<String>(2); + revs = new ArrayList<ObjectId>(2); + walk = new RevWalk(repo) { + @Override + public NameRevCommit createCommit(AnyObjectId id) { + return new NameRevCommit(id); + } + }; + } + + @Override + public Map<ObjectId, String> call() throws GitAPIException { + try { + Map<ObjectId, String> nonCommits = new HashMap<ObjectId, String>(); + FIFORevQueue pending = new FIFORevQueue(); + addPrefixes(nonCommits, pending); + int cutoff = minCommitTime() - COMMIT_TIME_SLOP; + + while (true) { + NameRevCommit c = (NameRevCommit) pending.next(); + if (c == null) + break; + if (c.getCommitTime() < cutoff) + continue; + boolean merge = c.getParentCount() > 1; + long cost = c.cost + (merge ? MERGE_COST : 1); + for (int i = 0; i < c.getParentCount(); i++) { + NameRevCommit p = (NameRevCommit) walk.parseCommit(c.getParent(i)); + if (p.tip == null || compare(c.tip, cost, p.tip, p.cost) < 0) { + if (merge) { + p.tip = c.format().append('^').append(i + 1).toString(); + p.distance = 0; + } else { + p.tip = c.tip; + p.distance = c.distance + 1; + } + p.cost = cost; + pending.add(p); + } + } + } + + Map<ObjectId, String> result = + new LinkedHashMap<ObjectId, String>(revs.size()); + for (ObjectId id : revs) { + RevObject o = walk.parseAny(id); + if (o instanceof NameRevCommit) { + NameRevCommit c = (NameRevCommit) o; + if (c.tip != null) + result.put(id, simplify(c.format().toString())); + } else { + String name = nonCommits.get(id); + if (name != null) + result.put(id, simplify(name)); + } + } + + setCallable(false); + walk.release(); + return result; + } catch (IOException e) { + walk.reset(); + throw new JGitInternalException(e.getMessage(), e); + } + } + + /** + * Add an object to search for. + * + * @param id + * object ID to add. + * @return {@code this} + * @throws MissingObjectException + * the object supplied is not available from the object + * database. + * @throws JGitInternalException + * a low-level exception of JGit has occurred. The original + * exception can be retrieved by calling + * {@link Exception#getCause()}. + */ + public NameRevCommand add(ObjectId id) throws MissingObjectException, + JGitInternalException { + checkCallable(); + try { + walk.parseAny(id); + } catch (MissingObjectException e) { + throw e; + } catch (IOException e) { + throw new JGitInternalException(e.getMessage(), e); + } + revs.add(id.copy()); + return this; + } + + /** + * Add multiple objects to search for. + * + * @param ids + * object IDs to add. + * @return {@code this} + * @throws MissingObjectException + * the object supplied is not available from the object + * database. + * @throws JGitInternalException + * a low-level exception of JGit has occurred. The original + * exception can be retrieved by calling + * {@link Exception#getCause()}. + */ + public NameRevCommand add(Iterable<ObjectId> ids) + throws MissingObjectException, JGitInternalException { + for (ObjectId id : ids) + add(id); + return this; + } + + /** + * Add a ref prefix that all results must match. + * <p> + * If an object matches refs under multiple prefixes equally well, the first + * prefix added to this command is preferred. + * + * @param prefix + * prefix to add; see {@link RefDatabase#getRefs(String)} + * @return {@code this} + */ + public NameRevCommand addPrefix(String prefix) { + checkCallable(); + prefixes.add(prefix); + return this; + } + + private void addPrefixes(Map<ObjectId, String> nonCommits, + FIFORevQueue pending) throws IOException { + if (!prefixes.isEmpty()) { + for (String prefix : prefixes) + addPrefix(prefix, nonCommits, pending); + } else + addPrefix(Constants.R_REFS, nonCommits, pending); + } + + private void addPrefix(String prefix, Map<ObjectId, String> nonCommits, + FIFORevQueue pending) throws IOException { + for (Ref ref : repo.getRefDatabase().getRefs(prefix).values()) { + if (ref.getObjectId() == null) + continue; + RevObject o = walk.parseAny(ref.getObjectId()); + while (o instanceof RevTag) { + RevTag t = (RevTag) o; + nonCommits.put(o, ref.getName()); + o = t.getObject(); + walk.parseHeaders(o); + } + if (o instanceof NameRevCommit) { + NameRevCommit c = (NameRevCommit) o; + if (c.tip == null) + c.tip = ref.getName(); + pending.add(c); + } else if (!nonCommits.containsKey(o)) + nonCommits.put(o, ref.getName()); + } + } + + private int minCommitTime() throws IOException { + int min = Integer.MAX_VALUE; + for (ObjectId id : revs) { + RevObject o = walk.parseAny(id); + while (o instanceof RevTag) { + o = ((RevTag) o).getObject(); + walk.parseHeaders(o); + } + if (o instanceof RevCommit) { + RevCommit c = (RevCommit) o; + if (c.getCommitTime() < min) + min = c.getCommitTime(); + } + } + return min; + } + + private long compare(String leftTip, long leftCost, String rightTip, long rightCost) { + long c = leftCost - rightCost; + if (c != 0 || prefixes.isEmpty()) + return c; + int li = -1; + int ri = -1; + for (int i = 0; i < prefixes.size(); i++) { + String prefix = prefixes.get(i); + if (li < 0 && leftTip.startsWith(prefix)) + li = i; + if (ri < 0 && rightTip.startsWith(prefix)) + ri = i; + } + // Don't tiebreak if prefixes are the same, in order to prefer first-parent + // paths. + return li - ri; + } + + private static String simplify(String refName) { + if (refName.startsWith(Constants.R_HEADS)) + return refName.substring(Constants.R_HEADS.length()); + if (refName.startsWith(Constants.R_TAGS)) + return refName.substring(Constants.R_TAGS.length()); + if (refName.startsWith(Constants.R_REFS)) + return refName.substring(Constants.R_REFS.length()); + return refName; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java index 68fee612fb..42851498b4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java @@ -437,6 +437,19 @@ public abstract class ObjectReader { } /** + * Advise the reader to avoid unreachable objects. + * <p> + * While enabled the reader will skip over anything previously proven to be + * unreachable. This may be dangerous in the face of concurrent writes. + * + * @param avoid + * true to avoid unreachable objects. + */ + public void setAvoidUnreachableObjects(boolean avoid) { + // Do nothing by default. + } + + /** * An index that can be used to speed up ObjectWalks. * * @return the index or null if one does not exist. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java index 937c76e20f..233856d745 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java @@ -70,6 +70,9 @@ public enum RepositoryState { public boolean canAmend() { return false; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return "Bare"; } }, @@ -90,6 +93,9 @@ public enum RepositoryState { public boolean canAmend() { return true; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return JGitText.get().repositoryState_normal; } }, @@ -109,6 +115,9 @@ public enum RepositoryState { public boolean canAmend() { return false; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return JGitText.get().repositoryState_conflicts; } }, @@ -130,6 +139,9 @@ public enum RepositoryState { public boolean canAmend() { return false; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return JGitText.get().repositoryState_merged; } }, @@ -149,6 +161,9 @@ public enum RepositoryState { public boolean canAmend() { return false; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return JGitText.get().repositoryState_conflicts; } }, @@ -170,6 +185,9 @@ public enum RepositoryState { public boolean canAmend() { return false; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return JGitText.get().repositoryState_merged; } }, @@ -189,6 +207,9 @@ public enum RepositoryState { public boolean canAmend() { return false; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return JGitText.get().repositoryState_conflicts; } }, @@ -210,6 +231,9 @@ public enum RepositoryState { public boolean canAmend() { return false; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return JGitText.get().repositoryState_merged; } }, @@ -230,6 +254,9 @@ public enum RepositoryState { public boolean canAmend() { return true; } @Override + public boolean isRebasing() { return true; } + + @Override public String getDescription() { return JGitText.get().repositoryState_rebaseOrApplyMailbox; } }, @@ -250,6 +277,9 @@ public enum RepositoryState { public boolean canAmend() { return true; } @Override + public boolean isRebasing() { return true; } + + @Override public String getDescription() { return JGitText.get().repositoryState_rebase; } }, @@ -270,6 +300,9 @@ public enum RepositoryState { public boolean canAmend() { return true; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return JGitText.get().repositoryState_applyMailbox; } }, @@ -290,6 +323,9 @@ public enum RepositoryState { public boolean canAmend() { return true; } @Override + public boolean isRebasing() { return true; } + + @Override public String getDescription() { return JGitText.get().repositoryState_rebaseWithMerge; } }, @@ -310,6 +346,9 @@ public enum RepositoryState { public boolean canAmend() { return true; } @Override + public boolean isRebasing() { return true; } + + @Override public String getDescription() { return JGitText.get().repositoryState_rebaseInteractive; } }, @@ -333,6 +372,9 @@ public enum RepositoryState { public boolean canAmend() { return false; } @Override + public boolean isRebasing() { return false; } + + @Override public String getDescription() { return JGitText.get().repositoryState_bisecting; } }; @@ -357,6 +399,12 @@ public enum RepositoryState { public abstract boolean canAmend(); /** + * @return true if the repository is currently in a rebase + * @since 2.4 + */ + public abstract boolean isRebasing(); + + /** * @return a human readable description of the state. */ public abstract String getDescription(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsGarbageCollector.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsGarbageCollector.java index 6027eadc42..76b36a416a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsGarbageCollector.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsGarbageCollector.java @@ -46,12 +46,11 @@ package org.eclipse.jgit.storage.dfs; import static org.eclipse.jgit.storage.dfs.DfsObjDatabase.PackSource.GC; import static org.eclipse.jgit.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE; import static org.eclipse.jgit.storage.pack.PackExt.BITMAP_INDEX; -import static org.eclipse.jgit.storage.pack.PackExt.PACK; import static org.eclipse.jgit.storage.pack.PackExt.INDEX; +import static org.eclipse.jgit.storage.pack.PackExt.PACK; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -69,6 +68,7 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.dfs.DfsObjDatabase.PackSource; import org.eclipse.jgit.storage.file.PackIndex; import org.eclipse.jgit.storage.pack.PackConfig; +import org.eclipse.jgit.storage.pack.PackExt; import org.eclipse.jgit.storage.pack.PackWriter; import org.eclipse.jgit.util.io.CountingOutputStream; @@ -90,6 +90,8 @@ public class DfsGarbageCollector { private PackConfig packConfig; + private long coalesceGarbageLimit = 50 << 20; + private Map<String, Ref> refsBefore; private List<DfsPackFile> packsBefore; @@ -139,6 +141,38 @@ public class DfsGarbageCollector { return this; } + /** @return garbage packs smaller than this size will be repacked. */ + public long getCoalesceGarbageLimit() { + return coalesceGarbageLimit; + } + + /** + * Set the byte size limit for garbage packs to be repacked. + * <p> + * Any UNREACHABLE_GARBAGE pack smaller than this limit will be repacked at + * the end of the run. This allows the garbage collector to coalesce + * unreachable objects into a single file. + * <p> + * If an UNREACHABLE_GARBAGE pack is already larger than this limit it will + * be left alone by the garbage collector. This avoids unnecessary disk IO + * reading and copying the objects. + * <p> + * If limit is set to 0 the UNREACHABLE_GARBAGE coalesce is disabled.<br> + * If limit is set to {@link Long#MAX_VALUE}, everything is coalesced. + * <p> + * Keeping unreachable garbage prevents race conditions with repository + * changes that may suddenly need an object whose only copy was stored in + * the UNREACHABLE_GARBAGE pack. + * + * @param limit + * size in bytes. + * @return {@code this} + */ + public DfsGarbageCollector setCoalesceGarbageLimit(long limit) { + coalesceGarbageLimit = limit; + return this; + } + /** * Create a single new pack file containing all of the live objects. * <p> @@ -167,7 +201,7 @@ public class DfsGarbageCollector { objdb.clearCache(); refsBefore = repo.getAllRefs(); - packsBefore = Arrays.asList(objdb.getPacks()); + packsBefore = packsToRebuild(); if (packsBefore.isEmpty()) return true; @@ -203,6 +237,19 @@ public class DfsGarbageCollector { } } + private List<DfsPackFile> packsToRebuild() throws IOException { + DfsPackFile[] packs = objdb.getPacks(); + List<DfsPackFile> out = new ArrayList<DfsPackFile>(packs.length); + for (DfsPackFile p : packs) { + DfsPackDescription d = p.getPackDescription(); + if (d.getPackSource() != UNREACHABLE_GARBAGE) + out.add(p); + else if (d.getFileSize(PackExt.PACK) < coalesceGarbageLimit) + out.add(p); + } + return out; + } + /** @return all of the source packs that fed into this compaction. */ public List<DfsPackDescription> getSourcePacks() { return toPrune(); @@ -264,9 +311,9 @@ public class DfsGarbageCollector { PackWriter pw = newPackWriter(); try { RevWalk pool = new RevWalk(ctx); + pm.beginTask("Finding garbage", (int) getObjectsBefore()); for (DfsPackFile oldPack : packsBefore) { PackIndex oldIdx = oldPack.getPackIndex(ctx); - pm.beginTask("Finding garbage", (int) oldIdx.getObjectCount()); for (PackIndex.MutableEntry ent : oldIdx) { pm.update(1); ObjectId id = ent.toObjectId(); @@ -276,8 +323,8 @@ public class DfsGarbageCollector { int type = oldPack.getObjectType(ctx, ent.getOffset()); pw.addObject(pool.lookupAny(id, type)); } - pm.endTask(); } + pm.endTask(); if (0 < pw.getObjectCount()) writePack(UNREACHABLE_GARBAGE, pw, pm); } finally { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsPackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsPackFile.java index 80cced84e8..707488b039 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsPackFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsPackFile.java @@ -45,9 +45,10 @@ package org.eclipse.jgit.storage.dfs; +import static org.eclipse.jgit.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE; import static org.eclipse.jgit.storage.pack.PackExt.BITMAP_INDEX; -import static org.eclipse.jgit.storage.pack.PackExt.PACK; import static org.eclipse.jgit.storage.pack.PackExt.INDEX; +import static org.eclipse.jgit.storage.pack.PackExt.PACK; import java.io.BufferedInputStream; import java.io.EOFException; @@ -276,8 +277,12 @@ public final class DfsPackFile { } } + final boolean isGarbage() { + return packDesc.getPackSource() == UNREACHABLE_GARBAGE; + } + PackBitmapIndex getBitmapIndex(DfsReader ctx) throws IOException { - if (invalid) + if (invalid || isGarbage()) return null; DfsBlockCache.Ref<PackBitmapIndex> idxref = bitmapIndex; if (idxref != null) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsReader.java index 7d5fb5b2a5..401c483d4d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/dfs/DfsReader.java @@ -118,6 +118,8 @@ public final class DfsReader extends ObjectReader implements ObjectReuseAsIs { private boolean wantReadAhead; + private boolean avoidUnreachable; + private List<ReadAheadTask.BlockFuture> pendingReadAhead; DfsReader(DfsObjDatabase db) { @@ -144,6 +146,11 @@ public final class DfsReader extends ObjectReader implements ObjectReuseAsIs { } @Override + public void setAvoidUnreachableObjects(boolean avoid) { + avoidUnreachable = avoid; + } + + @Override public BitmapIndex getBitmapIndex() throws IOException { for (DfsPackFile pack : db.getPacks()) { PackBitmapIndex bitmapIndex = pack.getBitmapIndex(this); @@ -169,8 +176,11 @@ public final class DfsReader extends ObjectReader implements ObjectReuseAsIs { throws IOException { if (id.isComplete()) return Collections.singleton(id.toObjectId()); + boolean noGarbage = avoidUnreachable; HashSet<ObjectId> matches = new HashSet<ObjectId>(4); for (DfsPackFile pack : db.getPacks()) { + if (noGarbage && pack.isGarbage()) + continue; pack.resolve(this, matches, id, 256); if (256 <= matches.size()) break; @@ -182,8 +192,9 @@ public final class DfsReader extends ObjectReader implements ObjectReuseAsIs { public boolean has(AnyObjectId objectId) throws IOException { if (last != null && last.hasObject(this, objectId)) return true; + boolean noGarbage = avoidUnreachable; for (DfsPackFile pack : db.getPacks()) { - if (last == pack) + if (pack == last || (noGarbage && pack.isGarbage())) continue; if (pack.hasObject(this, objectId)) { last = pack; @@ -203,8 +214,9 @@ public final class DfsReader extends ObjectReader implements ObjectReuseAsIs { return ldr; } + boolean noGarbage = avoidUnreachable; for (DfsPackFile pack : db.getPacks()) { - if (pack == last) + if (pack == last || (noGarbage && pack.isGarbage())) continue; ObjectLoader ldr = pack.get(this, objectId); if (ldr != null) { @@ -265,6 +277,7 @@ public final class DfsReader extends ObjectReader implements ObjectReuseAsIs { int lastIdx = 0; DfsPackFile lastPack = packList[lastIdx]; + boolean noGarbage = avoidUnreachable; OBJECT_SCAN: for (T t : objectIds) { try { @@ -281,6 +294,8 @@ public final class DfsReader extends ObjectReader implements ObjectReuseAsIs { if (i == lastIdx) continue; DfsPackFile pack = packList[i]; + if (noGarbage && pack.isGarbage()) + continue; try { long p = pack.findOffset(this, t); if (0 < p) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java index bc40454166..12f6886588 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -787,74 +787,24 @@ public class UploadPack { preUploadHook.onBeginNegotiateRound(this, wantIds, peerHas.size()); if (peerHas.isEmpty()) return last; + if (wantAll.isEmpty() && !wantIds.isEmpty()) + parseWants(); - List<ObjectId> toParse = peerHas; - HashSet<ObjectId> peerHasSet = null; - boolean needMissing = false; sentReady = false; - - if (wantAll.isEmpty() && !wantIds.isEmpty()) { - // We have not yet parsed the want list. Parse it now. - peerHasSet = new HashSet<ObjectId>(peerHas); - int cnt = wantIds.size() + peerHasSet.size(); - toParse = new ArrayList<ObjectId>(cnt); - toParse.addAll(wantIds); - toParse.addAll(peerHasSet); - needMissing = true; - } - - Set<RevObject> notAdvertisedWants = null; int haveCnt = 0; - AsyncRevObjectQueue q = walk.parseAny(toParse, needMissing); + walk.getObjectReader().setAvoidUnreachableObjects(true); + AsyncRevObjectQueue q = walk.parseAny(peerHas, false); try { for (;;) { RevObject obj; try { obj = q.next(); } catch (MissingObjectException notFound) { - ObjectId id = notFound.getObjectId(); - if (wantIds.contains(id)) { - String msg = MessageFormat.format( - JGitText.get().wantNotValid, id.name()); - throw new PackProtocolException(msg, notFound); - } continue; } if (obj == null) break; - // If the object is still found in wantIds, the want - // list wasn't parsed earlier, and was done in this batch. - // - if (wantIds.remove(obj)) { - if (!advertised.contains(obj) && requestPolicy != RequestPolicy.ANY) { - if (notAdvertisedWants == null) - notAdvertisedWants = new HashSet<RevObject>(); - notAdvertisedWants.add(obj); - } - - if (!obj.has(WANT)) { - obj.add(WANT); - wantAll.add(obj); - } - - if (!(obj instanceof RevCommit)) - obj.add(SATISFIED); - - if (obj instanceof RevTag) { - RevObject target = walk.peel(obj); - if (target instanceof RevCommit) { - if (!target.has(WANT)) { - target.add(WANT); - wantAll.add(target); - } - } - } - - if (!peerHasSet.contains(obj)) - continue; - } - last = obj; haveCnt++; @@ -889,25 +839,7 @@ public class UploadPack { } } finally { q.release(); - } - - // If the client asked for non advertised object, check our policy. - if (notAdvertisedWants != null && !notAdvertisedWants.isEmpty()) { - switch (requestPolicy) { - case ADVERTISED: - default: - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, - notAdvertisedWants.iterator().next().name())); - - case REACHABLE_COMMIT: - checkNotAdvertisedWants(notAdvertisedWants); - break; - - case ANY: - // Allow whatever was asked for. - break; - } + walk.getObjectReader().setAvoidUnreachableObjects(false); } int missCnt = peerHas.size() - haveCnt; @@ -952,7 +884,61 @@ public class UploadPack { return last; } - private void checkNotAdvertisedWants(Set<RevObject> notAdvertisedWants) + private void parseWants() throws IOException { + AsyncRevObjectQueue q = walk.parseAny(wantIds, true); + try { + List<RevCommit> checkReachable = null; + RevObject obj; + while ((obj = q.next()) != null) { + if (!advertised.contains(obj)) { + switch (requestPolicy) { + case ADVERTISED: + default: + throw new PackProtocolException(MessageFormat.format( + JGitText.get().wantNotValid, obj)); + case REACHABLE_COMMIT: + if (!(obj instanceof RevCommit)) { + throw new PackProtocolException(MessageFormat.format( + JGitText.get().wantNotValid, obj)); + } + if (checkReachable == null) + checkReachable = new ArrayList<RevCommit>(); + checkReachable.add((RevCommit) obj); + break; + case ANY: + break; + } + } + want(obj); + + if (!(obj instanceof RevCommit)) + obj.add(SATISFIED); + if (obj instanceof RevTag) { + obj = walk.peel(obj); + if (obj instanceof RevCommit) + want(obj); + } + } + if (checkReachable != null) + checkNotAdvertisedWants(checkReachable); + wantIds.clear(); + } catch (MissingObjectException notFound) { + ObjectId id = notFound.getObjectId(); + throw new PackProtocolException(MessageFormat.format( + JGitText.get().wantNotValid, id.name()), notFound); + } finally { + q.release(); + } + } + + private void want(RevObject obj) { + if (!obj.has(WANT)) { + obj.add(WANT); + wantAll.add(obj); + } + } + + private void checkNotAdvertisedWants(List<RevCommit> notAdvertisedWants) throws MissingObjectException, IncorrectObjectTypeException, IOException { // Walk the requested commits back to the advertised commits. // If any commit exists, a branch was deleted or rewound and @@ -960,15 +946,8 @@ public class UploadPack { // If the requested commit is merged into an advertised branch // it will be marked UNINTERESTING and no commits return. - for (RevObject o : notAdvertisedWants) { - if (!(o instanceof RevCommit)) { - throw new PackProtocolException(MessageFormat.format( - JGitText.get().wantNotValid, - notAdvertisedWants.iterator().next().name())); - } - walk.markStart((RevCommit) o); - } - + for (RevCommit c : notAdvertisedWants) + walk.markStart(c); for (ObjectId id : advertised) { try { walk.markUninteresting(walk.parseCommit(id)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutInputStream.java index cda2b59b43..fe452c255d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009, 2013 Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -137,7 +137,8 @@ public class TimeoutInputStream extends FilterInputStream { myTimer.end(); } - private static InterruptedIOException readTimedOut() { - return new InterruptedIOException(JGitText.get().readTimedOut); + private InterruptedIOException readTimedOut() { + return new InterruptedIOException(MessageFormat.format( + JGitText.get().readTimedOut, Integer.valueOf(timeout))); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutOutputStream.java index b074396a75..7ca11ca055 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009, 2013 Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -150,7 +150,8 @@ public class TimeoutOutputStream extends OutputStream { myTimer.end(); } - private static InterruptedIOException writeTimedOut() { - return new InterruptedIOException(JGitText.get().writeTimedOut); + private InterruptedIOException writeTimedOut() { + return new InterruptedIOException(MessageFormat.format( + JGitText.get().writeTimedOut, Integer.valueOf(timeout))); } } |