View Javadoc
1   /*
2    * Copyright (C) 2017, Two Sigma Open Source and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.api;
11  
12  import static org.eclipse.jgit.util.FileUtils.RECURSIVE;
13  
14  import java.io.File;
15  import java.io.IOException;
16  import java.text.MessageFormat;
17  import java.util.ArrayList;
18  import java.util.Collection;
19  import java.util.Collections;
20  import java.util.List;
21  
22  import org.eclipse.jgit.api.errors.GitAPIException;
23  import org.eclipse.jgit.api.errors.InvalidConfigurationException;
24  import org.eclipse.jgit.api.errors.JGitInternalException;
25  import org.eclipse.jgit.api.errors.NoHeadException;
26  import org.eclipse.jgit.errors.ConfigInvalidException;
27  import org.eclipse.jgit.internal.JGitText;
28  import org.eclipse.jgit.lib.ConfigConstants;
29  import org.eclipse.jgit.lib.ObjectId;
30  import org.eclipse.jgit.lib.Ref;
31  import org.eclipse.jgit.lib.Repository;
32  import org.eclipse.jgit.lib.StoredConfig;
33  import org.eclipse.jgit.revwalk.RevCommit;
34  import org.eclipse.jgit.revwalk.RevTree;
35  import org.eclipse.jgit.revwalk.RevWalk;
36  import org.eclipse.jgit.submodule.SubmoduleWalk;
37  import org.eclipse.jgit.treewalk.filter.PathFilter;
38  import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
39  import org.eclipse.jgit.treewalk.filter.TreeFilter;
40  import org.eclipse.jgit.util.FileUtils;
41  
42  /**
43   * A class used to execute a submodule deinit command.
44   * <p>
45   * This will remove the module(s) from the working tree, but won't affect
46   * .git/modules.
47   *
48   * @since 4.10
49   * @see <a href=
50   *      "http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html"
51   *      >Git documentation about submodules</a>
52   */
53  public class SubmoduleDeinitCommand
54  		extends GitCommand<Collection<SubmoduleDeinitResult>> {
55  
56  	private final Collection<String> paths;
57  
58  	private boolean force;
59  
60  	/**
61  	 * Constructor of SubmoduleDeinitCommand
62  	 *
63  	 * @param repo
64  	 */
65  	public SubmoduleDeinitCommand(Repository repo) {
66  		super(repo);
67  		paths = new ArrayList<>();
68  	}
69  
70  	/**
71  	 * {@inheritDoc}
72  	 * <p>
73  	 *
74  	 * @return the set of repositories successfully deinitialized.
75  	 * @throws NoSuchSubmoduleException
76  	 *             if any of the submodules which we might want to deinitialize
77  	 *             don't exist
78  	 */
79  	@Override
80  	public Collection<SubmoduleDeinitResult> call() throws GitAPIException {
81  		checkCallable();
82  		try {
83  			if (paths.isEmpty()) {
84  				return Collections.emptyList();
85  			}
86  			for (String path : paths) {
87  				if (!submoduleExists(path)) {
88  					throw new NoSuchSubmoduleException(path);
89  				}
90  			}
91  			List<SubmoduleDeinitResult> results = new ArrayList<>(paths.size());
92  			try (RevWalklk.html#RevWalk">RevWalk revWalk = new RevWalk(repo);
93  					SubmoduleWalk generator = SubmoduleWalk.forIndex(repo)) {
94  				generator.setFilter(PathFilterGroup.createFromStrings(paths));
95  				StoredConfig config = repo.getConfig();
96  				while (generator.next()) {
97  					String path = generator.getPath();
98  					String name = generator.getModuleName();
99  					SubmoduleDeinitStatus status = checkDirty(revWalk, path);
100 					switch (status) {
101 					case SUCCESS:
102 						deinit(path);
103 						break;
104 					case ALREADY_DEINITIALIZED:
105 						break;
106 					case DIRTY:
107 						if (force) {
108 							deinit(path);
109 							status = SubmoduleDeinitStatus.FORCED;
110 						}
111 						break;
112 					default:
113 						throw new JGitInternalException(MessageFormat.format(
114 								JGitText.get().unexpectedSubmoduleStatus,
115 								status));
116 					}
117 
118 					config.unsetSection(
119 							ConfigConstants.CONFIG_SUBMODULE_SECTION, name);
120 					results.add(new SubmoduleDeinitResult(path, status));
121 				}
122 			}
123 			return results;
124 		} catch (ConfigInvalidException e) {
125 			throw new InvalidConfigurationException(e.getMessage(), e);
126 		} catch (IOException e) {
127 			throw new JGitInternalException(e.getMessage(), e);
128 		}
129 	}
130 
131 	/**
132 	 * Recursively delete the *contents* of path, but leave path as an empty
133 	 * directory
134 	 *
135 	 * @param path
136 	 *            the path to clean
137 	 * @throws IOException
138 	 */
139 	private void deinit(String path) throws IOException {
140 		File dir = new File(repo.getWorkTree(), path);
141 		if (!dir.isDirectory()) {
142 			throw new JGitInternalException(MessageFormat.format(
143 					JGitText.get().expectedDirectoryNotSubmodule, path));
144 		}
145 		final File[] ls = dir.listFiles();
146 		if (ls != null) {
147 			for (File f : ls) {
148 				FileUtils.delete(f, RECURSIVE);
149 			}
150 		}
151 	}
152 
153 	/**
154 	 * Check if a submodule is dirty. A submodule is dirty if there are local
155 	 * changes to the submodule relative to its HEAD, including untracked files.
156 	 * It is also dirty if the HEAD of the submodule does not match the value in
157 	 * the parent repo's index or HEAD.
158 	 *
159 	 * @param revWalk
160 	 * @param path
161 	 * @return status of the command
162 	 * @throws GitAPIException
163 	 * @throws IOException
164 	 */
165 	private SubmoduleDeinitStatus checkDirty(RevWalk revWalk, String path)
166 			throws GitAPIException, IOException {
167 		Ref head = repo.exactRef("HEAD"); //$NON-NLS-1$
168 		if (head == null) {
169 			throw new NoHeadException(
170 					JGitText.get().invalidRepositoryStateNoHead);
171 		}
172 		RevCommit headCommit = revWalk.parseCommit(head.getObjectId());
173 		RevTree tree = headCommit.getTree();
174 
175 		ObjectId submoduleHead;
176 		try (SubmoduleWalk w = SubmoduleWalk.forPath(repo, tree, path)) {
177 			submoduleHead = w.getHead();
178 			if (submoduleHead == null) {
179 				// The submodule is not checked out.
180 				return SubmoduleDeinitStatus.ALREADY_DEINITIALIZED;
181 			}
182 			if (!submoduleHead.equals(w.getObjectId())) {
183 				// The submodule's current HEAD doesn't match the value in the
184 				// outer repo's HEAD.
185 				return SubmoduleDeinitStatus.DIRTY;
186 			}
187 		}
188 
189 		try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) {
190 			if (!w.next()) {
191 				// The submodule does not exist in the index (shouldn't happen
192 				// since we check this earlier)
193 				return SubmoduleDeinitStatus.DIRTY;
194 			}
195 			if (!submoduleHead.equals(w.getObjectId())) {
196 				// The submodule's current HEAD doesn't match the value in the
197 				// outer repo's index.
198 				return SubmoduleDeinitStatus.DIRTY;
199 			}
200 
201 			try (Repository submoduleRepo = w.getRepository()) {
202 				Status status = Git.wrap(submoduleRepo).status().call();
203 				return status.isClean() ? SubmoduleDeinitStatus.SUCCESS
204 						: SubmoduleDeinitStatus.DIRTY;
205 			}
206 		}
207 	}
208 
209 	/**
210 	 * Check if this path is a submodule by checking the index, which is what
211 	 * git submodule deinit checks.
212 	 *
213 	 * @param path
214 	 *            path of the submodule
215 	 *
216 	 * @return {@code true} if path exists and is a submodule in index,
217 	 *         {@code false} otherwise
218 	 * @throws IOException
219 	 */
220 	private boolean submoduleExists(String path) throws IOException {
221 		TreeFilter filter = PathFilter.create(path);
222 		try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) {
223 			return w.setFilter(filter).next();
224 		}
225 	}
226 
227 	/**
228 	 * Add repository-relative submodule path to deinitialize
229 	 *
230 	 * @param path
231 	 *            (with <code>/</code> as separator)
232 	 * @return this command
233 	 */
234 	public SubmoduleDeinitCommand addPath(String path) {
235 		paths.add(path);
236 		return this;
237 	}
238 
239 	/**
240 	 * If {@code true}, call() will deinitialize modules with local changes;
241 	 * else it will refuse to do so.
242 	 *
243 	 * @param force
244 	 * @return {@code this}
245 	 */
246 	public SubmoduleDeinitCommand setForce(boolean force) {
247 		this.force = force;
248 		return this;
249 	}
250 
251 	/**
252 	 * The user tried to deinitialize a submodule that doesn't exist in the
253 	 * index.
254 	 */
255 	public static class NoSuchSubmoduleException extends GitAPIException {
256 		private static final long serialVersionUID = 1L;
257 
258 		/**
259 		 * Constructor of NoSuchSubmoduleException
260 		 *
261 		 * @param path
262 		 *            path of non-existing submodule
263 		 */
264 		public NoSuchSubmoduleException(String path) {
265 			super(MessageFormat.format(JGitText.get().noSuchSubmodule, path));
266 		}
267 	}
268 
269 	/**
270 	 * The effect of a submodule deinit command for a given path
271 	 */
272 	public enum SubmoduleDeinitStatus {
273 		/**
274 		 * The submodule was not initialized in the first place
275 		 */
276 		ALREADY_DEINITIALIZED,
277 		/**
278 		 * The submodule was deinitialized
279 		 */
280 		SUCCESS,
281 		/**
282 		 * The submodule had local changes, but was deinitialized successfully
283 		 */
284 		FORCED,
285 		/**
286 		 * The submodule had local changes and force was false
287 		 */
288 		DIRTY,
289 	}
290 }