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