xref: /JGit/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java (revision f4b4dae2be1f6bb7e09a0c660e7fc8e093fb0617)
1243fba9aSDavid Turner /*
25c5f7c6bSMatthias Sohn  * Copyright (C) 2017, Two Sigma Open Source and others
3243fba9aSDavid Turner  *
45c5f7c6bSMatthias Sohn  * This program and the accompanying materials are made available under the
55c5f7c6bSMatthias Sohn  * terms of the Eclipse Distribution License v. 1.0 which is available at
65c5f7c6bSMatthias Sohn  * https://www.eclipse.org/org/documents/edl-v10.php.
7243fba9aSDavid Turner  *
85c5f7c6bSMatthias Sohn  * SPDX-License-Identifier: BSD-3-Clause
9243fba9aSDavid Turner  */
10243fba9aSDavid Turner package org.eclipse.jgit.api;
11243fba9aSDavid Turner 
12243fba9aSDavid Turner import static org.eclipse.jgit.util.FileUtils.RECURSIVE;
13243fba9aSDavid Turner 
14243fba9aSDavid Turner import java.io.File;
15243fba9aSDavid Turner import java.io.IOException;
16243fba9aSDavid Turner import java.text.MessageFormat;
17243fba9aSDavid Turner import java.util.ArrayList;
18243fba9aSDavid Turner import java.util.Collection;
19243fba9aSDavid Turner import java.util.Collections;
20243fba9aSDavid Turner import java.util.List;
21243fba9aSDavid Turner 
22243fba9aSDavid Turner import org.eclipse.jgit.api.errors.GitAPIException;
23*f4b4dae2SJohn Dallaway import org.eclipse.jgit.api.errors.InvalidConfigurationException;
24243fba9aSDavid Turner import org.eclipse.jgit.api.errors.JGitInternalException;
25243fba9aSDavid Turner import org.eclipse.jgit.api.errors.NoHeadException;
26*f4b4dae2SJohn Dallaway import org.eclipse.jgit.errors.ConfigInvalidException;
27243fba9aSDavid Turner import org.eclipse.jgit.internal.JGitText;
28243fba9aSDavid Turner import org.eclipse.jgit.lib.ConfigConstants;
29243fba9aSDavid Turner import org.eclipse.jgit.lib.ObjectId;
30243fba9aSDavid Turner import org.eclipse.jgit.lib.Ref;
31243fba9aSDavid Turner import org.eclipse.jgit.lib.Repository;
32243fba9aSDavid Turner import org.eclipse.jgit.lib.StoredConfig;
33243fba9aSDavid Turner import org.eclipse.jgit.revwalk.RevCommit;
34243fba9aSDavid Turner import org.eclipse.jgit.revwalk.RevTree;
35243fba9aSDavid Turner import org.eclipse.jgit.revwalk.RevWalk;
36243fba9aSDavid Turner import org.eclipse.jgit.submodule.SubmoduleWalk;
37243fba9aSDavid Turner import org.eclipse.jgit.treewalk.filter.PathFilter;
38243fba9aSDavid Turner import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
39243fba9aSDavid Turner import org.eclipse.jgit.treewalk.filter.TreeFilter;
40243fba9aSDavid Turner import org.eclipse.jgit.util.FileUtils;
41243fba9aSDavid Turner 
42243fba9aSDavid Turner /**
43243fba9aSDavid Turner  * A class used to execute a submodule deinit command.
44243fba9aSDavid Turner  * <p>
45243fba9aSDavid Turner  * This will remove the module(s) from the working tree, but won't affect
46243fba9aSDavid Turner  * .git/modules.
47243fba9aSDavid Turner  *
48243fba9aSDavid Turner  * @since 4.10
49243fba9aSDavid Turner  * @see <a href=
50243fba9aSDavid Turner  *      "http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html"
51243fba9aSDavid Turner  *      >Git documentation about submodules</a>
52243fba9aSDavid Turner  */
53243fba9aSDavid Turner public class SubmoduleDeinitCommand
54243fba9aSDavid Turner 		extends GitCommand<Collection<SubmoduleDeinitResult>> {
55243fba9aSDavid Turner 
56243fba9aSDavid Turner 	private final Collection<String> paths;
57243fba9aSDavid Turner 
58243fba9aSDavid Turner 	private boolean force;
59243fba9aSDavid Turner 
60243fba9aSDavid Turner 	/**
61243fba9aSDavid Turner 	 * Constructor of SubmoduleDeinitCommand
62243fba9aSDavid Turner 	 *
63243fba9aSDavid Turner 	 * @param repo
64243fba9aSDavid Turner 	 */
SubmoduleDeinitCommand(Repository repo)65243fba9aSDavid Turner 	public SubmoduleDeinitCommand(Repository repo) {
66243fba9aSDavid Turner 		super(repo);
67243fba9aSDavid Turner 		paths = new ArrayList<>();
68243fba9aSDavid Turner 	}
69243fba9aSDavid Turner 
70243fba9aSDavid Turner 	/**
71243fba9aSDavid Turner 	 * {@inheritDoc}
72243fba9aSDavid Turner 	 * <p>
73243fba9aSDavid Turner 	 *
74243fba9aSDavid Turner 	 * @return the set of repositories successfully deinitialized.
75243fba9aSDavid Turner 	 * @throws NoSuchSubmoduleException
76243fba9aSDavid Turner 	 *             if any of the submodules which we might want to deinitialize
77243fba9aSDavid Turner 	 *             don't exist
78243fba9aSDavid Turner 	 */
79243fba9aSDavid Turner 	@Override
call()80243fba9aSDavid Turner 	public Collection<SubmoduleDeinitResult> call() throws GitAPIException {
81243fba9aSDavid Turner 		checkCallable();
82243fba9aSDavid Turner 		try {
83243fba9aSDavid Turner 			if (paths.isEmpty()) {
84243fba9aSDavid Turner 				return Collections.emptyList();
85243fba9aSDavid Turner 			}
86243fba9aSDavid Turner 			for (String path : paths) {
87243fba9aSDavid Turner 				if (!submoduleExists(path)) {
88243fba9aSDavid Turner 					throw new NoSuchSubmoduleException(path);
89243fba9aSDavid Turner 				}
90243fba9aSDavid Turner 			}
91243fba9aSDavid Turner 			List<SubmoduleDeinitResult> results = new ArrayList<>(paths.size());
92243fba9aSDavid Turner 			try (RevWalk revWalk = new RevWalk(repo);
93243fba9aSDavid Turner 					SubmoduleWalk generator = SubmoduleWalk.forIndex(repo)) {
94243fba9aSDavid Turner 				generator.setFilter(PathFilterGroup.createFromStrings(paths));
95243fba9aSDavid Turner 				StoredConfig config = repo.getConfig();
96243fba9aSDavid Turner 				while (generator.next()) {
97243fba9aSDavid Turner 					String path = generator.getPath();
98243fba9aSDavid Turner 					String name = generator.getModuleName();
99243fba9aSDavid Turner 					SubmoduleDeinitStatus status = checkDirty(revWalk, path);
100243fba9aSDavid Turner 					switch (status) {
101243fba9aSDavid Turner 					case SUCCESS:
102243fba9aSDavid Turner 						deinit(path);
103243fba9aSDavid Turner 						break;
104243fba9aSDavid Turner 					case ALREADY_DEINITIALIZED:
105243fba9aSDavid Turner 						break;
106243fba9aSDavid Turner 					case DIRTY:
107243fba9aSDavid Turner 						if (force) {
108243fba9aSDavid Turner 							deinit(path);
109243fba9aSDavid Turner 							status = SubmoduleDeinitStatus.FORCED;
110243fba9aSDavid Turner 						}
111243fba9aSDavid Turner 						break;
112243fba9aSDavid Turner 					default:
113243fba9aSDavid Turner 						throw new JGitInternalException(MessageFormat.format(
114243fba9aSDavid Turner 								JGitText.get().unexpectedSubmoduleStatus,
115243fba9aSDavid Turner 								status));
116243fba9aSDavid Turner 					}
117243fba9aSDavid Turner 
118243fba9aSDavid Turner 					config.unsetSection(
119243fba9aSDavid Turner 							ConfigConstants.CONFIG_SUBMODULE_SECTION, name);
120243fba9aSDavid Turner 					results.add(new SubmoduleDeinitResult(path, status));
121243fba9aSDavid Turner 				}
122243fba9aSDavid Turner 			}
123243fba9aSDavid Turner 			return results;
124*f4b4dae2SJohn Dallaway 		} catch (ConfigInvalidException e) {
125*f4b4dae2SJohn Dallaway 			throw new InvalidConfigurationException(e.getMessage(), e);
126243fba9aSDavid Turner 		} catch (IOException e) {
127243fba9aSDavid Turner 			throw new JGitInternalException(e.getMessage(), e);
128243fba9aSDavid Turner 		}
129243fba9aSDavid Turner 	}
130243fba9aSDavid Turner 
131243fba9aSDavid Turner 	/**
132243fba9aSDavid Turner 	 * Recursively delete the *contents* of path, but leave path as an empty
133243fba9aSDavid Turner 	 * directory
134243fba9aSDavid Turner 	 *
135243fba9aSDavid Turner 	 * @param path
136243fba9aSDavid Turner 	 *            the path to clean
137243fba9aSDavid Turner 	 * @throws IOException
138243fba9aSDavid Turner 	 */
deinit(String path)139243fba9aSDavid Turner 	private void deinit(String path) throws IOException {
140243fba9aSDavid Turner 		File dir = new File(repo.getWorkTree(), path);
141243fba9aSDavid Turner 		if (!dir.isDirectory()) {
142243fba9aSDavid Turner 			throw new JGitInternalException(MessageFormat.format(
143243fba9aSDavid Turner 					JGitText.get().expectedDirectoryNotSubmodule, path));
144243fba9aSDavid Turner 		}
145243fba9aSDavid Turner 		final File[] ls = dir.listFiles();
146243fba9aSDavid Turner 		if (ls != null) {
1476e03645aSCarsten Hammer 			for (File f : ls) {
1486e03645aSCarsten Hammer 				FileUtils.delete(f, RECURSIVE);
149243fba9aSDavid Turner 			}
150243fba9aSDavid Turner 		}
151243fba9aSDavid Turner 	}
152243fba9aSDavid Turner 
153243fba9aSDavid Turner 	/**
154243fba9aSDavid Turner 	 * Check if a submodule is dirty. A submodule is dirty if there are local
155243fba9aSDavid Turner 	 * changes to the submodule relative to its HEAD, including untracked files.
156243fba9aSDavid Turner 	 * It is also dirty if the HEAD of the submodule does not match the value in
157243fba9aSDavid Turner 	 * the parent repo's index or HEAD.
158243fba9aSDavid Turner 	 *
159243fba9aSDavid Turner 	 * @param revWalk
160243fba9aSDavid Turner 	 * @param path
161243fba9aSDavid Turner 	 * @return status of the command
162243fba9aSDavid Turner 	 * @throws GitAPIException
163243fba9aSDavid Turner 	 * @throws IOException
164243fba9aSDavid Turner 	 */
checkDirty(RevWalk revWalk, String path)165243fba9aSDavid Turner 	private SubmoduleDeinitStatus checkDirty(RevWalk revWalk, String path)
166243fba9aSDavid Turner 			throws GitAPIException, IOException {
167243fba9aSDavid Turner 		Ref head = repo.exactRef("HEAD"); //$NON-NLS-1$
168243fba9aSDavid Turner 		if (head == null) {
169243fba9aSDavid Turner 			throw new NoHeadException(
170243fba9aSDavid Turner 					JGitText.get().invalidRepositoryStateNoHead);
171243fba9aSDavid Turner 		}
172243fba9aSDavid Turner 		RevCommit headCommit = revWalk.parseCommit(head.getObjectId());
173243fba9aSDavid Turner 		RevTree tree = headCommit.getTree();
174243fba9aSDavid Turner 
175243fba9aSDavid Turner 		ObjectId submoduleHead;
176243fba9aSDavid Turner 		try (SubmoduleWalk w = SubmoduleWalk.forPath(repo, tree, path)) {
177243fba9aSDavid Turner 			submoduleHead = w.getHead();
178243fba9aSDavid Turner 			if (submoduleHead == null) {
179243fba9aSDavid Turner 				// The submodule is not checked out.
180243fba9aSDavid Turner 				return SubmoduleDeinitStatus.ALREADY_DEINITIALIZED;
181243fba9aSDavid Turner 			}
182243fba9aSDavid Turner 			if (!submoduleHead.equals(w.getObjectId())) {
183243fba9aSDavid Turner 				// The submodule's current HEAD doesn't match the value in the
184243fba9aSDavid Turner 				// outer repo's HEAD.
185243fba9aSDavid Turner 				return SubmoduleDeinitStatus.DIRTY;
186243fba9aSDavid Turner 			}
187243fba9aSDavid Turner 		}
188243fba9aSDavid Turner 
189243fba9aSDavid Turner 		try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) {
190243fba9aSDavid Turner 			if (!w.next()) {
191243fba9aSDavid Turner 				// The submodule does not exist in the index (shouldn't happen
192243fba9aSDavid Turner 				// since we check this earlier)
193243fba9aSDavid Turner 				return SubmoduleDeinitStatus.DIRTY;
194243fba9aSDavid Turner 			}
195243fba9aSDavid Turner 			if (!submoduleHead.equals(w.getObjectId())) {
196243fba9aSDavid Turner 				// The submodule's current HEAD doesn't match the value in the
197243fba9aSDavid Turner 				// outer repo's index.
198243fba9aSDavid Turner 				return SubmoduleDeinitStatus.DIRTY;
199243fba9aSDavid Turner 			}
200243fba9aSDavid Turner 
2015a95e7e7SAndrey Loskutov 			try (Repository submoduleRepo = w.getRepository()) {
202243fba9aSDavid Turner 				Status status = Git.wrap(submoduleRepo).status().call();
203243fba9aSDavid Turner 				return status.isClean() ? SubmoduleDeinitStatus.SUCCESS
204243fba9aSDavid Turner 						: SubmoduleDeinitStatus.DIRTY;
205243fba9aSDavid Turner 			}
206243fba9aSDavid Turner 		}
2075a95e7e7SAndrey Loskutov 	}
208243fba9aSDavid Turner 
209243fba9aSDavid Turner 	/**
210243fba9aSDavid Turner 	 * Check if this path is a submodule by checking the index, which is what
211243fba9aSDavid Turner 	 * git submodule deinit checks.
212243fba9aSDavid Turner 	 *
213243fba9aSDavid Turner 	 * @param path
214243fba9aSDavid Turner 	 *            path of the submodule
215243fba9aSDavid Turner 	 *
216243fba9aSDavid Turner 	 * @return {@code true} if path exists and is a submodule in index,
217243fba9aSDavid Turner 	 *         {@code false} otherwise
218243fba9aSDavid Turner 	 * @throws IOException
219243fba9aSDavid Turner 	 */
submoduleExists(String path)220243fba9aSDavid Turner 	private boolean submoduleExists(String path) throws IOException {
221243fba9aSDavid Turner 		TreeFilter filter = PathFilter.create(path);
222243fba9aSDavid Turner 		try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) {
223243fba9aSDavid Turner 			return w.setFilter(filter).next();
224243fba9aSDavid Turner 		}
225243fba9aSDavid Turner 	}
226243fba9aSDavid Turner 
227243fba9aSDavid Turner 	/**
228243fba9aSDavid Turner 	 * Add repository-relative submodule path to deinitialize
229243fba9aSDavid Turner 	 *
230243fba9aSDavid Turner 	 * @param path
231243fba9aSDavid Turner 	 *            (with <code>/</code> as separator)
232243fba9aSDavid Turner 	 * @return this command
233243fba9aSDavid Turner 	 */
addPath(String path)234243fba9aSDavid Turner 	public SubmoduleDeinitCommand addPath(String path) {
235243fba9aSDavid Turner 		paths.add(path);
236243fba9aSDavid Turner 		return this;
237243fba9aSDavid Turner 	}
238243fba9aSDavid Turner 
239243fba9aSDavid Turner 	/**
240243fba9aSDavid Turner 	 * If {@code true}, call() will deinitialize modules with local changes;
241243fba9aSDavid Turner 	 * else it will refuse to do so.
242243fba9aSDavid Turner 	 *
243243fba9aSDavid Turner 	 * @param force
244243fba9aSDavid Turner 	 * @return {@code this}
245243fba9aSDavid Turner 	 */
setForce(boolean force)246243fba9aSDavid Turner 	public SubmoduleDeinitCommand setForce(boolean force) {
247243fba9aSDavid Turner 		this.force = force;
248243fba9aSDavid Turner 		return this;
249243fba9aSDavid Turner 	}
250243fba9aSDavid Turner 
251243fba9aSDavid Turner 	/**
252243fba9aSDavid Turner 	 * The user tried to deinitialize a submodule that doesn't exist in the
253243fba9aSDavid Turner 	 * index.
254243fba9aSDavid Turner 	 */
255243fba9aSDavid Turner 	public static class NoSuchSubmoduleException extends GitAPIException {
256243fba9aSDavid Turner 		private static final long serialVersionUID = 1L;
257243fba9aSDavid Turner 
258243fba9aSDavid Turner 		/**
259243fba9aSDavid Turner 		 * Constructor of NoSuchSubmoduleException
260243fba9aSDavid Turner 		 *
261243fba9aSDavid Turner 		 * @param path
262243fba9aSDavid Turner 		 *            path of non-existing submodule
263243fba9aSDavid Turner 		 */
NoSuchSubmoduleException(String path)264243fba9aSDavid Turner 		public NoSuchSubmoduleException(String path) {
265243fba9aSDavid Turner 			super(MessageFormat.format(JGitText.get().noSuchSubmodule, path));
266243fba9aSDavid Turner 		}
267243fba9aSDavid Turner 	}
268243fba9aSDavid Turner 
269243fba9aSDavid Turner 	/**
270243fba9aSDavid Turner 	 * The effect of a submodule deinit command for a given path
271243fba9aSDavid Turner 	 */
272243fba9aSDavid Turner 	public enum SubmoduleDeinitStatus {
273243fba9aSDavid Turner 		/**
274243fba9aSDavid Turner 		 * The submodule was not initialized in the first place
275243fba9aSDavid Turner 		 */
276243fba9aSDavid Turner 		ALREADY_DEINITIALIZED,
277243fba9aSDavid Turner 		/**
278243fba9aSDavid Turner 		 * The submodule was deinitialized
279243fba9aSDavid Turner 		 */
280243fba9aSDavid Turner 		SUCCESS,
281243fba9aSDavid Turner 		/**
282243fba9aSDavid Turner 		 * The submodule had local changes, but was deinitialized successfully
283243fba9aSDavid Turner 		 */
284243fba9aSDavid Turner 		FORCED,
285243fba9aSDavid Turner 		/**
286243fba9aSDavid Turner 		 * The submodule had local changes and force was false
287243fba9aSDavid Turner 		 */
288243fba9aSDavid Turner 		DIRTY,
289243fba9aSDavid Turner 	}
290243fba9aSDavid Turner }
291