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