Merge "Show summary for flows similar to checks"
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 2ef499f..2ed32e7 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -19,6 +19,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
@@ -27,12 +28,14 @@
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.config.Server;
+import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ListOption;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.httpd.raw.IndexPreloadingUtil.RequestedPage;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gson.Gson;
import com.google.template.soy.data.SanitizedContent;
import java.net.URI;
@@ -41,6 +44,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@@ -68,10 +72,13 @@
String requestedURL)
throws URISyntaxException, RestApiException {
ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
+ boolean asyncSubmitRequirements =
+ experimentFeatures.isFeatureEnabled(ExperimentFeaturesConstants.ASYNC_SUBMIT_REQUIREMENTS);
data.putAll(
staticTemplateData(
canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
- .putAll(dynamicTemplateData(gerritApi, requestedURL, canonicalURL));
+ .putAll(
+ dynamicTemplateData(gerritApi, requestedURL, canonicalURL, asyncSubmitRequirements));
Set<String> enabledExperiments = new HashSet<>();
enabledExperiments.addAll(experimentFeatures.getEnabledExperimentFeatures());
// Add all experiments enabled through url
@@ -107,7 +114,10 @@
/** Returns dynamic parameters of {@code index.html}. */
public static ImmutableMap<String, Object> dynamicTemplateData(
- GerritApi gerritApi, String requestedURL, String canonicalURL)
+ GerritApi gerritApi,
+ String requestedURL,
+ String canonicalURL,
+ boolean asyncSubmitRequirements)
throws RestApiException, URISyntaxException {
ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
Map<String, SanitizedContent> initialData = new HashMap<>();
@@ -127,15 +137,21 @@
Integer basePatchNum = computeBasePatchNum(requestedPath);
switch (page) {
case CHANGE, DIFF -> {
- if (basePatchNum.equals(0)) {
+ LinkedHashSet<ListChangesOption> changeDetailOptions =
+ new LinkedHashSet<>(
+ basePatchNum.equals(0)
+ ? IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITHOUT_PARENTS
+ : IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITH_PARENTS);
+ if (asyncSubmitRequirements) {
+ changeDetailOptions.remove(ListChangesOption.SUBMIT_REQUIREMENTS);
+ changeDetailOptions.remove(ListChangesOption.SUBMITTABLE);
data.put(
- "defaultChangeDetailHex",
- ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITHOUT_PARENTS));
- } else {
- data.put(
- "defaultChangeDetailHex",
- ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS_WITH_PARENTS));
+ "submitRequirementsHex",
+ ListOption.toHex(
+ ImmutableSet.of(
+ ListChangesOption.SUBMIT_REQUIREMENTS, ListChangesOption.SUBMITTABLE)));
}
+ data.put("defaultChangeDetailHex", ListOption.toHex(changeDetailOptions));
data.put(
"changeRequestsPath",
IndexPreloadingUtil.computeChangeRequestsPath(requestedPath, page).get());
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index cd91745..ce96132 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -60,4 +60,7 @@
/** Whether we allow fix suggestions in HumanComments. */
public static final String ALLOW_FIX_SUGGESTIONS_IN_COMMENTS =
"GerritBackendFeature__allow_fix_suggestions_in_comments";
+
+ /** Whether UI should request Submit Requirements separately from change detail. */
+ public static final String ASYNC_SUBMIT_REQUIREMENTS = "UiFeature__async_submit_requirements";
}
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index ff729b7..291d5a9 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -321,7 +321,16 @@
}
if (footersToAdd.length() > 0) {
- message = message.trim() + "\n\n" + footersToAdd.toString().trim();
+ String trimmedMsg = message.trim();
+ List<String> lines = Splitter.on('\n').splitToList(trimmedMsg);
+ String lastLine = lines.isEmpty() ? "" : lines.get(lines.size() - 1);
+ boolean endsWithFooter = lastLine.matches("^[a-zA-Z0-9-]+:.*");
+
+ if (endsWithFooter) {
+ message = trimmedMsg + "\n" + footersToAdd.toString().trim();
+ } else {
+ message = trimmedMsg + "\n\n" + footersToAdd.toString().trim();
+ }
}
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index a8f3db4..b3b56db 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -1757,10 +1757,54 @@
// Check that the footers are not duplicated
String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
- int bugOccurrences = commitMessage.split("Bug: 12345", -1).length - 1;
- assertThat(bugOccurrences).isEqualTo(1);
- int issueOccurrences = commitMessage.split("Issue: 67890", -1).length - 1;
- assertThat(issueOccurrences).isEqualTo(1);
+ String expectedMessage =
+ String.format(
+ """
+ Reverting this change
+
+ Bug: 12345
+ Issue: 67890
+ Change-Id: %s
+ """,
+ revertChange.changeId);
+ assertThat(commitMessage).isEqualTo(expectedMessage);
+ }
+
+ @Test
+ public void revertChangeWithSkipPresubmitFooter() throws Exception {
+ // Create a change with bug and issue footers
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Change with bug and issue\n" + "\n" + "Bug: 12345\n" + "Issue: 67890",
+ "a.txt",
+ "content");
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+ // Revert the change
+ RevertInput revertInput = new RevertInput();
+ revertInput.message = "Reverting this change\n\nSkip-Presubmit: true";
+ ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert(revertInput).get();
+
+ // Check that the revert commit message contains all footers and no extra newline
+ String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
+ String expectedMessage =
+ String.format(
+ """
+ Reverting this change
+
+ Skip-Presubmit: true
+ Bug: 12345
+ Issue: 67890
+ Change-Id: %s
+ """,
+ revertChange.changeId);
+ assertThat(commitMessage).isEqualTo(expectedMessage);
}
@Test
@@ -1780,8 +1824,6 @@
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
// Revert the submission with a message that already contains the footers
- RevertInput revertInput = new RevertInput();
- revertInput.message = "Reverting this change\n\nBug: 12345\nIssue: 67890";
List<ChangeInfo> revertChanges =
gApi.changes().id(r.getChangeId()).revertSubmission().revertChanges;
assertThat(revertChanges).hasSize(1);
@@ -1789,10 +1831,19 @@
// Check that the footers are not duplicated
String commitMessage = gApi.changes().id(revertChange.id).current().commit(false).message;
- int bugOccurrences = commitMessage.split("Bug: 12345", -1).length - 1;
- assertThat(bugOccurrences).isEqualTo(1);
- int issueOccurrences = commitMessage.split("Issue: 67890", -1).length - 1;
- assertThat(issueOccurrences).isEqualTo(1);
+ String expectedMessage =
+ String.format(
+ """
+ Revert "Change with bug and issue"
+
+ This reverts commit %s.
+
+ Bug: 12345
+ Issue: 67890
+ Change-Id: %s
+ """,
+ r.getCommit().name(), revertChange.changeId);
+ assertThat(commitMessage).isEqualTo(expectedMessage);
}
@Override
diff --git a/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
index fe3cb00..8dcd0f3 100644
--- a/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/config/ListExperimentsIT.java
@@ -52,7 +52,8 @@
.GERRIT_BACKEND_FEATURE_ALWAYS_REJECT_IMPLICIT_MERGES_ON_MERGE,
ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION,
ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_CHECK_IMPLICIT_MERGES_ON_MERGE,
- ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE)
+ ExperimentFeaturesConstants.GERRIT_BACKEND_FEATURE_REJECT_IMPLICIT_MERGES_ON_MERGE,
+ ExperimentFeaturesConstants.ASYNC_SUBMIT_REQUIREMENTS)
.inOrder();
// "GerritBackendFeature__check_implicit_merges_on_merge",
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index 2f6d565..68be60b 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -117,7 +117,7 @@
String requestedPath = "/c/project/+/123/4..6";
assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(4);
- assertThat(dynamicTemplateData(gerritApi, requestedPath, ""))
+ assertThat(dynamicTemplateData(gerritApi, requestedPath, "", false))
.containsAtLeast(
"defaultChangeDetailHex", "9996394",
"changeRequestsPath", "changes/project~123");
@@ -145,12 +145,41 @@
String requestedPath = "/c/project/+/123";
assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(0);
- assertThat(dynamicTemplateData(gerritApi, requestedPath, ""))
+ assertThat(dynamicTemplateData(gerritApi, requestedPath, "", false))
.containsAtLeast(
"defaultChangeDetailHex", "1996394",
"changeRequestsPath", "changes/project~123");
}
+ @Test
+ public void usePreloadRestWithAsyncSRs() throws Exception {
+ Accounts accountsApi = mock(Accounts.class);
+ when(accountsApi.self()).thenThrow(new AuthException("user needs to be authenticated"));
+
+ Server serverApi = mock(Server.class);
+ when(serverApi.getVersion()).thenReturn("123");
+ when(serverApi.topMenus()).thenReturn(ImmutableList.of());
+ ServerInfo serverInfo = new ServerInfo();
+ serverInfo.defaultTheme = "my-default-theme";
+ when(serverApi.getInfo()).thenReturn(serverInfo);
+
+ Config configApi = mock(Config.class);
+ when(configApi.server()).thenReturn(serverApi);
+
+ GerritApi gerritApi = mock(GerritApi.class);
+ when(gerritApi.accounts()).thenReturn(accountsApi);
+ when(gerritApi.config()).thenReturn(configApi);
+
+ String requestedPath = "/c/project/+/123";
+ assertThat(IndexHtmlUtil.computeBasePatchNum(requestedPath)).isEqualTo(0);
+
+ assertThat(dynamicTemplateData(gerritApi, requestedPath, "", true))
+ .containsAtLeast(
+ "defaultChangeDetailHex", "896394",
+ "submitRequirementsHex", "1100000",
+ "changeRequestsPath", "changes/project~123");
+ }
+
private static SanitizedContent ordain(String s) {
return UnsafeSanitizedContentOrdainer.ordainAsSafe(
s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_screenshot_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_screenshot_test.ts
index f45f5ae..87b50a2 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_screenshot_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_screenshot_test.ts
@@ -60,10 +60,11 @@
await element.updateComplete;
});
- test('normal view', async () => {
- await visualDiff(element, 'gr-change-metadata');
- await visualDiffDarkTheme(element, 'gr-change-metadata-dark');
- });
+ // TODO(b/447590232): Fix test flakiness and re-enable
+ // test('normal view', async () => {
+ // await visualDiff(element, 'gr-change-metadata');
+ // await visualDiffDarkTheme(element, 'gr-change-metadata-dark');
+ // });
test('show all sections with more data', async () => {
const changeModel = testResolver(changeModelToken);
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 8a58551..3963888 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -47,6 +47,7 @@
countRunningRunsForLabel,
} from '../../checks/gr-checks-util';
import {subscribe} from '../../lit/subscription-controller';
+import {when} from 'lit/directives/when.js';
/**
* @attr {Boolean} suppress-title - hide titles, currently for hovercard view
@@ -153,7 +154,6 @@
const submit_requirements = orderSubmitRequirements(
getRequirements(this.change)
);
- if (submit_requirements.length === 0) return nothing;
const requirementKey = (req: SubmitRequirementResultInfo, index: number) =>
`${index}-${req.name}`;
@@ -162,34 +162,44 @@
id="submit-requirements-caption"
>
Submit Requirements
+ ${submit_requirements.length === 0 ? '(Loading...)' : ''}
</h3>
- <table class="requirements" aria-labelledby="submit-requirements-caption">
- <thead hidden>
- <tr>
- <th>Status</th>
- <th>Name</th>
- <th>Votes</th>
- </tr>
- </thead>
- <tbody>
- ${repeat(submit_requirements, requirementKey, (requirement, index) =>
- this.renderRequirement(requirement, index)
- )}
- </tbody>
- </table>
- ${this.disableHovercards
- ? ''
- : submit_requirements.map(
- (requirement, index) => html`
- <gr-submit-requirement-hovercard
- for="requirement-${index}-${charsOnly(requirement.name)}"
- .requirement=${requirement}
- .change=${this.change}
- .account=${this.account}
- .mutable=${this.mutable ?? false}
- ></gr-submit-requirement-hovercard>
- `
- )}`;
+ ${when(
+ submit_requirements.length !== 0,
+ () => html`<table
+ class="requirements"
+ aria-labelledby="submit-requirements-caption"
+ >
+ <thead hidden>
+ <tr>
+ <th>Status</th>
+ <th>Name</th>
+ <th>Votes</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${repeat(
+ submit_requirements,
+ requirementKey,
+ (requirement, index) =>
+ this.renderRequirement(requirement, index)
+ )}
+ </tbody>
+ </table>
+ ${this.disableHovercards
+ ? ''
+ : submit_requirements.map(
+ (requirement, index) => html`
+ <gr-submit-requirement-hovercard
+ for="requirement-${index}-${charsOnly(requirement.name)}"
+ .requirement=${requirement}
+ .change=${this.change}
+ .account=${this.account}
+ .mutable=${this.mutable ?? false}
+ ></gr-submit-requirement-hovercard>
+ `
+ )}`
+ )}`;
}
private renderRequirement(
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
index 6812251..aee2017 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
@@ -23,24 +23,33 @@
SubmitRequirementResultInfo,
SubmitRequirementStatus,
} from '../../../api/rest-api';
-import {ParsedChangeInfo} from '../../../types/types';
+import {LoadingStatus, ParsedChangeInfo} from '../../../types/types';
import {RunStatus} from '../../../api/checks';
+import {testResolver} from '../../../test/common-test-setup';
+import {
+ ChangeModel,
+ changeModelToken,
+} from '../../../models/change/change-model';
suite('gr-submit-requirements tests', () => {
let element: GrSubmitRequirements;
let change: ParsedChangeInfo;
+ let changeModel: ChangeModel;
+
setup(async () => {
const submitRequirement: SubmitRequirementResultInfo = {
...createSubmitRequirementResultInfo(),
description: 'Test Description',
submittability_expression_result: createSubmitRequirementExpressionInfo(),
};
+ const submitRequirements: SubmitRequirementResultInfo[] = [
+ submitRequirement,
+ createNonApplicableSubmitRequirementResultInfo(),
+ ];
change = {
...createParsedChange(),
- submit_requirements: [
- submitRequirement,
- createNonApplicableSubmitRequirementResultInfo(),
- ],
+ submittable: false,
+ submit_requirements: submitRequirements,
labels: {
Verified: {
...createDetailedLabelInfo(),
@@ -54,12 +63,23 @@
},
};
const account = createAccountWithIdNameAndEmail();
+ changeModel = testResolver(changeModelToken);
+ changeModel.setState({
+ change,
+ submittabilityInfo: {
+ changeNum: change._number,
+ submitRequirements,
+ submittable: false,
+ },
+ loadingStatus: LoadingStatus.LOADED,
+ });
element = await fixture<GrSubmitRequirements>(
html`<gr-submit-requirements
.change=${change}
.account=${account}
></gr-submit-requirements>`
);
+ await element.updateComplete;
});
test('renders', () => {
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index df09209..5577075 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -43,7 +43,10 @@
ParsedChangeInfo,
} from '../../types/types';
import {fire, fireAlert, fireTitleChange} from '../../utils/event-util';
-import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {
+ RestApiService,
+ SubmittabilityInfo,
+} from '../../services/gr-rest-api/gr-rest-api';
import {select} from '../../utils/observable-util';
import {assertIsDefined} from '../../utils/common-util';
import {Model} from '../base/model';
@@ -86,6 +89,12 @@
loadingStatus: LoadingStatus;
change?: ParsedChangeInfo;
/**
+ * Information about submittablity and evaluation of SRs
+ *
+ * Corresponding values in `change` are always kept in sync.
+ */
+ submittabilityInfo?: SubmittabilityInfo;
+ /**
* The list of reviewed files, kept in the model because we want changes made
* in one view to reflect on other views without re-rendering the other views.
* Undefined means it's still loading and empty set means no files reviewed.
@@ -254,6 +263,32 @@
}
/**
+ * Returns new change object with the fields with submittability related fields
+ * updated.
+ *
+ * - if change is undefined return undefined.
+ * - if change number is different than the one in submittability info, no
+ * updates made
+ */
+export function fillFromSubmittabilityInfo(
+ change?: ParsedChangeInfo,
+ submittabilityInfo?: SubmittabilityInfo
+): ParsedChangeInfo | undefined {
+ if (
+ !change ||
+ !submittabilityInfo ||
+ submittabilityInfo.changeNum !== change._number
+ ) {
+ return change;
+ }
+ return {
+ ...change,
+ submittable: submittabilityInfo.submittable,
+ submit_requirements: submittabilityInfo.submitRequirements,
+ };
+}
+
+/**
* Derives the base patchset number from all the data that can potentially
* influence it. Mostly just returns `viewModelBasePatchNum` or PARENT, but has
* some special logic when looking at merge commits.
@@ -308,6 +343,8 @@
export class ChangeModel extends Model<ChangeState> {
private change?: ParsedChangeInfo;
+ private submittabilityInfo?: SubmittabilityInfo;
+
private patchNum?: RevisionPatchSetNum;
private basePatchNum?: BasePatchSetNum;
@@ -319,6 +356,21 @@
changeState => changeState.change
);
+ public readonly submittabilityInfo$ = select(
+ this.state$,
+ changeState => changeState.submittabilityInfo
+ );
+
+ public readonly submittable$ = select(
+ this.state$,
+ changeState => changeState.submittabilityInfo?.submittable
+ );
+
+ public readonly submitRequirements$ = select(
+ this.state$,
+ changeState => changeState.submittabilityInfo?.submitRequirements
+ );
+
public readonly changeLoadingStatus$ = select(
this.state$,
changeState => changeState.loadingStatus
@@ -551,6 +603,7 @@
);
this.subscriptions = [
this.loadChange(),
+ this.loadSubmittabilityInfo(),
this.loadMergeable(),
this.loadReviewedFiles(),
this.setOverviewTitle(),
@@ -561,6 +614,9 @@
this.refuseEditForOpenChange(),
this.refuseEditForClosedChange(),
this.change$.subscribe(change => (this.change = change)),
+ this.submittabilityInfo$.subscribe(
+ submittabilityInfo => (this.submittabilityInfo = submittabilityInfo)
+ ),
this.patchNum$.subscribe(patchNum => (this.patchNum = patchNum)),
this.basePatchNum$.subscribe(
basePatchNum => (this.basePatchNum = basePatchNum)
@@ -742,6 +798,36 @@
.subscribe(mergeable => this.updateState({mergeable}));
}
+ private loadSubmittabilityInfo() {
+ // Use the same trigger as loadChange, to run SR loading in parallel.
+ return this.viewModel.changeNum$
+ .pipe(
+ switchMap(changeNum => {
+ if (!changeNum) {
+ // On reload changeNum is set to undefined to reset change state.
+ // We propagate undefined and reset the state in this case.
+ return of(undefined);
+ }
+ return from(this.restApiService.getSubmittabilityInfo(changeNum));
+ })
+ )
+ .subscribe(submittabilityInfo => {
+ // TODO(b/445644919): Remove once the submit_requirements is never
+ // requested as part of the change detail.
+ if (this.change?.submit_requirements) {
+ return;
+ }
+ const change = fillFromSubmittabilityInfo(
+ this.change,
+ submittabilityInfo
+ );
+ this.updateState({
+ change,
+ submittabilityInfo,
+ });
+ });
+ }
+
private loadChange() {
return this.viewModel.changeNum$
.pipe(
@@ -1006,11 +1092,32 @@
if (this.change && change?._number !== this.change?._number) {
return;
}
+ if (!change) {
+ this.updateState({
+ change: undefined,
+ loadingStatus: LoadingStatus.NOT_LOADED,
+ });
+ return;
+ }
change = updateRevisionsWithCommitShas(change);
+ // TODO(b/445644919): Remove once the submit_requirements is never requested
+ // as part of the change detail.
+ if (change!.submit_requirements) {
+ this.updateState({
+ change,
+ submittabilityInfo: {
+ changeNum: change!._number,
+ submittable: change!.submittable!,
+ submitRequirements: change!.submit_requirements,
+ },
+ loadingStatus: LoadingStatus.LOADED,
+ });
+ return;
+ }
+ change = fillFromSubmittabilityInfo(change, this.submittabilityInfo);
this.updateState({
change,
- loadingStatus:
- change === undefined ? LoadingStatus.NOT_LOADED : LoadingStatus.LOADED,
+ loadingStatus: LoadingStatus.LOADED,
});
}
}
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index 2fc8f8a..a58a070 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -16,6 +16,7 @@
createMergeable,
createParsedChange,
createRevision,
+ createSubmitRequirementResultInfo,
TEST_NUMERIC_CHANGE_ID,
} from '../../test/test-data-generators';
import {
@@ -60,6 +61,7 @@
import {SinonStub} from 'sinon';
import {pluginLoaderToken} from '../../elements/shared/gr-js-api-interface/gr-plugin-loader';
import {ShowChangeDetail} from '../../elements/shared/gr-js-api-interface/gr-js-api-types';
+import {SubmittabilityInfo} from '../../services/gr-rest-api/gr-rest-api';
suite('updateRevisionsWithCommitShas() tests', () => {
test('undefined edit', async () => {
@@ -487,6 +489,97 @@
assert.deepEqual(state?.change, updateRevisionsWithCommitShas(knownChange));
});
+ test('load submit requirements (SRs load first)', async () => {
+ const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
+ const stubDetail = stubRestApi('getChangeDetail').callsFake(
+ () => promiseDetail
+ );
+ const promiseSrs = mockPromise<SubmittabilityInfo | undefined>();
+ const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
+ () => promiseSrs
+ );
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+ promiseSrs.resolve({
+ changeNum: knownChange._number,
+ submittable: false,
+ submitRequirements: [createSubmitRequirementResultInfo()],
+ });
+ await waitUntilObserved(
+ changeModel.state$,
+ state => state.submittabilityInfo !== undefined,
+ 'SubmitRequirements was never loaded'
+ );
+ promiseDetail.resolve(knownChange);
+ const state = await waitForLoadingStatus(LoadingStatus.LOADED);
+ assert.isTrue(state.submittabilityInfo?.submittable === false);
+ assert.isTrue(state.submittabilityInfo?.submitRequirements.length === 1);
+ assert.isTrue(state.change?.submittable === false);
+ assert.isTrue(state.change?.submit_requirements?.length === 1);
+ assert.equal(stubDetail.callCount, 1);
+ assert.equal(stubSrs.callCount, 1);
+ });
+
+ test('load submit requirements (Detail load first, experiment enabled)', async () => {
+ const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
+ const stubDetail = stubRestApi('getChangeDetail').callsFake(
+ () => promiseDetail
+ );
+ const promiseSrs = mockPromise<SubmittabilityInfo | undefined>();
+ const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
+ () => promiseSrs
+ );
+ let state: ChangeState;
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+ promiseDetail.resolve(knownChange);
+ state = await waitForLoadingStatus(LoadingStatus.LOADED);
+ promiseSrs.resolve({
+ changeNum: knownChange._number,
+ submittable: false,
+ submitRequirements: [createSubmitRequirementResultInfo()],
+ });
+ state = await waitUntilObserved(
+ changeModel.state$,
+ state => state.submittabilityInfo !== undefined,
+ 'SubmitRequirements was never loaded'
+ );
+ assert.isTrue(state.submittabilityInfo?.submittable === false);
+ assert.isTrue(state.submittabilityInfo?.submitRequirements.length === 1);
+ assert.isTrue(state.change?.submittable === false);
+ assert.isTrue(state.change?.submit_requirements?.length === 1);
+ assert.equal(stubDetail.callCount, 1);
+ assert.equal(stubSrs.callCount, 1);
+ });
+
+ test('load submit requirements (Detail load first, experiment disabled)', async () => {
+ const promiseDetail = mockPromise<ParsedChangeInfo | undefined>();
+ const stubDetail = stubRestApi('getChangeDetail').callsFake(
+ () => promiseDetail
+ );
+ const promiseSrs = mockPromise<SubmittabilityInfo | undefined>();
+ const stubSrs = stubRestApi('getSubmittabilityInfo').callsFake(
+ () => promiseSrs
+ );
+ let state: ChangeState;
+ testResolver(changeViewModelToken).setState(createChangeViewState());
+ promiseDetail.resolve({
+ ...knownChange,
+ submittable: false,
+ submit_requirements: [createSubmitRequirementResultInfo()],
+ });
+ state = await waitForLoadingStatus(LoadingStatus.LOADED);
+ promiseSrs.resolve(undefined);
+ state = await waitUntilObserved(
+ changeModel.state$,
+ state => state.submittabilityInfo !== undefined,
+ 'SubmitRequirements was never loaded'
+ );
+ // Validate that submit requirements didn't get reset to undefined.
+ assert.isTrue(state.submittabilityInfo?.submittable === false);
+ assert.isTrue(state.submittabilityInfo?.submitRequirements.length === 1);
+ assert.equal(stubDetail.callCount, 1);
+ assert.equal(stubSrs.callCount, 1);
+ });
+
test('navigating to another change', async () => {
// setting up a loaded change
let promise = mockPromise<ParsedChangeInfo | undefined>();
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 33fa6f2..2c7993c 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -24,4 +24,5 @@
ML_SUGGESTED_EDIT_FEEDBACK = 'UiFeature__ml_suggested_edit_feedback',
ML_SUGGESTED_EDIT_EDITABLE_SUGGESTION = 'UiFeature__ml_suggested_edit_editable_suggestion',
SHOW_FLOWS_TAB = 'UiFeature__show_flows_tab',
+ ASYNC_SUBMIT_REQUIREMENTS = 'UiFeature__async_submit_requirements',
}
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index deae598..ada11f9 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -110,7 +110,11 @@
DiffPreferencesInfo,
IgnoreWhitespaceType,
} from '../../types/diff';
-import {GetDiffCommentsOutput, RestApiService} from './gr-rest-api';
+import {
+ GetDiffCommentsOutput,
+ RestApiService,
+ SubmittabilityInfo,
+} from './gr-rest-api';
import {
CommentSide,
createDefaultDiffPrefs,
@@ -152,6 +156,7 @@
SiteBasedCache,
throwingErrorCallback,
} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {getAppContext} from '../app-context';
const MAX_PROJECT_RESULTS = 25;
@@ -265,6 +270,8 @@
// Used to serialize requests for certain RPCs
readonly _serialScheduler: Scheduler<Response>;
+ private readonly flags = getAppContext().flagsService;
+
constructor(
private readonly authService: AuthService,
private readonly flagService: FlagsService
@@ -1419,15 +1426,17 @@
ListChangesOption.DOWNLOAD_COMMANDS,
ListChangesOption.MESSAGES,
ListChangesOption.REVIEWER_UPDATES,
- ListChangesOption.SUBMITTABLE,
ListChangesOption.WEB_LINKS,
ListChangesOption.SKIP_DIFFSTAT,
- ListChangesOption.SUBMIT_REQUIREMENTS,
ListChangesOption.PARENTS,
];
if (config?.receive?.enable_signed_push) {
options.push(ListChangesOption.PUSH_CERTIFICATES);
}
+ if (!this.flags.isEnabled(KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS)) {
+ options.push(ListChangesOption.SUBMITTABLE);
+ options.push(ListChangesOption.SUBMIT_REQUIREMENTS);
+ }
return options;
}
@@ -1486,6 +1495,35 @@
);
}
+ async getSubmittabilityInfo(
+ changeNum: NumericChangeId
+ ): Promise<SubmittabilityInfo | undefined> {
+ if (!this.flags.isEnabled(KnownExperimentId.ASYNC_SUBMIT_REQUIREMENTS)) {
+ return undefined;
+ }
+ const optionsHex = listChangesOptionsToHex(
+ ListChangesOption.SUBMITTABLE,
+ ListChangesOption.SUBMIT_REQUIREMENTS
+ );
+ const change = await this.getChange(
+ changeNum,
+ /* errFn=*/ undefined,
+ optionsHex
+ );
+ if (
+ !change ||
+ change.submittable === undefined ||
+ change.submit_requirements === undefined
+ ) {
+ return undefined;
+ }
+ return {
+ changeNum,
+ submittable: change.submittable,
+ submitRequirements: change.submit_requirements,
+ };
+ }
+
async getChangeCommitInfo(
changeNum: NumericChangeId,
patchNum: PatchSetNum
@@ -3503,15 +3541,10 @@
)
) as Promise<ChangeInfo | undefined>;
} else {
- const params: FetchParams = {q: `change:${changeNum}`};
- if (optionsHex) {
- params['O'] = optionsHex;
- }
return this._restApiHelper
.fetchJSON(
{
url: `/changes/?q=change:${changeNum}`,
- params,
errFn,
anonymizedUrl: '/changes/?q=change:*',
},
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index f874886..3a37599 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -108,6 +108,7 @@
LabelDefinitionInfo,
LabelDefinitionInput,
SubmitRequirementInput,
+ SubmitRequirementResultInfo,
} from '../../api/rest-api';
export interface GetDiffCommentsOutput {
@@ -115,6 +116,12 @@
comments: CommentInfo[];
}
+export interface SubmittabilityInfo {
+ changeNum: NumericChangeId;
+ submittable: boolean;
+ submitRequirements: SubmitRequirementResultInfo[];
+}
+
export interface RestApiService extends Finalizable {
getConfig(
noCache?: boolean,
@@ -207,6 +214,13 @@
): Promise<ParsedChangeInfo | undefined>;
/**
+ * Returns information about submittability and Submit Requirements.
+ */
+ getSubmittabilityInfo(
+ changeNum: NumericChangeId
+ ): Promise<SubmittabilityInfo | undefined>;
+
+ /**
* For every revision of the change returns the list of FileInfo for files
* which are modified compared to revision's parent.
*/
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 1aead7d..818dbef 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -3,7 +3,10 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {
+ RestApiService,
+ SubmittabilityInfo,
+} from '../../services/gr-rest-api/gr-rest-api';
import {FlowInfo} from '../../api/rest-api';
import {
AccountCapabilityInfo,
@@ -248,6 +251,9 @@
if (changeNum === undefined) return Promise.resolve(undefined);
return Promise.resolve(createChange() as ParsedChangeInfo);
},
+ getSubmittabilityInfo(): Promise<SubmittabilityInfo | undefined> {
+ return Promise.resolve(undefined);
+ },
getChangeEdit(): Promise<EditInfo | undefined> {
return Promise.resolve(undefined);
},
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index db02f29..ee7aa3a 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -351,7 +351,16 @@
* Show only applicable.
*/
export function getRequirements(change?: ParsedChangeInfo | ChangeInfo) {
- return (change?.submit_requirements ?? []).filter(
+ return getApplicableRequirements(change?.submit_requirements);
+}
+
+/**
+ * Get applicable requirements.
+ */
+export function getApplicableRequirements(
+ requirements?: SubmitRequirementResultInfo[]
+) {
+ return (requirements ?? []).filter(
req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
);
}
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-dark-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-dark-dark.png
index 10423eb..b94a510 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-dark-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-dark-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all-dark-dark.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all-dark-dark.png
index 6da3f5a..d09e5cd 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all-dark-dark.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all-dark-dark.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all.png
index 173cdb4..fb4c9cf 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata-show-all.png
Binary files differ
diff --git a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata.png b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata.png
index c2eaf99..7bea9b3 100644
--- a/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata.png
+++ b/polygerrit-ui/screenshots/Chromium/baseline/gr-change-metadata.png
Binary files differ
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index bc6f619..eb84a2c 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -33,6 +33,7 @@
{@param? defaultDashboardHex: ?}
{@param? dashboardQuery: ?}
{@param? userIsAuthenticated: ?}
+ {@param? submitRequirementsHex: ?}
<!DOCTYPE html>{\n}
<html lang="en">{\n}
<meta charset="utf-8">{\n}
@@ -95,6 +96,9 @@
{if $userIsAuthenticated}
<link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/edit/?download-commands=true" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
{/if}
+ {if $submitRequirementsHex}
+ <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}?O={$submitRequirementsHex}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+ {/if}
{/if}
<link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/comments?enable-context=true&context-padding=3" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
<link rel="preload" href="{$canonicalPath}/changes/?q=change:{$changeNum}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 6815993..73305c1 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-acceptance-framework</artifactId>
- <version>3.12.3-SNAPSHOT</version>
+ <version>3.13.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Acceptance Test Framework</name>
<description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 9a9ed45..8a12804 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-extension-api</artifactId>
- <version>3.12.3-SNAPSHOT</version>
+ <version>3.13.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Extension API</name>
<description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index 40349e3..0d254b3 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-api</artifactId>
- <version>3.12.3-SNAPSHOT</version>
+ <version>3.13.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin API</name>
<description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index ddc370b..045a594 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-war</artifactId>
- <version>3.12.3-SNAPSHOT</version>
+ <version>3.13.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Gerrit Code Review - WAR</name>
<description>Gerrit WAR</description>
diff --git a/version.bzl b/version.bzl
index 02ae444..65c00f3 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
# Used by :api_install and :api_deploy targets
# when talking to the destination repository.
#
-GERRIT_VERSION = "3.12.3-SNAPSHOT"
+GERRIT_VERSION = "3.13.0-SNAPSHOT"