1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
diff --git a/src/aerie/sync.py b/src/aerie/sync.py
index 858bb58..d58a6b7 100644
--- a/src/aerie/sync.py
+++ b/src/aerie/sync.py
@@ -76,6 +76,26 @@ def ensure_clean_git_tree(path: Path) -> None:
)
+def count_local_commits_to_drop(path: Path, target: str) -> int:
+ commits = run(
+ ["git", "-C", str(path), "rev-list", "--count", f"{target}..HEAD"],
+ capture=True,
+ )
+ return int(commits.stdout.strip() or "0")
+
+
+def ensure_no_local_commits_to_drop(path: Path, target: str) -> None:
+ count = count_local_commits_to_drop(path, target)
+ if count:
+ raise AerieError(
+ f"cannot update {path}; local HEAD has {count} commit(s) that would be "
+ f"discarded by reset to {target}\n"
+ f"Inspect with: git -C {path} log --oneline {target}..HEAD\n"
+ "Publish those commits, disable auto-sync for generated repos, or use "
+ "'aerie sync --force' to discard them."
+ )
+
+
def discard_local_changes(path: Path) -> None:
run(["git", "-C", str(path), "reset", "--hard"])
run(["git", "-C", str(path), "clean", "-fd"])
@@ -95,8 +115,11 @@ def update_repo(repo: RepoConfig, release: str, *, force: bool = False) -> None:
else:
ensure_clean_git_tree(repo.location)
run(["git", "-C", str(repo.location), "fetch", "--prune", "origin"])
- run(["git", "-C", str(repo.location), "checkout", "-B", release, f"origin/{release}"])
- run(["git", "-C", str(repo.location), "reset", "--hard", f"origin/{release}"])
+ target = f"origin/{release}"
+ if not force:
+ ensure_no_local_commits_to_drop(repo.location, target)
+ run(["git", "-C", str(repo.location), "checkout", "-B", release, target])
+ run(["git", "-C", str(repo.location), "reset", "--hard", target])
def sync_repo(
diff --git a/tests/test_sync.py b/tests/test_sync.py
index ab85c07..d15a670 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -60,6 +60,40 @@ class SyncTests(unittest.TestCase):
run.assert_called_once_with(["git", "-C", "/repo", "status", "--porcelain"], capture=True)
+ def test_update_repo_rejects_local_commits_without_force(self):
+ repo = repo_config(Path("/repo"))
+
+ with patch("aerie.sync.run") as run:
+ run.side_effect = [
+ subprocess.CompletedProcess(args=[], returncode=0, stdout=""),
+ subprocess.CompletedProcess(args=[], returncode=0, stdout=""),
+ subprocess.CompletedProcess(args=[], returncode=0, stdout="2\n"),
+ ]
+ with self.assertRaisesRegex(AerieError, "local HEAD has 2 commit"):
+ update_repo(repo, "develop")
+
+ self.assertEqual(
+ run.call_args_list,
+ [
+ call(
+ ["git", "-C", "/repo", "status", "--porcelain"],
+ capture=True,
+ ),
+ call(["git", "-C", "/repo", "fetch", "--prune", "origin"]),
+ call(
+ [
+ "git",
+ "-C",
+ "/repo",
+ "rev-list",
+ "--count",
+ "origin/develop..HEAD",
+ ],
+ capture=True,
+ ),
+ ],
+ )
+
def test_run_reports_captured_git_status_stderr(self):
with patch("aerie.sync.subprocess.run") as subprocess_run:
subprocess_run.side_effect = subprocess.CalledProcessError(
|