1 /*******************************************************************************
2 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
3 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
4 * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org>
5 * Copyright (C) 2010, Jens Baumgart <jens.baumgart@sap.com>
6 * Copyright (C) 2010, 2011, Mathias Kinzler <mathias.kinzler@sap.com>
7 * Copyright (C) 2015, Stephan Hackstedt <stephan.hackstedt@googlemail.com>
9 * All rights reserved. This program and the accompanying materials
10 * are made available under the terms of the Eclipse Public License 2.0
11 * which accompanies this distribution, and is available at
12 * https://www.eclipse.org/legal/epl-2.0/
14 * SPDX-License-Identifier: EPL-2.0
15 *******************************************************************************/
16 package org
.eclipse
.egit
.core
.op
;
19 import java
.io
.IOException
;
20 import java
.text
.MessageFormat
;
21 import java
.util
.ArrayList
;
22 import java
.util
.HashMap
;
23 import java
.util
.List
;
26 import org
.eclipse
.core
.resources
.IProject
;
27 import org
.eclipse
.core
.resources
.IProjectDescription
;
28 import org
.eclipse
.core
.resources
.IWorkspace
;
29 import org
.eclipse
.core
.resources
.IWorkspaceRunnable
;
30 import org
.eclipse
.core
.resources
.ResourcesPlugin
;
31 import org
.eclipse
.core
.runtime
.CoreException
;
32 import org
.eclipse
.core
.runtime
.IPath
;
33 import org
.eclipse
.core
.runtime
.IProgressMonitor
;
34 import org
.eclipse
.core
.runtime
.SubMonitor
;
35 import org
.eclipse
.core
.runtime
.jobs
.ISchedulingRule
;
36 import org
.eclipse
.egit
.core
.Activator
;
37 import org
.eclipse
.egit
.core
.EclipseGitProgressTransformer
;
38 import org
.eclipse
.egit
.core
.internal
.CoreText
;
39 import org
.eclipse
.egit
.core
.internal
.job
.RuleUtil
;
40 import org
.eclipse
.egit
.core
.internal
.util
.ProjectUtil
;
41 import org
.eclipse
.jgit
.annotations
.NonNull
;
42 import org
.eclipse
.jgit
.api
.CheckoutCommand
;
43 import org
.eclipse
.jgit
.api
.CheckoutResult
;
44 import org
.eclipse
.jgit
.api
.CheckoutResult
.Status
;
45 import org
.eclipse
.jgit
.api
.Git
;
46 import org
.eclipse
.jgit
.api
.errors
.CheckoutConflictException
;
47 import org
.eclipse
.jgit
.api
.errors
.GitAPIException
;
48 import org
.eclipse
.jgit
.api
.errors
.JGitInternalException
;
49 import org
.eclipse
.jgit
.lib
.Constants
;
50 import org
.eclipse
.jgit
.lib
.ObjectId
;
51 import org
.eclipse
.jgit
.lib
.Ref
;
52 import org
.eclipse
.jgit
.lib
.Repository
;
53 import org
.eclipse
.jgit
.revwalk
.RevCommit
;
54 import org
.eclipse
.jgit
.treewalk
.AbstractTreeIterator
;
55 import org
.eclipse
.jgit
.treewalk
.FileTreeIterator
;
56 import org
.eclipse
.jgit
.treewalk
.TreeWalk
;
57 import org
.eclipse
.jgit
.treewalk
.filter
.AndTreeFilter
;
58 import org
.eclipse
.jgit
.treewalk
.filter
.PathSuffixFilter
;
59 import org
.eclipse
.jgit
.treewalk
.filter
.TreeFilter
;
60 import org
.eclipse
.jgit
.util
.FileUtils
;
61 import org
.eclipse
.osgi
.util
.NLS
;
64 * This class implements checkouts of a specific revision. A check is made that
65 * this can be done without data loss.
67 public class BranchOperation
extends BaseOperation
{
69 private final String target
;
71 private @NonNull CheckoutResult result
= CheckoutResult
.NOT_TRIED_RESULT
;
73 private boolean delete
;
76 * Construct a {@link BranchOperation} object for a {@link Ref}.
80 * a {@link Ref} name or {@link RevCommit} id
82 public BranchOperation(Repository repository
, String target
) {
83 this(repository
, target
, true);
87 * Construct a {@link BranchOperation} object for a {@link Ref}.
91 * a {@link Ref} name or {@link RevCommit} id
93 * true to delete missing projects on new branch, false to close
96 public BranchOperation(Repository repository
, String target
, boolean delete
) {
103 public void execute(IProgressMonitor m
) throws CoreException
{
104 IWorkspaceRunnable action
= new IWorkspaceRunnable() {
107 public void run(IProgressMonitor pm
) throws CoreException
{
108 SubMonitor progress
= SubMonitor
.convert(pm
, 4);
109 preExecute(progress
.newChild(1));
111 closeProjectsMissingAfterCheckout(progress
);
113 try (Git git
= new Git(repository
)) {
114 CheckoutCommand co
= git
.checkout().setProgressMonitor(
115 new EclipseGitProgressTransformer(
116 progress
.newChild(1)));
121 } catch (CheckoutConflictException e
) {
123 } catch (JGitInternalException e
) {
124 throw new CoreException(
125 Activator
.error(e
.getMessage(), e
));
126 } catch (GitAPIException e
) {
127 throw new CoreException(
128 Activator
.error(e
.getMessage(), e
));
130 result
= co
.getResult();
132 if (result
.getStatus() == Status
.NONDELETED
) {
133 retryDelete(result
.getUndeletedList());
135 refreshAffectedProjects(progress
);
137 postExecute(progress
.newChild(1));
141 private void closeProjectsMissingAfterCheckout(SubMonitor progress
)
142 throws CoreException
{
143 IProject
[] missing
= getMissingProjects(target
, ProjectUtil
144 .getValidOpenProjects(repository
));
146 progress
.setTaskName(NLS
.bind(
147 CoreText
.BranchOperation_performingBranch
, target
));
148 progress
.setWorkRemaining(missing
.length
> 0 ?
4 : 3);
150 if (missing
.length
> 0) {
151 SubMonitor closeMonitor
= progress
.newChild(1);
152 closeMonitor
.setWorkRemaining(missing
.length
);
153 for (IProject project
: missing
) {
154 closeMonitor
.subTask(MessageFormat
.format(
155 CoreText
.BranchOperation_closingMissingProject
,
157 project
.close(closeMonitor
.newChild(1));
162 private void refreshAffectedProjects(SubMonitor progress
)
163 throws CoreException
{
164 List
<String
> pathsToHandle
= new ArrayList
<>();
165 pathsToHandle
.addAll(result
.getModifiedList());
166 pathsToHandle
.addAll(result
.getRemovedList());
167 pathsToHandle
.addAll(result
.getConflictList());
168 IProject
[] refreshProjects
= ProjectUtil
169 .getProjectsContaining(repository
, pathsToHandle
);
170 ProjectUtil
.refreshValidProjects(refreshProjects
, delete
,
171 progress
.newChild(1));
174 // lock workspace to protect working tree changes
175 ResourcesPlugin
.getWorkspace().run(action
, getSchedulingRule(),
176 IWorkspace
.AVOID_UPDATE
, m
);
180 public ISchedulingRule
getSchedulingRule() {
181 return RuleUtil
.getRule(repository
);
185 * @return the result of the operation
188 public CheckoutResult
getResult() {
192 void retryDelete(List
<String
> pathList
) {
193 // try to delete, but for a short time only
194 long startTime
= System
.currentTimeMillis();
195 for (String path
: pathList
) {
196 if (System
.currentTimeMillis() - startTime
> 1000)
198 File fileToDelete
= new File(repository
.getWorkTree(), path
);
199 if (fileToDelete
.exists())
201 // Only files should be passed here, thus
202 // we ignore attempt to delete submodules when
203 // we switch to a branch without a submodule
204 if (!fileToDelete
.isFile())
205 FileUtils
.delete(fileToDelete
, FileUtils
.RETRY
);
206 } catch (IOException e
) {
213 * Compute the current projects that will be missing after the given branch
217 * @param currentProjects
218 * @return non-null but possibly empty array of missing projects
220 private IProject
[] getMissingProjects(String branch
,
221 IProject
[] currentProjects
) {
222 if (delete
|| currentProjects
.length
== 0)
223 return new IProject
[0];
225 ObjectId targetTreeId
;
226 ObjectId currentTreeId
;
228 targetTreeId
= repository
.resolve(branch
+ "^{tree}"); //$NON-NLS-1$
229 currentTreeId
= repository
.resolve(Constants
.HEAD
+ "^{tree}"); //$NON-NLS-1$
230 } catch (IOException e
) {
231 return new IProject
[0];
233 if (targetTreeId
== null || currentTreeId
== null)
234 return new IProject
[0];
236 Map
<File
, IProject
> locations
= new HashMap
<>();
237 for (IProject project
: currentProjects
) {
238 IPath location
= project
.getLocation();
239 if (location
== null)
242 .append(IProjectDescription
.DESCRIPTION_FILE_NAME
);
243 locations
.put(location
.toFile(), project
);
246 List
<IProject
> toBeClosed
= new ArrayList
<>();
247 File root
= repository
.getWorkTree();
248 try (TreeWalk walk
= new TreeWalk(repository
)) {
249 walk
.addTree(targetTreeId
);
250 walk
.addTree(currentTreeId
);
251 walk
.addTree(new FileTreeIterator(repository
));
252 walk
.setRecursive(true);
253 walk
.setFilter(AndTreeFilter
.create(PathSuffixFilter
254 .create(IProjectDescription
.DESCRIPTION_FILE_NAME
),
255 TreeFilter
.ANY_DIFF
));
256 while (walk
.next()) {
257 AbstractTreeIterator targetIter
= walk
.getTree(0,
258 AbstractTreeIterator
.class);
259 if (targetIter
!= null)
262 AbstractTreeIterator currentIter
= walk
.getTree(1,
263 AbstractTreeIterator
.class);
264 AbstractTreeIterator workingIter
= walk
.getTree(2,
265 AbstractTreeIterator
.class);
266 if (currentIter
== null || workingIter
== null)
269 IProject project
= locations
.get(new File(root
, walk
272 toBeClosed
.add(project
);
274 } catch (IOException e
) {
275 return new IProject
[0];
277 return toBeClosed
.toArray(new IProject
[0]);