Allow superprojects to subscribe to submodules updates

Before discussing about the feature introduced in this commit,
let's discuss a little about the Submodule feature. It is a git
feature that allows an external repository to be attached inside
one repository at a specific path.

Imagine a repository called 'super' and another one called 'a'.
Also consider 'a' available in a running gerrit instance on "server".
With this feature, one could attach 'a' inside of 'super' repository
in path 'a' by executing following command when inside 'super':
"git submodule add ssh://server/a a".

Still considering above example, after its execution notice inside
'super' local repository the 'a' folder is considered a gitlink
to external repository 'a'. Also notice a file called .gitmodules
is created (it is a config file containing the subscription of 'a').
The "git submodule status" command provides the sha-1 each gitlink
points to in the external repository.

In the example provided, if 'a' is updated and 'super' is supposed
to see latest sha-1 (considering here 'a' has only the master branch),
one should then commit 'a' modified gitlink in 'super' project.
Actually it would not need to be even an external update, one could
move to 'a' folder (insider 'super'), modify its content, commit,
then move back to 'super' and commit 'a' gitlink.

The feature introduced in this commit allows superprojects to
subscribe to submodules updates.

When a commit is merged to a project, the commit content is scanned
to identify if it registers submodules (if the commit contains new
gitlinks and .gitmodules file with required info) and if so, a new
submodule subscription is registered.

When a new commit of a registered submodule is merged, gerrit
automatically updates the subscribers to the submodule with new
commit having the updated gitlinks.

The most notable benefit of the feature is to not require
to push/merge commits of super projects (subscribers) with gitlinks
whenever a project being a submodule is updated. It is only
required to push commits with gitlinks when they are created
(and in this case it is also required to push .gitmodules file).

Submodule subscription is actually the subscription of a submodule
project and one of its branches for a branch of a super project.
Whenever a submodule subscription is created, the user should certify
that the branch field is filled in the .gitmodules configuration file,
otherwise the subscriptions are not registered in gerrit. It is not
automatically filled by git submodule command. Its value should indicate
the branch of submodule project that when updated will trigger
automatic update of its registered gitlink. The branch value
could be '.' if the submodule project branch has the same name that
the destination branch of commit having gitlinks/.gitmodules file.

Since it manages subscriptions in the branch scope, we could have
a scenario having a project called 'super' having a branch 'integration'
subscribed to a project called 'a' in branch 'integration', and also
having same 'super' project but in branch 'dev' subscribed to the 'a'
project in a branch called 'local-dev'.

If one does not want to have use of this feature, (s)he should
not add the branch field in an added submodule section of .gitmodules
file. If one have added a submodule subscription and want to drop it,
it is required to merge a commit updating subscribed super project/branch
removing the gitlink and the submodule section of .gitmodules file.

The branch field of a submodule section is a custom git submodule
feature introduced by this one and for gerrit use. One should always
certify to fill it by editing .gitmodules file after adding
submodules to a super project, if it is the intention to make use of
the gerrit feature introduced here.

The feature also requires the canonical web url provided
in the gerrit configuration file. It will automatically updates
only the subscribers of project urls of the running server (the one
described in canonical web url value).

Considering the functionalities introduced in this feature, we have
actually an option to the current way Android platform code is handled
(using manifest.xml files). And it offers the benefit of not
requiring users to introduce commits in the super project (platform
repository considering Android source code example) and not
using the repo tool.

Also notice that the feature makes possible to have recursive update:
a 'super' project could be the subscriber of an 'a' project, and the
'a' project could be the subscriber of a 'a-subproject' project.

Change-Id: Id067e85d914e13324f1f28418233cc96a60e3ab2
This commit is contained in:
Goran Lungberg
2011-08-02 12:41:36 -03:00
committed by Gustaf Lundh
parent a3f73aab40
commit d15704079c
15 changed files with 2019 additions and 3 deletions

View File

@@ -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;

View File

@@ -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.
* <p>
* This means a class instance represents a repo/branch subscription to a
* project/branch (the subscriber).
* <p>
* A subscriber operates a submodule in defined path.
*/
public final class SubmoduleSubscription {
/** Subscription key */
public static class Key extends CompoundKey<Branch.NameKey> {
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();
}
}

