before-push 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- before_push-0.1.0/PKG-INFO +9 -0
- before_push-0.1.0/README.md +1 -0
- before_push-0.1.0/pyproject.toml +20 -0
- before_push-0.1.0/setup.cfg +4 -0
- before_push-0.1.0/src/before_push.egg-info/PKG-INFO +9 -0
- before_push-0.1.0/src/before_push.egg-info/SOURCES.txt +15 -0
- before_push-0.1.0/src/before_push.egg-info/dependency_links.txt +1 -0
- before_push-0.1.0/src/before_push.egg-info/entry_points.txt +2 -0
- before_push-0.1.0/src/before_push.egg-info/requires.txt +1 -0
- before_push-0.1.0/src/before_push.egg-info/top_level.txt +4 -0
- before_push-0.1.0/src/checks.py +147 -0
- before_push-0.1.0/src/cli.py +25 -0
- before_push-0.1.0/src/git.py +60 -0
- before_push-0.1.0/src/output.py +153 -0
- before_push-0.1.0/tests/test_check.py +168 -0
- before_push-0.1.0/tests/test_cli.py +51 -0
- before_push-0.1.0/tests/test_git.py +122 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: before-push
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small CLI that checks your current Git branch before you push or open a pull request.
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: rich>=15.0.0
|
|
8
|
+
|
|
9
|
+
## BeforePush
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
## BeforePush
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "before-push"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A small CLI that checks your current Git branch before you push or open a pull request."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"rich>=15.0.0",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[dependency-groups]
|
|
12
|
+
dev = [
|
|
13
|
+
"pytest>=9.1.1",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
beforepush = "cli:main"
|
|
18
|
+
|
|
19
|
+
[tool.uv]
|
|
20
|
+
package = true
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: before-push
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small CLI that checks your current Git branch before you push or open a pull request.
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: rich>=15.0.0
|
|
8
|
+
|
|
9
|
+
## BeforePush
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/checks.py
|
|
4
|
+
src/cli.py
|
|
5
|
+
src/git.py
|
|
6
|
+
src/output.py
|
|
7
|
+
src/before_push.egg-info/PKG-INFO
|
|
8
|
+
src/before_push.egg-info/SOURCES.txt
|
|
9
|
+
src/before_push.egg-info/dependency_links.txt
|
|
10
|
+
src/before_push.egg-info/entry_points.txt
|
|
11
|
+
src/before_push.egg-info/requires.txt
|
|
12
|
+
src/before_push.egg-info/top_level.txt
|
|
13
|
+
tests/test_check.py
|
|
14
|
+
tests/test_cli.py
|
|
15
|
+
tests/test_git.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rich>=15.0.0
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
import git
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CheckStatus(Enum):
|
|
8
|
+
PASS = "pass"
|
|
9
|
+
WARNING = "warning"
|
|
10
|
+
FAIL = "fail"
|
|
11
|
+
|
|
12
|
+
# Check result data class
|
|
13
|
+
@dataclass
|
|
14
|
+
class CheckResult:
|
|
15
|
+
name: str
|
|
16
|
+
status: CheckStatus
|
|
17
|
+
message: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_git_repository() -> CheckResult:
|
|
21
|
+
"""Check whether the current directory is a Git repository."""
|
|
22
|
+
if git.is_git_repository():
|
|
23
|
+
return CheckResult(
|
|
24
|
+
name="Git repository",
|
|
25
|
+
status=CheckStatus.PASS,
|
|
26
|
+
message="Current directory is a Git repository.",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return CheckResult(
|
|
30
|
+
name="Git repository",
|
|
31
|
+
status=CheckStatus.FAIL,
|
|
32
|
+
message="Current directory is not a Git repository.",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_current_branch() -> CheckResult:
|
|
37
|
+
"""Check whether a branch is currently checked out."""
|
|
38
|
+
try:
|
|
39
|
+
branch = git.get_current_branch()
|
|
40
|
+
except RuntimeError:
|
|
41
|
+
return CheckResult(
|
|
42
|
+
name="Current branch",
|
|
43
|
+
status=CheckStatus.FAIL,
|
|
44
|
+
message="Could not determine the current branch.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if branch:
|
|
48
|
+
return CheckResult(
|
|
49
|
+
name="Current branch",
|
|
50
|
+
status=CheckStatus.PASS,
|
|
51
|
+
message=f"Currently on '{branch}'.",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return CheckResult(
|
|
55
|
+
name="Current branch",
|
|
56
|
+
status=CheckStatus.FAIL,
|
|
57
|
+
message="No branch is currently checked out.",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_working_tree() -> CheckResult:
|
|
62
|
+
"""Check whether the working tree is clean."""
|
|
63
|
+
try:
|
|
64
|
+
has_changes = git.has_changes()
|
|
65
|
+
except RuntimeError:
|
|
66
|
+
return CheckResult(
|
|
67
|
+
name="Working tree",
|
|
68
|
+
status=CheckStatus.FAIL,
|
|
69
|
+
message="Could not read the Git working tree.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not has_changes:
|
|
73
|
+
return CheckResult(
|
|
74
|
+
name="Working tree",
|
|
75
|
+
status=CheckStatus.PASS,
|
|
76
|
+
message="Working tree is clean.",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return CheckResult(
|
|
80
|
+
name="Working tree",
|
|
81
|
+
status=CheckStatus.FAIL,
|
|
82
|
+
message="Uncommitted or untracked changes detected.",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def check_upstream_branch() -> CheckResult:
|
|
86
|
+
"""Check whether the current branch has an upstream branch."""
|
|
87
|
+
try:
|
|
88
|
+
upstream = git.get_upstream_branch()
|
|
89
|
+
except RuntimeError:
|
|
90
|
+
return CheckResult(
|
|
91
|
+
name="Upstream branch",
|
|
92
|
+
status=CheckStatus.WARNING,
|
|
93
|
+
message="No upstream branch configured.",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return CheckResult(
|
|
97
|
+
name="Upstream branch",
|
|
98
|
+
status=CheckStatus.PASS,
|
|
99
|
+
message=f"Tracking '{upstream}'.",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def check_target_branch(target: str) -> CheckResult:
|
|
103
|
+
"""Check whether the current branch is behind the target branch."""
|
|
104
|
+
try:
|
|
105
|
+
current_branch = git.get_current_branch()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if current_branch == target:
|
|
109
|
+
return CheckResult(
|
|
110
|
+
name="Target branch",
|
|
111
|
+
status=CheckStatus.PASS,
|
|
112
|
+
message=f"Currently on target branch '{target}'.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
behind = git.get_behind_count(target)
|
|
116
|
+
|
|
117
|
+
except RuntimeError:
|
|
118
|
+
return CheckResult(
|
|
119
|
+
name="Target branch",
|
|
120
|
+
status=CheckStatus.FAIL,
|
|
121
|
+
message=f"Could not compare with '{target}'.",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if behind == 0:
|
|
125
|
+
return CheckResult(
|
|
126
|
+
name="Target branch",
|
|
127
|
+
status=CheckStatus.PASS,
|
|
128
|
+
message=f"Branch is up to date with {target}.",
|
|
129
|
+
)
|
|
130
|
+
commit_word = "commit" if behind == 1 else "commits"
|
|
131
|
+
return CheckResult(
|
|
132
|
+
name="Target branch",
|
|
133
|
+
status=CheckStatus.FAIL,
|
|
134
|
+
message=f"Branch is {behind} {commit_word} behind {target}. "
|
|
135
|
+
f"Update your branch before opening a PR.",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def run_checks(target: str) -> list[CheckResult]:
|
|
140
|
+
"""Run all repository readiness checks."""
|
|
141
|
+
return [
|
|
142
|
+
check_git_repository(),
|
|
143
|
+
check_current_branch(),
|
|
144
|
+
check_working_tree(),
|
|
145
|
+
check_upstream_branch(),
|
|
146
|
+
check_target_branch(target),
|
|
147
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from checks import run_checks
|
|
4
|
+
from output import display_results
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
description="Check whether your Git branch is ready before pushing or opening a PR."
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"--target",
|
|
14
|
+
default="main",
|
|
15
|
+
help="Target branch to compare against (default: main)",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
results = run_checks(args.target)
|
|
21
|
+
display_results(results, args.target)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def run_git_command(*args: str) -> str:
|
|
5
|
+
"""Run a Git command and return its output."""
|
|
6
|
+
result = subprocess.run(
|
|
7
|
+
["git", *args],
|
|
8
|
+
capture_output=True,
|
|
9
|
+
text=True,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
if result.returncode != 0:
|
|
13
|
+
raise RuntimeError(result.stderr.strip())
|
|
14
|
+
|
|
15
|
+
return result.stdout.strip()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_git_repository() -> bool:
|
|
19
|
+
"""Check whether the current directory is a Git repository."""
|
|
20
|
+
result = subprocess.run(
|
|
21
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
22
|
+
capture_output=True,
|
|
23
|
+
text=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return result.returncode == 0 and result.stdout.strip() == "true"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_current_branch() -> str:
|
|
30
|
+
"""Get the current branch name."""
|
|
31
|
+
return run_git_command("branch", "--show-current")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_status() -> str:
|
|
35
|
+
"""Get the repository status in porcelain format."""
|
|
36
|
+
return run_git_command("status", "--porcelain")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def has_changes() -> bool:
|
|
40
|
+
"""Check whether the working tree contains changes."""
|
|
41
|
+
return bool(get_status())
|
|
42
|
+
|
|
43
|
+
def get_upstream_branch() -> str:
|
|
44
|
+
"""Get the upstream branch configured for the current branch."""
|
|
45
|
+
return run_git_command(
|
|
46
|
+
"rev-parse",
|
|
47
|
+
"--abbrev-ref",
|
|
48
|
+
"--symbolic-full-name",
|
|
49
|
+
"@{u}",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def get_behind_count(target: str) -> int:
|
|
53
|
+
"""Return the number of commits the current branch is behind the target."""
|
|
54
|
+
output = run_git_command(
|
|
55
|
+
"rev-list",
|
|
56
|
+
"--count",
|
|
57
|
+
f"HEAD..{target}",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return int(output)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from checks import CheckResult, CheckStatus
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def print_header(target: str) -> None:
|
|
12
|
+
"""Print the application header."""
|
|
13
|
+
title = Text()
|
|
14
|
+
title.append("BeforePush", style="bold cyan")
|
|
15
|
+
title.append("\nGit branch readiness check", style="dim")
|
|
16
|
+
|
|
17
|
+
console.print()
|
|
18
|
+
console.print(
|
|
19
|
+
Panel(
|
|
20
|
+
title,
|
|
21
|
+
border_style="bright_blue",
|
|
22
|
+
padding=(0, 2),
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
console.print(f" [dim]Target[/dim] [bold]{target}[/bold]")
|
|
27
|
+
console.print()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_check_display(result: CheckResult) -> tuple[str, str]:
|
|
31
|
+
"""Return the symbol and style for a check result."""
|
|
32
|
+
if result.status == CheckStatus.PASS:
|
|
33
|
+
return "✓", "green"
|
|
34
|
+
|
|
35
|
+
if result.status == CheckStatus.WARNING:
|
|
36
|
+
return "⚠", "yellow"
|
|
37
|
+
|
|
38
|
+
return "✗", "red"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def print_check(result: CheckResult, animate: bool = True) -> None:
|
|
42
|
+
"""Print a single check result."""
|
|
43
|
+
symbol, style = get_check_display(result)
|
|
44
|
+
|
|
45
|
+
if animate and console.is_terminal:
|
|
46
|
+
with console.status(
|
|
47
|
+
f" [dim]Checking {result.name.lower()}...[/dim]",
|
|
48
|
+
spinner="dots",
|
|
49
|
+
):
|
|
50
|
+
time.sleep(0.25)
|
|
51
|
+
|
|
52
|
+
console.print(
|
|
53
|
+
f" [{style}]{symbol}[/{style}] "
|
|
54
|
+
f"[bold]{result.name}[/bold]"
|
|
55
|
+
)
|
|
56
|
+
console.print(
|
|
57
|
+
f" [dim]{result.message}[/dim]"
|
|
58
|
+
)
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def print_summary(
|
|
63
|
+
results: list[CheckResult],
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Print the final readiness summary."""
|
|
66
|
+
failed = sum(
|
|
67
|
+
result.status == CheckStatus.FAIL
|
|
68
|
+
for result in results
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
warnings = sum(
|
|
72
|
+
result.status == CheckStatus.WARNING
|
|
73
|
+
for result in results
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
console.print(" [dim]" + "─" * 44 + "[/dim]")
|
|
77
|
+
console.print()
|
|
78
|
+
|
|
79
|
+
if failed == 0 and warnings == 0:
|
|
80
|
+
summary = Text()
|
|
81
|
+
summary.append("✓ READY\n", style="bold green")
|
|
82
|
+
summary.append(
|
|
83
|
+
"All checks passed. Safe to push.",
|
|
84
|
+
style="green",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
console.print(
|
|
88
|
+
Panel(
|
|
89
|
+
summary,
|
|
90
|
+
border_style="green",
|
|
91
|
+
padding=(0, 2),
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
parts = []
|
|
97
|
+
|
|
98
|
+
if failed:
|
|
99
|
+
word = "check" if failed == 1 else "checks"
|
|
100
|
+
parts.append(f"{failed} {word} failed")
|
|
101
|
+
|
|
102
|
+
if warnings:
|
|
103
|
+
word = "warning" if warnings == 1 else "warnings"
|
|
104
|
+
parts.append(f"{warnings} {word}")
|
|
105
|
+
|
|
106
|
+
summary = Text()
|
|
107
|
+
summary.append("✗ NOT READY\n", style="bold red")
|
|
108
|
+
summary.append(
|
|
109
|
+
f"{' · '.join(parts)}.",
|
|
110
|
+
style="red",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
console.print(
|
|
114
|
+
Panel(
|
|
115
|
+
summary,
|
|
116
|
+
border_style="red",
|
|
117
|
+
padding=(0, 2),
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
console.print()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def print_results(
|
|
125
|
+
results: list[CheckResult],
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Print all check results and the final status."""
|
|
128
|
+
console.print(" [bold]Checks[/bold]")
|
|
129
|
+
console.print()
|
|
130
|
+
|
|
131
|
+
animate = console.is_terminal
|
|
132
|
+
|
|
133
|
+
for result in results:
|
|
134
|
+
print_check(result, animate=animate)
|
|
135
|
+
|
|
136
|
+
print_summary(results)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def display_results(
|
|
140
|
+
results: list[CheckResult],
|
|
141
|
+
target: str,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Display the complete check report."""
|
|
144
|
+
print_header(target)
|
|
145
|
+
|
|
146
|
+
if console.is_terminal:
|
|
147
|
+
with console.status(
|
|
148
|
+
"[bold cyan]Running checks...[/bold cyan]",
|
|
149
|
+
spinner="dots",
|
|
150
|
+
):
|
|
151
|
+
time.sleep(0.5)
|
|
152
|
+
|
|
153
|
+
print_results(results)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import git
|
|
2
|
+
|
|
3
|
+
from checks import (
|
|
4
|
+
CheckStatus,
|
|
5
|
+
check_current_branch,
|
|
6
|
+
check_git_repository,
|
|
7
|
+
check_target_branch,
|
|
8
|
+
check_upstream_branch,
|
|
9
|
+
check_working_tree,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_git_repository_pass(monkeypatch):
|
|
14
|
+
monkeypatch.setattr(git, "is_git_repository", lambda: True)
|
|
15
|
+
|
|
16
|
+
result = check_git_repository()
|
|
17
|
+
|
|
18
|
+
assert result.status == CheckStatus.PASS
|
|
19
|
+
assert result.name == "Git repository"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_git_repository_fail(monkeypatch):
|
|
23
|
+
monkeypatch.setattr(git, "is_git_repository", lambda: False)
|
|
24
|
+
|
|
25
|
+
result = check_git_repository()
|
|
26
|
+
|
|
27
|
+
assert result.status == CheckStatus.FAIL
|
|
28
|
+
assert result.name == "Git repository"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_current_branch_pass(monkeypatch):
|
|
32
|
+
monkeypatch.setattr(
|
|
33
|
+
git,
|
|
34
|
+
"get_current_branch",
|
|
35
|
+
lambda: "feature/test",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
result = check_current_branch()
|
|
39
|
+
|
|
40
|
+
assert result.status == CheckStatus.PASS
|
|
41
|
+
assert "feature/test" in result.message
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_current_branch_fail(monkeypatch):
|
|
45
|
+
monkeypatch.setattr(
|
|
46
|
+
git,
|
|
47
|
+
"get_current_branch",
|
|
48
|
+
lambda: "",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
result = check_current_branch()
|
|
52
|
+
|
|
53
|
+
assert result.status == CheckStatus.FAIL
|
|
54
|
+
assert "No branch" in result.message
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_working_tree_clean(monkeypatch):
|
|
58
|
+
monkeypatch.setattr(git, "has_changes", lambda: False)
|
|
59
|
+
|
|
60
|
+
result = check_working_tree()
|
|
61
|
+
|
|
62
|
+
assert result.status == CheckStatus.PASS
|
|
63
|
+
assert "clean" in result.message
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_working_tree_has_changes(monkeypatch):
|
|
67
|
+
monkeypatch.setattr(git, "has_changes", lambda: True)
|
|
68
|
+
|
|
69
|
+
result = check_working_tree()
|
|
70
|
+
|
|
71
|
+
assert result.status == CheckStatus.FAIL
|
|
72
|
+
assert "changes detected" in result.message
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_upstream_branch_pass(monkeypatch):
|
|
76
|
+
monkeypatch.setattr(
|
|
77
|
+
git,
|
|
78
|
+
"get_upstream_branch",
|
|
79
|
+
lambda: "origin/main",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
result = check_upstream_branch()
|
|
83
|
+
|
|
84
|
+
assert result.status == CheckStatus.PASS
|
|
85
|
+
assert "origin/main" in result.message
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_upstream_branch_warning(monkeypatch):
|
|
89
|
+
def raise_error():
|
|
90
|
+
raise RuntimeError("No upstream")
|
|
91
|
+
|
|
92
|
+
monkeypatch.setattr(
|
|
93
|
+
git,
|
|
94
|
+
"get_upstream_branch",
|
|
95
|
+
raise_error,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
result = check_upstream_branch()
|
|
99
|
+
|
|
100
|
+
assert result.status == CheckStatus.WARNING
|
|
101
|
+
assert "No upstream branch" in result.message
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_target_branch_current(monkeypatch):
|
|
105
|
+
monkeypatch.setattr(
|
|
106
|
+
git,
|
|
107
|
+
"get_current_branch",
|
|
108
|
+
lambda: "main",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
result = check_target_branch("main")
|
|
112
|
+
|
|
113
|
+
assert result.status == CheckStatus.PASS
|
|
114
|
+
assert "target branch 'main'" in result.message
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_target_branch_up_to_date(monkeypatch):
|
|
118
|
+
monkeypatch.setattr(
|
|
119
|
+
git,
|
|
120
|
+
"get_current_branch",
|
|
121
|
+
lambda: "feature/test",
|
|
122
|
+
)
|
|
123
|
+
monkeypatch.setattr(
|
|
124
|
+
git,
|
|
125
|
+
"get_behind_count",
|
|
126
|
+
lambda target: 0,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
result = check_target_branch("main")
|
|
130
|
+
|
|
131
|
+
assert result.status == CheckStatus.PASS
|
|
132
|
+
assert "up to date" in result.message
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_target_branch_behind_one_commit(monkeypatch):
|
|
136
|
+
monkeypatch.setattr(
|
|
137
|
+
git,
|
|
138
|
+
"get_current_branch",
|
|
139
|
+
lambda: "feature/test",
|
|
140
|
+
)
|
|
141
|
+
monkeypatch.setattr(
|
|
142
|
+
git,
|
|
143
|
+
"get_behind_count",
|
|
144
|
+
lambda target: 1,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
result = check_target_branch("main")
|
|
148
|
+
|
|
149
|
+
assert result.status == CheckStatus.FAIL
|
|
150
|
+
assert "1 commit behind main" in result.message
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_target_branch_behind_multiple_commits(monkeypatch):
|
|
154
|
+
monkeypatch.setattr(
|
|
155
|
+
git,
|
|
156
|
+
"get_current_branch",
|
|
157
|
+
lambda: "feature/test",
|
|
158
|
+
)
|
|
159
|
+
monkeypatch.setattr(
|
|
160
|
+
git,
|
|
161
|
+
"get_behind_count",
|
|
162
|
+
lambda target: 3,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
result = check_target_branch("main")
|
|
166
|
+
|
|
167
|
+
assert result.status == CheckStatus.FAIL
|
|
168
|
+
assert "3 commits behind main" in result.message
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import cli
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_cli_uses_main_by_default(monkeypatch):
|
|
5
|
+
captured = {}
|
|
6
|
+
|
|
7
|
+
def fake_run_checks(target):
|
|
8
|
+
captured["target"] = target
|
|
9
|
+
return []
|
|
10
|
+
|
|
11
|
+
def fake_display_results(results, target):
|
|
12
|
+
captured["results"] = results
|
|
13
|
+
captured["display_target"] = target
|
|
14
|
+
|
|
15
|
+
monkeypatch.setattr(cli, "run_checks", fake_run_checks)
|
|
16
|
+
monkeypatch.setattr(cli, "display_results", fake_display_results)
|
|
17
|
+
|
|
18
|
+
monkeypatch.setattr(
|
|
19
|
+
"sys.argv",
|
|
20
|
+
["beforepush"],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
cli.main()
|
|
24
|
+
|
|
25
|
+
assert captured["target"] == "main"
|
|
26
|
+
assert captured["display_target"] == "main"
|
|
27
|
+
assert captured["results"] == []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_cli_accepts_custom_target(monkeypatch):
|
|
31
|
+
captured = {}
|
|
32
|
+
|
|
33
|
+
def fake_run_checks(target):
|
|
34
|
+
captured["target"] = target
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
def fake_display_results(results, target):
|
|
38
|
+
captured["display_target"] = target
|
|
39
|
+
|
|
40
|
+
monkeypatch.setattr(cli, "run_checks", fake_run_checks)
|
|
41
|
+
monkeypatch.setattr(cli, "display_results", fake_display_results)
|
|
42
|
+
|
|
43
|
+
monkeypatch.setattr(
|
|
44
|
+
"sys.argv",
|
|
45
|
+
["beforepush", "--target", "develop"],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
cli.main()
|
|
49
|
+
|
|
50
|
+
assert captured["target"] == "develop"
|
|
51
|
+
assert captured["display_target"] == "develop"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import pytest
|
|
3
|
+
import git
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_run_git_command_success(monkeypatch):
|
|
7
|
+
def fake_run(*args, **kwargs):
|
|
8
|
+
return subprocess.CompletedProcess(
|
|
9
|
+
args=["git", "status"],
|
|
10
|
+
returncode=0,
|
|
11
|
+
stdout="clean",
|
|
12
|
+
stderr="",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
monkeypatch.setattr(subprocess, "run", fake_run)
|
|
16
|
+
|
|
17
|
+
result = git.run_git_command("status")
|
|
18
|
+
|
|
19
|
+
assert result == "clean"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_run_git_command_failure(monkeypatch):
|
|
23
|
+
def fake_run(*args, **kwargs):
|
|
24
|
+
return subprocess.CompletedProcess(
|
|
25
|
+
args=["git", "status"],
|
|
26
|
+
returncode=1,
|
|
27
|
+
stdout="",
|
|
28
|
+
stderr="fatal: not a git repository",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr(subprocess, "run", fake_run)
|
|
32
|
+
|
|
33
|
+
with pytest.raises(RuntimeError, match="not a git repository"):
|
|
34
|
+
git.run_git_command("status")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_is_git_repository_true(monkeypatch):
|
|
38
|
+
def fake_run(*args, **kwargs):
|
|
39
|
+
return subprocess.CompletedProcess(
|
|
40
|
+
args=["git", "rev-parse"],
|
|
41
|
+
returncode=0,
|
|
42
|
+
stdout="true\n",
|
|
43
|
+
stderr="",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
monkeypatch.setattr(subprocess, "run", fake_run)
|
|
47
|
+
|
|
48
|
+
assert git.is_git_repository() is True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_is_git_repository_false(monkeypatch):
|
|
52
|
+
def fake_run(*args, **kwargs):
|
|
53
|
+
return subprocess.CompletedProcess(
|
|
54
|
+
args=["git", "rev-parse"],
|
|
55
|
+
returncode=128,
|
|
56
|
+
stdout="",
|
|
57
|
+
stderr="fatal: not a git repository",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
monkeypatch.setattr(subprocess, "run", fake_run)
|
|
61
|
+
|
|
62
|
+
assert git.is_git_repository() is False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_get_current_branch(monkeypatch):
|
|
66
|
+
monkeypatch.setattr(
|
|
67
|
+
git,
|
|
68
|
+
"run_git_command",
|
|
69
|
+
lambda *args: "feature/test",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
assert git.get_current_branch() == "feature/test"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_get_status(monkeypatch):
|
|
76
|
+
monkeypatch.setattr(
|
|
77
|
+
git,
|
|
78
|
+
"run_git_command",
|
|
79
|
+
lambda *args: "M src/checks.py",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert git.get_status() == "M src/checks.py"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_has_changes_true(monkeypatch):
|
|
86
|
+
monkeypatch.setattr(
|
|
87
|
+
git,
|
|
88
|
+
"get_status",
|
|
89
|
+
lambda: " M src/checks.py",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assert git.has_changes() is True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_has_changes_false(monkeypatch):
|
|
96
|
+
monkeypatch.setattr(
|
|
97
|
+
git,
|
|
98
|
+
"get_status",
|
|
99
|
+
lambda: "",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
assert git.has_changes() is False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_get_upstream_branch(monkeypatch):
|
|
106
|
+
monkeypatch.setattr(
|
|
107
|
+
git,
|
|
108
|
+
"run_git_command",
|
|
109
|
+
lambda *args: "origin/main",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
assert git.get_upstream_branch() == "origin/main"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_get_behind_count(monkeypatch):
|
|
116
|
+
monkeypatch.setattr(
|
|
117
|
+
git,
|
|
118
|
+
"run_git_command",
|
|
119
|
+
lambda *args: "3",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
assert git.get_behind_count("main") == 3
|