replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / git4idea / src / git4idea / update / GitMergeUpdater.java
1 /*
2  * Copyright 2000-2011 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package git4idea.update;
17
18 import com.intellij.openapi.diagnostic.Logger;
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.util.Key;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.openapi.vcs.FilePath;
26 import com.intellij.openapi.vcs.VcsException;
27 import com.intellij.openapi.vcs.changes.Change;
28 import com.intellij.openapi.vcs.changes.ChangeListManager;
29 import com.intellij.openapi.vcs.changes.ContentRevision;
30 import com.intellij.openapi.vcs.changes.LocalChangeList;
31 import com.intellij.openapi.vcs.changes.ui.ChangeListViewerDialog;
32 import com.intellij.openapi.vcs.update.UpdatedFiles;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.util.containers.ContainerUtil;
35 import com.intellij.util.ui.UIUtil;
36 import com.intellij.vcsUtil.VcsUtil;
37 import git4idea.GitUtil;
38 import git4idea.branch.GitBranchPair;
39 import git4idea.commands.*;
40 import git4idea.merge.GitConflictResolver;
41 import git4idea.merge.GitMerger;
42 import git4idea.repo.GitRepository;
43 import git4idea.util.GitUIUtil;
44 import git4idea.util.GitUntrackedFilesHelper;
45 import org.jetbrains.annotations.NotNull;
46
47 import java.io.File;
48 import java.util.*;
49
50 import static com.intellij.util.ObjectUtils.assertNotNull;
51 import static java.util.Arrays.asList;
52
53 /**
54  * Handles {@code git pull} via merge.
55  */
56 public class GitMergeUpdater extends GitUpdater {
57   private static final Logger LOG = Logger.getInstance(GitMergeUpdater.class);
58
59   private final ChangeListManager myChangeListManager;
60
61   public GitMergeUpdater(Project project, @NotNull Git git,
62                          VirtualFile root,
63                          final GitBranchPair branchAndTracked,
64                          ProgressIndicator progressIndicator,
65                          UpdatedFiles updatedFiles) {
66     super(project, git, root, branchAndTracked, progressIndicator, updatedFiles);
67     myChangeListManager = ChangeListManager.getInstance(myProject);
68   }
69
70   @Override
71   @NotNull
72   protected GitUpdateResult doUpdate() {
73     LOG.info("doUpdate ");
74     final GitMerger merger = new GitMerger(myProject);
75
76     MergeLineListener mergeLineListener = new MergeLineListener();
77     GitUntrackedFilesOverwrittenByOperationDetector untrackedFilesDetector = new GitUntrackedFilesOverwrittenByOperationDetector(myRoot);
78
79     String originalText = myProgressIndicator.getText();
80     myProgressIndicator.setText("Merging" + GitUtil.mention(myRepository) + "...");
81     try {
82       GitCommandResult result = myGit.merge(myRepository, assertNotNull(myBranchPair.getDest()).getName(),
83                                             asList("--no-stat", "-v"), mergeLineListener, untrackedFilesDetector,
84                                             GitStandardProgressAnalyzer.createListener(myProgressIndicator));
85       myProgressIndicator.setText(originalText);
86       return result.success()
87              ? GitUpdateResult.SUCCESS
88              : handleMergeFailure(mergeLineListener, untrackedFilesDetector, merger, result.getErrorOutputAsJoinedString());
89     }
90     catch (ProcessCanceledException pce) {
91       cancel();
92       return GitUpdateResult.CANCEL;
93     }
94   }
95
96   @NotNull
97   private GitUpdateResult handleMergeFailure(MergeLineListener mergeLineListener,
98                                              GitMessageWithFilesDetector untrackedFilesWouldBeOverwrittenByMergeDetector,
99                                              final GitMerger merger,
100                                              String errorMessage) {
101     final MergeError error = mergeLineListener.getMergeError();
102     LOG.info("merge error: " + error);
103     if (error == MergeError.CONFLICT) {
104       LOG.info("Conflict detected");
105       final boolean allMerged =
106         new MyConflictResolver(myProject, myGit, merger, myRoot).merge();
107       return allMerged ? GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS : GitUpdateResult.INCOMPLETE;
108     }
109     else if (error == MergeError.LOCAL_CHANGES) {
110       LOG.info("Local changes would be overwritten by merge");
111       final List<FilePath> paths = getFilesOverwrittenByMerge(mergeLineListener.getOutput());
112       final Collection<Change> changes = getLocalChangesFilteredByFiles(paths);
113       UIUtil.invokeAndWaitIfNeeded((Runnable)() -> {
114         ChangeListViewerDialog dialog = new ChangeListViewerDialog(myProject, changes, false) {
115           @Override protected String getDescription() {
116             return "Your local changes to the following files would be overwritten by merge.<br/>" +
117                               "Please, commit your changes or stash them before you can merge.";
118           }
119         };
120         dialog.show();
121       });
122       return GitUpdateResult.ERROR;
123     }
124     else if (untrackedFilesWouldBeOverwrittenByMergeDetector.wasMessageDetected()) {
125       LOG.info("handleMergeFailure: untracked files would be overwritten by merge");
126       GitUntrackedFilesHelper.notifyUntrackedFilesOverwrittenBy(myProject, myRoot,
127                                                                 untrackedFilesWouldBeOverwrittenByMergeDetector.getRelativeFilePaths(),
128                                                                 "merge", null);
129       return GitUpdateResult.ERROR;
130     }
131     else {
132       LOG.info("Unknown error: " + errorMessage);
133       GitUIUtil.notifyImportantError(myProject, "Error merging", errorMessage);
134       return GitUpdateResult.ERROR;
135     }
136   }
137
138   @Override
139   public boolean isSaveNeeded() {
140     try {
141       if (GitUtil.hasLocalChanges(true, myProject, myRoot)) {
142         return true;
143       }
144     }
145     catch (VcsException e) {
146       LOG.info("isSaveNeeded failed to check staging area", e);
147       return true;
148     }
149
150     // git log --name-status master..origin/master
151     String currentBranch = myBranchPair.getBranch().getName();
152     String remoteBranch = myBranchPair.getDest().getName();
153     try {
154       GitRepository repository = GitUtil.getRepositoryManager(myProject).getRepositoryForRoot(myRoot);
155       if (repository == null) {
156         LOG.error("Repository is null for root " + myRoot);
157         return true; // fail safe
158       }
159       final Collection<String> remotelyChanged = GitUtil.getPathsDiffBetweenRefs(Git.getInstance(), repository,
160                                                                                  currentBranch, remoteBranch);
161       final List<File> locallyChanged = myChangeListManager.getAffectedPaths();
162       for (final File localPath : locallyChanged) {
163         if (ContainerUtil.exists(remotelyChanged, remotelyChangedPath -> FileUtil.pathsEqual(localPath.getPath(), remotelyChangedPath))) {
164           // found a file which was changed locally and remotely => need to save
165           return true;
166         }
167       }
168       return false;
169     } catch (VcsException e) {
170       LOG.info("failed to get remotely changed files for " + currentBranch + ".." + remoteBranch, e);
171       return true; // fail safe
172     }
173   }
174
175   private void cancel() {
176     try {
177       GitSimpleHandler h = new GitSimpleHandler(myProject, myRoot, GitCommand.RESET);
178       h.addParameters("--merge");
179       h.run();
180     } catch (VcsException e) {
181       LOG.info("cancel git reset --merge", e);
182       GitUIUtil.notifyImportantError(myProject, "Couldn't reset merge", e.getLocalizedMessage());
183     }
184   }
185
186   // parses the output of merge conflict returning files which would be overwritten by merge. These files will be stashed.
187   private List<FilePath> getFilesOverwrittenByMerge(@NotNull List<String> mergeOutput) {
188     final List<FilePath> paths = new ArrayList<>();
189     for  (String line : mergeOutput) {
190       if (StringUtil.isEmptyOrSpaces(line)) {
191         continue;
192       }
193       if (line.contains("Please, commit your changes or stash them before you can merge")) {
194         break;
195       }
196       line = line.trim();
197
198       final String path;
199       try {
200         path = myRoot.getPath() + "/" + GitUtil.unescapePath(line);
201         final File file = new File(path);
202         if (file.exists()) {
203           paths.add(VcsUtil.getFilePath(file, false));
204         }
205       } catch (VcsException e) { // just continue
206       }
207     }
208     return paths;
209   }
210
211   private Collection<Change> getLocalChangesFilteredByFiles(List<FilePath> paths) {
212     final Collection<Change> changes = new HashSet<>();
213     for(LocalChangeList list : myChangeListManager.getChangeLists()) {
214       for (Change change : list.getChanges()) {
215         final ContentRevision afterRevision = change.getAfterRevision();
216         final ContentRevision beforeRevision = change.getBeforeRevision();
217         if ((afterRevision != null && paths.contains(afterRevision.getFile())) || (beforeRevision != null && paths.contains(beforeRevision.getFile()))) {
218           changes.add(change);
219         }
220       }
221     }
222     return changes;
223   }
224
225   @Override
226   public String toString() {
227     return "Merge updater";
228   }
229
230   private enum MergeError {
231     CONFLICT,
232     LOCAL_CHANGES,
233     OTHER
234   }
235
236   private static class MergeLineListener extends GitLineHandlerAdapter {
237     private MergeError myMergeError;
238     private List<String> myOutput = new ArrayList<>();
239     private boolean myLocalChangesError = false;
240
241     @Override
242     public void onLineAvailable(String line, Key outputType) {
243       if (myLocalChangesError) {
244         myOutput.add(line);
245       } else if (line.contains("Automatic merge failed; fix conflicts and then commit the result")) {
246         myMergeError = MergeError.CONFLICT;
247       } else if (line.contains("Your local changes to the following files would be overwritten by merge")) {
248         myMergeError = MergeError.LOCAL_CHANGES;
249         myLocalChangesError = true;
250       }
251     }
252
253     public MergeError getMergeError() {
254       return myMergeError;
255     }
256
257     public List<String> getOutput() {
258       return myOutput;
259     }
260   }
261
262   private static class MyConflictResolver extends GitConflictResolver {
263     private final GitMerger myMerger;
264     private final VirtualFile myRoot;
265
266     public MyConflictResolver(Project project, @NotNull Git git, GitMerger merger, VirtualFile root) {
267       super(project, git, Collections.singleton(root), makeParams());
268       myMerger = merger;
269       myRoot = root;
270     }
271     
272     private static Params makeParams() {
273       Params params = new Params();
274       params.setErrorNotificationTitle("Can't complete update");
275       params.setMergeDescription("Merge conflicts detected. Resolve them before continuing update.");
276       return params;
277     }
278
279     @Override protected boolean proceedIfNothingToMerge() throws VcsException {
280       myMerger.mergeCommit(myRoot);
281       return true;
282     }
283
284     @Override protected boolean proceedAfterAllMerged() throws VcsException {
285       myMerger.mergeCommit(myRoot);
286       return true;
287     }
288   }
289 }