Skip to content

Instantly share code, notes, and snippets.

@Jwink3101
Created October 8, 2020 16:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jwink3101/689685056ebee919ca631fb8037b1728 to your computer and use it in GitHub Desktop.
Save Jwink3101/689685056ebee919ca631fb8037b1728 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Apply the same git commands to all git repositories below to a specified depth
(default 1). Note: Will go below a top level repo for submodules, etc. WARNING:
if there is ambiguity as to git flags vs git-all (this code) flags, separate
with `--`
"""
from __future__ import division, print_function, unicode_literals, absolute_import
from io import open
import sys
import os
import argparse
import subprocess
import itertools
import fnmatch
if sys.version_info >= (3,):
raw_input = input
unicode = str
try:
from os import scandir
except ImportError:
try:
from scandir import scandir
except ImportError:
scandir = None
if scandir is None:
def ldir(p):
try:
ll = os.listdir(p)
except OSError:
return
for l in ll:
if os.path.isdir(os.path.join(p,l)):
yield l
else:
def ldir(p):
try:
ll = scandir(p)
except OSError:
return
for l in ll:
if l.is_dir():
yield l.name
__version__ = '20180109.0'
def print(*A):
"""redefine print to also flush"""
sys.stdout.write(' '.join(A) + '\n')
sys.stdout.flush()
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-c','--cmd',action='store_true',
help='Execute entire command withOUT the `git` prefix. '
'Ex: `%(prog)s -c git status` is the same as `%(prog)s status`')
parser.add_argument('-d','--depth',metavar='D',default=1,type=int,
help='[1] Depth to traverse down to find git repos')
parser.add_argument('-E','--exclude',action='append',default=[],
metavar='P',
help='Directories to exclude. "%(metavar)s" follows glob patterns')
parser.add_argument('-e','--error',action='store_true',
help='Stop if an error occurs')
parser.add_argument('-p','--pause',action='store_true',
help='Pause (and clear screen) after each repo')
parser.add_argument('-P','--path',action='store_true',
help='Print the full path to each repo')
parser.add_argument('-v', '--version', action='version',
version='%(prog)s-' + __version__,help=argparse.SUPPRESS)
# Create two command options and then later combine them. This
# serves the following purpose:
# * The help documentation will correctly show the command
# * Will error if not given a command
# * Will correctly parse `--` pseudo-command
parser.add_argument('command',nargs=1,
help=('command to be evaluated at the root of each git repository. '
'May need to separate with "--" or quote entire command to resolve '
'ambiguity'))
parser.add_argument('extracommand',nargs=argparse.REMAINDER, help=argparse.SUPPRESS) # This catches the rest
opts = parser.parse_args(sys.argv[1:])
# Do some manipulation to the arguments
opts.command.extend(opts.extracommand)
del opts.extracommand
opts.command = ' '.join(opts.command)
if not opts.cmd:
opts.command = 'git ' + opts.command
pwd = os.getcwd()
def join(*A,**K):
res = os.path.normpath(os.path.join(*A,**K))
if res.startswith('./'):
res = res[2:]
return res
def find_git_repos(path,_d=0):
if _d > opts.depth:
return
for ddir in sorted(ldir(path),key=unicode.lower):
if ddir == '.git':
yield path
elif any(fnmatch.fnmatch(ddir,e) for e in opts.exclude):
pass
else:
for res in find_git_repos(join(path,ddir),_d=_d+1):
yield res
# Are we in a repo?
try:
curr_repo = subprocess.check_output('git rev-parse --show-toplevel 2>/dev/null',shell=True).strip().decode('utf8')
curr_repo = [curr_repo]
except subprocess.CalledProcessError:
curr_repo = []
for REPO in itertools.chain(curr_repo,find_git_repos('.')):
if opts.pause:
subprocess.call('clear',shell=True)
# Edge case when called from the root of the repo so there is an extra "."
if REPO == '.':
continue
# Also, top level ones REPO will be a full path:
if REPO.startswith('/'):
fullREPO = REPO
REPO = os.path.basename(REPO)
else:
fullREPO = join(pwd,REPO)
if opts.path:
print(fullREPO)
else:
print(REPO)
print('='*4)
os.chdir(fullREPO)
# Note that print was redefined to also flush to this works
try:
subprocess.check_call(opts.command,shell=True)
except subprocess.CalledProcessError:
print('\n ***Command returned error!***')
if opts.error:
os.chdir(pwd)
sys.exit(2)
os.chdir(pwd)
print('-'*50)
if opts.pause:
try:
inp = raw_input("Press <enter> key to continue (or <X> or CTRL-C to break)\n")
if inp.lower().strip() == 'x':
raise KeyboardInterrupt
except KeyboardInterrupt:
print('')
sys.exit()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment