View Javadoc
1   /*
2    * Copyright (C) 2017, Two Sigma Open Source
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.api;
44  
45  import static org.eclipse.jgit.util.FileUtils.RECURSIVE;
46  
47  import java.io.File;
48  import java.io.IOException;
49  import java.text.MessageFormat;
50  import java.util.ArrayList;
51  import java.util.Collection;
52  import java.util.Collections;
53  import java.util.List;
54  
55  import org.eclipse.jgit.api.errors.GitAPIException;
56  import org.eclipse.jgit.api.errors.JGitInternalException;
57  import org.eclipse.jgit.api.errors.NoHeadException;
58  import org.eclipse.jgit.internal.JGitText;
59  import org.eclipse.jgit.lib.ConfigConstants;
60  import org.eclipse.jgit.lib.ObjectId;
61  import org.eclipse.jgit.lib.Ref;
62  import org.eclipse.jgit.lib.Repository;
63  import org.eclipse.jgit.lib.StoredConfig;
64  import org.eclipse.jgit.revwalk.RevCommit;
65  import org.eclipse.jgit.revwalk.RevTree;
66  import org.eclipse.jgit.revwalk.RevWalk;
67  import org.eclipse.jgit.submodule.SubmoduleWalk;
68  import org.eclipse.jgit.treewalk.filter.PathFilter;
69  import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
70  import org.eclipse.jgit.treewalk.filter.TreeFilter;
71  import org.eclipse.jgit.util.FileUtils;
72  
73  /**
74   * A class used to execute a submodule deinit command.
75   * <p>
76   * This will remove the module(s) from the working tree, but won't affect
77   * .git/modules.
78   *
79   * @since 4.10
80   * @see <a href=
81   *      "http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html"
82   *      >Git documentation about submodules</a>
83   */
84  public class SubmoduleDeinitCommand
85  		extends GitCommand<Collection<SubmoduleDeinitResult>> {
86  
87  	private final Collection<String> paths;
88  
89  	private boolean force;
90  
91  	/**
92  	 * Constructor of SubmoduleDeinitCommand
93  	 *
94  	 * @param repo
95  	 */
96  	public SubmoduleDeinitCommand(Repository repo) {
97  		super(repo);
98  		paths = new ArrayList<>();
99  	}
100 
101 	/**
102 	 * {@inheritDoc}
103 	 * <p>
104 	 *
105 	 * @return the set of repositories successfully deinitialized.
106 	 * @throws NoSuchSubmoduleException
107 	 *             if any of the submodules which we might want to deinitialize
108 	 *             don't exist
109 	 */
110 	@Override
111 	public Collection<SubmoduleDeinitResult> call() throws GitAPIException {
112 		checkCallable();
113 		try {
114 			if (paths.isEmpty()) {
115 				return Collections.emptyList();
116 			}
117 			for (String path : paths) {
118 				if (!submoduleExists(path)) {
119 					throw new NoSuchSubmoduleException(path);
120 				}
121 			}
122 			List<SubmoduleDeinitResult> results = new ArrayList<>(paths.size());
123 			try (RevWalklk.html#RevWalk">RevWalk revWalk = new RevWalk(repo);
124 					SubmoduleWalk generator = SubmoduleWalk.forIndex(repo)) {
125 				generator.setFilter(PathFilterGroup.createFromStrings(paths));
126 				StoredConfig config = repo.getConfig();
127 				while (generator.next()) {
128 					String path = generator.getPath();
129 					String name = generator.getModuleName();
130 					SubmoduleDeinitStatus status = checkDirty(revWalk, path);
131 					switch (status) {
132 					case SUCCESS:
133 						deinit(path);
134 						break;
135 					case ALREADY_DEINITIALIZED:
136 						break;
137 					case DIRTY:
138 						if (force) {
139 							deinit(path);
140 							status = SubmoduleDeinitStatus.FORCED;
141 						}
142 						break;
143 					default:
144 						throw new JGitInternalException(MessageFormat.format(
145 								JGitText.get().unexpectedSubmoduleStatus,
146 								status));
147 					}
148 
149 					config.unsetSection(
150 							ConfigConstants.CONFIG_SUBMODULE_SECTION, name);
151 					results.add(new SubmoduleDeinitResult(path, status));
152 				}
153 			}
154 			return results;
155 		} catch (IOException e) {
156 			throw new JGitInternalException(e.getMessage(), e);
157 		}
158 	}
159 
160 	/**
161 	 * Recursively delete the *contents* of path, but leave path as an empty
162 	 * directory
163 	 *
164 	 * @param path
165 	 *            the path to clean
166 	 * @throws IOException
167 	 */
168 	private void deinit(String path) throws IOException {
169 		File dir = new File(repo.getWorkTree(), path);
170 		if (!dir.isDirectory()) {
171 			throw new JGitInternalException(MessageFormat.format(
172 					JGitText.get().expectedDirectoryNotSubmodule, path));
173 		}
174 		final File[] ls = dir.listFiles();
175 		if (ls != null) {
176 			for (int i = 0; i < ls.length; i++) {
177 				FileUtils.delete(ls[i], RECURSIVE);
178 			}
179 		}
180 	}
181 
182 	/**
183 	 * Check if a submodule is dirty. A submodule is dirty if there are local
184 	 * changes to the submodule relative to its HEAD, including untracked files.
185 	 * It is also dirty if the HEAD of the submodule does not match the value in
186 	 * the parent repo's index or HEAD.
187 	 *
188 	 * @param revWalk
189 	 * @param path
190 	 * @return status of the command
191 	 * @throws GitAPIException
192 	 * @throws IOException
193 	 */
194 	private SubmoduleDeinitStatus checkDirty(RevWalk revWalk, String path)
195 			throws GitAPIException, IOException {
196 		Ref head = repo.exactRef("HEAD"); //$NON-NLS-1$
197 		if (head == null) {
198 			throw new NoHeadException(
199 					JGitText.get().invalidRepositoryStateNoHead);
200 		}
201 		RevCommit headCommit = revWalk.parseCommit(head.getObjectId());
202 		RevTree tree = headCommit.getTree();
203 
204 		ObjectId submoduleHead;
205 		try (SubmoduleWalk w = SubmoduleWalk.forPath(repo, tree, path)) {
206 			submoduleHead = w.getHead();
207 			if (submoduleHead == null) {
208 				// The submodule is not checked out.
209 				return SubmoduleDeinitStatus.ALREADY_DEINITIALIZED;
210 			}
211 			if (!submoduleHead.equals(w.getObjectId())) {
212 				// The submodule's current HEAD doesn't match the value in the
213 				// outer repo's HEAD.
214 				return SubmoduleDeinitStatus.DIRTY;
215 			}
216 		}
217 
218 		try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) {
219 			if (!w.next()) {
220 				// The submodule does not exist in the index (shouldn't happen
221 				// since we check this earlier)
222 				return SubmoduleDeinitStatus.DIRTY;
223 			}
224 			if (!submoduleHead.equals(w.getObjectId())) {
225 				// The submodule's current HEAD doesn't match the value in the
226 				// outer repo's index.
227 				return SubmoduleDeinitStatus.DIRTY;
228 			}
229 
230 			try (Repository submoduleRepo = w.getRepository()) {
231 				Status status = Git.wrap(submoduleRepo).status().call();
232 				return status.isClean() ? SubmoduleDeinitStatus.SUCCESS
233 						: SubmoduleDeinitStatus.DIRTY;
234 			}
235 		}
236 	}
237 
238 	/**
239 	 * Check if this path is a submodule by checking the index, which is what
240 	 * git submodule deinit checks.
241 	 *
242 	 * @param path
243 	 *            path of the submodule
244 	 *
245 	 * @return {@code true} if path exists and is a submodule in index,
246 	 *         {@code false} otherwise
247 	 * @throws IOException
248 	 */
249 	private boolean submoduleExists(String path) throws IOException {
250 		TreeFilter filter = PathFilter.create(path);
251 		try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) {
252 			return w.setFilter(filter).next();
253 		}
254 	}
255 
256 	/**
257 	 * Add repository-relative submodule path to deinitialize
258 	 *
259 	 * @param path
260 	 *            (with <code>/</code> as separator)
261 	 * @return this command
262 	 */
263 	public SubmoduleDeinitCommand addPath(String path) {
264 		paths.add(path);
265 		return this;
266 	}
267 
268 	/**
269 	 * If {@code true}, call() will deinitialize modules with local changes;
270 	 * else it will refuse to do so.
271 	 *
272 	 * @param force
273 	 * @return {@code this}
274 	 */
275 	public SubmoduleDeinitCommand setForce(boolean force) {
276 		this.force = force;
277 		return this;
278 	}
279 
280 	/**
281 	 * The user tried to deinitialize a submodule that doesn't exist in the
282 	 * index.
283 	 */
284 	public static class NoSuchSubmoduleException extends GitAPIException {
285 		private static final long serialVersionUID = 1L;
286 
287 		/**
288 		 * Constructor of NoSuchSubmoduleException
289 		 *
290 		 * @param path
291 		 *            path of non-existing submodule
292 		 */
293 		public NoSuchSubmoduleException(String path) {
294 			super(MessageFormat.format(JGitText.get().noSuchSubmodule, path));
295 		}
296 	}
297 
298 	/**
299 	 * The effect of a submodule deinit command for a given path
300 	 */
301 	public enum SubmoduleDeinitStatus {
302 		/**
303 		 * The submodule was not initialized in the first place
304 		 */
305 		ALREADY_DEINITIALIZED,
306 		/**
307 		 * The submodule was deinitialized
308 		 */
309 		SUCCESS,
310 		/**
311 		 * The submodule had local changes, but was deinitialized successfully
312 		 */
313 		FORCED,
314 		/**
315 		 * The submodule had local changes and force was false
316 		 */
317 		DIRTY,
318 	}
319 }