Merge "Revert "Set CommitInfo.commit in ChangeJson#toCommit""
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index ca5d460..48ed355 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2291,6 +2291,7 @@
{
"reviewers": [
{
+ "input": "john.doe@example.com",
"approvals": {
"Verified": " 0",
"Code-Review": " 0"
@@ -2328,6 +2329,7 @@
)]}'
{
+ "input": "MyProjectVerifiers",
"error": "The group My Group has 15 members. Do you want to add them all as reviewers?",
"confirm": true
}
@@ -2342,7 +2344,7 @@
Content-Type: application/json; charset=UTF-8
{
- "reviewer": "MyProjectVerifiers",
+ "input": "MyProjectVerifiers",
"confirmed": true
}
----
@@ -4051,6 +4053,9 @@
[options="header",cols="1,^1,5"]
|===========================
|Field Name ||Description
+|`input` ||
+Value of the `reviewer` field from link:#reviewer-input[ReviewerInput]
+set while adding the reviewer.
|`reviewers` |optional|
The newly added reviewers as a list of link:#reviewer-info[
ReviewerInfo] entities.
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
new file mode 100644
index 0000000..0bd70ef
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.server.change.PostReviewers;
+import com.google.gson.stream.JsonReader;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ChangeReviewersIT extends AbstractDaemonTest {
+ @Test
+ public void addGroupAsReviewer() throws Exception {
+ // Set up two groups, one that is too large too add as reviewer, and one
+ // that is too large to add without confirmation.
+ String largeGroup = createGroup("largeGroup");
+ String mediumGroup = createGroup("mediumGroup");
+
+ int largeGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS + 1;
+ int mediumGroupSize = PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
+ List<TestAccount> users = new ArrayList<>(largeGroupSize);
+ List<String> largeGroupUsernames = new ArrayList<>(mediumGroupSize);
+ List<String> mediumGroupUsernames = new ArrayList<>(mediumGroupSize);
+ for (int i = 0; i < largeGroupSize; i++) {
+ users.add(accounts.create(name("u" + i), name("u" + i + "@example.com"),
+ "Full Name " + i));
+ largeGroupUsernames.add(users.get(i).username);
+ if (i < mediumGroupSize) {
+ mediumGroupUsernames.add(users.get(i).username);
+ }
+ }
+ gApi.groups().id(largeGroup).addMembers(
+ largeGroupUsernames.toArray(new String[largeGroupSize]));
+ gApi.groups().id(mediumGroup).addMembers(
+ mediumGroupUsernames.toArray(new String[mediumGroupSize]));
+
+ // Attempt to add overly large group as reviewers.
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ AddReviewerResult result = addReviewer(changeId, largeGroup);
+ assertThat(result.input).isEqualTo(largeGroup);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error)
+ .contains("has too many members to add them all as reviewers");
+ assertThat(result.reviewers).isNull();
+
+ // Attempt to add medium group without confirmation.
+ result = addReviewer(changeId, mediumGroup);
+ assertThat(result.input).isEqualTo(mediumGroup);
+ assertThat(result.confirm).isTrue();
+ assertThat(result.error)
+ .contains("has " + mediumGroupSize + " members. Do you want to add them"
+ + " all as reviewers?");
+ assertThat(result.reviewers).isNull();
+
+ // Add medium group with confirmation.
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = mediumGroup;
+ in.confirmed = true;
+ result = addReviewer(changeId, in);
+ assertThat(result.input).isEqualTo(mediumGroup);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error).isNull();
+ assertThat(result.reviewers).hasSize(mediumGroupSize);
+ }
+
+ private AddReviewerResult addReviewer(String changeId, String reviewer)
+ throws Exception {
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = reviewer;
+ return addReviewer(changeId, in);
+ }
+
+ private AddReviewerResult addReviewer(String changeId, AddReviewerInput in)
+ throws Exception {
+ RestResponse resp =
+ adminRestSession.post("/changes/" + changeId + "/reviewers", in);
+ return readContentFromJson(resp, AddReviewerResult.class);
+ }
+
+ private static <T> T readContentFromJson(RestResponse r, Class<T> clazz)
+ throws Exception {
+ r.assertOK();
+ JsonReader jsonReader = new JsonReader(r.getReader());
+ jsonReader.setLenient(true);
+ return newGson().fromJson(jsonReader, clazz);
+ }
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java
new file mode 100644
index 0000000..3bd93a4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerResult.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.common.Nullable;
+
+import java.util.List;
+
+/**
+ * Result object representing the outcome of a request to add a reviewer.
+ */
+public class AddReviewerResult {
+ /**
+ * The identifier of an account or group that was to be added as a reviewer.
+ */
+ public String input;
+
+ /**
+ * If non-null, a string describing why the reviewer could not be added.
+ */
+ @Nullable
+ public String error;
+
+ /**
+ * Non-null and true if the reviewer cannot be added without explicit
+ * confirmation. This may be the case for groups of a certain size.
+ */
+ @Nullable
+ public Boolean confirm;
+
+ /**
+ * @{List} of individual reviewers added to the change. The size of this list
+ * may be greater than one (e.g. when a group is added). Null if no reviewers
+ * were added.
+ */
+ @Nullable
+ public List<ReviewerInfo> reviewers;
+
+ /**
+ * Constructs a partially initialized result for the given reviewer.
+ *
+ * @param input String identifier of an account or group, from user request
+ */
+ public AddReviewerResult(String input) {
+ this.input = input;
+ }
+
+ /**
+ * Constructs an error result for the given account.
+ *
+ * @param reviewer String identifier of an account or group
+ * @param error Error message
+ */
+ public AddReviewerResult(String reviewer, String error) {
+ this(reviewer);
+ this.error = error;
+ }
+
+ /**
+ * Constructs a needs-confirmation result for the given account.
+ *
+ * @param confirm Whether confirmation is needed.
+ */
+ public AddReviewerResult(String reviewer, boolean confirm) {
+ this(reviewer);
+ this.confirm = confirm;
+ }
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
new file mode 100644
index 0000000..c81f8aa
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+import java.util.Map;
+
+/**
+ * Account and approval details for an added reviewer.
+ */
+public class ReviewerInfo extends AccountInfo {
+ /**
+ * {@link Map} of label name to initial value for each approval the reviewer
+ * is responsible for.
+ */
+ @Nullable
+ public Map<String, String> approvals;
+
+ public ReviewerInfo(Integer id) {
+ super(id);
+ }
+
+ @Override
+ public String toString() {
+ return username;
+ }
+}
\ No newline at end of file
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
index 06245c6..f4ff9ea4 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -115,7 +115,8 @@
@Override
public int compare(String s1, String s2) {
return Integer.compare(s2.length(), s1.length());
- }});
+ }
+ });
List<String> result = new ArrayList<>();
for (String s : queryTerms) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
index c90b3bc..533468d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.change;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
index 9a0c691..ccbd552 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
@@ -14,11 +14,11 @@
package com.google.gerrit.server.change;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 432ae4d..5090f8a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -72,7 +72,6 @@
// Injected fields.
private final PatchSetInfoFactory patchSetInfoFactory;
- private final ReviewDb db;
private final CommitValidators.Factory commitValidatorsFactory;
private final ReplacePatchSetSender.Factory replacePatchSetFactory;
private final RevisionCreated revisionCreated;
@@ -109,8 +108,7 @@
private ReviewerSet oldReviewers;
@AssistedInject
- public PatchSetInserter(ReviewDb db,
- ApprovalsUtil approvalsUtil,
+ public PatchSetInserter(ApprovalsUtil approvalsUtil,
ApprovalCopier approvalCopier,
ChangeMessagesUtil cmUtil,
PatchSetInfoFactory patchSetInfoFactory,
@@ -121,7 +119,6 @@
@Assisted ChangeControl ctl,
@Assisted PatchSet.Id psId,
@Assisted RevCommit commit) {
- this.db = db;
this.approvalsUtil = approvalsUtil;
this.approvalCopier = approvalCopier;
this.cmUtil = cmUtil;
@@ -208,6 +205,7 @@
@Override
public boolean updateChange(ChangeContext ctx)
throws ResourceConflictException, OrmException, IOException {
+ ReviewDb db = ctx.getDb();
ChangeControl ctl = ctx.getControl();
change = ctx.getChange();
@@ -222,12 +220,12 @@
List<String> newGroups = groups;
if (newGroups.isEmpty()) {
- PatchSet prevPs = psUtil.current(ctx.getDb(), ctx.getNotes());
+ PatchSet prevPs = psUtil.current(db, ctx.getNotes());
if (prevPs != null) {
newGroups = prevPs.getGroups();
}
}
- patchSet = psUtil.insert(ctx.getDb(), ctx.getRevWalk(), ctx.getUpdate(psId),
+ patchSet = psUtil.insert(db, ctx.getRevWalk(), ctx.getUpdate(psId),
psId, commit, draft, newGroups, null);
if (sendMail) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index fabedd7..4b15fce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -21,6 +21,8 @@
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -38,8 +40,6 @@
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.AccountsCollection;
import com.google.gerrit.server.account.GroupMembers;
-import com.google.gerrit.server.change.ReviewerJson.PostResult;
-import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.ReviewerAdded;
import com.google.gerrit.server.git.BatchUpdate;
@@ -128,7 +128,7 @@
}
@Override
- public PostResult apply(ChangeResource rsrc, AddReviewerInput input)
+ public AddReviewerResult apply(ChangeResource rsrc, AddReviewerInput input)
throws UpdateException, OrmException, RestApiException, IOException {
if (input.reviewer == null) {
throw new BadRequestException("missing reviewer field");
@@ -136,7 +136,7 @@
try {
Account.Id accountId = accounts.parse(input.reviewer).getAccountId();
- return putAccount(reviewerFactory.create(rsrc, accountId));
+ return putAccount(input.reviewer, reviewerFactory.create(rsrc, accountId));
} catch (UnprocessableEntityException e) {
try {
return putGroup(rsrc, input);
@@ -148,11 +148,11 @@
}
}
- private PostResult putAccount(ReviewerResource rsrc)
+ private AddReviewerResult putAccount(String reviewer, ReviewerResource rsrc)
throws OrmException, UpdateException, RestApiException {
Account member = rsrc.getReviewerUser().getAccount();
ChangeControl control = rsrc.getReviewerControl();
- PostResult result = new PostResult();
+ AddReviewerResult result = new AddReviewerResult(reviewer);
if (isValidReviewer(member, control)) {
addReviewers(rsrc.getChangeResource(), result,
ImmutableMap.of(member.getId(), control));
@@ -160,10 +160,10 @@
return result;
}
- private PostResult putGroup(ChangeResource rsrc, AddReviewerInput input)
+ private AddReviewerResult putGroup(ChangeResource rsrc, AddReviewerInput input)
throws UpdateException, RestApiException, OrmException, IOException {
GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer);
- PostResult result = new PostResult();
+ AddReviewerResult result = new AddReviewerResult(input.reviewer);
if (!isLegalReviewerGroup(group.getGroupUUID())) {
result.error = MessageFormat.format(
ChangeMessages.get().groupIsNotAllowed, group.getName());
@@ -227,7 +227,7 @@
private void addReviewers(
- ChangeResource rsrc, PostResult result, Map<Account.Id, ChangeControl> reviewers)
+ ChangeResource rsrc, AddReviewerResult result, Map<Account.Id, ChangeControl> reviewers)
throws OrmException, RestApiException, UpdateException {
try (BatchUpdate bu = batchUpdateFactory.create(
dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
@@ -240,8 +240,8 @@
for (PatchSetApproval psa : op.added) {
// New reviewers have value 0, don't bother normalizing.
result.reviewers.add(
- json.format(new ReviewerInfo(
- psa.getAccountId()), reviewers.get(psa.getAccountId()),
+ json.format(new ReviewerInfo(psa.getAccountId().get()),
+ reviewers.get(psa.getAccountId()),
ImmutableList.of(psa)));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index e24a290..69cd439 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -23,7 +23,7 @@
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -40,7 +40,6 @@
import java.util.Collection;
import java.util.List;
-import java.util.Map;
import java.util.TreeMap;
@Singleton
@@ -67,7 +66,7 @@
AccountLoader loader = accountLoaderFactory.create(true);
for (ReviewerResource rsrc : rsrcs) {
ReviewerInfo info = format(new ReviewerInfo(
- rsrc.getReviewerUser().getAccountId()),
+ rsrc.getReviewerUser().getAccountId().get()),
rsrc.getReviewerControl());
loader.put(info);
infos.add(info);
@@ -132,18 +131,4 @@
return out;
}
-
- public static class ReviewerInfo extends AccountInfo {
- Map<String, String> approvals;
-
- protected ReviewerInfo(Account.Id id) {
- super(id.get());
- }
- }
-
- public static class PostResult {
- public List<ReviewerInfo> reviewers;
- public String error;
- Boolean confirm;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java
index 8351e41..1de98d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java
@@ -37,6 +37,18 @@
return changesWrapper;
}
+ @Override
+ public void commit() {
+ throw new UnsupportedOperationException(
+ "do not call commit; BatchUpdate always manages transactions");
+ }
+
+ @Override
+ public void rollback() {
+ throw new UnsupportedOperationException(
+ "do not call rollback; BatchUpdate always manages transactions");
+ }
+
private static class BatchUpdateChanges extends ChangeAccessWrapper {
private BatchUpdateChanges(ChangeAccess delegate) {
super(delegate);
@@ -51,14 +63,14 @@
@Override
public void upsert(Iterable<Change> instances) {
throw new UnsupportedOperationException(
- "do not call upsert; either use InsertChangeOp for insertion, or"
- + " ChangeContext#saveChange() for update");
+ "do not call upsert; existing changes are updated automatically,"
+ + " or use InsertChangeOp for insertion");
}
@Override
public void update(Iterable<Change> instances) {
throw new UnsupportedOperationException(
- "do not call update; use ChangeContext#saveChange()");
+ "do not call update; change is updated automatically");
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
index cfdedd0..741fee1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -17,7 +17,6 @@
import com.google.common.collect.Sets;
import com.google.gerrit.common.data.GarbageCollectionResult;
import com.google.gerrit.extensions.events.GarbageCollectorListener;
-import com.google.gerrit.extensions.events.GarbageCollectorListener.Event;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.GcConfig;
@@ -115,17 +114,10 @@
}
private void fire(final Project.NameKey p, final Properties statistics) {
- Event event = new GarbageCollectorListener.Event() {
- @Override
- public String getProjectName() {
- return p.get();
- }
-
- @Override
- public Properties getStatistics() {
- return statistics;
- }
- };
+ if (!listeners.iterator().hasNext()) {
+ return;
+ }
+ Event event = new Event(p, statistics);
for (GarbageCollectorListener l : listeners) {
try {
l.onGarbageCollected(event);
@@ -204,4 +196,24 @@
writer.print(message);
}
}
+
+ private static class Event implements GarbageCollectorListener.Event {
+ private final Project.NameKey p;
+ private final Properties statistics;
+
+ Event(Project.NameKey p, Properties statistics) {
+ this.p = p;
+ this.statistics = statistics;
+ }
+
+ @Override
+ public String getProjectName() {
+ return p.get();
+ }
+
+ @Override
+ public Properties getStatistics() {
+ return statistics;
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 2a447e4..4c77038 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -94,7 +94,7 @@
private final ProjectJson json;
private final ProjectControl.GenericFactory projectControlFactory;
private final GitRepositoryManager repoManager;
- private final DynamicSet<NewProjectCreatedListener> createdListener;
+ private final DynamicSet<NewProjectCreatedListener> createdListeners;
private final ProjectCache projectCache;
private final GroupBackend groupBackend;
private final ProjectOwnerGroupsProvider.Factory projectOwnerGroups;
@@ -113,7 +113,7 @@
DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners,
ProjectControl.GenericFactory projectControlFactory,
GitRepositoryManager repoManager,
- DynamicSet<NewProjectCreatedListener> createdListener,
+ DynamicSet<NewProjectCreatedListener> createdListeners,
ProjectCache projectCache,
GroupBackend groupBackend,
ProjectOwnerGroupsProvider.Factory projectOwnerGroups,
@@ -131,7 +131,7 @@
this.json = json;
this.projectControlFactory = projectControlFactory;
this.repoManager = repoManager;
- this.createdListener = createdListener;
+ this.createdListeners = createdListeners;
this.projectCache = projectCache;
this.groupBackend = groupBackend;
this.projectOwnerGroups = projectOwnerGroups;
@@ -253,24 +253,7 @@
createEmptyCommits(repo, nameKey, args.branch);
}
- NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
- @Override
- public String getProjectName() {
- return nameKey.get();
- }
-
- @Override
- public String getHeadName() {
- return head;
- }
- };
- for (NewProjectCreatedListener l : createdListener) {
- try {
- l.onNewProjectCreated(event);
- } catch (RuntimeException e) {
- log.warn("Failure in NewProjectCreatedListener", e);
- }
- }
+ fire(nameKey, head);
return projectCache.get(nameKey).getProject();
}
@@ -395,4 +378,39 @@
throw e;
}
}
+
+ private void fire(Project.NameKey name, String head) {
+ if (!createdListeners.iterator().hasNext()) {
+ return;
+ }
+
+ Event event = new Event(name, head);
+ for (NewProjectCreatedListener l : createdListeners) {
+ try {
+ l.onNewProjectCreated(event);
+ } catch (RuntimeException e) {
+ log.warn("Failure in NewProjectCreatedListener", e);
+ }
+ }
+ }
+
+ static class Event implements NewProjectCreatedListener.Event {
+ private final Project.NameKey name;
+ private final String head;
+
+ Event(Project.NameKey name, String head) {
+ this.name = name;
+ this.head = head;
+ }
+
+ @Override
+ public String getProjectName() {
+ return name.get();
+ }
+
+ @Override
+ public String getHeadName() {
+ return head;
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
index aa06024..f964311 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -53,15 +54,15 @@
private final GitRepositoryManager repoManager;
private final Provider<IdentifiedUser> identifiedUser;
- private final DynamicSet<HeadUpdatedListener> headUpdatedListener;
+ private final DynamicSet<HeadUpdatedListener> headUpdatedListeners;
@Inject
SetHead(GitRepositoryManager repoManager,
Provider<IdentifiedUser> identifiedUser,
- DynamicSet<HeadUpdatedListener> headUpdatedListener) {
+ DynamicSet<HeadUpdatedListener> headUpdatedListeners) {
this.repoManager = repoManager;
this.identifiedUser = identifiedUser;
- this.headUpdatedListener = headUpdatedListener;
+ this.headUpdatedListeners = headUpdatedListeners;
}
@Override
@@ -106,33 +107,52 @@
throw new IOException("Setting HEAD failed with " + res);
}
- HeadUpdatedListener.Event event = new HeadUpdatedListener.Event() {
- @Override
- public String getProjectName() {
- return rsrc.getNameKey().get();
- }
-
- @Override
- public String getOldHeadName() {
- return oldHead;
- }
-
- @Override
- public String getNewHeadName() {
- return newHead;
- }
- };
- for (HeadUpdatedListener l : headUpdatedListener) {
- try {
- l.onHeadUpdated(event);
- } catch (RuntimeException e) {
- log.warn("Failure in HeadUpdatedListener", e);
- }
- }
+ fire(rsrc.getNameKey(), oldHead, newHead);
}
return ref;
} catch (RepositoryNotFoundException e) {
throw new ResourceNotFoundException(rsrc.getName());
}
}
+
+ private void fire(Project.NameKey nameKey, String oldHead, String newHead) {
+ if (!headUpdatedListeners.iterator().hasNext()) {
+ return;
+ }
+ Event event = new Event(nameKey, oldHead, newHead);
+ for (HeadUpdatedListener l : headUpdatedListeners) {
+ try {
+ l.onHeadUpdated(event);
+ } catch (RuntimeException e) {
+ log.warn("Failure in HeadUpdatedListener", e);
+ }
+ }
+ }
+
+ static class Event implements HeadUpdatedListener.Event {
+ private final Project.NameKey nameKey;
+ private final String oldHead;
+ private final String newHead;
+
+ Event(Project.NameKey nameKey, String oldHead, String newHead) {
+ this.nameKey = nameKey;
+ this.oldHead = oldHead;
+ this.newHead = newHead;
+ }
+
+ @Override
+ public String getProjectName() {
+ return nameKey.get();
+ }
+
+ @Override
+ public String getOldHeadName() {
+ return oldHead;
+ }
+
+ @Override
+ public String getNewHeadName() {
+ return newHead;
+ }
+ }
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
index 3208d00..f98d63f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -83,7 +83,7 @@
}
} catch (RepositoryNotFoundException e) {
repo = new Repo(name);
- repos.put(name.get().toLowerCase(), repo);
+ repos.put(normalize(name), repo);
}
return repo;
}
@@ -113,12 +113,20 @@
}
}
+ public synchronized void deleteRepository(Project.NameKey name) {
+ repos.remove(normalize(name));
+ }
+
private synchronized Repo get(Project.NameKey name)
throws RepositoryNotFoundException {
- Repo repo = repos.get(name.get().toLowerCase());
+ Repo repo = repos.get(normalize(name));
if (repo != null) {
return repo;
}
throw new RepositoryNotFoundException(name.get());
}
+
+ private static String normalize(Project.NameKey name) {
+ return name.get().toLowerCase();
+ }
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index c7705a4..40364fa 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -77,10 +77,12 @@
this._clearDiffContent();
+ console.time('diff render');
this.$.processor.process(diff.content).then(function() {
if (this.isImageDiff) {
this._builder.renderDiffImages();
}
+ console.timeEnd('diff render');
this.fire('render');
}.bind(this));
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index f4fa310..b035af1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -17,6 +17,8 @@
// Prevent redefinition.
if (window.GrDiffBuilder) { return; }
+ var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+
function GrDiffBuilder(diff, comments, prefs, outputEl) {
this._diff = diff;
this._comments = comments;
@@ -30,8 +32,6 @@
GrDiffBuilder.AMPERSAND_CODE = '&'.charCodeAt(0);
GrDiffBuilder.SEMICOLON_CODE = ';'.charCodeAt(0);
- GrDiffBuilder.TAB_REGEX = /\t/g;
-
GrDiffBuilder.LINE_FEED_HTML =
'<span class="style-scope gr-diff br"></span>';
@@ -298,10 +298,10 @@
td.classList.add(line.type);
var text = line.text;
var html = util.escapeHTML(text);
+ html = this._addTabWrappers(html, this._prefs.tab_size);
td.classList.add(line.highlights.length > 0 ?
'lightHighlight' : 'darkHighlight');
-
if (line.highlights.length > 0) {
html = this._addIntralineHighlights(text, html, line.highlights);
}
@@ -310,7 +310,6 @@
this._prefs.line_length) {
html = this._addNewlines(text, html);
}
- html = this._addTabWrappers(html);
var contentText = this._createElement('div', 'contentText');
@@ -327,12 +326,16 @@
return td;
};
+ /**
+ * Returns the text length after normalizing unicode and tabs.
+ * @return {Number} The normalized length of the text.
+ */
GrDiffBuilder.prototype._textLength = function(text, tabSize) {
- // TODO(andybons): Unicode support.
+ text = text.replace(REGEX_ASTRAL_SYMBOL, '_');
var numChars = 0;
for (var i = 0; i < text.length; i++) {
if (text[i] === '\t') {
- numChars += tabSize;
+ numChars += tabSize - (numChars % tabSize);
} else {
numChars++;
}
@@ -395,10 +398,34 @@
return result;
};
- GrDiffBuilder.prototype._addTabWrappers = function(html) {
- var htmlStr = this._getTabWrapper(this._prefs.tab_size,
- this._prefs.show_tabs);
- return html.replace(GrDiffBuilder.TAB_REGEX, htmlStr);
+ /**
+ * Takes a string of text (not HTML) and returns a string of HTML with tab
+ * elements in place of tab characters. In each case tab elements are given
+ * the width needed to reach the next tab-stop.
+ *
+ * @param {String} A line of text potentially containing tab characters.
+ * @param {Number} The width for tabs.
+ * @return {String} An HTML string potentially containing tab elements.
+ */
+ GrDiffBuilder.prototype._addTabWrappers = function(line, tabSize) {
+ if (!line.length) { return ''; }
+
+ var result = '';
+ var offset = 0;
+ var split = line.split('\t');
+ var width;
+
+ for (var i = 0; i < split.length - 1; i++) {
+ offset += split[i].length;
+ width = tabSize - (offset % tabSize);
+ result += split[i] + this._getTabWrapper(width, this._prefs.show_tabs);
+ offset += width;
+ }
+ if (split.length) {
+ result += split[split.length - 1];
+ }
+
+ return result;
};
GrDiffBuilder.prototype._addIntralineHighlights = function(content, html,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index cda4dc8..2c633f1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -104,19 +104,29 @@
'6789');
});
- test('text length with tabs', function() {
+ test('text length with tabs and unicode', function() {
assert.equal(builder._textLength('12345', 4), 5);
assert.equal(builder._textLength('\t\t12', 4), 10);
+ assert.equal(builder._textLength('abc💢123', 4), 7);
+
+ assert.equal(builder._textLength('abc\t', 8), 8);
+ assert.equal(builder._textLength('abc\t\t', 10), 20);
+ assert.equal(builder._textLength('', 10), 0);
+ assert.equal(builder._textLength('', 10), 0);
+ assert.equal(builder._textLength('abc\tde', 10), 12);
+ assert.equal(builder._textLength('abc\tde\t', 10), 20);
+ assert.equal(builder._textLength('\t\t\t\t\t', 20), 100);
});
test('tab wrapper insertion', function() {
var html = 'abc\tdef';
var wrapper = builder._getTabWrapper(
- builder._prefs.tab_size,
+ builder._prefs.tab_size - 3,
builder._prefs.show_tabs);
assert.ok(wrapper);
assert.isAbove(wrapper.length, 0);
- assert.equal(builder._addTabWrappers(html), 'abc' + wrapper + 'def');
+ assert.equal(builder._addTabWrappers(html, builder._prefs.tab_size),
+ 'abc' + wrapper + 'def');
assert.throws(builder._getTabWrapper.bind(
builder,
// using \x3c instead of < in string so gjslint can parse
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
new file mode 100644
index 0000000..ca3e5ec
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
@@ -0,0 +1,206 @@
+// Copyright (C) 2016 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.
+(function(window) {
+ 'use strict';
+
+ // Prevent redefinition.
+ if (window.GrAnnotation) { return; }
+
+ // TODO(wyatta): refactor this to be <MARK> rather than <HL>.
+ var ANNOTATION_TAG = 'HL';
+
+ // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
+ var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+
+ var GrAnnotation = {
+
+ /**
+ * The DOM API textContent.length calculation is broken when the text
+ * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
+ * @param {Text} A text node.
+ * @return {Number} The length of the text.
+ */
+ getLength: function(node) {
+ return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length;
+ },
+
+ /**
+ * Surrounds the element's text at specified range in an ANNOTATION_TAG
+ * element. If the element has child elements, the range is split and
+ * applied as deeply as possible.
+ */
+ annotateElement: function(parent, offset, length, cssClass) {
+ var nodes = [].slice.apply(parent.childNodes);
+ var node;
+ var nodeLength;
+ var subLength;
+
+ for (var i = 0; i < nodes.length; i++) {
+ node = nodes[i];
+ nodeLength = this.getLength(node);
+
+ // If the current node is completely before the offset.
+ if (nodeLength <= offset) {
+ offset -= nodeLength;
+ continue;
+ }
+
+ // Sublength is the annotation length for the current node.
+ subLength = Math.min(length, nodeLength - offset);
+
+ if (node instanceof Text) {
+ this._annotateText(node, offset, subLength, cssClass);
+ } else if (node instanceof HTMLElement) {
+ this.annotateElement(node, offset, subLength, cssClass);
+ }
+
+ // If there is still more to annotate, then shift the indices, otherwise
+ // work is done, so break the loop.
+ if (subLength < length) {
+ length -= subLength;
+ offset = 0;
+ } else {
+ break;
+ }
+ }
+ },
+
+ /**
+ * Wraps node in annotation tag with cssClass, replacing the node in DOM.
+ *
+ * @return {!Element} Wrapped node.
+ */
+ wrapInHighlight: function(node, cssClass) {
+ var hl;
+ if (node.tagName === ANNOTATION_TAG) {
+ hl = node;
+ hl.classList.add(cssClass);
+ } else {
+ hl = document.createElement(ANNOTATION_TAG);
+ hl.className = cssClass;
+ Polymer.dom(node.parentElement).replaceChild(hl, node);
+ hl.appendChild(node);
+ }
+ return hl;
+ },
+
+ /**
+ * Splits Text Node and wraps it in hl with cssClass.
+ * Wraps trailing part after split, tailing one if opt_firstPart is true.
+ *
+ * @param {!Node} node
+ * @param {number} offset
+ * @param {string} cssClass
+ * @param {boolean=} opt_firstPart
+ */
+ splitAndWrapInHighlight: function(node, offset, cssClass, opt_firstPart) {
+ if (this.getLength(node) === offset || offset === 0) {
+ return this.wrapInHighlight(node, cssClass);
+ } else {
+ if (opt_firstPart) {
+ this.splitNode(node, offset);
+ // Node points to first part of the Text, second one is sibling.
+ } else {
+ node = this.splitNode(node, offset);
+ }
+ return this.wrapInHighlight(node, cssClass);
+ }
+ },
+
+ /**
+ * Splits Node at offset.
+ * If Node is Element, it's cloned and the node at offset is split too.
+ *
+ * @param {!Node} node
+ * @param {number} offset
+ * @return {!Node} Trailing Node.
+ */
+ splitNode: function(element, offset) {
+ if (element instanceof Text) {
+ return this.splitTextNode(element, offset);
+ }
+ var tail = element.cloneNode(false);
+ element.parentElement.insertBefore(tail, element.nextSibling);
+ // Skip nodes before offset.
+ var node = element.firstChild;
+ while (node &&
+ this.getLength(node) <= offset ||
+ this.getLength(node) === 0) {
+ offset -= this.getLength(node);
+ node = node.nextSibling;
+ }
+ if (this.getLength(node) > offset) {
+ tail.appendChild(this.splitNode(node, offset));
+ }
+ while (node.nextSibling) {
+ tail.appendChild(node.nextSibling);
+ }
+ return tail;
+ },
+
+ /**
+ * Node.prototype.splitText Unicode-valid alternative.
+ *
+ * DOM Api for splitText() is broken for Unicode:
+ * https://mathiasbynens.be/notes/javascript-unicode
+ *
+ * @param {!Text} node
+ * @param {number} offset
+ * @return {!Text} Trailing Text Node.
+ */
+ splitTextNode: function(node, offset) {
+ if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
+ // TODO (viktard): Polyfill Array.from for IE10.
+ var head = Array.from(node.textContent);
+ var tail = head.splice(offset);
+ var parent = node.parentElement;
+ var headNode = document.createTextNode(head.join(''));
+ parent.replaceChild(headNode, node);
+ var tailNode = document.createTextNode(tail.join(''));
+ parent.insertBefore(tailNode, headNode.nextSibling);
+ return tailNode;
+ } else {
+ return node.splitText(offset);
+ }
+ },
+
+ _annotateText: function(node, offset, length, cssClass) {
+ var nodeLength = this.getLength(node);
+
+ // There are four cases:
+ // 1) Entire node is highlighted.
+ // 2) Highlight is at the start.
+ // 3) Highlight is at the end.
+ // 4) Highlight is in the middle.
+
+ if (offset === 0 && nodeLength === length) {
+ // Case 1.
+ this.wrapInHighlight(node, cssClass);
+ } else if (offset === 0) {
+ // Case 2.
+ this.splitAndWrapInHighlight(node, length, cssClass, true);
+ } else if (offset + length === nodeLength) {
+ // Case 3
+ this.splitAndWrapInHighlight(node, offset, cssClass, false);
+ } else {
+ // Case 4
+ this.splitAndWrapInHighlight(this.splitTextNode(node, offset), length,
+ cssClass, true);
+ }
+ },
+ };
+
+ window.GrAnnotation = GrAnnotation;
+})(window);
+
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
index bc3b23f..e44086e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
@@ -36,5 +36,6 @@
<content></content>
</div>
</template>
+ <script src="gr-annotation.js"></script>
<script src="gr-diff-highlight.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index c4e79eb..47a02a6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -333,107 +333,7 @@
if (node instanceof Element && node.classList.contains('content')) {
return this._getLength(node.querySelector('.contentText'));
} else {
- // DOM API for textContent.length is broken for Unicode:
- // https://mathiasbynens.be/notes/javascript-unicode
- return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length;
- }
- },
-
- /**
- * Wraps node in hl tag with cssClass, replacing the node in DOM.
- *
- * @return {!Element} Wrapped node.
- */
- _wrapInHighlight: function(node, cssClass) {
- var hl;
- if (node.tagName === 'HL') {
- hl = node;
- hl.classList.add(cssClass);
- } else {
- hl = document.createElement('hl');
- hl.className = cssClass;
- Polymer.dom(node.parentElement).replaceChild(hl, node);
- hl.appendChild(node);
- }
- return hl;
- },
-
- /**
- * Node.prototype.splitText Unicode-valid alternative.
- *
- * @param {!Text} node
- * @param {number} offset
- * @return {!Text} Trailing Text Node.
- */
- _splitTextNode: function(node, offset) {
- if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) {
- // DOM Api for splitText() is broken for Unicode:
- // https://mathiasbynens.be/notes/javascript-unicode
- // TODO (viktard): Polyfill Array.from for IE10.
- var head = Array.from(node.textContent);
- var tail = head.splice(offset);
- var parent = node.parentElement;
- var headNode = document.createTextNode(head.join(''));
- parent.replaceChild(headNode, node);
- var tailNode = document.createTextNode(tail.join(''));
- parent.insertBefore(tailNode, headNode.nextSibling);
- return tailNode;
- } else {
- return node.splitText(offset);
- }
- },
-
- /**
- * Split Node at offset.
- * If Node is Element, it's cloned and the node at offset is split too.
- *
- * @param {!Node} node
- * @param {number} offset
- * @return {!Node} Trailing Node.
- */
- _splitNode: function(element, offset) {
- if (element instanceof Text) {
- return this._splitTextNode(element, offset);
- }
- var tail = element.cloneNode(false);
- element.parentElement.insertBefore(tail, element.nextSibling);
- // Skip nodes before offset.
- var node = element.firstChild;
- while (node &&
- this._getLength(node) <= offset ||
- this._getLength(node) === 0) {
- offset -= this._getLength(node);
- node = node.nextSibling;
- }
- if (this._getLength(node) > offset) {
- tail.appendChild(this._splitNode(node, offset));
- }
- while (node.nextSibling) {
- tail.appendChild(node.nextSibling);
- }
- return tail;
- },
-
- /**
- * Split Text Node and wrap it in hl with cssClass.
- * Wraps trailing part after split, tailing one if opt_firstPart is true.
- *
- * @param {!Node} node
- * @param {number} offset
- * @param {string} cssClass
- * @param {boolean=} opt_firstPart
- */
- _splitAndWrapInHighlight: function(node, offset, cssClass, opt_firstPart) {
- if (this._getLength(node) === offset || offset === 0) {
- return this._wrapInHighlight(node, cssClass);
- } else {
- if (opt_firstPart) {
- this._splitNode(node, offset);
- // Node points to first part of the Text, second one is sibling.
- } else {
- node = this._splitNode(node, offset);
- }
- return this._wrapInHighlight(node, cssClass);
+ return GrAnnotation.getLength(node);
}
},
@@ -470,11 +370,11 @@
// Split Text node.
if (startNode instanceof Text) {
- startNode =
- this._splitAndWrapInHighlight(startNode, startOffset, cssClass);
+ startNode = GrAnnotation.splitAndWrapInHighlight(
+ startNode, startOffset, cssClass);
// Edge case: single line, text node wraps the highlight.
if (isOneLine && this._getLength(startNode) > length) {
- var extra = this._splitTextNode(startNode.firstChild, length);
+ var extra = GrAnnotation.splitTextNode(startNode.firstChild, length);
startContent.insertBefore(extra, startNode.nextSibling);
startContent.normalize();
}
@@ -483,10 +383,10 @@
// Edge case: single line, <hl> wraps the highlight.
// Should leave wrapping HL's content after the highlight.
if (isOneLine && startOffset + length < this._getLength(startNode)) {
- this._splitNode(startNode, startOffset + length);
+ GrAnnotation.splitNode(startNode, startOffset + length);
}
- startNode =
- this._splitAndWrapInHighlight(startNode, startOffset, cssClass);
+ startNode = GrAnnotation.splitAndWrapInHighlight(
+ startNode, startOffset, cssClass);
}
} else {
startNode = null;
@@ -524,13 +424,13 @@
if (!endNode) { return null; }
if (endNode instanceof Text) {
- endNode =
- this._splitAndWrapInHighlight(endNode, endOffset, cssClass, true);
+ endNode = GrAnnotation.splitAndWrapInHighlight(
+ endNode, endOffset, cssClass, true);
} else if (endNode.tagName == 'HL') {
if (!endNode.classList.contains(cssClass)) {
// Split text inside HL.
var hl = endNode;
- endNode = this._splitAndWrapInHighlight(
+ endNode = GrAnnotation.splitAndWrapInHighlight(
endNode, endOffset, cssClass, true);
if (hl.textContent.length === 0) {
hl.remove();
@@ -627,7 +527,7 @@
if (threadEl) {
content.appendChild(threadEl);
}
- this._wrapInHighlight(text, cssClass);
+ GrAnnotation.wrapInHighlight(text, cssClass);
}, this);
}
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index ddc081e..88f6be5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -20,6 +20,7 @@
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="gr-annotation.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-highlight.html">
@@ -891,6 +892,159 @@
assert.equal(getActionSide(), 'left');
});
+ suite('annotation', function() {
+ var str;
+ var parent;
+ var textNode;
+
+ setup(function() {
+ str = 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula';
+ parent = document.createElement('div');
+ parent.appendChild(document.createTextNode(str));
+ textNode = parent.childNodes[0];
+ });
+
+ test('library is loaded', function() {
+ assert.isOk(GrAnnotation);
+ });
+
+ test('_annotateText Case 1', function() {
+ assert.equal(parent.childNodes.length, 1);
+ assert.instanceOf(parent.childNodes[0], Text);
+
+ GrAnnotation._annotateText(textNode, 0, str.length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 1);
+ assert.instanceOf(parent.childNodes[0], HTMLElement);
+ assert.equal(parent.childNodes[0].className, 'foobar');
+ assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
+ assert.equal(parent.childNodes[0].childNodes[0].textContent, str);
+ });
+
+ test('_annotateText Case 2', function() {
+ var length = 12;
+ var substr = str.substr(0, length);
+ var remainder = str.substr(length);
+
+ assert.equal(parent.childNodes.length, 1);
+ assert.instanceOf(parent.childNodes[0], Text);
+
+ GrAnnotation._annotateText(textNode, 0, length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 2);
+
+ assert.instanceOf(parent.childNodes[0], HTMLElement);
+ assert.equal(parent.childNodes[0].className, 'foobar');
+ assert.instanceOf(parent.childNodes[0].childNodes[0], Text);
+ assert.equal(parent.childNodes[0].childNodes[0].textContent, substr);
+
+ assert.instanceOf(parent.childNodes[1], Text);
+ assert.equal(parent.childNodes[1].textContent, remainder);
+ });
+
+ test('_annotateText Case 3', function() {
+ var index = 12;
+ var length = str.length - index;
+ var remainder = str.substr(0, index);
+ var substr = str.substr(index);
+
+ assert.equal(parent.childNodes.length, 1);
+ assert.instanceOf(parent.childNodes[0], Text);
+
+ GrAnnotation._annotateText(textNode, index, length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 2);
+
+ assert.instanceOf(parent.childNodes[0], Text);
+ assert.equal(parent.childNodes[0].textContent, remainder);
+
+ assert.instanceOf(parent.childNodes[1], HTMLElement);
+ assert.equal(parent.childNodes[1].className, 'foobar');
+ assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
+ assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+ });
+
+ test('_annotateText Case 4', function() {
+ var index = str.indexOf('dolor');
+ var length = 'dolor '.length;
+
+ var remainderPre = str.substr(0, index);
+ var substr = str.substr(index, length);
+ var remainderPost = str.substr(index + length);
+
+ assert.equal(parent.childNodes.length, 1);
+ assert.instanceOf(parent.childNodes[0], Text);
+
+ GrAnnotation._annotateText(textNode, index, length, 'foobar');
+
+ assert.equal(parent.childNodes.length, 3);
+
+ assert.instanceOf(parent.childNodes[0], Text);
+ assert.equal(parent.childNodes[0].textContent, remainderPre);
+
+ assert.instanceOf(parent.childNodes[1], HTMLElement);
+ assert.equal(parent.childNodes[1].className, 'foobar');
+ assert.instanceOf(parent.childNodes[1].childNodes[0], Text);
+ assert.equal(parent.childNodes[1].childNodes[0].textContent, substr);
+
+ assert.instanceOf(parent.childNodes[2], Text);
+ assert.equal(parent.childNodes[2].textContent, remainderPost);
+ });
+
+ test('_annotateElement design doc example', function() {
+ var layers = [
+ 'amet, ',
+ 'inceptos ',
+ 'amet, ',
+ 'et, suspendisse ince'
+ ];
+
+ // Apply the layers successively.
+ layers.forEach(function(layer, i) {
+ GrAnnotation.annotateElement(
+ parent, str.indexOf(layer), layer.length, 'layer-' + (i + 1));
+ });
+
+ assert.equal(parent.textContent, str);
+
+ // Layer 1:
+ var layer1 = parent.querySelectorAll('.layer-1');
+ assert.equal(layer1.length, 1);
+ assert.equal(layer1[0].textContent, layers[0]);
+ assert.equal(layer1[0].parentElement, parent);
+
+ // Layer 2:
+ var layer2 = parent.querySelectorAll('.layer-2');
+ assert.equal(layer2.length, 1);
+ assert.equal(layer2[0].textContent, layers[1]);
+ assert.equal(layer2[0].parentElement, parent);
+
+ // Layer 3:
+ var layer3 = parent.querySelectorAll('.layer-3');
+ assert.equal(layer3.length, 1);
+ assert.equal(layer3[0].textContent, layers[2]);
+ assert.equal(layer3[0].parentElement, layer1[0]);
+
+ // Layer 4:
+ var layer4 = parent.querySelectorAll('.layer-4');
+ assert.equal(layer4.length, 3);
+
+ assert.equal(layer4[0].textContent, 'et, ');
+ assert.equal(layer4[0].parentElement, layer3[0]);
+
+ assert.equal(layer4[1].textContent, 'suspendisse ');
+ assert.equal(layer4[1].parentElement, parent);
+
+ assert.equal(layer4[2].textContent, 'ince');
+ assert.equal(layer4[2].parentElement, layer2[0]);
+
+ assert.equal(layer4[0].textContent +
+ layer4[1].textContent +
+ layer4[2].textContent,
+ layers[3]);
+ });
+ });
+
// TODO (viktard): Selection starts in line number.
// TODO (viktard): Empty lines in selection start.
// TODO (viktard): Empty lines in selection end.