diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java index 08d6783b4b..764d61d60c 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java @@ -113,6 +113,9 @@ public interface ReviewDb extends Schema { @Relation(id = 27) TrackingIdAccess trackingIds(); + @Relation(id = 28) + SubmoduleSubscriptionAccess submoduleSubscriptions(); + /** Create the next unique id for an {@link Account}. */ @Sequence(startWith = 1000000) int nextAccountId() throws OrmException; diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscription.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscription.java new file mode 100644 index 0000000000..b6c27bd33b --- /dev/null +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscription.java @@ -0,0 +1,117 @@ +// Copyright (C) 2011 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.reviewdb; + +import com.google.gwtorm.client.Column; +import com.google.gwtorm.client.CompoundKey; + +/** + * Defining a project/branch subscription to a project/branch project. + *

+ * This means a class instance represents a repo/branch subscription to a + * project/branch (the subscriber). + *

+ * A subscriber operates a submodule in defined path. + */ +public final class SubmoduleSubscription { + /** Subscription key */ + public static class Key extends CompoundKey { + private static final long serialVersionUID = 1L; + + /** + * Indicates the super project, aka subscriber: the project owner of the + * gitlinks to the submodules. + */ + @Column(id = 1) + protected Branch.NameKey superProject; + + /** + * Indicates the submodule, aka subscription: the project the subscriber's + * gitlink is pointed to. + */ + @Column(id = 2) + protected Branch.NameKey submodule; + + protected Key() { + superProject = new Branch.NameKey(); + submodule = new Branch.NameKey(); + } + + protected Key(final Branch.NameKey superProject, + final Branch.NameKey submodule) { + this.superProject = superProject; + this.submodule = submodule; + } + + @Override + public Branch.NameKey getParentKey() { + return superProject; + } + + @Override + public com.google.gwtorm.client.Key[] members() { + return new com.google.gwtorm.client.Key[] {submodule}; + } + + } + + @Column(id = 1, name = Column.NONE) + protected Key key; + + @Column(id = 2) + protected String path; + + protected SubmoduleSubscription() { + } + + public SubmoduleSubscription(final Branch.NameKey superProject, + final Branch.NameKey submodule, final String path) { + key = new Key(superProject, submodule); + this.path = path; + } + + @Override + public String toString() { + return key.superProject.getParentKey().get() + " " + key.superProject.get() + + ", " + key.submodule.getParentKey().get() + " " + + key.submodule.get() + ", " + path; + } + + public Branch.NameKey getSuperProject() { + return key.superProject; + } + + public Branch.NameKey getSubmodule() { + return key.submodule; + } + + public String getPath() { + return path; + } + + @Override + public boolean equals(Object o) { + if (o instanceof SubmoduleSubscription) { + return key.equals(((SubmoduleSubscription) o).key) + && path.equals(((SubmoduleSubscription) o).path); + } + return false; + } + + @Override + public int hashCode() { + return key.hashCode(); + } +} diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscriptionAccess.java new file mode 100644 index 0000000000..126da78d71 --- /dev/null +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscriptionAccess.java @@ -0,0 +1,38 @@ +// Copyright (C) 2011 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.reviewdb; + +import com.google.gwtorm.client.Access; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.PrimaryKey; +import com.google.gwtorm.client.Query; +import com.google.gwtorm.client.ResultSet; + +public interface SubmoduleSubscriptionAccess extends + Access { + @PrimaryKey("key") + SubmoduleSubscription get(SubmoduleSubscription.Key key) throws OrmException; + + @Query("ORDER BY key.superProject.projectName") + ResultSet all() throws OrmException; + + @Query("WHERE key.superProject = ?") + ResultSet bySuperProject(Branch.NameKey superProject) + throws OrmException; + + @Query("WHERE key.submodule = ?") + ResultSet bySubmodule(Branch.NameKey submodule) + throws OrmException; +} diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql index 9784a4dcd4..83f7d1569e 100644 --- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql +++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql @@ -170,3 +170,9 @@ ON tracking_ids (tracking_id); CREATE INDEX starred_changes_byChange ON starred_changes (change_id); + +-- ********************************************************************* +-- SubmoduleSubscriptionAccess + +CREATE INDEX submodule_subscription_access_bySubscription +ON submodule_subscriptions (submodule_project_name, submodule_branch_name); diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql index db6894defa..3afa7ef0c7 100644 --- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql +++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql @@ -252,3 +252,9 @@ ON tracking_ids (tracking_id); CREATE INDEX starred_changes_byChange ON starred_changes (change_id); + +-- ********************************************************************* +-- SubmoduleSubscriptionAccess + +CREATE INDEX submodule_subscription_access_bySubscription +ON submodule_subscriptions (submodule_project_name, submodule_branch_name); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java index 70e370a1ff..089cd78398 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java @@ -31,6 +31,7 @@ import com.google.gerrit.server.git.CreateCodeReviewNotes; import com.google.gerrit.server.git.MergeOp; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ReceiveCommits; +import com.google.gerrit.server.git.SubmoduleOp; import com.google.gerrit.server.mail.AbandonedSender; import com.google.gerrit.server.mail.AddReviewerSender; import com.google.gerrit.server.mail.CommentSender; @@ -71,6 +72,7 @@ public class GerritRequestModule extends FactoryModule { factory(ChangeQueryBuilder.Factory.class); factory(ReceiveCommits.Factory.class); + factory(SubmoduleOp.Factory.class); factory(MergeOp.Factory.class); factory(CreateCodeReviewNotes.Factory.class); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java index 7e080cf7e1..6bf69f693c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java @@ -163,6 +163,7 @@ public class MergeOp { private final AccountCache accountCache; private final TagCache tagCache; private final CreateCodeReviewNotes.Factory codeReviewNotesFactory; + private final SubmoduleOp.Factory subOpFactory; @Inject MergeOp(final GitRepositoryManager grm, final SchemaFactory sf, @@ -176,7 +177,8 @@ public class MergeOp { @GerritPersonIdent final PersonIdent myIdent, final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch, final ChangeHookRunner hooks, final AccountCache accountCache, - final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf) { + final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf, + final SubmoduleOp.Factory subOpFactory) { repoManager = grm; schemaFactory = sf; functionState = fs; @@ -194,6 +196,7 @@ public class MergeOp { this.accountCache = accountCache; this.tagCache = tagCache; codeReviewNotesFactory = crnf; + this.subOpFactory = subOpFactory; this.myIdent = myIdent; destBranch = branch; @@ -269,6 +272,7 @@ public class MergeOp { preMerge(); updateBranch(); updateChangeStatus(); + updateSubscriptions(); } catch (OrmException e) { throw new MergeException("Cannot query the database", e); } finally { @@ -1118,6 +1122,21 @@ public class MergeOp { GitRepositoryManager.REFS_NOTES_REVIEW); } + private void updateSubscriptions() throws MergeException { + if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) { + SubmoduleOp subOp = + subOpFactory.create(destBranch, mergeTip, rw, db, destProject, + submitted, commits); + try { + subOp.update(); + } catch (SubmoduleException e) { + log + .error("The gitLinks were not updated according to the subscriptions " + + e.getMessage()); + } + } + } + private Capable isSubmitStillPossible(final CodeReviewCommit commit) { final Capable capable; final Change c = commit.change; diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java index fb81fe76ec..0d5bbb4a48 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java @@ -153,6 +153,8 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { private String destTopicName; + private final SubmoduleOp.Factory subOpFactory; + @Inject ReceiveCommits(final ReviewDb db, final ApprovalTypes approvalTypes, final AccountResolver accountResolver, @@ -170,7 +172,8 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { final TrackingFooters trackingFooters, @Assisted final ProjectControl projectControl, - @Assisted final Repository repo) throws IOException { + @Assisted final Repository repo, + final SubmoduleOp.Factory subOpFactory) throws IOException { this.currentUser = (IdentifiedUser) projectControl.getCurrentUser(); this.db = db; this.approvalTypes = approvalTypes; @@ -194,6 +197,8 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { this.rp = new ReceivePack(repo); this.rejectCommits = loadRejectCommitsMap(); + this.subOpFactory = subOpFactory; + rp.setAllowCreates(true); rp.setAllowDeletes(true); rp.setAllowNonFastForwards(true); @@ -1743,10 +1748,24 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { closeChange(req.cmd, psi, req.newCommit); } } + + // It handles gitlinks if required. + + rw.reset(); + final RevCommit codeReviewCommit = rw.parseCommit(cmd.getNewId()); + + final SubmoduleOp subOp = + subOpFactory.create( + new Branch.NameKey(project.getNameKey(), cmd.getRefName()), + codeReviewCommit, rw, repo, project, new ArrayList(), + new HashMap()); + subOp.update(); } catch (IOException e) { log.error("Can't scan for changes to close", e); } catch (OrmException e) { log.error("Can't scan for changes to close", e); + } catch (SubmoduleException e) { + log.error("Can't complete git links check", e); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java new file mode 100644 index 0000000000..d7e8446408 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java @@ -0,0 +1,28 @@ +// Copyright (C) 2011 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.git; + +/** Indicates the gitlink's update cannot be processed at this time. */ +class SubmoduleException extends Exception { + private static final long serialVersionUID = 1L; + + SubmoduleException(final String msg) { + super(msg, null); + } + + SubmoduleException(final String msg, final Throwable why) { + super(msg, why); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java new file mode 100644 index 0000000000..a7a43b1b0c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java @@ -0,0 +1,379 @@ +// Copyright (C) 2011 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.git; + +import com.google.gerrit.reviewdb.Branch; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.reviewdb.SubmoduleSubscription; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.util.SubmoduleSectionParser; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.client.SchemaFactory; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; + +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.BlobBasedConfig; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +public class SubmoduleOp { + public interface Factory { + SubmoduleOp create(Branch.NameKey destBranch, RevCommit mergeTip, + RevWalk rw, Repository db, Project destProject, List submitted, + Map commits); + } + + private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class); + private static final String GIT_MODULES = ".gitmodules"; + + private final Branch.NameKey destBranch; + private RevCommit mergeTip; + private RevWalk rw; + private final Provider urlProvider; + private ReviewDb schema; + private Repository db; + private Project destProject; + private List submitted; + private final Map commits; + private final PersonIdent myIdent; + private final GitRepositoryManager repoManager; + private final ReplicationQueue replication; + private final SchemaFactory schemaFactory; + private final Set updatedSubscribers; + + @Inject + public SubmoduleOp(@Assisted final Branch.NameKey destBranch, + @Assisted RevCommit mergeTip, @Assisted RevWalk rw, + @CanonicalWebUrl @Nullable final Provider urlProvider, + final SchemaFactory sf, @Assisted Repository db, + @Assisted Project destProject, @Assisted List submitted, + @Assisted final Map commits, + @GerritPersonIdent final PersonIdent myIdent, + GitRepositoryManager repoManager, ReplicationQueue replication) { + this.destBranch = destBranch; + this.mergeTip = mergeTip; + this.rw = rw; + this.urlProvider = urlProvider; + this.schemaFactory = sf; + this.db = db; + this.destProject = destProject; + this.submitted = submitted; + this.commits = commits; + this.myIdent = myIdent; + this.repoManager = repoManager; + this.replication = replication; + + updatedSubscribers = new HashSet(); + } + + public void update() throws SubmoduleException { + try { + schema = schemaFactory.open(); + + updateSubmoduleSubscriptions(); + updateSuperProjects(destBranch, mergeTip.getId().toObjectId(), null); + } catch (OrmException e) { + throw new SubmoduleException("Cannot open database", e); + } finally { + if (schema != null) { + schema.close(); + schema = null; + } + } + } + + private void updateSubmoduleSubscriptions() throws SubmoduleException { + if (urlProvider.get() == null) { + logAndThrowSubmoduleException("Cannot establish canonical web url used to access gerrit." + + " It should be provided in gerrit.config file."); + } + + try { + final TreeWalk tw = TreeWalk.forPath(db, GIT_MODULES, mergeTip.getTree()); + if (tw != null + && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) || FileMode.EXECUTABLE_FILE + .equals(tw.getRawMode(0)))) { + + BlobBasedConfig bbc = + new BlobBasedConfig(null, db, mergeTip, GIT_MODULES); + + final String thisServer = new URI(urlProvider.get()).getHost(); + + final Branch.NameKey target = + new Branch.NameKey(new Project.NameKey(destProject.getName()), + destBranch.get()); + + final Set oldSubscriptions = + new HashSet(schema.submoduleSubscriptions() + .bySuperProject(destBranch).toList()); + final List newSubscriptions = + new SubmoduleSectionParser(bbc, thisServer, target, repoManager) + .parseAllSections(); + + final Set alreadySubscribeds = + new HashSet(); + for (SubmoduleSubscription s : newSubscriptions) { + if (oldSubscriptions.contains(s)) { + alreadySubscribeds.add(s); + } + } + + oldSubscriptions.removeAll(newSubscriptions); + newSubscriptions.removeAll(alreadySubscribeds); + + if (!oldSubscriptions.isEmpty()) { + schema.submoduleSubscriptions().delete(oldSubscriptions); + } + schema.submoduleSubscriptions().insert(newSubscriptions); + } + } catch (OrmException e) { + logAndThrowSubmoduleException( + "Database problem at update of subscriptions table from " + + GIT_MODULES + " file.", e); + } catch (ConfigInvalidException e) { + logAndThrowSubmoduleException( + "Problem at update of subscriptions table: " + GIT_MODULES + + " config file is invalid.", e); + } catch (IOException e) { + logAndThrowSubmoduleException( + "Problem at update of subscriptions table from " + GIT_MODULES + ".", + e); + } catch (URISyntaxException e) { + logAndThrowSubmoduleException( + "Incorrect gerrit canonical web url provided in gerrit.config file.", + e); + } + } + + private void updateSuperProjects(final Branch.NameKey updatedBranch, + final ObjectId mergedCommit, final String msg) throws SubmoduleException { + try { + final List subscribers = + schema.submoduleSubscriptions().bySubmodule(updatedBranch).toList(); + + if (!subscribers.isEmpty()) { + String msgbuf = msg; + if (msgbuf == null) { + // The first updatedBranch on a cascade event of automatic + // updates of repos is added to updatedSubscribers set so + // if we face a situation having + // submodule-a(master)-->super(master)-->submodule-a(master), + // it will be detected we have a circular subscription + // when updateSuperProjects is called having as updatedBranch + // the super(master) value. + updatedSubscribers.add(updatedBranch); + + for (final Change chg : submitted) { + final CodeReviewCommit c = commits.get(chg.getId()); + if (c != null + && (c.statusCode == CommitMergeStatus.CLEAN_MERGE || c.statusCode == CommitMergeStatus.CLEAN_PICK)) { + msgbuf += "\n"; + msgbuf += c.getFullMessage(); + } + } + } + + // update subscribers of this module + for (final SubmoduleSubscription s : subscribers) { + if (!updatedSubscribers.add(s.getSuperProject())) { + log.error("Possible circular subscription involving " + + s.toString()); + } else { + + Map modules = + new HashMap(1); + modules.put(updatedBranch, mergedCommit); + + Map paths = + new HashMap(1); + paths.put(updatedBranch, s.getPath()); + + try { + updateGitlinks(s.getSuperProject(), modules, paths, msgbuf); + } catch (SubmoduleException e) { + throw e; + } + } + } + } + } catch (OrmException e) { + logAndThrowSubmoduleException("Cannot read subscription records", e); + } + } + + private void updateGitlinks(final Branch.NameKey subscriber, + final Map modules, + final Map paths, final String msg) + throws SubmoduleException { + PersonIdent author = null; + + final StringBuilder msgbuf = new StringBuilder(); + msgbuf.append("Updated " + subscriber.getParentKey().get()); + Repository pdb = null; + + try { + boolean sameAuthorForAll = true; + + for (final Map.Entry me : modules.entrySet()) { + RevCommit c = rw.parseCommit(me.getValue()); + + msgbuf.append("\nProject: "); + msgbuf.append(me.getKey().getParentKey().get()); + msgbuf.append(" " + me.getValue().getName()); + msgbuf.append("\n"); + if (modules.size() == 1 && msg != null) { + msgbuf.append(msg); + } else { + msgbuf.append(c.getShortMessage()); + } + msgbuf.append("\n"); + + if (author == null) { + author = c.getAuthorIdent(); + } else if (!author.equals(c.getAuthorIdent())) { + sameAuthorForAll = false; + } + } + + if (!sameAuthorForAll || author == null) { + author = myIdent; + } + + pdb = repoManager.openRepository(subscriber.getParentKey()); + if (pdb.getRef(subscriber.get()) == null) { + throw new SubmoduleException( + "The branch was probably deleted from the subscriber repository"); + } + + final ObjectId currentCommitId = + pdb.getRef(subscriber.get()).getObjectId(); + + DirCache dc = readTree(pdb, pdb.getRef(subscriber.get())); + DirCacheEditor ed = dc.editor(); + for (final Map.Entry me : modules.entrySet()) { + ed.add(new PathEdit(paths.get(me.getKey())) { + public void apply(DirCacheEntry ent) { + ent.setFileMode(FileMode.GITLINK); + ent.setObjectId(me.getValue().copy()); + } + }); + } + ed.finish(); + + ObjectInserter oi = pdb.newObjectInserter(); + ObjectId tree = dc.writeTree(oi); + + final CommitBuilder commit = new CommitBuilder(); + commit.setTreeId(tree); + commit.setParentIds(new ObjectId[] {currentCommitId}); + commit.setAuthor(author); + commit.setCommitter(myIdent); + commit.setMessage(msgbuf.toString()); + oi.insert(commit); + + ObjectId commitId = oi.idFor(Constants.OBJ_COMMIT, commit.build()); + + final RefUpdate rfu = pdb.updateRef(subscriber.get()); + rfu.setForceUpdate(false); + rfu.setNewObjectId(commitId); + rfu.setExpectedOldObjectId(currentCommitId); + rfu + .setRefLogMessage("Submit to " + subscriber.getParentKey().get(), + true); + + switch (rfu.update()) { + case NEW: + case FAST_FORWARD: + replication.scheduleUpdate(subscriber.getParentKey(), rfu.getName()); + // TODO since this is performed "in the background" no mail will be + // sent to inform users about the updated branch + break; + + default: + throw new IOException(rfu.getResult().name()); + } + + // Recursive call: update subscribers of the subscriber + updateSuperProjects(subscriber, commitId, msgbuf.toString()); + } catch (IOException e) { + logAndThrowSubmoduleException("Cannot update gitlinks for " + + subscriber.get(), e); + } finally { + if (pdb != null) { + pdb.close(); + } + } + } + + private static DirCache readTree(final Repository pdb, final Ref branch) + throws MissingObjectException, IncorrectObjectTypeException, IOException { + final RevWalk rw = new RevWalk(pdb); + + final DirCache dc = DirCache.newInCore(); + final DirCacheBuilder b = dc.builder(); + b.addTree(new byte[0], // no prefix path + DirCacheEntry.STAGE_0, // standard stage + pdb.newObjectReader(), rw.parseTree(branch.getObjectId())); + b.finish(); + return dc; + } + + private static void logAndThrowSubmoduleException(final String errorMsg, + final Exception e) throws SubmoduleException { + log.error(errorMsg, e); + throw new SubmoduleException(errorMsg, e); + } + + private static void logAndThrowSubmoduleException(final String errorMsg) + throws SubmoduleException { + log.error(errorMsg); + throw new SubmoduleException(errorMsg); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java index de3c8e63ff..d7ae35cfba 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java @@ -32,7 +32,7 @@ import java.util.List; /** A version of the database schema. */ public abstract class SchemaVersion { /** The current schema version. */ - private static final Class C = Schema_60.class; + private static final Class C = Schema_61.class; public static class Module extends AbstractModule { @Override diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java new file mode 100644 index 0000000000..890c824480 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java @@ -0,0 +1,25 @@ +// Copyright (C) 2011 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.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_61 extends SchemaVersion { + @Inject + Schema_61(Provider prior) { + super(prior); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java new file mode 100644 index 0000000000..7388421205 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java @@ -0,0 +1,121 @@ +// Copyright (C) 2011 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.util; + +import com.google.gerrit.reviewdb.Branch; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.SubmoduleSubscription; +import com.google.gerrit.server.git.GitRepositoryManager; + +import org.eclipse.jgit.lib.BlobBasedConfig; +import org.eclipse.jgit.lib.Constants; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +/** + * It parses from a configuration file submodule sections. + *

+ * Example of submodule sections: + * + *

+ * [submodule "project-a"]
+ *     url = http://localhost/a
+ *     path = a
+ *     branch = .
+ *
+ * [submodule "project-b"]
+ *     url = http://localhost/b
+ *     path = b
+ *     branch = refs/heads/test
+ * 
+ */ +public class SubmoduleSectionParser { + private final BlobBasedConfig bbc; + private final String thisServer; + private final Branch.NameKey superProjectBranch; + private final GitRepositoryManager repoManager; + + public SubmoduleSectionParser(final BlobBasedConfig bbc, + final String thisServer, final Branch.NameKey superProjectBranch, + final GitRepositoryManager repoManager) { + this.bbc = bbc; + this.thisServer = thisServer; + this.superProjectBranch = superProjectBranch; + this.repoManager = repoManager; + } + + public List parseAllSections() { + List parsedSubscriptions = + new ArrayList(); + for (final String id : bbc.getSubsections("submodule")) { + final SubmoduleSubscription subscription = parse(id); + if (subscription != null) { + parsedSubscriptions.add(subscription); + } + } + return parsedSubscriptions; + } + + private SubmoduleSubscription parse(final String id) { + final String url = bbc.getString("submodule", id, "url"); + final String path = bbc.getString("submodule", id, "path"); + String branch = bbc.getString("submodule", id, "branch"); + + try { + if (url != null && url.length() > 0 && path != null && path.length() > 0 + && branch != null && branch.length() > 0) { + // All required fields filled. + + boolean urlIsRelative = url.startsWith("/"); + String server = null; + if (!urlIsRelative) { + // It is actually an URI. It could be ssh://localhost/project-a. + server = new URI(url).getHost(); + } + if ((urlIsRelative) + || (server != null && server.equalsIgnoreCase(thisServer))) { + // Subscription really related to this running server. + if (branch.equals(".")) { + branch = superProjectBranch.get(); + } else if (!branch.startsWith(Constants.R_REFS)) { + branch = Constants.R_HEADS + branch; + } + + final String urlExtractedPath = new URI(url).getPath(); + String projectName = urlExtractedPath; + int fromIndex = urlExtractedPath.length() - 1; + while (fromIndex > 0) { + fromIndex = urlExtractedPath.lastIndexOf('/', fromIndex - 1); + projectName = urlExtractedPath.substring(fromIndex + 1); + + if (repoManager.list().contains(new Project.NameKey(projectName))) { + return new SubmoduleSubscription( + superProjectBranch, + new Branch.NameKey(new Project.NameKey(projectName), branch), + path); + } + } + } + } + } catch (URISyntaxException e) { + // Error in url syntax (in fact it is uri syntax) + } + + return null; + } +} diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java new file mode 100644 index 0000000000..e0860708a3 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java @@ -0,0 +1,998 @@ +// Copyright (C) 2011 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.git; + +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.gerrit.reviewdb.Account; +import com.google.gerrit.reviewdb.Branch; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.reviewdb.SubmoduleSubscription; +import com.google.gerrit.reviewdb.SubmoduleSubscriptionAccess; +import com.google.gwtorm.client.KeyUtil; +import com.google.gwtorm.client.ResultSet; +import com.google.gwtorm.client.SchemaFactory; +import com.google.gwtorm.client.impl.ListResultSet; +import com.google.gwtorm.server.StandardKeyEncoder; +import com.google.inject.Provider; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +public class SubmoduleOpTest extends LocalDiskRepositoryTestCase { + static { + KeyUtil.setEncoderImpl(new StandardKeyEncoder()); + } + + private static final String newLine = System.getProperty("line.separator"); + + private SchemaFactory schemaFactory; + private SubmoduleSubscriptionAccess subscriptions; + private ReviewDb schema; + private Provider urlProvider; + private GitRepositoryManager repoManager; + private ReplicationQueue replication; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + schemaFactory = createStrictMock(SchemaFactory.class); + schema = createStrictMock(ReviewDb.class); + subscriptions = createStrictMock(SubmoduleSubscriptionAccess.class); + urlProvider = createStrictMock(Provider.class); + repoManager = createStrictMock(GitRepositoryManager.class); + replication = createStrictMock(ReplicationQueue.class); + } + + private void doReplay() { + replay(schemaFactory, schema, subscriptions, urlProvider, repoManager, + replication); + } + + private void doVerify() { + verify(schemaFactory, schema, subscriptions, urlProvider, repoManager, + replication); + } + + /** + * It tests Submodule.update in the scenario a merged commit is an empty one + * (it does not have a .gitmodule file) and the project the commit was merged + * is not a submodule of other project. + * + * @throws Exception If an exception occurs. + */ + @Test + public void testEmptyCommit() throws Exception { + expect(schemaFactory.open()).andReturn(schema); + + final Repository realDb = createWorkRepository(); + final Git git = new Git(realDb); + + final RevCommit mergeTip = git.commit().setMessage("test").call(); + + final Branch.NameKey branchNameKey = + new Branch.NameKey(new Project.NameKey("test-project"), "test-branch"); + + expect(urlProvider.get()).andReturn("http://localhost:8080"); + + expect(schema.submoduleSubscriptions()).andReturn(subscriptions); + final ResultSet emptySubscriptions = + new ListResultSet(new ArrayList()); + expect(subscriptions.bySubmodule(branchNameKey)).andReturn( + emptySubscriptions); + + schema.close(); + + doReplay(); + + final SubmoduleOp submoduleOp = + new SubmoduleOp(branchNameKey, mergeTip, new RevWalk(realDb), urlProvider, + schemaFactory, realDb, null, new ArrayList(), null, null, + null, null); + + submoduleOp.update(); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering: + *
    + *
  • no subscriptions existing to destination project
  • + *
  • a commit is merged to "dest-project"
  • + *
  • commit contains .gitmodules file with content
  • + *
+ * + *
+   *     [submodule "source"]
+   *       path = source
+   *       url = http://localhost:8080/source
+   *       branch = .
+   * 
+ *

+ * It expects to insert a new row in subscriptions table. The row inserted + * specifies: + *

    + *
  • target "dest-project" on branch "refs/heads/master"
  • + *
  • source "a" on branch "refs/heads/master"
  • + *
  • path "a"
  • + *
+ *

+ * + * @throws Exception If an exception occurs. + */ + @Test + public void testNewSubscriptionToDotBranchValue() throws Exception { + doOneSubscriptionInsert(buildSubmoduleSection("source", "source", + "http://localhost:8080/source", ".").toString(), "refs/heads/master"); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering: + *
    + *
  • no subscriptions existing to destination project
  • + *
  • a commit is merged to "dest-project"
  • + *
  • commit contains .gitmodules file with content
  • + *
+ * + *
+   *     [submodule "source"]
+   *       path = source
+   *       url = http://localhost:8080/source
+   *       branch = refs/heads/master
+   * 
+ * + *

+ * It expects to insert a new row in subscriptions table. The row inserted + * specifies: + *

    + *
  • target "dest-project" on branch "refs/heads/master"
  • + *
  • source "source" on branch "refs/heads/master"
  • + *
  • path "source"
  • + *
+ *

+ * + * @throws Exception If an exception occurs. + */ + @Test + public void testNewSubscriptionToSameBranch() throws Exception { + doOneSubscriptionInsert(buildSubmoduleSection("source", "source", + "http://localhost:8080/source", "refs/heads/master").toString(), + "refs/heads/master"); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering: + *
    + *
  • no subscriptions existing to destination project
  • + *
  • a commit is merged to "dest-project"
  • + *
  • commit contains .gitmodules file with content
  • + *
+ * + *
+   *     [submodule "source"]
+   *       path = source
+   *       url = http://localhost:8080/source
+   *       branch = refs/heads/test
+   * 
+ *

+ * It expects to insert a new row in subscriptions table. The row inserted + * specifies: + *

    + *
  • target "dest-project" on branch "refs/heads/master"
  • + *
  • source "source" on branch "refs/heads/test"
  • + *
  • path "source"
  • + *
+ *

+ * + * @throws Exception If an exception occurs. + */ + @Test + public void testNewSubscriptionToDifferentBranch() throws Exception { + doOneSubscriptionInsert(buildSubmoduleSection("source", "source", + "http://localhost:8080/source", "refs/heads/test").toString(), + "refs/heads/test"); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering: + *
    + *
  • no subscriptions existing to destination project
  • + *
  • a commit is merged to "dest-project" in "refs/heads/master" branch
  • + *
  • commit contains .gitmodules file with content
  • + *
+ * + *
+   *     [submodule "source-a"]
+   *       path = source-a
+   *       url = http://localhost:8080/source-a
+   *       branch = .
+   *
+   *     [submodule "source-b"]
+   *       path = source-b
+   *       url = http://localhost:8080/source-b
+   *       branch = .
+   * 
+ *

+ * It expects to insert new rows in subscriptions table. The rows inserted + * specifies: + *

    + *
  • target "dest-project" on branch "refs/heads/master"
  • + *
  • source "source-a" on branch "refs/heads/master" with "source-a" path
  • + *
  • source "source-b" on branch "refs/heads/master" with "source-b" path
  • + *
+ *

+ * + * @throws Exception If an exception occurs. + */ + @Test + public void testNewSubscriptionsWithDotBranchValue() throws Exception { + final StringBuilder sb = + buildSubmoduleSection("source-a", "source-a", + "http://localhost:8080/source-a", "."); + sb.append(buildSubmoduleSection("source-b", "source-b", + "http://localhost:8080/source-b", ".")); + + final Branch.NameKey mergedBranch = + new Branch.NameKey(new Project.NameKey("dest-project"), + "refs/heads/master"); + + final List subscriptionsToInsert = + new ArrayList(); + subscriptionsToInsert + .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey( + new Project.NameKey("source-a"), "refs/heads/master"), "source-a")); + subscriptionsToInsert + .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey( + new Project.NameKey("source-b"), "refs/heads/master"), "source-b")); + + doOnlySubscriptionInserts(sb.toString(), mergedBranch, + subscriptionsToInsert); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering: + *
    + *
  • no subscriptions existing to destination project
  • + *
  • a commit is merged to "dest-project" in "refs/heads/master" branch
  • + *
  • commit contains .gitmodules file with content
  • + *
+ * + *
+   *     [submodule "source-a"]
+   *       path = source-a
+   *       url = http://localhost:8080/source-a
+   *       branch = .
+   *
+   *     [submodule "source-b"]
+   *       path = source-b
+   *       url = http://localhost:8080/source-b
+   *       branch = refs/heads/master
+   * 
+ *

+ * It expects to insert new rows in subscriptions table. The rows inserted + * specifies: + *

    + *
  • target "dest-project" on branch "refs/heads/master"
  • + *
  • source "source-a" on branch "refs/heads/master" with "source-a" path
  • + *
  • source "source-b" on branch "refs/heads/master" with "source-b" path
  • + *
+ *

+ * + * @throws Exception If an exception occurs. + */ + @Test + public void testNewSubscriptionsDotAndSameBranchValues() throws Exception { + final StringBuilder sb = + buildSubmoduleSection("source-a", "source-a", + "http://localhost:8080/source-a", "."); + sb.append(buildSubmoduleSection("source-b", "source-b", + "http://localhost:8080/source-b", "refs/heads/master")); + + final Branch.NameKey mergedBranch = + new Branch.NameKey(new Project.NameKey("dest-project"), + "refs/heads/master"); + + final List subscriptionsToInsert = + new ArrayList(); + subscriptionsToInsert + .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey( + new Project.NameKey("source-a"), "refs/heads/master"), "source-a")); + subscriptionsToInsert + .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey( + new Project.NameKey("source-b"), "refs/heads/master"), "source-b")); + + doOnlySubscriptionInserts(sb.toString(), mergedBranch, + subscriptionsToInsert); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering: + *
    + *
  • no subscriptions existing to destination project
  • + *
  • a commit is merged to "dest-project" in "refs/heads/master" branch
  • + *
  • commit contains .gitmodules file with content
  • + * + *
    +   *     [submodule "source-a"]
    +   *       path = source-a
    +   *       url = http://localhost:8080/source-a
    +   *       branch = refs/heads/test-a
    +   *
    +   *     [submodule "source-b"]
    +   *       path = source-b
    +   *       url = http://localhost:8080/source-b
    +   *       branch = refs/heads/test-b
    +   * 
    + * + *

    + * It expects to insert new rows in subscriptions table. The rows inserted + * specifies: + *

      + *
    • target "dest-project" on branch "refs/heads/master"
    • + *
    • source "source-a" on branch "refs/heads/test-a" with "source-a" path
    • + *
    • source "source-b" on branch "refs/heads/test-b" with "source-b" path
    • + *
    + *

    + * + * @throws Exception If an exception occurs. + */ + @Test + public void testNewSubscriptionsSpecificBranchValues() throws Exception { + final StringBuilder sb = + buildSubmoduleSection("source-a", "source-a", + "http://localhost:8080/source-a", "refs/heads/test-a"); + sb.append(buildSubmoduleSection("source-b", "source-b", + "http://localhost:8080/source-b", "refs/heads/test-b")); + + final Branch.NameKey mergedBranch = + new Branch.NameKey(new Project.NameKey("dest-project"), + "refs/heads/master"); + + final List subscriptionsToInsert = + new ArrayList(); + subscriptionsToInsert + .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey( + new Project.NameKey("source-a"), "refs/heads/test-a"), "source-a")); + subscriptionsToInsert + .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey( + new Project.NameKey("source-b"), "refs/heads/test-b"), "source-b")); + + doOnlySubscriptionInserts(sb.toString(), mergedBranch, + subscriptionsToInsert); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering: + *
      + *
    • one subscription existing to destination project/branch
    • + *
    • a commit is merged to "dest-project" in "refs/heads/master" branch
    • + *
    • commit contains .gitmodules file with content
    • + *
    + * + *
    +   *     [submodule "source"]
    +   *       path = source
    +   *       url = http://localhost:8080/source
    +   *       branch = refs/heads/master
    +   * 
    + *

    + * It expects to insert a new row in subscriptions table. The rows inserted + * specifies: + *

      + *
    • target "dest-project" on branch "refs/heads/master"
    • + *
    • source "source" on branch "refs/heads/master" with "source" path
    • + *
    + *

    + *

    + * It also expects to remove the row in subscriptions table specifying another + * project/branch subscribed to merged branch. This one to be removed is: + *

      + *
    • target "dest-project" on branch "refs/heads/master"
    • + *
    • source "old-source" on branch "refs/heads/master" with "old-source" + * path
    • + *
    + *

    + * + * @throws Exception If an exception occurs. + */ + @Test + public void testSubscriptionsInsertOneRemoveOne() throws Exception { + final Branch.NameKey mergedBranch = + new Branch.NameKey(new Project.NameKey("dest-project"), + "refs/heads/master"); + + final List subscriptionsToInsert = + new ArrayList(); + subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch, + new Branch.NameKey(new Project.NameKey("source"), "refs/heads/master"), + "source")); + + final List oldOnesToMergedBranch = + new ArrayList(); + oldOnesToMergedBranch.add(new SubmoduleSubscription(mergedBranch, + new Branch.NameKey(new Project.NameKey("old-source"), + "refs/heads/master"), "old-source")); + + doOnlySubscriptionTableOperations(buildSubmoduleSection("source", "source", + "http://localhost:8080/source", "refs/heads/master").toString(), + mergedBranch, subscriptionsToInsert, oldOnesToMergedBranch); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering: + *
      + *
    • one subscription existing to destination project/branch with a source + * called old on refs/heads/master branch
    • + *
    • a commit is merged to "dest-project" in "refs/heads/master" branch
    • + *
    • + * commit contains .gitmodules file with content
    • + *
    + * + *
    +   *     [submodule "new"]
    +   *       path = new
    +   *       url = http://localhost:8080/new
    +   *       branch = refs/heads/master
    +   *
    +   *     [submodule "old"]
    +   *       path = old
    +   *       url = http://localhost:8080/old
    +   *       branch = refs/heads/master
    +   * 
    + *

    + * It expects to insert a new row in subscriptions table. It should not remove + * any row. The rows inserted specifies: + *

      + *
    • target "dest-project" on branch "refs/heads/master"
    • + *
    • source "new" on branch "refs/heads/master" with "new" path
    • + *
    + *

    + * + * @throws Exception If an exception occurs. + */ + @Test + public void testSubscriptionAddedAndMantainPreviousOne() throws Exception { + final StringBuilder sb = + buildSubmoduleSection("new", "new", "http://localhost:8080/new", + "refs/heads/master"); + sb.append(buildSubmoduleSection("old", "old", "http://localhost:8080/old", + "refs/heads/master")); + + final Branch.NameKey mergedBranch = + new Branch.NameKey(new Project.NameKey("dest-project"), + "refs/heads/master"); + + final SubmoduleSubscription old = + new SubmoduleSubscription(mergedBranch, new Branch.NameKey(new Project.NameKey( + "old"), "refs/heads/master"), "old"); + + final List extractedsubscriptions = + new ArrayList(); + extractedsubscriptions.add(new SubmoduleSubscription(mergedBranch, + new Branch.NameKey(new Project.NameKey("new"), "refs/heads/master"), + "new")); + extractedsubscriptions.add(old); + + final List oldOnesToMergedBranch = + new ArrayList(); + oldOnesToMergedBranch.add(old); + + doOnlySubscriptionTableOperations(sb.toString(), mergedBranch, + extractedsubscriptions, oldOnesToMergedBranch); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering an empty .gitmodules + * file is part of a commit to a destination project/branch having two sources + * subscribed. + *

    + * It expects to remove the subscriptions to destination project/branch. + *

    + * + * @throws Exception If an exception occurs. + */ + @Test + public void testRemoveSubscriptions() throws Exception { + final Branch.NameKey mergedBranch = + new Branch.NameKey(new Project.NameKey("dest-project"), + "refs/heads/master"); + + final List extractedsubscriptions = + new ArrayList(); + + final List oldOnesToMergedBranch = + new ArrayList(); + oldOnesToMergedBranch + .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey( + new Project.NameKey("source-a"), "refs/heads/master"), "source-a")); + oldOnesToMergedBranch + .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey( + new Project.NameKey("source-b"), "refs/heads/master"), "source-b")); + + doOnlySubscriptionTableOperations("", mergedBranch, extractedsubscriptions, + oldOnesToMergedBranch); + } + + /** + * It tests SubmoduleOp.update in a scenario considering no .gitmodules file + * in a merged commit to a destination project/branch that is a source one to + * one called "target-project". + *

    + * It expects to update the git link called "source-project" to be in target + * repository. + *

    + * + * @throws Exception If an exception occurs. + */ + @Test + public void testOneSubscriberToUpdate() throws Exception { + expect(schemaFactory.open()).andReturn(schema); + + final Repository sourceRepository = createWorkRepository(); + final Git sourceGit = new Git(sourceRepository); + + addRegularFileToIndex("file.txt", "test content", sourceRepository); + + final RevCommit sourceMergeTip = + sourceGit.commit().setMessage("test").call(); + + final Branch.NameKey sourceBranchNameKey = + new Branch.NameKey(new Project.NameKey("source-project"), + "refs/heads/master"); + + final CodeReviewCommit codeReviewCommit = + new CodeReviewCommit(sourceMergeTip.toObjectId()); + final Change submitedChange = + new Change(new Change.Key(sourceMergeTip.toObjectId().getName()), + new Change.Id(1), new Account.Id(1), sourceBranchNameKey); + codeReviewCommit.change = submitedChange; + + final Map mergedCommits = + new HashMap(); + mergedCommits.put(codeReviewCommit.change.getId(), codeReviewCommit); + + final List submited = new ArrayList(); + submited.add(submitedChange); + + final Repository targetRepository = createWorkRepository(); + final Git targetGit = new Git(targetRepository); + + addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository); + + targetGit.commit().setMessage("test").call(); + + final Branch.NameKey targetBranchNameKey = + new Branch.NameKey(new Project.NameKey("target-project"), + sourceBranchNameKey.get()); + + expect(urlProvider.get()).andReturn("http://localhost:8080"); + + expect(schema.submoduleSubscriptions()).andReturn(subscriptions); + final ResultSet subscribers = + new ListResultSet(Collections + .singletonList(new SubmoduleSubscription(targetBranchNameKey, + sourceBranchNameKey, "source-project"))); + expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn( + subscribers); + + expect(repoManager.openRepository(targetBranchNameKey.getParentKey())) + .andReturn(targetRepository); + + replication.scheduleUpdate(targetBranchNameKey.getParentKey(), + targetBranchNameKey.get()); + + expect(schema.submoduleSubscriptions()).andReturn(subscriptions); + final ResultSet emptySubscriptions = + new ListResultSet(new ArrayList()); + expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn( + emptySubscriptions); + + schema.close(); + + final PersonIdent myIdent = + new PersonIdent("test-user", "test-user@email.com"); + + doReplay(); + + final SubmoduleOp submoduleOp = + new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk( + sourceRepository), urlProvider, schemaFactory, sourceRepository, + new Project(sourceBranchNameKey.getParentKey()), submited, + mergedCommits, myIdent, repoManager, replication); + + submoduleOp.update(); + + doVerify(); + } + + /** + * It tests SubmoduleOp.update in a scenario considering established circular + * reference in submodule_subscriptions table. + *

    + * In the tested scenario there is no .gitmodules file in a merged commit to a + * destination project/branch that is a source one to one called + * "target-project". + *

    + * submodule_subscriptions table will be incorrect due source appearing as a + * subscriber or target-project: according to database target-project has as + * source the source-project, and source-project has as source the + * target-project. + *

    + * It expects to update the git link called "source-project" to be in target + * repository and ignoring the incorrect row in database establishing the + * circular reference. + *

    + * + * @throws Exception If an exception occurs. + */ + @Test + public void testAvoidingCircularReference() throws Exception { + expect(schemaFactory.open()).andReturn(schema); + + final Repository sourceRepository = createWorkRepository(); + final Git sourceGit = new Git(sourceRepository); + + addRegularFileToIndex("file.txt", "test content", sourceRepository); + + final RevCommit sourceMergeTip = + sourceGit.commit().setMessage("test").call(); + + final Branch.NameKey sourceBranchNameKey = + new Branch.NameKey(new Project.NameKey("source-project"), + "refs/heads/master"); + + final CodeReviewCommit codeReviewCommit = + new CodeReviewCommit(sourceMergeTip.toObjectId()); + final Change submitedChange = + new Change(new Change.Key(sourceMergeTip.toObjectId().getName()), + new Change.Id(1), new Account.Id(1), sourceBranchNameKey); + codeReviewCommit.change = submitedChange; + + final Map mergedCommits = + new HashMap(); + mergedCommits.put(codeReviewCommit.change.getId(), codeReviewCommit); + + final List submited = new ArrayList(); + submited.add(submitedChange); + + final Repository targetRepository = createWorkRepository(); + final Git targetGit = new Git(targetRepository); + + addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository); + + targetGit.commit().setMessage("test").call(); + + final Branch.NameKey targetBranchNameKey = + new Branch.NameKey(new Project.NameKey("target-project"), + sourceBranchNameKey.get()); + + expect(urlProvider.get()).andReturn("http://localhost:8080"); + + expect(schema.submoduleSubscriptions()).andReturn(subscriptions); + final ResultSet subscribers = + new ListResultSet(Collections + .singletonList(new SubmoduleSubscription(targetBranchNameKey, + sourceBranchNameKey, "source-project"))); + expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn( + subscribers); + + expect(repoManager.openRepository(targetBranchNameKey.getParentKey())) + .andReturn(targetRepository); + + replication.scheduleUpdate(targetBranchNameKey.getParentKey(), + targetBranchNameKey.get()); + + expect(schema.submoduleSubscriptions()).andReturn(subscriptions); + final ResultSet incorrectSubscriptions = + new ListResultSet(Collections + .singletonList(new SubmoduleSubscription(sourceBranchNameKey, + targetBranchNameKey, "target-project"))); + expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn( + incorrectSubscriptions); + + schema.close(); + + final PersonIdent myIdent = + new PersonIdent("test-user", "test-user@email.com"); + + doReplay(); + + final SubmoduleOp submoduleOp = + new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk( + sourceRepository), urlProvider, schemaFactory, sourceRepository, + new Project(sourceBranchNameKey.getParentKey()), submited, + mergedCommits, myIdent, repoManager, replication); + + submoduleOp.update(); + + doVerify(); + } + + /** + * It calls SubmoduleOp.update considering only one insert on Subscriptions + * table. + *

    + * It considers a commit containing a .gitmodules file was merged in + * refs/heads/master of a dest-project. + *

    + *

    + * The .gitmodules file content should indicate a source project called + * "source". + *

    + * + * @param gitModulesFileContent The .gitmodules file content. During the test + * this file is created, so the commit containing it. + * @param sourceBranchName The branch name of source project "pointed by" + * .gitmodule file. + * @throws Exception If an exception occurs. + */ + private void doOneSubscriptionInsert(final String gitModulesFileContent, + final String sourceBranchName) throws Exception { + final Branch.NameKey mergedBranch = + new Branch.NameKey(new Project.NameKey("dest-project"), + "refs/heads/master"); + + final List subscriptionsToInsert = + new ArrayList(); + subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch, + new Branch.NameKey(new Project.NameKey("source"), sourceBranchName), + "source")); + + doOnlySubscriptionInserts(gitModulesFileContent, mergedBranch, + subscriptionsToInsert); + } + + /** + * It calls SubmoduleOp.update method considering scenario only inserting new + * subscriptions. + *

    + * In this test a commit is created and considered merged to + * mergedBranch branch. + *

    + *

    + * The destination project the commit was merged is not considered to be a + * source of another project (no subscribers found to this project). + *

    + * + * @param gitModulesFileContent The .gitmodule file content. + * @param mergedBranch The {@link Branch.NameKey} instance representing the + * project/branch the commit was merged. + * @param extractedSubscriptions The subscription rows extracted from + * gitmodules file. + * @throws Exception If an exception occurs. + */ + private void doOnlySubscriptionInserts(final String gitModulesFileContent, + final Branch.NameKey mergedBranch, + final List extractedSubscriptions) throws Exception { + doOnlySubscriptionTableOperations(gitModulesFileContent, mergedBranch, + extractedSubscriptions, new ArrayList()); + } + + /** + * It calls SubmoduleOp.update method considering scenario only updating + * Subscriptions table. + *

    + * In this test a commit is created and considered merged to + * mergedBranch branch. + *

    + *

    + * The destination project the commit was merged is not considered to be a + * source of another project (no subscribers found to this project). + *

    + * + * @param gitModulesFileContent The .gitmodules file content. + * @param mergedBranch The {@link Branch.NameKey} instance representing the + * project/branch the commit was merged. + * @param extractedSubscriptions The subscription rows extracted from + * gitmodules file. + * @param previousSubscriptions The subscription rows to be considering as + * existing and pointing as target to the mergedBranch + * before updating the table. + * @throws Exception If an exception occurs. + */ + private void doOnlySubscriptionTableOperations( + final String gitModulesFileContent, final Branch.NameKey mergedBranch, + final List extractedSubscriptions, + final List previousSubscriptions) throws Exception { + expect(schemaFactory.open()).andReturn(schema); + + final Repository realDb = createWorkRepository(); + final Git git = new Git(realDb); + + addRegularFileToIndex(".gitmodules", gitModulesFileContent, realDb); + + final RevCommit mergeTip = git.commit().setMessage("test").call(); + + expect(urlProvider.get()).andReturn("http://localhost:8080").times(2); + + expect(schema.submoduleSubscriptions()).andReturn(subscriptions); + expect(subscriptions.bySuperProject(mergedBranch)).andReturn( + new ListResultSet(previousSubscriptions)); + + SortedSet existingProjects = + new TreeSet(); + + for (SubmoduleSubscription extracted : extractedSubscriptions) { + existingProjects.add(extracted.getSubmodule().getParentKey()); + } + + for (int index = 0; index < extractedSubscriptions.size(); index++) { + expect(repoManager.list()).andReturn(existingProjects); + } + + final Set alreadySubscribeds = + new HashSet(); + for (SubmoduleSubscription s : extractedSubscriptions) { + if (previousSubscriptions.contains(s)) { + alreadySubscribeds.add(s); + } + } + + final Set subscriptionsToRemove = + new HashSet(previousSubscriptions); + final List subscriptionsToInsert = + new ArrayList(extractedSubscriptions); + + subscriptionsToRemove.removeAll(subscriptionsToInsert); + subscriptionsToInsert.removeAll(alreadySubscribeds); + + if (!subscriptionsToRemove.isEmpty()) { + expect(schema.submoduleSubscriptions()).andReturn(subscriptions); + subscriptions.delete(subscriptionsToRemove); + } + + expect(schema.submoduleSubscriptions()).andReturn(subscriptions); + subscriptions.insert(subscriptionsToInsert); + + expect(schema.submoduleSubscriptions()).andReturn(subscriptions); + expect(subscriptions.bySubmodule(mergedBranch)).andReturn( + new ListResultSet(new ArrayList())); + + schema.close(); + + doReplay(); + + final SubmoduleOp submoduleOp = + new SubmoduleOp(mergedBranch, mergeTip, new RevWalk(realDb), + urlProvider, schemaFactory, realDb, new Project(mergedBranch + .getParentKey()), new ArrayList(), null, null, + repoManager, null); + + submoduleOp.update(); + } + + /** + * It creates and adds a regular file to git index of a repository. + * + * @param fileName The file name. + * @param content File content. + * @param repository The Repository instance. + * @throws IOException If an I/O exception occurs. + */ + private void addRegularFileToIndex(final String fileName, + final String content, final Repository repository) throws IOException { + final ObjectInserter oi = repository.newObjectInserter(); + AnyObjectId objectId = + oi.insert(Constants.OBJ_BLOB, Constants.encode(content)); + oi.flush(); + addEntryToIndex(fileName, FileMode.REGULAR_FILE, objectId, repository); + } + + /** + * It creates and adds a git link to git index of a repository. + * + * @param fileName The file name. + * @param objectId The sha-1 value of git link. + * @param repository The Repository instance. + * @throws IOException If an I/O exception occurs. + */ + private void addGitLinkToIndex(final String fileName, + final AnyObjectId objectId, final Repository repository) + throws IOException { + addEntryToIndex(fileName, FileMode.GITLINK, objectId, repository); + } + + /** + * It adds an entry to index. + * + * @param path The entry path. + * @param fileMode The entry file mode. + * @param objectId The ObjectId value of the entry. + * @param repository The repository instance. + * @throws IOException If an I/O exception occurs. + */ + private void addEntryToIndex(final String path, final FileMode fileMode, + final AnyObjectId objectId, final Repository repository) + throws IOException { + final DirCacheEntry e = new DirCacheEntry(path); + e.setFileMode(fileMode); + e.setObjectId(objectId); + + final DirCacheBuilder dirCacheBuilder = repository.lockDirCache().builder(); + dirCacheBuilder.add(e); + dirCacheBuilder.commit(); + } + + private static StringBuilder buildSubmoduleSection(final String name, + final String path, final String url, final String branch) { + final StringBuilder sb = new StringBuilder(); + + sb.append("[submodule \""); + sb.append(name); + sb.append("\"]"); + sb.append(newLine); + + sb.append("\tpath = "); + sb.append(path); + sb.append(newLine); + + sb.append("\turl = "); + sb.append(url); + sb.append(newLine); + + sb.append("\tbranch = "); + sb.append(branch); + sb.append(newLine); + + return sb; + } +} diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java new file mode 100644 index 0000000000..5598809290 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java @@ -0,0 +1,255 @@ +// Copyright (C) 2011 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.util; + +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; + +import com.google.gerrit.reviewdb.Branch; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.SubmoduleSubscription; +import com.google.gerrit.server.git.GitRepositoryManager; + +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.lib.BlobBasedConfig; +import org.junit.Before; +import org.junit.Test; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; + +public class SubmoduleSectionParserTest extends LocalDiskRepositoryTestCase { + private final static String THIS_SERVER = "localhost"; + private GitRepositoryManager repoManager; + private BlobBasedConfig bbc; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + repoManager = createStrictMock(GitRepositoryManager.class); + bbc = createStrictMock(BlobBasedConfig.class); + } + + private void doReplay() { + replay(repoManager, bbc); + } + + private void doVerify() { + verify(repoManager, bbc); + } + + @Test + public void testSubmodulesParseWithCorrectSections() throws Exception { + final Map sectionsToReturn = + new TreeMap(); + sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a", + ".")); + sectionsToReturn.put("b", new SubmoduleSection("ssh://localhost/b", "b", + ".")); + sectionsToReturn.put("c", new SubmoduleSection("ssh://localhost/test/c", + "c-path", "refs/heads/master")); + sectionsToReturn.put("d", new SubmoduleSection("ssh://localhost/d", + "d-parent/the-d-folder", "refs/heads/test")); + + final Map reposToBeFound = new HashMap(); + reposToBeFound.put("a", "a"); + reposToBeFound.put("b", "b"); + reposToBeFound.put("c", "test/c"); + reposToBeFound.put("d", "d"); + + final Branch.NameKey superBranchNameKey = + new Branch.NameKey(new Project.NameKey("super-project"), + "refs/heads/master"); + + final List expectedSubscriptions = + new ArrayList(); + expectedSubscriptions + .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey( + new Project.NameKey("a"), "refs/heads/master"), "a")); + expectedSubscriptions + .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey( + new Project.NameKey("b"), "refs/heads/master"), "b")); + expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey, + new Branch.NameKey(new Project.NameKey("test/c"), "refs/heads/master"), + "c-path")); + expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey, + new Branch.NameKey(new Project.NameKey("d"), "refs/heads/test"), + "d-parent/the-d-folder")); + + execute(superBranchNameKey, sectionsToReturn, reposToBeFound, + expectedSubscriptions); + } + + @Test + public void testSubmodulesParseWithAnInvalidSection() throws Exception { + final Map sectionsToReturn = + new TreeMap(); + sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a", + ".")); + // This one is invalid since "b" is not a recognized project + sectionsToReturn.put("b", new SubmoduleSection("ssh://localhost/b", "b", + ".")); + sectionsToReturn.put("c", new SubmoduleSection("ssh://localhost/test/c", + "c-path", "refs/heads/master")); + sectionsToReturn.put("d", new SubmoduleSection("ssh://localhost/d", + "d-parent/the-d-folder", "refs/heads/test")); + + // "b" will not be in this list + final Map reposToBeFound = new HashMap(); + reposToBeFound.put("a", "a"); + reposToBeFound.put("c", "test/c"); + reposToBeFound.put("d", "d"); + + final Branch.NameKey superBranchNameKey = + new Branch.NameKey(new Project.NameKey("super-project"), + "refs/heads/master"); + + final List expectedSubscriptions = + new ArrayList(); + expectedSubscriptions + .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey( + new Project.NameKey("a"), "refs/heads/master"), "a")); + expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey, + new Branch.NameKey(new Project.NameKey("test/c"), "refs/heads/master"), + "c-path")); + expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey, + new Branch.NameKey(new Project.NameKey("d"), "refs/heads/test"), + "d-parent/the-d-folder")); + + execute(superBranchNameKey, sectionsToReturn, reposToBeFound, + expectedSubscriptions); + } + + @Test + public void testSubmoduleSectionToOtherServer() throws Exception { + Map sectionsToReturn = + new HashMap(); + // The url is not to this server. + sectionsToReturn.put("a", new SubmoduleSection("ssh://review.source.com/a", + "a", ".")); + + execute(new Branch.NameKey(new Project.NameKey("super-project"), + "refs/heads/master"), sectionsToReturn, new HashMap(), + new ArrayList()); + } + + @Test + public void testProjectNotFound() throws Exception { + Map sectionsToReturn = + new HashMap(); + sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a", + ".")); + + execute(new Branch.NameKey(new Project.NameKey("super-project"), + "refs/heads/master"), sectionsToReturn, new HashMap(), + new ArrayList()); + } + + @Test + public void testProjectWithSlashesNotFound() throws Exception { + Map sectionsToReturn = + new HashMap(); + sectionsToReturn.put("project", new SubmoduleSection( + "ssh://localhost/company/tools/project", "project", ".")); + + execute(new Branch.NameKey(new Project.NameKey("super-project"), + "refs/heads/master"), sectionsToReturn, new HashMap(), + new ArrayList()); + } + + private void execute(final Branch.NameKey superProjectBranch, + final Map sectionsToReturn, + final Map reposToBeFound, + final List expectedSubscriptions) throws Exception { + expect(bbc.getSubsections("submodule")) + .andReturn(sectionsToReturn.keySet()); + + for (final String id : sectionsToReturn.keySet()) { + final SubmoduleSection section = sectionsToReturn.get(id); + expect(bbc.getString("submodule", id, "url")).andReturn(section.getUrl()); + expect(bbc.getString("submodule", id, "path")).andReturn( + section.getPath()); + expect(bbc.getString("submodule", id, "branch")).andReturn( + section.getBranch()); + + if (THIS_SERVER.equals(new URI(section.getUrl()).getHost())) { + String projectNameCandidate = null; + final String urlExtractedPath = new URI(section.getUrl()).getPath(); + int fromIndex = urlExtractedPath.length() - 1; + while (fromIndex > 0) { + fromIndex = urlExtractedPath.lastIndexOf('/', fromIndex - 1); + projectNameCandidate = urlExtractedPath.substring(fromIndex + 1); + if (projectNameCandidate.equals(reposToBeFound.get(id))) { + expect(repoManager.list()).andReturn( + new TreeSet(Collections + .singletonList(new Project.NameKey(projectNameCandidate)))); + break; + } else { + expect(repoManager.list()).andReturn( + new TreeSet(Collections.EMPTY_LIST)); + } + } + } + } + + doReplay(); + + final SubmoduleSectionParser ssp = + new SubmoduleSectionParser(bbc, THIS_SERVER, superProjectBranch, + repoManager); + + List returnedSubscriptions = ssp.parseAllSections(); + + doVerify(); + + assertEquals(expectedSubscriptions, returnedSubscriptions); + } + + private final static class SubmoduleSection { + private final String url; + private final String path; + private final String branch; + + public SubmoduleSection(final String url, final String path, + final String branch) { + this.url = url; + this.path = path; + this.branch = branch; + } + + public String getUrl() { + return url; + } + + public String getPath() { + return path; + } + + public String getBranch() { + return branch; + } + } +}