Merge changes from topic 'inline-4'

* changes:
  Inline Edit: Suggest paths to add
  ChangeScreen2: Fix double error glass when disconnected
  Inline Edit: Display cursor position in bottom left
  Inline Edit: Cleanup client utility API
  Remove /type REST API projections
  Inline Edit: Acquire content and type in one request
  Inline Edit: Honor project specific MIME types
This commit is contained in:
Shawn Pearce
2015-01-06 21:30:17 +00:00
committed by Gerrit Code Review
37 changed files with 1023 additions and 877 deletions

View File

@@ -1408,8 +1408,16 @@ Retrieves content of a file from a change edit.
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo HTTP/1.0
----
The content of the file is returned as text encoded inside base64. When
specified file was deleted in the change edit "`204 No Content`" is returned.
The content of the file is returned as text encoded inside base64.
The Content-Type header will always be `text/plain` reflecting the
outer base64 encoding. A Gerrit-specific `X-FYI-Content-Type` header
can be examined to find the server detected content type of the file.
When the specified file was deleted in the change edit
"`204 No Content`" is returned.
If only the content type is required, callers should use HEAD to
avoid downloading the encoded file contents.
.Response
----
@@ -1417,33 +1425,11 @@ specified file was deleted in the change edit "`204 No Content`" is returned.
Content-Disposition: attachment
Content-Type: text/plain; charset=ISO-8859-1
X-FYI-Content-Encoding: base64
X-FYI-Content-Type: text/xml
RnJvbSA3ZGFkY2MxNTNmZGVhMTdhYTg0ZmYzMmE2ZTI0NWRiYjY...
----
[[get-edit-file-mime-type]]
=== Retrieve file content MIME type from Change Edit
--
'GET /changes/link:#change-id[\{change-id\}]/edit/path%2fto%2ffile/type
--
Retrieves content MIME type of a file from a change edit.
.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo%2fbar%2fbaz%2fqux.txt/type HTTP/1.0
----
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
"text/plain"
----
[[get-edit-message]]
=== Retrieve commit message from Change Edit or current patch set of the change
--
@@ -2758,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
@@ -2789,43 +2780,25 @@ Gets the content of a file from a certain revision.
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/content HTTP/1.0
----
The content is returned as base64 encoded string.
The content is returned as base64 encoded string. The HTTP response
Content-Type is always `text/plain`, reflecting the base64 wrapping.
A Gerrit-specific `X-FYI-Content-Type` header is returned describing
the server detected content type of the file.
If only the content type is required, callers should use HEAD to
avoid downloading the encoded file contents.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: text/plain; charset=UTF-8
Content-Type: text/plain; charset=ISO-8859-1
X-FYI-Content-Encoding: base64
X-FYI-Content-Type: text/xml
Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
----
[[get-content-type]]
=== Get Content MIME Type
--
'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/type'
--
Gets the content MIME type of a file from a certain revision.
.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/readme.txt/type HTTP/1.0
----
The content MIME type is returned as string. The content type for the commit
message (`/COMMIT_MSG`) is always returned as "text/plain".
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
"text/plain"
----
[[get-diff]]
=== Get Diff
--

View File

@@ -59,6 +59,7 @@ import com.google.inject.Inject;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.StringUtils;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.joda.time.DateTime;
@@ -194,10 +195,10 @@ public class ChangeEditIT extends AbstractDaemonTest {
Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen();
modifier.rebaseEdit(edit, current);
edit = editUtil.byChange(change).get();
assertByteArray(fileUtil.getContent(edit.getChange().getProject(), edit
.getRevision().get(), FILE_NAME), CONTENT_NEW);
assertByteArray(fileUtil.getContent(edit.getChange().getProject(), edit
.getRevision().get(), FILE_NAME2), CONTENT_NEW2);
assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
ObjectId.fromString(edit.getRevision().get()), FILE_NAME), CONTENT_NEW);
assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
ObjectId.fromString(edit.getRevision().get()), FILE_NAME2), CONTENT_NEW2);
assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
current.getPatchSetId());
Date afterRebase = edit.getEditCommit().getCommitterIdent().getWhen();
@@ -218,10 +219,10 @@ public class ChangeEditIT extends AbstractDaemonTest {
RestResponse r = adminSession.post(urlRebase());
assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
edit = editUtil.byChange(change).get();
assertByteArray(fileUtil.getContent(edit.getChange().getProject(), edit
.getRevision().get(), FILE_NAME), CONTENT_NEW);
assertByteArray(fileUtil.getContent(edit.getChange().getProject(), edit
.getRevision().get(), FILE_NAME2), CONTENT_NEW2);
assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
ObjectId.fromString(edit.getRevision().get()), FILE_NAME), CONTENT_NEW);
assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
ObjectId.fromString(edit.getRevision().get()), FILE_NAME2), CONTENT_NEW2);
assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
current.getPatchSetId());
Date afterRebase = edit.getEditCommit().getCommitterIdent().getWhen();
@@ -235,9 +236,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
.isEqualTo(RefUpdate.Result.FORCED);
edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), CONTENT_NEW);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
editUtil.delete(edit.get());
edit = editUtil.byChange(change);
assertThat(edit.isPresent()).isFalse();
@@ -364,8 +364,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
RefUpdate.Result.FORCED);
edit = editUtil.byChange(change);
try {
fileUtil.getContent(edit.get().getChange().getProject(),
edit.get().getRevision().get(), FILE_NAME);
fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
fail("ResourceNotFoundException expected");
} catch (ResourceNotFoundException rnfe) {
}
@@ -377,8 +377,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
Optional<ChangeEdit> edit = editUtil.byChange(change);
try {
fileUtil.getContent(edit.get().getChange().getProject(),
edit.get().getRevision().get(), FILE_NAME);
fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
fail("ResourceNotFoundException expected");
} catch (ResourceNotFoundException rnfe) {
}
@@ -397,8 +397,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
SC_NO_CONTENT);
Optional<ChangeEdit> edit = editUtil.byChange(change);
try {
fileUtil.getContent(edit.get().getChange().getProject(),
edit.get().getRevision().get(), FILE_NAME);
fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
fail("ResourceNotFoundException expected");
} catch (ResourceNotFoundException rnfe) {
}
@@ -412,9 +412,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(modifier.restoreFile(edit.get(), FILE_NAME)).isEqualTo(
RefUpdate.Result.FORCED);
edit = editUtil.byChange(change2);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), CONTENT_OLD);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
}
@Test
@@ -424,9 +423,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(adminSession.post(urlEdit2(), in).getStatusCode()).isEqualTo(
SC_NO_CONTENT);
Optional<ChangeEdit> edit = editUtil.byChange(change2);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), CONTENT_OLD);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
}
@Test
@@ -436,15 +434,13 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
.isEqualTo(RefUpdate.Result.FORCED);
edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), CONTENT_NEW);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW2)))
.isEqualTo(RefUpdate.Result.FORCED);
edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), CONTENT_NEW2);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW2);
}
@Test
@@ -454,16 +450,14 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
.isEqualTo(SC_NO_CONTENT);
Optional<ChangeEdit> edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), CONTENT_NEW);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
in.content = RestSession.newRawInput(CONTENT_NEW2);
assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
.isEqualTo(SC_NO_CONTENT);
edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), CONTENT_NEW2);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW2);
}
@Test
@@ -474,9 +468,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
.isEqualTo(SC_NO_CONTENT);
Optional<ChangeEdit> edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), CONTENT_NEW);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
}
@Test
@@ -485,9 +478,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(adminSession.put(urlEditFile()).getStatusCode()).isEqualTo(
SC_NO_CONTENT);
Optional<ChangeEdit> edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), "".getBytes());
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), "".getBytes());
}
@Test
@@ -495,9 +487,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(adminSession.post(urlEdit()).getStatusCode()).isEqualTo(
SC_NO_CONTENT);
Optional<ChangeEdit> edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME), CONTENT_OLD);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
}
@Test
@@ -517,18 +508,6 @@ public class ChangeEditIT extends AbstractDaemonTest {
.isEqualTo(StringUtils.newStringUtf8(CONTENT_NEW2));
}
@Test
public void getFileContentTypeRest() throws Exception {
Put.Input in = new Put.Input();
in.content = RestSession.newRawInput(CONTENT_NEW);
assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
.isEqualTo(SC_NO_CONTENT);
RestResponse r = adminSession.get(urlEditFileContentType());
assertThat(r.getStatusCode()).isEqualTo(SC_OK);
String res = newGson().fromJson(r.getReader(), String.class);
assertThat(res).isEqualTo("application/octet-stream");
}
@Test
public void getFileNotFoundRest() throws Exception {
assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
@@ -536,8 +515,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
SC_NO_CONTENT);
Optional<ChangeEdit> edit = editUtil.byChange(change);
try {
fileUtil.getContent(edit.get().getChange().getProject(),
edit.get().getRevision().get(), FILE_NAME);
fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
fail("ResourceNotFoundException expected");
} catch (ResourceNotFoundException rnfe) {
}
@@ -552,9 +531,8 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RestSession.newRawInput(CONTENT_NEW)))
.isEqualTo(RefUpdate.Result.FORCED);
edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME2), CONTENT_NEW);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW);
}
@Test
@@ -564,15 +542,13 @@ public class ChangeEditIT extends AbstractDaemonTest {
assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RestSession.newRawInput(CONTENT_NEW)))
.isEqualTo(RefUpdate.Result.FORCED);
edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME2), CONTENT_NEW);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW);
assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RestSession.newRawInput(CONTENT_NEW2)))
.isEqualTo(RefUpdate.Result.FORCED);
edit = editUtil.byChange(change);
assertByteArray(
fileUtil.getContent(edit.get().getChange().getProject(), edit.get()
.getRevision().get(), FILE_NAME2), CONTENT_NEW2);
assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW2);
}
@Test
@@ -684,13 +660,6 @@ public class ChangeEditIT extends AbstractDaemonTest {
+ FILE_NAME;
}
private String urlEditFileContentType() {
return urlEdit()
+ "/"
+ FILE_NAME
+ "/type";
}
private String urlGetFiles() {
return urlEdit()
+ "?list";

View File

@@ -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<PopupPanel>() {
@Override
public void onClose(CloseEvent<PopupPanel> event) {
if (popup == p) {
popup = null;
}
}
});
p.add(addBox);
p.showRelativeTo(addButton);
GlobalKey.dialog(p);
addBox.setFocus(true);
popup = p;
}
}

