/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java

https://bitbucket.org/nbargnesi/idea · Java · 254 lines · 191 code · 23 blank · 40 comment · 34 complexity · 22cf83398bd602de716d2ea31346d4f5 MD5 · raw file

  1. /*
  2. * Copyright 2000-2012 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.branch;
  17. import com.intellij.notification.NotificationType;
  18. import com.intellij.openapi.progress.ProgressIndicator;
  19. import com.intellij.openapi.project.Project;
  20. import com.intellij.openapi.util.Pair;
  21. import com.intellij.openapi.vcs.changes.Change;
  22. import com.intellij.openapi.vfs.VirtualFile;
  23. import git4idea.GitVcs;
  24. import git4idea.commands.*;
  25. import git4idea.repo.GitRepository;
  26. import git4idea.util.GitPreservingProcess;
  27. import git4idea.util.GitUIUtil;
  28. import org.jetbrains.annotations.NotNull;
  29. import org.jetbrains.annotations.Nullable;
  30. import java.util.Collection;
  31. import java.util.List;
  32. import java.util.concurrent.atomic.AtomicBoolean;
  33. import static git4idea.commands.GitMessageWithFilesDetector.Event.LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT;
  34. import static git4idea.commands.GitMessageWithFilesDetector.Event.UNTRACKED_FILES_OVERWRITTEN_BY;
  35. import static git4idea.util.GitUIUtil.code;
  36. /**
  37. * Represents {@code git checkout} operation.
  38. * Fails to checkout if there are unmerged files.
  39. * Fails to checkout if there are untracked files that would be overwritten by checkout. Shows the list of files.
  40. * If there are local changes that would be overwritten by checkout, proposes to perform a "smart checkout" which means stashing local
  41. * changes, checking out, and then unstashing the changes back (possibly with showing the conflict resolving dialog).
  42. *
  43. * @author Kirill Likhodedov
  44. */
  45. class GitCheckoutOperation extends GitBranchOperation {
  46. public static final String ROLLBACK_PROPOSAL_FORMAT = "You may rollback (checkout back to %s) not to let branches diverge.";
  47. @NotNull private final String myStartPointReference;
  48. @Nullable private final String myNewBranch;
  49. @NotNull private final String myPreviousBranch;
  50. GitCheckoutOperation(@NotNull Project project, @NotNull Git git, @NotNull Collection<GitRepository> repositories,
  51. @NotNull String startPointReference, @Nullable String newBranch, @NotNull String previousBranch,
  52. @NotNull ProgressIndicator indicator) {
  53. super(project, git, repositories, previousBranch, indicator);
  54. myStartPointReference = startPointReference;
  55. myNewBranch = newBranch;
  56. myPreviousBranch = previousBranch;
  57. }
  58. @Override
  59. protected void execute() {
  60. saveAllDocuments();
  61. boolean fatalErrorHappened = false;
  62. while (hasMoreRepositories() && !fatalErrorHappened) {
  63. final GitRepository repository = next();
  64. VirtualFile root = repository.getRoot();
  65. GitMessageWithFilesDetector localChangesOverwrittenByCheckout = new GitMessageWithFilesDetector(LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT, root);
  66. GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT);
  67. GitMessageWithFilesDetector untrackedOverwrittenByCheckout = new GitMessageWithFilesDetector(UNTRACKED_FILES_OVERWRITTEN_BY, root);
  68. GitCommandResult result = myGit.checkout(repository, myStartPointReference, myNewBranch, false,
  69. localChangesOverwrittenByCheckout, unmergedFiles, untrackedOverwrittenByCheckout);
  70. if (result.success()) {
  71. refresh(repository);
  72. markSuccessful(repository);
  73. }
  74. else if (unmergedFiles.hasHappened()) {
  75. fatalUnmergedFilesError();
  76. fatalErrorHappened = true;
  77. }
  78. else if (localChangesOverwrittenByCheckout.wasMessageDetected()) {
  79. boolean smartCheckoutSucceeded = smartCheckoutOrNotify(repository, localChangesOverwrittenByCheckout);
  80. if (!smartCheckoutSucceeded) {
  81. fatalErrorHappened = true;
  82. }
  83. }
  84. else if (untrackedOverwrittenByCheckout.wasMessageDetected()) {
  85. fatalUntrackedFilesError(untrackedOverwrittenByCheckout.getFiles());
  86. fatalErrorHappened = true;
  87. }
  88. else {
  89. fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedString());
  90. fatalErrorHappened = true;
  91. }
  92. }
  93. if (!fatalErrorHappened) {
  94. notifySuccess();
  95. updateRecentBranch();
  96. }
  97. }
  98. private boolean smartCheckoutOrNotify(@NotNull GitRepository repository,
  99. @NotNull GitMessageWithFilesDetector localChangesOverwrittenByCheckout) {
  100. Pair<List<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
  101. getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myPreviousBranch, myStartPointReference);
  102. List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
  103. List<Change> affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond();
  104. int smartCheckoutDecision = GitSmartOperationDialog.showAndGetAnswer(myProject, affectedChanges, "checkout", true);
  105. if (smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) {
  106. boolean smartCheckedOutSuccessfully = smartCheckout(allConflictingRepositories, myStartPointReference, myNewBranch, getIndicator());
  107. if (smartCheckedOutSuccessfully) {
  108. for (GitRepository conflictingRepository : allConflictingRepositories) {
  109. markSuccessful(conflictingRepository);
  110. refresh(conflictingRepository);
  111. }
  112. return true;
  113. }
  114. else {
  115. // notification is handled in smartCheckout()
  116. return false;
  117. }
  118. }
  119. else if (smartCheckoutDecision == GitSmartOperationDialog.FORCE_EXIT_CODE) {
  120. return checkoutOrNotify(allConflictingRepositories, myStartPointReference, myNewBranch, true);
  121. }
  122. else {
  123. fatalLocalChangesError(myStartPointReference);
  124. return false;
  125. }
  126. }
  127. @NotNull
  128. @Override
  129. protected String getRollbackProposal() {
  130. return "However checkout has succeeded for the following " + repositories() + ":<br/>" +
  131. successfulRepositoriesJoined() +
  132. "<br/>" + String.format(ROLLBACK_PROPOSAL_FORMAT, myPreviousBranch);
  133. }
  134. @NotNull
  135. @Override
  136. protected String getOperationName() {
  137. return "checkout";
  138. }
  139. @Override
  140. protected void rollback() {
  141. GitCompoundResult checkoutResult = new GitCompoundResult(myProject);
  142. GitCompoundResult deleteResult = new GitCompoundResult(myProject);
  143. for (GitRepository repository : getSuccessfulRepositories()) {
  144. GitCommandResult result = myGit.checkout(repository, myPreviousBranch, null, true);
  145. checkoutResult.append(repository, result);
  146. if (result.success() && myNewBranch != null) {
  147. /*
  148. force delete is needed, because we create new branch from branch other that the current one
  149. e.g. being on master create newBranch from feature,
  150. then rollback => newBranch is not fully merged to master (although it is obviously fully merged to feature).
  151. */
  152. deleteResult.append(repository, myGit.branchDelete(repository, myNewBranch, true));
  153. }
  154. refresh(repository);
  155. }
  156. if (!checkoutResult.totalSuccess() || !deleteResult.totalSuccess()) {
  157. StringBuilder message = new StringBuilder();
  158. if (!checkoutResult.totalSuccess()) {
  159. message.append("Errors during checking out ").append(myPreviousBranch).append(": ");
  160. message.append(checkoutResult.getErrorOutputWithReposIndication());
  161. }
  162. if (!deleteResult.totalSuccess()) {
  163. message.append("Errors during deleting ").append(code(myNewBranch)).append(": ");
  164. message.append(deleteResult.getErrorOutputWithReposIndication());
  165. }
  166. GitUIUtil.notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, myProject, "Error during rollback",
  167. message.toString(), NotificationType.ERROR, null);
  168. }
  169. }
  170. @NotNull
  171. private String getCommonErrorTitle() {
  172. return "Couldn't checkout " + myStartPointReference;
  173. }
  174. @NotNull
  175. @Override
  176. public String getSuccessMessage() {
  177. if (myNewBranch == null) {
  178. return String.format("Checked out <b><code>%s</code></b>", myStartPointReference);
  179. }
  180. return String.format("Checked out new branch <b><code>%s</code></b> from <b><code>%s</code></b>", myNewBranch, myStartPointReference);
  181. }
  182. // stash - checkout - unstash
  183. private boolean smartCheckout(@NotNull final List<GitRepository> repositories, @NotNull final String reference, @Nullable final String newBranch, @NotNull ProgressIndicator indicator) {
  184. final AtomicBoolean result = new AtomicBoolean();
  185. GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myGit, repositories, "checkout", reference, indicator,
  186. new Runnable() {
  187. @Override
  188. public void run() {
  189. result.set(checkoutOrNotify(repositories, reference, newBranch, false));
  190. }
  191. });
  192. preservingProcess.execute();
  193. return result.get();
  194. }
  195. /**
  196. * Checks out or shows an error message.
  197. */
  198. private boolean checkoutOrNotify(@NotNull List<GitRepository> repositories,
  199. @NotNull String reference, @Nullable String newBranch, boolean force) {
  200. GitCompoundResult compoundResult = new GitCompoundResult(myProject);
  201. for (GitRepository repository : repositories) {
  202. compoundResult.append(repository, myGit.checkout(repository, reference, newBranch, force));
  203. }
  204. if (compoundResult.totalSuccess()) {
  205. return true;
  206. }
  207. notifyError("Couldn't checkout " + reference, compoundResult.getErrorOutputWithReposIndication());
  208. return false;
  209. }
  210. private static void refresh(GitRepository... repositories) {
  211. for (GitRepository repository : repositories) {
  212. // If repository is small, everything can happen so fast, that FileWatcher wouldn't report the change before refresh() handles it.
  213. // Performing a fair total refresh would be an overhead, so just waiting a bit to let file watcher report the change.
  214. // This is a hack, but other solutions would be either performance or programming overhead.
  215. // See http://youtrack.jetbrains.com/issue/IDEA-80573
  216. sleepABit();
  217. refreshRoot(repository);
  218. // repository state will be auto-updated with this VFS refresh => in general there is no need to call GitRepository#update()
  219. // but to avoid problems of the asynchronous refresh, let's force update the repository info.
  220. repository.update(GitRepository.TrackedTopic.CURRENT_BRANCH, GitRepository.TrackedTopic.CURRENT_REVISION);
  221. }
  222. }
  223. private static void sleepABit() {
  224. try {
  225. Thread.sleep(50);
  226. }
  227. catch (InterruptedException e) {
  228. throw new RuntimeException(e);
  229. }
  230. }
  231. }