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
11 * Mathias Kinzler (SAP AG) - initial implementation
12 * Thomas Wolf <thomas.wolf@paranor.ch> - Refactor
13 *******************************************************************************/
14 package org
.eclipse
.egit
.ui
.internal
.branch
;
17 import java
.util
.Arrays
;
18 import java
.util
.Collection
;
19 import java
.util
.List
;
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
;
61 * The UI wrapper for {@link BranchOperation}
63 public class BranchOperationUI
{
65 private final Repository
[] repositories
;
67 private String target
;
69 private boolean isSingleRepositoryOperation
;
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.
77 * This behavior is disabled when checking out multiple repositories at
81 private final boolean showQuestionsBeforeCheckout
;
84 * Create an operation for checking out a branch on multiple repositories.
88 * a valid {@link Ref} name or commit id
89 * @return the {@link BranchOperationUI}
91 public static BranchOperationUI
checkout(Repository
[] repositories
,
93 return new BranchOperationUI(repositories
, target
, true);
97 * Create an operation for checking out a branch
101 * a valid {@link Ref} name or commit id
102 * @return the {@link BranchOperationUI}
104 public static BranchOperationUI
checkout(Repository repository
,
106 return checkout(repository
, target
, true);
110 * Create an operation for checking out a branch
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
);
126 * the full ref name which will be checked out
127 * @return true if checkout will need additional input from the user before
130 public static boolean checkoutWillShowQuestionDialog(String refName
) {
131 return shouldShowCheckoutRemoteTrackingDialog(refName
);
135 * @param repositories
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
154 private String
confirmTarget(IProgressMonitor monitor
) {
155 if (target
== 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(
168 Collection
<Repository
> repos
= Arrays
.asList(repositories
);
169 if (LaunchFinder
.shouldCancelBecauseOfRunningLaunches(repos
, monitor
)) {
173 askForTargetIfNecessary();
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
,
183 MessageDialog
.openError(getShell(), UIText
.BranchAction_cannotCheckout
,
187 private void doCheckout(BranchOperation bop
, boolean restore
,
188 IProgressMonitor monitor
) throws CoreException
{
189 SubMonitor progress
= SubMonitor
.convert(monitor
, restore ?
10 : 1);
191 bop
.execute(progress
.newChild(1));
193 final BranchProjectTracker tracker
= new BranchProjectTracker(
195 ProjectTrackerMemento snapshot
= tracker
.snapshot();
196 bop
.execute(progress
.newChild(7));
197 tracker
.save(snapshot
);
198 IWorkspaceRunnable action
= new IWorkspaceRunnable() {
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) {
219 target
= confirmTarget(new NullProgressMonitor());
220 if (target
== null) {
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
);
228 job
.addJobChangeListener(new JobChangeAdapter() {
230 public void done(IJobChangeEvent cevent
) {
231 show(job
.getCheckoutResult());
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
) {
253 this.restore
= restore
;
257 public IStatus
run(IProgressMonitor monitor
) {
258 bop
= new BranchOperation(repositories
, target
, !restore
);
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
;
279 .createErrorStatus(UIText
.BranchAction_branchFailed
, e
);
281 GitLightweightDecorator
.refresh();
284 return Status
.OK_STATUS
;
288 public boolean belongsTo(Object family
) {
289 return JobFamilies
.CHECKOUT
.equals(family
)
290 || super.belongsTo(family
);
294 public Map
<Repository
, CheckoutResult
> getCheckoutResult() {
295 return bop
.getResults();
300 * Runs the operation synchronously.
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) {
312 final boolean restore
= Activator
.getDefault().getPreferenceStore()
313 .getBoolean(UIPreferences
.CHECKOUT_PROJECT_RESTORE
);
314 BranchOperation bop
= new BranchOperation(repositories
, target
,
316 doCheckout(bop
, restore
, progress
.newChild(80));
317 show(bop
.getResults());
320 private void askForTargetIfNecessary() {
321 if (target
== null || !showQuestionsBeforeCheckout
322 || !shouldShowCheckoutRemoteTrackingDialog(target
)) {
325 target
= getTargetWithCheckoutRemoteTrackingDialog(repositories
[0]);
328 private static boolean shouldShowCheckoutRemoteTrackingDialog(
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
;
346 private String
getTargetWithCheckoutRemoteTrackingDialog(Repository repo
) {
347 final String
[] dialogResult
= new String
[1];
348 PlatformUI
.getWorkbench().getDisplay().syncExec(
349 () -> dialogResult
[0] = getTargetWithCheckoutRemoteTrackingDialogInUI(
352 return dialogResult
[0];
355 private String
getTargetWithCheckoutRemoteTrackingDialogInUI(
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();
367 // Check out as new local branch
368 CreateBranchWizard wizard
= new CreateBranchWizard(repo
,
370 WizardDialog createBranchDialog
= new WizardDialog(getShell(),
372 createBranchDialog
.open();
374 } else if (result
== 1) {
383 private Shell
getShell() {
384 return PlatformUI
.getWorkbench().getDisplay().getActiveShell();
390 private void show(final @NonNull Map
<Repository
, CheckoutResult
> results
) {
391 if (allBranchOperationsSucceeded(results
)) {
392 if (anyRepositoryIsInDetachedHeadState(results
)) {
393 showDetachedHeadWarning();
397 if (this.isSingleRepositoryOperation
) {
398 Repository repo
= repositories
[0];
399 CheckoutResult result
= results
.get(repo
);
400 handleSingleRepositoryCheckoutOperationResult(repo
,
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
);
422 * Result of previous attempt to check out branch.
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()) {
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
,
438 BranchOperationUI
.checkout(repository
, target
, false)
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()) {
456 PlatformUI
.getWorkbench().getDisplay().asyncExec(() -> {
457 Shell shell
= PlatformUI
.getWorkbench()
458 .getActiveWorkbenchWindow().getShell();
459 new NonDeletedFilesDialog(shell
, repository
,
460 result
.getUndeletedList()).open();
467 String repoName
= RepositoryUtil
.INSTANCE
468 .getRepositoryName(repository
);
469 String message
= NLS
.bind(
470 UIText
.BranchOperationUI_CheckoutError_DialogMessage
,
472 PlatformUI
.getWorkbench().getDisplay().asyncExec(() -> {
473 Shell shell
= PlatformUI
.getWorkbench()
474 .getActiveWorkbenchWindow().getShell();
475 MessageDialog
.openError(shell
,
476 UIText
.BranchOperationUI_CheckoutError_DialogTitle
,
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()
503 UIText
.BranchOperationUI_DetachedHeadTitle
, null,
504 UIText
.BranchOperationUI_DetachedHeadMessage
,
505 MessageDialog
.INFORMATION
,
506 new String
[] { IDialogConstants
.CLOSE_LABEL
}, 0,
507 toggleMessage
, false);
509 if (dialog
.getToggleState()) {
510 store
.setValue(UIPreferences
.SHOW_DETACHED_HEAD_WARNING
,