Update org.apache.commons:commons-compress to 1.25.0
[egit/eclipse.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / branch / BranchOperationUI.java
blob92aeb6ff45cc21e2585e94fefd38adbe75d68a83
1 /*******************************************************************************
2 * Copyright (c) 2010, 2016 SAP AG and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License 2.0
5 * which accompanies this distribution, and is available at
6 * https://www.eclipse.org/legal/epl-2.0/
8 * SPDX-License-Identifier: EPL-2.0
10 * Contributors:
11 * Mathias Kinzler (SAP AG) - initial implementation
12 * Thomas Wolf <thomas.wolf@paranor.ch> - Refactor
13 *******************************************************************************/
14 package org.eclipse.egit.ui.internal.branch;
16 import java.io.File;
17 import java.util.Arrays;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.stream.Stream;
24 import org.eclipse.core.resources.IWorkspace;
25 import org.eclipse.core.resources.IWorkspaceRunnable;
26 import org.eclipse.core.resources.ResourcesPlugin;
27 import org.eclipse.core.runtime.CoreException;
28 import org.eclipse.core.runtime.IProgressMonitor;
29 import org.eclipse.core.runtime.IStatus;
30 import org.eclipse.core.runtime.NullProgressMonitor;
31 import org.eclipse.core.runtime.Status;
32 import org.eclipse.core.runtime.SubMonitor;
33 import org.eclipse.core.runtime.jobs.IJobChangeEvent;
34 import org.eclipse.core.runtime.jobs.Job;
35 import org.eclipse.core.runtime.jobs.JobChangeAdapter;
36 import org.eclipse.egit.core.RepositoryUtil;
37 import org.eclipse.egit.core.op.BranchOperation;
38 import org.eclipse.egit.ui.Activator;
39 import org.eclipse.egit.ui.JobFamilies;
40 import org.eclipse.egit.ui.UIPreferences;
41 import org.eclipse.egit.ui.internal.UIRepositoryUtils;
42 import org.eclipse.egit.ui.internal.UIText;
43 import org.eclipse.egit.ui.internal.decorators.GitLightweightDecorator;
44 import org.eclipse.egit.ui.internal.dialogs.NonDeletedFilesDialog;
45 import org.eclipse.egit.ui.internal.repository.CreateBranchWizard;
46 import org.eclipse.jface.dialogs.IDialogConstants;
47 import org.eclipse.jface.dialogs.MessageDialog;
48 import org.eclipse.jface.dialogs.MessageDialogWithToggle;
49 import org.eclipse.jface.preference.IPreferenceStore;
50 import org.eclipse.jface.wizard.WizardDialog;
51 import org.eclipse.jgit.annotations.NonNull;
52 import org.eclipse.jgit.api.CheckoutResult;
53 import org.eclipse.jgit.lib.Constants;
54 import org.eclipse.jgit.lib.Ref;
55 import org.eclipse.jgit.lib.Repository;
56 import org.eclipse.osgi.util.NLS;
57 import org.eclipse.swt.widgets.Shell;
58 import org.eclipse.ui.PlatformUI;
60 /**
61 * The UI wrapper for {@link BranchOperation}
63 public class BranchOperationUI {
65 private final Repository[] repositories;
67 private String target;
69 private boolean isSingleRepositoryOperation;
71 /**
72 * In the case of checkout conflicts, a dialog is shown to let the user
73 * stash, reset or commit. After that, checkout is tried again. The second
74 * time we do checkout, we don't want to ask any questions we already asked
75 * the first time, so this will be false then.
76 * <p>
77 * This behavior is disabled when checking out multiple repositories at
78 * once.
79 * </p>
81 private final boolean showQuestionsBeforeCheckout;
83 /**
84 * Create an operation for checking out a branch on multiple repositories.
86 * @param repositories
87 * @param target
88 * a valid {@link Ref} name or commit id
89 * @return the {@link BranchOperationUI}
91 public static BranchOperationUI checkout(Repository[] repositories,
92 String target) {
93 return new BranchOperationUI(repositories, target, true);
96 /**
97 * Create an operation for checking out a branch
99 * @param repository
100 * @param target
101 * a valid {@link Ref} name or commit id
102 * @return the {@link BranchOperationUI}
104 public static BranchOperationUI checkout(Repository repository,
105 String target) {
106 return checkout(repository, target, true);
110 * Create an operation for checking out a branch
112 * @param repository
113 * @param target
114 * a valid {@link Ref} name or commit id
115 * @param showQuestionsBeforeCheckout
116 * @return the {@link BranchOperationUI}
118 public static BranchOperationUI checkout(Repository repository,
119 String target, boolean showQuestionsBeforeCheckout) {
120 return new BranchOperationUI(new Repository[] { repository }, target,
121 showQuestionsBeforeCheckout);
125 * @param refName
126 * the full ref name which will be checked out
127 * @return true if checkout will need additional input from the user before
128 * continuing
130 public static boolean checkoutWillShowQuestionDialog(String refName) {
131 return shouldShowCheckoutRemoteTrackingDialog(refName);
135 * @param repositories
136 * @param target
137 * @param showQuestionsBeforeCheckout
139 private BranchOperationUI(Repository[] repositories, String target,
140 boolean showQuestionsBeforeCheckout) {
141 this.repositories = repositories;
142 this.target = target;
144 * We do not have support for CreateBranchWizards when performing
145 * checkout on multiple repositories at once, thus, the
146 * showQuestionsBeforeCheckout is forced to false in this case
148 this.isSingleRepositoryOperation = repositories.length == 1;
149 this.showQuestionsBeforeCheckout = isSingleRepositoryOperation
150 ? showQuestionsBeforeCheckout
151 : false;
154 private String confirmTarget(IProgressMonitor monitor) {
155 if (target == null) {
156 return null;
158 Optional<Repository> invalidRepo = Stream.of(repositories)
159 .filter(r -> !r.getRepositoryState().canCheckout()).findFirst();
161 if (invalidRepo.isPresent()) {
162 PlatformUI.getWorkbench().getDisplay()
163 .asyncExec(() -> showRepositoryInInvalidStateForCheckout(
164 invalidRepo.get()));
165 return null;
168 Collection<Repository> repos = Arrays.asList(repositories);
169 if (LaunchFinder.shouldCancelBecauseOfRunningLaunches(repos, monitor)) {
170 return null;
173 askForTargetIfNecessary();
174 return target;
177 private void showRepositoryInInvalidStateForCheckout(Repository repo) {
178 String repoName = RepositoryUtil.INSTANCE.getRepositoryName(repo);
179 String description = repo.getRepositoryState().getDescription();
180 String message = NLS.bind(UIText.BranchAction_repositoryState, repoName,
181 description);
183 MessageDialog.openError(getShell(), UIText.BranchAction_cannotCheckout,
184 message);
187 private void doCheckout(BranchOperation bop, boolean restore,
188 IProgressMonitor monitor) throws CoreException {
189 SubMonitor progress = SubMonitor.convert(monitor, restore ? 10 : 1);
190 if (!restore) {
191 bop.execute(progress.newChild(1));
192 } else {
193 final BranchProjectTracker tracker = new BranchProjectTracker(
194 repositories);
195 ProjectTrackerMemento snapshot = tracker.snapshot();
196 bop.execute(progress.newChild(7));
197 tracker.save(snapshot);
198 IWorkspaceRunnable action = new IWorkspaceRunnable() {
200 @Override
201 public void run(IProgressMonitor innerMonitor)
202 throws CoreException {
203 tracker.restore(innerMonitor);
206 ResourcesPlugin.getWorkspace().run(action,
207 ResourcesPlugin.getWorkspace().getRoot(),
208 IWorkspace.AVOID_UPDATE, progress.newChild(3));
213 * Starts the operation asynchronously
215 public void start() {
216 if (repositories == null || repositories.length == 0) {
217 return;
219 target = confirmTarget(new NullProgressMonitor());
220 if (target == null) {
221 return;
223 String jobname = getJobName(repositories, target);
224 boolean restore = Activator.getDefault().getPreferenceStore()
225 .getBoolean(UIPreferences.CHECKOUT_PROJECT_RESTORE);
226 final CheckoutJob job = new CheckoutJob(jobname, restore);
227 job.setUser(true);
228 job.addJobChangeListener(new JobChangeAdapter() {
229 @Override
230 public void done(IJobChangeEvent cevent) {
231 show(job.getCheckoutResult());
234 job.schedule();
237 private static String getJobName(Repository[] repos, String target) {
238 if (repos.length > 1) {
239 return NLS.bind(UIText.BranchAction_checkingOutMultiple, target);
241 String repoName = RepositoryUtil.INSTANCE.getRepositoryName(repos[0]);
242 return NLS.bind(UIText.BranchAction_checkingOut, repoName, target);
245 private class CheckoutJob extends Job {
247 private BranchOperation bop;
249 private final boolean restore;
251 public CheckoutJob(String jobName, boolean restore) {
252 super(jobName);
253 this.restore = restore;
256 @Override
257 public IStatus run(IProgressMonitor monitor) {
258 bop = new BranchOperation(repositories, target, !restore);
259 try {
260 doCheckout(bop, restore, monitor);
261 } catch (CoreException e) {
263 * For a checkout operation with multiple repositories we can
264 * handle any error status by displaying all of them in a table.
265 * For a single repository, though, we will stick to using a
266 * simple message in case of an unexpected exception.
268 if (!isSingleRepositoryOperation) {
269 return Status.OK_STATUS;
271 CheckoutResult result = bop.getResult(repositories[0]);
273 if (result.getStatus() == CheckoutResult.Status.CONFLICTS ||
274 result.getStatus() == CheckoutResult.Status.NONDELETED) {
275 return Status.OK_STATUS;
278 return Activator
279 .createErrorStatus(UIText.BranchAction_branchFailed, e);
280 } finally {
281 GitLightweightDecorator.refresh();
282 monitor.done();
284 return Status.OK_STATUS;
287 @Override
288 public boolean belongsTo(Object family) {
289 return JobFamilies.CHECKOUT.equals(family)
290 || super.belongsTo(family);
293 @NonNull
294 public Map<Repository, CheckoutResult> getCheckoutResult() {
295 return bop.getResults();
300 * Runs the operation synchronously.
302 * @param monitor
303 * @throws CoreException
306 public void run(IProgressMonitor monitor) throws CoreException {
307 SubMonitor progress = SubMonitor.convert(monitor, 100);
308 target = confirmTarget(progress.newChild(20));
309 if (target == null) {
310 return;
312 final boolean restore = Activator.getDefault().getPreferenceStore()
313 .getBoolean(UIPreferences.CHECKOUT_PROJECT_RESTORE);
314 BranchOperation bop = new BranchOperation(repositories, target,
315 !restore);
316 doCheckout(bop, restore, progress.newChild(80));
317 show(bop.getResults());
320 private void askForTargetIfNecessary() {
321 if (target == null || !showQuestionsBeforeCheckout
322 || !shouldShowCheckoutRemoteTrackingDialog(target)) {
323 return;
325 target = getTargetWithCheckoutRemoteTrackingDialog(repositories[0]);
328 private static boolean shouldShowCheckoutRemoteTrackingDialog(
329 String refName) {
330 boolean isRemoteTrackingBranch = refName != null
331 && refName.startsWith(Constants.R_REMOTES);
332 if (isRemoteTrackingBranch) {
333 boolean showDetachedHeadWarning = Activator.getDefault()
334 .getPreferenceStore()
335 .getBoolean(UIPreferences.SHOW_DETACHED_HEAD_WARNING);
336 // If the user has not yet chosen to ignore the warning about
337 // getting into a "detached HEAD" state, then we show them a dialog
338 // whether a remote-tracking branch should be checked out with a
339 // detached HEAD or checking it out as a new local branch.
340 return showDetachedHeadWarning;
341 } else {
342 return false;
346 private String getTargetWithCheckoutRemoteTrackingDialog(Repository repo) {
347 final String[] dialogResult = new String[1];
348 PlatformUI.getWorkbench().getDisplay().syncExec(
349 () -> dialogResult[0] = getTargetWithCheckoutRemoteTrackingDialogInUI(
350 repo));
352 return dialogResult[0];
355 private String getTargetWithCheckoutRemoteTrackingDialogInUI(
356 Repository repo) {
357 String[] buttons = new String[] {
358 UIText.BranchOperationUI_CheckoutRemoteTrackingAsLocal,
359 UIText.BranchOperationUI_CheckoutRemoteTrackingCommit,
360 IDialogConstants.CANCEL_LABEL };
361 MessageDialog questionDialog = new MessageDialog(getShell(),
362 UIText.BranchOperationUI_CheckoutRemoteTrackingTitle, null,
363 UIText.BranchOperationUI_CheckoutRemoteTrackingQuestion,
364 MessageDialog.QUESTION, buttons, 0);
365 int result = questionDialog.open();
366 if (result == 0) {
367 // Check out as new local branch
368 CreateBranchWizard wizard = new CreateBranchWizard(repo,
369 target);
370 WizardDialog createBranchDialog = new WizardDialog(getShell(),
371 wizard);
372 createBranchDialog.open();
373 return null;
374 } else if (result == 1) {
375 // Check out commit
376 return target;
377 } else {
378 // Cancel
379 return null;
383 private Shell getShell() {
384 return PlatformUI.getWorkbench().getDisplay().getActiveShell();
388 * @param results
390 private void show(final @NonNull Map<Repository, CheckoutResult> results) {
391 if (allBranchOperationsSucceeded(results)) {
392 if (anyRepositoryIsInDetachedHeadState(results)) {
393 showDetachedHeadWarning();
395 return;
397 if (this.isSingleRepositoryOperation) {
398 Repository repo = repositories[0];
399 CheckoutResult result = results.get(repo);
400 handleSingleRepositoryCheckoutOperationResult(repo,
401 result, target);
402 } else {
403 handleMultipleRepositoryCheckoutError(results);
407 private boolean allBranchOperationsSucceeded(
408 @NonNull Map<Repository, CheckoutResult> results) {
409 return results.values().stream()
410 .allMatch(r -> r.getStatus() == CheckoutResult.Status.OK);
413 private boolean anyRepositoryIsInDetachedHeadState(
414 final @NonNull Map<Repository, CheckoutResult> results) {
415 return results.keySet().stream()
416 .anyMatch(RepositoryUtil::isDetachedHead);
420 * @param repository
421 * @param result
422 * Result of previous attempt to check out branch.
423 * @param target
424 * Name of the branch to be checked out.
426 public static void handleSingleRepositoryCheckoutOperationResult(
427 Repository repository, CheckoutResult result, String target) {
429 switch (result.getStatus()) {
430 case CONFLICTS:
431 PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
432 Shell shell = PlatformUI.getWorkbench()
433 .getActiveWorkbenchWindow().getShell();
434 if (UIRepositoryUtils.showCleanupDialog(repository,
435 result.getConflictList(),
436 UIText.BranchResultDialog_CheckoutConflictsTitle,
437 shell)) {
438 BranchOperationUI.checkout(repository, target, false)
439 .start();
442 break;
443 case NONDELETED:
444 // double-check if the files are still there
445 boolean show = false;
446 List<String> pathList = result.getUndeletedList();
447 for (String path : pathList)
448 if (new File(repository.getWorkTree(), path).exists()) {
449 show = true;
450 break;
453 if (!show) {
454 return;
456 PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
457 Shell shell = PlatformUI.getWorkbench()
458 .getActiveWorkbenchWindow().getShell();
459 new NonDeletedFilesDialog(shell, repository,
460 result.getUndeletedList()).open();
462 break;
463 case OK:
464 return;
465 case NOT_TRIED:
466 case ERROR:
467 String repoName = RepositoryUtil.INSTANCE
468 .getRepositoryName(repository);
469 String message = NLS.bind(
470 UIText.BranchOperationUI_CheckoutError_DialogMessage,
471 repoName, target);
472 PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
473 Shell shell = PlatformUI.getWorkbench()
474 .getActiveWorkbenchWindow().getShell();
475 MessageDialog.openError(shell,
476 UIText.BranchOperationUI_CheckoutError_DialogTitle,
477 message);
482 private void handleMultipleRepositoryCheckoutError(
483 Map<Repository, CheckoutResult> results) {
484 PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
485 Shell shell = PlatformUI.getWorkbench()
486 .getActiveWorkbenchWindow().getShell();
487 new MultiBranchOperationResultDialog(shell, results).open();
492 private void showDetachedHeadWarning() {
493 PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
494 IPreferenceStore store = Activator.getDefault()
495 .getPreferenceStore();
497 if (store.getBoolean(UIPreferences.SHOW_DETACHED_HEAD_WARNING)) {
498 String toggleMessage = UIText.BranchResultDialog_DetachedHeadWarningDontShowAgain;
500 MessageDialogWithToggle dialog = new MessageDialogWithToggle(
501 PlatformUI.getWorkbench().getActiveWorkbenchWindow()
502 .getShell(),
503 UIText.BranchOperationUI_DetachedHeadTitle, null,
504 UIText.BranchOperationUI_DetachedHeadMessage,
505 MessageDialog.INFORMATION,
506 new String[] { IDialogConstants.CLOSE_LABEL }, 0,
507 toggleMessage, false);
508 dialog.open();
509 if (dialog.getToggleState()) {
510 store.setValue(UIPreferences.SHOW_DETACHED_HEAD_WARNING,
511 false);