diff --git a/gitall b/gitall index fa2f6ff..793bff0 100755 --- a/gitall +++ b/gitall @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import os import sys @@ -6,143 +6,139 @@ import argparse import re import subprocess - REPO_COLOR = '\033[95m' DASH_COLOR = '\033[91m' COLOR_STOP = '\033[0m' def main(): - # Command argument parsing - parser = argparse.ArgumentParser( - description="Perform a git operation on multiple git repositories in subfolders", - epilog="NOTE: --quiet and --verbose cancel each other out one by one so '-qqv' gives the same result as '-q'") - parser.add_argument('-I', '--include-from', metavar='includefile', dest='includefile', type=str, - help='Read repositories to operate on from specified file.') - parser.add_argument('-i', '--include', metavar='include', dest='include', type=str, - help='Specify comma-separated list of repositories to use. Suppresses automatic repo detection') - parser.add_argument('-e', '--exclude', metavar='exclude', dest='exclude', type=str, - help="Specify comma-separated list of repositories to exclude. Applied after auto-detect or include(-file)") - parser.add_argument('-d', '--date', metavar='date', dest='date', type=str, - help="Specify checkout by date instead of ref. This parameter must be used with gitall checkout . The date format is YYYY-MM-DD HH:MM:SS or a shorter format. See also the date format used by git rev-list") - parser.add_argument('-n', '--noseparator', dest='sep', action='store_false', default=True, - help='Suppress printing of separator line between repositories.') - parser.add_argument('-q', '--quiet', action="count", default=0, - help="decrease output verbosity. Repeat for more silence, or to cancel out -v") - parser.add_argument('-v', '--verbose', action="count", default=0, - help="increase output verbosity. Repeat for more noise, or to cancel out -q") - parser.add_argument('-r', '--raw', action="store_true", - help="Treat the specified command as a 'full' command, i.e. not a git 'sub'-command. Example: gitall --raw cat .gitignore") - parser.add_argument('operation', metavar='operation', type=str, nargs=argparse.REMAINDER, - help="The git operation to perform on each repository, i.e. the part usually put after 'git '. (unless running in --raw mode)") - args = parser.parse_args() - if not any(args.operation): - parser.error('No arguments provided.') - - # handle verbosity - global verbosity - verbosity = 0 - args.quiet + args.verbose - if verbosity<-4: - print "If you want it THAT quiet, just pipe everything to /dev/null and be done with it!" - - #header printing - commandstring = ' '.join(args.operation) - if not args.raw: - commandstring = 'git ' + commandstring - localDir = os.path.abspath('.') - verboseprint(1, 'Running command:', commandstring) - verboseprint(1, 'gitall started in: ',localDir) - printDelimiter(args.quiet==0 and args.sep) - - gitDirectories = set() - if (not args.includefile and not args.include): - # get a list of git directories in the specified parent - gitDirectories.update(get_subdirectories('.', isGitDirectory)) - if args.includefile: - # get list of git directories from specified include file (-I) - fileinc = get_repos_from_include_file(args.includefile) - gitDirectories.update(fileinc) - if args.include: - # get list of git directories from list specified on command line (-i) - cmdinc = convert_commasep_to_list(args.include) - gitDirectories.update(cmdinc) - - if args.exclude: - # remove any repositories specified as exclude on command line (-e) - gitDirectories -= set(convert_commasep_to_list(args.exclude)) - - for gitDirectory in gitDirectories: - fullDir = localDir + "/" + gitDirectory - os.chdir(fullDir) - if os.name == 'nt': - pinkrepo = os.path.relpath(fullDir, localDir) - else: - pinkrepo = REPO_COLOR + os.path.relpath(fullDir, localDir) + COLOR_STOP - repooutput = v(1, 'Current repo:') + (pinkrepo,) + v(2," in ( "+os.path.abspath('.')+" )") - verboseprint(-1, *repooutput) - #if date is specified, then use git rev-list to get the sha from the date - if args.date: - regex = re.compile('\w+') - match = regex.findall(commandstring) - command = match[1] - #check if a branch was specified - try: - branch = match[2] - except IndexError: - sys.exit("--date must be used with git checkout ") - #check if the command was git checkout - if ( not command == 'checkout'): - sys.exit("--date must be used with git checkout ") - else: - #use git rev-list to get most recent sha hash before specified date - revparameters = ["git","rev-list", "-1", "--before="+args.date, branch] - revcommand = subprocess.Popen(revparameters, stdout=subprocess.PIPE) - (sha, err) = revcommand.communicate() - #construct a new datecommandstring (as the commandstring will be parsed in the loop for other repositories) - datecommandstring = "git "+command +" "+sha - os.system(datecommandstring) - else: - os.system(commandstring) - printDelimiter(verbosity >=0 and args.sep) + # Command argument parsing + parser = argparse.ArgumentParser( + description="Perform a git operation on multiple git repositories in subfolders", + epilog="NOTE: --quiet and --verbose cancel each other out one by one so '-qqv' gives the same result as '-q'") + parser.add_argument('-I', '--include-from', metavar='includefile', dest='includefile', type=str, + help='Read repositories to operate on from specified file.') + parser.add_argument('-i', '--include', metavar='include', dest='include', type=str, + help='Specify comma-separated list of repositories to use. Suppresses automatic repo detection') + parser.add_argument('-e', '--exclude', metavar='exclude', dest='exclude', type=str, + help="Specify comma-separated list of repositories to exclude. Applied after auto-detect or include(-file)") + parser.add_argument('-d', '--date', metavar='date', dest='date', type=str, + help="Specify checkout by date instead of ref. Must be used with gitall checkout ") + parser.add_argument('-n', '--noseparator', dest='sep', action='store_false', default=True, + help='Suppress printing of separator line between repositories.') + parser.add_argument('-q', '--quiet', action="count", default=0, + help="Decrease output verbosity. Repeat for more silence, or to cancel out -v") + parser.add_argument('-v', '--verbose', action="count", default=0, + help="Increase output verbosity. Repeat for more noise, or to cancel out -q") + parser.add_argument('-r', '--raw', action="store_true", + help="Treat the specified command as a full command, not a git sub-command") + parser.add_argument('operation', metavar='operation', type=str, nargs=argparse.REMAINDER, + help="The git operation to perform on each repository") + + args = parser.parse_args() + if not any(args.operation): + parser.error('No arguments provided.') + + # Handle verbosity + global verbosity + verbosity = 0 - args.quiet + args.verbose + if verbosity < -4: + print("If you want it THAT quiet, just pipe everything to /dev/null and be done with it!") + + # Header printing + commandstring = ' '.join(args.operation) + if not args.raw: + commandstring = 'git ' + commandstring + localDir = os.path.abspath('.') + verboseprint(1, 'Running command:', commandstring) + verboseprint(1, 'gitall started in:', localDir) + printDelimiter(args.quiet == 0 and args.sep) + + gitDirectories = set() + if not args.includefile and not args.include: + gitDirectories.update(get_subdirectories('.', isGitDirectory)) + if args.includefile: + fileinc = get_repos_from_include_file(args.includefile) + gitDirectories.update(fileinc) + if args.include: + cmdinc = convert_commasep_to_list(args.include) + gitDirectories.update(cmdinc) + if args.exclude: + gitDirectories -= set(convert_commasep_to_list(args.exclude)) + + for gitDirectory in gitDirectories: + fullDir = os.path.join(localDir, gitDirectory) + os.chdir(fullDir) + pinkrepo = os.path.relpath(fullDir, localDir) + if os.name != 'nt': + pinkrepo = REPO_COLOR + pinkrepo + COLOR_STOP + repooutput = v(1, 'Current repo:') + (pinkrepo,) + v(2, " in ( " + os.path.abspath('.') + " )") + verboseprint(-1, *repooutput) + + if args.date: + regex = re.compile(r'\w+') + match = regex.findall(commandstring) + command = match[1] if len(match) > 1 else None + + try: + branch = match[2] + except IndexError: + sys.exit("--date must be used with git checkout ") + + if command != 'checkout': + sys.exit("--date must be used with git checkout ") + + revparameters = ["git", "rev-list", "-1", "--before=" + args.date, branch] + revcommand = subprocess.Popen(revparameters, stdout=subprocess.PIPE) + sha, _ = revcommand.communicate() + sha = sha.decode('utf-8').strip() + + if not sha: + print(f"No commit found before date: {args.date} on branch {branch}") + continue + + datecommandstring = "git " + command + " " + sha + os.system(datecommandstring) + else: + os.system(commandstring) + + printDelimiter(verbosity >= 0 and args.sep) def convert_commasep_to_list(include): - return include.split(',') + return include.split(',') def verboseprint(fromlevel, *args): - if verbosity >= fromlevel: - for arg in args: - print arg, - print + if verbosity >= fromlevel: + print(*args) def v(fromlevel, *args): - if verbosity >= fromlevel: - return args - else: - return () + if verbosity >= fromlevel: + return args + else: + return () def get_repos_from_include_file(file): - return [gitRepo.rstrip("\r\n") for gitRepo in open(file, 'r')] + with open(file, 'r') as f: + return [line.strip() for line in f] -def get_subdirectories(directory, filter = None): - directory = os.path.abspath(directory) - subdirectories = os.walk(directory).next()[1] - if filter is None: - return [i for i in subdirectories] - else: - return [i for i in subdirectories if filter(directory + '/' + i)] +def get_subdirectories(directory, filter=None): + directory = os.path.abspath(directory) + subdirectories = next(os.walk(directory))[1] + if filter is None: + return [i for i in subdirectories] + else: + return [i for i in subdirectories if filter(os.path.join(directory, i))] def isGitDirectory(directory): - return os.path.isdir(directory + '/.git/') + return os.path.isdir(os.path.join(directory, '.git')) def printDelimiter(show): - dash = '-' - if verbosity > 2: - dash = "#" - if show: - dashes = dash * 80 - if os.name == 'nt': - print dashes - else: - print DASH_COLOR + dashes + COLOR_STOP + dash = '-' if verbosity <= 2 else "#" + if show: + dashes = dash * 80 + if os.name == 'nt': + print(dashes) + else: + print(DASH_COLOR + dashes + COLOR_STOP) if __name__ == '__main__': - main() + main()