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
;
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
;
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
;
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
;
68 * construct an IgnoreOperation
73 public IgnoreOperation(Collection
<IPath
> paths
) {
75 gitignoreOutsideWSChanged
= false;
76 schedulingRule
= calcSchedulingRule();
80 public void execute(IProgressMonitor monitor
) throws CoreException
{
81 SubMonitor progress
= SubMonitor
.convert(monitor
,
82 CoreText
.IgnoreOperation_taskName
, 3);
84 Map
<IPath
, Collection
<String
>> perFolder
= getFolderMap(
85 progress
.newChild(1));
86 if (perFolder
== null) {
89 perFolder
= pruneFolderMap(perFolder
, progress
.newChild(1));
90 if (perFolder
== null) {
94 updateGitIgnores(perFolder
, progress
.newChild(1));
95 } catch (CoreException 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
;
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()) {
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());
137 private Map
<IPath
, Collection
<String
>> pruneFolderMap(
138 Map
<IPath
, Collection
<String
>> perFolder
, IProgressMonitor monitor
)
140 SubMonitor progress
= SubMonitor
.convert(monitor
, perFolder
.size());
141 for (Map
.Entry
<IPath
, Collection
<String
>> entry
: perFolder
143 pruneFolder(entry
.getKey(), entry
.getValue(), progress
.newChild(1));
144 if (progress
.isCanceled()) {
151 private void pruneFolder(IPath folder
, Collection
<String
> files
,
152 IProgressMonitor monitor
)
154 if (files
.isEmpty()) {
157 Repository repository
= Activator
.getDefault().getRepositoryCache()
158 .getRepository(folder
);
159 if (repository
== null || repository
.isBare()) {
163 WorkingTreeIterator treeIterator
= IteratorService
164 .createInitialIterator(repository
);
165 if (treeIterator
== null) {
169 IPath repoRelativePath
= folder
.makeRelativeTo(
170 new Path(repository
.getWorkTree().getAbsolutePath()));
171 if (repoRelativePath
.equals(folder
)) {
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
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()) {
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
)) {
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
208 if (progress
.isCanceled()) {
211 IContainer container
= ResourceUtil
212 .getContainerForLocation(entry
.getKey(), false);
213 if (container
instanceof IWorkspaceRoot
) {
216 Collection
<String
> files
= entry
.getValue();
217 if (files
.isEmpty()) {
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
;
230 isDirectory
= entry
.getKey().append(file
).toFile()
236 builder
.append('\n');
239 if (progress
.isCanceled()) {
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()
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;
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));
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();
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) {
303 prepend
= buffer
.get() != '\n';
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
{
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));
326 ignoreLine
= getEntry(gitIgnore
, ignoreLine
);
329 FileOutputStream os
= new FileOutputStream(gitIgnore
, true);
331 os
.write(ignoreLine
.getBytes(Constants
.CHARACTER_ENCODING
));
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
);