1

We know an empty git repository only has the .git directory in it.

I want to find all git repositories on a machine, which are empty.

I thought about this process:

  1. Find all directories that are named .git
  2. Exclude them if they are inside Trash
  3. Exclude others if they are not your repos (some third-party apps also pull git repos)
  4. Loop over them
  5. For each repo's parent directory, count its top-level files and folders
  6. If the count is zero (excluding .git) or is 1 (including .git), repo is empty. echo it.

And here's my script for this:

find / -type d -name .git 2>/dev/null | 
{
    while read gitFolder; do
        if [[ $gitFolder == *"/Temp/"* ]]; then
            continue;
        fi
        if [[ $gitFolder == *"/Trash/"* ]]; then
            continue;
        fi
        if [[ $gitFolder == *"/opt/"* ]]; then
            continue;
        fi
        parent=$(dirname $gitFolder);
        echo "";
        if [ $(ls $parent -A | wc -l ) != 1 ]; then
            echo $parent
        fi
    done
}

But this does not work as I expected it. It lists all of the repos which means I have a bug in my comparison part, that I can't find. What do I do wrong?

Also, I think it might not be the best approach. Any ideas on how to make it better?

2 Answers 2

3

This doesn’t explain why your comparisons don’t work, but you can do all this with find directly (if your find supports -execdir), without looping over its output:

find / -type d \( \( \( -name Temp -o -name Trash -o -name opt \) -prune \) \
                  -o \( -name .git -execdir sh -c '[ "$(ls -A)" = ".git" ] && pwd' \; \) \)

This looks for all directories, pruning Temp etc. (so it doesn’t even explore their subdirectories). When it finds a directory named .git, it runs a test in the parent directory to see if .git is the only file present, and if so, runs pwd to print the current directory.

2
  • Man this is really hard. Look at all those parantjeses and escapes and nesting. How can one maintain this code amongat many similar scripts. Is it faster? Jun 25, 2022 at 9:32
  • 1
    @SaeedNeamati 1. same as anything difficult or complex: practice, practice, practice. and yeah. shell's quoting requirements do make things harder to read and understand. That's one of the reasons why shell isn't a great language for complex tasks (shell's job is to run other programs to do work, not do the work itself). 2. yes, much faster. processing text in a shell loop is slow. There's probably faster ways to do it because this still forks sh -c with -execdir every time a directory called .git is found....maybe a perl script using File::Find
    – cas
    Jun 26, 2022 at 4:38
3
$ cat find-empty-git.pl 
#!/usr/bin/perl

use File::Find;
use List::Util qw(uniq);

push @ARGV, './' unless @ARGV;
@ARGV = uniq(@ARGV);
foreach (@ARGV) { die "$_ is not a directory" unless -d $_ };

find(\&wanted, @ARGV);

sub wanted {
  $File::Find::prune = 1 if $File::Find::name =~ m=/(Trash|Temp|opt)($|/)=;
  return unless (-d && /^\.git$/);

  opendir(my $dh, '.') ||
    warn "Can't open $File::Find::dir: $!" &&
    return;

  return if (grep { ! /^(\.{1,2}|\.git)$/ } readdir($dh));
  closedir($dh);

  print "$File::Find::dir\n";
}

This perl script uses the directory names specified on the command-line as the top-level directory (or directories) to search.

It defaults to ./ if no directory is specified. It checks that each argument is actually a directory, and it uses the the uniq() function from the List::Util module to eliminate duplicate directory names. The File::Find module is used to recursively search the specified directories. Both modules are core perl modules and are included with perl (i.e. they don't need to be installed separately).

For each filename found, the wanted subroutine is executed.

First it checks if the full pathname for the current filename ends in /Trash, /Temp, or /opt or is a descendant of one of those. If it does, it prunes that directory from the search tree.

Next, if the filename isn't a directory and isn't .git, the subroutine returns immediately.

Otherwise, the directory containing the file is opened (with a variable called $dh as the directory handle. See perldoc -f opendir) and the directory's contents are examined. If the directory can't be opened for read for any reason (e.g. permissions), it is treated as a non-fatal error (a warning message is printed to stderr and the subroutine returns).

The grep used in the wanted subroutine is perl's built-in grep function. It is NOT the grep external command. perl's grep function takes a list (array) as input and returns another list where the block of code evaluates as true. In list context, the readdir function returns a list of filenames in a directory. See perldoc -f grep and perldoc -f readdir.

In short: The return if grep... readdir($dh) line returns early (i.e. before printing the directory name) from the wanted function if there are any "files" in the directory that don't match either ., .., or .git. The word "files" is used here in the generic sense and includes regular files, symbolic links, directories, device nodes, named pipes, sockets, etc.

Finally, having made it this far, the directory name is printed.

BTW, the print "$File::Find::dir\n"; line could be changed to print "$File::Find::dir\0"; if you needed a NUL-separated list of directory names instead of newline-separated.

Sample run. First create a testing environment by making some directories (a, b, and c) with .git sub-directories in them. Create a file in one of those directories. Make another directory (d) that doesn't have a .git subdir but does have a subdir (e) which does. And some .git subdirs under ./Trash/ and ./Temp/

$ mkdir -p {a,b,c}/.git/
$ touch a/file1
$ mkdir -p d/e/.git
$ mkdir -p Trash/f/.git Temp/g/.git

$ tree --metafirst --noreport -paf a b c d Trash Temp
[drwxr-xr-x]  a
[-rw-r--r--]  ├── a/file1
[drwxr-xr-x]  └── a/.git
[drwxr-xr-x]  b
[drwxr-xr-x]  └── b/.git
[drwxr-xr-x]  c
[drwxr-xr-x]  └── c/.git
[drwxr-xr-x]  d
[drwxr-xr-x]  └── d/e
[drwxr-xr-x]      └── d/e/.git
[drwxr-xr-x]  Trash
[drwxr-xr-x]  └── Trash/f
[drwxr-xr-x]      └── Trash/f/.git
[drwxr-xr-x]  Temp
[drwxr-xr-x]  └── Temp/g
[drwxr-xr-x]      └── Temp/g/.git

Now make the script executable and run it. It prints the names of the directories that:

  1. aren't children of Trash, Temp, or opt directories,
  2. contain a .git subdir, and
  3. don't contain any other files

i.e. ./b and ./c and ./d/e.

$ chmod +x ./find-empty-git.pl
$ ./find-empty-git.pl ./
./b
./d/e
./c

BTW, depending on how comfortable you are reading and working with fairly linear code, this may or may not be easier to read and understand than a moderately long and complicated find command line. It's certainly easier for me (but that's probably because I've written dozens of little File::Find-based scripts like this over the last few decades).

It's hard to say whether this would be faster than running find or not. Probably. Maybe. Depends on how many directories you have that contain .git subdirectories. With this script, perl only has to be run once and it doesn't execute any external programs. Stephen's find command has to execute both sh and ls (and maybe pwd) once for each .git directory it finds, which can add up to a significant overhead if there are lots of .git directories.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .