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 extends SchemaVersion> C = Schema_60.class;
+ private static final Class extends SchemaVersion> 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;
+ }
+ }
+}