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
;
58 import java
.util
.concurrent
.atomic
.AtomicBoolean
;
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");
74 private final Project myProject
;
76 * The project settings
78 private final GitVcsSettings mySettings
;
81 * A constructor from settings
83 * @param project a project
85 public GitUpdateEnvironment(@NotNull Project project
, @NotNull GitVcs vcs
, GitVcsSettings settings
) {
88 mySettings
= settings
;
94 public void fillGroups(UpdatedFiles updatedFiles
) {
95 //unused, there are no custom categories yet
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();
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());
126 else if (c
.getBeforeRevision() != null) {
127 VirtualFile r
= GitUtil
.getGitRootOrNull(c
.getBeforeRevision().getFile());
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() {
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
) {
161 // check if there is a remote for the branch
162 final GitBranch branch
= GitBranch
.current(myProject
, root
);
163 if (branch
== null) {
166 final String value
= branch
.getTrackedRemoteName(myProject
, root
);
167 if (value
== null || value
.length() == 0) {
170 final Ref
<Boolean
> cancelled
= new Ref
<Boolean
>(false);
171 final Ref
<Throwable
> ex
= new Ref
<Throwable
>();
173 boolean stashCreated
= rootsToStash
.contains(root
) && GitStashUtils
.saveStash(myProject
, root
, stashMessage
);
175 // remember the current position
176 GitRevisionNumber before
= GitRevisionNumber
.resolve(myProject
, root
, "HEAD");
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
) {
183 h
.addParameters("--rebase");
186 h
.addParameters("--no-rebase");
189 // use default for the branch
192 assert false : "Unknown update type: " + mySettings
.UPDATE_TYPE
;
194 h
.addParameters("--no-stat");
195 h
.addParameters("-v");
197 RebaseConflictDetector rebaseConflictDetector
= new RebaseConflictDetector();
198 h
.addLineListener(rebaseConflictDetector
);
200 GitHandlerUtil
.doSynchronouslyWithExceptions(h
, progressIndicator
);
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()) {
223 doRebase(progressIndicator
, root
, rebaseConflictDetector
, "--continue");
224 final Ref
<Integer
> result
= new Ref
<Integer
>();
226 while (rebaseConflictDetector
.isNoChange()) {
227 UIUtil
.invokeAndWaitIfNeeded(new Runnable() {
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());
237 switch (result
.get()) {
239 doRebase(progressIndicator
, root
, rebaseConflictDetector
, "--skip");
240 continue noChangeLoop
;
242 continue 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");
256 if (!cancelled
.get()) {
257 // find out what have changed
258 MergeChangeCollector collector
= new MergeChangeCollector(myProject
, root
, before
, updatedFiles
);
259 collector
.collect(exceptions
);
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
) {
277 UIUtil
.invokeAndWaitIfNeeded(new Runnable() {
279 GitUIUtil
.showOperationError(myProject
, ue
, "Auto-unstash");
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
) {
301 projectManager
.unblockReloadingProjectOnExternalChanges();
303 return new GitUpdateSession(exceptions
);
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() {
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());
332 catch (Throwable 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() {
350 if (!GitUpdateLocallyModifiedDialog
.showIfNeeded(myProject
, root
)) {
354 catch (Throwable 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
,
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
);
385 public boolean validateOptions(Collection
<FilePath
> filePaths
) {
386 for (FilePath p
: filePaths
) {
387 if (!GitUtil
.isUnderGit(p
)) {
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);
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();
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
)) {