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  	 * Constructor for CleanCommand
83  	 *
84  	 * @param repo
85  	 *            the {@link org.eclipse.jgit.lib.Repository}
86  	 */
87  	protected CleanCommand(Repository repo) {
88  		super(repo);
89  	}
90  
91  	/**
92  	 * {@inheritDoc}
93  	 * <p>
94  	 * Executes the {@code clean} command with all the options and parameters
95  	 * collected by the setter methods of this class. Each instance of this
96  	 * class should only be used for one invocation of the command (means: one
97  	 * call to {@link #call()})
98  	 */
99  	@Override
100 	public Set<String> call() throws NoWorkTreeException, GitAPIException {
101 		Set<String> files = new TreeSet<>();
102 		try {
103 			StatusCommand command = new StatusCommand(repo);
104 			Status status = command.call();
105 
106 			Set<String> untrackedFiles = new TreeSet<>(status.getUntracked());
107 			Set<String> untrackedDirs = 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 					untrackedFiles.add(p);
115 				} else if (fs.isDirectory(f)) {
116 					untrackedDirs.add(p);
117 				}
118 			}
119 
120 			Set<String> filtered = filterFolders(untrackedFiles, untrackedDirs);
121 
122 			Set<String> notIgnoredFiles = filterIgnorePaths(filtered,
123 					status.getIgnoredNotInIndex(), true);
124 			Set<String> notIgnoredDirs = filterIgnorePaths(untrackedDirs,
125 					status.getIgnoredNotInIndex(), false);
126 
127 			for (String file : notIgnoredFiles)
128 				if (paths.isEmpty() || paths.contains(file)) {
129 					files = cleanPath(file, files);
130 				}
131 
132 			for (String dir : notIgnoredDirs)
133 				if (paths.isEmpty() || paths.contains(dir)) {
134 					files = cleanPath(dir, files);
135 				}
136 		} catch (IOException e) {
137 			throw new JGitInternalException(e.getMessage(), e);
138 		} finally {
139 			if (!dryRun && !files.isEmpty()) {
140 				repo.fireEvent(new WorkingTreeModifiedEvent(null, files));
141 			}
142 		}
143 		return files;
144 	}
145 
146 	/**
147 	 * When dryRun is false, deletes the specified path from disk. If dryRun
148 	 * is true, no paths are actually deleted. In both cases, the paths that
149 	 * would have been deleted are added to inFiles and returned.
150 	 *
151 	 * Paths that are directories are recursively deleted when
152 	 * {@link #directories} is true.
153 	 * Paths that are git repositories are recursively deleted when
154 	 * {@link #directories} and {@link #force} are both true.
155 	 *
156 	 * @param path
157 	 * 			The path to be cleaned
158 	 * @param inFiles
159 	 * 			A set of strings representing the files that have been cleaned
160 	 * 			already, the path to be cleaned will be added to this set
161 	 * 			before being returned.
162 	 *
163 	 * @return a set of strings with the cleaned path added to it
164 	 * @throws IOException
165 	 */
166 	private Set<String> cleanPath(String path, Set<String> inFiles)
167 			throws IOException {
168 		File curFile = new File(repo.getWorkTree(), path);
169 		if (curFile.isDirectory()) {
170 			if (directories) {
171 				// Is this directory a git repository?
172 				if (new File(curFile, DOT_GIT).exists()) {
173 					if (force) {
174 						if (!dryRun) {
175 							FileUtils.delete(curFile, FileUtils.RECURSIVE
176 									| FileUtils.SKIP_MISSING);
177 						}
178 						inFiles.add(path + "/"); //$NON-NLS-1$
179 					}
180 				} else {
181 					if (!dryRun) {
182 						FileUtils.delete(curFile,
183 								FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
184 					}
185 					inFiles.add(path + "/"); //$NON-NLS-1$
186 				}
187 			}
188 		} else {
189 			if (!dryRun) {
190 				FileUtils.delete(curFile, FileUtils.SKIP_MISSING);
191 			}
192 			inFiles.add(path);
193 		}
194 
195 		return inFiles;
196 	}
197 
198 	private Set<String> filterIgnorePaths(Set<String> inputPaths,
199 			Set<String> ignoredNotInIndex, boolean exact) {
200 		if (ignore) {
201 			Set<String> filtered = new TreeSet<>(inputPaths);
202 			for (String path : inputPaths)
203 				for (String ignored : ignoredNotInIndex)
204 					if ((exact && path.equals(ignored))
205 							|| (!exact && path.startsWith(ignored))) {
206 						filtered.remove(path);
207 						break;
208 					}
209 
210 			return filtered;
211 		}
212 		return inputPaths;
213 	}
214 
215 	private Set<String> filterFolders(Set<String> untracked,
216 			Set<String> untrackedFolders) {
217 		Set<String> filtered = new TreeSet<>(untracked);
218 		for (String file : untracked)
219 			for (String folder : untrackedFolders)
220 				if (file.startsWith(folder)) {
221 					filtered.remove(file);
222 					break;
223 				}
224 
225 
226 		return filtered;
227 	}
228 
229 	/**
230 	 * If paths are set, only these paths are affected by the cleaning.
231 	 *
232 	 * @param paths
233 	 *            the paths to set (with <code>/</code> as separator)
234 	 * @return {@code this}
235 	 */
236 	public CleanCommand setPaths(Set<String> paths) {
237 		this.paths = paths;
238 		return this;
239 	}
240 
241 	/**
242 	 * If dryRun is set, the paths in question will not actually be deleted.
243 	 *
244 	 * @param dryRun
245 	 *            whether to do a dry run or not
246 	 * @return {@code this}
247 	 */
248 	public CleanCommand setDryRun(boolean dryRun) {
249 		this.dryRun = dryRun;
250 		return this;
251 	}
252 
253 	/**
254 	 * If force is set, directories that are git repositories will also be
255 	 * deleted.
256 	 *
257 	 * @param force
258 	 *            whether or not to delete git repositories
259 	 * @return {@code this}
260 	 * @since 4.5
261 	 */
262 	public CleanCommand setForce(boolean force) {
263 		this.force = force;
264 		return this;
265 	}
266 
267 	/**
268 	 * If dirs is set, in addition to files, also clean directories.
269 	 *
270 	 * @param dirs
271 	 *            whether to clean directories too, or only files.
272 	 * @return {@code this}
273 	 */
274 	public CleanCommand setCleanDirectories(boolean dirs) {
275 		directories = dirs;
276 		return this;
277 	}
278 
279 	/**
280 	 * If ignore is set, don't report/clean files/directories that are ignored
281 	 * by a .gitignore. otherwise do handle them.
282 	 *
283 	 * @param ignore
284 	 *            whether to respect .gitignore or not.
285 	 * @return {@code this}
286 	 */
287 	public CleanCommand setIgnore(boolean ignore) {
288 		this.ignore = ignore;
289 		return this;
290 	}
291 }