diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java')
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java | 297 |
1 files changed, 297 insertions, 0 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java new file mode 100644 index 0000000000..0aa1515334 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleDeinitCommand.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2017, Two Sigma Open Source and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.api; + +import static org.eclipse.jgit.util.FileUtils.RECURSIVE; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidConfigurationException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.submodule.SubmoduleWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; +import org.eclipse.jgit.treewalk.filter.TreeFilter; +import org.eclipse.jgit.util.FileUtils; + +/** + * A class used to execute a submodule deinit command. + * <p> + * This will remove the module(s) from the working tree, but won't affect + * .git/modules. + * + * @since 4.10 + * @see <a href= + * "http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html" + * >Git documentation about submodules</a> + */ +public class SubmoduleDeinitCommand + extends GitCommand<Collection<SubmoduleDeinitResult>> { + + private final Collection<String> paths; + + private boolean force; + + /** + * Constructor of SubmoduleDeinitCommand + * + * @param repo + * repository this command works on + */ + public SubmoduleDeinitCommand(Repository repo) { + super(repo); + paths = new ArrayList<>(); + } + + /** + * {@inheritDoc} + * + * @return the set of repositories successfully deinitialized. + * @throws NoSuchSubmoduleException + * if any of the submodules which we might want to deinitialize + * don't exist + */ + @Override + public Collection<SubmoduleDeinitResult> call() throws GitAPIException { + checkCallable(); + try { + if (paths.isEmpty()) { + return Collections.emptyList(); + } + for (String path : paths) { + if (!submoduleExists(path)) { + throw new NoSuchSubmoduleException(path); + } + } + List<SubmoduleDeinitResult> results = new ArrayList<>(paths.size()); + try (RevWalk revWalk = new RevWalk(repo); + SubmoduleWalk generator = SubmoduleWalk.forIndex(repo)) { + generator.setFilter(PathFilterGroup.createFromStrings(paths)); + StoredConfig config = repo.getConfig(); + while (generator.next()) { + String path = generator.getPath(); + String name = generator.getModuleName(); + SubmoduleDeinitStatus status = checkDirty(revWalk, path); + switch (status) { + case SUCCESS: + deinit(path); + break; + case ALREADY_DEINITIALIZED: + break; + case DIRTY: + if (force) { + deinit(path); + status = SubmoduleDeinitStatus.FORCED; + } + break; + default: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().unexpectedSubmoduleStatus, + status)); + } + + config.unsetSection( + ConfigConstants.CONFIG_SUBMODULE_SECTION, name); + results.add(new SubmoduleDeinitResult(path, status)); + } + } + return results; + } catch (ConfigInvalidException e) { + throw new InvalidConfigurationException(e.getMessage(), e); + } catch (IOException e) { + throw new JGitInternalException(e.getMessage(), e); + } + } + + /** + * Recursively delete the *contents* of path, but leave path as an empty + * directory + * + * @param path + * the path to clean + * @throws IOException + * if an IO error occurred + */ + private void deinit(String path) throws IOException { + File dir = new File(repo.getWorkTree(), path); + if (!dir.isDirectory()) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().expectedDirectoryNotSubmodule, path)); + } + final File[] ls = dir.listFiles(); + if (ls != null) { + for (File f : ls) { + FileUtils.delete(f, RECURSIVE); + } + } + } + + /** + * Check if a submodule is dirty. A submodule is dirty if there are local + * changes to the submodule relative to its HEAD, including untracked files. + * It is also dirty if the HEAD of the submodule does not match the value in + * the parent repo's index or HEAD. + * + * @param revWalk + * used to walk commit graph + * @param path + * path of the submodule + * @return status of the command + * @throws GitAPIException + * if JGit API failed + * @throws IOException + * if an IO error occurred + */ + private SubmoduleDeinitStatus checkDirty(RevWalk revWalk, String path) + throws GitAPIException, IOException { + Ref head = repo.exactRef("HEAD"); //$NON-NLS-1$ + if (head == null) { + throw new NoHeadException( + JGitText.get().invalidRepositoryStateNoHead); + } + RevCommit headCommit = revWalk.parseCommit(head.getObjectId()); + RevTree tree = headCommit.getTree(); + + ObjectId submoduleHead; + try (SubmoduleWalk w = SubmoduleWalk.forPath(repo, tree, path)) { + submoduleHead = w.getHead(); + if (submoduleHead == null) { + // The submodule is not checked out. + return SubmoduleDeinitStatus.ALREADY_DEINITIALIZED; + } + if (!submoduleHead.equals(w.getObjectId())) { + // The submodule's current HEAD doesn't match the value in the + // outer repo's HEAD. + return SubmoduleDeinitStatus.DIRTY; + } + } + + try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) { + if (!w.next()) { + // The submodule does not exist in the index (shouldn't happen + // since we check this earlier) + return SubmoduleDeinitStatus.DIRTY; + } + if (!submoduleHead.equals(w.getObjectId())) { + // The submodule's current HEAD doesn't match the value in the + // outer repo's index. + return SubmoduleDeinitStatus.DIRTY; + } + + try (Repository submoduleRepo = w.getRepository()) { + Status status = Git.wrap(submoduleRepo).status().call(); + return status.isClean() ? SubmoduleDeinitStatus.SUCCESS + : SubmoduleDeinitStatus.DIRTY; + } + } + } + + /** + * Check if this path is a submodule by checking the index, which is what + * git submodule deinit checks. + * + * @param path + * path of the submodule + * + * @return {@code true} if path exists and is a submodule in index, + * {@code false} otherwise + * @throws IOException + * if an IO error occurred + */ + private boolean submoduleExists(String path) throws IOException { + TreeFilter filter = PathFilter.create(path); + try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) { + return w.setFilter(filter).next(); + } + } + + /** + * Add repository-relative submodule path to deinitialize + * + * @param path + * (with <code>/</code> as separator) + * @return this command + */ + public SubmoduleDeinitCommand addPath(String path) { + paths.add(path); + return this; + } + + /** + * If {@code true}, call() will deinitialize modules with local changes; + * else it will refuse to do so. + * + * @param force + * execute the command forcefully if there are local modifications + * @return {@code this} + */ + public SubmoduleDeinitCommand setForce(boolean force) { + this.force = force; + return this; + } + + /** + * The user tried to deinitialize a submodule that doesn't exist in the + * index. + */ + public static class NoSuchSubmoduleException extends GitAPIException { + private static final long serialVersionUID = 1L; + + /** + * Constructor of NoSuchSubmoduleException + * + * @param path + * path of non-existing submodule + */ + public NoSuchSubmoduleException(String path) { + super(MessageFormat.format(JGitText.get().noSuchSubmodule, path)); + } + } + + /** + * The effect of a submodule deinit command for a given path + */ + public enum SubmoduleDeinitStatus { + /** + * The submodule was not initialized in the first place + */ + ALREADY_DEINITIALIZED, + /** + * The submodule was deinitialized + */ + SUCCESS, + /** + * The submodule had local changes, but was deinitialized successfully + */ + FORCED, + /** + * The submodule had local changes and force was false + */ + DIRTY, + } +} |