/* * 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. *

* This will remove the module(s) from the working tree, but won't affect * .git/modules. * * @since 4.10 * @see Git documentation about submodules */ public class SubmoduleDeinitCommand extends GitCommand> { private final Collection paths; private boolean force; /** * Constructor of SubmoduleDeinitCommand * * @param repo */ 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 call() throws GitAPIException { checkCallable(); try { if (paths.isEmpty()) { return Collections.emptyList(); } for (String path : paths) { if (!submoduleExists(path)) { throw new NoSuchSubmoduleException(path); } } List 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 */ 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 * @param path * @return status of the command * @throws GitAPIException * @throws IOException */ 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 */ 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 / 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 * @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, } }