View File

@@ -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<SubmoduleSubscription, SubmoduleSubscription.Key> {
@PrimaryKey("key")
SubmoduleSubscription get(SubmoduleSubscription.Key key) throws OrmException;
@Query("ORDER BY key.superProject.projectName")
ResultSet<SubmoduleSubscription> all() throws OrmException;
@Query("WHERE key.superProject = ?")
ResultSet<SubmoduleSubscription> bySuperProject(Branch.NameKey superProject)
throws OrmException;
@Query("WHERE key.submodule = ?")
ResultSet<SubmoduleSubscription> bySubmodule(Branch.NameKey submodule)
throws OrmException;
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<ReviewDb> 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;

View File

@@ -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<Change>(),
new HashMap<Change.Id, CodeReviewCommit>());
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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<Change> submitted,
Map<Change.Id, CodeReviewCommit> 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<String> urlProvider;
private ReviewDb schema;
private Repository db;
private Project destProject;
private List<Change> submitted;
private final Map<Change.Id, CodeReviewCommit> commits;
private final PersonIdent myIdent;
private final GitRepositoryManager repoManager;
private final ReplicationQueue replication;
private final SchemaFactory<ReviewDb> schemaFactory;
private final Set<Branch.NameKey> updatedSubscribers;
@Inject
public SubmoduleOp(@Assisted final Branch.NameKey destBranch,
@Assisted RevCommit mergeTip, @Assisted RevWalk rw,
@CanonicalWebUrl @Nullable final Provider<String> urlProvider,
final SchemaFactory<ReviewDb> sf, @Assisted Repository db,
@Assisted Project destProject, @Assisted List<Change> submitted,
@Assisted final Map<Change.Id, CodeReviewCommit> 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<Branch.NameKey>();
}
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<SubmoduleSubscription> oldSubscriptions =
new HashSet<SubmoduleSubscription>(schema.submoduleSubscriptions()
.bySuperProject(destBranch).toList());
final List<SubmoduleSubscription> newSubscriptions =
new SubmoduleSectionParser(bbc, thisServer, target, repoManager)
.parseAllSections();
final Set<SubmoduleSubscription> alreadySubscribeds =
new HashSet<SubmoduleSubscription>();
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<SubmoduleSubscription> 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<Branch.NameKey, ObjectId> modules =
new HashMap<Branch.NameKey, ObjectId>(1);
modules.put(updatedBranch, mergedCommit);
Map<Branch.NameKey, String> paths =
new HashMap<Branch.NameKey, String>(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<Branch.NameKey, ObjectId> modules,
final Map<Branch.NameKey, String> 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<Branch.NameKey, ObjectId> 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<Branch.NameKey, ObjectId> 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);
}
}

View File

@@ -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

View File

@@ -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<Schema_60> prior) {
super(prior);
}
}

View File

@@ -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.
* <p>
* Example of submodule sections:
*
* <pre>
* [submodule "project-a"]
* url = http://localhost/a
* path = a
* branch = .
*
* [submodule "project-b"]
* url = http://localhost/b
* path = b
* branch = refs/heads/test
* </pre>
*/
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<SubmoduleSubscription> parseAllSections() {
List<SubmoduleSubscription> parsedSubscriptions =
new ArrayList<SubmoduleSubscription>();
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;
}
}

View File

