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();