Avoid refresh on up-to-date pull operation
[egit/eclipse.git] / org.eclipse.egit.core / src / org / eclipse / egit / core / op / IgnoreOperation.java
blob8af042bcbdd64af0aaad968a3701d16bb9e624b0
1 /*******************************************************************************
2 * Copyright (C) 2009, Alex Blewitt <alex.blewitt@gmail.com>
3 * Copyright (C) 2010, Jens Baumgart <jens.baumgart@sap.com>
4 * Copyright (C) 2012, 2013 Robin Stocker <robin@nibor.org>
5 * Copyright (C) 2015, Stephan Hackstedt <stephan.hackstedt@googlemail.com>
6 * Copyright (C) 2016, Thomas Wolf <thomas.wolf@paranor.ch>
8 * All rights reserved. This program and the accompanying materials
9 * are made available under the terms of the Eclipse Public License 2.0
10 * which accompanies this distribution, and is available at
11 * https://www.eclipse.org/legal/epl-2.0/
13 * SPDX-License-Identifier: EPL-2.0
14 *******************************************************************************/
15 package org.eclipse.egit.core.op;
17 import java.io.ByteArrayInputStream;
18 import java.io.File;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.RandomAccessFile;
22 import java.io.UnsupportedEncodingException;
23 import java.nio.ByteBuffer;
24 import java.nio.channels.FileChannel;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.LinkedHashSet;
29 import java.util.Map;
31 import org.eclipse.core.resources.IContainer;
32 import org.eclipse.core.resources.IFile;
33 import org.eclipse.core.resources.IResource;
34 import org.eclipse.core.resources.IWorkspaceRoot;
35 import org.eclipse.core.runtime.CoreException;
36 import org.eclipse.core.runtime.IPath;
37 import org.eclipse.core.runtime.IProgressMonitor;
38 import org.eclipse.core.runtime.IStatus;
39 import org.eclipse.core.runtime.Path;
40 import org.eclipse.core.runtime.SubMonitor;
41 import org.eclipse.core.runtime.jobs.ISchedulingRule;
42 import org.eclipse.egit.core.Activator;
43 import org.eclipse.egit.core.IteratorService;
44 import org.eclipse.egit.core.internal.CoreText;
45 import org.eclipse.egit.core.internal.job.RuleUtil;
46 import org.eclipse.egit.core.internal.util.ResourceUtil;
47 import org.eclipse.jgit.lib.Constants;
48 import org.eclipse.jgit.lib.FileMode;
49 import org.eclipse.jgit.lib.Repository;
50 import org.eclipse.jgit.treewalk.TreeWalk;
51 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
52 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
53 import org.eclipse.osgi.util.NLS;
55 /**
56 * IgnoreOperation adds resources to a .gitignore file
59 public class IgnoreOperation implements IEGitOperation {
61 private final Collection<IPath> paths;
63 private boolean gitignoreOutsideWSChanged;
65 private ISchedulingRule schedulingRule;
67 /**
68 * construct an IgnoreOperation
70 * @param paths
71 * @since 2.2
73 public IgnoreOperation(Collection<IPath> paths) {
74 this.paths = paths;
75 gitignoreOutsideWSChanged = false;
76 schedulingRule = calcSchedulingRule();
79 @Override
80 public void execute(IProgressMonitor monitor) throws CoreException {
81 SubMonitor progress = SubMonitor.convert(monitor,
82 CoreText.IgnoreOperation_taskName, 3);
83 try {
84 Map<IPath, Collection<String>> perFolder = getFolderMap(
85 progress.newChild(1));
86 if (perFolder == null) {
87 return;
89 perFolder = pruneFolderMap(perFolder, progress.newChild(1));
90 if (perFolder == null) {
91 return;
94 updateGitIgnores(perFolder, progress.newChild(1));
95 } catch (CoreException e) {
96 throw e;
97 } catch (Exception e) {
98 throw new CoreException(Activator.error(
99 CoreText.IgnoreOperation_error, e));
104 * @return true if a gitignore file outside the workspace was changed. In
105 * this case the caller may need to perform manual UI refreshes
106 * because there was no ResourceChanged event.
108 public boolean isGitignoreOutsideWSChanged() {
109 return gitignoreOutsideWSChanged;
112 @Override
113 public ISchedulingRule getSchedulingRule() {
114 return schedulingRule;
117 private Map<IPath, Collection<String>> getFolderMap(
118 IProgressMonitor monitor) {
119 SubMonitor progress = SubMonitor.convert(monitor, paths.size());
120 Map<IPath, Collection<String>> result = new HashMap<>();
121 for (IPath path : paths) {
122 if (progress.isCanceled()) {
123 return null;
125 IPath parent = path.removeLastSegments(1);
126 Collection<String> values = result.get(parent);
127 if (values == null) {
128 values = new LinkedHashSet<>();
129 result.put(parent, values);
131 values.add(path.lastSegment());
132 progress.worked(1);
134 return result;
137 private Map<IPath, Collection<String>> pruneFolderMap(
138 Map<IPath, Collection<String>> perFolder, IProgressMonitor monitor)
139 throws IOException {
140 SubMonitor progress = SubMonitor.convert(monitor, perFolder.size());
141 for (Map.Entry<IPath, Collection<String>> entry : perFolder
142 .entrySet()) {
143 pruneFolder(entry.getKey(), entry.getValue(), progress.newChild(1));
144 if (progress.isCanceled()) {
145 return null;
148 return perFolder;
151 private void pruneFolder(IPath folder, Collection<String> files,
152 IProgressMonitor monitor)
153 throws IOException {
154 if (files.isEmpty()) {
155 return;
157 Repository repository = Activator.getDefault().getRepositoryCache()
158 .getRepository(folder);
159 if (repository == null || repository.isBare()) {
160 files.clear();
161 return;
163 WorkingTreeIterator treeIterator = IteratorService
164 .createInitialIterator(repository);
165 if (treeIterator == null) {
166 files.clear();
167 return;
169 IPath repoRelativePath = folder.makeRelativeTo(
170 new Path(repository.getWorkTree().getAbsolutePath()));
171 if (repoRelativePath.equals(folder)) {
172 files.clear();
173 return;
175 Collection<String> repoRelative = new HashSet<>(files.size());
176 for (String file : files) {
177 repoRelative.add(repoRelativePath.append(file).toPortableString());
179 // Remove all entries,then re-add only those found during the tree walk
180 // that are not ignored already
181 files.clear();
182 try (TreeWalk walk = new TreeWalk(repository)) {
183 walk.addTree(treeIterator);
184 walk.setFilter(PathFilterGroup.createFromStrings(repoRelative));
185 while (walk.next()) {
186 if (monitor.isCanceled()) {
187 return;
189 WorkingTreeIterator workingTreeIterator = walk.getTree(0,
190 WorkingTreeIterator.class);
191 if (repoRelative.contains(walk.getPathString())) {
192 if (!workingTreeIterator.isEntryIgnored()) {
193 files.add(walk.getNameString());
195 } else if (workingTreeIterator.getEntryFileMode()
196 .equals(FileMode.TREE)) {
197 walk.enterSubtree();
203 private void updateGitIgnores(Map<IPath, Collection<String>> perFolder,
204 IProgressMonitor monitor) throws CoreException, IOException {
205 SubMonitor progress = SubMonitor.convert(monitor, perFolder.size() * 2);
206 for (Map.Entry<IPath, Collection<String>> entry : perFolder
207 .entrySet()) {
208 if (progress.isCanceled()) {
209 return;
211 IContainer container = ResourceUtil
212 .getContainerForLocation(entry.getKey(), false);
213 if (container instanceof IWorkspaceRoot) {
214 container = null;
216 Collection<String> files = entry.getValue();
217 if (files.isEmpty()) {
218 progress.worked(1);
219 continue;
221 StringBuilder builder = new StringBuilder();
222 for (String file : files) {
223 builder.append('/').append(file);
224 boolean isDirectory = false;
225 IResource resource = container != null
226 ? container.findMember(file) : null;
227 if (resource != null) {
228 isDirectory = resource.getType() != IResource.FILE;
229 } else {
230 isDirectory = entry.getKey().append(file).toFile()
231 .isDirectory();
233 if (isDirectory) {
234 builder.append('/');
236 builder.append('\n');
238 progress.worked(1);
239 if (progress.isCanceled()) {
240 return;
242 addToGitIgnore(container, entry.getKey(), builder.toString(),
243 progress.newChild(1));
247 private void addToGitIgnore(IContainer container, IPath parent,
248 String entry, IProgressMonitor monitor)
249 throws CoreException, IOException {
250 SubMonitor progress = SubMonitor.convert(monitor, 1);
251 if (container == null) {
252 // .gitignore outside of workspace
253 Repository repository = Activator.getDefault().getRepositoryCache()
254 .getRepository(parent);
255 if (repository == null || repository.isBare()) {
256 String message = NLS.bind(
257 CoreText.IgnoreOperation_parentOutsideRepo,
258 parent.toOSString(), null);
259 IStatus status = Activator.error(message, null);
260 throw new CoreException(status);
262 IPath gitIgnorePath = parent.append(Constants.GITIGNORE_FILENAME);
263 IPath repoPath = new Path(repository.getWorkTree()
264 .getAbsolutePath());
265 if (!repoPath.isPrefixOf(gitIgnorePath)) {
266 String message = NLS.bind(
267 CoreText.IgnoreOperation_parentOutsideRepo,
268 parent.toOSString(), repoPath.toOSString());
269 IStatus status = Activator.error(message, null);
270 throw new CoreException(status);
272 File gitIgnore = new File(gitIgnorePath.toOSString());
273 updateGitIgnore(gitIgnore, entry);
274 // no resource change event when updating .gitignore outside
275 // workspace => trigger manual decorator refresh
276 gitignoreOutsideWSChanged = true;
277 } else {
278 // .gitignore is in workspace
279 IFile gitignore = container.getFile(new Path(
280 Constants.GITIGNORE_FILENAME));
281 String toAdd = getEntry(gitignore.getLocation().toFile(), entry);
282 ByteArrayInputStream entryBytes = asStream(toAdd);
283 if (gitignore.exists()) {
284 gitignore.appendContents(entryBytes, true, true,
285 progress.newChild(1));
286 } else {
287 gitignore.create(entryBytes, true, progress.newChild(1));
292 private boolean prependNewline(File file) throws IOException {
293 boolean prepend = false;
294 long length = file.length();
295 if (length > 0) {
296 try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { //$NON-NLS-1$
297 // Read the last byte and see if it is a newline
298 ByteBuffer buffer = ByteBuffer.allocate(1);
299 FileChannel channel = raf.getChannel();
300 channel.position(length - 1);
301 if (channel.read(buffer) > 0) {
302 buffer.rewind();
303 prepend = buffer.get() != '\n';
307 return prepend;
310 private String getEntry(File file, String entry) throws IOException {
311 return prependNewline(file) ? '\n' + entry : entry;
314 private void updateGitIgnore(File gitIgnore, String entry)
315 throws CoreException {
316 try {
317 String ignoreLine = entry;
318 if (!gitIgnore.exists()) {
319 if (!gitIgnore.createNewFile()) {
320 String error = NLS.bind(
321 CoreText.IgnoreOperation_creatingFailed,
322 gitIgnore.getAbsolutePath());
323 throw new CoreException(Activator.error(error, null));
325 } else {
326 ignoreLine = getEntry(gitIgnore, ignoreLine);
329 FileOutputStream os = new FileOutputStream(gitIgnore, true);
330 try {
331 os.write(ignoreLine.getBytes(Constants.CHARACTER_ENCODING));
332 } finally {
333 os.close();
335 } catch (IOException e) {
336 String error = NLS.bind(CoreText.IgnoreOperation_updatingFailed,
337 gitIgnore.getAbsolutePath());
338 throw new CoreException(Activator.error(error, e));
342 private ByteArrayInputStream asStream(String entry)
343 throws UnsupportedEncodingException {
344 return new ByteArrayInputStream(
345 entry.getBytes(Constants.CHARACTER_ENCODING));
348 private ISchedulingRule calcSchedulingRule() {
349 return RuleUtil.getRuleForContainers(paths);