SubmoduleDeinitCommand.java

  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. }