@@ -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<ReviewDb> schemaFactory;
private SubmoduleSubscriptionAccess subscriptions;
private ReviewDb schema;
private Provider<String> 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<SubmoduleSubscription> emptySubscriptions =
new ListResultSet<SubmoduleSubscription>(new ArrayList<SubmoduleSubscription>());
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<Change>(), null, null,
null, null);
submoduleOp.update();
doVerify();
}
/**
* It tests SubmoduleOp.update in a scenario considering:
* <ul>
* <li>no subscriptions existing to destination project</li>
* <li>a commit is merged to "dest-project"</li>
* <li>commit contains .gitmodules file with content</li>
* </ul>
*
* <pre>
* [submodule "source"]
* path = source
* url = http://localhost:8080/source
* branch = .
* </pre>
* <p>
* It expects to insert a new row in subscriptions table. The row inserted
* specifies:
* <ul>
* <li>target "dest-project" on branch "refs/heads/master"</li>
* <li>source "a" on branch "refs/heads/master"</li>
* <li>path "a"</li>
* </ul>
* </p>
*
* @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:
* <ul>
* <li>no subscriptions existing to destination project</li>
* <li>a commit is merged to "dest-project"</li>
* <li>commit contains .gitmodules file with content</li>
* </ul>
*
* <pre>
* [submodule "source"]
* path = source
* url = http://localhost:8080/source
* branch = refs/heads/master
* </pre>
*
* <p>
* It expects to insert a new row in subscriptions table. The row inserted
* specifies:
* <ul>
* <li>target "dest-project" on branch "refs/heads/master"</li>
* <li>source "source" on branch "refs/heads/master"</li>
* <li>path "source"</li>
* </ul>
* </p>
*
* @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:
* <ul>
* <li>no subscriptions existing to destination project</li>
* <li>a commit is merged to "dest-project"</li>
* <li>commit contains .gitmodules file with content</li>
* </ul>
*
* <pre>
* [submodule "source"]
* path = source
* url = http://localhost:8080/source
* branch = refs/heads/test
* </pre>
* <p>
* It expects to insert a new row in subscriptions table. The row inserted
* specifies:
* <ul>
* <li>target "dest-project" on branch "refs/heads/master"</li>
* <li>source "source" on branch "refs/heads/test"</li>
* <li>path "source"</li>
* </ul>
* </p>
*
* @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:
* <ul>
* <li>no subscriptions existing to destination project</li>
* <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
* <li>commit contains .gitmodules file with content</li>
* </ul>
*
* <pre>
* [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 = .
* </pre>
* <p>
* It expects to insert new rows in subscriptions table. The rows inserted
* specifies:
* <ul>
* <li>target "dest-project" on branch "refs/heads/master"</li>
* <li>source "source-a" on branch "refs/heads/master" with "source-a" path</li>
* <li>source "source-b" on branch "refs/heads/master" with "source-b" path</li>
* </ul>
* </p>
*
* @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<SubmoduleSubscription> subscriptionsToInsert =
new ArrayList<SubmoduleSubscription>();
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:
* <ul>
* <li>no subscriptions existing to destination project</li>
* <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
* <li>commit contains .gitmodules file with content</li>
* </ul>
*
* <pre>
* [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
* </pre>
* <p>
* It expects to insert new rows in subscriptions table. The rows inserted
* specifies:
* <ul>
* <li>target "dest-project" on branch "refs/heads/master"</li>
* <li>source "source-a" on branch "refs/heads/master" with "source-a" path</li>
* <li>source "source-b" on branch "refs/heads/master" with "source-b" path</li>
* </ul>
* </p>
*
* @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<SubmoduleSubscription> subscriptionsToInsert =
new ArrayList<SubmoduleSubscription>();
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:
* <ul>
* <li>no subscriptions existing to destination project</li>
* <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
* <li>commit contains .gitmodules file with content</li>
*
* <pre>
* [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
* </pre>
*
* <p>
* It expects to insert new rows in subscriptions table. The rows inserted
* specifies:
* <ul>
* <li>target "dest-project" on branch "refs/heads/master"</li>
* <li>source "source-a" on branch "refs/heads/test-a" with "source-a" path</li>
* <li>source "source-b" on branch "refs/heads/test-b" with "source-b" path</li>
* </ul>
* </p>
*
* @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<SubmoduleSubscription> subscriptionsToInsert =
new ArrayList<SubmoduleSubscription>();
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:
* <ul>
* <li>one subscription existing to destination project/branch</li>
* <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
* <li>commit contains .gitmodules file with content</li>
* </ul>
*
* <pre>
* [submodule "source"]
* path = source
* url = http://localhost:8080/source
* branch = refs/heads/master
* </pre>
* <p>
* It expects to insert a new row in subscriptions table. The rows inserted
* specifies:
* <ul>
* <li>target "dest-project" on branch "refs/heads/master"</li>
* <li>source "source" on branch "refs/heads/master" with "source" path</li>
* </ul>
* </p>
* <p>
* It also expects to remove the row in subscriptions table specifying another
* project/branch subscribed to merged branch. This one to be removed is:
* <ul>
* <li>target "dest-project" on branch "refs/heads/master"</li>
* <li>source "old-source" on branch "refs/heads/master" with "old-source"
* path</li>
* </ul>
* </p>
*
* @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<SubmoduleSubscription> subscriptionsToInsert =
new ArrayList<SubmoduleSubscription>();
subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch,
new Branch.NameKey(new Project.NameKey("source"), "refs/heads/master"),
"source"));
final List<SubmoduleSubscription> oldOnesToMergedBranch =
new ArrayList<SubmoduleSubscription>();
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:
* <ul>
* <li>one subscription existing to destination project/branch with a source
* called old on refs/heads/master branch</li>
* <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
* <li>
* commit contains .gitmodules file with content</li>
* </ul>
*
* <pre>
* [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
* </pre>
* <p>
* It expects to insert a new row in subscriptions table. It should not remove
* any row. The rows inserted specifies:
* <ul>
* <li>target "dest-project" on branch "refs/heads/master"</li>
* <li>source "new" on branch "refs/heads/master" with "new" path</li>
* </ul>
* </p>
*
* @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<SubmoduleSubscription> extractedsubscriptions =
new ArrayList<SubmoduleSubscription>();
extractedsubscriptions.add(new SubmoduleSubscription(mergedBranch,
new Branch.NameKey(new Project.NameKey("new"), "refs/heads/master"),
"new"));
extractedsubscriptions.add(old);
final List<SubmoduleSubscription> oldOnesToMergedBranch =
new ArrayList<SubmoduleSubscription>();
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.
* <p>
* It expects to remove the subscriptions to destination project/branch.
* </p>
*
* @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<SubmoduleSubscription> extractedsubscriptions =
new ArrayList<SubmoduleSubscription>();
final List<SubmoduleSubscription> oldOnesToMergedBranch =
new ArrayList<SubmoduleSubscription>();
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".
* <p>
* It expects to update the git link called "source-project" to be in target
* repository.
* </p>
*
* @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<Change.Id, CodeReviewCommit> mergedCommits =
new HashMap<Change.Id, CodeReviewCommit>();
mergedCommits.put(codeReviewCommit.change.getId(), codeReviewCommit);
final List<Change> submited = new ArrayList<Change>();
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<SubmoduleSubscription> subscribers =
new ListResultSet<SubmoduleSubscription>(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<SubmoduleSubscription> emptySubscriptions =
new ListResultSet<SubmoduleSubscription>(new ArrayList<SubmoduleSubscription>());
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.
* <p>
* 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".
* <p>
* 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.
* <p>
* 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.
* </p>
*
* @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<Change.Id, CodeReviewCommit> mergedCommits =
new HashMap<Change.Id, CodeReviewCommit>();
mergedCommits.put(codeReviewCommit.change.getId(), codeReviewCommit);
final List<Change> submited = new ArrayList<Change>();
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<SubmoduleSubscription> subscribers =
new ListResultSet<SubmoduleSubscription>(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<SubmoduleSubscription> incorrectSubscriptions =
new ListResultSet<SubmoduleSubscription>(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.
* <p>
* It considers a commit containing a .gitmodules file was merged in
* refs/heads/master of a dest-project.
* </p>
* <p>
* The .gitmodules file content should indicate a source project called
* "source".
* </p>
*
* @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<SubmoduleSubscription> subscriptionsToInsert =
new ArrayList<SubmoduleSubscription>();
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.
* <p>
* In this test a commit is created and considered merged to
* <code>mergedBranch</code> branch.
* </p>
* <p>
* The destination project the commit was merged is not considered to be a
* source of another project (no subscribers found to this project).
* </p>
*
* @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<SubmoduleSubscription> extractedSubscriptions) throws Exception {
doOnlySubscriptionTableOperations(gitModulesFileContent, mergedBranch,
extractedSubscriptions, new ArrayList<SubmoduleSubscription>());
}
/**
* It calls SubmoduleOp.update method considering scenario only updating
* Subscriptions table.
* <p>
* In this test a commit is created and considered merged to
* <code>mergedBranch</code> branch.
* </p>
* <p>
* The destination project the commit was merged is not considered to be a
* source of another project (no subscribers found to this project).
* </p>
*
* @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 <code>mergedBranch</code>
* before updating the table.
* @throws Exception If an exception occurs.
*/
private void doOnlySubscriptionTableOperations(
final String gitModulesFileContent, final Branch.NameKey mergedBranch,
final List<SubmoduleSubscription> extractedSubscriptions,
final List<SubmoduleSubscription> 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<SubmoduleSubscription>(previousSubscriptions));
SortedSet<Project.NameKey> existingProjects =
new TreeSet<Project.NameKey>();
for (SubmoduleSubscription extracted : extractedSubscriptions) {
existingProjects.add(extracted.getSubmodule().getParentKey());
}
for (int index = 0; index < extractedSubscriptions.size(); index++) {
expect(repoManager.list()).andReturn(existingProjects);
}
final Set<SubmoduleSubscription> alreadySubscribeds =
new HashSet<SubmoduleSubscription>();
for (SubmoduleSubscription s : extractedSubscriptions) {
if (previousSubscriptions.contains(s)) {
alreadySubscribeds.add(s);
}
}
final Set<SubmoduleSubscription> subscriptionsToRemove =
new HashSet<SubmoduleSubscription>(previousSubscriptions);
final List<SubmoduleSubscription> subscriptionsToInsert =
new ArrayList<SubmoduleSubscription>(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<SubmoduleSubscription>(new ArrayList<SubmoduleSubscription>()));
schema.close();
doReplay();
final SubmoduleOp submoduleOp =
new SubmoduleOp(mergedBranch, mergeTip, new RevWalk(realDb),
urlProvider, schemaFactory, realDb, new Project(mergedBranch
.getParentKey()), new ArrayList<Change>(), 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;
}
}

View File

@@ -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<String, SubmoduleSection> sectionsToReturn =
new TreeMap<String, SubmoduleSection>();
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<String, String> reposToBeFound = new HashMap<String, String>();
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<SubmoduleSubscription> expectedSubscriptions =
new ArrayList<SubmoduleSubscription>();
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<String, SubmoduleSection> sectionsToReturn =
new TreeMap<String, SubmoduleSection>();
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<String, String> reposToBeFound = new HashMap<String, String>();
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<SubmoduleSubscription> expectedSubscriptions =
new ArrayList<SubmoduleSubscription>();
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<String, SubmoduleSection> sectionsToReturn =
new HashMap<String, SubmoduleSection>();
// 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<String, String>(),
new ArrayList<SubmoduleSubscription>());
}
@Test
public void testProjectNotFound() throws Exception {
Map<String, SubmoduleSection> sectionsToReturn =
new HashMap<String, SubmoduleSection>();
sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
"."));
execute(new Branch.NameKey(new Project.NameKey("super-project"),
"refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
new ArrayList<SubmoduleSubscription>());
}
@Test
public void testProjectWithSlashesNotFound() throws Exception {
Map<String, SubmoduleSection> sectionsToReturn =
new HashMap<String, SubmoduleSection>();
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<String, String>(),
new ArrayList<SubmoduleSubscription>());
}
private void execute(final Branch.NameKey superProjectBranch,
final Map<String, SubmoduleSection> sectionsToReturn,
final Map<String, String> reposToBeFound,
final List<SubmoduleSubscription> 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<Project.NameKey>(Collections
.singletonList(new Project.NameKey(projectNameCandidate))));
break;
} else {
expect(repoManager.list()).andReturn(
new TreeSet<Project.NameKey>(Collections.EMPTY_LIST));
}
}
}
}
doReplay();
final SubmoduleSectionParser ssp =
new SubmoduleSectionParser(bbc, THIS_SERVER, superProjectBranch,
repoManager);
List<SubmoduleSubscription> 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;
}
}
}