Coverage for collectra/gui/updates.py: 95.00%
60 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 09:56 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 09:56 +0000
1"""Check and fast-forward the two source repositories used by this GUI."""
3import os
4import subprocess
5from pathlib import Path
8def git(root, *args):
9 env = dict(os.environ, GIT_TERMINAL_PROMPT="0", GCM_INTERACTIVE="never")
10 env.setdefault("GIT_SSH_COMMAND", "ssh -oBatchMode=yes")
11 result = subprocess.run(
12 ["git", "-C", str(root), *args], capture_output=True, text=True,
13 timeout=120, env=env,
14 )
15 if result.returncode:
16 raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "Git command failed")
17 return result.stdout.strip()
20def collectra_repository():
21 import collectra
23 source = Path(collectra.__file__).resolve()
24 for root in source.parents:
25 if (root / ".git").exists():
26 try:
27 git(root, "ls-files", "--error-unmatch", "--", str(source.relative_to(root)))
28 return root
29 except (RuntimeError, OSError, subprocess.TimeoutExpired):
30 return None
31 return None
34def inspect_repository(root, label, fetch=True):
35 result = {"label": label, "path": str(root) if root else "", "can_update": False}
36 if root is None or not (Path(root) / ".git").exists():
37 return {**result, "status": "unavailable", "message": "Not installed from a Git checkout."}
38 root = Path(root)
39 try:
40 branch = git(root, "rev-parse", "--abbrev-ref", "HEAD")
41 if branch == "HEAD":
42 raise RuntimeError("This checkout has a detached HEAD. Switch to a tracking branch before updating.")
43 remotes = git(root, "remote").splitlines()
44 if not remotes:
45 return {**result, "status": "unavailable", "message": "No Git remote configured."}
46 remote = git(root, "for-each-ref", "--format=%(upstream:remotename)", f"refs/heads/{branch}")
47 if remote not in remotes:
48 raise RuntimeError("The current branch does not track a remote branch.")
49 if fetch:
50 git(root, "fetch", "--prune", "--", remote)
51 upstream = git(root, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}")
52 ahead, behind = map(int, git(root, "rev-list", "--left-right", "--count", "HEAD...@{upstream}").split())
53 dirty = bool(git(root, "status", "--porcelain"))
54 result.update(branch=branch, upstream=upstream, ahead=ahead, behind=behind)
55 if dirty:
56 return {**result, "status": "blocked", "message": "Local changes are present. Commit or stash them before updating."}
57 if ahead and behind:
58 return {**result, "status": "blocked", "message": "Local and remote branches have diverged. Resolve this in Git before updating."}
59 if behind:
60 return {**result, "status": "available", "can_update": True,
61 "message": f"{behind} new commit(s) available from {upstream}."}
62 return {**result, "status": "current", "message": "Up to date." if not ahead else "No incoming updates; local commits are ahead of the remote."}
63 except (RuntimeError, OSError, subprocess.TimeoutExpired) as error:
64 return {**result, "status": "error", "message": str(error)}
67def update_repository(root, label):
68 # Recheck immediately before modifying a checkout: status may have changed
69 # since the dialog opened. Never merge or reset local work.
70 result = inspect_repository(root, label)
71 if not result["can_update"]:
72 return result
73 try:
74 git(root, "merge", "--ff-only", "@{upstream}")
75 return {**result, "can_update": False, "status": "updated",
76 "message": "Updated successfully. Restart the GUI to use the new code."}
77 except (RuntimeError, OSError, subprocess.TimeoutExpired) as error:
78 return {**result, "can_update": False, "status": "error", "message": str(error)}