#!/usr/bin/env python3 import os import subprocess import sys from concurrent.futures import ThreadPoolExecutor SAFEBRANCHES = ['master', 'develop', 'rc'] DEBUG = False # TODO: local changes, branch updated. Is there a way to know, whether rebase would be successful? If yes: do so def dPrint(*args, **kwargs): if DEBUG: print(' '.join(map(str, args)), **kwargs) def getStdout(command): process = None process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if process.returncode != 0: print('Failed to execute command:', command) if process.stdout: print(' Stdout:') print(process.stdout.decode('UTF-8')) if process.stderr: print(' Stderr:') print(process.stderr.decode('UTF-8')) return '' if process.stdout: return process.stdout.decode('UTF-8').strip() return '' def getGitStdout(path, command): dPrint('[ running', command, 'on', path, ']') if isinstance(command, str): cmd = 'git -C %s %s' % (path, command) return getStdout(cmd.split()) if isinstance(command, list): cmd = ['git', '-C', path] cmd.extend(command) return getStdout(cmd) def findChanges(statusoutput): dPrint('Parsing for changes:') for line in statusoutput.split('\n'): dPrint(' Status Line:', line) if line.startswith('A ') or line.startswith('D ') or line.startswith('M '): return True return False def updatePath(path): friendly_path = os.path.basename(path) dPrint('Repository:', path) head = getGitStdout(path, 'rev-parse --abbrev-ref HEAD') changed = findChanges(getGitStdout(path, 'status --porcelain')) cur_branch = getGitStdout(path, 'rev-parse --abbrev-ref HEAD') # Fetch changes, delete removed repositories getGitStdout(path, 'fetch --all --prune') # Go through branches and merge with upstream branches = getGitStdout(path, [ 'for-each-ref', '--format=%(refname:short)#%(upstream)#%(upstream:track,nobracket)', 'refs/heads' ]) for branch in branches.split('\n'): local_branch, upstream_branch, tracking = branch.split('#') if not upstream_branch or tracking == 'gone': print('[%s]' % friendly_path, local_branch, 'has no upstream branch.') continue dPrint('Update for', friendly_path, local_branch, upstream_branch) local = getGitStdout(path, ['rev-parse', '--quiet', '--verify', local_branch]) remote = getGitStdout(path, ['rev-parse', '--quiet', '--verify', upstream_branch]) base = getGitStdout(path, ['merge-base', local_branch, upstream_branch]) dPrint(' Branch:', local_branch) dPrint(' Upstream:', upstream_branch) dPrint(' Local', local) dPrint(' Remote', remote) dPrint(' Base', base) if local == remote: continue if local_branch == cur_branch and changed: print('[%s]' % friendly_path, 'has a working branch with changes. Stashing.') getGitStdout(path, ['stash', 'push', '--include-untracked', '--message', 'Automatic stash by updateGit.py']) if local == base: if local_branch == cur_branch: getGitStdout(path, ['merge', remote]) local_short = getGitStdout(path, ['rev-parse', '--short', local]) remote_short = getGitStdout(path, ['rev-parse', '--short', remote]) shortstat = getGitStdout(path, ['diff', '--shortstat', local, remote]) print('[%s]' % friendly_path, 'Updated branch', cur_branch, '{}..{}'.format(local_short, remote_short), "\n", shortstat) else: getGitStdout(path, ['branch', '-f', local_branch, remote]) shortstat = getGitStdout(path, ['diff', '--shortstat', local, remote]) print('[%s] Updated branch %s:' % (friendly_path, cur_branch), '{}..{}'.format(local, remote), "\n", shortstat) if remote == base: print('[%s] %s is ahead. Consider pushing your changes.' % (friendly_path, local_branch)) continue if local_branch == cur_branch and changed: print('[%s]' % friendly_path, 'Reapplying stashed changes.') getGitStdout(path, ['stash', 'pop']) # Find safe branch (master/development/rc) branches = getGitStdout(path, [ 'for-each-ref', '--format=%(refname:short)', 'refs/heads' ]).split('\n') for branch in SAFEBRANCHES: if branch in branches: safe_branch = branch break # No safe branch found. What is this? if not safe_branch: print('Does the repository', friendly_path, 'not have any safe branch?') return # print('Safe branch:', safe_branch) # Checkout master, if detached HEAD and not proceeded if head == 'HEAD': if changed: print('[%s] Head detached, but changes found. Stash them to checkout %s.' % ( friendly_path, safe_branch )) else: headrev = getGitStdout(path, 'rev-parse --quiet --verify HEAD') mergerev = getGitStdout(path, ['merge-base', 'HEAD', safe_branch]) if headrev == mergerev: print('[%s] Head detached, checking %s out.' % (friendly_path, safe_branch), getGitStdout(path, 'checkout ' + safe_branch)) else: print('[%s] Head detached, but not part of safe branch!' % friendly_path) # Delete branches, that were already merged to master merged_branches = getGitStdout(path, [ 'branch', '--format=%(refname:short)', '--merged=%s' % safe_branch ]).split('\n') # print('[%s]' % friendly_path, 'Deletion of branches', merged_branches, SAFEBRANCHES, cur_branch) for merged_branch in merged_branches: if merged_branch in SAFEBRANCHES: continue if merged_branch == cur_branch: continue get_tracking_status = getGitStdout(path, [ 'for-each-ref', '--format=%(upstream:track,nobracket)', 'refs/heads/%s' % merged_branch ]) if get_tracking_status.strip() == 'gone': print('[%s]' % friendly_path, 'Removing branch by force, upstream is gone.') print('[%s]' % friendly_path, getGitStdout(path, ['branch', '-D', merged_branch])) else: print('[%s]' % friendly_path, getGitStdout(path, ['branch', '-d', merged_branch])) def main(): paths = [] if len(sys.argv) == 1: paths.append('.') else: for path in sys.argv[1:]: paths.append(path) repos = [] for path in paths: for subdir, dirs, _ in os.walk(path): # dPrint(subdir, dir, files) if '.git' in dirs: repos.append(subdir) if len(repos) == 0: print('No repositories found.') return print('Updating', len(repos), 'repositories.') # executor = ThreadPoolExecutor(max_workers=len(repos)) executor = ThreadPoolExecutor(max_workers=8) for repo in repos: # updatePath(subdir) executor.submit(updatePath, repo) executor.shutdown() if __name__ == "__main__": main()