How to find all git repositories within given folders (fast)

Okay, I still don't totally sure how this works, but I've tested it and it works.

.
├── a
│   ├── .git
│   └── a
│       └── .git
└── b
    └── .git

6 directories, 0 files

% find . -type d -exec test -e '{}/.git' ';' -print -prune
./a
./b

I'm looking forward into making the same faster.


Possible Solution

For GNU find and other implementations that support -execdir:

find dir1 dir2 dir3 -type d -execdir test -d '.git' \; -print -prune

(see the comments)

Previously discussed stuff

Solution if pruning below .git is enough

find dir1 dir2 dir3 -type d -path '*/.git' -print -prune | xargs -I {} dirname {}

If -printf '%h' is supported (as in the case of GNU's find) we don't need dirname:

find dir1 dir2 dir3 -type d -path '*/.git' -printf '%h\n' -prune

Once it comes across a folder .git in the current path it will output it and then stop looking further down the subtree.

Solution if the whole folder tree should be pruned once a .git is found

Using -quit if your find supports it:

for d in dir1 dir2 dir3; do
  find "$d" -type d -name .git -print -quit
done | xargs -I {} dirname {}

(According to this detailed post by Stéphane Chazelas -quit is supported in GNU's and FreeBSD's find and in NetBSD as -exit.)

Again with -printf '%h' if supported:

for d in dir1 dir2 dir3; do
  find "$d" -type d -name .git -printf '%h\n' -quit
done

Solution for pruning at the same level as where the .git folder is

See the "Possible Solution" part for the current solution for this particular problem.

(Oh and obviously the solutions using xargs assume there are no newlines in the paths, otherwise you would need null-byte magic.)


Ideally, you'd want to crawl directory trees for directories that contain a .git entry and stop searching further down those (assuming you don't have further git repos inside git repos).

The problem is that with standard find, doing this kind of check (that a directory contains a .git entry) involves spawning a process that executes a test utility using the -exec predicate, which is going to be less efficient than listing the content of a few directories.

An exception would be if you use the find builtin of the bosh shell (a POSIXified fork of the Bourne shell developed by @schily) which has a -call predicate to evaluate code in the shell without having to spawn a new sh interpreter:

#! /path/to/bosh
find . -name '.?*' -prune -o \
  -type d -call '[ -e "$1/.git" ]' {} \; -prune -print

Or use perl's File::Find:

perl -MFile::Find -le '
  sub wanted {
    if (/^\../) {$File::Find::prune = 1; return}
    if (-d && -e "$_/.git") {
       print $File::Find::name; $File::Find::prune = 1
    }
  }; find \&wanted, @ARGV' .

Longer, but faster than zsh's printf '%s\n' **/.git(:h) (which descends into all non-hidden directories), or GNU find's find . -name '.?*' -prune -o -type d -exec test -e '{}/.git' \; -prune -print which runs one test command in a new process for each non-hidden directory.

Tags:

Git

Find