CleanCommand.java

  1. /*
  2.  * Copyright (C) 2011, Chris Aniszczyk <zx@redhat.com>
  3.  * Copyright (C) 2011, Abhishek Bhatnagar <abhatnag@redhat.com> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */
  11. package org.eclipse.jgit.api;

  12. import static org.eclipse.jgit.lib.Constants.DOT_GIT;

  13. import java.io.File;
  14. import java.io.IOException;
  15. import java.util.Collections;
  16. import java.util.Set;
  17. import java.util.TreeSet;

  18. import org.eclipse.jgit.api.errors.GitAPIException;
  19. import org.eclipse.jgit.api.errors.JGitInternalException;
  20. import org.eclipse.jgit.errors.NoWorkTreeException;
  21. import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
  22. import org.eclipse.jgit.lib.Repository;
  23. import org.eclipse.jgit.util.FS;
  24. import org.eclipse.jgit.util.FileUtils;
  25. import org.eclipse.jgit.util.Paths;

  26. /**
  27.  * Remove untracked files from the working tree
  28.  *
  29.  * @see <a
  30.  *      href="http://www.kernel.org/pub/software/scm/git/docs/git-clean.html"
  31.  *      >Git documentation about Clean</a>
  32.  */
  33. public class CleanCommand extends GitCommand<Set<String>> {

  34.     private Set<String> paths = Collections.emptySet();

  35.     private boolean dryRun;

  36.     private boolean directories;

  37.     private boolean ignore = true;

  38.     private boolean force = false;

  39.     /**
  40.      * Constructor for CleanCommand
  41.      *
  42.      * @param repo
  43.      *            the {@link org.eclipse.jgit.lib.Repository}
  44.      */
  45.     protected CleanCommand(Repository repo) {
  46.         super(repo);
  47.     }

  48.     /**
  49.      * {@inheritDoc}
  50.      * <p>
  51.      * Executes the {@code clean} command with all the options and parameters
  52.      * collected by the setter methods of this class. Each instance of this
  53.      * class should only be used for one invocation of the command (means: one
  54.      * call to {@link #call()})
  55.      */
  56.     @Override
  57.     public Set<String> call() throws NoWorkTreeException, GitAPIException {
  58.         Set<String> files = new TreeSet<>();
  59.         try {
  60.             StatusCommand command = new StatusCommand(repo);
  61.             Status status = command.call();

  62.             Set<String> untrackedFiles = new TreeSet<>(status.getUntracked());
  63.             Set<String> untrackedDirs = new TreeSet<>(
  64.                     status.getUntrackedFolders());

  65.             FS fs = getRepository().getFS();
  66.             for (String p : status.getIgnoredNotInIndex()) {
  67.                 File f = new File(repo.getWorkTree(), p);
  68.                 if (fs.isFile(f) || fs.isSymLink(f)) {
  69.                     untrackedFiles.add(p);
  70.                 } else if (fs.isDirectory(f)) {
  71.                     untrackedDirs.add(p);
  72.                 }
  73.             }

  74.             Set<String> filtered = filterFolders(untrackedFiles, untrackedDirs);

  75.             Set<String> notIgnoredFiles = filterIgnorePaths(filtered,
  76.                     status.getIgnoredNotInIndex(), true);
  77.             Set<String> notIgnoredDirs = filterIgnorePaths(untrackedDirs,
  78.                     status.getIgnoredNotInIndex(), false);

  79.             for (String file : notIgnoredFiles) {
  80.                 if (paths.isEmpty() || paths.contains(file)) {
  81.                     files = cleanPath(file, files);
  82.                 }
  83.             }
  84.             for (String dir : notIgnoredDirs) {
  85.                 if (paths.isEmpty() || paths.contains(dir)) {
  86.                     files = cleanPath(dir, files);
  87.                 }
  88.             }
  89.         } catch (IOException e) {
  90.             throw new JGitInternalException(e.getMessage(), e);
  91.         } finally {
  92.             if (!dryRun && !files.isEmpty()) {
  93.                 repo.fireEvent(new WorkingTreeModifiedEvent(null, files));
  94.             }
  95.         }
  96.         return files;
  97.     }

  98.     /**
  99.      * When dryRun is false, deletes the specified path from disk. If dryRun
  100.      * is true, no paths are actually deleted. In both cases, the paths that
  101.      * would have been deleted are added to inFiles and returned.
  102.      *
  103.      * Paths that are directories are recursively deleted when
  104.      * {@link #directories} is true.
  105.      * Paths that are git repositories are recursively deleted when
  106.      * {@link #directories} and {@link #force} are both true.
  107.      *
  108.      * @param path
  109.      *          The path to be cleaned
  110.      * @param inFiles
  111.      *          A set of strings representing the files that have been cleaned
  112.      *          already, the path to be cleaned will be added to this set
  113.      *          before being returned.
  114.      *
  115.      * @return a set of strings with the cleaned path added to it
  116.      * @throws IOException
  117.      */
  118.     private Set<String> cleanPath(String path, Set<String> inFiles)
  119.             throws IOException {
  120.         File curFile = new File(repo.getWorkTree(), path);
  121.         if (curFile.isDirectory()) {
  122.             if (directories) {
  123.                 // Is this directory a git repository?
  124.                 if (new File(curFile, DOT_GIT).exists()) {
  125.                     if (force) {
  126.                         if (!dryRun) {
  127.                             FileUtils.delete(curFile, FileUtils.RECURSIVE
  128.                                     | FileUtils.SKIP_MISSING);
  129.                         }
  130.                         inFiles.add(path + '/');
  131.                     }
  132.                 } else {
  133.                     if (!dryRun) {
  134.                         FileUtils.delete(curFile,
  135.                                 FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
  136.                     }
  137.                     inFiles.add(path + '/');
  138.                 }
  139.             }
  140.         } else {
  141.             if (!dryRun) {
  142.                 FileUtils.delete(curFile, FileUtils.SKIP_MISSING);
  143.             }
  144.             inFiles.add(path);
  145.         }

  146.         return inFiles;
  147.     }

  148.     private Set<String> filterIgnorePaths(Set<String> inputPaths,
  149.             Set<String> ignoredNotInIndex, boolean exact) {
  150.         if (ignore) {
  151.             Set<String> filtered = new TreeSet<>(inputPaths);
  152.             for (String path : inputPaths) {
  153.                 for (String ignored : ignoredNotInIndex) {
  154.                     if ((exact && path.equals(ignored))
  155.                             || (!exact
  156.                                     && Paths.isEqualOrPrefix(ignored, path))) {
  157.                         filtered.remove(path);
  158.                         break;
  159.                     }
  160.                 }
  161.             }
  162.             return filtered;
  163.         }
  164.         return inputPaths;
  165.     }

  166.     private Set<String> filterFolders(Set<String> untracked,
  167.             Set<String> untrackedFolders) {
  168.         Set<String> filtered = new TreeSet<>(untracked);
  169.         for (String file : untracked) {
  170.             for (String folder : untrackedFolders) {
  171.                 if (Paths.isEqualOrPrefix(folder, file)) {
  172.                     filtered.remove(file);
  173.                     break;
  174.                 }
  175.             }
  176.         }
  177.         return filtered;
  178.     }

  179.     /**
  180.      * If paths are set, only these paths are affected by the cleaning.
  181.      *
  182.      * @param paths
  183.      *            the paths to set (with <code>/</code> as separator)
  184.      * @return {@code this}
  185.      */
  186.     public CleanCommand setPaths(Set<String> paths) {
  187.         this.paths = paths;
  188.         return this;
  189.     }

  190.     /**
  191.      * If dryRun is set, the paths in question will not actually be deleted.
  192.      *
  193.      * @param dryRun
  194.      *            whether to do a dry run or not
  195.      * @return {@code this}
  196.      */
  197.     public CleanCommand setDryRun(boolean dryRun) {
  198.         this.dryRun = dryRun;
  199.         return this;
  200.     }

  201.     /**
  202.      * If force is set, directories that are git repositories will also be
  203.      * deleted.
  204.      *
  205.      * @param force
  206.      *            whether or not to delete git repositories
  207.      * @return {@code this}
  208.      * @since 4.5
  209.      */
  210.     public CleanCommand setForce(boolean force) {
  211.         this.force = force;
  212.         return this;
  213.     }

  214.     /**
  215.      * If dirs is set, in addition to files, also clean directories.
  216.      *
  217.      * @param dirs
  218.      *            whether to clean directories too, or only files.
  219.      * @return {@code this}
  220.      */
  221.     public CleanCommand setCleanDirectories(boolean dirs) {
  222.         directories = dirs;
  223.         return this;
  224.     }

  225.     /**
  226.      * If ignore is set, don't report/clean files/directories that are ignored
  227.      * by a .gitignore. otherwise do handle them.
  228.      *
  229.      * @param ignore
  230.      *            whether to respect .gitignore or not.
  231.      * @return {@code this}
  232.      */
  233.     public CleanCommand setIgnore(boolean ignore) {
  234.         this.ignore = ignore;
  235.         return this;
  236.     }
  237. }