Update org.apache.commons:commons-compress to 1.25.0
[egit/eclipse.git] / org.eclipse.egit.core / src / org / eclipse / egit / core / op / BranchOperation.java
blob0c77212e2dd21c33f810bfc3283d5ca599ce546e
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;
18 import java.io.File;
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;
25 import java.util.Map;
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;
65 /**
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;
79 /**
80 * Construct a {@link BranchOperation} object for a {@link Ref}.
82 * @param repository
83 * @param target
84 * a {@link Ref} name or {@link RevCommit} id
86 public BranchOperation(Repository repository, String target) {
87 this(repository, target, true);
90 /**
91 * Construct a {@link BranchOperation} object for a {@link Ref}.
93 * @param repository
94 * @param target
95 * a {@link Ref} name or {@link RevCommit} id
96 * @param delete
97 * true to delete missing projects on new branch, false to close
98 * them
100 public BranchOperation(Repository repository, String target, boolean delete) {
101 this(new Repository[] { repository }, target, delete);
106 * @param repositories
107 * @param target
108 * @param delete
110 public BranchOperation(Repository[] repositories, String target,
111 boolean delete) {
112 this.repositories = repositories;
113 this.target = target;
114 this.delete = delete;
117 @Override
118 public void execute(IProgressMonitor m) throws CoreException {
119 IWorkspaceRunnable action = new IWorkspaceRunnable() {
121 @Override
122 public void run(IProgressMonitor pm) throws CoreException {
123 try {
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
133 // filled
134 result = CheckoutResult.NOT_TRIED_RESULT;
136 else {
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));
149 } finally {
150 pm.done();
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)));
163 co.setName(target);
164 try {
165 co.call();
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());
172 if (logErrors) {
173 Activator.logError(msg, e);
174 } else {
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,
188 missing.length);
189 closeMonitor.setWorkRemaining(missing.length);
190 for (IProject project : missing) {
191 if (closeMonitor.isCanceled()) {
192 break;
194 closeMonitor.subTask(MessageFormat.format(
195 CoreText.BranchOperation_closingMissingProject,
196 project.getName()));
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,
209 monitor);
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);
237 @Override
238 public ISchedulingRule getSchedulingRule() {
239 return RuleUtil.getRuleForRepositories(Arrays.asList(repositories));
243 * @return the result of the operation
245 @NonNull
246 public Map<Repository, CheckoutResult> getResults() {
247 return results;
251 * @param repo
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;
259 return 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) {
267 break;
269 File fileToDelete = new File(repo.getWorkTree(), path);
270 if (fileToDelete.exists()) {
271 try {
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) {
279 // ignore here
286 * Compute the current projects that will be missing after the given branch
287 * is checked out
289 * @param repository
290 * @param branch
291 * @param monitor
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;
303 try {
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()) {
315 break;
317 IPath location = project.getLocation();
318 if (location == null) {
319 continue;
321 location = location
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()) {
338 break;
340 AbstractTreeIterator targetIter = walk.getTree(0,
341 AbstractTreeIterator.class);
342 if (targetIter != null) {
343 continue;
345 AbstractTreeIterator currentIter = walk.getTree(1,
346 AbstractTreeIterator.class);
347 AbstractTreeIterator workingIter = walk.getTree(2,
348 AbstractTreeIterator.class);
349 if (currentIter == null || workingIter == null) {
350 continue;
352 IProject project = locations.get(new File(root, walk
353 .getPathString()));
354 if (project != null) {
355 toBeClosed.add(project);
358 } catch (IOException e) {
359 return new IProject[0];
361 return toBeClosed.toArray(new IProject[0]);