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

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

  34.     private boolean dryRun;

  35.     private boolean directories;

  36.     private boolean ignore = true;

  37.     private boolean force = false;

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

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

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

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

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

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

  78.             for (String file : notIgnoredFiles)
  79.                 if (paths.isEmpty() || paths.contains(file)) {
  80.                     files = cleanPath(file, files);
  81.                 }

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

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

  143.         return inFiles;
  144.     }

  145.     private Set<String> filterIgnorePaths(Set<String> inputPaths,
  146.             Set<String> ignoredNotInIndex, boolean exact) {
  147.         if (ignore) {
  148.             Set<String> filtered = new TreeSet<>(inputPaths);
  149.             for (String path : inputPaths)
  150.                 for (String ignored : ignoredNotInIndex)
  151.                     if ((exact && path.equals(ignored))
  152.                             || (!exact && path.startsWith(ignored))) {
  153.                         filtered.remove(path);
  154.                         break;
  155.                     }

  156.             return filtered;
  157.         }
  158.         return inputPaths;
  159.     }

  160.     private Set<String> filterFolders(Set<String> untracked,
  161.             Set<String> untrackedFolders) {
  162.         Set<String> filtered = new TreeSet<>(untracked);
  163.         for (String file : untracked)
  164.             for (String folder : untrackedFolders)
  165.                 if (file.startsWith(folder)) {
  166.                     filtered.remove(file);
  167.                     break;
  168.                 }


  169.         return filtered;
  170.     }

  171.     /**
  172.      * If paths are set, only these paths are affected by the cleaning.
  173.      *
  174.      * @param paths
  175.      *            the paths to set (with <code>/</code> as separator)
  176.      * @return {@code this}
  177.      */
  178.     public CleanCommand setPaths(Set<String> paths) {
  179.         this.paths = paths;
  180.         return this;
  181.     }

  182.     /**
  183.      * If dryRun is set, the paths in question will not actually be deleted.
  184.      *
  185.      * @param dryRun
  186.      *            whether to do a dry run or not
  187.      * @return {@code this}
  188.      */
  189.     public CleanCommand setDryRun(boolean dryRun) {
  190.         this.dryRun = dryRun;
  191.         return this;
  192.     }

  193.     /**
  194.      * If force is set, directories that are git repositories will also be
  195.      * deleted.
  196.      *
  197.      * @param force
  198.      *            whether or not to delete git repositories
  199.      * @return {@code this}
  200.      * @since 4.5
  201.      */
  202.     public CleanCommand setForce(boolean force) {
  203.         this.force = force;
  204.         return this;
  205.     }

  206.     /**
  207.      * If dirs is set, in addition to files, also clean directories.
  208.      *
  209.      * @param dirs
  210.      *            whether to clean directories too, or only files.
  211.      * @return {@code this}
  212.      */
  213.     public CleanCommand setCleanDirectories(boolean dirs) {
  214.         directories = dirs;
  215.         return this;
  216.     }

  217.     /**
  218.      * If ignore is set, don't report/clean files/directories that are ignored
  219.      * by a .gitignore. otherwise do handle them.
  220.      *
  221.      * @param ignore
  222.      *            whether to respect .gitignore or not.
  223.      * @return {@code this}
  224.      */
  225.     public CleanCommand setIgnore(boolean ignore) {
  226.         this.ignore = ignore;
  227.         return this;
  228.     }
  229. }