View Javadoc
1   /*
2    * Copyright (C) 2011, Chris Aniszczyk <zx@redhat.com>
3    * Copyright (C) 2011, Abhishek Bhatnagar <abhatnag@redhat.com>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  package org.eclipse.jgit.api;
45  
46  import static org.eclipse.jgit.lib.Constants.DOT_GIT;
47  
48  import java.io.File;
49  import java.io.IOException;
50  import java.util.Collections;
51  import java.util.Set;
52  import java.util.TreeSet;
53  
54  import org.eclipse.jgit.api.errors.GitAPIException;
55  import org.eclipse.jgit.api.errors.JGitInternalException;
56  import org.eclipse.jgit.errors.NoWorkTreeException;
57  import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
58  import org.eclipse.jgit.lib.Repository;
59  import org.eclipse.jgit.util.FS;
60  import org.eclipse.jgit.util.FileUtils;
61  
62  /**
63   * Remove untracked files from the working tree
64   *
65   * @see <a
66   *      href="http://www.kernel.org/pub/software/scm/git/docs/git-clean.html"
67   *      >Git documentation about Clean</a>
68   */
69  public class CleanCommand extends GitCommand<Set<String>> {
70  
71  	private Set<String> paths = Collections.emptySet();
72  
73  	private boolean dryRun;
74  
75  	private boolean directories;
76  
77  	private boolean ignore = true;
78  
79  	private boolean force = false;
80  
81  	/**
82  	 * @param repo
83  	 */
84  	protected CleanCommand(Repository repo) {
85  		super(repo);
86  	}
87  
88  	/**
89  	 * Executes the {@code clean} command with all the options and parameters
90  	 * collected by the setter methods of this class. Each instance of this
91  	 * class should only be used for one invocation of the command (means: one
92  	 * call to {@link #call()})
93  	 *
94  	 * @return a set of strings representing each file cleaned.
95  	 * @throws GitAPIException
96  	 * @throws NoWorkTreeException
97  	 */
98  	@Override
99  	public Set<String> call() throws NoWorkTreeException, GitAPIException {
100 		Set<String> files = new TreeSet<>();
101 		try {
102 			StatusCommand command = new StatusCommand(repo);
103 			Status status = command.call();
104 
105 			Set<String> untrackedAndIgnoredFiles = new TreeSet<>(
106 					status.getUntracked());
107 			Set<String> untrackedAndIgnoredDirs = new TreeSet<>(
108 					status.getUntrackedFolders());
109 
110 			FS fs = getRepository().getFS();
111 			for (String p : status.getIgnoredNotInIndex()) {
112 				File f = new File(repo.getWorkTree(), p);
113 				if (fs.isFile(f) || fs.isSymLink(f))
114 					untrackedAndIgnoredFiles.add(p);
115 				else if (fs.isDirectory(f))
116 					untrackedAndIgnoredDirs.add(p);
117 			}
118 
119 			Set<String> filtered = filterFolders(untrackedAndIgnoredFiles,
120 					untrackedAndIgnoredDirs);
121 
122 			Set<String> notIgnoredFiles = filterIgnorePaths(filtered,
123 					status.getIgnoredNotInIndex(), true);
124 			Set<String> notIgnoredDirs = filterIgnorePaths(
125 					untrackedAndIgnoredDirs,
126 					status.getIgnoredNotInIndex(), false);
127 
128 			for (String file : notIgnoredFiles)
129 				if (paths.isEmpty() || paths.contains(file)) {
130 					files = cleanPath(file, files);
131 				}
132 
133 			for (String dir : notIgnoredDirs)
134 				if (paths.isEmpty() || paths.contains(dir)) {
135 					files = cleanPath(dir, files);
136 				}
137 		} catch (IOException e) {
138 			throw new JGitInternalException(e.getMessage(), e);
139 		} finally {
140 			if (!files.isEmpty()) {
141 				repo.fireEvent(new WorkingTreeModifiedEvent(null, files));
142 			}
143 		}
144 		return files;
145 	}
146 
147 	/**
148 	 * When dryRun is false, deletes the specified path from disk. If dryRun
149 	 * is true, no paths are actually deleted. In both cases, the paths that
150 	 * would have been deleted are added to inFiles and returned.
151 	 *
152 	 * Paths that are directories are recursively deleted when
153 	 * {@link #directories} is true.
154 	 * Paths that are git repositories are recursively deleted when
155 	 * {@link #directories} and {@link #force} are both true.
156 	 *
157 	 * @param path
158 	 * 			The path to be cleaned
159 	 * @param inFiles
160 	 * 			A set of strings representing the files that have been cleaned
161 	 * 			already, the path to be cleaned will be added to this set
162 	 * 			before being returned.
163 	 *
164 	 * @return a set of strings with the cleaned path added to it
165 	 * @throws IOException
166 	 */
167 	private Set<String> cleanPath(String path, Set<String> inFiles)
168 			throws IOException {
169 		File curFile = new File(repo.getWorkTree(), path);
170 		if (curFile.isDirectory()) {
171 			if (directories) {
172 				// Is this directory a git repository?
173 				if (new File(curFile, DOT_GIT).exists()) {
174 					if (force) {
175 						if (!dryRun) {
176 							FileUtils.delete(curFile, FileUtils.RECURSIVE);
177 						}
178 						inFiles.add(path + "/"); //$NON-NLS-1$
179 					}
180 				} else {
181 					if (!dryRun) {
182 						FileUtils.delete(curFile, FileUtils.RECURSIVE);
183 					}
184 					inFiles.add(path + "/"); //$NON-NLS-1$
185 				}
186 			}
187 		} else {
188 			if (!dryRun) {
189 				FileUtils.delete(curFile, FileUtils.NONE);
190 			}
191 			inFiles.add(path);
192 		}
193 
194 		return inFiles;
195 	}
196 
197 	private Set<String> filterIgnorePaths(Set<String> inputPaths,
198 			Set<String> ignoredNotInIndex, boolean exact) {
199 		if (ignore) {
200 			Set<String> filtered = new TreeSet<>(inputPaths);
201 			for (String path : inputPaths)
202 				for (String ignored : ignoredNotInIndex)
203 					if ((exact && path.equals(ignored))
204 							|| (!exact && path.startsWith(ignored))) {
205 						filtered.remove(path);
206 						break;
207 					}
208 
209 			return filtered;
210 		}
211 		return inputPaths;
212 	}
213 
214 	private Set<String> filterFolders(Set<String> untracked,
215 			Set<String> untrackedFolders) {
216 		Set<String> filtered = new TreeSet<>(untracked);
217 		for (String file : untracked)
218 			for (String folder : untrackedFolders)
219 				if (file.startsWith(folder)) {
220 					filtered.remove(file);
221 					break;
222 				}
223 
224 
225 		return filtered;
226 	}
227 
228 	/**
229 	 * If paths are set, only these paths are affected by the cleaning.
230 	 *
231 	 * @param paths
232 	 *            the paths to set (with <code>/</code> as separator)
233 	 * @return {@code this}
234 	 */
235 	public CleanCommand setPaths(Set<String> paths) {
236 		this.paths = paths;
237 		return this;
238 	}
239 
240 	/**
241 	 * If dryRun is set, the paths in question will not actually be deleted.
242 	 *
243 	 * @param dryRun
244 	 *            whether to do a dry run or not
245 	 * @return {@code this}
246 	 */
247 	public CleanCommand setDryRun(boolean dryRun) {
248 		this.dryRun = dryRun;
249 		return this;
250 	}
251 
252 	/**
253 	 * If force is set, directories that are git repositories will also be
254 	 * deleted.
255 	 *
256 	 * @param force
257 	 *            whether or not to delete git repositories
258 	 * @return {@code this}
259 	 * @since 4.5
260 	 */
261 	public CleanCommand setForce(boolean force) {
262 		this.force = force;
263 		return this;
264 	}
265 
266 	/**
267 	 * If dirs is set, in addition to files, also clean directories.
268 	 *
269 	 * @param dirs
270 	 *            whether to clean directories too, or only files.
271 	 * @return {@code this}
272 	 */
273 	public CleanCommand setCleanDirectories(boolean dirs) {
274 		directories = dirs;
275 		return this;
276 	}
277 
278 	/**
279 	 * If ignore is set, don't report/clean files/directories that are ignored
280 	 * by a .gitignore. otherwise do handle them.
281 	 *
282 	 * @param ignore
283 	 *            whether to respect .gitignore or not.
284 	 * @return {@code this}
285 	 */
286 	public CleanCommand setIgnore(boolean ignore) {
287 		this.ignore = ignore;
288 		return this;
289 	}
290 }