git4idea: Date and time are now added in the stash message
[fedora-idea.git] / plugins / git4idea / src / git4idea / update / GitUpdateEnvironment.java
blob221e57f07130aa590d222f5ee1c43862f8a4cd01
1 /*
2 * Copyright 2000-2009 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.diagnostic.Logger;
19 import com.intellij.openapi.options.Configurable;
20 import com.intellij.openapi.progress.ProcessCanceledException;
21 import com.intellij.openapi.progress.ProgressIndicator;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.project.ex.ProjectManagerEx;
24 import com.intellij.openapi.ui.Messages;
25 import com.intellij.openapi.util.Key;
26 import com.intellij.openapi.util.Ref;
27 import com.intellij.openapi.vcs.AbstractVcsHelper;
28 import com.intellij.openapi.vcs.FilePath;
29 import com.intellij.openapi.vcs.VcsException;
30 import com.intellij.openapi.vcs.changes.Change;
31 import com.intellij.openapi.vcs.changes.ChangeListManagerEx;
32 import com.intellij.openapi.vcs.changes.LocalChangeList;
33 import com.intellij.openapi.vcs.update.SequentialUpdatesContext;
34 import com.intellij.openapi.vcs.update.UpdateEnvironment;
35 import com.intellij.openapi.vcs.update.UpdateSession;
36 import com.intellij.openapi.vcs.update.UpdatedFiles;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.util.ui.UIUtil;
39 import git4idea.GitBranch;
40 import git4idea.GitRevisionNumber;
41 import git4idea.GitUtil;
42 import git4idea.GitVcs;
43 import git4idea.changes.GitChangeUtils;
44 import git4idea.commands.GitHandler;
45 import git4idea.commands.GitHandlerUtil;
46 import git4idea.commands.GitLineHandler;
47 import git4idea.commands.GitLineHandlerAdapter;
48 import git4idea.config.GitVcsSettings;
49 import git4idea.i18n.GitBundle;
50 import git4idea.merge.MergeChangeCollector;
51 import git4idea.rebase.GitRebaseUtils;
52 import git4idea.ui.GitUIUtil;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.Nullable;
56 import java.text.DateFormat;
57 import java.util.*;
58 import java.util.concurrent.atomic.AtomicBoolean;
60 /**
61 * Git update environment implementation. The environment does
62 * {@code git pull -v} for each vcs root. Rebase variant is detected
63 * and processed as well.
65 public class GitUpdateEnvironment implements UpdateEnvironment {
66 private static final Logger LOG = Logger.getInstance("#git4idea.update.GitUpdateEnvironment");
67 /**
68 * The vcs instance
70 private GitVcs myVcs;
71 /**
72 * The context project
74 private final Project myProject;
75 /**
76 * The project settings
78 private final GitVcsSettings mySettings;
80 /**
81 * A constructor from settings
83 * @param project a project
85 public GitUpdateEnvironment(@NotNull Project project, @NotNull GitVcs vcs, GitVcsSettings settings) {
86 myVcs = vcs;
87 myProject = project;
88 mySettings = settings;
91 /**
92 * {@inheritDoc}
94 public void fillGroups(UpdatedFiles updatedFiles) {
95 //unused, there are no custom categories yet
98 /**
99 * {@inheritDoc}
101 @NotNull
102 public UpdateSession updateDirectories(@NotNull FilePath[] filePaths,
103 UpdatedFiles updatedFiles,
104 ProgressIndicator progressIndicator,
105 @NotNull Ref<SequentialUpdatesContext> sequentialUpdatesContextRef)
106 throws ProcessCanceledException {
107 List<VcsException> exceptions = new ArrayList<VcsException>();
108 ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx();
109 projectManager.blockReloadingProjectOnExternalChanges();
110 try {
111 HashSet<VirtualFile> rootsToStash = new HashSet<VirtualFile>();
112 List<LocalChangeList> listsCopy = null;
113 ChangeListManagerEx changeManager = (ChangeListManagerEx)ChangeListManagerEx.getInstance(myProject);
114 if (mySettings.UPDATE_STASH) {
115 listsCopy = changeManager.getChangeListsCopy();
116 for (LocalChangeList l : listsCopy) {
117 final Collection<Change> changeCollection = l.getChanges();
118 LOG.debug("Stashing " + changeCollection.size() + " changes from '" + l.getName() + "'");
119 for (Change c : changeCollection) {
120 if (c.getAfterRevision() != null) {
121 VirtualFile r = GitUtil.getGitRootOrNull(c.getAfterRevision().getFile());
122 if (r != null) {
123 rootsToStash.add(r);
126 else if (c.getBeforeRevision() != null) {
127 VirtualFile r = GitUtil.getGitRootOrNull(c.getBeforeRevision().getFile());
128 if (r != null) {
129 rootsToStash.add(r);
135 Set<VirtualFile> roots = GitUtil.gitRoots(Arrays.asList(filePaths));
136 Set<VirtualFile> rebasingRoots = new TreeSet<VirtualFile>(GitUtil.VIRTUAL_FILE_COMPARATOR);
137 for (final VirtualFile root : roots) {
138 if (GitRebaseUtils.isRebaseInTheProgress(root)) {
139 rebasingRoots.add(root);
142 if (!rebasingRoots.isEmpty()) {
143 final StringBuilder files = new StringBuilder();
144 for (VirtualFile r : rebasingRoots) {
145 files.append(GitBundle.message("update.root.rebasing.item", r.getPresentableUrl()));
146 //noinspection ThrowableInstanceNeverThrown
147 exceptions.add(new VcsException(GitBundle.message("update.root.rebasing", r.getPresentableUrl())));
149 UIUtil.invokeAndWaitIfNeeded(new Runnable() {
150 public void run() {
151 Messages.showErrorDialog(myProject, GitBundle.message("update.root.rebasing.message", files.toString()),
152 GitBundle.message("update.root.rebasing.title"));
155 return new GitUpdateSession(exceptions);
157 String stashMessage = "Uncommitted changes before update operation at " +
158 DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.US).format(new Date());
159 for (final VirtualFile root : roots) {
160 try {
161 // check if there is a remote for the branch
162 final GitBranch branch = GitBranch.current(myProject, root);
163 if (branch == null) {
164 continue;
166 final String value = branch.getTrackedRemoteName(myProject, root);
167 if (value == null || value.length() == 0) {
168 continue;
170 final Ref<Boolean> cancelled = new Ref<Boolean>(false);
171 final Ref<Throwable> ex = new Ref<Throwable>();
172 try {
173 boolean stashCreated = rootsToStash.contains(root) && GitStashUtils.saveStash(myProject, root, stashMessage);
174 try {
175 // remember the current position
176 GitRevisionNumber before = GitRevisionNumber.resolve(myProject, root, "HEAD");
177 // do pull
178 GitLineHandler h = new GitLineHandler(myProject, root, GitHandler.PULL);
179 // ignore merge failure for the pull
180 h.ignoreErrorCode(1);
181 switch (mySettings.UPDATE_TYPE) {
182 case REBASE:
183 h.addParameters("--rebase");
184 break;
185 case MERGE:
186 h.addParameters("--no-rebase");
187 break;
188 case BRANCH_DEFAULT:
189 // use default for the branch
190 break;
191 default:
192 assert false : "Unknown update type: " + mySettings.UPDATE_TYPE;
194 h.addParameters("--no-stat");
195 h.addParameters("-v");
196 try {
197 RebaseConflictDetector rebaseConflictDetector = new RebaseConflictDetector();
198 h.addLineListener(rebaseConflictDetector);
199 try {
200 GitHandlerUtil.doSynchronouslyWithExceptions(h, progressIndicator);
202 finally {
203 if (!rebaseConflictDetector.isRebaseConflict()) {
204 exceptions.addAll(h.errors());
207 while (rebaseConflictDetector.isRebaseConflict() && !cancelled.get()) {
208 mergeFiles(root, cancelled, ex);
209 //noinspection ThrowableResultOfMethodCallIgnored
210 if (ex.get() != null) {
211 //noinspection ThrowableResultOfMethodCallIgnored
212 throw GitUtil.rethrowVcsException(ex.get());
214 checkLocallyModified(root, cancelled, ex);
215 //noinspection ThrowableResultOfMethodCallIgnored
216 if (ex.get() != null) {
217 //noinspection ThrowableResultOfMethodCallIgnored
218 throw GitUtil.rethrowVcsException(ex.get());
220 if (cancelled.get()) {
221 break;
223 doRebase(progressIndicator, root, rebaseConflictDetector, "--continue");
224 final Ref<Integer> result = new Ref<Integer>();
225 noChangeLoop:
226 while (rebaseConflictDetector.isNoChange()) {
227 UIUtil.invokeAndWaitIfNeeded(new Runnable() {
228 public void run() {
229 int rc = Messages.showDialog(myProject, GitBundle.message("update.rebase.no.change", root.getPresentableUrl()),
230 GitBundle.getString("update.rebase.no.change.title"),
231 new String[]{GitBundle.getString("update.rebase.no.change.skip"),
232 GitBundle.getString("update.rebase.no.change.retry"),
233 GitBundle.getString("update.rebase.no.change.cancel")}, 0, Messages.getErrorIcon());
234 result.set(rc);
237 switch (result.get()) {
238 case 0:
239 doRebase(progressIndicator, root, rebaseConflictDetector, "--skip");
240 continue noChangeLoop;
241 case 1:
242 continue noChangeLoop;
243 case 2:
244 cancelled.set(true);
245 break noChangeLoop;
249 if (cancelled.get()) {
250 //noinspection ThrowableInstanceNeverThrown
251 exceptions.add(new VcsException("The update process was cancelled for " + root.getPresentableUrl()));
252 doRebase(progressIndicator, root, rebaseConflictDetector, "--abort");
255 finally {
256 if (!cancelled.get()) {
257 // find out what have changed
258 MergeChangeCollector collector = new MergeChangeCollector(myProject, root, before, updatedFiles);
259 collector.collect(exceptions);
263 finally {
264 if (stashCreated) {
265 try {
266 GitStashUtils.popLastStash(myProject, root);
267 for (LocalChangeList changeList : listsCopy) {
268 final Collection<Change> changes = changeList.getChanges();
269 if (! changes.isEmpty()) {
270 LOG.debug("After unstash: moving " + changes.size() + " changes to '" + changeList.getName() + "'");
271 changeManager.moveChangesTo(changeList, changes.toArray(new Change[changes.size()]));
275 catch (final VcsException ue) {
276 exceptions.add(ue);
277 UIUtil.invokeAndWaitIfNeeded(new Runnable() {
278 public void run() {
279 GitUIUtil.showOperationError(myProject, ue, "Auto-unstash");
286 finally {
287 mergeFiles(root, cancelled, ex);
288 //noinspection ThrowableResultOfMethodCallIgnored
289 if (ex.get() != null) {
290 //noinspection ThrowableResultOfMethodCallIgnored
291 exceptions.add(GitUtil.rethrowVcsException(ex.get()));
295 catch (VcsException ex) {
296 exceptions.add(ex);
300 finally {
301 projectManager.unblockReloadingProjectOnExternalChanges();
303 return new GitUpdateSession(exceptions);
307 * Merge files
309 * @param root the project root
310 * @param cancelled the cancelled indicator
311 * @param ex the exception holder
313 private void mergeFiles(final VirtualFile root, final Ref<Boolean> cancelled, final Ref<Throwable> ex) {
314 UIUtil.invokeAndWaitIfNeeded(new Runnable() {
315 public void run() {
316 try {
317 List<VirtualFile> affectedFiles = GitChangeUtils.unmergedFiles(myProject, root);
318 while (affectedFiles.size() != 0) {
319 AbstractVcsHelper.getInstance(myProject).showMergeDialog(affectedFiles, myVcs.getMergeProvider());
320 affectedFiles = GitChangeUtils.unmergedFiles(myProject, root);
321 if (affectedFiles.size() != 0) {
322 int result = Messages
323 .showYesNoDialog(myProject, GitBundle.message("update.rebase.unmerged", affectedFiles.size(), root.getPresentableUrl()),
324 GitBundle.getString("update.rebase.unmerged.title"), Messages.getErrorIcon());
325 if (result != 0) {
326 cancelled.set(true);
327 return;
332 catch (Throwable t) {
333 ex.set(t);
340 * Check and process locally modified files
342 * @param root the project root
343 * @param cancelled the cancelled indicator
344 * @param ex the exception holder
346 private void checkLocallyModified(final VirtualFile root, final Ref<Boolean> cancelled, final Ref<Throwable> ex) {
347 UIUtil.invokeAndWaitIfNeeded(new Runnable() {
348 public void run() {
349 try {
350 if (!GitUpdateLocallyModifiedDialog.showIfNeeded(myProject, root)) {
351 cancelled.set(true);
354 catch (Throwable t) {
355 ex.set(t);
362 * Do rebase operation as part of update operator
364 * @param progressIndicator the progress indicator for the update
365 * @param root the vcs root
366 * @param rebaseConflictDetector the detector of conflicts in rebase operation
367 * @param action the rebase action to execute
369 private void doRebase(ProgressIndicator progressIndicator,
370 VirtualFile root,
371 RebaseConflictDetector rebaseConflictDetector,
372 final String action) {
373 GitLineHandler rh = new GitLineHandler(myProject, root, GitHandler.REBASE);
374 // ignore failure for abort
375 rh.ignoreErrorCode(1);
376 rh.addParameters(action);
377 rebaseConflictDetector.reset();
378 rh.addLineListener(rebaseConflictDetector);
379 GitHandlerUtil.doSynchronouslyWithExceptions(rh, progressIndicator);
383 * {@inheritDoc}
385 public boolean validateOptions(Collection<FilePath> filePaths) {
386 for (FilePath p : filePaths) {
387 if (!GitUtil.isUnderGit(p)) {
388 return false;
391 return true;
395 * {@inheritDoc}
397 @Nullable
398 public Configurable createConfigurable(Collection<FilePath> files) {
399 return new GitUpdateConfigurable(mySettings);
403 * The detector of conflict conditions for rebase operation
405 static class RebaseConflictDetector extends GitLineHandlerAdapter {
407 * The line that indicates that there is a rebase conflict.
409 private final static String REBASE_CONFLICT_INDICATOR = "When you have resolved this problem run \"git rebase --continue\".";
411 * The line that indicates "no change" condition.
413 private static final String REBASE_NO_CHANGE_INDICATOR = "No changes - did you forget to use 'git add'?";
415 * if true, the rebase conflict happened
417 AtomicBoolean rebaseConflict = new AtomicBoolean(false);
419 * if true, the no changes were detected in the rebase operations
421 AtomicBoolean noChange = new AtomicBoolean(false);
424 * Reset detector before new operation
426 public void reset() {
427 rebaseConflict.set(false);
428 noChange.set(false);
432 * @return true if "no change" condition was detected during the operation
434 public boolean isNoChange() {
435 return noChange.get();
439 * @return true if conflict during rebase was detected
441 public boolean isRebaseConflict() {
442 return rebaseConflict.get();
446 * {@inheritDoc}
448 @Override
449 public void onLineAvailable(String line, Key outputType) {
450 if (line.startsWith(REBASE_CONFLICT_INDICATOR)) {
451 rebaseConflict.set(true);
453 if (line.startsWith(REBASE_NO_CHANGE_INDICATOR)) {
454 noChange.set(true);