git4idea: Implemeted forced merge/rebase and stash options for update operation
[fedora-idea.git] / plugins / git4idea / src / git4idea / update / GitUpdateEnvironment.java
blobb44968699d380b458f03ea153c557457bb113268
1 /*
2 * Copyright 2000-2008 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package git4idea.update;
18 import com.intellij.openapi.options.Configurable;
19 import com.intellij.openapi.progress.ProcessCanceledException;
20 import com.intellij.openapi.progress.ProgressIndicator;
21 import com.intellij.openapi.project.Project;
22 import com.intellij.openapi.ui.Messages;
23 import com.intellij.openapi.util.Key;
24 import com.intellij.openapi.util.Ref;
25 import com.intellij.openapi.vcs.AbstractVcsHelper;
26 import com.intellij.openapi.vcs.FilePath;
27 import com.intellij.openapi.vcs.VcsException;
28 import com.intellij.openapi.vcs.update.SequentialUpdatesContext;
29 import com.intellij.openapi.vcs.update.UpdateEnvironment;
30 import com.intellij.openapi.vcs.update.UpdateSession;
31 import com.intellij.openapi.vcs.update.UpdatedFiles;
32 import com.intellij.openapi.vfs.VirtualFile;
33 import com.intellij.util.ui.UIUtil;
34 import git4idea.GitBranch;
35 import git4idea.GitRevisionNumber;
36 import git4idea.GitUtil;
37 import git4idea.GitVcs;
38 import git4idea.changes.GitChangeUtils;
39 import git4idea.commands.*;
40 import git4idea.config.GitVcsSettings;
41 import git4idea.i18n.GitBundle;
42 import git4idea.merge.MergeChangeCollector;
43 import org.jetbrains.annotations.NotNull;
44 import org.jetbrains.annotations.Nullable;
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.Collection;
49 import java.util.List;
50 import java.util.concurrent.atomic.AtomicBoolean;
52 /**
53 * Git update environment implementation. The environment does just a simple
54 * {@code git pull -v} for each content root.
56 public class GitUpdateEnvironment implements UpdateEnvironment {
57 /**
58 * The vcs instance
60 private GitVcs myVcs;
61 /**
62 * The context project
64 private final Project myProject;
65 /**
66 * The project settings
68 private final GitVcsSettings mySettings;
70 /**
71 * A constructor from settings
73 * @param project a project
75 public GitUpdateEnvironment(@NotNull Project project, @NotNull GitVcs vcs, GitVcsSettings settings) {
76 myVcs = vcs;
77 myProject = project;
78 mySettings = settings;
81 /**
82 * {@inheritDoc}
84 public void fillGroups(UpdatedFiles updatedFiles) {
85 //unused, there are no custom categories yet
88 /**
89 * {@inheritDoc}
91 @NotNull
92 public UpdateSession updateDirectories(@NotNull FilePath[] filePaths,
93 UpdatedFiles updatedFiles,
94 ProgressIndicator progressIndicator,
95 @NotNull Ref<SequentialUpdatesContext> sequentialUpdatesContextRef)
96 throws ProcessCanceledException {
97 List<VcsException> exceptions = new ArrayList<VcsException>();
98 for (final VirtualFile root : GitUtil.gitRoots(Arrays.asList(filePaths))) {
99 try {
100 // check if there is a remote for the branch
101 final GitBranch branch = GitBranch.current(myProject, root);
102 if (branch == null) {
103 continue;
105 final String value = branch.getTrackedRemoteName(myProject, root);
106 if (value == null || value.length() == 0) {
107 continue;
109 boolean stashCreated =
110 mySettings.UPDATE_STASH && GitStashUtils.saveStash(myProject, root, "Uncommitted changes before update operation");
111 try {
112 // remember the current position
113 GitRevisionNumber before = GitRevisionNumber.resolve(myProject, root, "HEAD");
114 // do pull
115 GitLineHandler h = new GitLineHandler(myProject, root, GitHandler.PULL);
116 // ignore merge failure for the pull
117 h.ignoreErrorCode(1);
118 switch (mySettings.UPDATE_TYPE) {
119 case REBASE:
120 h.addParameters("--rebase");
121 break;
122 case MERGE:
123 h.addParameters("--no-rebase");
124 break;
125 case BRANCH_DEFAULT:
126 // use default for the branch
127 break;
128 default:
129 assert false : "Unknown update type: " + mySettings.UPDATE_TYPE;
131 h.addParameters("--no-stat");
132 h.addParameters("-v");
133 final Ref<Boolean> cancelled = new Ref<Boolean>(false);
134 try {
135 RebaseConflictDetector rebaseConflictDetector = new RebaseConflictDetector();
136 h.addLineListener(rebaseConflictDetector);
137 try {
138 GitHandlerUtil.doSynchronouslyWithExceptions(h, progressIndicator);
140 finally {
141 if (!rebaseConflictDetector.isRebaseConflict()) {
142 exceptions.addAll(h.errors());
145 do {
146 final Ref<Throwable> ex = new Ref<Throwable>();
147 UIUtil.invokeAndWaitIfNeeded(new Runnable() {
148 public void run() {
149 try {
150 List<VirtualFile> affectedFiles = GitChangeUtils.unmergedFiles(myProject, root);
151 while (affectedFiles.size() != 0) {
152 AbstractVcsHelper.getInstance(myProject).showMergeDialog(affectedFiles, myVcs.getMergeProvider());
153 affectedFiles = GitChangeUtils.unmergedFiles(myProject, root);
154 if (affectedFiles.size() != 0) {
155 int result = Messages.showYesNoDialog(myProject, GitBundle.message("update.rebase.unmerged", affectedFiles.size(),
156 root.getPresentableUrl()),
157 GitBundle.getString("update.rebase.unmerged.title"), Messages.getErrorIcon());
158 if (result != 0) {
159 cancelled.set(true);
160 return;
165 catch (Throwable t) {
166 ex.set(t);
170 //noinspection ThrowableResultOfMethodCallIgnored
171 if (ex.get() != null) {
172 //noinspection ThrowableResultOfMethodCallIgnored
173 throw GitUtil.rethrowVcsException(ex.get());
175 if (cancelled.get()) {
176 break;
178 doRebase(progressIndicator, root, rebaseConflictDetector, "--continue");
179 final Ref<Integer> result = new Ref<Integer>();
180 noChangeLoop:
181 while (rebaseConflictDetector.isNoChange()) {
182 UIUtil.invokeAndWaitIfNeeded(new Runnable() {
183 public void run() {
184 int rc = Messages.showDialog(myProject, GitBundle.message("update.rebase.no.change", root.getPresentableUrl()),
185 GitBundle.getString("update.rebase.no.change.title"),
186 new String[]{GitBundle.getString("update.rebase.no.change.skip"),
187 GitBundle.getString("update.rebase.no.change.retry"),
188 GitBundle.getString("update.rebase.no.change.cancel")}, 0, Messages.getErrorIcon());
189 result.set(rc);
192 switch (result.get()) {
193 case 0:
194 doRebase(progressIndicator, root, rebaseConflictDetector, "--skip");
195 continue noChangeLoop;
196 case 1:
197 continue noChangeLoop;
198 case 2:
199 cancelled.set(true);
200 break noChangeLoop;
204 while (rebaseConflictDetector.isRebaseConflict() && !cancelled.get());
205 if (cancelled.get()) {
206 //noinspection ThrowableInstanceNeverThrown
207 exceptions.add(new VcsException("The update process was cancelled for " + root.getPresentableUrl()));
208 doRebase(progressIndicator, root, rebaseConflictDetector, "--abort");
211 finally {
212 if (!cancelled.get()) {
213 // find out what have changed
214 MergeChangeCollector collector = new MergeChangeCollector(myProject, root, before, updatedFiles);
215 collector.collect(exceptions);
219 finally {
220 if (stashCreated) {
221 GitStashUtils.popLastStash(myProject, root);
225 catch (VcsException ex) {
226 exceptions.add(ex);
229 return new GitUpdateSession(exceptions);
233 * Do rebase operation as part of update operator
235 * @param progressIndicator the progress indicator for the update
236 * @param root the vcs root
237 * @param rebaseConflictDetector the detector of conflicts in rebase operation
238 * @param action the rebase action to execute
240 private void doRebase(ProgressIndicator progressIndicator,
241 VirtualFile root,
242 RebaseConflictDetector rebaseConflictDetector,
243 final String action) {
244 GitLineHandler rh = new GitLineHandler(myProject, root, GitHandler.REBASE);
245 // ignore failure for abort
246 rh.ignoreErrorCode(1);
247 rh.addParameters(action);
248 rebaseConflictDetector.reset();
249 rh.addLineListener(rebaseConflictDetector);
250 GitHandlerUtil.doSynchronouslyWithExceptions(rh, progressIndicator);
254 * {@inheritDoc}
256 public boolean validateOptions(Collection<FilePath> filePaths) {
257 for (FilePath p : filePaths) {
258 if (!GitUtil.isUnderGit(p)) {
259 return false;
262 return true;
266 * {@inheritDoc}
268 @Nullable
269 public Configurable createConfigurable(Collection<FilePath> files) {
270 return new GitUpdateConfigurable(mySettings);
274 * The detector of conflict conditions for rebase operation
276 static class RebaseConflictDetector extends GitLineHandlerAdapter {
278 * The line that indicates that there is a rebase conflict.
280 private final static String REBASE_CONFLICT_INDICATOR = "When you have resolved this problem run \"git rebase --continue\".";
282 * The line that indicates "no change" condition.
284 private static final String REBASE_NO_CHANGE_INDICATOR = "No changes - did you forget to use 'git add'?";
286 * if true, the rebase conflict happened
288 AtomicBoolean rebaseConflict = new AtomicBoolean(false);
290 * if true, the no changes were detected in the rebase operations
292 AtomicBoolean noChange = new AtomicBoolean(false);
295 * Reset detector before new operation
297 public void reset() {
298 rebaseConflict.set(false);
299 noChange.set(false);
303 * @return true if "no change" condition was detected during the operation
305 public boolean isNoChange() {
306 return noChange.get();
310 * @return true if conflict during rebase was detected
312 public boolean isRebaseConflict() {
313 return rebaseConflict.get();
317 * {@inheritDoc}
319 @Override
320 public void onLineAvailable(String line, Key outputType) {
321 if (line.startsWith(REBASE_CONFLICT_INDICATOR)) {
322 rebaseConflict.set(true);
324 if (line.startsWith(REBASE_NO_CHANGE_INDICATOR)) {
325 noChange.set(true);