before-push 0.1.0__py3-none-any.whl

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.
@@ -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,9 @@
1
+ checks.py,sha256=FMzIPq4aorqzmpfY6K12GY6u9H7VJhVe8JS1E5NE1EA,4147
2
+ cli.py,sha256=UtJAxu-wmn-FmCPe_BBAEKslPQPQZ0rx9GZkYG_bbeU,550
3
+ git.py,sha256=zC3CdYiP87zuir1iTZe6m5jNMQ1BKeSIC9CmH6IsPGA,1566
4
+ output.py,sha256=tIv7OgyAbZ6zsNLqoYOu4tWE0ntaPazTmG_744rrD28,3734
5
+ before_push-0.1.0.dist-info/METADATA,sha256=ScmxitPQf8jDdExZY57dvtyUP0-cbLABWXgCrCx4kfk,268
6
+ before_push-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ before_push-0.1.0.dist-info/entry_points.txt,sha256=-_w-mJny_mlijddoGNJuqAr6aYQ0qYN1qFzDx9Kh_DU,40
8
+ before_push-0.1.0.dist-info/top_level.txt,sha256=frDQg37l8ZhH1hLVKWkyjppgqFphRtxPFsleaYhxKi0,22
9
+ before_push-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ beforepush = cli:main
@@ -0,0 +1,4 @@
1
+ checks
2
+ cli
3
+ git
4
+ output
checks.py ADDED
@@ -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
+ ]
cli.py ADDED
@@ -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()
git.py ADDED
@@ -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)
output.py ADDED
@@ -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)