From fc7f92af2a5b8521fc35ce1f49c46badc1b3f613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Gl=C3=BCpker?= Date: Mon, 17 Jul 2023 12:49:47 +0200 Subject: Big update, forgot to add to git regularly... * Locking * New print statements using colors * Code formatting / style / ... * Features? --- updateGit.py | 438 ++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 315 insertions(+), 123 deletions(-) diff --git a/updateGit.py b/updateGit.py index 86f1781..0353aa4 100755 --- a/updateGit.py +++ b/updateGit.py @@ -3,192 +3,384 @@ import os import subprocess import sys +import contextvars from concurrent.futures import ThreadPoolExecutor +from threading import Lock -SAFEBRANCHES = ['master', 'develop', 'rc'] +SAFE_BRANCHES = ["develop", "master", "main", "rc", "release"] DEBUG = False +original_print = print +p_print_lock = Lock() # Print Lock +c_print_lock = Lock() # Color Lock -def dPrint(*args, **kwargs): +repository_path = contextvars.ContextVar("repository", default="") + + +def print(*args, **kwargs): + with p_print_lock: + original_print(" ".join(map(str, args)), **kwargs) + + +def goodPrint(*args, **kwargs): + with c_print_lock: + print("\033[32m", end="") + if path := repository_path.get(): + print(f"[{path}]", end=" ") + print(*args, **kwargs, end="\033[00m\n") + + +def warnPrint(*args, **kwargs): + with c_print_lock: + print("\033[33m", end="") + if path := repository_path.get(): + print(f"[{path}]", end=" ") + print(*args, **kwargs, end="\033[00m\n") + + +def errorPrint(*args, **kwargs): + with c_print_lock: + print("\033[31m", end="") + if path := repository_path.get(): + print(f"[{path}]", end=" ") + print(*args, **kwargs, end="\033[00m\n") + + +def debugPrint(*args, **kwargs): if DEBUG: - print(' '.join(map(str, args)), **kwargs) + if path := repository_path.get(): + print(f"[{path}]", end=" ") + print(" ".join(map(str, args)), **kwargs) + def getStdout(command): - process = None - process = subprocess.run(command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if process.returncode != 0: - print('Failed to execute command:', command) + errorPrint("Failed to execute command:", command) if process.stdout: - print(' Stdout:') - print(process.stdout.decode('UTF-8')) + for line in process.stdout.decode("UTF-8").strip().split("\n"): + errorPrint(" Stdout: ", line) if process.stderr: - print(' Stderr:') - print(process.stderr.decode('UTF-8')) - return '' + for line in process.stderr.decode("UTF-8").strip().split("\n"): + errorPrint(" Stderr: ", line) + return "" if process.stdout: - return process.stdout.decode('UTF-8').strip() - return '' + stdout = process.stdout.decode("UTF-8").strip() + if stdout: + for line in stdout.split("\n"): + debugPrint(" Stdout: ", line) + return stdout + return "" + def getGitStdout(path, command): - dPrint('[ running', command, 'on', path, ']') + debugPrint(f"git({path}): run", command) if isinstance(command, str): - cmd = 'git -C %s %s' % (path, command) + cmd = f"git -C {path} {command}" return getStdout(cmd.split()) if isinstance(command, list): - cmd = ['git', '-C', path] + 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 '): + +def contains_changes(git_status_output): + debugPrint("Parsing for changes:") + for line in git_status_output.split("\n"): + debugPrint(" Status Line:", line) + if line.startswith("A ") or line.startswith("D ") or line.startswith("M "): return True return False -def updatePath(path): + +def update_repository(path): friendly_path = os.path.basename(path) - dPrint('Repository:', path) - changed = findChanges(getGitStdout(path, 'status --porcelain')) - cur_branch = getGitStdout(path, 'rev-parse --abbrev-ref HEAD') + repository_path.set(friendly_path) + debugPrint("Starting git update...") + changed = contains_changes(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.') + getGitStdout(path, "fetch --all --prune") + + debugPrint("Go through branches and merge with upstream") + # Switched format 'upstream' to 'push' + branches = getGitStdout( + path, + [ + "for-each-ref", + "--format=%(refname:short)#%(push)#%(upstream:track,nobracket)", + "refs/heads", + ], + ).split("\n") + remote_branches = getGitStdout(path, ['for-each-ref', '--format=%(refname)', 'refs/remotes/origin']).split('\n') + for branch in branches: + local_branch, push_branch, tracking = branch.split("#") + debugPrint(f"Trying to update {local_branch=} {push_branch=} {tracking=}") + if tracking == "gone": + # Upstream was merged, branch is removed later + warnPrint("Upsteam branch for", local_branch, "is gone.") + continue + if push_branch not in remote_branches: + # warnPrint(local_branch, "does not exist remotely. Either not yet created or already merged/removed.") 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) + local = getGitStdout(path, ["rev-parse", "--quiet", "--verify", local_branch]) + remote = getGitStdout( + path, ["rev-parse", "--quiet", "--verify", push_branch] + ) + if not remote: + raise Exception("Why was rev-parse called, if remote_branch does not exist?") + merge_base = getGitStdout(path, ["merge-base", local_branch, push_branch]) + debugPrint(" Branch:", local_branch) + debugPrint(" Upstream:", push_branch) + debugPrint(" Local", local) + debugPrint(" Remote", remote) + debugPrint(" Base", merge_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: + warnPrint("Repository has a working branch with changes. Stashing.") + getGitStdout( + path, + [ + "stash", + "push", + "--all", # "--include-untracked", + "--message", + "Automatic stash by updateGit.py", + ], + ) + if local == merge_base: # Remote updates on branch, pull them in 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]) - shortlog = getGitStdout(path, ['log', '--pretty=format:… %s', '{}..{}'.format(local, remote)]) - print('[%s]' % friendly_path, 'Updated branch', cur_branch, '{}..{}'.format(local_short, remote_short), "\n", shortstat) - print(shortlog) + 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]) + shortlog = getGitStdout( + path, + [ + "log", + "--pretty=format:…… %s", + "--no-merges", + f"{local}..{remote}", + ], + ) + goodPrint(f"Updated branch {cur_branch} {local_short}..{remote_short}") + goodPrint(shortstat) + debugPrint(shortlog) else: - getGitStdout(path, ['branch', '-f', local_branch, remote]) - shortstat = getGitStdout(path, ['diff', '--shortstat', local, remote]) - shortlog = getGitStdout(path, ['log', '--pretty=format:… %s', '{}..{}'.format(local, remote)]) - print('[%s] Updated branch %s:' % (friendly_path, cur_branch), '{}..{}'.format(local, remote), "\n", shortstat) - print(shortlog) - if remote == base: - print('[%s] %s is ahead. Consider pushing your changes.' % (friendly_path, local_branch)) + getGitStdout(path, ["branch", "-f", local_branch, remote]) + shortstat = getGitStdout(path, ["diff", "--shortstat", local, remote]) + shortlog = getGitStdout( + path, + [ + "log", + "--pretty=format:…… %s", + "--no-merges", + f"{local}..{remote}", + ], + ) + goodPrint(f"Updated branch {cur_branch}: {local}..{remote}") + goodPrint(shortstat) + debugPrint(shortlog) + if remote == merge_base: # Local updates on branch + warnPrint(f"{local_branch} is ahead. Consider pushing your changes.") continue + if local_branch == cur_branch and local != remote != merge_base: + warnPrint( + "Local, Remote and their base are different. Trying to rebase..." + ) + getGitStdout(path, ["rebase", remote]) 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: + warnPrint("Reapplying stashed changes.") + getGitStdout(path, ["stash", "pop"]) + if local_branch != cur_branch and local != remote != merge_base: + rebase_check = getGitStdout(path, [ + "log", + "--oneline", + "--cherry", + f"{remote}...{local}", + ]) + was_just_rebased = all(line.startswith('= ') for line in rebase_check.split('\n')) + if was_just_rebased: + goodPrint("Resetting", local_branch, "to origin version after remote rebase.") + getGitStdout(path, [ + "branch", + "--force", + local_branch, + f"origin/{local_branch}", + ]) + else: + warnPrint( + local_branch, + "was modified remotely, but not just rebased.", + ) + + debugPrint("Find safe branch from", SAFE_BRANCHES) + safe_branch = None + branches = getGitStdout( + path, ["for-each-ref", "--format=%(refname:short)", "refs/heads"] + ).split("\n") + debugPrint("Branches:", branches) + for branch in SAFE_BRANCHES: if branch in branches: safe_branch = branch + debugPrint("Local safe branch:", safe_branch) break - # No safe branch found. What is this? if not safe_branch: - print('Does the repository', friendly_path, 'not have any safe branch?') + debugPrint("No local safe branch, trying to find it remotely...") + branches = getGitStdout( + path, ["for-each-ref", "--format=%(refname:short)"] + ).split("\n") + debugPrint("Branches:", branches) + for branch in SAFE_BRANCHES: + for branchname in branches: + if branchname.endswith(branch): + safe_branch = branch + debugPrint("Remote safe branch:", safe_branch) + safe_branch_switch = getGitStdout( + path, ["switch", safe_branch] + ).split("\n")[-1] + # warnPrint(f"Safe Branch Switch: {safe_branch_switch}.") + break + if not safe_branch: + warnPrint("The repository seems to not contain a safe branch! Abort!") return - # print('Safe branch:', safe_branch) - - # Checkout master, if detached HEAD and not proceeded - if cur_branch == 'HEAD': + # Checkout safe branch, if detached HEAD and not proceeded + if cur_branch == "HEAD": + debugPrint("Should the safe branch be checked out?", cur_branch) if changed: - print('[%s] Head detached, but changes found. Stash them to checkout %s.' % ( - friendly_path, - safe_branch - )) + errorPrint( + "Head detached and changes found." + f"Stash them to checkout {safe_branch}." + ) + return + revision_head = getGitStdout(path, "rev-parse --quiet --verify HEAD") + revision_merge = getGitStdout(path, ["merge-base", "HEAD", safe_branch]) + if revision_head == revision_merge: + warnPrint( + f"Head detached, checking {safe_branch} out.", + getGitStdout(path, ["checkout", 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) + warnPrint("Head detached, but not part of safe branch!") + # Bad, if diverged from origin/master + # else: + # switch = getGitStdout(path, ['switch', safe_branch]) + # debugPrint(switch) + + # Delete branches, that were already merged to our safe_branch + # Experiment: Merged into origin/safe_branch, because I sometimes push to local + merged_branches = ( + getGitStdout( + path, + [ + "branch", + "--format=%(refname:short)", + f"--merged=origin/{safe_branch}", + ], + ) + .strip() + .split("\n") + ) + # debugPrint('Deletion of branches', merged_branches, cur_branch) for merged_branch in merged_branches: - if merged_branch in SAFEBRANCHES: + if not merged_branch: # List might contain empty string continue - get_tracking_status = getGitStdout(path, [ - 'for-each-ref', - '--format=%(upstream:track,nobracket)', - 'refs/heads/%s' % merged_branch - ]) + if merged_branch in SAFE_BRANCHES: + continue + get_tracking_status = getGitStdout( + path, + [ + "for-each-ref", + "--format=%(upstream:track,nobracket)", + "refs/heads/%s" % merged_branch, + ], + ) if merged_branch == cur_branch: - print("Checking out safe branch, as current is merged.") - getGitStdout(path, 'checkout ' + safe_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])) + # TODO: This does not work it seems? + warnPrint("Current branch is merged. Checking out safe branch.") + if changed: + warnPrint("Local changes detected, creating temporary stash") + getGitStdout( + path, + [ + "stash", + "push", + "--include-untracked", + "--message", + "Automatic stash by updateGit.py", + ], + ) + getGitStdout(path, ["checkout", safe_branch]) + if changed: + warnPrint("Unstashing local changes") + getGitStdout(path, ["stash", "pop"]) + if get_tracking_status.strip() == "gone": + warnPrint("Removing branch by force, upstream is gone.") + getGitStdout(path, ["branch", "-D", merged_branch]), else: - print('[%s]' % friendly_path, - getGitStdout(path, ['branch', '-d', merged_branch])) + # Might fail / not properly merged? + warnPrint( + "Not sure, why this condition was here...", + getGitStdout(path, ["branch", "-d", merged_branch]), + ) + + cur_branch = getGitStdout(path, "rev-parse --abbrev-ref HEAD") + if cur_branch not in SAFE_BRANCHES: + warnPrint(f"Currently on {cur_branch}, which is not considered a safe branch.") + # getGitStdout(path, ["gc", "--quiet", "--no-prune"]) + + +class Version: + def __init__(self, version): + self.parts = version.split(".") + + def __lt__(self, other): + if len(self.parts) != len(other.parts): + return 0 + for i, part in enumerate(self.parts): + if part != other.parts[i]: + return part < other.parts[i] + return 0 + + def __repr__(self): + return "Version(%s)" % ".".join(self.parts) + + def __str__(self): + return ".".join(self.parts) + def main(): paths = [] if len(sys.argv) == 1: - paths.append('.') + 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) + for current_dir, dirs, _files in os.walk(path): + if '/.' in current_dir: + continue + if ".git" in dirs and os.path.exists( + os.path.join(current_dir, ".git", "HEAD") + ): + debugPrint("Found repository", current_dir) + repos.append(current_dir) if len(repos) == 0: - print('No repositories found.') + errorPrint("No repositories found.") return - print('Updating', len(repos), 'repositories.') + goodPrint("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) + # update_repository(subdir) + executor.submit(update_repository, repo) executor.shutdown() -- cgit v1.2.3