You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SubmoduleDeinitCommand.java 8.1KB

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