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