View File

@@ -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<HTMLPanel, AddFileBox> {}
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<String>() {
@Override
public void onSelection(SelectionEvent<String> event) {
open(event.getSelectedItem());
}
});
path.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
@Override
public void onClose(CloseEvent<RemoteSuggestBox> 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<JsArrayString>() {
@Override
public void onSuccess(JsArrayString result) {
List<Suggestion> 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<Suggestion> 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;
}
}
}

View File

@@ -16,40 +16,22 @@ limitations under the License.
-->
<ui:UiBinder
xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
xmlns:f='urn:import:com.google.gerrit.client.change'
xmlns:u='urn:import:com.google.gerrit.client.ui'
xmlns:g='urn:import:com.google.gwt.user.client.ui'>
<ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
<ui:style>
.fileContent {
background-color: white;
font-family: monospace;
}
.cancel { float: right; }
</ui:style>
<g:HTMLPanel>
<div class='{res.style.section}'>
<div>
<ui:msg>Path:</ui:msg>
</div>
<div>
<f:FileTextBox ui:field='file' visibleLength='79'/>
</div>
<div>
<ui:msg>Content:</ui:msg>
</div>
<c:NpTextArea
visibleLines='30'
characterWidth='78'
styleName='{style.fileContent}'
ui:field='content'/>
<ui:msg>Path: <u:RemoteSuggestBox ui:field='path' visibleLength='86'/></ui:msg>
</div>
<div class='{res.style.section}'>
<g:Button ui:field='save'
title='Create new revision edit'
<g:Button ui:field='open'
title='Open file in editor'
styleName='{res.style.button}'>
<ui:attribute name='title'/>
<div><ui:msg>Save</ui:msg></div>
<div><ui:msg>Open</ui:msg></div>
</g:Button>
<g:Button ui:field='cancel'
styleName='{res.style.button}'

View File

