View Javadoc
1   /*
2    * Copyright (C) 2011, Chris Aniszczyk <zx@redhat.com>
3    * Copyright (C) 2011, Abhishek Bhatnagar <abhatnag@redhat.com> and others
4    *
5    * This program and the accompanying materials are made available under the
6    * terms of the Eclipse Distribution License v. 1.0 which is available at
7    * https://www.eclipse.org/org/documents/edl-v10.php.
8    *
9    * SPDX-License-Identifier: BSD-3-Clause
10   */
11  package org.eclipse.jgit.api;
12  
13  import static org.eclipse.jgit.lib.Constants.DOT_GIT;
14  
15  import java.io.File;
16  import java.io.IOException;
17  import java.util.Collections;
18  import java.util.Set;
19  import java.util.TreeSet;
20  
21  import org.eclipse.jgit.api.errors.GitAPIException;
22  import org.eclipse.jgit.api.errors.JGitInternalException;
23  import org.eclipse.jgit.errors.NoWorkTreeException;
24  import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
25  import org.eclipse.jgit.lib.Repository;
26  import org.eclipse.jgit.util.FS;
27  import org.eclipse.jgit.util.FileUtils;
28  import org.eclipse.jgit.util.Paths;
29  
30  /**
31   * Remove untracked files from the working tree
32   *
33   * @see <a
34   *      href="http://www.kernel.org/pub/software/scm/git/docs/git-clean.html"
35   *      >Git documentation about Clean</a>
36   */
37  public class CleanCommand extends GitCommand<Set<String>> {
38  
39  	private Set<String> paths = Collections.emptySet();
40  
41  	private boolean dryRun;
42  
43  	private boolean directories;
44  
45  	private boolean ignore = true;
46  
47  	private boolean force = false;
48  
49  	/**
50  	 * Constructor for CleanCommand
51  	 *
52  	 * @param repo
53  	 *            the {@link org.eclipse.jgit.lib.Repository}
54  	 */
55  	protected CleanCommand(Repository repo) {
56  		super(repo);
57  	}
58  
59  	/**
60  	 * {@inheritDoc}
61  	 * <p>
62  	 * Executes the {@code clean} command with all the options and parameters
63  	 * collected by the setter methods of this class. Each instance of this
64  	 * class should only be used for one invocation of the command (means: one
65  	 * call to {@link #call()})
66  	 */
67  	@Override
68  	public Set<String> call() throws NoWorkTreeException, GitAPIException {
69  		Set<String> files = new TreeSet<>();
70  		try {
71  			StatusCommand command = new StatusCommand(repo);
72  			Status status = command.call();
73  
74  			Set<String> untrackedFiles = new TreeSet<>(status.getUntracked());
75  			Set<String> untrackedDirs = new TreeSet<>(
76  					status.getUntrackedFolders());
77  
78  			FS fs = getRepository().getFS();
79  			for (String p : status.getIgnoredNotInIndex()) {
80  				File f = new File(repo.getWorkTree(), p);
81  				if (fs.isFile(f) || fs.isSymLink(f)) {
82  					untrackedFiles.add(p);
83  				} else if (fs.isDirectory(f)) {
84  					untrackedDirs.add(p);
85  				}
86  			}
87  
88  			Set<String> filtered = filterFolders(untrackedFiles, untrackedDirs);
89  
90  			Set<String> notIgnoredFiles = filterIgnorePaths(filtered,
91  					status.getIgnoredNotInIndex(), true);
92  			Set<String> notIgnoredDirs = filterIgnorePaths(untrackedDirs,
93  					status.getIgnoredNotInIndex(), false);
94  
95  			for (String file : notIgnoredFiles) {
96  				if (paths.isEmpty() || paths.contains(file)) {
97  					files = cleanPath(file, files);
98  				}
99  			}
100 			for (String dir : notIgnoredDirs) {
101 				if (paths.isEmpty() || paths.contains(dir)) {
102 					files = cleanPath(dir, files);
103 				}
104 			}
105 		} catch (IOException e) {
106 			throw new JGitInternalException(e.getMessage(), e);
107 		} finally {
108 			if (!dryRun && !files.isEmpty()) {
109 				repo.fireEvent(new WorkingTreeModifiedEvent(null, files));
110 			}
111 		}
112 		return files;
113 	}
114 
115 	/**
116 	 * When dryRun is false, deletes the specified path from disk. If dryRun
117 	 * is true, no paths are actually deleted. In both cases, the paths that
118 	 * would have been deleted are added to inFiles and returned.
119 	 *
120 	 * Paths that are directories are recursively deleted when
121 	 * {@link #directories} is true.
122 	 * Paths that are git repositories are recursively deleted when
123 	 * {@link #directories} and {@link #force} are both true.
124 	 *
125 	 * @param path
126 	 * 			The path to be cleaned
127 	 * @param inFiles
128 	 * 			A set of strings representing the files that have been cleaned
129 	 * 			already, the path to be cleaned will be added to this set
130 	 * 			before being returned.
131 	 *
132 	 * @return a set of strings with the cleaned path added to it
133 	 * @throws IOException
134 	 */
135 	private Set<String> cleanPath(String path, Set<String> inFiles)
136 			throws IOException {
137 		File curFile = new File(repo.getWorkTree(), path);
138 		if (curFile.isDirectory()) {
139 			if (directories) {
140 				// Is this directory a git repository?
141 				if (new File(curFile, DOT_GIT).exists()) {
142 					if (force) {
143 						if (!dryRun) {
144 							FileUtils.delete(curFile, FileUtils.RECURSIVE
145 									| FileUtils.SKIP_MISSING);
146 						}
147 						inFiles.add(path + '/');
148 					}
149 				} else {
150 					if (!dryRun) {
151 						FileUtils.delete(curFile,
152 								FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
153 					}
154 					inFiles.add(path + '/');
155 				}
156 			}
157 		} else {
158 			if (!dryRun) {
159 				FileUtils.delete(curFile, FileUtils.SKIP_MISSING);
160 			}
161 			inFiles.add(path);
162 		}
163 
164 		return inFiles;
165 	}
166 
167 	private Set<String> filterIgnorePaths(Set<String> inputPaths,
168 			Set<String> ignoredNotInIndex, boolean exact) {
169 		if (ignore) {
170 			Set<String> filtered = new TreeSet<>(inputPaths);
171 			for (String path : inputPaths) {
172 				for (String ignored : ignoredNotInIndex) {
173 					if ((exact && path.equals(ignored))
174 							|| (!exact
175 									&& Paths.isEqualOrPrefix(ignored, path))) {
176 						filtered.remove(path);
177 						break;
178 					}
179 				}
180 			}
181 			return filtered;
182 		}
183 		return inputPaths;
184 	}
185 
186 	private Set<String> filterFolders(Set<String> untracked,
187 			Set<String> untrackedFolders) {
188 		Set<String> filtered = new TreeSet<>(untracked);
189 		for (String file : untracked) {
190 			for (String folder : untrackedFolders) {
191 				if (Paths.isEqualOrPrefix(folder, file)) {
192 					filtered.remove(file);
193 					break;
194 				}
195 			}
196 		}
197 		return filtered;
198 	}
199 
200 	/**
201 	 * If paths are set, only these paths are affected by the cleaning.
202 	 *
203 	 * @param paths
204 	 *            the paths to set (with <code>/</code> as separator)
205 	 * @return {@code this}
206 	 */
207 	public CleanCommand setPaths(Set<String> paths) {
208 		this.paths = paths;
209 		return this;
210 	}
211 
212 	/**
213 	 * If dryRun is set, the paths in question will not actually be deleted.
214 	 *
215 	 * @param dryRun
216 	 *            whether to do a dry run or not
217 	 * @return {@code this}
218 	 */
219 	public CleanCommand setDryRun(boolean dryRun) {
220 		this.dryRun = dryRun;
221 		return this;
222 	}
223 
224 	/**
225 	 * If force is set, directories that are git repositories will also be
226 	 * deleted.
227 	 *
228 	 * @param force
229 	 *            whether or not to delete git repositories
230 	 * @return {@code this}
231 	 * @since 4.5
232 	 */
233 	public CleanCommand setForce(boolean force) {
234 		this.force = force;
235 		return this;
236 	}
237 
238 	/**
239 	 * If dirs is set, in addition to files, also clean directories.
240 	 *
241 	 * @param dirs
242 	 *            whether to clean directories too, or only files.
243 	 * @return {@code this}
244 	 */
245 	public CleanCommand setCleanDirectories(boolean dirs) {
246 		directories = dirs;
247 		return this;
248 	}
249 
250 	/**
251 	 * If ignore is set, don't report/clean files/directories that are ignored
252 	 * by a .gitignore. otherwise do handle them.
253 	 *
254 	 * @param ignore
255 	 *            whether to respect .gitignore or not.
256 	 * @return {@code this}
257 	 */
258 	public CleanCommand setIgnore(boolean ignore) {
259 		this.ignore = ignore;
260 		return this;
261 	}
262 }