/* * GitReviewPresenter.java * * Copyright (C) 2009-12 by RStudio, Inc. * * Unless you have received this program directly from RStudio pursuant * to the terms of a commercial license agreement with RStudio, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ package org.rstudio.studio.client.workbench.views.vcs.git.dialog; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.event.dom.client.*; import com.google.gwt.event.logical.shared.HasAttachHandlers; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.json.client.JSONNumber; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.ui.HasText; import com.google.gwt.user.client.ui.HasValue; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.Widget; import com.google.gwt.view.client.RowCountChangeEvent; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.inject.Inject; import org.rstudio.core.client.Debug; import org.rstudio.core.client.Invalidation; import org.rstudio.core.client.Invalidation.Token; import org.rstudio.core.client.StringUtil; import org.rstudio.core.client.WidgetHandlerRegistration; import org.rstudio.core.client.command.CommandBinder; import org.rstudio.core.client.command.Handler; import org.rstudio.core.client.jsonrpc.RpcObjectList; import org.rstudio.core.client.widget.DoubleClickState; import org.rstudio.core.client.widget.Operation; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.common.GlobalDisplay; import org.rstudio.studio.client.common.SimpleRequestCallback; import org.rstudio.studio.client.common.SuperDevMode; import org.rstudio.studio.client.common.console.ConsoleProcess; import org.rstudio.studio.client.common.console.ProcessExitEvent; import org.rstudio.studio.client.common.vcs.DiffResult; import org.rstudio.studio.client.common.vcs.GitServerOperations; import org.rstudio.studio.client.common.vcs.GitServerOperations.PatchMode; import org.rstudio.studio.client.common.vcs.StatusAndPath; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.server.Void; import org.rstudio.studio.client.workbench.commands.Commands; import org.rstudio.studio.client.workbench.model.ClientState; import org.rstudio.studio.client.workbench.model.Session; import org.rstudio.studio.client.workbench.model.helper.IntStateValue; import org.rstudio.studio.client.workbench.views.files.events.FileChangeEvent; import org.rstudio.studio.client.workbench.views.files.events.FileChangeHandler; import org.rstudio.studio.client.workbench.views.vcs.common.ChangelistTable; import org.rstudio.studio.client.workbench.views.vcs.common.ConsoleProgressDialog; import org.rstudio.studio.client.workbench.views.vcs.common.VCSFileOpener; import org.rstudio.studio.client.workbench.views.vcs.common.diff.*; import org.rstudio.studio.client.workbench.views.vcs.common.events.*; import org.rstudio.studio.client.workbench.views.vcs.common.events.DiffChunkActionEvent.Action; import org.rstudio.studio.client.workbench.views.vcs.common.events.VcsRefreshEvent.Reason; import org.rstudio.studio.client.workbench.views.vcs.dialog.CommitInfo; import org.rstudio.studio.client.workbench.views.vcs.dialog.ReviewPresenter; import org.rstudio.studio.client.workbench.views.vcs.git.GitChangelistTable; import org.rstudio.studio.client.workbench.views.vcs.git.GitPresenterCore; import org.rstudio.studio.client.workbench.views.vcs.git.model.GitState; import java.util.ArrayList; public class GitReviewPresenter implements ReviewPresenter { public interface Binder extends CommandBinder<Commands, GitReviewPresenter> {} public interface Display extends IsWidget, HasAttachHandlers { ArrayList<String> getSelectedPaths(); ArrayList<String> getSelectedDiscardablePaths(); void setSelectedStatusAndPaths(ArrayList<StatusAndPath> selectedPaths); HasValue<Boolean> getStagedCheckBox(); HasValue<Boolean> getUnstagedCheckBox(); LineTablePresenter.Display getLineTableDisplay(); ChangelistTable getChangelistTable(); HasValue<Integer> getContextLines(); HasClickHandlers getSwitchViewButton(); HasClickHandlers getStageFilesButton(); HasClickHandlers getRevertFilesButton(); void setFilesCommandsEnabled(boolean enabled); HasClickHandlers getIgnoreButton(); HasClickHandlers getStageAllButton(); HasClickHandlers getDiscardAllButton(); HasClickHandlers getUnstageAllButton(); HasText getCommitMessage(); HasClickHandlers getCommitButton(); HasValue<Boolean> getCommitIsAmend(); void setData(ArrayList<ChunkOrLine> lines, PatchMode patchMode); HasClickHandlers getOverrideSizeWarningButton(); void showSizeWarning(long sizeInBytes); void hideSizeWarning(); void showContextMenu(int clientX, int clientY, Command openSelectedCommand); void onShow(); void setShowActions(boolean showActions); } private class ApplyPatchClickHandler implements ClickHandler, Command { public ApplyPatchClickHandler(PatchMode patchMode, boolean reverse) { patchMode_ = patchMode; reverse_ = reverse; } @Override public void onClick(ClickEvent event) { execute(); } @Override public void execute() { ArrayList<String> paths = view_.getSelectedPaths(); if (patchMode_ == PatchMode.Stage && !reverse_) server_.gitStage(paths, new SimpleRequestCallback<Void>("Stage")); else if (patchMode_ == PatchMode.Stage && reverse_) server_.gitUnstage(paths, new SimpleRequestCallback<Void>("Unstage")); else if (patchMode_ == PatchMode.Working && reverse_) server_.gitDiscard(paths, new SimpleRequestCallback<Void>("Discard")); else throw new RuntimeException("Unknown patchMode and reverse combo"); view_.getChangelistTable().moveSelectionDown(); } private final PatchMode patchMode_; private final boolean reverse_; } private class ApplyPatchHandler implements DiffChunkActionHandler, DiffLinesActionHandler { @Override public void onDiffChunkAction(DiffChunkActionEvent event) { ArrayList<DiffChunk> chunks = new ArrayList<DiffChunk>(); chunks.add(event.getDiffChunk()); doPatch(event.getAction(), event.getDiffChunk().getLines(), chunks); } @Override public void onDiffLinesAction(DiffLinesActionEvent event) { ArrayList<Line> lines = view_.getLineTableDisplay().getSelectedLines(); doPatch(event.getAction(), lines, activeChunks_); } private void doPatch(Action action, ArrayList<Line> lines, ArrayList<DiffChunk> chunks) { boolean reverse; PatchMode patchMode; switch (action) { case Stage: reverse = false; patchMode = GitServerOperations.PatchMode.Stage; break; case Unstage: reverse = true; patchMode = GitServerOperations.PatchMode.Stage; break; case Discard: reverse = true; patchMode = GitServerOperations.PatchMode.Working; break; default: throw new IllegalArgumentException("Unhandled diff chunk action"); } applyPatch(chunks, lines, reverse, patchMode); } } @Inject public GitReviewPresenter(GitPresenterCore gitPresenterCore, GitServerOperations server, Display view, Binder binder, Commands commands, final EventBus events, final GitState gitState, final Session session, final GlobalDisplay globalDisplay, VCSFileOpener vcsFileOpener) { gitPresenterCore_ = gitPresenterCore; server_ = server; view_ = view; globalDisplay_ = globalDisplay; gitState_ = gitState; vcsFileOpener_ = vcsFileOpener; binder.bind(commands, this); new WidgetHandlerRegistration(view.asWidget()) { @Override protected HandlerRegistration doRegister() { return gitState_.addVcsRefreshHandler(new VcsRefreshHandler() { @Override public void onVcsRefresh(VcsRefreshEvent event) { if (event.getReason() == Reason.VcsOperation) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { updateDiff(true); initialized_ = true; } }); } } }, false); } }; new WidgetHandlerRegistration(view.asWidget()) { @Override protected HandlerRegistration doRegister() { return events.addHandler(FileChangeEvent.TYPE, new FileChangeHandler() { @Override public void onFileChange(FileChangeEvent event) { ArrayList<StatusAndPath> paths = view_.getChangelistTable() .getSelectedItems(); if (paths.size() != 1) { clearDiff(); return; } StatusAndPath vcsStatus = StatusAndPath.fromInfo( event.getFileChange().getFile().getGitStatus()); if (paths.get(0).getRawPath().equals(vcsStatus.getRawPath())) { gitState.refresh(false); } } }); } }; view_.getChangelistTable().addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { overrideSizeWarning_ = false; view_.setFilesCommandsEnabled(view_.getSelectedPaths().size() > 0); if (initialized_) updateDiff(true); } }); view_.getChangelistTable().addRowCountChangeHandler(new RowCountChangeEvent.Handler() { @Override public void onRowCountChange(RowCountChangeEvent event) { // This is necessary because during initial load, the selection // model has its selection set before any items are loaded into // the table (so therefore view_.getSelectedPaths().size() is always // 0, and the files commands are not enabled until selection changes // again). By updating the files commands' enabled state on row // count change as well, we can make sure they get enabled. view_.setFilesCommandsEnabled(view_.getSelectedPaths().size() > 0); } }); view_.getChangelistTable().addKeyDownHandler(new KeyDownHandler() { @Override public void onKeyDown(KeyDownEvent event) { // Space toggles the staged/unstaged state of the current selection. // Enter does the same plus moves the selection down. if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER || event.getNativeKeyCode() == ' ') { getTable().toggleStaged( event.getNativeKeyCode() == KeyCodes.KEY_ENTER); event.preventDefault(); } } }); view_.getChangelistTable().addMouseDownHandler(new MouseDownHandler() { private DoubleClickState dblClick = new DoubleClickState(); @Override public void onMouseDown(MouseDownEvent event) { if (dblClick.checkForDoubleClick(event.getNativeEvent())) { event.preventDefault(); event.stopPropagation(); getTable().toggleStaged(false); } } }); view_.getChangelistTable().addContextMenuHandler(new ContextMenuHandler() { @Override public void onContextMenu(ContextMenuEvent event) { NativeEvent nativeEvent = event.getNativeEvent(); view_.showContextMenu(nativeEvent.getClientX(), nativeEvent.getClientY(), new Command() { @Override public void execute() { openSelectedFiles(); } }); } }); view_.getStageFilesButton().addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { ArrayList<String> paths = view_.getSelectedPaths(); if (paths.size() == 0) return; server_.gitStage(paths, new SimpleRequestCallback<Void>()); view_.getChangelistTable().focus(); } }); view_.getRevertFilesButton().addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { final ArrayList<String> paths = view_.getSelectedPaths(); if (paths.size() == 0) return; String noun = paths.size() == 1 ? "file" : "files"; globalDisplay_.showYesNoMessage( GlobalDisplay.MSG_WARNING, "Revert Changes", "Changes to the selected " + noun + " will be lost, including " + "staged changes.\n\nAre you sure you want to continue?", new Operation() { @Override public void execute() { view_.getChangelistTable().selectNextUnselectedItem(); server_.gitRevert( paths, new SimpleRequestCallback<Void>("Revert Changes")); view_.getChangelistTable().focus(); } }, false); } }); view_.getIgnoreButton().addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { gitPresenterCore_.onVcsIgnore( view_.getChangelistTable().getSelectedItems()); } }); view_.getCommitIsAmend().addValueChangeHandler(new ValueChangeHandler<Boolean>() { @Override public void onValueChange(ValueChangeEvent<Boolean> booleanValueChangeEvent) { server_.gitHistory("", null, 0, 1, null, new ServerRequestCallback<RpcObjectList<CommitInfo>>() { @Override public void onResponseReceived(RpcObjectList<CommitInfo> response) { if (response.length() == 1) { String description = response.get(0).getDescription(); if (view_.getCommitIsAmend().getValue()) { if (view_.getCommitMessage().getText().length() == 0) view_.getCommitMessage().setText(description); } else { if (view_.getCommitMessage().getText().equals(description)) view_.getCommitMessage().setText(""); } } } @Override public void onError(ServerError error) { Debug.logError(error); } }); } }); view_.getStageAllButton().addClickHandler( new ApplyPatchClickHandler(PatchMode.Stage, false)); view_.getDiscardAllButton().addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { String which = view_.getLineTableDisplay().getSelectedLines().size() == 0 ? "All unstaged" : "The selected"; globalDisplay.showYesNoMessage( GlobalDisplay.MSG_WARNING, "Discard All", which + " changes in this file will be " + "lost.\n\nAre you sure you want to continue?", new Operation() { @Override public void execute() { new ApplyPatchClickHandler(PatchMode.Working, true).execute(); } }, false); } }); view_.getUnstageAllButton().addClickHandler( new ApplyPatchClickHandler(PatchMode.Stage, true)); view_.getStagedCheckBox().addValueChangeHandler( new ValueChangeHandler<Boolean>() { @Override public void onValueChange(ValueChangeEvent<Boolean> event) { if (initialized_) updateDiff(false); } }); view_.getLineTableDisplay().addDiffChunkActionHandler(new ApplyPatchHandler()); view_.getLineTableDisplay().addDiffLineActionHandler(new ApplyPatchHandler()); new IntStateValue(MODULE_GIT, KEY_CONTEXT_LINES, ClientState.PERSISTENT, session.getSessionInfo().getClientState()) { @Override protected void onInit(Integer value) { if (value != null) view_.getContextLines().setValue(value); } @Override protected Integer getValue() { return view_.getContextLines().getValue(); } }; view_.getContextLines().addValueChangeHandler(new ValueChangeHandler<Integer>() { @Override public void onValueChange(ValueChangeEvent<Integer> event) { updateDiff(false); } }); view_.getCommitButton().addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { server_.gitCommit( view_.getCommitMessage().getText(), view_.getCommitIsAmend().getValue(), false, new SimpleRequestCallback<ConsoleProcess>() { @Override public void onResponseReceived(ConsoleProcess proc) { proc.addProcessExitHandler(new ProcessExitEvent.Handler() { @Override public void onProcessExit(ProcessExitEvent event) { if (event.getExitCode() == 0) { view_.getCommitMessage().setText(""); if (view_.getCommitIsAmend().getValue()) view_.getCommitIsAmend().setValue(false); } } }); new ConsoleProgressDialog(proc, server_).showModal(); } @Override public void onError(ServerError error) { if (error.getClientInfo() != null && error.getClientInfo().isString() != null) { globalDisplay_.showErrorMessage( "Commit", error.getClientInfo().isString().stringValue()); } else { super.onError(error); } } }); } }); view_.getOverrideSizeWarningButton().addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { overrideSizeWarning_ = true; updateDiff(false); } }); } private GitChangelistTable getTable() { return (GitChangelistTable) view_.getChangelistTable(); } private void applyPatch(ArrayList<DiffChunk> chunks, ArrayList<Line> lines, boolean reverse, PatchMode patchMode) { chunks = new ArrayList<DiffChunk>(chunks); if (reverse) { for (int i = 0; i < chunks.size(); i++) chunks.set(i, chunks.get(i).reverse()); lines = Line.reverseLines(lines); } String path = view_.getChangelistTable().getSelectedPaths().get(0); if (path.indexOf(" -> ") >= 0) path = path.substring(path.indexOf(" -> ") + " -> ".length()); UnifiedEmitter emitter = new UnifiedEmitter(path); for (DiffChunk chunk : chunks) emitter.addContext(chunk); emitter.addDiffs(lines); String patch = emitter.createPatch(true); softModeSwitch_ = true; server_.gitApplyPatch(patch, patchMode, StringUtil.notNull(currentSourceEncoding_), new SimpleRequestCallback<Void>()); } private void updateDiff(boolean allowModeSwitch) { view_.hideSizeWarning(); final ArrayList<StatusAndPath> paths = view_.getChangelistTable().getSelectedItems(); if (paths.size() != 1) { clearDiff(); return; } final StatusAndPath item = paths.get(0); if (allowModeSwitch) { if (!softModeSwitch_) { boolean staged = item.getStatus().charAt(0) != ' ' && item.getStatus().charAt(1) == ' '; HasValue<Boolean> checkbox = staged ? view_.getStagedCheckBox() : view_.getUnstagedCheckBox(); if (!checkbox.getValue()) { clearDiff(); checkbox.setValue(true, true); } } else { if (view_.getStagedCheckBox().getValue() && (item.getStatus().charAt(0) == ' ' || item.getStatus().charAt(0) == '?')) { clearDiff(); view_.getUnstagedCheckBox().setValue(true, true); } else if (view_.getUnstagedCheckBox().getValue() && item.getStatus().charAt(1) == ' ') { clearDiff(); view_.getStagedCheckBox().setValue(true, true); } } } softModeSwitch_ = false; if (!item.getPath().equals(currentFilename_)) { clearDiff(); currentFilename_ = item.getPath(); } diffInvalidation_.invalidate(); final Token token = diffInvalidation_.getInvalidationToken(); final PatchMode patchMode = view_.getStagedCheckBox().getValue() ? PatchMode.Stage : PatchMode.Working; server_.gitDiffFile( item.getPath(), patchMode, view_.getContextLines().getValue(), overrideSizeWarning_, new SimpleRequestCallback<DiffResult>("Diff Error") { @Override public void onResponseReceived(DiffResult diffResult) { if (token.isInvalid()) return; // Use lastResponse_ to prevent unnecessary flicker String response = diffResult.getDecodedValue(); if (response.equals(currentResponse_)) return; currentResponse_ = response; currentSourceEncoding_ = diffResult.getSourceEncoding(); UnifiedParser parser = new UnifiedParser(response); parser.nextFilePair(); ArrayList<ChunkOrLine> allLines = new ArrayList<ChunkOrLine>(); activeChunks_.clear(); for (DiffChunk chunk; null != (chunk = parser.nextChunk());) { activeChunks_.add(chunk); allLines.add(new ChunkOrLine(chunk)); for (Line line : chunk.getLines()) allLines.add(new ChunkOrLine(line)); } view_.setShowActions( !"??".equals(item.getStatus()) && !"UU".equals(item.getStatus())); view_.setData(allLines, patchMode); } @Override public void onError(ServerError error) { JSONNumber size = error.getClientInfo().isNumber(); if (size != null) view_.showSizeWarning((long) size.doubleValue()); else { if (error.getCode() != ServerError.TRANSMISSION) super.onError(error); } } }); } private void clearDiff() { softModeSwitch_ = false; currentResponse_ = null; currentFilename_ = null; view_.getLineTableDisplay().clear(); } private void openSelectedFiles() { vcsFileOpener_.openFiles(view_.getChangelistTable().getSelectedItems()); } @Override public Widget asWidget() { return view_.asWidget(); } @Override public HandlerRegistration addSwitchViewHandler( final SwitchViewEvent.Handler h) { return view_.getSwitchViewButton().addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { h.onSwitchView(new SwitchViewEvent()); } }); } @Override public void setSelectedPaths(ArrayList<StatusAndPath> selectedPaths) { view_.setSelectedStatusAndPaths(selectedPaths); } public void onShow() { // Ensure that we're fresh gitState_.refresh(); view_.onShow(); } @Handler public void onVcsPull() { gitPresenterCore_.onVcsPull(); } @Handler public void onVcsPush() { gitPresenterCore_.onVcsPush(); } @Handler public void onVcsIgnore() { gitPresenterCore_.onVcsIgnore( view_.getChangelistTable().getSelectedItems()); } @Handler public void onRefreshSuperDevMode() { SuperDevMode.reload(); } private final Invalidation diffInvalidation_ = new Invalidation(); private final GitServerOperations server_; private final GitPresenterCore gitPresenterCore_; private final Display view_; private final GlobalDisplay globalDisplay_; private ArrayList<DiffChunk> activeChunks_ = new ArrayList<DiffChunk>(); private String currentResponse_; private String currentSourceEncoding_; private String currentFilename_; // Hack to prevent us flipping to unstaged view when a line is unstaged // from staged view private boolean softModeSwitch_; private GitState gitState_; private final VCSFileOpener vcsFileOpener_; private boolean initialized_; private static final String MODULE_GIT = "vcs_git"; private static final String KEY_CONTEXT_LINES = "context_lines"; private boolean overrideSizeWarning_ = false; }