@@ -193,7 +193,7 @@ public class ChangeScreen2 extends Screen {
private IncludedInAction includedInAction;
private PatchSetsAction patchSetsAction;
private DownloadAction downloadAction;
private EditFileAction editFileAction;
private AddFileAction addFileAction;
public ChangeScreen2(Change.Id changeId, String base, String revision,
boolean openReplyBox, FileTable.Mode mode) {
@@ -215,11 +215,15 @@ public class ChangeScreen2 extends Screen {
CallbackGroup group = new CallbackGroup();
if (Gerrit.isSignedIn()) {
ChangeApi.editWithFiles(changeId.get(), group.add(
new GerritCallback<EditInfo>() {
new AsyncCallback<EditInfo>() {
@Override
public void onSuccess(EditInfo result) {
edit = result;
}
@Override
public void onFailure(Throwable caught) {
}
}));
}
loadChangeInfo(true, group.addFinal(
@@ -444,9 +448,9 @@ public class ChangeScreen2 extends Screen {
editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
addFile.setVisible(!editMode.isVisible());
reviewMode.setVisible(!editMode.isVisible());
editFileAction = new EditFileAction(
new PatchSet.Id(changeId, edit == null ? rev._number() : 0),
"", "", style, editMessage, reply);
addFileAction = new AddFileAction(
changeId, info.revision(revision),
style, addFile);
} else {
editMode.setVisible(false);
addFile.setVisible(false);
@@ -621,7 +625,7 @@ public class ChangeScreen2 extends Screen {
@UiHandler("addFile")
void onAddFile(@SuppressWarnings("unused") ClickEvent e) {
editFileAction.onEdit();
addFileAction.onEdit();
}
private void refreshFileTable() {

View File

@@ -1,80 +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.reviewdb.client.PatchSet;
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 EditFileAction {
private final PatchSet.Id id;
private final String content;
private final String file;
private final ChangeScreen2.Style style;
private final Widget editMessageButton;
private final Widget relativeTo;
private EditFileBox editBox;
private PopupPanel popup;
EditFileAction(
PatchSet.Id id,
String content,
String file,
ChangeScreen2.Style style,
Widget editButton,
Widget relativeTo) {
this.id = id;
this.content = content;
this.file = file;
this.style = style;
this.editMessageButton = editButton;
this.relativeTo = relativeTo;
}
public void onEdit() {
if (popup != null) {
popup.hide();
return;
}
if (editBox == null) {
editBox = new EditFileBox(
id,
content,
file);
}
final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
p.setStyleName(style.replyBox());
p.addAutoHidePartner(editMessageButton.getElement());
p.addCloseHandler(new CloseHandler<PopupPanel>() {
@Override
public void onClose(CloseEvent<PopupPanel> event) {
if (popup == p) {
popup = null;
}
}
});
p.add(editBox);
p.showRelativeTo(relativeTo);
GlobalKey.dialog(p);
popup = p;
}
}

View File

@@ -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.ChangeFileApi;
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<HTMLPanel, EditFileBox> {}
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) {
ChangeFileApi.putContent(id, file.getText(), content.getText(),
new GerritCallback<VoidResult>() {
@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));
}
}
}

View File

@@ -16,7 +16,7 @@ package com.google.gerrit.client.change;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.changes.ChangeFileApi;
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;
@@ -79,7 +79,7 @@ class EditMessageBox extends Composite {
@UiHandler("save")
void onSave(@SuppressWarnings("unused") ClickEvent e) {
save.setEnabled(false);
ChangeFileApi.putMessage(changeId, message.getText().trim(),
ChangeEditApi.putMessage(changeId.get(), message.getText().trim(),
new GerritCallback<VoidResult>() {
@Override
public void onSuccess(VoidResult result) {

View File

@@ -18,7 +18,7 @@ import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeFileApi;
import com.google.gerrit.client.changes.ChangeEditApi;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.changes.ReviewInfo;
import com.google.gerrit.client.changes.Util;
@@ -317,7 +317,7 @@ public class FileTable extends FlowPanel {
void onDelete(int idx) {
String path = list.get(idx).path();
ChangeFileApi.deleteContent(curr, path,
ChangeEditApi.delete(curr.getParentKey().get(), path,
new AsyncCallback<VoidResult>() {
@Override
public void onSuccess(VoidResult result) {
@@ -333,7 +333,7 @@ public class FileTable extends FlowPanel {
void onRestore(int idx) {
String path = list.get(idx).path();
ChangeFileApi.restoreContent(curr, path,
ChangeEditApi.restore(curr.getParentKey().get(), path,
new AsyncCallback<VoidResult>() {
@Override
public void onSuccess(VoidResult result) {

View File

@@ -1,70 +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.ChangeFileApi;
import com.google.gerrit.client.rpc.GerritCallback;
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() {
ChangeFileApi.getContent(id, getText(), new GerritCallback<String>() {
@Override
public void onSuccess(String result) {
textArea.setText(result);
}
@Override
public void onFailure(Throwable caught) {
if (RestApi.isNotFound(caught)) {
// that means that the file doesn't exist in the repository
} else {
super.onFailure(caught);
}
}
});
}
}

View File

@@ -0,0 +1,90 @@
// 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.changes;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.HttpCallback;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.rpc.AsyncCallback;
/** REST API helpers to remotely edit a change. */
public class ChangeEditApi {
/** Get file (or commit message) contents. */
public static void get(PatchSet.Id id, String path,
HttpCallback<NativeString> cb) {
RestApi api;
if (id.get() != 0) {
// Read from a published revision, when change edit doesn't
// exist for the caller, or is not currently active.
api = ChangeApi.revision(id).view("files").id(path).view("content");
} else if (Patch.COMMIT_MSG.equals(path)) {
api = editMessage(id.getParentKey().get());
} else {
api = editFile(id.getParentKey().get(), path);
}
api.get(cb);
}
/** Put message into a change edit. */
public static void putMessage(int id, String m, GerritCallback<VoidResult> cb) {
editMessage(id).put(m, cb);
}
/** Put contents into a file or commit message in a change edit. */
public static void put(int id, String path, String content,
GerritCallback<VoidResult> cb) {
if (Patch.COMMIT_MSG.equals(path)) {
putMessage(id, content, cb);
} else {
editFile(id, path).put(content, cb);
}
}
/** Delete a file in the pending edit. */
public static void delete(int id, String path, AsyncCallback<VoidResult> cb) {
editFile(id, path).delete(cb);
}
/** Restore (undo delete/modify) a file in the pending edit. */
public static void restore(int id, String path, AsyncCallback<VoidResult> cb) {
Input in = Input.create();
in.restore_path(path);
ChangeApi.edit(id).post(in, cb);
}
private static RestApi editMessage(int id) {
return ChangeApi.change(id).view("edit:message");
}
private static RestApi editFile(int id, String path) {
return ChangeApi.edit(id).id(path);
}
private static class Input extends JavaScriptObject {
static Input create() {
return createObject().cast();
}
final native void restore_path(String p) /*-{ this.restore_path=p }-*/;
protected Input() {
}
}
}

View File

@@ -1,166 +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.changes;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.rpc.AsyncCallback;
/**
* A collection of static methods which work on the Gerrit REST API for specific
* files in a change.
*/
public class ChangeFileApi {
static abstract class CallbackWrapper<I, O> implements AsyncCallback<I> {
protected AsyncCallback<O> wrapped;
public CallbackWrapper(AsyncCallback<O> callback) {
wrapped = callback;
}
@Override
public abstract void onSuccess(I result);
@Override
public void onFailure(Throwable caught) {
wrapped.onFailure(caught);
}
}
private static CallbackWrapper<NativeString, String> wrapper(
AsyncCallback<String> cb) {
return new CallbackWrapper<NativeString, String>(cb) {
@Override
public void onSuccess(NativeString b64) {
if (b64 != null) {
wrapped.onSuccess(b64decode(b64.asString()));
}
}
};
}
/** Get the contents of a File in a PatchSet or change edit. */
public static void getContent(PatchSet.Id id, String filename,
AsyncCallback<String> cb) {
contentEditOrPs(id, filename).get(wrapper(cb));
}
/** Get the content type of a File in a PatchSet or change edit. */
public static void getContentType(PatchSet.Id id, String filename,
AsyncCallback<String> cb) {
contentTypeEditOrPs(id, filename).get(
new CallbackWrapper<NativeString, String>(cb) {
@Override
public void onSuccess(NativeString str) {
if (str != null) {
wrapped.onSuccess(str.asString());
}
}
});
}
/**
* Get the contents of a file or commit message in a PatchSet or change
* edit.
**/
public static void getContentOrMessage(PatchSet.Id id, String path,
AsyncCallback<String> cb) {
RestApi api = (Patch.COMMIT_MSG.equals(path) && id.get() == 0)
? messageEdit(id)
: contentEditOrPs(id, path);
api.get(wrapper(cb));
}
/** Put contents into a File in a change edit. */
public static void putContent(PatchSet.Id id, String filename,
String content, GerritCallback<VoidResult> result) {
contentEdit(id.getParentKey(), filename).put(content, result);
}
/** Put contents into a File or commit message in a change edit. */
public static void putContentOrMessage(PatchSet.Id id, String path,
String content, GerritCallback<VoidResult> result) {
if (Patch.COMMIT_MSG.equals(path)) {
putMessage(id, content, result);
} else {
contentEdit(id.getParentKey(), path).put(content, result);
}
}
/** Put message into a change edit. */
private static void putMessage(PatchSet.Id id, String m,
GerritCallback<VoidResult> r) {
putMessage(id.getParentKey(), m, r);
}
/** Put message into a change edit. */
public static void putMessage(Change.Id id, String m,
GerritCallback<VoidResult> r) {
ChangeApi.change(id.get()).view("edit:message").put(m, r);
}
/** Restore contents of a File in a change edit. */
public static void restoreContent(PatchSet.Id id, String filename,
AsyncCallback<VoidResult> result) {
Input in = Input.create();
in.restore_path(filename);
ChangeApi.edit(id.getParentKey().get()).post(in, result);
}
/** Delete a file from a change edit. */
public static void deleteContent(PatchSet.Id id, String filename,
AsyncCallback<VoidResult> result) {
contentEdit(id.getParentKey(), filename).delete(result);
}
private static RestApi contentEditOrPs(PatchSet.Id id, String filename) {
return id.get() == 0
? contentEdit(id.getParentKey(), filename)
: ChangeApi.revision(id).view("files").id(filename).view("content");
}
private static RestApi messageEdit(PatchSet.Id id) {
return ChangeApi.change(id.getParentKey().get()).view("edit:message");
}
private static RestApi contentTypeEditOrPs(PatchSet.Id id, String filename) {
return id.get() == 0
? contentEdit(id.getParentKey(), filename).view("type")
: ChangeApi.revision(id).view("files").id(filename).view("type");
}
private static RestApi contentEdit(Change.Id id, String filename) {
return ChangeApi.edit(id.get()).id(filename);
}
private static native String b64decode(String a) /*-{ return window.atob(a); }-*/;
private static class Input extends JavaScriptObject {
final native void restore_path(String p) /*-{ if(p)this.restore_path=p; }-*/;
static Input create() {
return (Input) createObject();
}
protected Input() {
}
}
}

View File

@@ -18,11 +18,11 @@ import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.CommentApi;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.rpc.AsyncCallback;
import java.util.Collections;
import java.util.Comparator;
@@ -53,39 +53,55 @@ class CommentsCollections {
}
}
private GerritCallback<NativeMap<JsArray<CommentInfo>>> publishedBase() {
return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
private AsyncCallback<NativeMap<JsArray<CommentInfo>>> publishedBase() {
return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
@Override
public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
publishedBase = sort(result.get(path));
}
@Override
public void onFailure(Throwable caught) {
}
};
}
private GerritCallback<NativeMap<JsArray<CommentInfo>>> publishedRevision() {
return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
private AsyncCallback<NativeMap<JsArray<CommentInfo>>> publishedRevision() {
return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
@Override
public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
publishedRevision = sort(result.get(path));
}
};
}
private GerritCallback<NativeMap<JsArray<CommentInfo>>> draftsBase() {
return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
@Override
public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
draftsBase = sort(result.get(path));
public void onFailure(Throwable caught) {
}
};
}
private GerritCallback<NativeMap<JsArray<CommentInfo>>> draftsRevision() {
return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
private AsyncCallback<NativeMap<JsArray<CommentInfo>>> draftsBase() {
return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
@Override
public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
draftsBase = sort(result.get(path));
}
@Override
public void onFailure(Throwable caught) {
}
};
}
private AsyncCallback<NativeMap<JsArray<CommentInfo>>> draftsRevision() {
return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
@Override
public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
draftsRevision = sort(result.get(path));
}
@Override
public void onFailure(Throwable caught) {
}
};
}

View File

@@ -167,26 +167,36 @@ public class SideBySide2 extends Screen {
protected void onLoad() {
super.onLoad();
CallbackGroup cmGroup = new CallbackGroup();
CodeMirror.initLibrary(cmGroup.<Void> addEmpty());
CallbackGroup group1 = new CallbackGroup();
final CallbackGroup group2 = new CallbackGroup();
final CallbackGroup group = new CallbackGroup();
final AsyncCallback<Void> themeCallback = group.addEmpty();
final AsyncCallback<Void> modeInjectorCb = group.addEmpty();
CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
final AsyncCallback<Void> themeCallback = group2.addEmpty();
@Override
public void onSuccess(Void result) {
// Load theme after CM library to ensure theme can override CSS.
ThemeLoader.loadTheme(prefs.theme(), themeCallback);
}
@Override
public void onFailure(Throwable caught) {
}
}));
DiffApi.diff(revision, path)
.base(base)
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
.get(cmGroup.addFinal(new GerritCallback<DiffInfo>() {
.get(group1.addFinal(new GerritCallback<DiffInfo>() {
final AsyncCallback<Void> modeInjectorCb = group2.addEmpty();
@Override
public void onSuccess(DiffInfo diffInfo) {
diff = diffInfo;
fileSize = bucketFileSize(diffInfo);
// Load theme after CM library to ensure theme can override CSS.
ThemeLoader.loadTheme(prefs.theme(), themeCallback);
if (prefs.syntaxHighlighting()) {
if (fileSize.compareTo(FileSize.SMALL) > 0) {
modeInjectorCb.onSuccess(null);
@@ -200,22 +210,26 @@ public class SideBySide2 extends Screen {
}));
if (Gerrit.isSignedIn()) {
ChangeApi.edit(changeId.get(), group.add(
new GerritCallback<EditInfo>() {
ChangeApi.edit(changeId.get(), group2.add(
new AsyncCallback<EditInfo>() {
@Override
public void onSuccess(EditInfo result) {
edit = result;
}
@Override
public void onFailure(Throwable caught) {
}
}));
}
final CommentsCollections comments = new CommentsCollections();
comments.load(base, revision, path, group);
comments.load(base, revision, path, group2);
RestApi call = ChangeApi.detail(changeId.get());
ChangeList.addOptions(call, EnumSet.of(
ListChangesOption.ALL_REVISIONS));
call.get(group.add(new GerritCallback<ChangeInfo>() {
call.get(group2.add(new AsyncCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo info) {
info.revisions().copyKeysIntoChildren("name");
@@ -230,9 +244,14 @@ public class SideBySide2 extends Screen {
diffTable.set(prefs, list, diff, edit != null, currentPatchSet,
info.status().isOpen());
header.setChangeInfo(info);
}}));
}
ConfigInfoCache.get(changeId, group.addFinal(
@Override
public void onFailure(Throwable caught) {
}
}));
ConfigInfoCache.get(changeId, group2.addFinal(
new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide2.this) {
@Override
protected void preDisplay(ConfigInfoCache.Entry result) {

View File

@@ -14,17 +14,23 @@
package com.google.gerrit.client.editor;
import static com.google.gwt.dom.client.Style.Visibility.HIDDEN;
import static com.google.gwt.dom.client.Style.Visibility.VISIBLE;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.JumpKeys;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.account.DiffPreferences;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeFileApi;
import com.google.gerrit.client.changes.ChangeEditApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.diff.FileInfo;
import com.google.gerrit.client.diff.Header;
import com.google.gerrit.client.rpc.CallbackGroup;
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.ScreenLoadCallback;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.PageLinks;
@@ -68,11 +74,14 @@ public class EditScreen extends Screen {
private final String path;
private DiffPreferences prefs;
private CodeMirror cm;
private String type;
private HttpResponse<NativeString> content;
@UiField Element header;
@UiField Element project;
@UiField Element filePath;
@UiField Element cursLine;
@UiField Element cursCol;
@UiField Element dirty;
@UiField Button close;
@UiField Button save;
@UiField Element editor;
@@ -100,10 +109,11 @@ public class EditScreen extends Screen {
protected void onLoad() {
super.onLoad();
CallbackGroup cmGroup = new CallbackGroup();
final CallbackGroup group = new CallbackGroup();
CodeMirror.initLibrary(cmGroup.add(new AsyncCallback<Void>() {
final AsyncCallback<Void> themeCallback = group.addEmpty();
CallbackGroup group1 = new CallbackGroup();
final CallbackGroup group2 = new CallbackGroup();
CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
final AsyncCallback<Void> themeCallback = group2.addEmpty();
@Override
public void onSuccess(Void result) {
@@ -116,36 +126,48 @@ public class EditScreen extends Screen {
}
}));
if (prefs.syntaxHighlighting() && !Patch.COMMIT_MSG.equals(path)) {
final AsyncCallback<Void> modeInjectorCb = group.addEmpty();
ChangeFileApi.getContentType(revision, path,
cmGroup.add(new GerritCallback<String>() {
@Override
public void onSuccess(String result) {
ModeInfo mode = ModeInfo.findMode(result, path);
type = mode != null ? mode.mime() : null;
injectMode(result, modeInjectorCb);
}
}));
}
cmGroup.done();
ChangeApi.detail(revision.getParentKey().get(),
group.add(new GerritCallback<ChangeInfo>() {
group1.add(new AsyncCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo c) {
project.setInnerText(c.project());
SafeHtml.setInnerHTML(filePath, Header.formatPath(path, null, null));
}
}));
ChangeFileApi.getContentOrMessage(revision, path,
group.addFinal(new ScreenLoadCallback<String>(this) {
@Override
protected void preDisplay(String content) {
initEditor(content);
public void onFailure(Throwable caught) {
}
}));
ChangeEditApi.get(revision, path,
group1.add(new HttpCallback<NativeString>() {
final AsyncCallback<Void> modeCallback = group2.addEmpty();
@Override
public void onSuccess(HttpResponse<NativeString> fc) {
content = fc;
if (prefs.syntaxHighlighting()) {
injectMode(fc.getContentType(), modeCallback);
} else {
modeCallback.onSuccess(null);
}
}
@Override
public void onFailure(Throwable e) {
GerritCallback.showFailure(e);
}
}));
group2.addListener(new ScreenLoadCallback<Void>(this) {
@Override
protected void preDisplay(Void result) {
initEditor(content);
content = null;
}
});
group1.done();
group2.done();
}
@Override
@@ -172,11 +194,11 @@ public class EditScreen extends Screen {
});
generation = cm.changeGeneration(true);
save.setEnabled(false);
setClean(true);
cm.on(new ChangesHandler() {
@Override
public void handle(CodeMirror cm) {
save.setEnabled(!cm.isClean(generation));
setClean(cm.isClean(generation));
}
});
@@ -223,12 +245,12 @@ public class EditScreen extends Screen {
Gerrit.display(PageLinks.toChangeInEditMode(revision.getParentKey()));
}
private void initEditor(String content) {
private void initEditor(HttpResponse<NativeString> file) {
ModeInfo mode = prefs.syntaxHighlighting()
? ModeInfo.findMode(type, path)
? ModeInfo.findMode(file.getContentType(), path)
: null;
cm = CodeMirror.create(editor, Configuration.create()
.set("value", content)
.set("value", file.getResult().asString())
.set("readOnly", false)
.set("cursorBlinkRate", 0)
.set("cursorHeight", 0.85)
@@ -272,9 +294,16 @@ public class EditScreen extends Screen {
private void updateActiveLine() {
Pos p = cm.getCursor("end");
cursLine.setInnerText(Integer.toString(p.line() + 1));
cursCol.setInnerText(Integer.toString(p.ch() + 1));
cm.extras().activeLine(cm.getLineHandleVisualStart(p.line()));
}
private void setClean(boolean clean) {
save.setEnabled(!clean);
dirty.getStyle().setVisibility(!clean ? VISIBLE : HIDDEN);
}
private Runnable save() {
return new Runnable() {
@Override
@@ -282,12 +311,12 @@ public class EditScreen extends Screen {
if (!cm.isClean(generation)) {
String text = cm.getValue();
final int g = cm.changeGeneration(false);
ChangeFileApi.putContentOrMessage(revision, path, text,
ChangeEditApi.put(revision.getParentKey().get(), path, text,
new GerritCallback<VoidResult>() {
@Override
public void onSuccess(VoidResult result) {
generation = g;
save.setEnabled(!cm.isClean(g));
setClean(cm.isClean(g));
}
});
}

View File

@@ -62,6 +62,34 @@ limitations under the License.
.path {
white-space: nowrap;
}
.statusLine {
position: fixed;
bottom: 0;
left: 0;
width: 175px;
height: 19px;
background-color: #f7f7f7;
border-top: 1px solid #ddd;
border-right: 1px solid #ddd;
}
.statusLine div {
height: inherit;
}
.cursorPosition {
display: inline-block;
margin: 0 5px 0 35px;
white-space: nowrap;
}
.dirty {
display: inline-block;
margin: 0 5px 0 5px;
padding: 0 0 0 5px;
border-left: 1px solid #ddd;
font-weight: bold;
}
</ui:style>
<g:HTMLPanel>
<div class='{style.headerLine}' ui:field='header'>
@@ -82,5 +110,9 @@ limitations under the License.
<span class='{style.path}'><span ui:field='project'/> / <span ui:field='filePath'/></span>
</div>
<div ui:field='editor' />
<div class='{style.statusLine}'>
<div class='{style.cursorPosition}'><span ui:field='cursLine'/> : <span ui:field='cursCol'/></div>
<div class='{style.dirty}' ui:field='dirty'>Unsaved</div>
</div>
</g:HTMLPanel>
</ui:UiBinder>

View File

@@ -42,8 +42,8 @@ import java.util.Set;
* processing it.
*/
public class CallbackGroup {
private final List<CallbackImpl<?>> callbacks;
private final Set<CallbackImpl<?>> remaining;
private final List<CallbackGlue> callbacks;
private final Set<CallbackGlue> remaining;
private boolean finalAdded;
private boolean failed;
@@ -76,6 +76,27 @@ public class CallbackGroup {
return handleAdd(cb);
}
public <T> HttpCallback<T> add(HttpCallback<T> cb) {
checkFinalAdded();
if (failed) {
cb.onFailure(failedThrowable);
return new HttpCallback<T>() {
@Override
public void onSuccess(HttpResponse<T> result) {
}
@Override
public void onFailure(Throwable caught) {
}
};
}
HttpCallbackImpl<T> w = new HttpCallbackImpl<>(cb);
callbacks.add(w);
remaining.add(w);
return w;
}
public <T> Callback<T> addFinal(final AsyncCallback<T> cb) {
checkFinalAdded();
finalAdded = true;
@@ -84,7 +105,7 @@ public class CallbackGroup {
public void done() {
finalAdded = true;
applyAllSuccess();
apply();
}
public void addListener(AsyncCallback<Void> cb) {
@@ -99,19 +120,30 @@ public class CallbackGroup {
addListener(group.<Void> addEmpty());
}
private void applyAllSuccess() {
if (!failed && finalAdded && remaining.isEmpty()) {
for (CallbackImpl<?> cb : callbacks) {
cb.applySuccess();
}
callbacks.clear();
}
private void success(CallbackGlue cb) {
remaining.remove(cb);
apply();
}
private void applyAllFailed() {
if (failed && finalAdded && remaining.isEmpty()) {
for (CallbackImpl<?> cb : callbacks) {
cb.applyFailed();
private <T> void failure(CallbackGlue w, Throwable caught) {
if (!failed) {
failed = true;
failedThrowable = caught;
}
remaining.remove(w);
apply();
}
private void apply() {
if (finalAdded && remaining.isEmpty()) {
if (failed) {
for (CallbackGlue cb : callbacks) {
cb.applyFailed();
}
} else {
for (CallbackGlue cb : callbacks) {
cb.applySuccess();
}
}
callbacks.clear();
}
@@ -139,7 +171,12 @@ public class CallbackGroup {
extends AsyncCallback<T>, com.google.gwtjsonrpc.common.AsyncCallback<T> {
}
private class CallbackImpl<T> implements Callback<T> {
private interface CallbackGlue {
void applySuccess();
void applyFailed();
}
private class CallbackImpl<T> implements Callback<T>, CallbackGlue {
AsyncCallback<T> delegate;
T result;
@@ -150,21 +187,16 @@ public class CallbackGroup {
@Override
public void onSuccess(T value) {
this.result = value;
remaining.remove(this);
CallbackGroup.this.applyAllSuccess();
success(this);
}
@Override
public void onFailure(Throwable caught) {
if (!failed) {
failed = true;
failedThrowable = caught;
}
remaining.remove(this);
CallbackGroup.this.applyAllFailed();
failure(this, caught);
}
void applySuccess() {
@Override
public void applySuccess() {
AsyncCallback<T> cb = delegate;
if (cb != null) {
delegate = null;
@@ -173,7 +205,8 @@ public class CallbackGroup {
}
}
void applyFailed() {
@Override
public void applyFailed() {
AsyncCallback<T> cb = delegate;
if (cb != null) {
delegate = null;
@@ -182,4 +215,44 @@ public class CallbackGroup {
}
}
}
private class HttpCallbackImpl<T> implements HttpCallback<T>, CallbackGlue {
private HttpCallback<T> delegate;
private HttpResponse<T> result;
HttpCallbackImpl(HttpCallback<T> delegate) {
this.delegate = delegate;
}
@Override
public void onSuccess(HttpResponse<T> result) {
this.result = result;
success(this);
}
@Override
public void onFailure(Throwable caught) {
failure(this, caught);
}
@Override
public void applySuccess() {
HttpCallback<T> cb = delegate;
if (cb != null) {
delegate = null;
cb.onSuccess(result);
result = null;
}
}
@Override
public void applyFailed() {
HttpCallback<T> cb = delegate;
if (cb != null) {
delegate = null;
result = null;
cb.onFailure(failedThrowable);
}
}
}
}

View File

@@ -23,7 +23,6 @@ import com.google.gerrit.common.errors.NoSuchAccountException;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.common.errors.NotSignedInException;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.InvocationException;
import com.google.gwtjsonrpc.client.RemoteJsonException;
import com.google.gwtjsonrpc.client.ServerUnavailableException;
@@ -35,6 +34,10 @@ public abstract class GerritCallback<T> implements
com.google.gwt.user.client.rpc.AsyncCallback<T> {
@Override
public void onFailure(final Throwable caught) {
showFailure(caught);
}
public static void showFailure(Throwable caught) {
if (isNotSignedIn(caught) || isInvalidXSRF(caught)) {
new NotSignedInDialog().center();
@@ -70,7 +73,6 @@ public abstract class GerritCallback<T> implements
new ErrorDialog(RpcConstants.C.errorServerUnavailable()).center();
} else {
GWT.log(getClass().getName() + " caught " + caught, caught);
new ErrorDialog(caught).center();
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (C) 2015 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.rpc;
/** AsyncCallback supplied with HTTP response headers. */
public interface HttpCallback<T> {
void onSuccess(HttpResponse<T> result);
void onFailure(Throwable caught);
}

View File

@@ -0,0 +1,56 @@
// Copyright (C) 2015 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.rpc;
import com.google.gwt.http.client.Response;
/** Wraps decoded server reply with HTTP headers. */
public class HttpResponse<T> {
private final Response httpResponse;
private final String contentType;
private final T result;
HttpResponse(Response httpResponse, String contentType, T result) {
this.httpResponse = httpResponse;
this.contentType = contentType;
this.result = result;
}
/** HTTP status code, always in the 2xx family. */
public int getStatusCode() {
return httpResponse.getStatusCode();
}
/**
* Content type supplied by the server.
*
* This helper simplifies the common {@code getHeader("Content-Type")} case.
*/
public String getContentType() {
return contentType;
}
/** Lookup an arbitrary reply header. */
public String getHeader(String header) {
if ("Content-Type".equals(header)) {
return contentType;
}
return httpResponse.getHeader(header);
}
public T getResult() {
return result;
}
}

View File

@@ -104,21 +104,21 @@ public class RestApi {
}
}
private static class HttpCallback<T extends JavaScriptObject>
private static class HttpImpl<T extends JavaScriptObject>
implements RequestCallback {
private final boolean background;
private final AsyncCallback<T> cb;
private final HttpCallback<T> cb;
HttpCallback(boolean bg, AsyncCallback<T> cb) {
HttpImpl(boolean bg, HttpCallback<T> cb) {
this.background = bg;
this.cb = cb;
}
@Override
public void onResponseReceived(Request req, Response res) {
public void onResponseReceived(Request req, final Response res) {
int status = res.getStatusCode();
if (status == Response.SC_NO_CONTENT) {
cb.onSuccess(null);
cb.onSuccess(new HttpResponse<T>(res, null, null));
if (!background) {
RpcStatus.INSTANCE.onRpcComplete();
}
@@ -126,12 +126,12 @@ public class RestApi {
} else if (200 <= status && status < 300) {
long start = System.currentTimeMillis();
final T data;
if (isTextBody(res)) {
data = NativeString.wrap(res.getText()).cast();
} else if (isJsonBody(res)) {
final String type;
if (isJsonBody(res)) {
try {
// javac generics bug
data = RestApi.<T>cast(parseJson(res));
data = RestApi.<T> cast(parseJson(res));
type = JSON_TYPE;
} catch (JSONException e) {
if (!background) {
RpcStatus.INSTANCE.onRpcComplete();
@@ -140,6 +140,12 @@ public class RestApi {
"Invalid JSON: " + e.getMessage()));
return;
}
} else if (isEncodedBase64(res)) {
data = NativeString.wrap(decodeBase64(res.getText())).cast();
type = simpleType(res.getHeader("X-FYI-Content-Type"));
} else if (isTextBody(res)) {
data = NativeString.wrap(res.getText()).cast();
type = TEXT_TYPE;
} else {
if (!background) {
RpcStatus.INSTANCE.onRpcComplete();
@@ -154,7 +160,7 @@ public class RestApi {
@Override
public void execute() {
try {
cb.onSuccess(data);
cb.onSuccess(new HttpResponse<>(res, type, data));
} finally {
if (!background) {
RpcStatus.INSTANCE.onRpcComplete();
@@ -318,21 +324,24 @@ public class RestApi {
}
public <T extends JavaScriptObject> void get(AsyncCallback<T> cb) {
get(wrap(cb));
}
public <T extends JavaScriptObject> void get(HttpCallback<T> cb) {
send(GET, cb);
}
public <T extends JavaScriptObject> void delete(AsyncCallback<T> cb) {
delete(wrap(cb));
}
public <T extends JavaScriptObject> void delete(HttpCallback<T> cb) {
send(DELETE, cb);
}
public <T extends JavaScriptObject> void delete(JavaScriptObject content,
AsyncCallback<T> cb) {
sendJSON(DELETE, content, cb);
}
private <T extends JavaScriptObject> void send(
Method method, AsyncCallback<T> cb) {
HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
private <T extends JavaScriptObject> void send(Method method,
HttpCallback<T> cb) {
HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
try {
if (!background) {
RpcStatus.INSTANCE.onRpcStart();
@@ -346,33 +355,59 @@ public class RestApi {
public <T extends JavaScriptObject> void post(
JavaScriptObject content,
AsyncCallback<T> cb) {
post(content, wrap(cb));
}
public <T extends JavaScriptObject> void post(
JavaScriptObject content,
HttpCallback<T> cb) {
sendJSON(POST, content, cb);
}
public <T extends JavaScriptObject> void post(String content,
AsyncCallback<T> cb) {
post(content, wrap(cb));
}
public <T extends JavaScriptObject> void post(String content,
HttpCallback<T> cb) {
sendRaw(POST, content, cb);
}
public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) {
put(wrap(cb));
}
public <T extends JavaScriptObject> void put(HttpCallback<T> cb) {
send(PUT, cb);
}
public <T extends JavaScriptObject> void put(String content,
AsyncCallback<T> cb) {
put(content, wrap(cb));
}
public <T extends JavaScriptObject> void put(String content,
HttpCallback<T> cb) {
sendRaw(PUT, content, cb);
}
public <T extends JavaScriptObject> void put(
JavaScriptObject content,
AsyncCallback<T> cb) {
put(content, wrap(cb));
}
public <T extends JavaScriptObject> void put(
JavaScriptObject content,
HttpCallback<T> cb) {
sendJSON(PUT, content, cb);
}
private <T extends JavaScriptObject> void sendJSON(
Method method, JavaScriptObject content,
AsyncCallback<T> cb) {
HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
HttpCallback<T> cb) {
HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
try {
if (!background) {
RpcStatus.INSTANCE.onRpcStart();
@@ -385,11 +420,15 @@ public class RestApi {
}
}
private static native String str(JavaScriptObject jso) /*-{ return JSON.stringify(jso); }-*/;
private static native String str(JavaScriptObject jso)
/*-{ return JSON.stringify(jso) }-*/;
private static native String decodeBase64(String a)
/*-{ return $wnd.atob(a) }-*/;
private <T extends JavaScriptObject> void sendRaw(Method method, String body,
AsyncCallback<T> cb) {
HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
HttpCallback<T> cb) {
HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
try {
if (!background) {
RpcStatus.INSTANCE.onRpcStart();
@@ -422,16 +461,22 @@ public class RestApi {
return isContentType(res, TEXT_TYPE);
}
private static boolean isEncodedBase64(Response res) {
return "base64".equals(res.getHeader("X-FYI-Content-Encoding"))
&& isTextBody(res);
}
private static boolean isContentType(Response res, String want) {
String type = res.getHeader("Content-Type");
if (type == null) {
return false;
}
return type != null && want.equals(simpleType(type));
}
private static String simpleType(String type) {
int semi = type.indexOf(';');
if (semi >= 0) {
type = type.substring(0, semi).trim();
return type.substring(0, semi).trim();
}
return want.equals(type);
return type;
}
private static JSONValue parseJson(Response res)
@@ -464,4 +509,19 @@ public class RestApi {
throw new JSONException("unsupported JSON type");
}
}
private static <T extends JavaScriptObject> HttpCallback<T> wrap(
final AsyncCallback<T> cb) {
return new HttpCallback<T>() {
@Override
public void onSuccess(HttpResponse<T> r) {
cb.onSuccess(r.getResult());
}
@Override
public void onFailure(Throwable e) {
cb.onFailure(e);
}
};
}
}

View File

@@ -15,7 +15,6 @@
package net.codemirror.lib;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.ScriptInjector;
import com.google.gwt.dom.client.ScriptElement;
@@ -39,11 +38,15 @@ public class Loader {
CallbackGroup group = new CallbackGroup();
injectCss(Lib.I.css(), group.<Void> addEmpty());
injectScript(Lib.I.js().getSafeUri(), group.add(new GerritCallback<Void>() {
injectScript(Lib.I.js().getSafeUri(), group.add(new AsyncCallback<Void>() {
@Override
public void onSuccess(Void result) {
Vim.initKeyMap();
}
@Override
public void onFailure(Throwable caught) {
}
}));
group.addListener(cb);
group.done();

View File

@@ -187,7 +187,7 @@ class RevisionApiImpl extends RevisionApi.NotImplemented implements RevisionApi
return ImmutableSet.copyOf((Iterable<String>) listFiles
.get().setReviewed(true)
.apply(revision).value());
} catch (OrmException e) {
} catch (OrmException | IOException e) {
throw new RestApiException("Cannot list reviewed files", e);
}
}

View File

@@ -52,6 +52,7 @@ import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.lib.ObjectId;
import org.kohsuke.args4j.Option;
import java.io.IOException;
@@ -433,8 +434,8 @@ public class ChangeEdits implements
throws ResourceNotFoundException, IOException {
try {
return Response.ok(fileContentUtil.getContent(
rsrc.getChangeEdit().getChange().getProject(),
rsrc.getChangeEdit().getRevision().get(),
rsrc.getControl().getProjectControl().getProjectState(),
ObjectId.fromString(rsrc.getChangeEdit().getRevision().get()),
rsrc.getPath()));
} catch (ResourceNotFoundException rnfe) {
return Response.none();
@@ -502,29 +503,12 @@ public class ChangeEdits implements
IOException, ResourceNotFoundException {
Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
if (edit.isPresent()) {
return BinaryResult.create(
edit.get().getEditCommit().getFullMessage()).base64();
String msg = edit.get().getEditCommit().getFullMessage();
return BinaryResult.create(msg)
.setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
.base64();
}
throw new ResourceNotFoundException();
}
}
@Singleton
public static class GetType implements RestReadView<ChangeEditResource> {
private final FileContentUtil fileContentUtil;
@Inject
GetType(FileContentUtil fileContentUtil) {
this.fileContentUtil = fileContentUtil;
}
@Override
public String apply(ChangeEditResource rsrc)
throws ResourceNotFoundException, IOException {
return fileContentUtil.getContentType(
rsrc.getChangeEdit().getChange().getProject(),
rsrc.getChangeEdit().getRevision().get(),
rsrc.getPath());
}
}
}

View File

@@ -16,14 +16,19 @@ package com.google.gerrit.server.change;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.gerrit.common.data.PatchScript.FileMode;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.server.FileTypeRegistry;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
@@ -36,6 +41,11 @@ import java.io.OutputStream;
@Singleton
public class FileContentUtil {
public static final String TEXT_X_GERRIT_COMMIT_MESSAGE = "text/x-gerrit-commit-message";
private static final String X_GIT_SYMLINK = "x-git/symlink";
private static final String X_GIT_GITLINK = "x-git/gitlink";
private static final int MAX_SIZE = 5 << 20;
private final GitRepositoryManager repoManager;
private final FileTypeRegistry registry;
@@ -46,28 +56,50 @@ public class FileContentUtil {
this.registry = ftr;
}
public BinaryResult getContent(Project.NameKey project, String revstr,
public BinaryResult getContent(ProjectState project, ObjectId revstr,
String path) throws ResourceNotFoundException, IOException {
Repository repo = repoManager.openRepository(project);
Repository repo = openRepository(project);
try {
RevWalk rw = new RevWalk(repo);
try {
RevCommit commit = rw.parseCommit(repo.resolve(revstr));
TreeWalk tw =
TreeWalk.forPath(rw.getObjectReader(), path,
commit.getTree().getId());
RevCommit commit = rw.parseCommit(revstr);
ObjectReader reader = rw.getObjectReader();
TreeWalk tw = TreeWalk.forPath(reader, path, commit.getTree());
if (tw == null) {
throw new ResourceNotFoundException();
}
final ObjectLoader object = repo.open(tw.getObjectId(0));
@SuppressWarnings("resource")
BinaryResult result = new BinaryResult() {
@Override
public void writeTo(OutputStream os) throws IOException {
object.copyTo(os);
}
};
return result.setContentLength(object.getSize()).base64();
org.eclipse.jgit.lib.FileMode mode = tw.getFileMode(0);
ObjectId id = tw.getObjectId(0);
if (mode == org.eclipse.jgit.lib.FileMode.GITLINK) {
return BinaryResult.create(id.name())
.setContentType(X_GIT_GITLINK)
.base64();
}
final ObjectLoader obj = repo.open(id, OBJ_BLOB);
byte[] raw;
try {
raw = obj.getCachedBytes(MAX_SIZE);
} catch (LargeObjectException e) {
raw = null;
}
BinaryResult result;
if (raw != null) {
result = BinaryResult.create(raw);
} else {
result = asBinaryResult(obj);
}
String type;
if (mode == org.eclipse.jgit.lib.FileMode.SYMLINK) {
type = X_GIT_SYMLINK;
} else {
type = registry.getMimeType(path, raw).toString();
type = resolveContentType(project, path, FileMode.FILE, type);
}
return result.setContentType(type).base64();
} finally {
rw.release();
}
@@ -76,31 +108,44 @@ public class FileContentUtil {
}
}
public String getContentType(Project.NameKey project, String revstr,
String path) throws ResourceNotFoundException, IOException {
Repository repo = repoManager.openRepository(project);
try {
RevWalk rw = new RevWalk(repo);
ObjectReader reader = repo.newObjectReader();
try {
RevCommit commit = rw.parseCommit(repo.resolve(revstr));
TreeWalk tw =
TreeWalk.forPath(rw.getObjectReader(), path,
commit.getTree().getId());
if (tw == null) {
throw new ResourceNotFoundException();
}
ObjectLoader blobLoader = reader.open(tw.getObjectId(0), OBJ_BLOB);
byte[] raw = blobLoader.isLarge()
? null
: blobLoader.getCachedBytes();
return registry.getMimeType(path, raw).toString();
} finally {
reader.release();
rw.release();
private static BinaryResult asBinaryResult(final ObjectLoader obj) {
@SuppressWarnings("resource")
BinaryResult result = new BinaryResult() {
@Override
public void writeTo(OutputStream os) throws IOException {
obj.copyTo(os);
}
} finally {
repo.close();
}.setContentLength(obj.getSize());
return result;
}
public static String resolveContentType(ProjectState project, String path,
FileMode fileMode, String mimeType) {
switch (fileMode) {
case FILE:
if (Patch.COMMIT_MSG.equals(path)) {
return TEXT_X_GERRIT_COMMIT_MESSAGE;
}
if (project != null) {
for (ProjectState p : project.tree()) {
String t = p.getConfig().getMimeTypes().getMimeType(path);
if (t != null) {
return t;
}
}
}
return mimeType;
case GITLINK:
return X_GIT_GITLINK;
case SYMLINK:
return X_GIT_SYMLINK;
default:
throw new IllegalStateException("file mode: " + fileMode);
}
}
private Repository openRepository(ProjectState project)
throws RepositoryNotFoundException, IOException {
return repoManager.openRepository(project.getProject().getNameKey());
}
}

View File

@@ -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<RevisionResource, FileResource> {
@Option(name = "--reviewed")
boolean reviewed;
@Option(name = "-q")
String query;
private final Provider<ReviewDb> db;
private final Provider<CurrentUser> self;
private final FileInfoJson fileInfoJson;
@@ -125,11 +132,13 @@ public class Files implements ChildCollection<RevisionResource, FileResource> {
@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<RevisionResource, FileResource> {
}
}
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<String> 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<String> 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<String> reviewed(RevisionResource resource)
throws AuthException, OrmException {
CurrentUser user = self.get();

View File

@@ -24,6 +24,8 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.ObjectId;
import java.io.IOException;
@Singleton
@@ -44,12 +46,14 @@ public class GetContent implements RestReadView<FileResource> {
OrmException {
String path = rsrc.getPatchKey().get();
if (Patch.COMMIT_MSG.equals(path)) {
return BinaryResult.create(
changeUtil.getMessage(rsrc.getRevision().getChange())).base64();
String msg = changeUtil.getMessage(rsrc.getRevision().getChange());
return BinaryResult.create(msg)
.setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
.base64();
}
return fileContentUtil.getContent(
rsrc.getRevision().getControl().getProject().getNameKey(),
rsrc.getRevision().getPatchSet().getRevision().get(),
rsrc.getRevision().getControl().getProjectControl().getProjectState(),
ObjectId.fromString(rsrc.getRevision().getPatchSet().getRevision().get()),
path);
}
}

View File

@@ -1,46 +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.server.change;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@Singleton
public class GetContentType implements RestReadView<FileResource> {
private final FileContentUtil fileContentUtil;
@Inject
GetContentType(FileContentUtil fileContentUtil) {
this.fileContentUtil = fileContentUtil;
}
@Override
public String apply(FileResource rsrc)
throws ResourceNotFoundException, IOException {
String path = rsrc.getPatchKey().get();
if (Patch.COMMIT_MSG.equals(path)) {
return "text/plain";
}
return fileContentUtil.getContentType(
rsrc.getRevision().getControl().getProject().getNameKey(),
rsrc.getRevision().getPatchSet().getRevision().get(),
path);
}
}

View File

@@ -24,7 +24,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.common.data.PatchScript.FileMode;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
@@ -194,7 +193,8 @@ public class GetDiff implements RestReadView<FileResource> {
result.metaA = new FileMeta();
result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(),
ps.getNewName());
setContentType(result.metaA, state, ps.getFileModeA(), ps.getMimeTypeA());
result.metaA.contentType = FileContentUtil.resolveContentType(
state, result.metaA.name, ps.getFileModeA(), ps.getMimeTypeA());
result.metaA.lines = ps.getA().size();
result.metaA.webLinks =
getFileWebLinks(state.getProject(), revA, result.metaA.name);
@@ -203,7 +203,8 @@ public class GetDiff implements RestReadView<FileResource> {
if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
result.metaB = new FileMeta();
result.metaB.name = ps.getNewName();
setContentType(result.metaB, state, ps.getFileModeB(), ps.getMimeTypeB());
result.metaB.contentType = FileContentUtil.resolveContentType(
state, result.metaB.name, ps.getFileModeB(), ps.getMimeTypeB());
result.metaB.lines = ps.getB().size();
result.metaB.webLinks =
getFileWebLinks(state.getProject(), revB, result.metaB.name);
@@ -250,34 +251,6 @@ public class GetDiff implements RestReadView<FileResource> {
return links.isEmpty() ? null : links.toList();
}
private void setContentType(FileMeta meta, ProjectState project,
FileMode fileMode, String mimeType) {
switch (fileMode) {
case FILE:
if (Patch.COMMIT_MSG.equals(meta.name)) {
mimeType = "text/x-gerrit-commit-message";
} else if (project != null) {
for (ProjectState p : project.tree()) {
String t = p.getConfig().getMimeTypes().getMimeType(meta.name);
if (t != null) {
mimeType = t;
break;
}
}
}
meta.contentType = mimeType;
break;
case GITLINK:
meta.contentType = "x-git/gitlink";
break;
case SYMLINK:
meta.contentType = "x-git/symlink";
break;
default:
throw new IllegalStateException("file mode: " + fileMode);
}
}
private static class Content {
final List<ContentEntry> lines;
final SparseFileContent fileA;

View File

@@ -103,7 +103,6 @@ public class Module extends RestApiModule {
put(FILE_KIND, "reviewed").to(PutReviewed.class);
delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
get(FILE_KIND, "content").to(GetContent.class);
get(FILE_KIND, "type").to(GetContentType.class);
get(FILE_KIND, "diff").to(GetDiff.class);
child(CHANGE_KIND, "edit").to(ChangeEdits.class);
@@ -115,7 +114,6 @@ public class Module extends RestApiModule {
put(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Put.class);
delete(CHANGE_EDIT_KIND).to(ChangeEdits.DeleteContent.class);
get(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Get.class);
get(CHANGE_EDIT_KIND, "type").to(ChangeEdits.GetType.class);
install(new FactoryModule() {
@Override

View File

@@ -41,4 +41,8 @@ public class BranchResource extends ProjectResource {
public String getRef() {
return branchInfo.ref;
}
public String getRevision() {
return branchInfo.revision;
}
}

View File

@@ -16,7 +16,6 @@ package com.google.gerrit.server.project;
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Project;
import com.google.inject.TypeLiteral;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -33,8 +32,8 @@ public class CommitResource implements RestResource {
this.commit = commit;
}
public Project.NameKey getProject() {
return project.getNameKey();
public ProjectControl getProject() {
return project.getControl();
}
public RevCommit getCommit() {

View File

@@ -16,28 +16,29 @@ package com.google.gerrit.server.project;
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Project;
import com.google.inject.TypeLiteral;
import org.eclipse.jgit.lib.ObjectId;
public class FileResource implements RestResource {
public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
new TypeLiteral<RestView<FileResource>>() {};
private final Project.NameKey project;
private final String rev;
private final ProjectControl project;
private final ObjectId rev;
private final String path;
public FileResource(Project.NameKey project, String rev, String path) {
public FileResource(ProjectControl project, ObjectId rev, String path) {
this.project = project;
this.rev = rev;
this.path = path;
}
public Project.NameKey getProject() {
public ProjectControl getProject() {
return project;
}
public String getRev() {
public ObjectId getRev() {
return rev;
}

View File

@@ -22,6 +22,8 @@ import com.google.gerrit.extensions.restapi.RestView;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.ObjectId;
@Singleton
public class FilesCollection implements
ChildCollection<BranchResource, FileResource> {
@@ -39,7 +41,10 @@ public class FilesCollection implements
@Override
public FileResource parse(BranchResource parent, IdString id) {
return new FileResource(parent.getNameKey(), parent.getRef(), id.get());
return new FileResource(
parent.getControl(),
ObjectId.fromString(parent.getRevision()),
id.get());
}
@Override

View File

@@ -40,8 +40,7 @@ public class FilesInCommitCollection implements
@Override
public FileResource parse(CommitResource parent, IdString id)
throws ResourceNotFoundException {
return new FileResource(parent.getProject(), parent.getCommit().getName(),
id.get());
return new FileResource(parent.getProject(), parent.getCommit(), id.get());
}
@Override

View File

@@ -35,7 +35,9 @@ public class GetContent implements RestReadView<FileResource> {
@Override
public BinaryResult apply(FileResource rsrc)
throws ResourceNotFoundException, IOException {
return fileContentUtil.getContent(rsrc.getProject(), rsrc.getRev(),
return fileContentUtil.getContent(
rsrc.getProject().getProjectState(),
rsrc.getRev(),
rsrc.getPath());
}
}