summaryrefslogtreecommitdiff
path: root/updateGit.py
diff options
context:
space:
mode:
Diffstat (limited to 'updateGit.py')
-rwxr-xr-xupdateGit.py438
1 files 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()