From 8ca03a6455cc150dd48c2190359b6bae0172fbcc Mon Sep 17 00:00:00 2001 From: Shawn Pearce Date: Fri, 2 Jan 2015 22:03:20 -0800 Subject: [PATCH] Inline Edit: Suggest paths to add Offer a suggestion box completing any known file path in the repository whose path name contains the letters offered by the user. E.g. "AddFile" would suggest the first three files of this change. Instead of loading content into a small primitive text area, load the file into an EditScreen where CodeMirror is configured. This makes it easier to begin editing the file right away and do something useful with it. Change-Id: I2d34bdb65b909def24ffc56e8136efd305fec3b1 --- Documentation/rest-api-changes.txt | 5 + .../gerrit/client/change/AddFileAction.java | 71 ++++++++ .../gerrit/client/change/AddFileBox.java | 159 ++++++++++++++++++ .../{EditFileBox.ui.xml => AddFileBox.ui.xml} | 28 +-- .../gerrit/client/change/ChangeScreen2.java | 10 +- .../gerrit/client/change/EditFileAction.java | 80 --------- .../gerrit/client/change/EditFileBox.java | 119 ------------- .../gerrit/client/change/FileTextBox.java | 73 -------- .../server/api/changes/RevisionApiImpl.java | 2 +- .../google/gerrit/server/change/Files.java | 62 ++++++- 10 files changed, 304 insertions(+), 305 deletions(-) create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java rename gerrit-gwtui/src/main/java/com/google/gerrit/client/change/{EditFileBox.ui.xml => AddFileBox.ui.xml} (65%) delete mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileAction.java delete mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.java delete mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTextBox.java diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index 6157d37921..ab74b64305 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt @@ -2744,6 +2744,11 @@ The request parameter `reviewed` changes the response to return a list of the paths the caller has marked as reviewed. Clients that also need the FileInfo should make two requests. +The request parameter `q` changes the response to return a list +of all files (modified or unmodified) that contain that substring +in the path name. This is useful to implement suggestion services +finding a file by partial name. + .Request ---- GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/?reviewed HTTP/1.0 diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java new file mode 100644 index 0000000000..6e2ee006be --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java @@ -0,0 +1,71 @@ +//Copyright (C) 2013 The Android Open Source Project +// +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +package com.google.gerrit.client.change; + +import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwtexpui.globalkey.client.GlobalKey; +import com.google.gwtexpui.user.client.PluginSafePopupPanel; + +class AddFileAction { + private final Change.Id changeId; + private final RevisionInfo revision; + private final ChangeScreen2.Style style; + private final Widget addButton; + + private AddFileBox addBox; + private PopupPanel popup; + + AddFileAction(Change.Id changeId, RevisionInfo revision, + ChangeScreen2.Style style, Widget addButton) { + this.changeId = changeId; + this.revision = revision; + this.style = style; + this.addButton = addButton; + } + + public void onEdit() { + if (popup != null) { + popup.hide(); + return; + } + + if (addBox == null) { + addBox = new AddFileBox(changeId, revision); + } + addBox.clearPath(); + + final PluginSafePopupPanel p = new PluginSafePopupPanel(true); + p.setStyleName(style.replyBox()); + p.addAutoHidePartner(addButton.getElement()); + p.addCloseHandler(new CloseHandler() { + @Override + public void onClose(CloseEvent event) { + if (popup == p) { + popup = null; + } + } + }); + p.add(addBox); + p.showRelativeTo(addButton); + GlobalKey.dialog(p); + addBox.setFocus(true); + popup = p; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java new file mode 100644 index 0000000000..6e47f264e8 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java @@ -0,0 +1,159 @@ +//Copyright (C) 2013 The Android Open Source Project +// +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +package com.google.gerrit.client.change; + +import com.google.gerrit.client.Dispatcher; +import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.changes.ChangeApi; +import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo; +import com.google.gerrit.client.rpc.Natives; +import com.google.gerrit.client.ui.RemoteSuggestBox; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JsArrayString; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.event.logical.shared.SelectionEvent; +import com.google.gwt.event.logical.shared.SelectionHandler; +import com.google.gwt.uibinder.client.UiBinder; +import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HTMLPanel; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +class AddFileBox extends Composite { + interface Binder extends UiBinder {} + private static final Binder uiBinder = GWT.create(Binder.class); + + private final Change.Id changeId; + private final RevisionInfo revision; + + @UiField Button open; + @UiField Button cancel; + + @UiField(provided = true) + RemoteSuggestBox path; + + AddFileBox(Change.Id changeId, RevisionInfo revision) { + this.changeId = changeId; + this.revision = revision; + + path = new RemoteSuggestBox(new PathSuggestOracle()); + path.addSelectionHandler(new SelectionHandler() { + @Override + public void onSelection(SelectionEvent event) { + open(event.getSelectedItem()); + } + }); + path.addCloseHandler(new CloseHandler() { + @Override + public void onClose(CloseEvent event) { + hide(); + } + }); + + initWidget(uiBinder.createAndBindUi(this)); + } + + void setFocus(boolean focus) { + path.setFocus(focus); + } + + void clearPath() { + path.setText(""); + } + + @UiHandler("open") + void onOpen(@SuppressWarnings("unused") ClickEvent e) { + open(path.getText()); + } + + private void open(String path) { + hide(); + Gerrit.display(Dispatcher.toEditScreen( + new PatchSet.Id(changeId, revision._number()), + path)); + } + + @UiHandler("cancel") + void onCancel(@SuppressWarnings("unused") ClickEvent e) { + hide(); + } + + private void hide() { + for (Widget w = getParent(); w != null; w = w.getParent()) { + if (w instanceof PopupPanel) { + ((PopupPanel) w).hide(); + break; + } + } + } + + private class PathSuggestOracle extends HighlightSuggestOracle { + @Override + protected void onRequestSuggestions(final Request req, final Callback cb) { + ChangeApi.revision(changeId.get(), revision.name()) + .view("files") + .addParameter("q", req.getQuery()) + .background() + .get(new AsyncCallback() { + @Override + public void onSuccess(JsArrayString result) { + List r = new ArrayList<>(); + for (String path : Natives.asList(result)) { + r.add(new PathSuggestion(path)); + } + cb.onSuggestionsReady(req, new Response(r)); + } + + @Override + public void onFailure(Throwable caught) { + List none = Collections.emptyList(); + cb.onSuggestionsReady(req, new Response(none)); + } + }); + } + } + + private static class PathSuggestion implements Suggestion { + private final String path; + + PathSuggestion(String path) { + this.path = path; + } + + @Override + public String getDisplayString() { + return path; + } + + @Override + public String getReplacementString() { + return path; + } + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml similarity index 65% rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.ui.xml rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml index 3a0e0bece4..d8236e6827 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.ui.xml +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml @@ -16,40 +16,22 @@ limitations under the License. --> - .fileContent { - background-color: white; - font-family: monospace; - } .cancel { float: right; }
-
- Path: -
-
- -
-
- Content: -
- + Path:
- -
Save
+
Open
() { - @Override - public void onClose(CloseEvent event) { - if (popup == p) { - popup = null; - } - } - }); - p.add(editBox); - p.showRelativeTo(relativeTo); - GlobalKey.dialog(p); - popup = p; - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.java deleted file mode 100644 index 9c5ea31d59..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.java +++ /dev/null @@ -1,119 +0,0 @@ -//Copyright (C) 2013 The Android Open Source Project -// -//Licensed under the Apache License, Version 2.0 (the "License"); -//you may not use this file except in compliance with the License. -//You may obtain a copy of the License at -// -//http://www.apache.org/licenses/LICENSE-2.0 -// -//Unless required by applicable law or agreed to in writing, software -//distributed under the License is distributed on an "AS IS" BASIS, -//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -//See the License for the specific language governing permissions and -//limitations under the License. - -package com.google.gerrit.client.change; - -import com.google.gerrit.client.Gerrit; -import com.google.gerrit.client.VoidResult; -import com.google.gerrit.client.changes.ChangeEditApi; -import com.google.gerrit.client.rpc.GerritCallback; -import com.google.gerrit.client.ui.TextBoxChangeListener; -import com.google.gerrit.common.PageLinks; -import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gwt.core.client.GWT; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.uibinder.client.UiBinder; -import com.google.gwt.uibinder.client.UiField; -import com.google.gwt.uibinder.client.UiHandler; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.Composite; -import com.google.gwt.user.client.ui.HTMLPanel; -import com.google.gwt.user.client.ui.PopupPanel; -import com.google.gwt.user.client.ui.TextBoxBase; -import com.google.gwt.user.client.ui.Widget; -import com.google.gwtexpui.globalkey.client.NpTextArea; - -class EditFileBox extends Composite { - interface Binder extends UiBinder {} - private static final Binder uiBinder = GWT.create(Binder.class); - - private final PatchSet.Id id; - private final String fileName; - private final String fileContent; - - @UiField FileTextBox file; - @UiField NpTextArea content; - @UiField Button save; - @UiField Button cancel; - - EditFileBox( - PatchSet.Id id, - String fileC, - String fileName) { - this.id = id; - this.fileName = fileName; - this.fileContent = fileC; - initWidget(uiBinder.createAndBindUi(this)); - new EditFileBoxListener(content); - new EditFileBoxListener(file); - } - - @Override - protected void onLoad() { - file.set(id, content); - file.setText(fileName); - file.setEnabled(fileName.isEmpty()); - content.setText(fileContent); - save.setEnabled(false); - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - if (fileName.isEmpty()) { - file.setFocus(true); - } else { - content.setFocus(true); - } - }}); - } - - @UiHandler("save") - void onSave(@SuppressWarnings("unused") ClickEvent e) { - ChangeEditApi.put(id.getParentKey().get(), file.getText(), content.getText(), - new GerritCallback() { - @Override - public void onSuccess(VoidResult result) { - Gerrit.display(PageLinks.toChangeInEditMode(id.getParentKey())); - hide(); - } - }); - } - - @UiHandler("cancel") - void onCancel(@SuppressWarnings("unused") ClickEvent e) { - hide(); - } - - protected void hide() { - for (Widget w = getParent(); w != null; w = w.getParent()) { - if (w instanceof PopupPanel) { - ((PopupPanel) w).hide(); - break; - } - } - } - - private class EditFileBoxListener extends TextBoxChangeListener { - public EditFileBoxListener(TextBoxBase base) { - super(base); - } - - @Override - public void onTextChanged(String newText) { - save.setEnabled(!file.getText().trim().isEmpty() - && !newText.trim().equals(fileContent)); - } - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTextBox.java deleted file mode 100644 index e511e2476a..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTextBox.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (C) 2014 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.client.change; - -import com.google.gerrit.client.changes.ChangeEditApi; -import com.google.gerrit.client.rpc.GerritCallback; -import com.google.gerrit.client.rpc.HttpCallback; -import com.google.gerrit.client.rpc.HttpResponse; -import com.google.gerrit.client.rpc.NativeString; -import com.google.gerrit.client.rpc.RestApi; -import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwtexpui.globalkey.client.NpTextArea; -import com.google.gwtexpui.globalkey.client.NpTextBox; - -class FileTextBox extends NpTextBox { - private HandlerRegistration blurHandler; - private NpTextArea textArea; - private PatchSet.Id id; - - @Override - protected void onLoad() { - blurHandler = addBlurHandler(new BlurHandler() { - @Override - public void onBlur(BlurEvent event) { - loadFileContent(); - } - }); - } - - @Override - protected void onUnload() { - super.onUnload(); - blurHandler.removeHandler(); - } - - void set(PatchSet.Id id, NpTextArea content) { - this.id = id; - this.textArea = content; - } - - private void loadFileContent() { - ChangeEditApi.get(id, getText(), new HttpCallback() { - @Override - public void onSuccess(HttpResponse result) { - textArea.setText(result.getResult().asString()); - } - - @Override - public void onFailure(Throwable caught) { - if (RestApi.isNotFound(caught)) { - // that means that the file doesn't exist in the repository - } else { - GerritCallback.showFailure(caught); - } - } - }); - } -} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java index 566b29878c..ab788c945e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java @@ -187,7 +187,7 @@ class RevisionApiImpl extends RevisionApi.NotImplemented implements RevisionApi return ImmutableSet.copyOf((Iterable) listFiles .get().setReviewed(true) .apply(revision).value()); - } catch (OrmException e) { + } catch (OrmException | IOException e) { throw new RestApiException("Cannot list reviewed files", e); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java index 3035ce15bc..4a3082c8c3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java @@ -43,8 +43,11 @@ import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; @@ -53,6 +56,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -96,6 +100,9 @@ public class Files implements ChildCollection { @Option(name = "--reviewed") boolean reviewed; + @Option(name = "-q") + String query; + private final Provider db; private final Provider self; private final FileInfoJson fileInfoJson; @@ -125,11 +132,13 @@ public class Files implements ChildCollection { @Override public Response apply(RevisionResource resource) throws AuthException, - BadRequestException, ResourceNotFoundException, OrmException { - if (base != null && reviewed) { - throw new BadRequestException("cannot combine base and reviewed"); - } else if (reviewed) { + BadRequestException, ResourceNotFoundException, OrmException, + RepositoryNotFoundException, IOException { + checkOptions(); + if (reviewed) { return Response.ok(reviewed(resource)); + } else if (query != null) { + return Response.ok(query(resource)); } PatchSet basePatchSet = null; @@ -152,6 +161,51 @@ public class Files implements ChildCollection { } } + private void checkOptions() throws BadRequestException { + int supplied = 0; + if (base != null) { + supplied++; + } + if (reviewed) { + supplied++; + } + if (query != null) { + supplied++; + } + if (supplied > 1) { + throw new BadRequestException("cannot combine base, reviewed, query"); + } + } + + private List query(RevisionResource resource) + throws RepositoryNotFoundException, IOException { + Repository git = + gitManager.openRepository(resource.getChange().getProject()); + try { + TreeWalk tw = new TreeWalk(git); + try { + RevCommit c = new RevWalk(tw.getObjectReader()) + .parseCommit(ObjectId.fromString( + resource.getPatchSet().getRevision().get())); + + tw.addTree(c.getTree()); + tw.setRecursive(true); + List paths = new ArrayList<>(); + while (tw.next() && paths.size() < 20) { + String s = tw.getPathString(); + if (s.contains(query)) { + paths.add(s); + } + } + return paths; + } finally { + tw.release(); + } + } finally { + git.close(); + } + } + private List reviewed(RevisionResource resource) throws AuthException, OrmException { CurrentUser user = self.get();