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
.Arrays
;
23 import java
.util
.HashMap
;
24 import java
.util
.List
;
26 import java
.util
.Map
.Entry
;
27 import java
.util
.stream
.Stream
;
29 import org
.eclipse
.core
.resources
.IProject
;
30 import org
.eclipse
.core
.resources
.IProjectDescription
;
31 import org
.eclipse
.core
.resources
.IWorkspace
;
32 import org
.eclipse
.core
.resources
.IWorkspaceRunnable
;
33 import org
.eclipse
.core
.resources
.ResourcesPlugin
;
34 import org
.eclipse
.core
.runtime
.CoreException
;
35 import org
.eclipse
.core
.runtime
.IPath
;
36 import org
.eclipse
.core
.runtime
.IProgressMonitor
;
37 import org
.eclipse
.core
.runtime
.SubMonitor
;
38 import org
.eclipse
.core
.runtime
.jobs
.ISchedulingRule
;
39 import org
.eclipse
.egit
.core
.Activator
;
40 import org
.eclipse
.egit
.core
.EclipseGitProgressTransformer
;
41 import org
.eclipse
.egit
.core
.internal
.CoreText
;
42 import org
.eclipse
.egit
.core
.internal
.job
.RuleUtil
;
43 import org
.eclipse
.egit
.core
.internal
.util
.ProjectUtil
;
44 import org
.eclipse
.jgit
.annotations
.NonNull
;
45 import org
.eclipse
.jgit
.api
.CheckoutCommand
;
46 import org
.eclipse
.jgit
.api
.CheckoutResult
;
47 import org
.eclipse
.jgit
.api
.CheckoutResult
.Status
;
48 import org
.eclipse
.jgit
.api
.Git
;
49 import org
.eclipse
.jgit
.api
.errors
.CheckoutConflictException
;
50 import org
.eclipse
.jgit
.api
.errors
.GitAPIException
;
51 import org
.eclipse
.jgit
.api
.errors
.JGitInternalException
;
52 import org
.eclipse
.jgit
.lib
.Constants
;
53 import org
.eclipse
.jgit
.lib
.ObjectId
;
54 import org
.eclipse
.jgit
.lib
.Ref
;
55 import org
.eclipse
.jgit
.lib
.Repository
;
56 import org
.eclipse
.jgit
.revwalk
.RevCommit
;
57 import org
.eclipse
.jgit
.treewalk
.AbstractTreeIterator
;
58 import org
.eclipse
.jgit
.treewalk
.FileTreeIterator
;
59 import org
.eclipse
.jgit
.treewalk
.TreeWalk
;
60 import org
.eclipse
.jgit
.treewalk
.filter
.AndTreeFilter
;
61 import org
.eclipse
.jgit
.treewalk
.filter
.PathSuffixFilter
;
62 import org
.eclipse
.jgit
.treewalk
.filter
.TreeFilter
;
63 import org
.eclipse
.jgit
.util
.FileUtils
;
66 * This class implements checkouts of a specific revision. A check is made that
67 * this can be done without data loss.
69 public class BranchOperation
implements IEGitOperation
{
71 private final String target
;
73 private Repository
[] repositories
;
75 private @NonNull Map
<Repository
, CheckoutResult
> results
= new HashMap
<>();
77 private boolean delete
;
80 * Construct a {@link BranchOperation} object for a {@link Ref}.
84 * a {@link Ref} name or {@link RevCommit} id
86 public BranchOperation(Repository repository
, String target
) {
87 this(repository
, target
, true);
91 * Construct a {@link BranchOperation} object for a {@link Ref}.
95 * a {@link Ref} name or {@link RevCommit} id
97 * true to delete missing projects on new branch, false to close
100 public BranchOperation(Repository repository
, String target
, boolean delete
) {
101 this(new Repository
[] { repository
}, target
, delete
);
106 * @param repositories
110 public BranchOperation(Repository
[] repositories
, String target
,
112 this.repositories
= repositories
;
113 this.target
= target
;
114 this.delete
= delete
;
118 public void execute(IProgressMonitor m
) throws CoreException
{
119 IWorkspaceRunnable action
= new IWorkspaceRunnable() {
122 public void run(IProgressMonitor pm
) throws CoreException
{
124 pm
.setTaskName(MessageFormat
.format(
125 CoreText
.BranchOperation_performingBranch
, target
));
126 int numberOfRepositories
= repositories
.length
;
127 SubMonitor progress
= SubMonitor
.convert(pm
,
128 numberOfRepositories
* 2);
129 for (Repository repository
: repositories
) {
130 CheckoutResult result
;
131 if (pm
.isCanceled()) {
132 // don't break from the loop, the result map must be
134 result
= CheckoutResult
.NOT_TRIED_RESULT
;
137 result
= checkoutRepository(repository
,
138 progress
.newChild(1),
139 numberOfRepositories
> 1);
140 if (result
.getStatus() == Status
.NONDELETED
) {
141 retryDelete(repository
,
142 result
.getUndeletedList());
145 results
.put(repository
, result
);
147 refreshAffectedProjects(
148 progress
.newChild(numberOfRepositories
));
154 public CheckoutResult
checkoutRepository(Repository repo
,
155 IProgressMonitor monitor
, boolean logErrors
)
156 throws CoreException
{
157 SubMonitor progress
= SubMonitor
.convert(monitor
, 2);
158 closeProjectsMissingAfterCheckout(repo
, progress
.newChild(1));
159 try (Git git
= new Git(repo
)) {
160 CheckoutCommand co
= git
.checkout().setProgressMonitor(
161 new EclipseGitProgressTransformer(
162 progress
.newChild(1)));
166 } catch (CheckoutConflictException e
) {
167 // Covered by the status return below.
168 } catch (JGitInternalException
| GitAPIException e
) {
169 String msg
= MessageFormat
.format(
170 CoreText
.BranchOperation_checkoutError
,
171 target
, repo
.getDirectory());
173 Activator
.logError(msg
, e
);
175 throw new CoreException(Activator
.error(msg
, e
));
178 return co
.getResult();
182 private void closeProjectsMissingAfterCheckout(Repository repo
,
183 IProgressMonitor monitor
) throws CoreException
{
184 IProject
[] missing
= getMissingProjects(repo
, target
, monitor
);
186 if (missing
.length
> 0) {
187 SubMonitor closeMonitor
= SubMonitor
.convert(monitor
,
189 closeMonitor
.setWorkRemaining(missing
.length
);
190 for (IProject project
: missing
) {
191 if (closeMonitor
.isCanceled()) {
194 closeMonitor
.subTask(MessageFormat
.format(
195 CoreText
.BranchOperation_closingMissingProject
,
197 project
.close(closeMonitor
.newChild(1));
202 private void refreshAffectedProjects(IProgressMonitor monitor
)
203 throws CoreException
{
204 IProject
[] refreshProjects
= results
.entrySet().stream()
205 .map(this::getAffectedProjects
)
206 .flatMap(Stream
::of
).distinct()
207 .toArray(IProject
[]::new);
208 ProjectUtil
.refreshValidProjects(refreshProjects
, delete
,
212 private IProject
[] getAffectedProjects(
213 Entry
<Repository
, CheckoutResult
> entry
) {
214 CheckoutResult result
= entry
.getValue();
216 if (result
.getStatus() != Status
.OK
217 && result
.getStatus() != Status
.NONDELETED
) {
218 // the checkout did not succeed
219 return new IProject
[0];
222 Repository repo
= entry
.getKey();
223 List
<String
> pathsToHandle
= new ArrayList
<>();
224 pathsToHandle
.addAll(result
.getModifiedList());
225 pathsToHandle
.addAll(result
.getRemovedList());
226 pathsToHandle
.addAll(result
.getConflictList());
227 IProject
[] refreshProjects
= ProjectUtil
228 .getProjectsContaining(repo
, pathsToHandle
);
229 return refreshProjects
;
232 // lock workspace to protect working tree changes
233 ResourcesPlugin
.getWorkspace().run(action
, getSchedulingRule(),
234 IWorkspace
.AVOID_UPDATE
, m
);
238 public ISchedulingRule
getSchedulingRule() {
239 return RuleUtil
.getRuleForRepositories(Arrays
.asList(repositories
));
243 * @return the result of the operation
246 public Map
<Repository
, CheckoutResult
> getResults() {
252 * @return return the result specific to a repository
254 public CheckoutResult
getResult(Repository repo
) {
255 CheckoutResult result
= results
.get(repo
);
256 if (result
== null) {
257 return CheckoutResult
.NOT_TRIED_RESULT
;
262 void retryDelete(Repository repo
, List
<String
> pathList
) {
263 // try to delete, but for a short time only
264 long startTime
= System
.currentTimeMillis();
265 for (String path
: pathList
) {
266 if (System
.currentTimeMillis() - startTime
> 1000) {
269 File fileToDelete
= new File(repo
.getWorkTree(), path
);
270 if (fileToDelete
.exists()) {
272 // Only files should be passed here, thus
273 // we ignore attempt to delete submodules when
274 // we switch to a branch without a submodule
275 if (!fileToDelete
.isFile()) {
276 FileUtils
.delete(fileToDelete
, FileUtils
.RETRY
);
278 } catch (IOException e
) {
286 * Compute the current projects that will be missing after the given branch
292 * @return non-null but possibly empty array of missing projects
293 * @throws CoreException
295 private IProject
[] getMissingProjects(Repository repository
,
296 String branch
, IProgressMonitor monitor
) throws CoreException
{
297 IProject
[] openProjects
= ProjectUtil
.getValidOpenProjects(repository
);
298 if (delete
|| openProjects
.length
== 0) {
299 return new IProject
[0];
301 ObjectId targetTreeId
;
302 ObjectId currentTreeId
;
304 targetTreeId
= repository
.resolve(branch
+ "^{tree}"); //$NON-NLS-1$
305 currentTreeId
= repository
.resolve(Constants
.HEAD
+ "^{tree}"); //$NON-NLS-1$
306 } catch (IOException e
) {
307 return new IProject
[0];
309 if (targetTreeId
== null || currentTreeId
== null) {
310 return new IProject
[0];
312 Map
<File
, IProject
> locations
= new HashMap
<>();
313 for (IProject project
: openProjects
) {
314 if (monitor
.isCanceled()) {
317 IPath location
= project
.getLocation();
318 if (location
== null) {
322 .append(IProjectDescription
.DESCRIPTION_FILE_NAME
);
323 locations
.put(location
.toFile(), project
);
326 List
<IProject
> toBeClosed
= new ArrayList
<>();
327 File root
= repository
.getWorkTree();
328 try (TreeWalk walk
= new TreeWalk(repository
)) {
329 walk
.addTree(targetTreeId
);
330 walk
.addTree(currentTreeId
);
331 walk
.addTree(new FileTreeIterator(repository
));
332 walk
.setRecursive(true);
333 walk
.setFilter(AndTreeFilter
.create(PathSuffixFilter
334 .create(IProjectDescription
.DESCRIPTION_FILE_NAME
),
335 TreeFilter
.ANY_DIFF
));
336 while (walk
.next()) {
337 if (monitor
.isCanceled()) {
340 AbstractTreeIterator targetIter
= walk
.getTree(0,
341 AbstractTreeIterator
.class);
342 if (targetIter
!= null) {
345 AbstractTreeIterator currentIter
= walk
.getTree(1,
346 AbstractTreeIterator
.class);
347 AbstractTreeIterator workingIter
= walk
.getTree(2,
348 AbstractTreeIterator
.class);
349 if (currentIter
== null || workingIter
== null) {
352 IProject project
= locations
.get(new File(root
, walk
354 if (project
!= null) {
355 toBeClosed
.add(project
);
358 } catch (IOException e
) {
359 return new IProject
[0];
361 return toBeClosed
.toArray(new IProject
[0]);