Created
October 8, 2020 16:45
-
-
Save Jwink3101/689685056ebee919ca631fb8037b1728 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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