Merge branch 'stable-3.2'
Bring in all the changes made by Christian Aistleitner for the migration
of Wikimedia to stable-3.2.
Change-Id: Ia2a20068201e3cdc2b50d4e3cca23d5eeed8eba0
diff --git a/BUILD b/BUILD
index d5f6b00..3c5aaa8 100644
--- a/BUILD
+++ b/BUILD
@@ -15,6 +15,7 @@
name = "zuul",
srcs = glob(["src/main/java/**/*.java"]),
resources = glob(["src/main/**/*"]),
+ deps = ["@commons-lang3//jar"],
manifest_entries = [
"Gerrit-PluginName: zuul",
"Gerrit-Module: com.googlesource.gerrit.plugins.zuul.Module",
@@ -39,6 +40,7 @@
visibility = ["//visibility:public"],
exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
":zuul__plugin",
+ "@commons-lang3//jar",
],
)
diff --git a/gr-zuul/gr-zuul.js b/gr-zuul/gr-zuul.js
index 4afc62b..89e5527 100644
--- a/gr-zuul/gr-zuul.js
+++ b/gr-zuul/gr-zuul.js
@@ -53,10 +53,48 @@
return this.plugin.restApi().send('GET', url).then(crd => {
this._crd = crd;
this._crd_loaded = true;
- this.setHidden(!(crd.depends_on.length || crd.needed_by.length));
+ this.setHidden(!(this._isDependsOnSectionVisible()
+ || crd.needed_by.length));
});
}
+ // copied from gr-related-changes-list.js, which is inaccessible from here.
+ // Resolved uses of `this.ChangeStatus.[...]`, as that's inaccessible from here too.
+ // Removed _isIndirectAncestor check, as the needed data is inaccessible from here.
+ // Not all code paths are reachable, as we only have shallow ChangeInfo objects. We leave the
+ // code here nonetheless, to allow for easier updating from gr-related-changes-list.js.
+ _computeChangeStatusClass(change) {
+ const classes = ['status'];
+ if (change._revision_number != change._current_revision_number) {
+ classes.push('notCurrent');
+ } else if (change.submittable) {
+ classes.push('submittable');
+ } else if (change.status == 'NEW') {
+ classes.push('hidden');
+ }
+ return classes.join(' ');
+ }
+
+ // copied from gr-related-changes-list.js, which is inaccessible from here.
+ // Resolved uses of `this.ChangeStatus.[...]`, as that's inaccessible from here too.
+ // Removed _isIndirectAncestor check, as the needed data is inaccessible from here.
+ // Not all code paths are reachable, as we only have shallow ChangeInfo objects. We leave the
+ // code here nonetheless, to allow for easier updating from gr-related-changes-list.js.
+ _computeChangeStatus(change) {
+ switch (change.status) {
+ case 'MERGED':
+ return 'Merged';
+ case 'ABANDONED':
+ return 'Abandoned';
+ }
+ if (change._revision_number != change._current_revision_number) {
+ return 'Not current';
+ } else if (change.submittable) {
+ return 'Submittable';
+ }
+ return '';
+ }
+
setHidden(hidden) {
if (this.hidden != hidden) {
this.hidden = hidden;
@@ -68,8 +106,13 @@
}
}
- _computeDependencyUrl(changeId) {
- return Gerrit.Nav.getUrlForSearchQuery(changeId);
+ _computeDependencyUrl(changeInfo) {
+ return Gerrit.Nav.getUrlForSearchQuery(changeInfo.change_id);
+ }
+
+ _isDependsOnSectionVisible() {
+ return !!(this._crd.depends_on_found.length
+ + this._crd.depends_on_missing.length);
}
}
diff --git a/gr-zuul/gr-zuul_html.js b/gr-zuul/gr-zuul_html.js
index 6827042..5e208d4 100644
--- a/gr-zuul/gr-zuul_html.js
+++ b/gr-zuul/gr-zuul_html.js
@@ -60,19 +60,28 @@
.dependencyCycleDetected {
color: #d17171;
}
+ .missingFromThisServer {
+ color: #d17171;
+ }
+ .hidden {
+ display: none;
+ }
</style>
<template is="dom-if" if="[[_crd_loaded]]">
- <template is="dom-if" if="[[_crd.depends_on.length]]">
+ <template is="dom-if" if="[[_isDependsOnSectionVisible()]]">
<section class="related-changes-section">
<h4>Depends on</h4>
- <template is="dom-repeat" items="[[_crd.depends_on]]">
+ <template is="dom-repeat" items="[[_crd.depends_on_found]]">
<div class="changeContainer zuulDependencyContainer">
<a
href$="[[_computeDependencyUrl(item)]]"
- title$="[[item]]"
+ title$="[[item.project]]: [[item.branch]]: [[item.subject]]"
>
- [[item]]
+ [[item.project]]: [[item.branch]]: [[item.subject]]
</a>
+ <span class$="[[_computeChangeStatusClass(item)]]">
+ ([[_computeChangeStatus(item)]])
+ </span>
<template is="dom-if" if="[[_crd.cycle]]">
<span class="status dependencyCycleDetected">
(Dependency cycle detected)
@@ -80,6 +89,16 @@
</template>
</div>
</template>
+ <template is="dom-repeat" items="[[_crd.depends_on_missing]]">
+ <div class="changeContainer zuulDependencyContainer">
+ <span>
+ [[item]]
+ </span>
+ <span class="status missingFromThisServer">
+ (Missing from this server)
+ </span>
+ </div>
+ </template>
</section>
</template>
<template is="dom-if" if="[[_crd.needed_by.length]]">
@@ -89,10 +108,13 @@
<div class="changeContainer zuulDependencyContainer">
<a
href$="[[_computeDependencyUrl(item)]]"
- title$="[[item]]"
+ title$="[[item.project]]: [[item.branch]]: [[item.subject]]"
>
- [[item]]
+ [[item.project]]: [[item.branch]]: [[item.subject]]
</a>
+ <span class$="[[_computeChangeStatusClass(item)]]">
+ ([[_computeChangeStatus(item)]])
+ </span>
<template is="dom-if" if="[[_crd.cycle]]">
<span class="status dependencyCycleDetected">
(Dependency cycle detected)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuul/CrdInfo.java b/src/main/java/com/googlesource/gerrit/plugins/zuul/CrdInfo.java
index 3788684..84fbfa4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/zuul/CrdInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuul/CrdInfo.java
@@ -14,10 +14,20 @@
package com.googlesource.gerrit.plugins.zuul;
+import com.google.gerrit.extensions.common.ChangeInfo;
import java.util.List;
+/** Cross-repository dependencies of a Change */
public class CrdInfo {
- public List<String> dependsOn;
- public List<String> neededBy;
+ /** Shallow ChangeInfos of changes that depend on this Change and are available on this server */
+ public List<ChangeInfo> dependsOnFound;
+
+ /** Change-Ids of changes that depend on this Change and are not available on this server */
+ public List<String> dependsOnMissing;
+
+ /** Shallow ChangeInfos of changes that depend on this Change */
+ public List<ChangeInfo> neededBy;
+
+ /** true, if this change is contained in a dependency cycle */
public boolean cycle;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuul/GetCrd.java b/src/main/java/com/googlesource/gerrit/plugins/zuul/GetCrd.java
index 6e8b1a7..e536b2a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/zuul/GetCrd.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuul/GetCrd.java
@@ -14,82 +14,59 @@
package com.googlesource.gerrit.plugins.zuul;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.restapi.change.ChangesCollection;
-import com.google.gerrit.server.restapi.change.QueryChanges;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.zuul.util.DependsOnFetcher;
+import com.googlesource.gerrit.plugins.zuul.util.NeededByFetcher;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@Singleton
public class GetCrd implements RestReadView<RevisionResource> {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- private final ChangesCollection changes;
- private final CommitMessageFetcher commitMessageFetcher;
+ private final DependsOnFetcher dependsOnFetcher;
+ private final NeededByFetcher neededByFetcher;
@Inject
- GetCrd(ChangesCollection changes, CommitMessageFetcher commitMessageFetcher) {
- this.changes = changes;
- this.commitMessageFetcher = commitMessageFetcher;
+ GetCrd(DependsOnFetcher dependsOnFetcher, NeededByFetcher neededByFetcher) {
+ this.dependsOnFetcher = dependsOnFetcher;
+ this.neededByFetcher = neededByFetcher;
}
@Override
- @SuppressWarnings("unchecked")
public Response<CrdInfo> apply(RevisionResource rsrc)
throws RepositoryNotFoundException, IOException, BadRequestException, AuthException,
PermissionBackendException {
CrdInfo out = new CrdInfo();
- out.dependsOn = new ArrayList<>();
- out.neededBy = new ArrayList<>();
+ Pair<List<ChangeInfo>, List<String>> dependsOn = dependsOnFetcher.fetchForRevision(rsrc);
+ out.dependsOnFound = dependsOn.getLeft();
+ out.dependsOnMissing = dependsOn.getRight();
- Change.Key thisId = rsrc.getChange().getKey();
+ out.neededBy = neededByFetcher.fetchForChangeKey(rsrc.getChange().getKey());
- // get depends on info
- Project.NameKey p = rsrc.getChange().getProject();
- String rev = rsrc.getPatchSet().commitId().getName();
- String commitMsg = commitMessageFetcher.fetch(p, rev);
- Pattern pattern = Pattern.compile("[Dd]epends-[Oo]n:? (I[0-9a-f]{8,40})", Pattern.DOTALL);
- Matcher matcher = pattern.matcher(commitMsg);
- while (matcher.find()) {
- String otherId = matcher.group(1);
- logger.atFinest().log("Change %s depends on change %s", thisId, otherId);
- out.dependsOn.add(otherId);
- }
+ List<String> dependsOnAllKeys = new ArrayList<>(out.dependsOnMissing);
+ dependsOnAllKeys.addAll(
+ out.dependsOnFound.stream()
+ .map(changeInfo -> changeInfo.changeId)
+ .collect(Collectors.toList()));
- // get needed by info
- QueryChanges query = changes.list();
- String neededByQuery = "message:" + thisId + " -change:" + thisId;
- query.addQuery(neededByQuery);
- Response<List<?>> response = query.apply(TopLevelResource.INSTANCE);
- List<ChangeInfo> changes = (List<ChangeInfo>) response.value();
- // check for dependency cycles
- for (ChangeInfo other : changes) {
- String otherId = other.changeId;
- logger.atFinest().log("Change %s needed by %s", thisId, otherId);
- if (out.dependsOn.contains(otherId)) {
- logger.atFiner().log(
- "Detected dependency cycle between changes %s and %s", thisId, otherId);
+ out.cycle = false;
+ for (ChangeInfo changeInfo : out.neededBy) {
+ if (dependsOnAllKeys.contains(changeInfo.changeId)) {
out.cycle = true;
+ break;
}
- out.neededBy.add(otherId);
}
-
return Response.ok(out);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuul/CommitMessageFetcher.java b/src/main/java/com/googlesource/gerrit/plugins/zuul/util/CommitMessageFetcher.java
similarity index 69%
rename from src/main/java/com/googlesource/gerrit/plugins/zuul/CommitMessageFetcher.java
rename to src/main/java/com/googlesource/gerrit/plugins/zuul/util/CommitMessageFetcher.java
index 961cad8..6bb2d4e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/zuul/CommitMessageFetcher.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuul/util/CommitMessageFetcher.java
@@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.googlesource.gerrit.plugins.zuul;
+package com.googlesource.gerrit.plugins.zuul.util;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import java.io.IOException;
@@ -40,4 +42,19 @@
return commit.getFullMessage();
}
}
+
+ /**
+ * Extracts the commit message of the most current revision of a change.
+ *
+ * <p>The ChangeInfo must have the {@link CommitInfo} of at least the most current revision
+ * loaded.
+ *
+ * @param changeInfo The ChangeInfo to extract the commit message from
+ * @return the extracted commit message
+ */
+ public String fetch(ChangeInfo changeInfo) {
+ String current = changeInfo.currentRevision;
+ CommitInfo commitInfo = changeInfo.revisions.get(current).commit;
+ return commitInfo.message;
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnExtractor.java
new file mode 100644
index 0000000..5860940
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnExtractor.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2020 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.googlesource.gerrit.plugins.zuul.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Extracts dependency information from texts. */
+public class DependsOnExtractor {
+ public List<String> extract(String commitMessage) {
+ // TODO Add support for URL based `Depends-On` references.
+ List<String> dependsOn = new ArrayList<>();
+ Pattern pattern =
+ Pattern.compile(
+ "^Depends-On: (I[0-9a-f]{40})\\s*$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
+ Matcher matcher = pattern.matcher(commitMessage);
+ while (matcher.find()) {
+ String key = matcher.group(1);
+ dependsOn.add(key);
+ }
+ return dependsOn;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnFetcher.java b/src/main/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnFetcher.java
new file mode 100644
index 0000000..0d599a1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnFetcher.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2020 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.googlesource.gerrit.plugins.zuul.util;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.change.ChangesCollection;
+import com.google.gerrit.server.restapi.change.QueryChanges;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.commons.lang3.tuple.Pair;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+/** Fetches the Depends-On part of cross repository dependencies. */
+public class DependsOnFetcher {
+ private final ChangesCollection changes;
+ private final CommitMessageFetcher commitMessageFetcher;
+ private final DependsOnExtractor dependsOnExtractor;
+
+ @Inject
+ public DependsOnFetcher(
+ ChangesCollection changes,
+ CommitMessageFetcher commitMessageFetcher,
+ DependsOnExtractor dependsOnExtractor) {
+ this.changes = changes;
+ this.commitMessageFetcher = commitMessageFetcher;
+ this.dependsOnExtractor = dependsOnExtractor;
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<ChangeInfo> fetchChangeInfosForChangeKeys(List<String> keys)
+ throws BadRequestException, AuthException, PermissionBackendException {
+ List<ChangeInfo> ret;
+ if (keys.isEmpty()) {
+ ret = new ArrayList<>();
+ } else {
+ QueryChanges query = changes.list();
+ String queryString = "change:" + String.join(" OR change:", keys);
+ query.addQuery(queryString);
+ Response<List<?>> response = query.apply(TopLevelResource.INSTANCE);
+ ret = (List<ChangeInfo>) response.value();
+ }
+ return ret;
+ }
+
+ public Pair<List<ChangeInfo>, List<String>> fetchForRevision(RevisionResource rsrc)
+ throws RepositoryNotFoundException, IOException, BadRequestException, AuthException,
+ PermissionBackendException {
+ Project.NameKey p = rsrc.getChange().getProject();
+ String rev = rsrc.getPatchSet().commitId().getName();
+ String commitMsg = commitMessageFetcher.fetch(p, rev);
+
+ List<String> extractedChangeKeys = dependsOnExtractor.extract(commitMsg);
+ List<ChangeInfo> foundChangeInfos = fetchChangeInfosForChangeKeys(extractedChangeKeys);
+
+ // `extracted` and `found` need not agree in size. It might be that a Change-Id from
+ // `extracted` matches more than one Change (E.g.: cherry-picked to different branch). And it
+ // might be that a Change-Id from `extracted` does not yield a result.
+ // So we need to check that `found` holds at least one ChangeInfo for each Change-Id in
+ // `extractedDependsOn`.
+
+ List<String> resultMissing = Lists.newArrayList(extractedChangeKeys);
+ List<ChangeInfo> resultFound = new ArrayList<>(extractedChangeKeys.size());
+
+ for (ChangeInfo changeInfo : foundChangeInfos) {
+ String changeId = changeInfo.changeId.toString();
+ if (extractedChangeKeys.contains(changeId)) {
+ resultMissing.remove(changeId);
+ resultFound.add(changeInfo);
+ }
+ }
+ return new ImmutablePair<>(resultFound, resultMissing);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/zuul/util/NeededByFetcher.java b/src/main/java/com/googlesource/gerrit/plugins/zuul/util/NeededByFetcher.java
new file mode 100644
index 0000000..7475fb4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/zuul/util/NeededByFetcher.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2020 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.googlesource.gerrit.plugins.zuul.util;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.change.ChangesCollection;
+import com.google.gerrit.server.restapi.change.QueryChanges;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Fetches the Needed-By part of cross repository dependencies. */
+public class NeededByFetcher {
+ private final ChangesCollection changes;
+ private final CommitMessageFetcher commitMessageFetcher;
+ private final DependsOnExtractor dependsOnExtractor;
+
+ @Inject
+ public NeededByFetcher(
+ ChangesCollection changes,
+ CommitMessageFetcher commitMessageFetcher,
+ DependsOnExtractor dependsOnExtractor) {
+ this.changes = changes;
+ this.commitMessageFetcher = commitMessageFetcher;
+ this.dependsOnExtractor = dependsOnExtractor;
+ }
+
+ public List<ChangeInfo> fetchForChangeKey(Change.Key key)
+ throws BadRequestException, AuthException, PermissionBackendException {
+ // TODO Add support for URL based `Depends-On` references.
+ String keyString = key.toString();
+ List<ChangeInfo> neededBy = new ArrayList<>();
+
+ QueryChanges query = changes.list();
+ String neededByQuery = "message:" + keyString + " -change:" + keyString;
+ query.addOption(ListChangesOption.CURRENT_REVISION);
+ query.addOption(ListChangesOption.CURRENT_COMMIT);
+ query.addQuery(neededByQuery);
+ Response<List<?>> response = query.apply(TopLevelResource.INSTANCE);
+ @SuppressWarnings("unchecked")
+ List<ChangeInfo> changes = (List<ChangeInfo>) response.value();
+ for (ChangeInfo changeInfo : changes) {
+ // The search found the key somewhere in the commit message. But this need not be a
+ // `Depends-On`. `key` might be mentioned for a completely different reason. So we need to
+ // check if `key` occurs in a `Depends-On`.
+ String commitMessage = commitMessageFetcher.fetch(changeInfo);
+ List<String> dependencies = dependsOnExtractor.extract(commitMessage);
+ if (dependencies.contains(keyString)) {
+ neededBy.add(changeInfo);
+ }
+ }
+ return neededBy;
+ }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 19d67ec..dfa5148 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -1,5 +1,21 @@
-The @PLUGIN@ plugin detects Zuul [cross repository dependencies] (CRD) in
-commit messages.
+The @PLUGIN@ plugin detects Zuul [cross repository dependencies] (CRD) in
+commit messages and displays them on the Gerrit UI as "Depends on" and "Needed
+by" sections in the related changes section.
-[cross repository dependencies]: http://docs.openstack.org/infra/zuul/gating.html#cross-repository-dependencies
-[Cycles]: http://docs.openstack.org/infra/zuul/gating.html#cycles
+![user interface overview](images/overview.png)
+
+[Dependency cycles] (although currently only direct dependencies are considered) get highlighted
+
+![cycles screenshot](images/cycles.png)
+
+And dependencies that are not available on the server get highlighted too.
+
+![missing dependencies screenshot](images/missing.png)
+
+The cross repository dependencies are also exposed through the [REST API](rest-api-changes.html).
+
+This plugin currently does not support URL based dependencies, but only
+`Change-Id` dependencies, which got deprecated in Zuul v3.
+
+[cross repository dependencies]: https://zuul-ci.org/docs/zuul/discussion/gating.html#cross-project-dependencies
+[Dependency cycles]: https://zuul-ci.org/docs/zuul/discussion/gating.html#cycles
diff --git a/src/main/resources/Documentation/images/cycles.png b/src/main/resources/Documentation/images/cycles.png
new file mode 100644
index 0000000..8cad8b3
--- /dev/null
+++ b/src/main/resources/Documentation/images/cycles.png
Binary files differ
diff --git a/src/main/resources/Documentation/images/missing.png b/src/main/resources/Documentation/images/missing.png
new file mode 100644
index 0000000..2fc4516
--- /dev/null
+++ b/src/main/resources/Documentation/images/missing.png
Binary files differ
diff --git a/src/main/resources/Documentation/images/overview.png b/src/main/resources/Documentation/images/overview.png
new file mode 100644
index 0000000..172112e
--- /dev/null
+++ b/src/main/resources/Documentation/images/overview.png
Binary files differ
diff --git a/src/main/resources/Documentation/rest-api-changes.md b/src/main/resources/Documentation/rest-api-changes.md
index 4f24231..9095d96 100644
--- a/src/main/resources/Documentation/rest-api-changes.md
+++ b/src/main/resources/Documentation/rest-api-changes.md
@@ -12,7 +12,7 @@
### <a id="get-crd"> Get CRD
-__GET__ /changes/{change-id}/revisions/{revision-id}/@PLUGIN@~crd
+'GET /changes/[\{change-id\}](../../../Documentation/rest-api-changes.html#change-id)/revisions/[\{revision-id\}](../../../Documentation/rest-api-changes.html#revision-id)/crd'
Gets the zuul [CRD](#crd-info) for a change. Please refer to the
general [changes rest api](../../../Documentation/rest-api-changes.html#get-review)
@@ -21,9 +21,11 @@
#### Request
```
- GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/@PLUGIN@~crd HTTP/1.0
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/crd HTTP/1.0
```
+As response a [CrdInfo](#crd-info) entity is returned that describes the cross-repository dependencies.
+
#### Response
```
@@ -33,12 +35,25 @@
)]}'
{
- "depends_on": [
- "Ic79ed94daa9b58527139aadba1b0d59d1f54754b",
- "I66853bf0c18e60f8de14d44dfb7c2ca1c3793111"
+ "depends_on_found": [
+ {
+ "id": "repo1~master~Ic0f5bcc8f998dfc0f1b7164de7a824f7832d4abe",
+ "project": "zuul/repo1",
+ "branch": "master",
+ [...]
+ }
+ ],
+ "depends_on_missing": [
+ "Ib01834990d3791330d65c469e9a3f93db6eb41f0",
+ "Ic0f5bcc8f998dfc0f1b7164de7a824f7832d4abe",
],
"needed_by": [
- "I66853bf0c18e60f8de14d44dfb7c2ca1c379311d"
+ {
+ "id": "another%2Frepo~master~I8944323ed34d55af7a17a48c8d8509f3cf62b6bf",
+ "project": "zuul/repo1",
+ "branch": "master",
+ [...]
+ }
],
"cycle": false
}
@@ -51,11 +66,12 @@
The `CrdInfo` entity shows zuul dependencies on a patch set.
-|Field Name |Description|
-|:----------|:----------|
-|depends_on |List of changes that this change depends on|
-|needed_by |List of changes that is dependent on this change|
-|cycle |Whether this change is in a circular dependency chain|
+|Field Name |Description|
+|:------------------|:----------|
+|depends_on_found |List of shallow [ChangeInfo](../../../Documentation/rest-api-changes.html#change-info) entities. One for each Change that is available on this server and this change depends on|
+|depends_on_missing |List of Change-Ids. One for each change that is not available on this server although this change depends on|
+|needed_by |List of shallow [ChangeInfo](../../../Documentation/rest-api-changes.html#change-info) entities. One for each change that is dependent on this change|
+|cycle |Whether this change is in a circular dependency chain|
SEE ALSO
diff --git a/src/test/java/com/googlesource/gerrit/plugins/zuul/GetCrdTest.java b/src/test/java/com/googlesource/gerrit/plugins/zuul/GetCrdTest.java
index 019f5c3..71a46ee 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/zuul/GetCrdTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/zuul/GetCrdTest.java
@@ -14,210 +14,242 @@
package com.googlesource.gerrit.plugins.zuul;
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.restapi.change.ChangesCollection;
-import com.google.gerrit.server.restapi.change.QueryChanges;
-import java.sql.Timestamp;
+import com.googlesource.gerrit.plugins.zuul.util.DependsOnFetcher;
+import com.googlesource.gerrit.plugins.zuul.util.NeededByFetcher;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-import org.eclipse.jgit.lib.ObjectId;
+import java.util.Map;
+import org.apache.commons.lang3.tuple.ImmutablePair;
import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
public class GetCrdTest {
- private ChangesCollection changes;
- private CommitMessageFetcher commitMessageFetcher;
private RevisionResource rsrc;
+ private DependsOnFetcher dependsOnFetcher;
+ private NeededByFetcher neededByFetcher;
+ private Map<Integer, ChangeInfo> changeInfos = new HashMap<>();
@Test
public void testNoDependencies() throws Exception {
- String commitMessage = "subject";
- configureMocks(commitMessage, new ArrayList<>());
+ configureMocks(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
GetCrd getCrd = createGetCrd();
Response<CrdInfo> response = getCrd.apply(rsrc);
assertThat(response.statusCode()).isEqualTo(200);
CrdInfo crdInfo = response.value();
- assertThat(crdInfo.dependsOn).isEmpty();
+ assertThat(crdInfo.dependsOnFound).isEmpty();
+ assertThat(crdInfo.dependsOnMissing).isEmpty();
assertThat(crdInfo.neededBy).isEmpty();
assertThat(crdInfo.cycle).isFalse();
}
@Test
- public void testSingleDependsOn() throws Exception {
- String commitMessage = "subject\nDepends-On: I00000000";
- configureMocks(commitMessage, new ArrayList<>());
+ public void testSingleFoundDependsOn() throws Exception {
+ ArrayList<ChangeInfo> dependsOnFound = new ArrayList<>();
+ dependsOnFound.add(getChangeInfo(0));
+
+ configureMocks(dependsOnFound, new ArrayList<>(), new ArrayList<>());
GetCrd getCrd = createGetCrd();
Response<CrdInfo> response = getCrd.apply(rsrc);
assertThat(response.statusCode()).isEqualTo(200);
CrdInfo crdInfo = response.value();
- assertThat(crdInfo.dependsOn).containsExactly("I00000000");
+ assertThat(crdInfo.dependsOnFound).containsExactly(getChangeInfo(0));
+ assertThat(crdInfo.dependsOnMissing).isEmpty();
assertThat(crdInfo.neededBy).isEmpty();
assertThat(crdInfo.cycle).isFalse();
}
@Test
- public void testMultipleDependsOn() throws Exception {
- String commitMessage =
- "subject\nDepends-On: I00000000\nDepends-On: I00000002\nDepends-On: I00000004";
- configureMocks(commitMessage, new ArrayList<>());
+ public void testSingleMissingDependsOn() throws Exception {
+ ArrayList<String> dependsOnMissing = new ArrayList<>();
+ dependsOnMissing.add(getChangeKey(0));
+
+ configureMocks(new ArrayList<>(), dependsOnMissing, new ArrayList<>());
GetCrd getCrd = createGetCrd();
Response<CrdInfo> response = getCrd.apply(rsrc);
assertThat(response.statusCode()).isEqualTo(200);
CrdInfo crdInfo = response.value();
- assertThat(crdInfo.dependsOn).containsExactly("I00000000", "I00000002", "I00000004");
+ assertThat(crdInfo.dependsOnFound).isEmpty();
+ assertThat(crdInfo.dependsOnMissing).containsExactly(getChangeKey(0));
+ assertThat(crdInfo.neededBy).isEmpty();
+ assertThat(crdInfo.cycle).isFalse();
+ }
+
+ @Test
+ public void testMultipleFoundDependsOn() throws Exception {
+ ArrayList<ChangeInfo> dependsOnFound = new ArrayList<>();
+ dependsOnFound.add(getChangeInfo(0));
+ dependsOnFound.add(getChangeInfo(2));
+ dependsOnFound.add(getChangeInfo(4));
+
+ configureMocks(dependsOnFound, new ArrayList<>(), new ArrayList<>());
+
+ GetCrd getCrd = createGetCrd();
+ Response<CrdInfo> response = getCrd.apply(rsrc);
+
+ assertThat(response.statusCode()).isEqualTo(200);
+ CrdInfo crdInfo = response.value();
+ assertThat(crdInfo.dependsOnFound)
+ .containsExactly(getChangeInfo(0), getChangeInfo(2), getChangeInfo(4));
+ assertThat(crdInfo.dependsOnMissing).isEmpty();
+ assertThat(crdInfo.neededBy).isEmpty();
+ assertThat(crdInfo.cycle).isFalse();
+ }
+
+ @Test
+ public void testMultipleMissingDependsOn() throws Exception {
+ ArrayList<String> dependsOnMissing = new ArrayList<>();
+ dependsOnMissing.add(getChangeKey(0));
+ dependsOnMissing.add(getChangeKey(2));
+ dependsOnMissing.add(getChangeKey(4));
+
+ configureMocks(new ArrayList<>(), dependsOnMissing, new ArrayList<>());
+
+ GetCrd getCrd = createGetCrd();
+ Response<CrdInfo> response = getCrd.apply(rsrc);
+
+ assertThat(response.statusCode()).isEqualTo(200);
+ CrdInfo crdInfo = response.value();
+ assertThat(crdInfo.dependsOnFound).isEmpty();
+ assertThat(crdInfo.dependsOnMissing)
+ .containsExactly(getChangeKey(0), getChangeKey(2), getChangeKey(4));
assertThat(crdInfo.neededBy).isEmpty();
assertThat(crdInfo.cycle).isFalse();
}
@Test
public void testSingleNeededBy() throws Exception {
- String commitMessage = "subject";
- List<ChangeInfo> searchResults = new ArrayList<>();
- searchResults.add(changeInfo("I00000001"));
- configureMocks(commitMessage, searchResults);
+ List<ChangeInfo> neededBy = new ArrayList<>();
+ neededBy.add(getChangeInfo(1));
+
+ configureMocks(new ArrayList<>(), new ArrayList<>(), neededBy);
GetCrd getCrd = createGetCrd();
Response<CrdInfo> response = getCrd.apply(rsrc);
assertThat(response.statusCode()).isEqualTo(200);
CrdInfo crdInfo = response.value();
- assertThat(crdInfo.dependsOn).isEmpty();
- assertThat(crdInfo.neededBy).containsExactly("I00000001");
+ assertThat(crdInfo.dependsOnFound).isEmpty();
+ assertThat(crdInfo.dependsOnMissing).isEmpty();
+ assertThat(crdInfo.neededBy).containsExactly(getChangeInfo(1));
assertThat(crdInfo.cycle).isFalse();
}
@Test
public void testMultipleNeededBy() throws Exception {
- String commitMessage = "subject";
- List<ChangeInfo> searchResults = new ArrayList<>();
- searchResults.add(changeInfo("I00000001"));
- searchResults.add(changeInfo("I00000003"));
- searchResults.add(changeInfo("I00000005"));
- configureMocks(commitMessage, searchResults);
+ List<ChangeInfo> neededBy = new ArrayList<>();
+ neededBy.add(getChangeInfo(1));
+ neededBy.add(getChangeInfo(3));
+ neededBy.add(getChangeInfo(5));
+
+ configureMocks(new ArrayList<>(), new ArrayList<>(), neededBy);
GetCrd getCrd = createGetCrd();
Response<CrdInfo> response = getCrd.apply(rsrc);
assertThat(response.statusCode()).isEqualTo(200);
CrdInfo crdInfo = response.value();
- assertThat(crdInfo.dependsOn).isEmpty();
- assertThat(crdInfo.neededBy).containsExactly("I00000001", "I00000003", "I00000005");
+ assertThat(crdInfo.dependsOnFound).isEmpty();
+ assertThat(crdInfo.dependsOnMissing).isEmpty();
+ assertThat(crdInfo.neededBy)
+ .containsExactly(getChangeInfo(1), getChangeInfo(3), getChangeInfo(5));
assertThat(crdInfo.cycle).isFalse();
}
@Test
public void testMixed() throws Exception {
- String commitMessage = "subject\nDepends-On: I00000002\nDepends-On: I00000004";
- List<ChangeInfo> searchResults = new ArrayList<>();
- searchResults.add(changeInfo("I00000001"));
- searchResults.add(changeInfo("I00000003"));
- configureMocks(commitMessage, searchResults);
+ List<ChangeInfo> dependsOnFound = new ArrayList<>();
+ dependsOnFound.add(getChangeInfo(2));
+ dependsOnFound.add(getChangeInfo(4));
+
+ List<String> dependsOnMissing = new ArrayList<>();
+ dependsOnMissing.add(getChangeKey(5));
+ dependsOnMissing.add(getChangeKey(6));
+
+ List<ChangeInfo> neededBy = new ArrayList<>();
+ neededBy.add(getChangeInfo(1));
+ neededBy.add(getChangeInfo(3));
+
+ configureMocks(dependsOnFound, dependsOnMissing, neededBy);
GetCrd getCrd = createGetCrd();
Response<CrdInfo> response = getCrd.apply(rsrc);
assertThat(response.statusCode()).isEqualTo(200);
CrdInfo crdInfo = response.value();
- assertThat(crdInfo.dependsOn).containsExactly("I00000002", "I00000004");
- assertThat(crdInfo.neededBy).containsExactly("I00000001", "I00000003");
+ assertThat(crdInfo.dependsOnFound).containsExactly(getChangeInfo(2), getChangeInfo(4));
+ assertThat(crdInfo.dependsOnMissing).containsExactly(getChangeKey(5), getChangeKey(6));
+ assertThat(crdInfo.neededBy).containsExactly(getChangeInfo(1), getChangeInfo(3));
assertThat(crdInfo.cycle).isFalse();
}
@Test
public void testSimpleCycle() throws Exception {
- String commitMessage = "subject\nDepends-On: I00000001";
- List<ChangeInfo> searchResults = new ArrayList<>();
- searchResults.add(changeInfo("I00000001"));
- configureMocks(commitMessage, searchResults);
+ List<ChangeInfo> dependsOn = new ArrayList<>();
+ dependsOn.add(getChangeInfo(1));
+
+ List<ChangeInfo> neededBy = new ArrayList<>();
+ neededBy.add(getChangeInfo(1));
+
+ configureMocks(dependsOn, new ArrayList<>(), neededBy);
GetCrd getCrd = createGetCrd();
Response<CrdInfo> response = getCrd.apply(rsrc);
assertThat(response.statusCode()).isEqualTo(200);
CrdInfo crdInfo = response.value();
- assertThat(crdInfo.dependsOn).containsExactly("I00000001");
- assertThat(crdInfo.neededBy).containsExactly("I00000001");
+ assertThat(crdInfo.dependsOnFound).containsExactly(getChangeInfo(1));
+ assertThat(crdInfo.dependsOnMissing).isEmpty();
+ assertThat(crdInfo.neededBy).containsExactly(getChangeInfo(1));
assertThat(crdInfo.cycle).isTrue();
}
- public void configureMocks(String commitMessage, final List<ChangeInfo> searchResult)
+ public void configureMocks(
+ final List<ChangeInfo> dependsOnFound,
+ final List<String> dependsOnMissing,
+ final List<ChangeInfo> neededBy)
throws Exception {
- String commitId = "0123456789012345678901234567890123456789";
-
- Project.NameKey projectNameKey = Project.nameKey("projectFoo");
-
- PatchSet patchSet = mock(PatchSet.class);
- when(patchSet.commitId()).thenReturn(ObjectId.fromString(commitId));
-
- Change change =
- new Change(
- Change.key("I0123456789"),
- Change.id(4711),
- Account.id(23),
- BranchNameKey.create(projectNameKey, "branchBar"),
- new Timestamp(0));
-
+ Change.Key changeKey = Change.key("I0123456789");
+ Change change = new Change(changeKey, null, null, null, null);
rsrc = mock(RevisionResource.class);
when(rsrc.getChange()).thenReturn(change);
- when(rsrc.getPatchSet()).thenReturn(patchSet);
- QueryChanges queryChanges = mock(QueryChanges.class);
- final AtomicBoolean addedQuery = new AtomicBoolean(false);
- doAnswer(
- new Answer<Void>() {
- @Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
- addedQuery.getAndSet(true);
- return null;
- }
- })
- .when(queryChanges)
- .addQuery("message:I0123456789 -change:I0123456789");
- when(queryChanges.apply(TopLevelResource.INSTANCE))
- .thenAnswer(
- new Answer<Response<List<ChangeInfo>>>() {
+ dependsOnFetcher = mock(DependsOnFetcher.class);
+ when(dependsOnFetcher.fetchForRevision(rsrc))
+ .thenReturn(new ImmutablePair<>(dependsOnFound, dependsOnMissing));
- @Override
- public Response<List<ChangeInfo>> answer(InvocationOnMock invocation)
- throws Throwable {
- return Response.ok(addedQuery.get() ? searchResult : null);
- }
- });
-
- changes = mock(ChangesCollection.class);
- when(changes.list()).thenReturn(queryChanges);
-
- commitMessageFetcher = mock(CommitMessageFetcher.class);
- when(commitMessageFetcher.fetch(projectNameKey, commitId)).thenReturn(commitMessage);
+ neededByFetcher = mock(NeededByFetcher.class);
+ when(neededByFetcher.fetchForChangeKey(changeKey)).thenReturn(neededBy);
}
- private ChangeInfo changeInfo(String changeId) {
- ChangeInfo changeInfo = new ChangeInfo();
- changeInfo.changeId = changeId;
- return changeInfo;
+ private String getChangeKey(int keyEnding) {
+ return "I0123456789abcdef0000000000000000000" + (10000 + keyEnding);
+ }
+
+ private ChangeInfo getChangeInfo(int keyEnding) {
+ return changeInfos.computeIfAbsent(
+ keyEnding,
+ neededKeyEnding -> {
+ ChangeInfo changeInfo = new ChangeInfo();
+ changeInfo.changeId = getChangeKey(neededKeyEnding);
+ changeInfo._number = neededKeyEnding;
+ return changeInfo;
+ });
}
private GetCrd createGetCrd() {
- return new GetCrd(changes, commitMessageFetcher);
+ return new GetCrd(dependsOnFetcher, neededByFetcher);
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/zuul/CommitMessageFetcherTest.java b/src/test/java/com/googlesource/gerrit/plugins/zuul/util/CommitMessageFetcherTest.java
similarity index 76%
rename from src/test/java/com/googlesource/gerrit/plugins/zuul/CommitMessageFetcherTest.java
rename to src/test/java/com/googlesource/gerrit/plugins/zuul/util/CommitMessageFetcherTest.java
index edda09e..905683c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/zuul/CommitMessageFetcherTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/zuul/util/CommitMessageFetcherTest.java
@@ -11,7 +11,7 @@
// 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.googlesource.gerrit.plugins.zuul;
+package com.googlesource.gerrit.plugins.zuul.util;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -21,9 +21,13 @@
import static org.mockito.Mockito.when;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.server.git.GitRepositoryManager;
import java.io.IOException;
import java.math.BigInteger;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jgit.errors.MissingObjectException;
@@ -84,6 +88,34 @@
assertThat(commitMessage).isEqualTo("CommitMsg\n");
}
+ @Test
+ public void testFetchChangeInfoSingleRevision() {
+ ChangeInfo changeInfo = new ChangeInfo();
+ changeInfo.currentRevision = "foo";
+ changeInfo.revisions = new HashMap<>();
+ changeInfo.revisions.put("foo", createRevisionInfo("CommitMsg"));
+
+ CommitMessageFetcher fetcher = createCommitMessageFetcher();
+ String commitMessage = fetcher.fetch(changeInfo);
+
+ assertThat(commitMessage).isEqualTo("CommitMsg");
+ }
+
+ @Test
+ public void testFetchChangeInfoMultipleRevisions() {
+ ChangeInfo changeInfo = new ChangeInfo();
+ changeInfo.currentRevision = "bar";
+ changeInfo.revisions = new HashMap<>();
+ changeInfo.revisions.put("foo", createRevisionInfo("CommitMsgFoo"));
+ changeInfo.revisions.put("bar", createRevisionInfo("CommitMsgBar"));
+ changeInfo.revisions.put("baz", createRevisionInfo("CommitMsgBaz"));
+
+ CommitMessageFetcher fetcher = createCommitMessageFetcher();
+ String commitMessage = fetcher.fetch(changeInfo);
+
+ assertThat(commitMessage).isEqualTo("CommitMsgBar");
+ }
+
@Before
public void setUp() throws Exception {
ObjectLoader objectLoaderBlob = mock(ObjectLoader.class);
@@ -117,6 +149,16 @@
when(repoManager.openRepository(eq(Project.nameKey("ProjectFoo")))).thenReturn(repo);
}
+ private RevisionInfo createRevisionInfo(String commitMessage) {
+ CommitInfo commitInfo = new CommitInfo();
+ commitInfo.message = commitMessage;
+
+ RevisionInfo revisionInfo = new RevisionInfo();
+ revisionInfo.commit = commitInfo;
+
+ return revisionInfo;
+ }
+
private CommitMessageFetcher createCommitMessageFetcher() {
return new CommitMessageFetcher(repoManager);
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnExtractorTest.java
new file mode 100644
index 0000000..70344f7
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnExtractorTest.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2020 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.googlesource.gerrit.plugins.zuul.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Change;
+import java.util.List;
+import org.junit.Test;
+
+public class DependsOnExtractorTest {
+ @Test
+ public void testExtractNoDependies() throws Exception {
+ DependsOnExtractor extractor = new DependsOnExtractor();
+
+ List<String> extracted = extractor.extract("foo");
+
+ assertThat(extracted).isEmpty();
+ }
+
+ @Test
+ public void testExtractNoDependencyTooShortChangeId() throws Exception {
+ DependsOnExtractor extractor = new DependsOnExtractor();
+ String commitMessage = "subject\n\nDepends-On: I123456789112345678921234567893123456789";
+
+ List<String> extracted = extractor.extract(commitMessage);
+
+ assertThat(extracted).isEmpty();
+ }
+
+ @Test
+ public void testExtractNoDependencyTooLongChangeId() throws Exception {
+ DependsOnExtractor extractor = new DependsOnExtractor();
+ String commitMessage = "subject\n\nDepends-On: I12345678911234567892123456789312345678941";
+
+ List<String> extracted = extractor.extract(commitMessage);
+
+ assertThat(extracted).isEmpty();
+ }
+
+ @Test
+ public void testExtractSingleDependencyLineEnd() throws Exception {
+ DependsOnExtractor extractor = new DependsOnExtractor();
+ String commitMessage = "subject\n\nDepends-On: " + getChangeKey(1);
+
+ List<String> extracted = extractor.extract(commitMessage);
+
+ assertThat(extracted).containsExactly(getChangeKey(1));
+ }
+
+ @Test
+ public void testExtractSingleDependencyContinuedLine() throws Exception {
+ DependsOnExtractor extractor = new DependsOnExtractor();
+ String commitMessage = "subject\n\nDepends-On: " + getChangeKey(1) + " ";
+
+ List<String> extracted = extractor.extract(commitMessage);
+
+ assertThat(extracted).containsExactly(getChangeKey(1));
+ }
+
+ @Test
+ public void testExtractMultipleDependencies() throws Exception {
+ DependsOnExtractor extractor = new DependsOnExtractor();
+ String commitMessage =
+ "subject\n\nDepends-On: "
+ + getChangeKey(1)
+ + "\nDepends-On: "
+ + getChangeKey(2)
+ + "\nDepends-On: "
+ + getChangeKey(3);
+
+ List<String> extracted = extractor.extract(commitMessage);
+
+ assertThat(extracted).containsExactly(getChangeKey(1), getChangeKey(2), getChangeKey(3));
+ }
+
+ @Test
+ public void testExtractLowerCase() throws Exception {
+ DependsOnExtractor extractor = new DependsOnExtractor();
+ String commitMessage = "subject\n\ndepends-on: " + getChangeKey(1);
+
+ List<String> extracted = extractor.extract(commitMessage);
+
+ assertThat(extracted).containsExactly(getChangeKey(1));
+ }
+
+ @Test
+ public void testExtractMixedCases() throws Exception {
+ DependsOnExtractor extractor = new DependsOnExtractor();
+ String commitMessage =
+ "subject\n\ndepends-On: " + getChangeKey(1) + "\nDePeNds-on: " + getChangeKey(2);
+
+ List<String> extracted = extractor.extract(commitMessage);
+
+ assertThat(extracted).containsExactly(getChangeKey(1), getChangeKey(2));
+ }
+
+ private String getChangeKey(int keyEnding) {
+ return Change.key("I0123456789abcdef0000000000000000000" + (10000 + keyEnding)).toString();
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnFetcherTest.java b/src/test/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnFetcherTest.java
new file mode 100644
index 0000000..75247df
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/zuul/util/DependsOnFetcherTest.java
@@ -0,0 +1,264 @@
+// Copyright (C) 2020 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.googlesource.gerrit.plugins.zuul.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.change.ChangesCollection;
+import com.google.gerrit.server.restapi.change.QueryChanges;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.commons.lang3.tuple.Pair;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+public class DependsOnFetcherTest {
+ private ChangesCollection changes;
+ private CommitMessageFetcher commitMessageFetcher;
+ private DependsOnExtractor dependsOnExtractor;
+ private RevisionResource rsrc;
+ private Map<Integer, ChangeInfo> changeInfos = new HashMap<>();
+
+ @Test
+ public void testExtractNoDependencies() throws Exception {
+ configureMocks(new ArrayList<>(), new ArrayList<>());
+
+ DependsOnFetcher fetcher = createFetcher();
+ Pair<List<ChangeInfo>, List<String>> dependsOn = fetcher.fetchForRevision(rsrc);
+
+ assertThat(dependsOn.getLeft()).isEmpty();
+ assertThat(dependsOn.getRight()).isEmpty();
+ }
+
+ @Test
+ public void testExtractSingleFoundDependency() throws Exception {
+ List<String> extracted = new ArrayList<>();
+ extracted.add(getChangeKey(1));
+
+ List<ChangeInfo> searchResult = new ArrayList<>();
+ searchResult.add(getChangeInfo(1));
+ configureMocks(extracted, searchResult);
+
+ DependsOnFetcher fetcher = createFetcher();
+ Pair<List<ChangeInfo>, List<String>> dependsOn = fetcher.fetchForRevision(rsrc);
+
+ assertThat(dependsOn.getLeft()).containsExactly(getChangeInfo(1));
+ assertThat(dependsOn.getRight()).isEmpty();
+ }
+
+ @Test
+ public void testExtractMultipleFoundDependencies() throws Exception {
+ List<String> extracted = new ArrayList<>();
+ extracted.add(getChangeKey(1));
+ extracted.add(getChangeKey(2));
+ extracted.add(getChangeKey(3));
+ List<ChangeInfo> searchResult = new ArrayList<>();
+ searchResult.add(getChangeInfo(1));
+ searchResult.add(getChangeInfo(2));
+ searchResult.add(getChangeInfo(3));
+ configureMocks(extracted, searchResult);
+
+ DependsOnFetcher fetcher = createFetcher();
+ Pair<List<ChangeInfo>, List<String>> dependsOn = fetcher.fetchForRevision(rsrc);
+
+ assertThat(dependsOn.getLeft())
+ .containsExactly(getChangeInfo(1), getChangeInfo(2), getChangeInfo(3));
+ assertThat(dependsOn.getRight()).isEmpty();
+ }
+
+ @Test
+ public void testExtractSingleMissingDependency() throws Exception {
+ List<String> extracted = new ArrayList<>();
+ extracted.add(getChangeKey(1));
+
+ configureMocks(extracted, new ArrayList<>());
+
+ DependsOnFetcher fetcher = createFetcher();
+ Pair<List<ChangeInfo>, List<String>> dependsOn = fetcher.fetchForRevision(rsrc);
+
+ assertThat(dependsOn.getLeft()).isEmpty();
+ assertThat(dependsOn.getRight()).containsExactly(getChangeKey(1));
+ }
+
+ @Test
+ public void testExtractMultipleMissingDependencies() throws Exception {
+ List<String> extracted = new ArrayList<>();
+ extracted.add(getChangeKey(1));
+ extracted.add(getChangeKey(2));
+ extracted.add(getChangeKey(3));
+ configureMocks(extracted, new ArrayList<>());
+
+ DependsOnFetcher fetcher = createFetcher();
+ Pair<List<ChangeInfo>, List<String>> dependsOn = fetcher.fetchForRevision(rsrc);
+
+ assertThat(dependsOn.getLeft()).isEmpty();
+ assertThat(dependsOn.getRight())
+ .containsExactly(getChangeKey(1), getChangeKey(2), getChangeKey(3));
+ }
+
+ @Test
+ public void testExtractMultipleDependenciesMultipleResultsForChangeId() throws Exception {
+ List<String> extracted = new ArrayList<>();
+ extracted.add(getChangeKey(1));
+ extracted.add(getChangeKey(2));
+ extracted.add(getChangeKey(3));
+
+ List<ChangeInfo> searchResult = new ArrayList<>();
+ searchResult.add(getChangeInfo(1));
+ searchResult.add(getChangeInfo(2));
+ searchResult.add(getChangeInfo(3));
+
+ ChangeInfo changeInfo = getChangeInfo(102);
+ changeInfo.changeId = getChangeInfo(2).changeId;
+ searchResult.add(changeInfo);
+
+ configureMocks(extracted, searchResult);
+
+ DependsOnFetcher fetcher = createFetcher();
+ Pair<List<ChangeInfo>, List<String>> dependsOn = fetcher.fetchForRevision(rsrc);
+
+ assertThat(dependsOn.getLeft())
+ .containsExactly(getChangeInfo(1), getChangeInfo(2), getChangeInfo(3), getChangeInfo(102));
+ assertThat(dependsOn.getRight()).isEmpty();
+ }
+
+ @Test
+ public void testExtractMixed() throws Exception {
+ List<String> extracted = new ArrayList<>();
+ extracted.add(getChangeKey(1));
+ extracted.add(getChangeKey(2));
+ extracted.add(getChangeKey(3));
+ extracted.add(getChangeKey(4));
+
+ List<ChangeInfo> searchResult = new ArrayList<>();
+ searchResult.add(getChangeInfo(2));
+ searchResult.add(getChangeInfo(3));
+
+ ChangeInfo changeInfo = getChangeInfo(102);
+ changeInfo.changeId = getChangeInfo(2).changeId;
+ searchResult.add(changeInfo);
+
+ configureMocks(extracted, searchResult);
+
+ DependsOnFetcher fetcher = createFetcher();
+ Pair<List<ChangeInfo>, List<String>> dependsOn = fetcher.fetchForRevision(rsrc);
+
+ assertThat(dependsOn.getLeft())
+ .containsExactly(getChangeInfo(2), getChangeInfo(102), getChangeInfo(3));
+ assertThat(dependsOn.getRight()).containsExactly(getChangeKey(1), getChangeKey(4));
+ }
+
+ private void configureMocks(
+ List<String> extractedDependsOn, List<ChangeInfo> searchResultDependsOn)
+ throws RepositoryNotFoundException, IOException, BadRequestException, AuthException,
+ PermissionBackendException {
+ String commitId = "0123456789012345678901234567890123456789";
+
+ Project.NameKey projectNameKey = Project.nameKey("projectFoo");
+
+ PatchSet patchSet = mock(PatchSet.class);
+ when(patchSet.commitId()).thenReturn(ObjectId.fromString(commitId));
+
+ BranchNameKey branchNameKey = BranchNameKey.create(projectNameKey, "branchBar");
+
+ Change.Key changeKey = Change.key("I0123456789");
+ Change change = new Change(changeKey, null, null, branchNameKey, null);
+
+ rsrc = mock(RevisionResource.class);
+ when(rsrc.getChange()).thenReturn(change);
+ when(rsrc.getPatchSet()).thenReturn(patchSet);
+
+ commitMessageFetcher = mock(CommitMessageFetcher.class);
+ when(commitMessageFetcher.fetch(projectNameKey, commitId)).thenReturn("commitMsgFoo");
+
+ dependsOnExtractor = mock(DependsOnExtractor.class);
+ when(dependsOnExtractor.extract("commitMsgFoo")).thenReturn(extractedDependsOn);
+
+ QueryChanges queryChanges = mock(QueryChanges.class);
+ final AtomicBoolean addedQuery = new AtomicBoolean(false);
+
+ if (!extractedDependsOn.isEmpty()) {
+ doAnswer(
+ new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ if (addedQuery.getAndSet(true)) {
+ fail("flag has already been set");
+ }
+ return null;
+ }
+ })
+ .when(queryChanges)
+ .addQuery("change:" + String.join(" OR change:", extractedDependsOn));
+
+ when(queryChanges.apply(TopLevelResource.INSTANCE))
+ .thenAnswer(
+ new Answer<Response<List<ChangeInfo>>>() {
+
+ @Override
+ public Response<List<ChangeInfo>> answer(InvocationOnMock invocation)
+ throws Throwable {
+ if (!addedQuery.get()) {
+ fail("executed query before all options were set");
+ }
+ return Response.ok(searchResultDependsOn);
+ }
+ });
+ }
+
+ changes = mock(ChangesCollection.class);
+ when(changes.list()).thenReturn(queryChanges);
+ }
+
+ private String getChangeKey(int keyEnding) {
+ return "I0123456789abcdef0000000000000000000" + (10000 + keyEnding);
+ }
+
+ private ChangeInfo getChangeInfo(int keyEnding) {
+ return changeInfos.computeIfAbsent(
+ keyEnding,
+ neededKeyEnding -> {
+ ChangeInfo changeInfo = new ChangeInfo();
+ changeInfo.changeId = getChangeKey(neededKeyEnding);
+ changeInfo._number = neededKeyEnding;
+ return changeInfo;
+ });
+ }
+
+ private DependsOnFetcher createFetcher() {
+ return new DependsOnFetcher(changes, commitMessageFetcher, dependsOnExtractor);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/zuul/util/NeededByFetcherTest.java b/src/test/java/com/googlesource/gerrit/plugins/zuul/util/NeededByFetcherTest.java
new file mode 100644
index 0000000..69c6977
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/zuul/util/NeededByFetcherTest.java
@@ -0,0 +1,232 @@
+// Copyright (C) 2020 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.googlesource.gerrit.plugins.zuul.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.restapi.change.ChangesCollection;
+import com.google.gerrit.server.restapi.change.QueryChanges;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+public class NeededByFetcherTest {
+ private ChangesCollection changes;
+ private CommitMessageFetcher commitMessageFetcher;
+ private DependsOnExtractor dependsOnExtractor;
+ private Change.Key changeKey = getChangeKey(1);
+ private Map<Integer, ChangeInfo> changeInfos = new HashMap<>();
+
+ @Test
+ public void testFetchForChangeKeyNoResults() throws Exception {
+ List<ChangeInfo> searchResult = new ArrayList<>();
+
+ configureMocks(searchResult);
+
+ NeededByFetcher fetcher = createFetcher();
+
+ List<ChangeInfo> neededBy = fetcher.fetchForChangeKey(changeKey);
+
+ assertThat(neededBy).isEmpty();
+ }
+
+ @Test
+ public void testFetchForChangeKeySingleResult() throws Exception {
+ List<ChangeInfo> searchResult = new ArrayList<>();
+ searchResult.add(getChangeInfo(2));
+
+ configureMocks(searchResult);
+
+ NeededByFetcher fetcher = createFetcher();
+
+ List<ChangeInfo> neededBy = fetcher.fetchForChangeKey(changeKey);
+
+ assertThat(neededBy).containsExactly(getChangeInfo(2));
+ }
+
+ @Test
+ public void testFetchForChangeKeySingleResultUnmatchedEmpty() throws Exception {
+ List<ChangeInfo> searchResult = new ArrayList<>();
+ searchResult.add(getChangeInfo(2));
+
+ configureMocks(searchResult);
+ when(dependsOnExtractor.extract("commitMessage2")).thenReturn(new ArrayList<>());
+
+ NeededByFetcher fetcher = createFetcher();
+
+ List<ChangeInfo> neededBy = fetcher.fetchForChangeKey(changeKey);
+
+ assertThat(neededBy).isEmpty();
+ }
+
+ @Test
+ public void testFetchForChangeKeySingleResultUnmatchedDifferent() throws Exception {
+ List<ChangeInfo> searchResult = new ArrayList<>();
+ searchResult.add(getChangeInfo(2));
+
+ configureMocks(searchResult);
+ List<String> extracted = new ArrayList<>();
+ extracted.add(getChangeKey(3).toString());
+ when(dependsOnExtractor.extract("commitMessage2")).thenReturn(extracted);
+
+ NeededByFetcher fetcher = createFetcher();
+
+ List<ChangeInfo> neededBy = fetcher.fetchForChangeKey(changeKey);
+
+ assertThat(neededBy).isEmpty();
+ }
+
+ @Test
+ public void testFetchForChangeKeyMultipleResults() throws Exception {
+ List<ChangeInfo> searchResult = new ArrayList<>();
+ searchResult.add(getChangeInfo(2));
+ searchResult.add(getChangeInfo(3));
+
+ configureMocks(searchResult);
+
+ NeededByFetcher fetcher = createFetcher();
+
+ List<ChangeInfo> neededBy = fetcher.fetchForChangeKey(changeKey);
+
+ assertThat(neededBy).containsExactly(getChangeInfo(2), getChangeInfo(3));
+ }
+
+ @Test
+ public void testFetchForChangeKeyMultipleResultsSomeUnmatched() throws Exception {
+ List<ChangeInfo> searchResult = new ArrayList<>();
+ searchResult.add(getChangeInfo(2));
+ searchResult.add(getChangeInfo(3));
+ searchResult.add(getChangeInfo(4));
+ searchResult.add(getChangeInfo(5));
+
+ configureMocks(searchResult);
+ when(dependsOnExtractor.extract("commitMessage3")).thenReturn(new ArrayList<>());
+ when(dependsOnExtractor.extract("commitMessage4")).thenReturn(new ArrayList<>());
+
+ NeededByFetcher fetcher = createFetcher();
+
+ List<ChangeInfo> neededBy = fetcher.fetchForChangeKey(changeKey);
+
+ assertThat(neededBy).containsExactly(getChangeInfo(2), getChangeInfo(5));
+ }
+
+ /**
+ * Sets up mocks for a given search result.
+ *
+ * <p>Each search result is configured to have a `Depends-On` to the changeKey per default. To
+ * refine, override the extraction for ("commitMessage" + number) to the desired list of
+ * Change-Ids.
+ *
+ * @param searchResult The search result to configure.
+ * @throws Exception thrown upon issues.
+ */
+ public void configureMocks(final List<ChangeInfo> searchResult) throws Exception {
+ QueryChanges queryChanges = mock(QueryChanges.class);
+ final AtomicBoolean addedQuery = new AtomicBoolean(false);
+ final AtomicBoolean addedOptionCurrentRevision = new AtomicBoolean(false);
+ final AtomicBoolean addedOptionCurrentCommit = new AtomicBoolean(false);
+
+ mockQueryChangesWithSwitch(queryChanges, addedOptionCurrentCommit)
+ .addOption(ListChangesOption.CURRENT_COMMIT);
+ mockQueryChangesWithSwitch(queryChanges, addedOptionCurrentRevision)
+ .addOption(ListChangesOption.CURRENT_REVISION);
+ mockQueryChangesWithSwitch(queryChanges, addedQuery)
+ .addQuery("message:" + changeKey + " -change:" + changeKey);
+ when(queryChanges.apply(TopLevelResource.INSTANCE))
+ .thenAnswer(
+ new Answer<Response<List<ChangeInfo>>>() {
+
+ @Override
+ public Response<List<ChangeInfo>> answer(InvocationOnMock invocation)
+ throws Throwable {
+ boolean ready =
+ addedOptionCurrentRevision.get()
+ && addedOptionCurrentCommit.get()
+ && addedQuery.get();
+ if (!ready) {
+ fail("executed query before all options were set");
+ }
+ return Response.ok(searchResult);
+ }
+ });
+
+ changes = mock(ChangesCollection.class);
+ when(changes.list()).thenReturn(queryChanges);
+
+ commitMessageFetcher = mock(CommitMessageFetcher.class);
+ when(commitMessageFetcher.fetch(any(ChangeInfo.class)))
+ .thenAnswer(
+ new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) throws Throwable {
+ ChangeInfo changeInfo = invocation.getArgument(0);
+ return "commitMessage" + changeInfo._number;
+ }
+ });
+
+ dependsOnExtractor = mock(DependsOnExtractor.class);
+ List<String> extractedMatchList = new ArrayList<>();
+ extractedMatchList.add(changeKey.toString());
+ when(dependsOnExtractor.extract(any(String.class))).thenReturn(extractedMatchList);
+ }
+
+ private QueryChanges mockQueryChangesWithSwitch(
+ QueryChanges queryChanges, AtomicBoolean booleanSwitch) {
+ return doAnswer(
+ new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ if (booleanSwitch.getAndSet(true)) {
+ fail("flag has already been set");
+ }
+ return null;
+ }
+ })
+ .when(queryChanges);
+ }
+
+ private Change.Key getChangeKey(int keyEnding) {
+ return Change.key("I0123456789abcdef0000000000000000000" + (10000 + keyEnding));
+ }
+
+ private ChangeInfo getChangeInfo(int keyEnding) {
+ return changeInfos.computeIfAbsent(
+ keyEnding,
+ neededKeyEnding -> {
+ ChangeInfo changeInfo = new ChangeInfo();
+ changeInfo.changeId = getChangeKey(neededKeyEnding).toString();
+ changeInfo._number = neededKeyEnding;
+ return changeInfo;
+ });
+ }
+
+ private NeededByFetcher createFetcher() {
+ return new NeededByFetcher(changes, commitMessageFetcher, dependsOnExtractor);
+ }
+}