diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java index c07ee517bd..0c7df3e495 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java @@ -20,6 +20,7 @@ import com.google.gerrit.httpd.rpc.Handler; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.change.IncludedInResolver; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; @@ -29,23 +30,15 @@ import com.google.inject.assistedinject.Assisted; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; /** Creates a {@link IncludedInDetail} of a {@link Change}. */ class IncludedInDetailFactory extends Handler { - private static final Logger log = - LoggerFactory.getLogger(IncludedInDetailFactory.class); interface Factory { IncludedInDetailFactory create(Change.Id id); @@ -56,7 +49,6 @@ class IncludedInDetailFactory extends Handler { private final GitRepositoryManager repoManager; private final Change.Id changeId; - private IncludedInDetail detail; private ChangeControl control; @Inject @@ -92,11 +84,7 @@ class IncludedInDetailFactory extends Handler { throw new InvalidRevisionException(); } - detail = new IncludedInDetail(); - detail.setBranches(includedIn(repo, rw, rev, Constants.R_HEADS)); - detail.setTags(includedIn(repo, rw, rev, Constants.R_TAGS)); - - return detail; + return IncludedInResolver.resolve(repo, rw, rev); } finally { rw.release(); } @@ -104,32 +92,4 @@ class IncludedInDetailFactory extends Handler { repo.close(); } } - - private List includedIn(final Repository repo, final RevWalk rw, - final RevCommit rev, final String namespace) throws IOException, - MissingObjectException, IncorrectObjectTypeException { - final List result = new ArrayList(); - for (final Ref ref : repo.getRefDatabase().getRefs(namespace).values()) { - final RevCommit tip; - try { - tip = rw.parseCommit(ref.getObjectId()); - } catch (IncorrectObjectTypeException notCommit) { - // Its OK for a tag reference to point to a blob or a tree, this - // is common in the Linux kernel or git.git repository. - // - continue; - } catch (MissingObjectException notHere) { - // Log the problem with this branch, but keep processing. - // - log.warn("Reference " + ref.getName() + " in " + repo.getDirectory() - + " points to dangling object " + ref.getObjectId()); - continue; - } - - if (rw.isMergedInto(rev, tip)) { - result.add(ref.getName().substring(namespace.length())); - } - } - return result; - } -} +} \ No newline at end of file diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java new file mode 100644 index 0000000000..b02f6f0fb5 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java @@ -0,0 +1,159 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.change; + +import com.google.gerrit.common.data.IncludedInDetail; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Resolve in which tags and branches a commit is included. + */ +public class IncludedInResolver { + + private static final Logger log = LoggerFactory + .getLogger(IncludedInResolver.class); + + public static IncludedInDetail resolve(final Repository repo, + final RevWalk rw, final RevCommit commit) throws IOException { + + Set tags = + new HashSet(repo.getRefDatabase().getRefs(Constants.R_TAGS) + .values()); + Set branches = + new HashSet(repo.getRefDatabase().getRefs(Constants.R_HEADS) + .values()); + Set allTagsAndBranches = new HashSet(); + allTagsAndBranches.addAll(tags); + allTagsAndBranches.addAll(branches); + Set allMatchingTagsAndBranches = + includedIn(repo, rw, commit, allTagsAndBranches); + + IncludedInDetail detail = new IncludedInDetail(); + detail + .setBranches(getMatchingRefNames(allMatchingTagsAndBranches, branches)); + detail.setTags(getMatchingRefNames(allMatchingTagsAndBranches, tags)); + + return detail; + } + + /** + * Resolves which tip refs include the target commit. + */ + private static Set includedIn(final Repository repo, final RevWalk rw, + final RevCommit target, final Set tipRefs) throws IOException, + MissingObjectException, IncorrectObjectTypeException { + + Set result = new HashSet(); + + Map> tipsAndCommits = parseCommits(repo, rw, tipRefs); + + List tips = new ArrayList(tipsAndCommits.keySet()); + Collections.sort(tips, new Comparator() { + @Override + public int compare(RevCommit c1, RevCommit c2) { + return c1.getCommitTime() - c2.getCommitTime(); + } + }); + + Set targetReachableFrom = new HashSet(); + targetReachableFrom.add(target); + + for (RevCommit tip : tips) { + boolean commitFound = false; + rw.resetRetain(RevFlag.UNINTERESTING); + rw.markStart(tip); + for (RevCommit commit : rw) { + if (targetReachableFrom.contains(commit)) { + commitFound = true; + targetReachableFrom.add(tip); + result.addAll(tipsAndCommits.get(tip)); + break; + } + } + if (!commitFound) { + rw.markUninteresting(tip); + } + } + + return result; + } + + /** + * Returns the short names of refs which are as well in the matchingRefs list + * as well as in the allRef list. + */ + private static List getMatchingRefNames(Set matchingRefs, + Set allRefs) { + List refNames = new ArrayList(); + for (Ref matchingRef : matchingRefs) { + if (allRefs.contains(matchingRef)) { + refNames.add(Repository.shortenRefName(matchingRef.getName())); + } + } + return refNames; + } + + /** + * Parse commit of ref and store the relation between ref and commit. + */ + private static Map> parseCommits(final Repository repo, + final RevWalk rw, final Set refs) throws IOException { + Map> result = new HashMap>(); + for (Ref ref : refs) { + final RevCommit commit; + try { + commit = rw.parseCommit(ref.getObjectId()); + } catch (IncorrectObjectTypeException notCommit) { + // Its OK for a tag reference to point to a blob or a tree, this + // is common in the Linux kernel or git.git repository. + // + continue; + } catch (MissingObjectException notHere) { + // Log the problem with this branch, but keep processing. + // + log.warn("Reference " + ref.getName() + " in " + repo.getDirectory() + + " points to dangling object " + ref.getObjectId()); + continue; + } + Set relatedRefs = result.get(commit); + if (relatedRefs == null) { + relatedRefs = new HashSet(); + result.put(commit, relatedRefs); + } + relatedRefs.add(ref); + } + return result; + } +} diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java new file mode 100644 index 0000000000..ae58819900 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java @@ -0,0 +1,205 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.change; + +import com.google.gerrit.common.data.IncludedInDetail; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeCommand.FastForwardMode; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class IncludedInResolverTest extends RepositoryTestCase { + + // Branch names + private static final String BRANCH_MASTER = "master"; + private static final String BRANCH_1_0 = "rel-1.0"; + private static final String BRANCH_1_3 = "rel-1.3"; + private static final String BRANCH_2_0 = "rel-2.0"; + private static final String BRANCH_2_5 = "rel-2.5"; + + // Tag names + private static final String TAG_1_0 = "1.0"; + private static final String TAG_1_0_1 = "1.0.1"; + private static final String TAG_1_3 = "1.3"; + private static final String TAG_2_0_1 = "2.0.1"; + private static final String TAG_2_0 = "2.0"; + private static final String TAG_2_5 = "2.5"; + private static final String TAG_2_5_ANNOTATED = "2.5-annotated"; + private static final String TAG_2_5_ANNOTATED_TWICE = "2.5-annotated_twice"; + + // Commits + private RevCommit commit_initial; + private RevCommit commit_v1_3; + private RevCommit commit_v2_5; + + private List expTags = new ArrayList(); + private List expBranches = new ArrayList(); + + private RevWalk revWalk; + + @Before + public void setUp() throws Exception { + super.setUp(); + + /*- The following graph will be created. + + o tag 2.5, 2.5_annotated, 2.5_annotated_twice + |\ + | o tag 2.0.1 + | o tag 2.0 + o | tag 1.3 + |/ + o c3 + + | o tag 1.0.1 + |/ + o tag 1.0 + o c2 + o c1 + + */ + + Git git = new Git(db); + revWalk = new RevWalk(db); + // Version 1.0 + commit_initial = git.commit().setMessage("c1").call(); + git.commit().setMessage("c2").call(); + RevCommit commit_v1_0 = git.commit().setMessage("version 1.0").call(); + git.tag().setName(TAG_1_0).setObjectId(commit_v1_0).call(); + RevCommit c3 = git.commit().setMessage("c3").call(); + // Version 1.01 + createAndCheckoutBranch(commit_v1_0, BRANCH_1_0); + RevCommit commit_v1_0_1 = + git.commit().setMessage("verREFS_HEADS_RELsion 1.0.1").call(); + git.tag().setName(TAG_1_0_1).setObjectId(commit_v1_0_1).call(); + // Version 1.3 + createAndCheckoutBranch(c3, BRANCH_1_3); + commit_v1_3 = git.commit().setMessage("version 1.3").call(); + git.tag().setName(TAG_1_3).setObjectId(commit_v1_3).call(); + // Version 2.0 + createAndCheckoutBranch(c3, BRANCH_2_0); + RevCommit commit_v2_0 = git.commit().setMessage("version 2.0").call(); + git.tag().setName(TAG_2_0).setObjectId(commit_v2_0).call(); + RevCommit commit_v2_0_1 = git.commit().setMessage("version 2.0.1").call(); + git.tag().setName(TAG_2_0_1).setObjectId(commit_v2_0_1).call(); + + // Version 2.5 + createAndCheckoutBranch(commit_v1_3, BRANCH_2_5); + git.merge().include(commit_v2_0_1).setCommit(false) + .setFastForward(FastForwardMode.NO_FF).call(); + commit_v2_5 = git.commit().setMessage("version 2.5").call(); + git.tag().setName(TAG_2_5).setObjectId(commit_v2_5).setAnnotated(false) + .call(); + Ref ref_tag_2_5_annotated = + git.tag().setName(TAG_2_5_ANNOTATED).setObjectId(commit_v2_5) + .setAnnotated(true).call(); + RevTag tag_2_5_annotated = + revWalk.parseTag(ref_tag_2_5_annotated.getObjectId()); + git.tag().setName(TAG_2_5_ANNOTATED_TWICE).setObjectId(tag_2_5_annotated) + .setAnnotated(true).call(); + } + + @After + public void tearDown() throws Exception { + revWalk.release(); + super.tearDown(); + } + + @Test + public void resolveLatestCommit() throws Exception { + // Check tip commit + IncludedInDetail detail = resolve(commit_v2_5); + + // Check that only tags and branches which refer the tip are returned + expTags.add(TAG_2_5); + expTags.add(TAG_2_5_ANNOTATED); + expTags.add(TAG_2_5_ANNOTATED_TWICE); + assertEquals(expTags, detail.getTags()); + expBranches.add(BRANCH_2_5); + assertEquals(expBranches, detail.getBranches()); + } + + @Test + public void resolveFirstCommit() throws Exception { + // Check first commit + IncludedInDetail detail = resolve(commit_initial); + + // Check whether all tags and branches are returned + expTags.add(TAG_1_0); + expTags.add(TAG_1_0_1); + expTags.add(TAG_1_3); + expTags.add(TAG_2_0); + expTags.add(TAG_2_0_1); + expTags.add(TAG_2_5); + expTags.add(TAG_2_5_ANNOTATED); + expTags.add(TAG_2_5_ANNOTATED_TWICE); + assertEquals(expTags, detail.getTags()); + + expBranches.add(BRANCH_MASTER); + expBranches.add(BRANCH_1_0); + expBranches.add(BRANCH_1_3); + expBranches.add(BRANCH_2_0); + expBranches.add(BRANCH_2_5); + assertEquals(expBranches, detail.getBranches()); + } + + @Test + public void resolveBetwixtCommit() throws Exception { + // Check a commit somewhere in the middle + IncludedInDetail detail = resolve(commit_v1_3); + + // Check whether all succeeding tags and branches are returned + expTags.add(TAG_1_3); + expTags.add(TAG_2_5); + expTags.add(TAG_2_5_ANNOTATED); + expTags.add(TAG_2_5_ANNOTATED_TWICE); + assertEquals(expTags, detail.getTags()); + + expBranches.add(BRANCH_1_3); + expBranches.add(BRANCH_2_5); + assertEquals(expBranches, detail.getBranches()); + } + + private IncludedInDetail resolve(RevCommit commit) throws Exception { + return IncludedInResolver.resolve(db, revWalk, commit); + } + + private void assertEquals(List list1, List list2) { + Collections.sort(list1); + Collections.sort(list2); + Assert.assertEquals(list1, list2); + } + + private void createAndCheckoutBranch(ObjectId objectId, String branchName) + throws IOException { + String fullBranchName = "refs/heads/" + branchName; + super.createBranch(objectId, fullBranchName); + super.checkoutBranch(fullBranchName); + } +}