Merge "Refactor getUrlForChange to take options"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 58a3724..64aa6e0 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7111,6 +7111,9 @@
Additional information about whom to notify about the update as a map
of link:user-notify.html#recipient-types[recipient type] to
link:#notify-info[NotifyInfo] entity.
+|`ignore_automatic_attention_set_rules`|optional|
+If set to true, ignore all automatic attention set rules described in the
+link:#attention-set[attention set]. When not set, the default is false.
|=============================
[[description-input]]
diff --git a/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java b/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
index ee10a1d..8432c8f 100644
--- a/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
@@ -25,4 +25,10 @@
public NotifyHandling notify = NotifyHandling.ALL;
public Map<RecipientType, NotifyInfo> notifyDetails;
+
+ /**
+ * Users in the attention set will not be added/removed from this endpoint call. Normally, users
+ * are added to the attention set upon deletion of their vote by other users.
+ */
+ public boolean ignoreAutomaticAttentionSetRules;
}
diff --git a/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java b/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
index 6e640f3..b887323 100644
--- a/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
+++ b/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
@@ -23,7 +23,6 @@
import com.google.gerrit.extensions.validators.CommentValidationContext;
import com.google.gerrit.extensions.validators.CommentValidationFailure;
import com.google.gerrit.extensions.validators.CommentValidator;
-import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
@@ -32,7 +31,8 @@
/**
* Limits the total size of all comments and change messages to prevent space/time complexity
- * issues. Note that autogenerated change messages are not subject to validation.
+ * issues. Note that autogenerated change messages are not subject to validation. However, we still
+ * count autogenerated messages for the limit (which will be notified on a further comment).
*/
public class CommentCumulativeSizeValidator implements CommentValidator {
public static final int DEFAULT_CUMULATIVE_COMMENT_SIZE_LIMIT = 3 << 20;
@@ -60,17 +60,11 @@
notes.getRobotComments().values().stream())
.mapToInt(Comment::getApproximateSize)
.sum()
- + notes.getChangeMessages().stream()
- // Auto-generated change messages are not counted for the limit. This method is not
- // called when those change messages are created, but we should also skip them when
- // counting the size for unrelated messages.
- .filter(cm -> !ChangeMessagesUtil.isAutogenerated(cm.getTag()))
- .mapToInt(cm -> cm.getMessage().length())
- .sum();
+ + notes.getChangeMessages().stream().mapToInt(cm -> cm.getMessage().length()).sum();
int newCumulativeSize =
comments.stream().mapToInt(CommentForValidation::getApproximateSize).sum();
ImmutableList.Builder<CommentValidationFailure> failures = ImmutableList.builder();
- if (!comments.isEmpty() && existingCumulativeSize + newCumulativeSize > maxCumulativeSize) {
+ if (!comments.isEmpty() && !isEnoughSpace(notes, newCumulativeSize, maxCumulativeSize)) {
// This warning really applies to the set of all comments, but we need to pick one to attach
// the message to.
CommentForValidation commentForFailureMessage = Iterables.getLast(comments);
@@ -84,4 +78,19 @@
}
return failures.build();
}
+
+ /**
+ * Returns {@code true} if there is available space and the new size that we wish to add is less
+ * than the maximum allowed size. {@code false} otherwise (if there is not enough space).
+ */
+ public static boolean isEnoughSpace(ChangeNotes notes, int addedBytes, int maxCumulativeSize) {
+ int existingCumulativeSize =
+ Stream.concat(
+ notes.getHumanComments().values().stream(),
+ notes.getRobotComments().values().stream())
+ .mapToInt(Comment::getApproximateSize)
+ .sum()
+ + notes.getChangeMessages().stream().mapToInt(cm -> cm.getMessage().length()).sum();
+ return existingCumulativeSize + addedBytes < maxCumulativeSize;
+ }
}
diff --git a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
index 572d73d..c918e3d 100644
--- a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
+++ b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
@@ -70,6 +70,7 @@
*/
public class SubmitWithStickyApprovalDiff {
private static final int HEAP_EST_SIZE = 32 * 1024;
+ private static final int DEFAULT_POST_SUBMIT_SIZE_LIMIT = 300 * 1024; // 300 KiB
private final DiffOperations diffOperations;
private final ProjectCache projectCache;
@@ -88,6 +89,15 @@
this.projectCache = projectCache;
this.patchScriptFactoryFactory = patchScriptFactoryFactory;
this.repositoryManager = repositoryManager;
+ // (November 2021) We define the max cumulative comment size to 300 KIB since it's a reasonable
+ // size that is large enough for all purposes but not too large to choke the change index by
+ // exceeding the cumulative comment size limit (new comments are not allowed once the limit
+ // is reached). At Google, the change index limit is 5MB, while the cumulative size limit is
+ // set at 3MB. In this example, we can reach at most 3.3MB hence we ensure not to exceed the
+ // limit of 5MB.
+ // The reason we exclude the post submit diff from the cumulative comment size limit is
+ // just because change messages not currently being validated. Change messages are still
+ // counted towards the limit, though.
maxCumulativeSize =
serverConfig.getInt(
"change",
@@ -129,7 +139,9 @@
diff.append("The change was submitted with unreviewed changes in the following files:\n\n");
TemporaryBuffer.Heap buffer =
- new TemporaryBuffer.Heap(Math.min(HEAP_EST_SIZE, maxCumulativeSize), maxCumulativeSize);
+ new TemporaryBuffer.Heap(
+ Math.min(HEAP_EST_SIZE, DEFAULT_POST_SUBMIT_SIZE_LIMIT),
+ DEFAULT_POST_SUBMIT_SIZE_LIMIT);
try (Repository repository = repositoryManager.openRepository(notes.getProjectName());
DiffFormatter formatter = new DiffFormatter(buffer)) {
formatter.setRepository(repository);
@@ -150,6 +162,12 @@
throw e;
}
}
+ if (formatterResult != null) {
+ int addedBytes = formatterResult.stream().mapToInt(String::length).sum();
+ if (!CommentCumulativeSizeValidator.isEnoughSpace(notes, addedBytes, maxCumulativeSize)) {
+ isDiffTooLarge = true;
+ }
+ }
for (FileDiffOutput fileDiff : modifiedFilesList) {
diff.append(
getDiffForFile(
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 45d1f5a..4387524 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -40,6 +40,7 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.AddToAttentionSetOp;
+import com.google.gerrit.server.change.AttentionSetUnchangedOp;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.VoteResource;
@@ -146,7 +147,8 @@
r.getReviewerUser().state(),
rsrc.getLabel(),
input));
- if (!r.getReviewerUser().getAccountId().equals(currentUserProvider.get().getAccountId())) {
+ if (!input.ignoreAutomaticAttentionSetRules
+ && !r.getReviewerUser().getAccountId().equals(currentUserProvider.get().getAccountId())) {
bu.addOp(
change.getId(),
attentionSetOpFactory.create(
@@ -154,6 +156,9 @@
/* reason= */ "Their vote was deleted",
/* notify= */ false));
}
+ if (input.ignoreAutomaticAttentionSetRules) {
+ bu.addOp(change.getId(), new AttentionSetUnchangedOp());
+ }
bu.execute();
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
index 5124d11..29fdc15 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
@@ -19,6 +19,7 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static org.eclipse.jgit.lib.Constants.HEAD;
import com.google.common.collect.ImmutableList;
@@ -35,6 +36,7 @@
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.inject.Inject;
import java.util.HashSet;
@@ -345,8 +347,8 @@
}
@Test
- @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "1k")
- public void autoGeneratedPostSubmitDiffIsNotPartOfTheCommentSizeLimit() throws Exception {
+ @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "10k")
+ public void autoGeneratedPostSubmitDiffIsPartOfTheCommentSizeLimit() throws Exception {
Change.Id changeId =
changeOperations.newChange().project(project).file("file").content("content").create();
gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
@@ -356,35 +358,69 @@
// Post a submit diff that is almost the cumulativeCommentSizeLimit
gApi.changes().id(changeId.get()).current().submit();
assertThat(Iterables.getLast(gApi.changes().id(changeId.get()).messages()).message)
- .doesNotContain("many unreviewed changes");
+ .doesNotContain("The diff is too large to show. Please review the diff");
- // unrelated comment and change message posting works fine, since the post submit diff is not
+ // unrelated comment and change message posting doesn't work, since the post submit diff is
// counted towards the cumulativeCommentSizeLimit for unrelated follow-up comments.
- // 800 + 400 + 400 > 1k, but 400 + 400 < 1k, hence these comments are accepted (the original
- // 800 is not counted).
- String message = new String(new char[400]).replace("\0", "a");
+ // 800 + 9500 > 10k.
+ String message = new String(new char[9500]).replace("\0", "a");
ReviewInput reviewInput = new ReviewInput().message(message);
CommentInput commentInput = new CommentInput();
commentInput.line = 1;
- commentInput.message = message;
commentInput.path = "file";
reviewInput.comments = ImmutableMap.of("file", ImmutableList.of(commentInput));
- gApi.changes().id(changeId.get()).current().review(reviewInput);
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.changes().id(changeId.get()).current().review(reviewInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Exceeding maximum cumulative size of comments and change messages");
}
@Test
- @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "1k")
public void postSubmitDiffCannotBeTooBig() throws Exception {
Change.Id changeId =
changeOperations.newChange().project(project).file("file").content("content").create();
gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
- String content = new String(new char[1100]).replace("\0", "a");
+ // max post submit diff size is 300k
+ String content = new String(new char[320000]).replace("\0", "a");
changeOperations.change(changeId).newPatchset().file("file").content(content).create();
- // Post submit diff is over the cumulativeCommentSizeLimit, so we shorten the message.
+ // Post submit diff is over the postSubmitDiffSizeLimit (300k).
+ gApi.changes().id(changeId.get()).current().submit();
+ assertThat(Iterables.getLast(gApi.changes().id(changeId.get()).messages()).message)
+ .isEqualTo(
+ "Change has been successfully merged\n\n1 is the latest approved patch-set.\nThe "
+ + "change was submitted with unreviewed changes in the following "
+ + "files:\n\n```\nThe name of the file: file\nInsertions: 1, Deletions: 1.\n\nThe"
+ + " diff is too large to show. Please review the diff.\n```\n");
+ }
+
+ @Test
+ @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "10k")
+ public void postSubmitDiffCannotBeTooBigWithLargeComments() throws Exception {
+ Change.Id changeId =
+ changeOperations.newChange().project(project).file("file").content("content").create();
+ gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
+
+ // unrelated comment taking up most of the space, making post submit diff shorter.
+ String message = new String(new char[9700]).replace("\0", "a");
+ ReviewInput reviewInput = new ReviewInput().message(message);
+ CommentInput commentInput = new CommentInput();
+ commentInput.line = 1;
+ commentInput.path = "file";
+ reviewInput.comments = ImmutableMap.of("file", ImmutableList.of(commentInput));
+ gApi.changes().id(changeId.get()).current().review(reviewInput);
+
+ String content = new String(new char[500]).replace("\0", "a");
+ changeOperations.change(changeId).newPatchset().file("file").content(content).create();
+
+ // Post submit diff is over the cumulativeCommentSizeLimit, since the comment took most of
+ // the space (even though the post submit diff is not limited).
gApi.changes().id(changeId.get()).current().submit();
assertThat(Iterables.getLast(gApi.changes().id(changeId.get()).messages()).message)
.isEqualTo(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 4bce5d8..9246442 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -46,6 +46,7 @@
import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.api.changes.AttentionSetInput;
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
+import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewerInput;
@@ -1941,6 +1942,30 @@
}
@Test
+ public void deleteVotesDoesNotAffectAttentionSetWhenIgnoreAutomaticRulesIsSet() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ requestScopeOperations.setApiUser(user.id());
+ recommend(r.getChangeId());
+
+ requestScopeOperations.setApiUser(admin.id());
+
+ DeleteVoteInput deleteVoteInput = new DeleteVoteInput();
+ deleteVoteInput.label = LabelId.CODE_REVIEW;
+
+ // set this to true to not change the attention set.
+ deleteVoteInput.ignoreAutomaticAttentionSetRules = true;
+
+ gApi.changes()
+ .id(r.getChangeId())
+ .current()
+ .reviewer(user.id().toString())
+ .deleteVote(deleteVoteInput);
+
+ assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty();
+ }
+
+ @Test
public void deleteVotesOfOthersAddThemToAttentionSet() throws Exception {
PushOneCommit.Result r = createChange();
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 47c820a..a7a615fc 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -101,7 +101,6 @@
"elements/admin/gr-repo-access/gr-repo-access_html.ts",
"elements/admin/gr-repo-commands/gr-repo-commands_html.ts",
"elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.ts",
- "elements/admin/gr-repo/gr-repo_html.ts",
"elements/admin/gr-rule-editor/gr-rule-editor_html.ts",
"elements/change-list/gr-change-list-view/gr-change-list-view_html.ts",
"elements/change-list/gr-change-list/gr-change-list_html.ts",
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 905d6be..90d9289 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -484,5 +484,20 @@
createCommentInPlace(): void;
resetScrollMode(): void;
- moveToLineNumber(lineNum: number, side: Side, path?: string): void;
+
+ /**
+ * Moves to a specific line number in the diff
+ *
+ * @param lineNum which line number should be selected
+ * @param side which side should be selected
+ * @param path file path for the file that should be selected
+ * @param intentionalMove Defines if move-related controls should be applied
+ * (e.g. GrCursorManager.focusOnMove)
+ **/
+ moveToLineNumber(
+ lineNum: number,
+ side: Side,
+ path?: string,
+ intentionalMove?: boolean
+ ): void;
}
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index e2b1502..0c47cdc 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -524,6 +524,7 @@
plugin_config?: PluginNameToPluginParametersMap;
actions?: {[viewName: string]: ActionInfo};
reject_empty_commit?: InheritedBooleanInfo;
+ enable_reviewer_by_email: InheritedBooleanInfo;
}
export declare interface ConfigListParameterInfo
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index 2328a05..48f42a4 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -39,6 +39,7 @@
CapabilityInfoMap,
GitRef,
LabelNameToLabelTypeInfoMap,
+ RepoName,
} from '../../../types/common';
import {PolymerDomRepeatEvent} from '../../../types/types';
import {fireEvent} from '../../../utils/event-util';
@@ -75,6 +76,9 @@
return htmlTemplate;
}
+ @property({type: String})
+ repo?: RepoName;
+
@property({type: Object})
capabilities?: CapabilityInfoMap;
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts
index 1438825..65a3199 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts
@@ -123,6 +123,7 @@
section="[[section.id]]"
editing="[[editing]]"
groups="[[groups]]"
+ repo="[[repo]]"
on-added-permission-removed="_handleAddedPermissionRemoved"
>
</gr-permission>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
index b7ea237..4d5ecd3 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -39,6 +39,7 @@
ProjectAccessGroups,
GroupId,
GitRef,
+ RepoName,
} from '../../../types/common';
import {
AutocompleteQuery,
@@ -98,6 +99,9 @@
return htmlTemplate;
}
+ @property({type: String})
+ repo?: RepoName;
+
@property({type: Object})
labels?: LabelNameToLabelTypeInfoMap;
@@ -320,7 +324,11 @@
_getGroupSuggestions(): Promise<AutocompleteSuggestion[]> {
return this.restApiService
- .getSuggestedGroups(this._groupFilter || '', MAX_AUTOCOMPLETE_RESULTS)
+ .getSuggestedGroups(
+ this._groupFilter || '',
+ this.repo,
+ MAX_AUTOCOMPLETE_RESULTS
+ )
.then(response => {
const groups: GroupSuggestion[] = [];
for (const [name, value] of Object.entries(response ?? {})) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts
index 65f0564..8f88619 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts
@@ -116,6 +116,7 @@
editing="[[_editing]]"
owner-of="[[_ownerOf]]"
groups="[[_groups]]"
+ repo="[[repo]]"
on-added-section-removed="_handleAddedSectionRemoved"
></gr-access-section>
</template>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index 7092c9b..37c88f2 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -43,7 +43,6 @@
export interface ConfigChangeInfo {
_key: string; // parameterName of PluginParameterToConfigParameterInfoMap
info: ConfigParameterInfo;
- notifyPath: string;
}
export interface PluginData {
@@ -54,7 +53,6 @@
export interface PluginConfigChangeDetail {
name: string; // parameterName of PluginParameterToConfigParameterInfoMap
config: PluginParameterToConfigParameterInfoMap;
- notifyPath: string;
}
@customElement('gr-repo-plugin-config')
@@ -248,7 +246,6 @@
return {
_key,
info,
- notifyPath: `${_key}.value`,
};
}
@@ -256,7 +253,7 @@
this._handleChange(e.detail);
}
- _handleChange({_key, info, notifyPath}: ConfigChangeInfo) {
+ _handleChange({_key, info}: ConfigChangeInfo) {
// If pluginData is not set, editors are not created and this method
// can't be called
const {name, config} = this.pluginData!;
@@ -265,7 +262,6 @@
const detail: PluginConfigChangeDetail = {
name,
config: {...config, [_key]: info},
- notifyPath: `${name}.${notifyPath}`,
};
this.dispatchEvent(
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
index 8c2e6b3..4076b747f 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
@@ -44,7 +44,6 @@
element._handleChange({
_key: 'plugin',
info: {value: 'newTest'},
- notifyPath: 'plugin.value',
});
assert.isTrue(eventStub.called);
@@ -52,7 +51,6 @@
const {detail} = eventStub.lastCall.args[0];
assert.equal(detail.name, 'testName');
assert.deepEqual(detail.config, {plugin: {value: 'newTest'}});
- assert.equal(detail.notifyPath, 'testName.plugin.value');
});
suite('option types', () => {
@@ -151,7 +149,6 @@
const detail = element._buildConfigChangeInfo('newTest', 'plugin');
assert.equal(detail._key, 'plugin');
assert.deepEqual(detail.info, {value: 'newTest'});
- assert.equal(detail.notifyPath, 'plugin.value');
});
});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
index cd5b095..bf0b393 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -14,38 +14,42 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '@polymer/iron-input/iron-input';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import '../../shared/gr-button/gr-button';
import '../../shared/gr-download-commands/gr-download-commands';
import '../../shared/gr-select/gr-select';
-import '../../../styles/gr-font-styles';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-subpage-styles';
-import '../../../styles/shared-styles';
+import '../../shared/gr-textarea/gr-textarea';
import '../gr-repo-plugin-config/gr-repo-plugin-config';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-repo_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property, observe} from '@polymer/decorators';
import {
ConfigInfo,
RepoName,
InheritedBooleanInfo,
SchemesInfoMap,
ConfigInput,
+ MaxObjectSizeLimitInfo,
PluginParameterToConfigParameterInfoMap,
- PluginNameToPluginParametersMap,
} from '../../../types/common';
-import {PluginData} from '../gr-repo-plugin-config/gr-repo-plugin-config';
-import {ProjectState} from '../../../constants/constants';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
+import {
+ InheritedBooleanInfoConfiguredValue,
+ ProjectState,
+ SubmitType,
+} from '../../../constants/constants';
import {hasOwnProperty} from '../../../utils/common-util';
import {firePageError, fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
import {WebLinkInfo} from '../../../types/diff';
import {ErrorCallback} from '../../../api/rest';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {subpageStyles} from '../../../styles/gr-subpage-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {BindValueChangeEvent} from '../../../types/events';
+import {deepClone} from '../../../utils/object-util';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
const STATES = {
active: {value: ProjectState.ACTIVE, label: 'Active'},
@@ -81,92 +85,669 @@
},
};
-@customElement('gr-repo')
-export class GrRepo extends PolymerElement {
- static get template() {
- return htmlTemplate;
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-repo': GrRepo;
}
+}
+
+@customElement('gr-repo')
+export class GrRepo extends LitElement {
+ private schemes: string[] = [];
@property({type: String})
repo?: RepoName;
- @property({type: Boolean})
- _configChanged = false;
+ /* private but used in test */
+ @state() loading = true;
- @property({type: Boolean})
- _loading = true;
+ /* private but used in test */
+ @state() repoConfig?: ConfigInfo;
- @property({type: Boolean, observer: '_loggedInChanged'})
- _loggedIn = false;
+ /* private but used in test */
+ @state() readOnly = true;
- @property({type: Object})
- _repoConfig?: ConfigInfo;
+ @state() private states = Object.values(STATES);
- @property({
- type: Array,
- computed: '_computePluginData(_repoConfig.plugin_config.*)',
- })
- _pluginData?: PluginData[];
+ @state() private originalConfig?: ConfigInfo;
- @property({type: Boolean})
- _readOnly = true;
+ @state() private selectedScheme?: string;
- @property({type: Array})
- _states = Object.values(STATES);
+ /* private but used in test */
+ @state() schemesObj?: SchemesInfoMap;
- @property({
- type: Array,
- computed: '_computeSchemes(_schemesDefault, _schemesObj)',
- observer: '_schemesChanged',
- })
- _schemes: string[] = [];
+ @state() private weblinks: WebLinkInfo[] = [];
- // This is workaround to have _schemes with default value [],
- // because assignment doesn't work when property has a computed attribute.
- @property({type: Array})
- _schemesDefault: string[] = [];
-
- @property({type: String})
- _selectedCommand = 'Clone';
-
- @property({type: String})
- _selectedScheme?: string;
-
- @property({type: Object})
- _schemesObj?: SchemesInfoMap;
-
- @property({type: Array})
- weblinks: WebLinkInfo[] = [];
+ @state() private pluginConfigChanged = false;
private readonly restApiService = appContext.restApiService;
override connectedCallback() {
super.connectedCallback();
- this._loadRepo();
+ this.loadRepo();
fireTitleChange(this, `${this.repo}`);
}
- _computePluginData(
- configRecord: PolymerDeepPropertyChange<
- PluginNameToPluginParametersMap,
- PluginNameToPluginParametersMap
- >
- ) {
- if (!configRecord || !configRecord.base) {
- return [];
- }
+ static override get styles() {
+ return [
+ fontStyles,
+ formStyles,
+ subpageStyles,
+ sharedStyles,
+ css`
+ .info {
+ margin-bottom: var(--spacing-xl);
+ }
+ h2.edited:after {
+ color: var(--deemphasized-text-color);
+ content: ' *';
+ }
+ .loading,
+ .hide {
+ display: none;
+ }
+ #loading.loading {
+ display: block;
+ }
+ #loading:not(.loading) {
+ display: none;
+ }
+ #options .repositorySettings {
+ display: none;
+ }
+ #options .repositorySettings.showConfig {
+ display: block;
+ }
+ `,
+ ];
+ }
- const pluginConfig = configRecord.base;
+ override render() {
+ const configChanged = this.hasConfigChanged();
+ return html`
+ <div class="main gr-form-styles read-only">
+ <div class="info">
+ <h1 id="Title" class="heading-1">${this.repo}</h1>
+ <hr />
+ <div>
+ <a href=${this.weblinks?.[0]?.url}
+ ><gr-button link ?disabled=${!this.weblinks?.[0]?.url}
+ >Browse</gr-button
+ ></a
+ ><a href=${this.computeChangesUrl(this.repo)}
+ ><gr-button link>View Changes</gr-button></a
+ >
+ </div>
+ </div>
+ <div id="loading" class=${this.loading ? 'loading' : ''}>
+ Loading...
+ </div>
+ <div id="loadedContent" class=${this.loading ? 'loading' : ''}>
+ ${this.renderDownloadCommands()}
+ <h2
+ id="configurations"
+ class="heading-2 ${configChanged ? 'edited' : ''}"
+ >
+ Configurations
+ </h2>
+ <div id="form">
+ <fieldset>
+ ${this.renderDescription()} ${this.renderRepoOptions()}
+ ${this.renderPluginConfig()}
+ <gr-button
+ ?disabled=${this.readOnly || !configChanged}
+ @click=${this.handleSaveRepoConfig}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <gr-endpoint-decorator name="repo-config">
+ <gr-endpoint-param
+ name="repoName"
+ .value=${this.repo}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="readOnly"
+ .value=${this.readOnly}
+ ></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderDownloadCommands() {
+ return html`
+ <div
+ id="downloadContent"
+ class=${!this.schemes || !this.schemes.length ? 'hide' : ''}
+ >
+ <h2 id="download" class="heading-2">Download</h2>
+ <fieldset>
+ <gr-download-commands
+ id="downloadCommands"
+ .commands=${this.computeCommands(
+ this.repo,
+ this.schemesObj,
+ this.selectedScheme
+ )}
+ .schemes=${this.schemes}
+ .selectedScheme=${this.selectedScheme}
+ @selected-scheme-changed=${this.handleSelectedSchemeValueChanged}
+ ></gr-download-commands>
+ </fieldset>
+ </div>
+ `;
+ }
+
+ private renderDescription() {
+ return html`
+ <h3 id="Description" class="heading-3">Description</h3>
+ <fieldset>
+ <gr-textarea
+ id="descriptionInput"
+ class="description"
+ autocomplete="on"
+ placeholder="<Insert repo description here>"
+ rows="4"
+ monospace
+ ?disabled=${this.readOnly}
+ .text=${this.repoConfig?.description}
+ @text-changed=${this.handleDescriptionTextChanged}
+ >
+ </fieldset>
+ `;
+ }
+
+ private renderRepoOptions() {
+ return html`
+ <h3 id="Options" class="heading-3">Repository Options</h3>
+ <fieldset id="options">
+ ${this.renderState()} ${this.renderSubmitType()}
+ ${this.renderContentMerges()} ${this.renderNewChange()}
+ ${this.renderChangeId()} ${this.renderEnableSignedPush()}
+ ${this.renderRequireSignedPush()} ${this.renderRejectImplicitMerges()}
+ ${this.renderUnRegisteredCc()} ${this.renderPrivateByDefault()}
+ ${this.renderWorkInProgressByDefault()} ${this.renderMaxGitObjectSize()}
+ ${this.renderMatchAuthoredDateWithCommitterDate()}
+ ${this.renderRejectEmptyCommit()}
+ </fieldset>
+ <h3 id="Options" class="heading-3">Contributor Agreements</h3>
+ <fieldset id="agreements">
+ ${this.renderContributorAgreement()} ${this.renderUseSignedOffBy()}
+ </fieldset>
+ `;
+ }
+
+ private renderState() {
+ return html`
+ <section>
+ <span class="title">State</span>
+ <span class="value">
+ <gr-select
+ id="stateSelect"
+ .bindValue=${this.repoConfig?.state}
+ @bind-value-changed=${this.handleStateSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.states.map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderSubmitType() {
+ return html`
+ <section>
+ <span class="title">Submit type</span>
+ <span class="value">
+ <gr-select
+ id="submitTypeSelect"
+ .bindValue=${this.repoConfig?.submit_type}
+ @bind-value-changed=${this.handleSubmitTypeSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatSubmitTypeSelect(this.repoConfig).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderContentMerges() {
+ return html`
+ <section>
+ <span class="title">Allow content merges</span>
+ <span class="value">
+ <gr-select
+ id="contentMergeSelect"
+ .bindValue=${this.repoConfig?.use_content_merge?.configured_value}
+ @bind-value-changed=${this.handleContentMergeSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.use_content_merge
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderNewChange() {
+ return html`
+ <section>
+ <span class="title">
+ Create a new change for every commit not in the target branch
+ </span>
+ <span class="value">
+ <gr-select
+ id="newChangeSelect"
+ .bindValue=${this.repoConfig
+ ?.create_new_change_for_all_not_in_target?.configured_value}
+ @bind-value-changed=${this.handleNewChangeSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.create_new_change_for_all_not_in_target
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderChangeId() {
+ return html`
+ <section>
+ <span class="title">Require Change-Id in commit message</span>
+ <span class="value">
+ <gr-select
+ id="requireChangeIdSelect"
+ .bindValue=${this.repoConfig?.require_change_id?.configured_value}
+ @bind-value-changed=${this
+ .handleRequireChangeIdSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.require_change_id
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderEnableSignedPush() {
+ return html`
+ <section
+ id="enableSignedPushSettings"
+ class="repositorySettings ${this.repoConfig?.enable_signed_push
+ ? 'showConfig'
+ : ''}"
+ >
+ <span class="title">Enable signed push</span>
+ <span class="value">
+ <gr-select
+ id="enableSignedPush"
+ .bindValue=${this.repoConfig?.enable_signed_push?.configured_value}
+ @bind-value-changed=${this.handleEnableSignedPushBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.enable_signed_push
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderRequireSignedPush() {
+ return html`
+ <section
+ id="requireSignedPushSettings"
+ class="repositorySettings ${this.repoConfig?.require_signed_push
+ ? 'showConfig'
+ : ''}"
+ >
+ <span class="title">Require signed push</span>
+ <span class="value">
+ <gr-select
+ id="requireSignedPush"
+ .bindValue=${this.repoConfig?.require_signed_push?.configured_value}
+ @bind-value-changed=${this.handleRequireSignedPushBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.require_signed_push
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderRejectImplicitMerges() {
+ return html`
+ <section>
+ <span class="title">
+ Reject implicit merges when changes are pushed for review</span
+ >
+ <span class="value">
+ <gr-select
+ id="rejectImplicitMergesSelect"
+ .bindValue=${this.repoConfig?.reject_implicit_merges
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleRejectImplicitMergeSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.reject_implicit_merges
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderUnRegisteredCc() {
+ return html`
+ <section>
+ <span class="title">
+ Enable adding unregistered users as reviewers and CCs on changes</span
+ >
+ <span class="value">
+ <gr-select
+ id="unRegisteredCcSelect"
+ .bindValue=${this.repoConfig?.enable_reviewer_by_email
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleUnRegisteredCcSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.enable_reviewer_by_email
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderPrivateByDefault() {
+ return html`
+ <section>
+ <span class="title"> Set all new changes private by default</span>
+ <span class="value">
+ <gr-select
+ id="setAllnewChangesPrivateByDefaultSelect"
+ .bindValue=${this.repoConfig?.private_by_default?.configured_value}
+ @bind-value-changed=${this
+ .handleSetAllNewChangesPrivateByDefaultSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.private_by_default
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderWorkInProgressByDefault() {
+ return html`
+ <section>
+ <span class="title">
+ Set new changes to "work in progress" by default</span
+ >
+ <span class="value">
+ <gr-select
+ id="setAllNewChangesWorkInProgressByDefaultSelect"
+ .bindValue=${this.repoConfig?.work_in_progress_by_default
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleSetAllNewChangesWorkInProgressByDefaultSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.work_in_progress_by_default
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderMaxGitObjectSize() {
+ return html`
+ <section>
+ <span class="title">Maximum Git object size limit</span>
+ <span class="value">
+ <iron-input
+ id="maxGitObjSizeIronInput"
+ .bindValue=${this.repoConfig?.max_object_size_limit
+ ?.configured_value}
+ type="text"
+ ?disabled=${this.readOnly}
+ @bind-value-changed=${this.handleMaxGitObjSizeBindValueChanged}
+ >
+ <input
+ id="maxGitObjSizeInput"
+ type="text"
+ ?disabled=${this.readOnly}
+ />
+ </iron-input>
+ ${this.repoConfig?.max_object_size_limit?.value
+ ? `effective: ${this.repoConfig.max_object_size_limit.value} bytes`
+ : ''}
+ </span>
+ </section>
+ `;
+ }
+
+ private renderMatchAuthoredDateWithCommitterDate() {
+ return html`
+ <section>
+ <span class="title"
+ >Match authored date with committer date upon submit</span
+ >
+ <span class="value">
+ <gr-select
+ id="matchAuthoredDateWithCommitterDateSelect"
+ .bindValue=${this.repoConfig?.match_author_to_committer_date
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleMatchAuthoredDateWithCommitterDateSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.match_author_to_committer_date
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderRejectEmptyCommit() {
+ return html`
+ <section>
+ <span class="title">Reject empty commit upon submit</span>
+ <span class="value">
+ <gr-select
+ id="rejectEmptyCommitSelect"
+ .bindValue=${this.repoConfig?.reject_empty_commit?.configured_value}
+ @bind-value-changed=${this
+ .handleRejectEmptyCommitSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.reject_empty_commit
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderContributorAgreement() {
+ return html`
+ <section>
+ <span class="title">
+ Require a valid contributor agreement to upload</span
+ >
+ <span class="value">
+ <gr-select
+ id="contributorAgreementSelect"
+ .bindValue=${this.repoConfig?.use_contributor_agreements
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleUseContributorAgreementsBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.use_contributor_agreements
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderUseSignedOffBy() {
+ return html`
+ <section>
+ <span class="title">Require Signed-off-by in commit message</span>
+ <span class="value">
+ <gr-select
+ id="useSignedOffBySelect"
+ .bindValue=${this.repoConfig?.use_signed_off_by?.configured_value}
+ @bind-value-changed=${this
+ .handleUseSignedOffBySelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.use_signed_off_by
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderPluginConfig() {
+ const pluginData = this.computePluginData();
+ return html` <div
+ class="pluginConfig ${!pluginData || !pluginData.length ? 'hide' : ''}"
+ @plugin-config-changed=${this.handlePluginConfigChanged}
+ >
+ <h3 class="heading-3">Plugins</h3>
+ ${pluginData.map(
+ item => html`
+ <gr-repo-plugin-config .pluginData=${item}></gr-repo-plugin-config>
+ `
+ )}
+ </div>`;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('schemesObj')) {
+ this.computeSchemesAndDefault();
+ }
+ }
+
+ /* private but used in test */
+ computePluginData() {
+ if (!this.repoConfig || !this.repoConfig.plugin_config) return [];
+ const pluginConfig = this.repoConfig.plugin_config;
return Object.keys(pluginConfig).map(name => {
return {name, config: pluginConfig[name]};
});
}
- _loadRepo() {
- if (!this.repo) {
- return Promise.resolve();
- }
+ /* private but used in test */
+ async loadRepo() {
+ if (!this.repo) return Promise.resolve();
const promises = [];
@@ -175,11 +756,16 @@
};
promises.push(
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
+ this.restApiService.getLoggedIn().then(loggedIn => {
if (loggedIn) {
const repo = this.repo;
if (!repo) throw new Error('undefined repo');
+ this.restApiService.getPreferences().then(prefs => {
+ if (prefs?.download_scheme) {
+ // Note (issue 5180): normalize the download scheme with lower-case.
+ this.selectedScheme = prefs.download_scheme.toLowerCase();
+ }
+ });
this.restApiService.getRepo(repo).then(repo => {
if (!repo?.web_links) return;
this.weblinks = repo.web_links;
@@ -190,71 +776,60 @@
}
// If the user is not an owner, is_owner is not a property.
- this._readOnly = !access[repo]?.is_owner;
+ this.readOnly = !access[repo]?.is_owner;
});
}
})
);
- promises.push(
- this.restApiService.getProjectConfig(this.repo, errFn).then(config => {
- if (!config) {
- return;
- }
+ const repoConfigHelper = async () => {
+ const config = await this.restApiService.getProjectConfig(
+ this.repo as RepoName,
+ errFn
+ );
+ if (!config) return;
- if (config.default_submit_type) {
- // The gr-select is bound to submit_type, which needs to be the
- // *configured* submit type. When default_submit_type is
- // present, the server reports the *effective* submit type in
- // submit_type, so we need to overwrite it before storing the
- // config in this.
- config.submit_type = config.default_submit_type.configured_value;
- }
- if (!config.state) {
- config.state = STATES.active.value;
- }
- this._repoConfig = config;
- this._loading = false;
- })
- );
-
- promises.push(
- this.restApiService.getConfig().then(config => {
- if (!config) {
- return;
- }
-
- this._schemesObj = config.download.schemes;
- })
- );
-
- return Promise.all(promises);
- }
-
- _computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
- }
-
- _computeHideClass(arr?: PluginData[] | string[]) {
- return !arr || !arr.length ? 'hide' : '';
- }
-
- _loggedInChanged(_loggedIn?: boolean) {
- if (!_loggedIn) {
- return;
- }
- this.restApiService.getPreferences().then(prefs => {
- if (prefs?.download_scheme) {
- // Note (issue 5180): normalize the download scheme with lower-case.
- this._selectedScheme = prefs.download_scheme.toLowerCase();
+ if (config.default_submit_type) {
+ // The gr-select is bound to submit_type, which needs to be the
+ // *configured* submit type. When default_submit_type is
+ // present, the server reports the *effective* submit type in
+ // submit_type, so we need to overwrite it before storing the
+ // config in this.
+ config.submit_type = config.default_submit_type.configured_value;
}
- });
+ if (!config.state) {
+ config.state = STATES.active.value as ProjectState;
+ }
+ // To properly check if the config has changed we need it to be a string
+ // as it's converted to a string in the input.
+ if (config.description === undefined) {
+ config.description = '';
+ }
+ // To properly check if the config has changed we need it to be a string
+ // as it's converted to a string in the input.
+ if (config.max_object_size_limit.configured_value === undefined) {
+ config.max_object_size_limit.configured_value = '';
+ }
+ this.repoConfig = config;
+ this.originalConfig = deepClone(config);
+ this.loading = false;
+ };
+ promises.push(repoConfigHelper());
+
+ const configHelper = async () => {
+ const config = await this.restApiService.getConfig();
+ if (!config) return;
+
+ this.schemesObj = config.download.schemes;
+ };
+ promises.push(configHelper());
+
+ await Promise.all(promises);
}
- _formatBooleanSelect(item: InheritedBooleanInfo) {
- if (!item) {
- return;
- }
+ /* private but used in test */
+ formatBooleanSelect(item?: InheritedBooleanInfo) {
+ if (!item) return [];
let inheritLabel = 'Inherit';
if (!(item.inherited_value === undefined)) {
inheritLabel = `Inherit (${item.inherited_value})`;
@@ -275,12 +850,10 @@
];
}
- _formatSubmitTypeSelect(projectConfig: ConfigInfo) {
- if (!projectConfig) {
- return;
- }
+ private formatSubmitTypeSelect(repoConfig?: ConfigInfo) {
+ if (!repoConfig) return [];
const allValues = Object.values(SUBMIT_TYPES);
- const type = projectConfig.default_submit_type;
+ const type = repoConfig.default_submit_type;
if (!type) {
// Server is too old to report default_submit_type, so assume INHERIT
// is not a valid value.
@@ -306,15 +879,9 @@
];
}
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _getLoggedIn() {
- return this.restApiService.getLoggedIn();
- }
-
- _formatRepoConfigForSave(repoConfig: ConfigInfo): ConfigInput {
+ /* private but used in test */
+ formatRepoConfigForSave(repoConfig?: ConfigInfo): ConfigInput {
+ if (!repoConfig) return {};
const configInputObj: ConfigInput = {};
for (const configKey of Object.keys(repoConfig)) {
const key = configKey as keyof ConfigInfo;
@@ -329,7 +896,7 @@
} else if (typeof repoConfig[key] === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const repoConfigObj: any = repoConfig[key];
- if (repoConfigObj.configured_value) {
+ if (repoConfigObj.configured_value !== undefined) {
configInputObj[key as keyof ConfigInput] =
repoConfigObj.configured_value;
}
@@ -341,56 +908,173 @@
return configInputObj;
}
- _handleSaveRepoConfig() {
- if (!this._repoConfig || !this.repo)
+ /* private but used in test */
+ async handleSaveRepoConfig() {
+ if (!this.repoConfig || !this.repo)
return Promise.reject(new Error('undefined repoConfig or repo'));
- return this.restApiService
- .saveRepoConfig(
- this.repo,
- this._formatRepoConfigForSave(this._repoConfig)
+ await this.restApiService.saveRepoConfig(
+ this.repo,
+ this.formatRepoConfigForSave(this.repoConfig)
+ );
+ this.originalConfig = deepClone(this.repoConfig);
+ this.pluginConfigChanged = false;
+ return;
+ }
+
+ private isEdited(
+ original?: InheritedBooleanInfo | MaxObjectSizeLimitInfo,
+ repo?: InheritedBooleanInfo | MaxObjectSizeLimitInfo
+ ) {
+ return original?.configured_value !== repo?.configured_value;
+ }
+
+ private hasConfigChanged() {
+ const {repoConfig, originalConfig} = this;
+
+ if (!repoConfig || !originalConfig) return false;
+
+ if (originalConfig.description !== repoConfig.description) {
+ return true;
+ }
+ if (originalConfig.state !== repoConfig.state) {
+ return true;
+ }
+ if (originalConfig.submit_type !== repoConfig.submit_type) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.use_content_merge,
+ repoConfig.use_content_merge
)
- .then(() => {
- this._configChanged = false;
- });
- }
-
- @observe('_repoConfig.*')
- _handleConfigChanged() {
- if (this._isLoading()) {
- return;
+ ) {
+ return true;
}
- this._configChanged = true;
- }
-
- _computeButtonDisabled(readOnly: boolean, configChanged: boolean) {
- return readOnly || !configChanged;
- }
-
- _computeHeaderClass(configChanged: boolean) {
- return configChanged ? 'edited' : '';
- }
-
- _computeSchemes(schemesDefault: string[], schemesObj?: SchemesInfoMap) {
- return !schemesObj ? schemesDefault : Object.keys(schemesObj);
- }
-
- _schemesChanged(schemes: string[]) {
- if (schemes.length === 0) {
- return;
+ if (
+ this.isEdited(
+ originalConfig.create_new_change_for_all_not_in_target,
+ repoConfig.create_new_change_for_all_not_in_target
+ )
+ ) {
+ return true;
}
- if (!this._selectedScheme || !schemes.includes(this._selectedScheme)) {
- this._selectedScheme = schemes.sort()[0];
+ if (
+ this.isEdited(
+ originalConfig.require_change_id,
+ repoConfig.require_change_id
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.enable_signed_push,
+ repoConfig.enable_signed_push
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.require_signed_push,
+ repoConfig.require_signed_push
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.reject_implicit_merges,
+ repoConfig.reject_implicit_merges
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.enable_reviewer_by_email,
+ repoConfig.enable_reviewer_by_email
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.private_by_default,
+ repoConfig.private_by_default
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.work_in_progress_by_default,
+ repoConfig.work_in_progress_by_default
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.max_object_size_limit,
+ repoConfig.max_object_size_limit
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.match_author_to_committer_date,
+ repoConfig.match_author_to_committer_date
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.reject_empty_commit,
+ repoConfig.reject_empty_commit
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.use_contributor_agreements,
+ repoConfig.use_contributor_agreements
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.use_signed_off_by,
+ repoConfig.use_signed_off_by
+ )
+ ) {
+ return true;
+ }
+
+ return this.pluginConfigChanged;
+ }
+
+ private computeSchemesAndDefault() {
+ this.schemes = !this.schemesObj ? [] : Object.keys(this.schemesObj);
+ if (this.schemes.length > 0) {
+ if (!this.selectedScheme || !this.schemes.includes(this.selectedScheme)) {
+ this.selectedScheme = this.schemes.sort()[0];
+ }
}
}
- _computeCommands(
+ private computeCommands(
repo?: RepoName,
schemesObj?: SchemesInfoMap,
- _selectedScheme?: string
+ selectedScheme?: string
) {
- if (!schemesObj || !repo || !_selectedScheme) return [];
- if (!hasOwnProperty(schemesObj, _selectedScheme)) return [];
- const commandObj = schemesObj[_selectedScheme].clone_commands;
+ if (!schemesObj || !repo || !selectedScheme) return [];
+ if (!hasOwnProperty(schemesObj, selectedScheme)) return [];
+ const commandObj = schemesObj[selectedScheme].clone_commands;
const commands = [];
for (const [title, command] of Object.entries(commandObj)) {
commands.push({
@@ -406,36 +1090,171 @@
return commands;
}
- _computeRepositoriesClass(config: InheritedBooleanInfo) {
- return config ? 'showConfig' : '';
+ private computeChangesUrl(name?: RepoName) {
+ if (!name) return '';
+ return GerritNav.getUrlForProjectChanges(name as RepoName);
}
- _computeChangesUrl(name: RepoName) {
- return GerritNav.getUrlForProjectChanges(name);
- }
-
- _computeBrowseUrl(weblinks: WebLinkInfo[]) {
- return weblinks?.[0]?.url;
- }
-
- _handlePluginConfigChanged({
- detail: {name, config, notifyPath},
+ /* private but used in test */
+ handlePluginConfigChanged({
+ detail: {name, config},
}: {
detail: {
name: string;
config: PluginParameterToConfigParameterInfoMap;
- notifyPath: string;
};
}) {
- if (this._repoConfig?.plugin_config) {
- this._repoConfig.plugin_config[name] = config;
- this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
+ if (this.repoConfig?.plugin_config) {
+ this.repoConfig.plugin_config[name] = config;
+ this.pluginConfigChanged = true;
+ this.requestUpdate();
}
}
-}
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-repo': GrRepo;
+ private handleSelectedSchemeValueChanged(e: CustomEvent) {
+ if (this.loading) return;
+ this.selectedScheme = e.detail.value;
+ }
+
+ private handleDescriptionTextChanged(e: CustomEvent) {
+ if (!this.repoConfig || this.loading) return;
+ this.repoConfig = {
+ ...this.repoConfig,
+ description: e.detail.value,
+ };
+ this.requestUpdate();
+ }
+
+ private handleStateSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig || this.loading) return;
+ this.repoConfig = {
+ ...this.repoConfig,
+ state: e.detail.value as ProjectState,
+ };
+ this.requestUpdate();
+ }
+
+ private handleSubmitTypeSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig || this.loading) return;
+ this.repoConfig = {
+ ...this.repoConfig,
+ submit_type: e.detail.value as SubmitType,
+ };
+ this.requestUpdate();
+ }
+
+ private handleContentMergeSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.use_content_merge || this.loading) return;
+ this.repoConfig.use_content_merge.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleNewChangeSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (
+ !this.repoConfig?.create_new_change_for_all_not_in_target ||
+ this.loading
+ )
+ return;
+ this.repoConfig.create_new_change_for_all_not_in_target.configured_value = e
+ .detail.value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleRequireChangeIdSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.require_change_id || this.loading) return;
+ this.repoConfig.require_change_id.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleEnableSignedPushBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.enable_signed_push || this.loading) return;
+ this.repoConfig.enable_signed_push.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleRequireSignedPushBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.require_signed_push || this.loading) return;
+ this.repoConfig.require_signed_push.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleRejectImplicitMergeSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.reject_implicit_merges || this.loading) return;
+ this.repoConfig.reject_implicit_merges.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleUnRegisteredCcSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.enable_reviewer_by_email || this.loading) return;
+ this.repoConfig.enable_reviewer_by_email.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleSetAllNewChangesPrivateByDefaultSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.private_by_default || this.loading) return;
+ this.repoConfig.private_by_default.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleSetAllNewChangesWorkInProgressByDefaultSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.work_in_progress_by_default || this.loading) return;
+ this.repoConfig.work_in_progress_by_default.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleMaxGitObjSizeBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.max_object_size_limit || this.loading) return;
+ this.repoConfig.max_object_size_limit.value = e.detail.value;
+ this.repoConfig.max_object_size_limit.configured_value = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleMatchAuthoredDateWithCommitterDateSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.match_author_to_committer_date || this.loading)
+ return;
+ this.repoConfig.match_author_to_committer_date.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleRejectEmptyCommitSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.reject_empty_commit || this.loading) return;
+ this.repoConfig.reject_empty_commit.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleUseContributorAgreementsBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.use_contributor_agreements || this.loading) return;
+ this.repoConfig.use_contributor_agreements.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleUseSignedOffBySelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.use_signed_off_by || this.loading) return;
+ this.repoConfig.use_signed_off_by.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts
deleted file mode 100644
index 71abec0..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts
+++ /dev/null
@@ -1,449 +0,0 @@
-/**
- * @license
- * 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.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- .info {
- margin-bottom: var(--spacing-xl);
- }
- h2.edited:after {
- color: var(--deemphasized-text-color);
- content: ' *';
- }
- .loading,
- .hide {
- display: none;
- }
- #loading.loading {
- display: block;
- }
- #loading:not(.loading) {
- display: none;
- }
- #options .repositorySettings {
- display: none;
- }
- #options .repositorySettings.showConfig {
- display: block;
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="main gr-form-styles read-only">
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="info">
- <h1 id="Title" class="heading-1">[[repo]]</h1>
- <hr />
- <div>
- <a href$="[[_computeBrowseUrl(weblinks)]]"
- ><gr-button link disabled="[[!_computeBrowseUrl(weblinks)]]"
- >Browse</gr-button
- ></a
- ><a href$="[[_computeChangesUrl(repo)]]"
- ><gr-button link>View Changes</gr-button></a
- >
- </div>
- </div>
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
- <h2 id="download" class="heading-2">Download</h2>
- <fieldset>
- <gr-download-commands
- id="downloadCommands"
- commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]"
- schemes="[[_schemes]]"
- selected-scheme="{{_selectedScheme}}"
- ></gr-download-commands>
- </fieldset>
- </div>
- <h2
- id="configurations"
- class$="heading-2 [[_computeHeaderClass(_configChanged)]]"
- >
- Configurations
- </h2>
- <div id="form">
- <fieldset>
- <h3 id="Description" class="heading-3">Description</h3>
- <fieldset>
- <iron-autogrow-textarea
- id="descriptionInput"
- class="description"
- autocomplete="on"
- placeholder="<Insert repo description here>"
- bind-value="{{_repoConfig.description}}"
- disabled$="[[_readOnly]]"
- ></iron-autogrow-textarea>
- </fieldset>
- <h3 id="Options" class="heading-3">Repository Options</h3>
- <fieldset id="options">
- <section>
- <span class="title">State</span>
- <span class="value">
- <gr-select id="stateSelect" bind-value="{{_repoConfig.state}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat" items="[[_states]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Submit type</span>
- <span class="value">
- <gr-select
- id="submitTypeSelect"
- bind-value="{{_repoConfig.submit_type}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatSubmitTypeSelect(_repoConfig)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Allow content merges</span>
- <span class="value">
- <gr-select
- id="contentMergeSelect"
- bind-value="{{_repoConfig.use_content_merge.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Create a new change for every commit not in the target branch
- </span>
- <span class="value">
- <gr-select
- id="newChangeSelect"
- bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Require Change-Id in commit message</span>
- <span class="value">
- <gr-select
- id="requireChangeIdSelect"
- bind-value="{{_repoConfig.require_change_id.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section
- id="enableSignedPushSettings"
- class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]"
- >
- <span class="title">Enable signed push</span>
- <span class="value">
- <gr-select
- id="enableSignedPush"
- bind-value="{{_repoConfig.enable_signed_push.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section
- id="requireSignedPushSettings"
- class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]"
- >
- <span class="title">Require signed push</span>
- <span class="value">
- <gr-select
- id="requireSignedPush"
- bind-value="{{_repoConfig.require_signed_push.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Reject implicit merges when changes are pushed for review</span
- >
- <span class="value">
- <gr-select
- id="rejectImplicitMergesSelect"
- bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Enable adding unregistered users as reviewers and CCs on
- changes</span
- >
- <span class="value">
- <gr-select
- id="unRegisteredCcSelect"
- bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title"> Set all new changes private by default</span>
- <span class="value">
- <gr-select
- id="setAllnewChangesPrivateByDefaultSelect"
- bind-value="{{_repoConfig.private_by_default.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Set new changes to "work in progress" by default</span
- >
- <span class="value">
- <gr-select
- id="setAllNewChangesWorkInProgressByDefaultSelect"
- bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Maximum Git object size limit</span>
- <span class="value">
- <iron-input
- id="maxGitObjSizeIronInput"
- bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
- type="text"
- disabled$="[[_readOnly]]"
- >
- <input
- id="maxGitObjSizeInput"
- bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
- is="iron-input"
- type="text"
- disabled$="[[_readOnly]]"
- />
- </iron-input>
- <template
- is="dom-if"
- if="[[_repoConfig.max_object_size_limit.value]]"
- >
- effective: [[_repoConfig.max_object_size_limit.value]] bytes
- </template>
- </span>
- </section>
- <section>
- <span class="title"
- >Match authored date with committer date upon submit</span
- >
- <span class="value">
- <gr-select
- id="matchAuthoredDateWithCommitterDateSelect"
- bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Reject empty commit upon submit</span>
- <span class="value">
- <gr-select
- id="rejectEmptyCommitSelect"
- bind-value="{{_repoConfig.reject_empty_commit.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- </fieldset>
- <h3 id="Options" class="heading-3">Contributor Agreements</h3>
- <fieldset id="agreements">
- <section>
- <span class="title">
- Require a valid contributor agreement to upload</span
- >
- <span class="value">
- <gr-select
- id="contributorAgreementSelect"
- bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Require Signed-off-by in commit message</span>
- <span class="value">
- <gr-select
- id="useSignedOffBySelect"
- bind-value="{{_repoConfig.use_signed_off_by.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- </fieldset>
- <div
- class$="pluginConfig [[_computeHideClass(_pluginData)]]"
- on-plugin-config-changed="_handlePluginConfigChanged"
- >
- <h3 class="heading-3">Plugins</h3>
- <template is="dom-repeat" items="[[_pluginData]]" as="data">
- <gr-repo-plugin-config
- plugin-data="[[data]]"
- ></gr-repo-plugin-config>
- </template>
- </div>
- <gr-button
- on-click="_handleSaveRepoConfig"
- disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]"
- >Save changes</gr-button
- >
- </fieldset>
- <gr-endpoint-decorator name="repo-config">
- <gr-endpoint-param
- name="repoName"
- value="[[repo]]"
- ></gr-endpoint-param>
- <gr-endpoint-param
- name="readOnly"
- value="[[_readOnly]]"
- ></gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
deleted file mode 100644
index 89ad86e..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
+++ /dev/null
@@ -1,358 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-repo.js';
-import {mockPromise} from '../../../test/test-utils.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {addListenerForTest, stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-repo');
-
-suite('gr-repo tests', () => {
- let element;
- let loggedInStub;
- let repoStub;
- const repoConf = {
- description: 'Access inherited by all other projects.',
- use_contributor_agreements: {
- value: false,
- configured_value: 'FALSE',
- },
- use_content_merge: {
- value: false,
- configured_value: 'FALSE',
- },
- use_signed_off_by: {
- value: false,
- configured_value: 'FALSE',
- },
- create_new_change_for_all_not_in_target: {
- value: false,
- configured_value: 'FALSE',
- },
- require_change_id: {
- value: false,
- configured_value: 'FALSE',
- },
- enable_signed_push: {
- value: false,
- configured_value: 'FALSE',
- },
- require_signed_push: {
- value: false,
- configured_value: 'FALSE',
- },
- reject_implicit_merges: {
- value: false,
- configured_value: 'FALSE',
- },
- private_by_default: {
- value: false,
- configured_value: 'FALSE',
- },
- match_author_to_committer_date: {
- value: false,
- configured_value: 'FALSE',
- },
- reject_empty_commit: {
- value: false,
- configured_value: 'FALSE',
- },
- enable_reviewer_by_email: {
- value: false,
- configured_value: 'FALSE',
- },
- max_object_size_limit: {},
- submit_type: 'MERGE_IF_NECESSARY',
- default_submit_type: {
- value: 'MERGE_IF_NECESSARY',
- configured_value: 'INHERIT',
- inherited_value: 'MERGE_IF_NECESSARY',
- },
- };
-
- const REPO = 'test-repo';
- const SCHEMES = {http: {}, repo: {}, ssh: {}};
-
- function getFormFields() {
- const selects = Array.from(
- element.root.querySelectorAll('select'));
- const textareas = Array.from(
- element.root.querySelectorAll('iron-autogrow-textarea'));
- const inputs = Array.from(
- element.root.querySelectorAll('input'));
- return inputs.concat(textareas).concat(selects);
- }
-
- setup(() => {
- loggedInStub = stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- stubRestApi('getConfig').returns(Promise.resolve({download: {}}));
- repoStub =
- stubRestApi('getProjectConfig').returns(Promise.resolve(repoConf));
- element = basicFixture.instantiate();
- });
-
- test('_computePluginData', () => {
- assert.deepEqual(element._computePluginData(), []);
- assert.deepEqual(element._computePluginData({}), []);
- assert.deepEqual(element._computePluginData({base: {}}), []);
- assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}),
- [{name: 'plugin', config: 'data'}]);
- });
-
- test('_handlePluginConfigChanged', () => {
- const notifyStub = sinon.stub(element, 'notifyPath');
- element._repoConfig = {plugin_config: {}};
- element._handlePluginConfigChanged({detail: {
- name: 'test',
- config: 'data',
- notifyPath: 'path',
- }});
- flush();
-
- assert.equal(element._repoConfig.plugin_config.test, 'data');
- assert.equal(notifyStub.lastCall.args[0],
- '_repoConfig.plugin_config.path');
- });
-
- test('loading displays before repo config is loaded', () => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
- assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
- assert.isTrue(element.$.loadedContent.classList.contains('loading'));
- assert.isTrue(getComputedStyle(element.$.loadedContent)
- .display === 'none');
- });
-
- test('download commands visibility', () => {
- element._loading = false;
- flush();
- assert.isTrue(element.$.downloadContent.classList.contains('hide'));
- assert.isTrue(getComputedStyle(element.$.downloadContent)
- .display == 'none');
- element._schemesObj = SCHEMES;
- flush();
- assert.isFalse(element.$.downloadContent.classList.contains('hide'));
- assert.isFalse(getComputedStyle(element.$.downloadContent)
- .display == 'none');
- });
-
- test('form defaults to read only', () => {
- assert.isTrue(element._readOnly);
- });
-
- test('form defaults to read only when not logged in', async () => {
- element.repo = REPO;
- await element._loadRepo();
- assert.isTrue(element._readOnly);
- });
-
- test('form defaults to read only when logged in and not admin', async () => {
- element.repo = REPO;
- stubRestApi('getRepoAccess')
- .callsFake(() => Promise.resolve({'test-repo': {}}));
- await element._loadRepo();
- assert.isTrue(element._readOnly);
- });
-
- test('all form elements are disabled when not admin', async () => {
- element.repo = REPO;
- await element._loadRepo();
- flush();
- const formFields = getFormFields();
- for (const field of formFields) {
- assert.isTrue(field.hasAttribute('disabled'));
- }
- });
-
- test('_formatBooleanSelect', () => {
- let item = {inherited_value: true};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit (true)',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
-
- item = {inherited_value: false};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit (false)',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
-
- // For items without inherited values
- item = {};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
- });
-
- test('fires page-error', async () => {
- repoStub.restore();
-
- element.repo = 'test';
-
- const pageErrorFired = mockPromise();
- const response = {status: 404};
- stubRestApi('getProjectConfig').callsFake((repo, errFn) => {
- errFn(response);
- return Promise.resolve(undefined);
- });
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- pageErrorFired.resolve();
- });
-
- element._loadRepo();
- await pageErrorFired;
- });
-
- suite('admin', () => {
- setup(() => {
- element.repo = REPO;
- loggedInStub.returns(Promise.resolve(true));
- stubRestApi('getRepoAccess')
- .returns(Promise.resolve({'test-repo': {is_owner: true}}));
- });
-
- test('all form elements are enabled', async () => {
- await element._loadRepo();
- await flush();
- const formFields = getFormFields();
- for (const field of formFields) {
- assert.isFalse(field.hasAttribute('disabled'));
- }
- assert.isFalse(element._loading);
- });
-
- test('state gets set correctly', async () => {
- await element._loadRepo();
- assert.equal(element._repoConfig.state, 'ACTIVE');
- assert.equal(element.$.stateSelect.bindValue, 'ACTIVE');
- });
-
- test('inherited submit type value is calculated correctly', async () => {
- await element._loadRepo();
- const sel = element.$.submitTypeSelect;
- assert.equal(sel.bindValue, 'INHERIT');
- assert.equal(
- sel.nativeSelect.options[0].text,
- 'Inherit (Merge if necessary)'
- );
- });
-
- test('fields update and save correctly', async () => {
- const configInputObj = {
- description: 'new description',
- use_contributor_agreements: 'TRUE',
- use_content_merge: 'TRUE',
- use_signed_off_by: 'TRUE',
- create_new_change_for_all_not_in_target: 'TRUE',
- require_change_id: 'TRUE',
- enable_signed_push: 'TRUE',
- require_signed_push: 'TRUE',
- reject_implicit_merges: 'TRUE',
- private_by_default: 'TRUE',
- match_author_to_committer_date: 'TRUE',
- reject_empty_commit: 'TRUE',
- max_object_size_limit: 10,
- submit_type: 'FAST_FORWARD_ONLY',
- state: 'READ_ONLY',
- enable_reviewer_by_email: 'TRUE',
- };
-
- const saveStub = stubRestApi('saveRepoConfig')
- .callsFake(() => Promise.resolve({}));
-
- const button = element.root.querySelectorAll('gr-button')[2];
-
- await element._loadRepo();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- element.$.descriptionInput.bindValue = configInputObj.description;
- element.$.stateSelect.bindValue = configInputObj.state;
- element.$.submitTypeSelect.bindValue = configInputObj.submit_type;
- element.$.contentMergeSelect.bindValue =
- configInputObj.use_content_merge;
- element.$.newChangeSelect.bindValue =
- configInputObj.create_new_change_for_all_not_in_target;
- element.$.requireChangeIdSelect.bindValue =
- configInputObj.require_change_id;
- element.$.enableSignedPush.bindValue =
- configInputObj.enable_signed_push;
- element.$.requireSignedPush.bindValue =
- configInputObj.require_signed_push;
- element.$.rejectImplicitMergesSelect.bindValue =
- configInputObj.reject_implicit_merges;
- element.$.setAllnewChangesPrivateByDefaultSelect.bindValue =
- configInputObj.private_by_default;
- element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
- configInputObj.match_author_to_committer_date;
- const inputElement = PolymerElement ?
- element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
- inputElement.bindValue = configInputObj.max_object_size_limit;
- element.$.contributorAgreementSelect.bindValue =
- configInputObj.use_contributor_agreements;
- element.$.useSignedOffBySelect.bindValue =
- configInputObj.use_signed_off_by;
- element.$.rejectEmptyCommitSelect.bindValue =
- configInputObj.reject_empty_commit;
- element.$.unRegisteredCcSelect.bindValue =
- configInputObj.enable_reviewer_by_email;
-
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.configurations.classList.contains('edited'));
-
- const formattedObj =
- element._formatRepoConfigForSave(element._repoConfig);
- assert.deepEqual(formattedObj, configInputObj);
-
- await element._handleSaveRepoConfig();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.isTrue(saveStub.lastCall.calledWithExactly(REPO,
- configInputObj));
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
new file mode 100644
index 0000000..82338d3
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
@@ -0,0 +1,570 @@
+/**
+ * @license
+ * Copyright (C) 2017 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.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-repo';
+import {GrRepo} from './gr-repo';
+import {mockPromise} from '../../../test/test-utils';
+import {
+ addListenerForTest,
+ queryAll,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {
+ createInheritedBoolean,
+ createServerInfo,
+} from '../../../test/test-data-generators';
+import {
+ ConfigInfo,
+ GitRef,
+ GroupId,
+ GroupName,
+ InheritedBooleanInfo,
+ MaxObjectSizeLimitInfo,
+ PluginParameterToConfigParameterInfoMap,
+ ProjectAccessGroups,
+ ProjectAccessInfoMap,
+ RepoName,
+} from '../../../types/common';
+import {
+ ConfigParameterInfoType,
+ InheritedBooleanInfoConfiguredValue,
+ ProjectState,
+ SubmitType,
+} from '../../../constants/constants';
+import {
+ createConfig,
+ createDownloadSchemes,
+} from '../../../test/test-data-generators';
+import {PageErrorEvent} from '../../../types/events.js';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrSelect} from '../../shared/gr-select/gr-select';
+import {GrTextarea} from '../../shared/gr-textarea/gr-textarea';
+import {IronInputElement} from '@polymer/iron-input/iron-input';
+
+const basicFixture = fixtureFromElement('gr-repo');
+
+suite('gr-repo tests', () => {
+ let element: GrRepo;
+ let loggedInStub: sinon.SinonStub;
+ let repoStub: sinon.SinonStub;
+
+ const repoConf: ConfigInfo = {
+ description: 'Access inherited by all other projects.',
+ use_contributor_agreements: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ use_content_merge: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ use_signed_off_by: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ create_new_change_for_all_not_in_target: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ require_change_id: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ enable_signed_push: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ require_signed_push: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ reject_implicit_merges: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ match_author_to_committer_date: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ reject_empty_commit: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ enable_reviewer_by_email: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ private_by_default: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ work_in_progress_by_default: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ max_object_size_limit: {},
+ commentlinks: {},
+ submit_type: SubmitType.MERGE_IF_NECESSARY,
+ default_submit_type: {
+ value: SubmitType.MERGE_IF_NECESSARY,
+ configured_value: SubmitType.INHERIT,
+ inherited_value: SubmitType.MERGE_IF_NECESSARY,
+ },
+ };
+
+ const REPO = 'test-repo';
+ const SCHEMES = {
+ ...createDownloadSchemes(),
+ http: {
+ url: 'test',
+ is_auth_required: false,
+ is_auth_supported: false,
+ commands: 'test',
+ clone_commands: {clone: 'test'},
+ },
+ repo: {
+ url: 'test',
+ is_auth_required: false,
+ is_auth_supported: false,
+ commands: 'test',
+ clone_commands: {clone: 'test'},
+ },
+ ssh: {
+ url: 'test',
+ is_auth_required: false,
+ is_auth_supported: false,
+ commands: 'test',
+ clone_commands: {clone: 'test'},
+ },
+ };
+
+ function getFormFields() {
+ const selects = Array.from(queryAll(element, 'select'));
+ const textareas = Array.from(queryAll(element, 'iron-autogrow-textarea'));
+ const inputs = Array.from(queryAll(element, 'input'));
+ return inputs.concat(textareas).concat(selects);
+ }
+
+ setup(async () => {
+ loggedInStub = stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+ stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
+ repoStub = stubRestApi('getProjectConfig').returns(
+ Promise.resolve(repoConf)
+ );
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ test('_computePluginData', async () => {
+ element.repoConfig = {
+ ...createConfig(),
+ plugin_config: {},
+ };
+ await element.updateComplete;
+ assert.deepEqual(element.computePluginData(), []);
+
+ element.repoConfig.plugin_config = {
+ 'test-plugin': {
+ test: {display_name: 'test plugin', type: 'STRING'},
+ } as PluginParameterToConfigParameterInfoMap,
+ };
+ await element.updateComplete;
+ assert.deepEqual(element.computePluginData(), [
+ {
+ name: 'test-plugin',
+ config: {
+ test: {
+ display_name: 'test plugin',
+ type: 'STRING' as ConfigParameterInfoType,
+ },
+ },
+ },
+ ]);
+ });
+
+ test('handlePluginConfigChanged', async () => {
+ const requestUpdateStub = sinon.stub(element, 'requestUpdate');
+ element.repoConfig = {
+ ...createConfig(),
+ plugin_config: {},
+ };
+ element.handlePluginConfigChanged({
+ detail: {
+ name: 'test',
+ config: {
+ test: {display_name: 'test plugin', type: 'STRING'},
+ } as PluginParameterToConfigParameterInfoMap,
+ },
+ });
+ await element.updateComplete;
+
+ assert.deepEqual(element.repoConfig!.plugin_config!.test, {
+ test: {display_name: 'test plugin', type: 'STRING'},
+ } as PluginParameterToConfigParameterInfoMap);
+ assert.isTrue(requestUpdateStub.called);
+ });
+
+ test('loading displays before repo config is loaded', () => {
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+ 'loading'
+ )
+ );
+ assert.isFalse(
+ getComputedStyle(queryAndAssert<HTMLDivElement>(element, '#loading'))
+ .display === 'none'
+ );
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#loadedContent'
+ ).classList.contains('loading')
+ );
+ assert.isTrue(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#loadedContent')
+ ).display === 'none'
+ );
+ });
+
+ test('download commands visibility', async () => {
+ element.loading = false;
+ await element.updateComplete;
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#downloadContent'
+ ).classList.contains('hide')
+ );
+ assert.isTrue(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#downloadContent')
+ ).display === 'none'
+ );
+ element.schemesObj = SCHEMES;
+ await element.updateComplete;
+ assert.isFalse(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#downloadContent'
+ ).classList.contains('hide')
+ );
+ assert.isFalse(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#downloadContent')
+ ).display === 'none'
+ );
+ });
+
+ test('form defaults to read only', () => {
+ assert.isTrue(element.readOnly);
+ });
+
+ test('form defaults to read only when not logged in', async () => {
+ element.repo = REPO as RepoName;
+ await element.loadRepo();
+ assert.isTrue(element.readOnly);
+ });
+
+ test('form defaults to read only when logged in and not admin', async () => {
+ element.repo = REPO as RepoName;
+
+ stubRestApi('getRepoAccess').callsFake(() =>
+ Promise.resolve({
+ 'test-repo': {
+ revision: 'xxxx',
+ local: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {xxx: {action: 'ALLOW', force: false}}},
+ },
+ },
+ },
+ owner_of: ['refs/*'] as GitRef[],
+ groups: {
+ xxxx: {
+ id: 'xxxx' as GroupId,
+ url: 'test',
+ name: 'test' as GroupName,
+ },
+ } as ProjectAccessGroups,
+ config_web_links: [{name: 'gitiles', url: 'test'}],
+ },
+ } as ProjectAccessInfoMap)
+ );
+ await element.loadRepo();
+ assert.isTrue(element.readOnly);
+ });
+
+ test('all form elements are disabled when not admin', async () => {
+ element.repo = REPO as RepoName;
+ await element.loadRepo();
+ await element.updateComplete;
+ const formFields = getFormFields();
+ for (const field of formFields) {
+ assert.isTrue(field.hasAttribute('disabled'));
+ }
+ });
+
+ test('formatBooleanSelect', () => {
+ let item: InheritedBooleanInfo = {
+ ...createInheritedBoolean(true),
+ inherited_value: true,
+ };
+ assert.deepEqual(element.formatBooleanSelect(item), [
+ {
+ label: 'Inherit (true)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ },
+ {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
+ item = {...createInheritedBoolean(false), inherited_value: false};
+ assert.deepEqual(element.formatBooleanSelect(item), [
+ {
+ label: 'Inherit (false)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ },
+ {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
+ // For items without inherited values
+ item = createInheritedBoolean(false);
+ assert.deepEqual(element.formatBooleanSelect(item), [
+ {
+ label: 'Inherit',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ },
+ {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+ });
+
+ test('fires page-error', async () => {
+ repoStub.restore();
+
+ element.repo = 'test' as RepoName;
+
+ const pageErrorFired = mockPromise();
+ const response = {...new Response(), status: 404};
+ stubRestApi('getProjectConfig').callsFake((_, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ }
+ return Promise.resolve(undefined);
+ });
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as PageErrorEvent).detail.response, response);
+ pageErrorFired.resolve();
+ });
+
+ element.loadRepo();
+ await pageErrorFired;
+ });
+
+ suite('admin', () => {
+ setup(() => {
+ element.repo = REPO as RepoName;
+ loggedInStub.returns(Promise.resolve(true));
+ stubRestApi('getRepoAccess').callsFake(() =>
+ Promise.resolve({
+ 'test-repo': {
+ revision: 'xxxx',
+ local: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {xxx: {action: 'ALLOW', force: false}}},
+ },
+ },
+ },
+ is_owner: true,
+ owner_of: ['refs/*'] as GitRef[],
+ groups: {
+ xxxx: {
+ id: 'xxxx' as GroupId,
+ url: 'test',
+ name: 'test' as GroupName,
+ },
+ } as ProjectAccessGroups,
+ config_web_links: [{name: 'gitiles', url: 'test'}],
+ },
+ } as ProjectAccessInfoMap)
+ );
+ });
+
+ test('all form elements are enabled', async () => {
+ await element.loadRepo();
+ await element.updateComplete;
+ const formFields = getFormFields();
+ for (const field of formFields) {
+ assert.isFalse(field.hasAttribute('disabled'));
+ }
+ assert.isFalse(element.loading);
+ });
+
+ test('state gets set correctly', async () => {
+ await element.loadRepo();
+ assert.equal(element.repoConfig!.state, ProjectState.ACTIVE);
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#stateSelect').bindValue,
+ ProjectState.ACTIVE
+ );
+ });
+
+ test('inherited submit type value is calculated correctly', async () => {
+ await element.loadRepo();
+ const sel = queryAndAssert<GrSelect>(element, '#submitTypeSelect');
+ assert.equal(sel.bindValue, 'INHERIT');
+ assert.equal(
+ sel.nativeSelect.options[0].text,
+ 'Inherit (Merge if necessary)'
+ );
+ });
+
+ test('fields update and save correctly', async () => {
+ const configInputObj = {
+ description: 'new description',
+ use_contributor_agreements: InheritedBooleanInfoConfiguredValue.TRUE,
+ use_content_merge: InheritedBooleanInfoConfiguredValue.TRUE,
+ use_signed_off_by: InheritedBooleanInfoConfiguredValue.TRUE,
+ create_new_change_for_all_not_in_target:
+ InheritedBooleanInfoConfiguredValue.TRUE,
+ require_change_id: InheritedBooleanInfoConfiguredValue.TRUE,
+ enable_signed_push: InheritedBooleanInfoConfiguredValue.TRUE,
+ require_signed_push: InheritedBooleanInfoConfiguredValue.TRUE,
+ reject_implicit_merges: InheritedBooleanInfoConfiguredValue.TRUE,
+ private_by_default: InheritedBooleanInfoConfiguredValue.TRUE,
+ work_in_progress_by_default: InheritedBooleanInfoConfiguredValue.TRUE,
+ match_author_to_committer_date:
+ InheritedBooleanInfoConfiguredValue.TRUE,
+ reject_empty_commit: InheritedBooleanInfoConfiguredValue.TRUE,
+ max_object_size_limit: '10' as MaxObjectSizeLimitInfo,
+ submit_type: SubmitType.FAST_FORWARD_ONLY,
+ state: ProjectState.READ_ONLY,
+ enable_reviewer_by_email: InheritedBooleanInfoConfiguredValue.TRUE,
+ };
+
+ const saveStub = stubRestApi('saveRepoConfig').callsFake(() =>
+ Promise.resolve(new Response())
+ );
+
+ const button = queryAll<GrButton>(element, 'gr-button')[2];
+
+ await element.loadRepo();
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#Title'
+ ).classList.contains('edited')
+ );
+ queryAndAssert<GrTextarea>(element, '#descriptionInput').text =
+ configInputObj.description;
+ queryAndAssert<GrSelect>(element, '#stateSelect').bindValue =
+ configInputObj.state;
+ queryAndAssert<GrSelect>(element, '#submitTypeSelect').bindValue =
+ configInputObj.submit_type;
+ queryAndAssert<GrSelect>(element, '#contentMergeSelect').bindValue =
+ configInputObj.use_content_merge;
+ queryAndAssert<GrSelect>(element, '#newChangeSelect').bindValue =
+ configInputObj.create_new_change_for_all_not_in_target;
+ queryAndAssert<GrSelect>(element, '#requireChangeIdSelect').bindValue =
+ configInputObj.require_change_id;
+ queryAndAssert<GrSelect>(element, '#enableSignedPush').bindValue =
+ configInputObj.enable_signed_push;
+ queryAndAssert<GrSelect>(element, '#requireSignedPush').bindValue =
+ configInputObj.require_signed_push;
+ queryAndAssert<GrSelect>(
+ element,
+ '#rejectImplicitMergesSelect'
+ ).bindValue = configInputObj.reject_implicit_merges;
+ queryAndAssert<GrSelect>(
+ element,
+ '#setAllnewChangesPrivateByDefaultSelect'
+ ).bindValue = configInputObj.private_by_default;
+ queryAndAssert<GrSelect>(
+ element,
+ '#setAllNewChangesWorkInProgressByDefaultSelect'
+ ).bindValue = configInputObj.work_in_progress_by_default;
+ queryAndAssert<GrSelect>(
+ element,
+ '#matchAuthoredDateWithCommitterDateSelect'
+ ).bindValue = configInputObj.match_author_to_committer_date;
+ queryAndAssert<IronInputElement>(
+ element,
+ '#maxGitObjSizeIronInput'
+ ).bindValue = String(configInputObj.max_object_size_limit);
+ queryAndAssert<GrSelect>(
+ element,
+ '#contributorAgreementSelect'
+ ).bindValue = configInputObj.use_contributor_agreements;
+ queryAndAssert<GrSelect>(element, '#useSignedOffBySelect').bindValue =
+ configInputObj.use_signed_off_by;
+ queryAndAssert<GrSelect>(element, '#rejectEmptyCommitSelect').bindValue =
+ configInputObj.reject_empty_commit;
+ queryAndAssert<GrSelect>(element, '#unRegisteredCcSelect').bindValue =
+ configInputObj.enable_reviewer_by_email;
+
+ await element.updateComplete;
+
+ assert.isFalse(button.hasAttribute('disabled'));
+ assert.isTrue(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#configurations'
+ ).classList.contains('edited')
+ );
+
+ const formattedObj = element.formatRepoConfigForSave(element.repoConfig);
+ assert.deepEqual(formattedObj, configInputObj);
+
+ await element.handleSaveRepoConfig();
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#Title'
+ ).classList.contains('edited')
+ );
+ assert.isTrue(
+ saveStub.lastCall.calledWithExactly(REPO as RepoName, configInputObj)
+ );
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 58090e1..f73b703 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -40,65 +40,7 @@
//
// Each object has a `view` property with a value from GerritNav.View. The
// remaining properties depend on the value used for view.
-//
-// - GerritNav.View.CHANGE:
-// - `changeNum`, required, String: the numeric ID of the change.
-// - `project`, optional, String: the project name.
-// - `patchNum`, optional, Number: the patch for the right-hand-side of
-// the diff.
-// - `basePatchNum`, optional, Number: the patch for the left-hand-side
-// of the diff. If `basePatchNum` is provided, then `patchNum` must
-// also be provided.
-// - `edit`, optional, Boolean: whether or not to load the file list with
-// edit controls.
-// - `messageHash`, optional, String: the hash of the change message to
-// scroll to.
-//
-// - GerritNav.View.SEARCH:
-// - `query`, optional, String: the literal search query. If provided,
-// the string will be used as the query, and all other params will be
-// ignored.
-// - `owner`, optional, String: the owner name.
-// - `project`, optional, String: the project name.
-// - `branch`, optional, String: the branch name.
-// - `topic`, optional, String: the topic name.
-// - `hashtag`, optional, String: the hashtag name.
-// - `statuses`, optional, Array<String>: the list of change statuses to
-// search for. If more than one is provided, the search will OR them
-// together.
-// - `offset`, optional, Number: the offset for the query.
-//
-// - GerritNav.View.DIFF:
-// - `changeNum`, required, String: the numeric ID of the change.
-// - `path`, required, String: the filepath of the diff.
-// - `patchNum`, required, Number: the patch for the right-hand-side of
-// the diff.
-// - `basePatchNum`, optional, Number: the patch for the left-hand-side
-// of the diff. If `basePatchNum` is provided, then `patchNum` must
-// also be provided.
-// - `lineNum`, optional, Number: the line number to be selected on load.
-// - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value
-// of true selects the line from base of the patch range. False by
-// default.
-//
-// - GerritNav.View.GROUP:
-// - `groupId`, required, String: the ID of the group.
-// - `detail`, optional, String: the name of the group detail view.
-// Takes any value from GerritNav.GroupDetailView.
-//
-// - GerritNav.View.REPO:
-// - `repoName`, required, String: the name of the repo
-// - `detail`, optional, String: the name of the repo detail view.
-// Takes any value from GerritNav.RepoDetailView.
-//
-// - GerritNav.View.DASHBOARD
-// - `repo`, optional, String.
-// - `sections`, optional, Array of objects with `title` and `query`
-// strings.
-// - `user`, optional, String.
-//
-// - GerritNav.View.ROOT:
-// - no possible parameters.
+// GenerateUrlParameters lists all the possible view parameters.
const uninitialized = () => {
console.warn('Use of uninitialized routing');
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
index 6a73d8d..f5a28f8 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.ts
@@ -122,7 +122,7 @@
return Promise.resolve([]);
}
return this.restApiService
- .getSuggestedGroups(expression, MAX_AUTOCOMPLETE_RESULTS)
+ .getSuggestedGroups(expression, undefined, MAX_AUTOCOMPLETE_RESULTS)
.then(groups => {
if (!groups) {
return [];
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
index 958f367..89ab885 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -222,11 +222,16 @@
return result;
}
- moveToLineNumber(number: number, side: Side, path?: string) {
+ moveToLineNumber(
+ number: number,
+ side: Side,
+ path?: string,
+ intentionalMove?: boolean
+ ) {
const row = this._findRowByNumberAndFile(number, side, path);
if (row) {
this.side = side;
- this.cursorManager.setCursor(row);
+ this.cursorManager.setCursor(row, undefined, intentionalMove);
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index 9f65dd4..81b6fd4 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -221,8 +221,10 @@
*
* @param noScroll prevent any potential scrolling in response
* setting the cursor.
+ * @param applyFocus indicates if it should try to focus after move operation
+ * (e.g. focusOnMove).
*/
- setCursor(element: HTMLElement, noScroll?: boolean) {
+ setCursor(element: HTMLElement, noScroll?: boolean, applyFocus?: boolean) {
if (!this.targetableStops.includes(element)) {
this.unsetCursor();
return;
@@ -238,6 +240,9 @@
this._updateIndex();
this._decorateTarget();
+ if (applyFocus) {
+ this._focusAfterMove();
+ }
if (noScroll && behavior) {
this.scrollMode = behavior;
}
@@ -341,15 +346,17 @@
this._targetHeight = this.target.scrollHeight;
}
- if (this.focusOnMove) {
- this.target.focus();
- }
-
this._decorateTarget();
-
+ this._focusAfterMove();
return clipped ? CursorMoveResult.CLIPPED : CursorMoveResult.MOVED;
}
+ _focusAfterMove() {
+ if (this.focusOnMove) {
+ this.target?.focus();
+ }
+ }
+
_decorateTarget() {
if (this.target && this.cursorTargetClass) {
this.target.classList.add(this.cursorTargetClass);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index c2b0269..9890bb2 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -238,6 +238,7 @@
[paramName: string]: string | undefined | null | number;
s: string;
n?: number;
+ p?: string;
}
interface QuerySuggestedReviewersParams {
@@ -1589,12 +1590,16 @@
getSuggestedGroups(
inputVal: string,
+ project?: RepoName,
n?: number
): Promise<GroupNameToGroupInfoMap | undefined> {
const params: QueryGroupsParams = {s: inputVal};
if (n) {
params.n = n;
}
+ if (project) {
+ params.p = encodeURIComponent(project);
+ }
return this._restApiHelper.fetchJSON({
url: '/groups/',
params,
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 1378211..445e932 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
@@ -180,6 +180,7 @@
): Promise<AccountInfo[] | undefined>;
getSuggestedGroups(
input: string,
+ project?: RepoName,
n?: number
): Promise<GroupNameToGroupInfoMap | undefined>;
executeChangeAction(
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 91cd2f3..0fd3550 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -160,6 +160,7 @@
work_in_progress_by_default: createInheritedBoolean(),
max_object_size_limit: createMaxObjectSizeLimit(),
default_submit_type: createSubmitType(),
+ enable_reviewer_by_email: createInheritedBoolean(),
submit_type: SubmitType.INHERIT,
commentlinks: createCommentLinks(),
};
diff --git a/polygerrit-ui/app/utils/object-util.ts b/polygerrit-ui/app/utils/object-util.ts
new file mode 100644
index 0000000..95676c5
--- /dev/null
+++ b/polygerrit-ui/app/utils/object-util.ts
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright (C) 2021 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.
+ */
+
+/**
+ * @param obj Object
+ */
+export function deepClone(obj?: object) {
+ if (!obj) return undefined;
+ return JSON.parse(JSON.stringify(obj));
+}