Merge "Make gr-change-list a true HTML table"
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt
index a7e8909..c777327 100644
--- a/Documentation/dev-build-plugins.txt
+++ b/Documentation/dev-build-plugins.txt
@@ -56,9 +56,10 @@
The fact that plugin contains `BUILD` file doesn't mean that building this
-plugin from the plugin directory works. For now it doesn't. Bazel in tree driven
-means it can only be built from within Gerrit tree. Clone or link the plugin
-into gerrit/plugins directory:
+plugin from the plugin directory works.
+
+Bazel in tree driven means it can only be built from within Gerrit tree. Clone
+or link the plugin into gerrit/plugins directory:
----
cd gerrit
@@ -74,6 +75,26 @@
Some plugins describe their build process in `src/main/resources/Documentation/build.md`
file. It may worth checking.
+=== Plugins with external dependencies ===
+
+If the plugin has external dependencies, then they must be included from Gerrit's
+own WORKSPACE file. This can be achieved by including them in `external_plugin_deps.bzl`.
+During the build in Gerrit tree, this file must be copied over the dummy one in
+`plugins` directory.
+
+Example for content of `external_plugin_deps.bzl` file:
+
+----
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+ maven_jar(
+ name = 'org_apache_tika_tika_core',
+ artifact = 'org.apache.tika:tika-core:1.12',
+ sha1 = '5ab95580d22fe1dee79cffbcd98bb509a32da09b',
+ )
+----
+
== Bazel standalone driven
Only few plugins support that mode for now:
diff --git a/WORKSPACE b/WORKSPACE
index 3329460..5233b0d 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -2,6 +2,7 @@
load("//tools/bzl:maven_jar.bzl", "maven_jar", "GERRIT", "MAVEN_LOCAL")
load("//lib/codemirror:cm.bzl", "CM_VERSION", "DIFF_MATCH_PATCH_VERSION")
+load("//plugins:external_plugin_deps.bzl", "external_plugin_deps")
ANTLR_VERS = "3.5.2"
@@ -1104,3 +1105,4 @@
load("//lib/js:bower_archives.bzl", "load_bower_archives")
load_bower_archives()
+external_plugin_deps()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index cc8db0a..d4939e0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -42,6 +42,7 @@
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Patch;
@@ -839,8 +840,8 @@
r1.assertOkStatus();
// Try to create edit as admin
+ exception.expect(AuthException.class);
createEmptyEditFor(r1.getChangeId());
- assertThat(getEdit(changeId)).isAbsent();
}
private void createArbitraryEditFor(String changeId) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 5b087a4..68b87da 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -150,6 +150,40 @@
}
@Test
+ public void postCommentWithReply() throws Exception {
+ for (Integer line : lines) {
+ String file = "file";
+ String contents = "contents " + line;
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+ "first subject", file, contents);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ ReviewInput input = new ReviewInput();
+ CommentInput comment =
+ newComment(file, Side.REVISION, line, "comment 1", false);
+ input.comments = new HashMap<>();
+ input.comments.put(comment.path, Lists.newArrayList(comment));
+ revision(r).review(input);
+ Map<String, List<CommentInfo>> result =
+ getPublishedComments(changeId, revId);
+ CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
+
+ input = new ReviewInput();
+ comment = newComment(file, Side.REVISION, line, "comment 1 reply", false);
+ comment.inReplyTo = actual.id;
+ input.comments = new HashMap<>();
+ input.comments.put(comment.path, Lists.newArrayList(comment));
+ revision(r).review(input);
+ result = getPublishedComments(changeId, revId);
+ actual = result.get(comment.path).get(1);
+ assertThat(comment).isEqualTo(infoToInput(file).apply(actual));
+ assertThat(comment).isEqualTo(infoToInput(file).apply(
+ getPublishedComment(changeId, revId, actual.id)));
+ }
+ }
+
+ @Test
public void postCommentWithUnresolved() throws Exception {
for (Integer line : lines) {
String file = "file";
@@ -786,5 +820,6 @@
to.message = from.message;
to.range = from.range;
to.unresolved = from.unresolved;
+ to.inReplyTo = from.inReplyTo;
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index cb3a9ff..00be7ba 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -208,7 +208,18 @@
public void publishedComment() throws Exception {
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
- putComment(user, id, 1, "comment");
+ putComment(user, id, 1, "comment", null);
+ checker.rebuildAndCheckChanges(id);
+ }
+
+ @Test
+ public void publishedCommentAndReply() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getPatchSetId().getParentKey();
+ putComment(user, id, 1, "comment", null);
+ Map<String, List<CommentInfo>> comments = getPublishedComments(id);
+ String parentUuid = comments.get("a.txt").get(0).id;
+ putComment(user, id, 1, "comment", parentUuid);
checker.rebuildAndCheckChanges(id);
}
@@ -242,7 +253,7 @@
PushOneCommit.Result r = createChange();
Change.Id id = r.getPatchSetId().getParentKey();
putDraft(user, id, 1, "draft comment", null);
- putComment(user, id, 1, "published comment");
+ putComment(user, id, 1, "published comment", null);
checker.rebuildAndCheckChanges(id);
}
@@ -1283,11 +1294,12 @@
}
}
- private void putComment(TestAccount account, Change.Id id, int line, String msg)
- throws Exception {
+ private void putComment(TestAccount account, Change.Id id, int line,
+ String msg, String inReplyTo) throws Exception {
CommentInput in = new CommentInput();
in.line = line;
in.message = msg;
+ in.inReplyTo = inReplyTo;
ReviewInput rin = new ReviewInput();
rin.comments = new HashMap<>();
rin.comments.put(PushOneCommit.FILE_NAME, ImmutableList.of(in));
@@ -1348,4 +1360,8 @@
saveProjectConfig(allProjects, cfg);
}
+ private Map<String, List<CommentInfo>> getPublishedComments(Change.Id id)
+ throws Exception {
+ return gApi.changes().id(id.get()).current().comments();
+ }
}
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
index dec996d..3a0d7e5 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
@@ -39,7 +39,7 @@
[library "mysqlDriver"]
name = MySQL Connector/J 5.1.40
url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.40/mysql-connector-java-5.1.40.jar
- sha1 = 1e7732c5ad1bca3e77f156ca5df1175b3305b902
+ sha1 = ef2a2ceab1735eaaae0b5d1cccf574fb7c6e1c52
remove = mysql-connector-java-.*[.]jar
[library "oracleDriver"]
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
index 0e15774..a9af7e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -169,6 +169,7 @@
new Comment.Key(ChangeUtil.messageUuid(), path, psId.get()),
ctx.getUser().getAccountId(), ctx.getWhen(), side, message, serverId,
unresolved);
+ c.parentUuid = parentUuid;
ctx.getUser().updateRealAccountId(c::setRealAuthor);
return c;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
index 04ac741..b186767 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -31,7 +31,6 @@
import com.google.gerrit.server.change.PublishChangeEdit;
import com.google.gerrit.server.change.RebaseChangeEdit;
import com.google.gerrit.server.git.UpdateException;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -98,7 +97,7 @@
public void create() throws RestApiException {
try {
changeEditsPost.apply(changeResource, null);
- } catch (InvalidChangeOperationException | IOException | OrmException e) {
+ } catch (IOException | OrmException e) {
throw new RestApiException("Cannot create change edit", e);
}
}
@@ -116,7 +115,7 @@
public void rebase() throws RestApiException {
try {
rebaseChangeEdit.apply(changeResource, null);
- } catch (IOException | InvalidChangeOperationException | OrmException e) {
+ } catch (IOException | OrmException e) {
throw new RestApiException("Cannot rebase change edit", e);
}
}
@@ -146,7 +145,7 @@
return fileResponse.isNone()
? Optional.empty()
: Optional.of(fileResponse.value());
- } catch (IOException | InvalidChangeOperationException | OrmException e) {
+ } catch (IOException | OrmException e) {
throw new RestApiException("Cannot retrieve file of change edit", e);
}
}
@@ -159,7 +158,7 @@
renameInput.oldPath = oldFilePath;
renameInput.newPath = newFilePath;
changeEditsPost.apply(changeResource, renameInput);
- } catch (InvalidChangeOperationException | IOException | OrmException e) {
+ } catch (IOException | OrmException e) {
throw new RestApiException("Cannot rename file of change edit", e);
}
}
@@ -170,7 +169,7 @@
ChangeEdits.Post.Input restoreInput = new ChangeEdits.Post.Input();
restoreInput.restorePath = filePath;
changeEditsPost.apply(changeResource, restoreInput);
- } catch (InvalidChangeOperationException | IOException | OrmException e) {
+ } catch (IOException | OrmException e) {
throw new RestApiException("Cannot restore file of change edit", e);
}
}
@@ -179,12 +178,8 @@
public void modifyFile(String filePath, RawInput newContent)
throws RestApiException {
try {
- ChangeEditResource changeEditResource =
- getOrCreateChangeEditResource(filePath);
- ChangeEdits.Put.Input input = new ChangeEdits.Put.Input();
- input.content = newContent;
- changeEditsPut.apply(changeEditResource, input);
- } catch (IOException | InvalidChangeOperationException | OrmException e) {
+ changeEditsPut.apply(changeResource.getControl(), filePath, newContent);
+ } catch (IOException | OrmException e) {
throw new RestApiException("Cannot modify file of change edit", e);
}
}
@@ -192,10 +187,8 @@
@Override
public void deleteFile(String filePath) throws RestApiException {
try {
- ChangeEditResource changeEditResource =
- getOrCreateChangeEditResource(filePath);
- changeEditDeleteContent.apply(changeEditResource, null);
- } catch (IOException | InvalidChangeOperationException | OrmException e) {
+ changeEditDeleteContent.apply(changeResource.getControl(), filePath);
+ } catch (IOException | OrmException e) {
throw new RestApiException("Cannot delete file of change edit", e);
}
}
@@ -219,28 +212,15 @@
input.message = newCommitMessage;
try {
modifyChangeEditCommitMessage.apply(changeResource, input);
- } catch (IOException | InvalidChangeOperationException | OrmException e) {
+ } catch (IOException | OrmException e) {
throw new RestApiException("Cannot modify commit message of change edit",
e);
}
}
- private ChangeEditResource getOrCreateChangeEditResource(String filePath)
- throws InvalidChangeOperationException, RestApiException, OrmException,
- IOException {
- // For some cases, the REST API automatically creates a change edit resource
- // if it doesn't exist. We mimic this behavior here.
- try {
- return getChangeEditResource(filePath);
- } catch (ResourceNotFoundException e) {
- create();
- return getChangeEditResource(filePath);
- }
- }
-
private ChangeEditResource getChangeEditResource(String filePath)
throws ResourceNotFoundException, AuthException, IOException,
- InvalidChangeOperationException, OrmException {
+ OrmException {
return changeEdits.parse(changeResource, IdString.fromDecoded(filePath));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
index c57f5a0..9deb864 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
@@ -17,7 +17,6 @@
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.project.ChangeControl;
@@ -60,10 +59,6 @@
return getChangeResource().getControl();
}
- public Change getChange() {
- return edit.getChange();
- }
-
public ChangeEdit getChangeEdit() {
return edit;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index 6fba99f..640d785 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -38,8 +38,7 @@
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.edit.ChangeEditJson;
@@ -48,6 +47,7 @@
import com.google.gerrit.server.edit.UnchangedCommitMessageException;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -106,7 +106,7 @@
@Override
public ChangeEditResource parse(ChangeResource rsrc, IdString id)
throws ResourceNotFoundException, AuthException, IOException,
- InvalidChangeOperationException, OrmException {
+ OrmException {
Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
if (!edit.isPresent()) {
throw new ResourceNotFoundException(id);
@@ -118,7 +118,7 @@
@Override
public Create create(ChangeResource parent, IdString id)
throws RestApiException {
- return createFactory.create(parent.getChange(), id.get());
+ return createFactory.create(id.get());
}
@@ -148,58 +148,51 @@
RestModifyView<ChangeResource, Put.Input> {
interface Factory {
- Create create(Change change, String path);
+ Create create(String path);
}
- private final Provider<ReviewDb> db;
- private final ChangeEditUtil editUtil;
private final ChangeEditModifier editModifier;
- private final PatchSetUtil psUtil;
+ private final GitRepositoryManager repositoryManager;
private final Put putEdit;
- private final Change change;
private final String path;
@Inject
- Create(Provider<ReviewDb> db,
- ChangeEditUtil editUtil,
- ChangeEditModifier editModifier,
- PatchSetUtil psUtil,
+ Create(ChangeEditModifier editModifier,
+ GitRepositoryManager repositoryManager,
Put putEdit,
- @Assisted Change change,
@Assisted @Nullable String path) {
- this.db = db;
- this.editUtil = editUtil;
this.editModifier = editModifier;
- this.psUtil = psUtil;
+ this.repositoryManager = repositoryManager;
this.putEdit = putEdit;
- this.change = change;
this.path = path;
}
@Override
public Response<?> apply(ChangeResource resource, Put.Input input)
- throws AuthException, IOException, ResourceConflictException,
- OrmException, InvalidChangeOperationException {
- Optional<ChangeEdit> edit = editUtil.byChange(change);
- if (edit.isPresent()) {
- throw new ResourceConflictException(String.format(
- "edit already exists for the change %s",
- resource.getId()));
- }
- edit = createEdit(resource);
- if (!Strings.isNullOrEmpty(path)) {
- putEdit.apply(new ChangeEditResource(resource, edit.get(), path),
- input);
+ throws AuthException, ResourceConflictException, IOException,
+ OrmException {
+
+ if (Strings.isNullOrEmpty(path)) {
+ // TODO(aliceks): Consider the following:
+ // Generating an edit with a put (instead of a post) isn't mentioned in
+ // the documentation of the REST API. We should consider whether we want
+ // to keep this hidden 'feature', make it public via the documentation,
+ // or remove it.
+ createEdit(resource);
+ } else {
+ putEdit.apply(resource.getControl(), path, input.content);
}
return Response.none();
}
- private Optional<ChangeEdit> createEdit(ChangeResource resource)
- throws AuthException, IOException, ResourceConflictException,
- OrmException {
- editModifier.createEdit(change,
- psUtil.current(db.get(), resource.getNotes()));
- return editUtil.byChange(change);
+ private void createEdit(ChangeResource resource) throws AuthException,
+ IOException, OrmException, ResourceConflictException {
+ Project.NameKey project = resource.getProject();
+ try (Repository repository = repositoryManager.openRepository(project)) {
+ editModifier.createEdit(repository, resource.getControl());
+ } catch (InvalidChangeOperationException e) {
+ throw new ResourceConflictException(e.getMessage());
+ }
}
}
@@ -214,20 +207,17 @@
private final ChangeEditUtil editUtil;
private final ChangeEditModifier editModifier;
- private final PatchSetUtil psUtil;
- private final Provider<ReviewDb> db;
+ private final GitRepositoryManager repoManager;
private final String path;
@Inject
DeleteFile(ChangeEditUtil editUtil,
ChangeEditModifier editModifier,
- PatchSetUtil psUtil,
- Provider<ReviewDb> db,
+ GitRepositoryManager repoManager,
@Assisted String path) {
this.editUtil = editUtil;
this.editModifier = editModifier;
- this.psUtil = psUtil;
- this.db = db;
+ this.repoManager = repoManager;
this.path = path;
}
@@ -235,6 +225,10 @@
public Response<?> apply(ChangeResource rsrc, DeleteFile.Input in)
throws IOException, AuthException, ResourceConflictException,
OrmException, InvalidChangeOperationException, BadRequestException {
+ // TODO(aliceks): Consider the following:
+ // This check is strange. If there was already a change edit, this method
+ // wouldn't be called because DeleteContent would take over. We should
+ // consider to remove the positive case.
Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
if (edit.isPresent()) {
// Edit is wiped out
@@ -244,10 +238,10 @@
// Even if the latest patch set changed since the user triggered
// the operation, deleting the whole file is probably still what
// they intended.
- editModifier.createEdit(rsrc.getChange(),
- psUtil.current(db.get(), rsrc.getNotes()));
- edit = editUtil.byChange(rsrc.getChange());
- editModifier.deleteFile(edit.get(), path);
+ Project.NameKey project = rsrc.getProject();
+ try (Repository repository = repoManager.openRepository(project)) {
+ editModifier.deleteFile(repository, rsrc.getControl(), path);
+ }
}
return Response.none();
}
@@ -329,48 +323,46 @@
public String newPath;
}
- private final Provider<ReviewDb> db;
- private final ChangeEditUtil editUtil;
private final ChangeEditModifier editModifier;
- private final PatchSetUtil psUtil;
+ private final GitRepositoryManager repositoryManager;
@Inject
- Post(Provider<ReviewDb> db,
- ChangeEditUtil editUtil,
- ChangeEditModifier editModifier,
- PatchSetUtil psUtil) {
- this.db = db;
- this.editUtil = editUtil;
+ Post(ChangeEditModifier editModifier,
+ GitRepositoryManager repositoryManager) {
this.editModifier = editModifier;
- this.psUtil = psUtil;
+ this.repositoryManager = repositoryManager;
}
@Override
public Response<?> apply(ChangeResource resource, Post.Input input)
- throws AuthException, InvalidChangeOperationException, IOException,
- ResourceConflictException, OrmException {
- Optional<ChangeEdit> edit = editUtil.byChange(resource.getChange());
- if (!edit.isPresent()) {
- edit = createEdit(resource);
- }
-
- if (input != null) {
- if (!Strings.isNullOrEmpty(input.restorePath)) {
- editModifier.restoreFile(edit.get(), input.restorePath);
- } else if (!Strings.isNullOrEmpty(input.oldPath)
- && !Strings.isNullOrEmpty(input.newPath)) {
- editModifier.renameFile(edit.get(), input.oldPath, input.newPath);
+ throws AuthException, IOException, ResourceConflictException,
+ OrmException {
+ Project.NameKey project = resource.getProject();
+ try (Repository repository = repositoryManager.openRepository(project)) {
+ ChangeControl changeControl = resource.getControl();
+ if (isRestoreFile(input)) {
+ editModifier.restoreFile(repository, changeControl,
+ input.restorePath);
+ } else if (isRenameFile(input)) {
+ editModifier.renameFile(repository, changeControl, input.oldPath,
+ input.newPath);
+ } else {
+ editModifier.createEdit(repository, changeControl);
}
+ } catch (InvalidChangeOperationException e) {
+ throw new ResourceConflictException(e.getMessage());
}
return Response.none();
}
- private Optional<ChangeEdit> createEdit(ChangeResource resource)
- throws AuthException, IOException, ResourceConflictException,
- OrmException {
- editModifier.createEdit(resource.getChange(),
- psUtil.current(db.get(), resource.getNotes()));
- return editUtil.byChange(resource.getChange());
+ private static boolean isRestoreFile(Input input) {
+ return input != null && !Strings.isNullOrEmpty(input.restorePath);
+ }
+
+ private static boolean isRenameFile(Input input) {
+ return input != null
+ && !Strings.isNullOrEmpty(input.oldPath)
+ && !Strings.isNullOrEmpty(input.newPath);
}
}
@@ -387,26 +379,33 @@
}
private final ChangeEditModifier editModifier;
+ private final GitRepositoryManager repositoryManager;
@Inject
- Put(ChangeEditModifier editModifier) {
+ Put(ChangeEditModifier editModifier,
+ GitRepositoryManager repositoryManager) {
this.editModifier = editModifier;
+ this.repositoryManager = repositoryManager;
}
@Override
public Response<?> apply(ChangeEditResource rsrc, Input input)
- throws AuthException, ResourceConflictException {
- String path = rsrc.getPath();
+ throws AuthException, ResourceConflictException, IOException,
+ OrmException {
+ return apply(rsrc.getControl(), rsrc.getPath(), input.content);
+ }
+
+ public Response<?> apply(ChangeControl changeControl, String path,
+ RawInput newContent) throws ResourceConflictException, AuthException,
+ IOException, OrmException {
if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
throw new ResourceConflictException("Invalid path: " + path);
}
- try {
- editModifier.modifyFile(
- rsrc.getChangeEdit(),
- rsrc.getPath(),
- input.content);
- } catch (InvalidChangeOperationException | IOException e) {
+ Project.NameKey project = changeControl.getChange().getProject();
+ try (Repository repository = repositoryManager.openRepository(project)) {
+ editModifier.modifyFile(repository, changeControl, path, newContent);
+ } catch (InvalidChangeOperationException e) {
throw new ResourceConflictException(e.getMessage());
}
return Response.none();
@@ -426,18 +425,29 @@
}
private final ChangeEditModifier editModifier;
+ private final GitRepositoryManager repositoryManager;
@Inject
- DeleteContent(ChangeEditModifier editModifier) {
+ DeleteContent(ChangeEditModifier editModifier,
+ GitRepositoryManager repositoryManager) {
this.editModifier = editModifier;
+ this.repositoryManager = repositoryManager;
}
@Override
public Response<?> apply(ChangeEditResource rsrc, DeleteContent.Input input)
- throws AuthException, ResourceConflictException {
- try {
- editModifier.deleteFile(rsrc.getChangeEdit(), rsrc.getPath());
- } catch (InvalidChangeOperationException | IOException e) {
+ throws AuthException, ResourceConflictException, OrmException,
+ IOException {
+ return apply(rsrc.getControl(), rsrc.getPath());
+ }
+
+ public Response<?> apply(ChangeControl changeControl, String filePath)
+ throws AuthException, IOException, OrmException,
+ ResourceConflictException {
+ Project.NameKey project = changeControl.getChange().getProject();
+ try (Repository repository = repositoryManager.openRepository(project)) {
+ editModifier.deleteFile(repository, changeControl, filePath);
+ } catch (InvalidChangeOperationException e) {
throw new ResourceConflictException(e.getMessage());
}
return Response.none();
@@ -515,41 +525,30 @@
public String message;
}
- private final Provider<ReviewDb> db;
private final ChangeEditModifier editModifier;
- private final ChangeEditUtil editUtil;
- private final PatchSetUtil psUtil;
+ private final GitRepositoryManager repositoryManager;
@Inject
- EditMessage(Provider<ReviewDb> db,
- ChangeEditModifier editModifier,
- ChangeEditUtil editUtil,
- PatchSetUtil psUtil) {
- this.db = db;
+ EditMessage(ChangeEditModifier editModifier,
+ GitRepositoryManager repositoryManager) {
this.editModifier = editModifier;
- this.editUtil = editUtil;
- this.psUtil = psUtil;
+ this.repositoryManager = repositoryManager;
}
@Override
public Object apply(ChangeResource rsrc, Input input) throws AuthException,
- IOException, InvalidChangeOperationException, BadRequestException,
- ResourceConflictException, OrmException {
- Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
- if (!edit.isPresent()) {
- editModifier.createEdit(rsrc.getChange(),
- psUtil.current(db.get(), rsrc.getNotes()));
- edit = editUtil.byChange(rsrc.getChange());
- }
-
+ IOException, BadRequestException, ResourceConflictException,
+ OrmException {
if (input == null || Strings.isNullOrEmpty(input.message)) {
throw new BadRequestException("commit message must be provided");
}
- try {
- editModifier.modifyMessage(edit.get(), input.message);
- } catch (UnchangedCommitMessageException ucm) {
- throw new ResourceConflictException(ucm.getMessage());
+ Project.NameKey project = rsrc.getProject();
+ try (Repository repository = repositoryManager.openRepository(project)) {
+ ChangeControl changeControl = rsrc.getControl();
+ editModifier.modifyMessage(repository, changeControl, input.message);
+ } catch (UnchangedCommitMessageException e) {
+ throw new ResourceConflictException(e.getMessage());
}
return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
index ac1b770..db125eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
@@ -25,20 +25,17 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.edit.ChangeEditModifier;
-import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Repository;
+
import java.io.IOException;
-import java.util.Optional;
@Singleton
public class RebaseChangeEdit implements
@@ -78,41 +75,26 @@
public static class Input {
}
+ private final GitRepositoryManager repositoryManager;
private final ChangeEditModifier editModifier;
- private final ChangeEditUtil editUtil;
- private final PatchSetUtil psUtil;
- private final Provider<ReviewDb> db;
@Inject
- Rebase(ChangeEditModifier editModifier,
- ChangeEditUtil editUtil,
- PatchSetUtil psUtil,
- Provider<ReviewDb> db) {
+ Rebase(GitRepositoryManager repositoryManager,
+ ChangeEditModifier editModifier) {
+ this.repositoryManager = repositoryManager;
this.editModifier = editModifier;
- this.editUtil = editUtil;
- this.psUtil = psUtil;
- this.db = db;
}
@Override
public Response<?> apply(ChangeResource rsrc, Rebase.Input in)
throws AuthException, ResourceConflictException, IOException,
- InvalidChangeOperationException, OrmException {
- Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
- if (!edit.isPresent()) {
- throw new ResourceConflictException(String.format(
- "no edit exists for change %s",
- rsrc.getChange().getChangeId()));
+ OrmException {
+ Project.NameKey project = rsrc.getProject();
+ try (Repository repository = repositoryManager.openRepository(project)) {
+ editModifier.rebaseEdit(repository, rsrc.getControl());
+ } catch (InvalidChangeOperationException e) {
+ throw new ResourceConflictException(e.getMessage());
}
-
- PatchSet current = psUtil.current(db.get(), rsrc.getNotes());
- if (current.getId().equals(edit.get().getBasePatchSet().getId())) {
- throw new ResourceConflictException(String.format(
- "edit for change %s is already on latest patch set: %s",
- rsrc.getChange().getChangeId(),
- current.getId()));
- }
- editModifier.rebaseEdit(edit.get(), current);
return Response.none();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index d8046ce..e7e093a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -19,27 +19,25 @@
import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.RawInput;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
import com.google.gerrit.server.edit.tree.DeleteFileModification;
import com.google.gerrit.server.edit.tree.RenameFileModification;
import com.google.gerrit.server.edit.tree.RestoreFileModification;
import com.google.gerrit.server.edit.tree.TreeCreator;
import com.google.gerrit.server.edit.tree.TreeModification;
-import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -51,19 +49,18 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import java.io.IOException;
import java.sql.Timestamp;
-import java.util.Map;
+import java.util.Optional;
import java.util.TimeZone;
/**
@@ -78,321 +75,454 @@
public class ChangeEditModifier {
private final TimeZone tz;
- private final GitRepositoryManager gitManager;
private final ChangeIndexer indexer;
private final Provider<ReviewDb> reviewDb;
private final Provider<CurrentUser> currentUser;
- private final ChangeControl.GenericFactory changeControlFactory;
+ private final ChangeEditUtil changeEditUtil;
+ private final PatchSetUtil patchSetUtil;
@Inject
ChangeEditModifier(@GerritPersonIdent PersonIdent gerritIdent,
- GitRepositoryManager gitManager,
ChangeIndexer indexer,
Provider<ReviewDb> reviewDb,
Provider<CurrentUser> currentUser,
- ChangeControl.GenericFactory changeControlFactory) {
- this.gitManager = gitManager;
+ ChangeEditUtil changeEditUtil,
+ PatchSetUtil patchSetUtil) {
this.indexer = indexer;
this.reviewDb = reviewDb;
this.currentUser = currentUser;
this.tz = gerritIdent.getTimeZone();
- this.changeControlFactory = changeControlFactory;
+ this.changeEditUtil = changeEditUtil;
+ this.patchSetUtil = patchSetUtil;
}
/**
- * Create new change edit.
+ * Creates a new change edit.
*
- * @param change to create change edit for
- * @param ps patch set to create change edit on
- * @return result
- * @throws AuthException
- * @throws IOException
- * @throws ResourceConflictException When change edit already
- * exists for the change
- * @throws OrmException
+ * @param repository the affected Git repository
+ * @param changeControl the {@code ChangeControl} of the change for which
+ * the change edit should be created
+ * @throws AuthException if the user isn't authenticated or not allowed to
+ * use change edits
+ * @throws InvalidChangeOperationException if a change edit already existed
+ * for the change
*/
- public RefUpdate.Result createEdit(Change change, PatchSet ps)
- throws AuthException, IOException, ResourceConflictException, OrmException {
- if (!currentUser.get().isIdentifiedUser()) {
- throw new AuthException("Authentication required");
- }
- IdentifiedUser me = currentUser.get().asIdentifiedUser();
- String refPrefix = RefNames.refsEditPrefix(me.getAccountId(), change.getId());
+ public void createEdit(Repository repository, ChangeControl changeControl)
+ throws AuthException, IOException, InvalidChangeOperationException,
+ OrmException {
+ ensureAuthenticatedAndPermitted(changeControl);
- try {
- ChangeControl c =
- changeControlFactory.controlFor(reviewDb.get(), change, me);
- if (!c.canAddPatchSet(reviewDb.get())) {
- return RefUpdate.Result.REJECTED;
- }
- } catch (NoSuchChangeException e) {
- return RefUpdate.Result.NO_CHANGE;
+ Optional<ChangeEdit> changeEdit = lookupChangeEdit(changeControl);
+ if (changeEdit.isPresent()) {
+ throw new InvalidChangeOperationException(String.format("A change edit "
+ + "already exists for change %s", changeControl.getId()));
}
- try (Repository repo = gitManager.openRepository(change.getProject())) {
- Map<String, Ref> refs = repo.getRefDatabase().getRefs(refPrefix);
- if (!refs.isEmpty()) {
- throw new ResourceConflictException("edit already exists");
- }
-
- try (RevWalk rw = new RevWalk(repo)) {
- ObjectId revision = ObjectId.fromString(ps.getRevision().get());
- String editRefName = RefNames.refsEdit(me.getAccountId(), change.getId(),
- ps.getId());
- Result res = update(repo, me, editRefName, rw, ObjectId.zeroId(),
- revision, TimeUtil.nowTs());
- indexer.index(reviewDb.get(), change);
- return res;
- }
- }
+ PatchSet currentPatchSet = lookupCurrentPatchSet(changeControl);
+ ObjectId patchSetCommitId = getPatchSetCommitId(currentPatchSet);
+ createEditReference(repository, changeControl, currentPatchSet,
+ patchSetCommitId, TimeUtil.nowTs());
}
/**
* Rebase change edit on latest patch set
*
- * @param edit change edit that contains edit to rebase
- * @param current patch set to rebase the edit on
- * @throws AuthException
- * @throws ResourceConflictException thrown if rebase fails due to merge conflicts
- * @throws InvalidChangeOperationException
- * @throws IOException
+ * @param repository the affected Git repository
+ * @param changeControl the {@code ChangeControl} of the change whose change
+ * edit should be rebased
+ * @throws AuthException if the user isn't authenticated or not allowed to
+ * use change edits
+ * @throws InvalidChangeOperationException if a change edit doesn't exist
+ * for the specified change, the change edit is already based on the latest
+ * patch set, or the change represents the root commit
+ * @throws MergeConflictException if rebase fails due to merge conflicts
*/
- public void rebaseEdit(ChangeEdit edit, PatchSet current)
- throws AuthException, ResourceConflictException,
- InvalidChangeOperationException, IOException {
- if (!currentUser.get().isIdentifiedUser()) {
- throw new AuthException("Authentication required");
+ public void rebaseEdit(Repository repository, ChangeControl changeControl)
+ throws AuthException, InvalidChangeOperationException, IOException,
+ OrmException, MergeConflictException {
+ ensureAuthenticatedAndPermitted(changeControl);
+
+ Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
+ if (!optionalChangeEdit.isPresent()) {
+ throw new InvalidChangeOperationException(String.format(
+ "No change edit exists for change %s", changeControl.getId()));
+ }
+ ChangeEdit changeEdit = optionalChangeEdit.get();
+
+ PatchSet currentPatchSet = lookupCurrentPatchSet(changeControl);
+ if (isBasedOn(changeEdit, currentPatchSet)) {
+ throw new InvalidChangeOperationException(String.format(
+ "Change edit for change %s is already based on latest patch set %s",
+ changeControl.getId(), currentPatchSet.getId()));
}
- Change change = edit.getChange();
- IdentifiedUser me = currentUser.get().asIdentifiedUser();
- String refName = RefNames.refsEdit(me.getAccountId(), change.getId(),
- current.getId());
- try (Repository repo = gitManager.openRepository(change.getProject());
- RevWalk rw = new RevWalk(repo);
- ObjectInserter inserter = repo.newObjectInserter()) {
- BatchRefUpdate ru = repo.getRefDatabase().newBatchUpdate();
- RevCommit editCommit = edit.getEditCommit();
- if (editCommit.getParentCount() == 0) {
- throw new InvalidChangeOperationException(
- "Rebase edit against root commit not supported");
- }
- RevCommit tip = rw.parseCommit(ObjectId.fromString(
- current.getRevision().get()));
- ThreeWayMerger m = MergeStrategy.RESOLVE.newMerger(repo, true);
- m.setObjectInserter(inserter);
- m.setBase(ObjectId.fromString(
- edit.getBasePatchSet().getRevision().get()));
+ rebase(repository, changeEdit, currentPatchSet);
+ }
- if (m.merge(tip, editCommit)) {
- ObjectId tree = m.getResultTreeId();
-
- CommitBuilder commit = new CommitBuilder();
- commit.setTreeId(tree);
- for (int i = 0; i < tip.getParentCount(); i++) {
- commit.addParentId(tip.getParent(i));
- }
- commit.setAuthor(editCommit.getAuthorIdent());
- commit.setCommitter(new PersonIdent(
- editCommit.getCommitterIdent(), TimeUtil.nowTs()));
- commit.setMessage(editCommit.getFullMessage());
- ObjectId newEdit = inserter.insert(commit);
- inserter.flush();
-
- ru.addCommand(new ReceiveCommand(ObjectId.zeroId(), newEdit,
- refName));
- ru.addCommand(new ReceiveCommand(edit.getRef().getObjectId(),
- ObjectId.zeroId(), edit.getRefName()));
- ru.execute(rw, NullProgressMonitor.INSTANCE);
- for (ReceiveCommand cmd : ru.getCommands()) {
- if (cmd.getResult() != ReceiveCommand.Result.OK) {
- throw new IOException("failed: " + cmd);
- }
- }
- } else {
- // TODO(davido): Allow to resolve conflicts inline
- throw new ResourceConflictException("merge conflict");
- }
+ private void rebase(Repository repository, ChangeEdit changeEdit,
+ PatchSet currentPatchSet) throws IOException, MergeConflictException,
+ InvalidChangeOperationException, OrmException {
+ RevCommit currentEditCommit = changeEdit.getEditCommit();
+ if (currentEditCommit.getParentCount() == 0) {
+ throw new InvalidChangeOperationException(
+ "Rebase change edit against root commit not supported");
}
+
+ Change change = changeEdit.getChange();
+ RevCommit basePatchSetCommit = lookupCommit(repository, currentPatchSet);
+ RevTree basePatchSetTree = basePatchSetCommit.getTree();
+
+ ObjectId newTreeId = merge(repository, changeEdit, basePatchSetTree);
+ Timestamp nowTimestamp = TimeUtil.nowTs();
+ String commitMessage = currentEditCommit.getFullMessage();
+ ObjectId newEditCommitId = createCommit(repository, basePatchSetCommit,
+ newTreeId, commitMessage, nowTimestamp);
+
+ String newEditRefName = getEditRefName(change, currentPatchSet);
+ updateReferenceWithNameChange(repository, changeEdit.getRefName(),
+ currentEditCommit, newEditRefName, newEditCommitId, nowTimestamp);
+ reindex(change);
}
/**
- * Modify commit message in existing change edit.
+ * Modifies the commit message of a change edit. If the change edit doesn't
+ * exist, a new one will be created based on the current patch set.
*
- * @param edit change edit
- * @param msg new commit message
- * @return result
- * @throws AuthException
- * @throws InvalidChangeOperationException
- * @throws IOException
- * @throws UnchangedCommitMessageException
+ * @param repository the affected Git repository
+ * @param changeControl the {@code ChangeControl} of the change whose change
+ * edit's message should be modified
+ * @param newCommitMessage the new commit message
+ * @throws AuthException if the user isn't authenticated or not allowed to
+ * use change edits
+ * @throws UnchangedCommitMessageException if the commit message is the same
+ * as before
*/
- public RefUpdate.Result modifyMessage(ChangeEdit edit, String msg)
- throws AuthException, InvalidChangeOperationException, IOException,
- UnchangedCommitMessageException {
- msg = msg.trim() + "\n";
- checkState(!Strings.isNullOrEmpty(msg), "message cannot be null");
- if (!currentUser.get().isIdentifiedUser()) {
- throw new AuthException("Authentication required");
- }
+ public void modifyMessage(Repository repository, ChangeControl changeControl,
+ String newCommitMessage) throws AuthException, IOException,
+ UnchangedCommitMessageException, OrmException {
+ ensureAuthenticatedAndPermitted(changeControl);
+ newCommitMessage = getWellFormedCommitMessage(newCommitMessage);
- RevCommit prevEdit = edit.getEditCommit();
- if (prevEdit.getFullMessage().equals(msg)) {
+ Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
+ PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl);
+ RevCommit basePatchSetCommit = lookupCommit(repository, basePatchSet);
+ RevCommit baseCommit = optionalChangeEdit.map(ChangeEdit::getEditCommit)
+ .orElse(basePatchSetCommit);
+
+ String currentCommitMessage = baseCommit.getFullMessage();
+ if (newCommitMessage.equals(currentCommitMessage)) {
throw new UnchangedCommitMessageException();
}
- IdentifiedUser me = currentUser.get().asIdentifiedUser();
- Project.NameKey project = edit.getChange().getProject();
- try (Repository repo = gitManager.openRepository(project);
- RevWalk rw = new RevWalk(repo);
- ObjectInserter inserter = repo.newObjectInserter()) {
- String refName = edit.getRefName();
- Timestamp now = TimeUtil.nowTs();
- ObjectId commit = createCommit(me, inserter, prevEdit,
- prevEdit.getTree(),
- msg, now);
- inserter.flush();
- return update(repo, me, refName, rw, prevEdit, commit, now);
+ RevTree baseTree = baseCommit.getTree();
+ Timestamp nowTimestamp = TimeUtil.nowTs();
+ ObjectId newEditCommit = createCommit(repository, basePatchSetCommit,
+ baseTree, newCommitMessage, nowTimestamp);
+
+ if (optionalChangeEdit.isPresent()) {
+ updateEditReference(repository, optionalChangeEdit.get(), newEditCommit,
+ nowTimestamp);
+ } else {
+ createEditReference(repository, changeControl, basePatchSet,
+ newEditCommit, nowTimestamp);
}
}
/**
- * Modify file in existing change edit from its base commit.
+ * Modifies the contents of a file of a change edit. If the change edit
+ * doesn't exist, a new one will be created based on the current patch set.
*
- * @param edit change edit
- * @param file path to modify
- * @param content new content
- * @return result
- * @throws AuthException
- * @throws InvalidChangeOperationException
- * @throws IOException
+ * @param repository the affected Git repository
+ * @param changeControl the {@code ChangeControl} of the change whose change
+ * edit should be modified
+ * @param filePath the path of the file whose contents should be modified
+ * @param newContent the new file content
+ * @throws AuthException if the user isn't authenticated or not allowed to
+ * use change edits
+ * @throws InvalidChangeOperationException if the file already had the
+ * specified content
*/
- public RefUpdate.Result modifyFile(ChangeEdit edit,
- String file, RawInput content) throws AuthException,
- InvalidChangeOperationException, IOException {
- return modify(edit, new ChangeFileContentModification(file, content));
+ public void modifyFile(Repository repository, ChangeControl changeControl,
+ String filePath, RawInput newContent) throws AuthException,
+ InvalidChangeOperationException, IOException, OrmException {
+ modifyTree(repository, changeControl,
+ new ChangeFileContentModification(filePath, newContent));
}
/**
- * Delete file in existing change edit.
+ * Deletes a file from the Git tree of a change edit. If the change edit
+ * doesn't exist, a new one will be created based on the current patch set.
*
- * @param edit change edit
- * @param file path to delete
- * @return result
- * @throws AuthException
- * @throws InvalidChangeOperationException
- * @throws IOException
+ * @param repository the affected Git repository
+ * @param changeControl the {@code ChangeControl} of the change whose change
+ * edit should be modified
+ * @param file path of the file which should be deleted
+ * @throws AuthException if the user isn't authenticated or not allowed to
+ * use change edits
+ * @throws InvalidChangeOperationException if the file does not exist
*/
- public RefUpdate.Result deleteFile(ChangeEdit edit,
+ public void deleteFile(Repository repository, ChangeControl changeControl,
String file) throws AuthException, InvalidChangeOperationException,
- IOException {
- return modify(edit, new DeleteFileModification(file));
+ IOException, OrmException {
+ modifyTree(repository, changeControl, new DeleteFileModification(file));
}
/**
- * Rename file in existing change edit.
+ * Renames a file of a change edit or moves it to another directory. If the
+ * change edit doesn't exist, a new one will be created based on the current
+ * patch set.
*
- * @param edit change edit
- * @param file path to rename
- * @param newFile path to rename the file to
- * @return result
- * @throws AuthException
- * @throws InvalidChangeOperationException
- * @throws IOException
+ * @param repository the affected Git repository
+ * @param changeControl the {@code ChangeControl} of the change whose change
+ * edit should be modified
+ * @param currentFilePath the current path/name of the file
+ * @param newFilePath the desired path/name of the file
+ * @throws AuthException if the user isn't authenticated or not allowed to
+ * use change edits
+ * @throws InvalidChangeOperationException if the file was already renamed
+ * to the specified new name
*/
- public RefUpdate.Result renameFile(ChangeEdit edit, String file,
- String newFile) throws AuthException, InvalidChangeOperationException,
- IOException {
- RevCommit editCommit = edit.getEditCommit();
- return modify(edit, new RenameFileModification(editCommit, file, newFile));
+ public void renameFile(Repository repository, ChangeControl changeControl,
+ String currentFilePath, String newFilePath) throws AuthException,
+ InvalidChangeOperationException, IOException, OrmException {
+ modifyTree(repository, changeControl,
+ new RenameFileModification(currentFilePath, newFilePath));
}
/**
- * Restore file in existing change edit.
+ * Restores a file of a change edit to the state it was in before the patch
+ * set on which the change edit is based. If the change edit doesn't exist, a
+ * new one will be created based on the current patch set.
*
- * @param edit change edit
- * @param file path to restore
- * @return result
- * @throws AuthException
- * @throws InvalidChangeOperationException
- * @throws IOException
+ * @param repository the affected Git repository
+ * @param changeControl the {@code ChangeControl} of the change whose change
+ * edit should be modified
+ * @param file the path of the file which should be restored
+ * @throws AuthException if the user isn't authenticated or not allowed to
+ * use change edits
+ * @throws InvalidChangeOperationException if the file was already restored
*/
- public RefUpdate.Result restoreFile(ChangeEdit edit,
+ public void restoreFile(Repository repository, ChangeControl changeControl,
String file) throws AuthException, InvalidChangeOperationException,
- IOException {
- RevCommit editCommit = edit.getEditCommit();
- return modify(edit, new RestoreFileModification(editCommit, file));
+ IOException, OrmException {
+ modifyTree(repository, changeControl, new RestoreFileModification(file));
}
- private RefUpdate.Result modify(ChangeEdit edit,
+ private void modifyTree(Repository repository, ChangeControl changeControl,
TreeModification treeModification) throws AuthException, IOException,
- InvalidChangeOperationException {
+ OrmException, InvalidChangeOperationException {
+ ensureAuthenticatedAndPermitted(changeControl);
+
+ Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
+ PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl);
+ RevCommit basePatchSetCommit = lookupCommit(repository, basePatchSet);
+ RevCommit baseCommit = optionalChangeEdit.map(ChangeEdit::getEditCommit)
+ .orElse(basePatchSetCommit);
+
+ ObjectId newTreeId = createNewTree(repository, baseCommit,
+ treeModification);
+
+ String commitMessage = baseCommit.getFullMessage();
+ Timestamp nowTimestamp = TimeUtil.nowTs();
+ ObjectId newEditCommit = createCommit(repository, basePatchSetCommit,
+ newTreeId, commitMessage, nowTimestamp);
+
+ if (optionalChangeEdit.isPresent()) {
+ updateEditReference(repository, optionalChangeEdit.get(), newEditCommit,
+ nowTimestamp);
+ } else {
+ createEditReference(repository, changeControl, basePatchSet,
+ newEditCommit, nowTimestamp);
+ }
+ }
+
+ private void ensureAuthenticatedAndPermitted(ChangeControl changeControl)
+ throws AuthException, OrmException {
+ ensureAuthenticated();
+ ensurePermitted(changeControl);
+ }
+
+ private void ensureAuthenticated() throws AuthException {
if (!currentUser.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
- IdentifiedUser me = currentUser.get().asIdentifiedUser();
- Project.NameKey project = edit.getChange().getProject();
- try (Repository repo = gitManager.openRepository(project);
- RevWalk rw = new RevWalk(repo);
- ObjectInserter inserter = repo.newObjectInserter()) {
- String refName = edit.getRefName();
- RevCommit prevEdit = edit.getEditCommit();
+ }
- TreeCreator treeCreator = new TreeCreator(prevEdit.getTree());
- treeCreator.addTreeModification(treeModification);
- ObjectId newTree = treeCreator.createNewTreeAndGetId(repo);
-
- if (ObjectId.equals(newTree, prevEdit.getTree())) {
- throw new InvalidChangeOperationException("no changes were made");
- }
-
- Timestamp now = TimeUtil.nowTs();
- ObjectId commit = createCommit(me, inserter, prevEdit, newTree, now);
- inserter.flush();
- return update(repo, me, refName, rw, prevEdit, commit, now);
+ private void ensurePermitted(ChangeControl changeControl)
+ throws OrmException, AuthException {
+ if (!changeControl.canAddPatchSet(reviewDb.get())) {
+ throw new AuthException("Not allowed to edit a change.");
}
}
- private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter,
- RevCommit revision, ObjectId tree, Timestamp when) throws IOException {
- return createCommit(me, inserter, revision, tree,
- revision.getFullMessage(), when);
+ private String getWellFormedCommitMessage(String commitMessage) {
+ String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim();
+ checkState(!wellFormedMessage.isEmpty(),
+ "Commit message cannot be null or empty");
+ wellFormedMessage = wellFormedMessage + "\n";
+ return wellFormedMessage;
}
- private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter,
- RevCommit revision, ObjectId tree, String msg, Timestamp when)
+ private Optional<ChangeEdit> lookupChangeEdit(ChangeControl changeControl)
+ throws AuthException, IOException {
+ return changeEditUtil.byChange(changeControl);
+ }
+
+ private PatchSet getBasePatchSet(Optional<ChangeEdit> optionalChangeEdit,
+ ChangeControl changeControl) throws OrmException {
+ Optional<PatchSet> editBasePatchSet =
+ optionalChangeEdit.map(ChangeEdit::getBasePatchSet);
+ return editBasePatchSet.isPresent()
+ ? editBasePatchSet.get()
+ : lookupCurrentPatchSet(changeControl);
+ }
+
+ private PatchSet lookupCurrentPatchSet(ChangeControl changeControl)
+ throws OrmException {
+ return patchSetUtil.current(reviewDb.get(), changeControl.getNotes());
+ }
+
+ private static boolean isBasedOn(ChangeEdit changeEdit, PatchSet patchSet) {
+ PatchSet editBasePatchSet = changeEdit.getBasePatchSet();
+ return editBasePatchSet.getId().equals(patchSet.getId());
+ }
+
+ private static RevCommit lookupCommit(Repository repository,
+ PatchSet patchSet) throws IOException {
+ ObjectId patchSetCommitId = getPatchSetCommitId(patchSet);
+ try (RevWalk revWalk = new RevWalk(repository)) {
+ return revWalk.parseCommit(patchSetCommitId);
+ }
+ }
+
+ private static ObjectId createNewTree(Repository repository,
+ RevCommit baseCommit, TreeModification treeModification)
+ throws IOException, InvalidChangeOperationException {
+ TreeCreator treeCreator = new TreeCreator(baseCommit);
+ treeCreator.addTreeModification(treeModification);
+ ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+
+ if (ObjectId.equals(newTreeId, baseCommit.getTree())) {
+ throw new InvalidChangeOperationException("no changes were made");
+ }
+ return newTreeId;
+ }
+
+ private ObjectId merge(Repository repository, ChangeEdit changeEdit,
+ ObjectId newTreeId) throws IOException, MergeConflictException {
+ PatchSet basePatchSet = changeEdit.getBasePatchSet();
+ ObjectId basePatchSetCommitId = getPatchSetCommitId(basePatchSet);
+ ObjectId editCommitId = changeEdit.getEditCommit();
+
+ ThreeWayMerger threeWayMerger =
+ MergeStrategy.RESOLVE.newMerger(repository, true);
+ threeWayMerger.setBase(basePatchSetCommitId);
+ boolean successful = threeWayMerger.merge(newTreeId, editCommitId);
+
+ if (!successful) {
+ throw new MergeConflictException(
+ "The existing change edit could not be merged with another tree.");
+ }
+ return threeWayMerger.getResultTreeId();
+ }
+
+ private ObjectId createCommit(Repository repository,
+ RevCommit basePatchSetCommit, ObjectId tree, String commitMessage,
+ Timestamp timestamp) throws IOException {
+ try (ObjectInserter objectInserter = repository.newObjectInserter()) {
+ CommitBuilder builder = new CommitBuilder();
+ builder.setTreeId(tree);
+ builder.setParentIds(basePatchSetCommit.getParents());
+ builder.setAuthor(basePatchSetCommit.getAuthorIdent());
+ builder.setCommitter(getCommitterIdent(timestamp));
+ builder.setMessage(commitMessage);
+ ObjectId newCommitId = objectInserter.insert(builder);
+ objectInserter.flush();
+ return newCommitId;
+ }
+ }
+
+ private PersonIdent getCommitterIdent(Timestamp commitTimestamp) {
+ IdentifiedUser user = currentUser.get().asIdentifiedUser();
+ return user.newCommitterIdent(commitTimestamp, tz);
+ }
+
+ private static ObjectId getPatchSetCommitId(PatchSet patchSet) {
+ return ObjectId.fromString(patchSet.getRevision().get());
+ }
+
+ private void createEditReference(Repository repository,
+ ChangeControl changeControl, PatchSet basePatchSet,
+ ObjectId newEditCommit, Timestamp timestamp)
+ throws IOException, OrmException {
+ Change change = changeControl.getChange();
+ String editRefName = getEditRefName(change, basePatchSet);
+ updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommit,
+ timestamp);
+ reindex(change);
+ }
+
+ private String getEditRefName(Change change, PatchSet basePatchSet) {
+ IdentifiedUser me = currentUser.get().asIdentifiedUser();
+ return RefNames.refsEdit(me.getAccountId(), change.getId(),
+ basePatchSet.getId());
+ }
+
+ private void updateEditReference(Repository repository, ChangeEdit changeEdit,
+ ObjectId newEditCommit, Timestamp timestamp)
+ throws IOException, OrmException {
+ String editRefName = changeEdit.getRefName();
+ RevCommit currentEditCommit = changeEdit.getEditCommit();
+ updateReference(repository, editRefName, currentEditCommit, newEditCommit,
+ timestamp);
+ reindex(changeEdit.getChange());
+ }
+
+ private void updateReference(Repository repository, String refName,
+ ObjectId currentObjectId, ObjectId targetObjectId, Timestamp timestamp)
throws IOException {
- CommitBuilder builder = new CommitBuilder();
- builder.setTreeId(tree);
- builder.setParentIds(revision.getParents());
- builder.setAuthor(revision.getAuthorIdent());
- builder.setCommitter(getCommitterIdent(me, when));
- builder.setMessage(msg);
- return inserter.insert(builder);
- }
-
- private RefUpdate.Result update(Repository repo, IdentifiedUser me,
- String refName, RevWalk rw, ObjectId oldObjectId, ObjectId newEdit,
- Timestamp when) throws IOException {
- RefUpdate ru = repo.updateRef(refName);
- ru.setExpectedOldObjectId(oldObjectId);
- ru.setNewObjectId(newEdit);
- ru.setRefLogIdent(getRefLogIdent(me, when));
+ RefUpdate ru = repository.updateRef(refName);
+ ru.setExpectedOldObjectId(currentObjectId);
+ ru.setNewObjectId(targetObjectId);
+ ru.setRefLogIdent(getRefLogIdent(timestamp));
ru.setRefLogMessage("inline edit (amend)", false);
ru.setForceUpdate(true);
- RefUpdate.Result res = ru.update(rw);
- if (res != RefUpdate.Result.NEW &&
- res != RefUpdate.Result.FORCED) {
- throw new IOException("update failed: " + ru);
+ try (RevWalk revWalk = new RevWalk(repository)) {
+ RefUpdate.Result res = ru.update(revWalk);
+ if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) {
+ throw new IOException("update failed: " + ru);
+ }
}
- return res;
}
- private PersonIdent getCommitterIdent(IdentifiedUser user, Timestamp when) {
- return user.newCommitterIdent(when, tz);
+ private void updateReferenceWithNameChange(Repository repository,
+ String currentRefName, ObjectId currentObjectId, String newRefName,
+ ObjectId targetObjectId, Timestamp timestamp)
+ throws IOException {
+ BatchRefUpdate batchRefUpdate =
+ repository.getRefDatabase().newBatchUpdate();
+ batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(),
+ targetObjectId, newRefName));
+ batchRefUpdate.addCommand(new ReceiveCommand(currentObjectId,
+ ObjectId.zeroId(), currentRefName));
+ batchRefUpdate.setRefLogMessage("rebase edit", false);
+ batchRefUpdate.setRefLogIdent(getRefLogIdent(timestamp));
+ try (RevWalk revWalk = new RevWalk(repository)) {
+ batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
+ }
+ for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
+ if (cmd.getResult() != ReceiveCommand.Result.OK) {
+ throw new IOException("failed: " + cmd);
+ }
+ }
}
- private PersonIdent getRefLogIdent(IdentifiedUser user, Timestamp when) {
- return user.newRefLogIdent(when, tz);
+ private PersonIdent getRefLogIdent(Timestamp timestamp) {
+ IdentifiedUser user = currentUser.get().asIdentifiedUser();
+ return user.newRefLogIdent(timestamp, tz);
+ }
+
+ private void reindex(Change change) throws IOException, OrmException {
+ indexer.index(reviewDb.get(), change);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index fbe4bef..e83155d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -27,6 +27,7 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -52,7 +53,8 @@
}
@Override
- public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository) {
+ public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository,
+ RevCommit baseCommit) {
DirCacheEditor.PathEdit changeContentEdit = new ChangeContent(filePath,
newContent, repository);
return Collections.singletonList(changeContentEdit);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
index 887fb4f..64fe63b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
@@ -16,6 +16,7 @@
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import java.util.Collections;
import java.util.List;
@@ -32,7 +33,8 @@
}
@Override
- public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository) {
+ public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository,
+ RevCommit baseCommit) {
DirCacheEditor.DeletePath deletePathEdit =
new DirCacheEditor.DeletePath(filePath);
return Collections.singletonList(deletePathEdit);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
index 4978446..32c1e0d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
@@ -31,24 +31,22 @@
*/
public class RenameFileModification implements TreeModification {
- private final RevCommit currentCommit;
private final String currentFilePath;
private final String newFilePath;
- public RenameFileModification(RevCommit currentCommit,
- String currentFilePath, String newFilePath) {
- this.currentCommit = currentCommit;
+ public RenameFileModification(String currentFilePath, String newFilePath) {
this.currentFilePath = currentFilePath;
this.newFilePath = newFilePath;
}
@Override
- public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository)
+ public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository,
+ RevCommit baseCommit)
throws IOException {
try (RevWalk revWalk = new RevWalk(repository)) {
- revWalk.parseHeaders(currentCommit);
+ revWalk.parseHeaders(baseCommit);
try (TreeWalk treeWalk = TreeWalk.forPath(revWalk.getObjectReader(),
- currentFilePath, currentCommit.getTree())) {
+ currentFilePath, baseCommit.getTree())) {
if (treeWalk == null) {
return Collections.emptyList();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
index cbe40a3..b5cc9e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
@@ -30,24 +30,23 @@
*/
public class RestoreFileModification implements TreeModification {
- private final RevCommit currentCommit;
private final String filePath;
- public RestoreFileModification(RevCommit currentCommit, String filePath) {
- this.currentCommit = currentCommit;
+ public RestoreFileModification(String filePath) {
this.filePath = filePath;
}
@Override
- public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository)
+ public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository,
+ RevCommit baseCommit)
throws IOException {
- if (currentCommit.getParentCount() == 0) {
+ if (baseCommit.getParentCount() == 0) {
DirCacheEditor.DeletePath deletePath =
new DirCacheEditor.DeletePath(filePath);
return Collections.singletonList(deletePath);
}
- RevCommit base = currentCommit.getParent(0);
+ RevCommit base = baseCommit.getParent(0);
try (RevWalk revWalk = new RevWalk(repository)) {
revWalk.parseHeaders(base);
try (TreeWalk treeWalk = TreeWalk.forPath(revWalk.getObjectReader(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java
index ed8d719..661956f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java
@@ -24,25 +24,25 @@
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevCommit;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
/**
- * A creator for a new Git tree. To create the new tree, another tree is taken
- * as a basis and modified.
+ * A creator for a new Git tree. To create the new tree, the tree of another
+ * commit is taken as a basis and modified.
*/
public class TreeCreator {
- private final RevTree baseTree;
+ private final RevCommit baseCommit;
// At the moment, a list wouldn't be necessary as only one modification is
// applied per created tree. This is going to change in the near future.
private final List<TreeModification> treeModifications = new LinkedList<>();
- public TreeCreator(RevTree baseTree) {
- this.baseTree = checkNotNull(baseTree, "baseTree is required");
+ public TreeCreator(RevCommit baseCommit) {
+ this.baseCommit = checkNotNull(baseCommit, "baseCommit is required");
}
/**
@@ -85,7 +85,7 @@
DirCache dirCache = DirCache.newInCore();
DirCacheBuilder dirCacheBuilder = dirCache.builder();
dirCacheBuilder.addTree(new byte[0], DirCacheEntry.STAGE_0, objectReader,
- baseTree);
+ baseCommit.getTree());
dirCacheBuilder.finish();
return dirCache;
}
@@ -95,7 +95,7 @@
throws IOException {
List<DirCacheEditor.PathEdit> pathEdits = new LinkedList<>();
for (TreeModification treeModification : treeModifications) {
- pathEdits.addAll(treeModification.getPathEdits(repository));
+ pathEdits.addAll(treeModification.getPathEdits(repository, baseCommit));
}
return pathEdits;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java
index bbb10ec..4b66cd4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java
@@ -16,6 +16,7 @@
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import java.io.IOException;
import java.util.List;
@@ -31,10 +32,11 @@
* {@code PathEdit}s can be crucial and hence shouldn't be changed.
*
* @param repository the affected Git repository
+ * @param baseCommit the commit to whose tree this modification is applied
* @return an ordered list of necessary {@code PathEdit}s
* @throws IOException if problems arise when accessing the repository
*/
- List<DirCacheEditor.PathEdit> getPathEdits(Repository repository)
- throws IOException;
+ List<DirCacheEditor.PathEdit> getPathEdits(Repository repository,
+ RevCommit baseCommit) throws IOException;
}
diff --git a/plugins/external_plugin_deps.bzl b/plugins/external_plugin_deps.bzl
new file mode 100644
index 0000000..391f920
--- /dev/null
+++ b/plugins/external_plugin_deps.bzl
@@ -0,0 +1,2 @@
+def external_plugin_deps():
+ pass
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index b7a76a2..f54cfc9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -19,6 +19,7 @@
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -38,7 +39,8 @@
section {
display: inline-block;
}
- gr-button {
+ gr-button,
+ gr-dropdown {
margin-left: .5em;
}
gr-button:before {
@@ -63,29 +65,29 @@
}
</style>
<div>
- <section hidden$="[[_shouldHideActions(actions.*, _additionalActions.*, _loading)]]">
- <template is="dom-repeat" items="[[_changeActionValues]]" as="action">
+ <gr-dropdown
+ id="moreActions"
+ down-arrow
+ vertical-offset="32"
+ horizontal-align="right"
+ on-tap-item-abandon="_handleAbandonTap"
+ on-tap-item-cherrypick="_handleCherrypickTap"
+ on-tap-item-delete="_handleDeleteTap"
+ hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]"
+ items="[[_menuActions]]">More</gr-dropdown>
+ <section hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]">
+ <template
+ is="dom-repeat"
+ items="[[_topLevelActions]]"
+ as="action">
<gr-button title$="[[action.title]]"
- hidden$="[[_computeActionHidden(action.__key, _hiddenChangeActions.*)]]"
+ hidden$="[[_computeActionHidden(action.__key, _hiddenActions.*)]]"
primary$="[[action.__primary]]"
- hidden$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
data-label$="[[action.label]]"
data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
- on-tap="_handleActionTap"></gr-button>
- </template>
- </section>
- <section hidden$="[[_shouldHideActions(revisionActions.*, _additionalActions.*, _loading)]]">
- <template is="dom-repeat" items="[[_revisionActionValues]]" as="action">
- <gr-button title$="[[action.title]]"
- hidden$="[[_computeActionHidden(action.__key, _hiddenRevisionActions.*)]]"
- primary$="[[action.__primary]]"
disabled$="[[!action.enabled]]"
- data-action-key$="[[action.__key]]"
- data-action-type$="[[action.__type]]"
- data-label$="[[action.label]]"
- data-loading-label$="[[_computeLoadingLabel(action.__key)]]"
on-tap="_handleActionTap"></gr-button>
</template>
</section>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 6fdc3ad..331c3c5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -87,6 +87,16 @@
method: 'POST',
};
+ /**
+ * Keys for actions to appear in the overflow menu rather than the top-level
+ * set of action buttons.
+ */
+ var MENU_ACTION_KEYS = [
+ 'abandon',
+ 'cherrypick',
+ '/', // '/' is the key for the delete action.
+ ];
+
Polymer({
is: 'gr-change-actions',
@@ -96,6 +106,12 @@
* @event reload-change
*/
+ /**
+ * Fired when an action is tapped.
+ *
+ * @event <action key>-tap
+ */
+
properties: {
change: Object,
actions: {
@@ -129,25 +145,26 @@
type: Boolean,
value: true,
},
- _revisionActionValues: {
+
+ _allActionValues: {
type: Array,
- computed: '_computeRevisionActionValues(revisionActions.*, ' +
- 'primaryActionKeys.*, _additionalActions.*)',
- },
- _changeActionValues: {
- type: Array,
- computed: '_computeChangeActionValues(actions.*, ' +
+ computed: '_computeAllActions(actions.*, revisionActions.*,' +
'primaryActionKeys.*, _additionalActions.*, change)',
},
+ _topLevelActions: {
+ type: Array,
+ computed: '_computeTopLevelActions(_allActionValues.*)',
+ },
+ _menuActions: {
+ type: Array,
+ computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*)',
+ },
+
_additionalActions: {
type: Array,
value: function() { return []; },
},
- _hiddenChangeActions: {
- type: Array,
- value: function() { return []; },
- },
- _hiddenRevisionActions: {
+ _hiddenActions: {
type: Array,
value: function() { return []; },
},
@@ -220,20 +237,15 @@
},
setActionHidden: function(type, key, hidden) {
- var path;
- if (type === ActionType.CHANGE) {
- path = '_hiddenChangeActions';
- } else if (type === ActionType.REVISION) {
- path = '_hiddenRevisionActions';
- } else {
+ if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
throw Error('Invalid action type given: ' + type);
}
- var idx = this.get(path).indexOf(key);
+ var idx = this._hiddenActions.indexOf(key);
if (hidden && idx === -1) {
- this.push(path, key);
+ this.push('_hiddenActions', key);
} else if (!hidden && idx !== -1) {
- this.splice(path, idx, 1);
+ this.splice('_hiddenActions', idx, 1);
}
},
@@ -251,16 +263,8 @@
this.patchNum);
},
- _shouldHideActions: function(actionsRecord, additionalActionsRecord,
- loading) {
- if (loading) { return true; }
- return !this._actionCount(actionsRecord, additionalActionsRecord);
- },
-
- _actionCount: function(actionsRecord, additionalActionsRecord) {
- var additionalActions = (additionalActionsRecord &&
- additionalActionsRecord.base) || [];
- return this._keyCount(actionsRecord) + additionalActions.length;
+ _shouldHideActions: function(actions, loading) {
+ return loading || !actions || !actions.base || !actions.base.length;
},
_keyCount: function(changeRecord) {
@@ -282,24 +286,6 @@
});
},
- _computeRevisionActionValues: function(actionsChangeRecord,
- primariesChangeRecord, additionalActionsChangeRecord) {
- return this._getActionValues(actionsChangeRecord, primariesChangeRecord,
- additionalActionsChangeRecord, ActionType.REVISION);
- },
-
- _computeChangeActionValues: function(actionsChangeRecord,
- primariesChangeRecord, additionalActionsChangeRecord, change) {
- var actions = this._getActionValues(
- actionsChangeRecord, primariesChangeRecord,
- additionalActionsChangeRecord, ActionType.CHANGE, change);
- var quickApprove = this._getQuickApproveAction();
- if (quickApprove) {
- actions.unshift(quickApprove);
- }
- return actions;
- },
-
_getLabelStatus: function(label) {
if (label.approved) {
return LabelStatus.OK;
@@ -466,14 +452,10 @@
var type = el.getAttribute('data-action-type');
if (type === ActionType.REVISION) {
this._handleRevisionAction(key);
- } else if (key === ChangeActions.DELETE) {
- this._showActionDialog(this.$.confirmDeleteDialog);
} else if (key === ChangeActions.REVERT) {
this.showRevertDialog();
- } else if (key === ChangeActions.ABANDON) {
- this._showActionDialog(this.$.confirmAbandonDialog);
} else if (key === QUICK_APPROVE_ACTION.key) {
- var action = this._changeActionValues.find(function(o) {
+ var action = this._allActionValues.find(function(o) {
return o.key === key;
});
this._fireAction(
@@ -488,10 +470,6 @@
case RevisionActions.REBASE:
this._showActionDialog(this.$.confirmRebase);
break;
- case RevisionActions.CHERRYPICK:
- this.$.confirmCherrypick.branch = '';
- this._showActionDialog(this.$.confirmCherrypick);
- break;
case RevisionActions.SUBMIT:
if (!this._canSubmitChange()) {
return;
@@ -664,5 +642,92 @@
return response;
}.bind(this)).then(this._handleResponseError.bind(this));
},
+
+ _handleAbandonTap: function() {
+ this._showActionDialog(this.$.confirmAbandonDialog);
+ },
+
+ _handleCherrypickTap: function() {
+ this.$.confirmCherrypick.branch = '';
+ this._showActionDialog(this.$.confirmCherrypick);
+ },
+
+ _handleDeleteTap: function() {
+ this._showActionDialog(this.$.confirmDeleteDialog);
+ },
+
+ /**
+ * Merge sources of change actions into a single ordered array of action
+ * values.
+ * @param {splices} changeActionsRecord
+ * @param {splices} revisionActionsRecord
+ * @param {splices} primariesRecord
+ * @param {splices} additionalActionsRecord
+ * @param {Object} change The change object.
+ * @return {Array}
+ */
+ _computeAllActions: function(changeActionsRecord, revisionActionsRecord,
+ primariesRecord, additionalActionsRecord, change) {
+ var revisionActionValues = this._getActionValues(revisionActionsRecord,
+ primariesRecord, additionalActionsRecord, ActionType.REVISION);
+ var changeActionValues = this._getActionValues(changeActionsRecord,
+ primariesRecord, additionalActionsRecord, ActionType.CHANGE, change);
+ var quickApprove = this._getQuickApproveAction();
+ if (quickApprove) {
+ changeActionValues.unshift(quickApprove);
+ }
+ return revisionActionValues
+ .concat(changeActionValues)
+ .sort(this._actionComparator);
+ },
+
+ /**
+ * Sort comparator to define the order of change actions.
+ */
+ _actionComparator: function(actionA, actionB) {
+ // The code review action always appears first.
+ if (actionA.__key === 'review') {
+ return -1;
+ } else if (actionB.__key === 'review') {
+ return 1;
+ }
+
+ // Primary actions always appear last.
+ if (actionA.__primary) {
+ return 1;
+ } else if (actionB.__primary) {
+ return -1;
+ }
+
+ // Change actions appear before revision actions.
+ if (actionA.__type === 'change' && actionB.__type === 'revision') {
+ return -1;
+ } else if (actionA.__type === 'revision' && actionB.__type === 'change') {
+ return 1;
+ }
+
+ // Otherwise, sort by the button label.
+ return actionA.label > actionB.label ? 1 : -1;
+ },
+
+ _computeTopLevelActions: function(actionRecord) {
+ return actionRecord.base.filter(function(a) {
+ return MENU_ACTION_KEYS.indexOf(a.__key) === -1;
+ });
+ },
+
+ _computeMenuActions: function(actionRecord, hiddenActionsRecord) {
+ var hiddenActions = hiddenActionsRecord.base || [];
+ return actionRecord.base
+ .filter(function(a) {
+ return MENU_ACTION_KEYS.indexOf(a.__key) !== -1 &&
+ hiddenActions.indexOf(a.__key) === -1;
+ })
+ .map(function(action) {
+ var key = action.__key;
+ if (key === '/') { key = 'delete'; }
+ return {name: action.label, id: key, };
+ });
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 7dbd903..dbdf00f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -99,11 +99,9 @@
});
test('_shouldHideActions', function() {
- assert.isTrue(element._shouldHideActions(undefined, undefined, true));
- assert.isTrue(element._shouldHideActions({base: {}},
- {base: []}, false));
- assert.isFalse(element._shouldHideActions({base: { test: 'test' }},
- {base: ['test']}, false));
+ assert.isTrue(element._shouldHideActions(undefined, true));
+ assert.isTrue(element._shouldHideActions({base: {}}, false));
+ assert.isFalse(element._shouldHideActions({base: ['test']}, false));
});
test('hide revision action', function(done) {
@@ -114,10 +112,10 @@
assert.throws(element.setActionHidden.bind(element, 'invalid type'));
element.setActionHidden(element.ActionType.REVISION,
element.RevisionActions.SUBMIT, true);
- assert.lengthOf(element._hiddenRevisionActions, 1);
+ assert.lengthOf(element._hiddenActions, 1);
element.setActionHidden(element.ActionType.REVISION,
element.RevisionActions.SUBMIT, true);
- assert.lengthOf(element._hiddenRevisionActions, 1);
+ assert.lengthOf(element._hiddenActions, 1);
flush(function() {
var buttonEl = element.$$('[data-action-key="submit"]');
assert.isOk(buttonEl);
@@ -135,29 +133,26 @@
});
});
- test('hide change action', function(done) {
+ test('hide menu action', function(done) {
flush(function() {
- var buttonEl = element.$$('[data-action-key="/"]');
+ var buttonEl = element.$.moreActions.$$('span[data-id="delete"]');
assert.isOk(buttonEl);
- assert.isFalse(buttonEl.hasAttribute('hidden'));
assert.throws(element.setActionHidden.bind(element, 'invalid type'));
element.setActionHidden(element.ActionType.CHANGE,
element.ChangeActions.DELETE, true);
- assert.lengthOf(element._hiddenChangeActions, 1);
+ assert.lengthOf(element._hiddenActions, 1);
element.setActionHidden(element.ActionType.CHANGE,
element.ChangeActions.DELETE, true);
- assert.lengthOf(element._hiddenChangeActions, 1);
+ assert.lengthOf(element._hiddenActions, 1);
flush(function() {
- var buttonEl = element.$$('[data-action-key="/"]');
- assert.isOk(buttonEl);
- assert.isTrue(buttonEl.hasAttribute('hidden'));
+ var buttonEl = element.$.moreActions.$$('span[data-id="delete"]');
+ assert.isNotOk(buttonEl);
element.setActionHidden(element.ActionType.CHANGE,
element.RevisionActions.DELETE, false);
flush(function() {
- var buttonEl = element.$$('[data-action-key="/"]');
+ var buttonEl = element.$.moreActions.$$('span[data-id="delete"]');
assert.isOk(buttonEl);
- assert.isFalse(buttonEl.hasAttribute('hidden'));
done();
});
});
@@ -169,7 +164,8 @@
flush(function() {
var buttonEls = Polymer.dom(element.root)
.querySelectorAll('gr-button');
- assert.equal(buttonEls.length, 6);
+ var menuItems = element.$.moreActions.items;
+ assert.equal(buttonEls.length + menuItems.length, 6);
assert.isFalse(element.hidden);
done();
});
@@ -177,18 +173,18 @@
test('delete buttons have explicit labels', function(done) {
flush(function() {
- var buttonEls =
- Polymer.dom(element.root).querySelectorAll('[data-action-key="/"]');
- assert.equal(buttonEls.length, 2);
- assert.notEqual(buttonEls[0].getAttribute('data-label'),
- buttonEls[1].getAttribute['data-label']);
+ var deleteItems = element.$.moreActions.items.filter(function(item) {
+ return item.id === 'delete';
+ });
+ assert.equal(deleteItems.length, 2);
+ assert.notEqual(deleteItems[0].name, deleteItems[1].name)
assert.isTrue(
- buttonEls[0].getAttribute('data-label') === 'Delete Revision' ||
- buttonEls[0].getAttribute('data-label') === 'Delete Change'
+ deleteItems[0].name === 'Delete Revision' ||
+ deleteItems[0].name === 'Delete Change'
);
assert.isTrue(
- buttonEls[1].getAttribute('data-label') === 'Delete Revision' ||
- buttonEls[1].getAttribute('data-label') === 'Delete Change'
+ deleteItems[1].name === 'Delete Revision' ||
+ deleteItems[1].name === 'Delete Change'
);
done();
});
@@ -205,6 +201,18 @@
assert.deepEqual(element._getRevision(change, '2'), revObj);
});
+ test('_actionComparator sort order', function() {
+ var actions = [
+ {label: '123', type: 'change', __key: 'review'},
+ {label: 'abc',type: 'revision'},
+ {label: 'abc',type: 'change'},
+ {label: 'def', type: 'change'},
+ {label: 'def', type: 'change',__primary: true},
+ ];
+ var result = actions.slice().sort(element._actionComparator);
+ assert.deepEqual(result, actions);
+ });
+
test('submit change', function(done) {
element.change = {
revisions: {
@@ -285,10 +293,7 @@
flushAsynchronousOperations();
assert.isFalse(element.$.confirmRebase.hidden);
- var cherryPickButton =
- element.$$('gr-button[data-action-key="cherrypick"]');
- assert.ok(cherryPickButton);
- MockInteractions.tap(cherryPickButton);
+ element._handleCherrypickTap();
flushAsynchronousOperations();
assert.isTrue(element.$.confirmRebase.hidden);
assert.isFalse(element.$.confirmCherrypick.hidden);
@@ -311,9 +316,7 @@
});
test('works', function() {
- var cherryPickButton =
- element.$$('gr-button[data-action-key="cherrypick"]');
- MockInteractions.tap(cherryPickButton);
+ element._handleCherrypickTap();
var action = {
__key: 'cherrypick',
__type: 'revision',
@@ -350,12 +353,10 @@
});
test('branch name cleared when re-open cherrypick', function() {
- var cherryPickButton =
- element.$$('gr-button[data-action-key="cherrypick"]');
var emptyBranchName = '';
element.$.confirmCherrypick.branch = 'master';
- MockInteractions.tap(cherryPickButton);
+ element._handleCherrypickTap();
assert.equal(element.$.confirmCherrypick.branch, emptyBranchName);
});
});
@@ -457,12 +458,6 @@
var fireActionStub;
var deleteAction;
- var tapDeleteAction = function() {
- var deleteButton = element.$$('gr-button[data-action-key=\'/\']');
- MockInteractions.tap(deleteButton);
- flushAsynchronousOperations();
- };
-
setup(function() {
fireActionStub = sinon.stub(element, '_fireAction');
element.change = {
@@ -484,12 +479,12 @@
});
test('does not delete on action', function() {
- tapDeleteAction();
+ element._handleDeleteTap();
assert.isFalse(fireActionStub.called);
});
test('shows confirm dialog', function() {
- tapDeleteAction();
+ element._handleDeleteTap();
assert.isFalse(element.$$('#confirmDeleteDialog').hidden);
MockInteractions.tap(
element.$$('#confirmDeleteDialog').$$('gr-button[primary]'));
@@ -498,7 +493,7 @@
});
test('hides delete confirm on cancel', function() {
- tapDeleteAction();
+ element._handleDeleteTap();
MockInteractions.tap(
element.$$('#confirmDeleteDialog').$$('gr-button:not([primary])'));
flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 18dfdc0..873acddc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -255,12 +255,14 @@
<a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><!--
--><template is="dom-if" if="[[_changeStatus]]">
([[_changeStatus]]<!--
- --><template is="dom-if" if="[[_computeShowCommitInfo(_changeStatus)]]">
+ --><template
+ is="dom-if"
+ if="[[_computeShowCommitInfo(_changeStatus, _change.current_revision)]]">
as
<gr-commit-info
- change="[[_change]]"
- commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]"
- server-config="[[serverConfig]]"></gr-commit-info><!--
+ change="[[_change]]"
+ commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]"
+ server-config="[[serverConfig]]"></gr-commit-info><!--
--></template><!--
-->)<!--
--></template><!--
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index d66917c..9c7ce99 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -563,8 +563,8 @@
return statusString || '';
},
- _computeShowCommitInfo: function(changeStatus) {
- return changeStatus === 'Merged';
+ _computeShowCommitInfo: function(changeStatus, current_revision) {
+ return changeStatus === 'Merged' && current_revision;
},
_computeMergedCommitInfo: function(current_revision, revisions) {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index cead416..07c922a 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -34,7 +34,10 @@
vertical-align: middle;
}
</style>
- <gr-dropdown items=[[links]] top-content=[[topContent]]
+ <gr-dropdown
+ link
+ items=[[links]]
+ top-content=[[topContent]]
horizontal-align="right">
<span hidden$="[[_hasAvatars]]" hidden>[[account.name]]</span>
<gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index c3d9b16..74dc9e7 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -52,23 +52,8 @@
.linksTitle {
color: black;
display: inline-block;
- padding-right: 1em;
position: relative;
}
- .downArrow {
- border-left: .36em solid transparent;
- border-right: .36em solid transparent;
- border-top: .36em solid #ccc;
- height: 0;
- position: absolute;
- right: 0;
- top: calc(50% - .05em);
- transition: border-top-color 200ms;
- width: 0;
- }
- .links li:hover .downArrow {
- border-top-color: #666;
- }
.rightItems {
align-items: center;
display: flex;
@@ -119,10 +104,12 @@
<template is="dom-repeat" items="[[_links]]" as="linkGroup">
<li>
<gr-dropdown
+ link
+ down-arrow
items = [[linkGroup.links]]
horizontal-align="left">
<span class="linksTitle" id="[[linkGroup.title]]">
- [[linkGroup.title]] <i class="downArrow"></i>
+ [[linkGroup.title]]
</span>
</gr-dropdown>
</li>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 7e91b0e..800b1df 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -18,6 +18,10 @@
is: 'gr-button',
properties: {
+ link: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
disabled: {
type: Boolean,
observer: '_disabledChanged',
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index 95b03fc..f640dd6 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -38,6 +38,9 @@
font: inherit;
padding: .3em 0;
}
+ :host[down-arrow] .dropdown-trigger {
+ padding-right: 1.4em;
+ }
gr-avatar {
height: 2em;
width: 2em;
@@ -46,11 +49,6 @@
gr-button[link] {
padding: 1em 0;
}
- gr-button:focus:not([primary]):not([secondary]),
- gr-button:hover:not([primary]):not([secondary]) {
- background-color: transparent;
- border-color: transparent;
- }
ul {
list-style: none;
}
@@ -58,16 +56,17 @@
font-weight: bold;
}
li .accountInfo,
- li a {
+ li .itemAction {
+ cursor: pointer;
display: block;
padding: .85em 1em;
}
- li a:link,
- li a:visited {
+ li .itemAction:link,
+ li .itemAction:visited {
color: #00e;
text-decoration: none;
}
- li a:hover {
+ li .itemAction:hover {
background-color: #6B82D6;
color: #fff;
}
@@ -78,14 +77,30 @@
.bold-text {
font-weight: bold;
}
+ :host:not([down-arrow]) .downArrow { display: none; }
+ :host[down-arrow] .downArrow {
+ border-left: .36em solid transparent;
+ border-right: .36em solid transparent;
+ border-top: .36em solid #ccc;
+ height: 0;
+ position: absolute;
+ right: .3em;
+ top: calc(50% - .05em);
+ transition: border-top-color 200ms;
+ width: 0;
+ }
+ .dropdown-trigger:hover .downArrow {
+ border-top-color: #666;
+ }
</style>
- <gr-button link class="dropdown-trigger" id="trigger"
+ <gr-button link="[[link]]" class="dropdown-trigger" id="trigger"
on-tap="_showDropdownTapHandler">
<content></content>
+ <i class="downArrow"></i>
</gr-button>
<iron-dropdown id="dropdown"
vertical-align="top"
- vertical-offset="40"
+ vertical-offset="[[verticalOffset]]"
allow-outside-scroll="true"
horizontal-align="[[horizontalAlign]]"
on-tap="_handleDropdownTap">
@@ -109,7 +124,16 @@
items="[[items]]"
as="link"
initial-count="75">
- <li><a href$="[[_computeRelativeURL(link.url)]]">[[link.name]]</a>
+ <li>
+ <span
+ class="itemAction"
+ data-id$="[[link.id]]"
+ on-tap="_handleItemTap"
+ hidden$="[[link.url]]">[[link.name]]</span>
+ <a
+ class="itemAction"
+ href$="[[_computeRelativeURL(link.url)]]"
+ hidden$="[[!link.url]]">[[link.name]]</a>
</li>
</template>
</ul>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index d10d219..d1fae7f 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -17,6 +17,12 @@
Polymer({
is: 'gr-dropdown',
+ /**
+ * Fired when a non-link dropdown item with the given ID is tapped.
+ *
+ * @event tap-item-<id>
+ */
+
properties: {
items: Array,
topContent: Object,
@@ -24,6 +30,20 @@
type: String,
value: 'left',
},
+
+ /**
+ * Style the dropdown trigger as a link (rather than a button).
+ */
+ link: {
+ type: Boolean,
+ value: false,
+ },
+
+ verticalOffset: {
+ type: Number,
+ value: 40,
+ },
+
_hasAvatars: String,
},
@@ -53,5 +73,10 @@
var host = window.location.host;
return this._computeURLHelper(host, path);
},
+
+ _handleItemTap: function(e) {
+ var id = e.target.getAttribute('data-id');
+ if (id) { this.dispatchEvent(new CustomEvent('tap-item-' + id)); }
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index b2f2d21..2794caf 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -70,5 +70,15 @@
assert.isTrue(topItems[0].classList.contains('bold-text'));
assert.isFalse(topItems[1].classList.contains('bold-text'));
});
+
+ test('non link items', function() {
+ element.items = [
+ {name: 'item one', id: 'foo'}, {name: 'item two', id: 'bar'}];
+ var stub = sinon.stub();
+ element.addEventListener('tap-item-foo', stub);
+ flushAsynchronousOperations();
+ MockInteractions.tap(element.$$('.itemAction'));
+ assert.isTrue(stub.called);
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 93e676c..c02583a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -49,6 +49,7 @@
setup(function() {
element = fixture('basic');
+ element.change = {};
var plugin;
Gerrit.install(function(p) { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');