gitpush-tool 0.4.0__tar.gz → 0.4.1__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.
Files changed (24) hide show
  1. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/LONG_DESCRIPTION.md +35 -0
  2. {gitpush_tool-0.4.0/gitpush_tool.egg-info → gitpush_tool-0.4.1}/PKG-INFO +36 -1
  3. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/README.md +37 -0
  4. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/gitpush/__doc__.py +2 -1
  5. gitpush_tool-0.4.1/gitpush/__init__.py +1 -0
  6. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/gitpush/cli.py +10 -1
  7. gitpush_tool-0.4.1/gitpush/status.py +369 -0
  8. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1/gitpush_tool.egg-info}/PKG-INFO +36 -1
  9. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/gitpush_tool.egg-info/SOURCES.txt +3 -1
  10. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/gitpush_tool.egg-info/entry_points.txt +1 -0
  11. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/setup.py +4 -7
  12. gitpush_tool-0.4.1/tests/test_cli.py +0 -0
  13. gitpush_tool-0.4.1/tests/test_safety.py +0 -0
  14. gitpush_tool-0.4.1/tests/test_status.py +113 -0
  15. gitpush_tool-0.4.0/gitpush/__init__.py +0 -1
  16. gitpush_tool-0.4.0/tests/test_cli.py +0 -25
  17. gitpush_tool-0.4.0/tests/test_safety.py +0 -169
  18. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/LICENSE +0 -0
  19. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/MANIFEST.in +0 -0
  20. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/gitpush/safety.py +0 -0
  21. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/gitpush_tool.egg-info/dependency_links.txt +0 -0
  22. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/gitpush_tool.egg-info/top_level.txt +0 -0
  23. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/pyproject.toml +0 -0
  24. {gitpush_tool-0.4.0 → gitpush_tool-0.4.1}/setup.cfg +0 -0
@@ -9,6 +9,7 @@ A supercharged Git CLI and safety tool that simplifies repository creation and p
9
9
 
10
10
  ## ✨ Features
11
11
 
12
+ - **Decision-Support Status Dashboard (`gitpush status`):** Actionable repository overview showing worktree breakdown, commit sync, intelligent next-step recommendations, and a 0-100 Repo Health score.
12
13
  - **Pre-Push Safety & Diffstat Preview:** See changed files, line additions/deletions (`+X, -Y`), ahead/behind status, and pending commits before pushing.
13
14
  - **Dangerous Git Operation Guard:** Analyzes and protects against risky actions (`reset --hard`, force pushes, deleting unmerged branches, dirty `git clean`, detached HEADs).
14
15
  - **Protected Branch Safeguard:** Automatically detects and alerts when pushing or force-pushing to `main`, `master`, `prod`, `release`, or `dev`.
@@ -19,6 +20,40 @@ A supercharged Git CLI and safety tool that simplifies repository creation and p
19
20
 
20
21
  ---
21
22
 
23
+ ## 📊 Decision-Support Status Dashboard (`gitpush status`)
24
+
25
+ ```text
26
+ Repository: my-project
27
+ Branch: feature/payment
28
+ Tracking: origin/feature/payment
29
+
30
+ WORKTREE
31
+ ────────────────────────
32
+ Modified 4
33
+ Added 2
34
+ Deleted 1
35
+ Untracked 3
36
+ Line Changes +142, -18
37
+
38
+ COMMITS
39
+ ────────────────────────
40
+ Ahead 3
41
+ Behind 1
42
+
43
+ PUSH / STATUS
44
+ ────────────────────────
45
+ ⚠ Remote has 1 commit(s) you don't have locally.
46
+
47
+ Recommended:
48
+ git pull --rebase
49
+ gitpush "Commit message"
50
+
51
+ Health:
52
+ 82/100 [████████░░] Good (Minor Action Needed)
53
+ ```
54
+
55
+ ---
56
+
22
57
  ## 🛡️ Pre-Push Warning & Inspection
23
58
 
24
59
  Whenever you run `gitpush`, you get an instant, clear breakdown of your repository state:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpush-tool
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Supercharged Git push tool with automatic GitHub repo creation and pushing
5
5
  Home-page: https://github.com/inevitablegs/gitpush
6
6
  Author: Ganesh Sonawane
@@ -38,6 +38,7 @@ A supercharged Git CLI and safety tool that simplifies repository creation and p
38
38
 
39
39
  ## ✨ Features
40
40
 
41
+ - **Decision-Support Status Dashboard (`gitpush status`):** Actionable repository overview showing worktree breakdown, commit sync, intelligent next-step recommendations, and a 0-100 Repo Health score.
41
42
  - **Pre-Push Safety & Diffstat Preview:** See changed files, line additions/deletions (`+X, -Y`), ahead/behind status, and pending commits before pushing.
42
43
  - **Dangerous Git Operation Guard:** Analyzes and protects against risky actions (`reset --hard`, force pushes, deleting unmerged branches, dirty `git clean`, detached HEADs).
43
44
  - **Protected Branch Safeguard:** Automatically detects and alerts when pushing or force-pushing to `main`, `master`, `prod`, `release`, or `dev`.
@@ -48,6 +49,40 @@ A supercharged Git CLI and safety tool that simplifies repository creation and p
48
49
 
49
50
  ---
50
51
 
52
+ ## 📊 Decision-Support Status Dashboard (`gitpush status`)
53
+
54
+ ```text
55
+ Repository: my-project
56
+ Branch: feature/payment
57
+ Tracking: origin/feature/payment
58
+
59
+ WORKTREE
60
+ ────────────────────────
61
+ Modified 4
62
+ Added 2
63
+ Deleted 1
64
+ Untracked 3
65
+ Line Changes +142, -18
66
+
67
+ COMMITS
68
+ ────────────────────────
69
+ Ahead 3
70
+ Behind 1
71
+
72
+ PUSH / STATUS
73
+ ────────────────────────
74
+ ⚠ Remote has 1 commit(s) you don't have locally.
75
+
76
+ Recommended:
77
+ git pull --rebase
78
+ gitpush "Commit message"
79
+
80
+ Health:
81
+ 82/100 [████████░░] Good (Minor Action Needed)
82
+ ```
83
+
84
+ ---
85
+
51
86
  ## 🛡️ Pre-Push Warning & Inspection
52
87
 
53
88
  Whenever you run `gitpush`, you get an instant, clear breakdown of your repository state:
@@ -9,6 +9,7 @@ A supercharged Git CLI and safety tool that simplifies repository creation and p
9
9
 
10
10
  ## ✨ Features
11
11
 
12
+ - **Decision-Support Status Dashboard (`gitpush status`):** Actionable repository overview showing worktree breakdown, commit sync, intelligent next-step recommendations, and a 0-100 Repo Health score.
12
13
  - **Pre-Push Safety & Diffstat Preview:** See changed files, line additions/deletions (`+X, -Y`), ahead/behind status, and pending commits before pushing.
13
14
  - **Dangerous Git Operation Guard:** Analyzes and protects against risky actions (`reset --hard`, force pushes, deleting unmerged branches, dirty `git clean`, detached HEADs).
14
15
  - **Protected Branch Safeguard:** Automatically detects and alerts when pushing or force-pushing to `main`, `master`, `prod`, `release`, or `dev`.
@@ -27,6 +28,42 @@ pip install gitpush-tool
27
28
 
28
29
  ---
29
30
 
31
+ ## 📊 Decision-Support Status Dashboard (`gitpush status`)
32
+
33
+ Instead of raw, cryptic Git output, `gitpush status` gives you structured decision support:
34
+
35
+ ```text
36
+ Repository: my-project
37
+ Branch: feature/payment
38
+ Tracking: origin/feature/payment
39
+
40
+ WORKTREE
41
+ ────────────────────────
42
+ Modified 4
43
+ Added 2
44
+ Deleted 1
45
+ Untracked 3
46
+ Line Changes +142, -18
47
+
48
+ COMMITS
49
+ ────────────────────────
50
+ Ahead 3
51
+ Behind 1
52
+
53
+ PUSH / STATUS
54
+ ────────────────────────
55
+ ⚠ Remote has 1 commit(s) you don't have locally.
56
+
57
+ Recommended:
58
+ git pull --rebase
59
+ gitpush "Commit message"
60
+
61
+ Health:
62
+ 82/100 [████████░░] Good (Minor Action Needed)
63
+ ```
64
+
65
+ ---
66
+
30
67
  ## 🛡️ Pre-Push Warning & Inspection
31
68
 
32
69
  Whenever you run `gitpush`, you get an instant, clear breakdown of your repository state:
@@ -1,5 +1,5 @@
1
1
  """
2
- GitPush Tool - Supercharged Git CLI (v0.4.0)
2
+ GitPush Tool - Supercharged Git CLI (v0.4.1)
3
3
 
4
4
  A powerful command-line tool that simplifies Git operations with automatic GitHub
5
5
  repository creation, pre-push diffstat inspection, and dangerous operation safeguards.
@@ -14,6 +14,7 @@ Key Features:
14
14
  • GitHub CLI integration
15
15
 
16
16
  Basic Usage:
17
+ gitpush status # Decision-support repository status
17
18
  gitpush "Commit message" # Standard push with preview
18
19
  gitpush "Commit message" -y # Push without confirmation prompt
19
20
  gitpush --dry-run # Preview changes without pushing
@@ -0,0 +1 @@
1
+ __version__ = "0.4.1"
@@ -45,6 +45,8 @@ from .safety import (
45
45
  Style
46
46
  )
47
47
 
48
+ from .status import format_status_dashboard
49
+
48
50
 
49
51
  # --- GitHub CLI Installation Orchestrator and Helpers ---
50
52
 
@@ -684,6 +686,12 @@ def handle_guard_command(raw_args: List[str], auto_confirm: bool = False) -> int
684
686
  # --- Main Entry Point ---
685
687
 
686
688
  def run():
689
+ # Handle status subcommand upfront if detected
690
+ if len(sys.argv) > 1 and sys.argv[1].lower() == "status":
691
+ verbose = "-v" in sys.argv or "--verbose" in sys.argv
692
+ print(format_status_dashboard(verbose=verbose))
693
+ sys.exit(0)
694
+
687
695
  # Handle guard subcommand upfront if detected
688
696
  if len(sys.argv) > 1 and sys.argv[1].lower() in ("guard", "check", "verify-danger"):
689
697
  guard_args = sys.argv[2:]
@@ -692,9 +700,10 @@ def run():
692
700
  sys.exit(handle_guard_command(clean_args, auto_confirm=auto_confirm))
693
701
 
694
702
  parser = argparse.ArgumentParser(
695
- description="🚀 Supercharged Git push tool with GitHub repo creation, diff previews, and safety guards",
703
+ description="🚀 Supercharged Git push tool with decision-support status dashboard and safety guards",
696
704
  formatter_class=argparse.RawDescriptionHelpFormatter,
697
705
  epilog="""Examples:
706
+ Decision status: gitpush status
698
707
  Standard push: gitpush "My new feature"
699
708
  Push without prompt: gitpush "My new feature" -y
700
709
  Dry run preview: gitpush "My new feature" --dry-run
@@ -0,0 +1,369 @@
1
+ """
2
+ GitPush Decision-Support Status Dashboard
3
+ Provides a rich, actionable overview of repository health, worktree breakdown,
4
+ commit sync state, and intelligent contextual recommendations.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import subprocess
10
+ from typing import Optional, Dict, List, Tuple, Any
11
+
12
+ from .safety import (
13
+ is_git_repo,
14
+ get_repo_name,
15
+ get_repo_root,
16
+ get_current_branch,
17
+ is_detached_head,
18
+ is_protected_branch,
19
+ get_ahead_behind,
20
+ get_diffstat_summary,
21
+ run_git,
22
+ colorize,
23
+ Style
24
+ )
25
+
26
+
27
+ def get_tracking_branch(cwd: Optional[str] = None) -> Optional[str]:
28
+ """Get the upstream tracking branch for the current HEAD, e.g. origin/main."""
29
+ res = run_git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd=cwd)
30
+ if res.returncode == 0 and res.stdout.strip():
31
+ return res.stdout.strip()
32
+ return None
33
+
34
+
35
+ def get_stash_count(cwd: Optional[str] = None) -> int:
36
+ """Get count of stashes in the stash list."""
37
+ res = run_git(["stash", "list"], cwd=cwd)
38
+ if res.returncode == 0 and res.stdout.strip():
39
+ return len(res.stdout.strip().splitlines())
40
+ return 0
41
+
42
+
43
+ def get_worktree_breakdown(cwd: Optional[str] = None) -> Dict[str, int]:
44
+ """
45
+ Categorizes working tree changes into:
46
+ - modified
47
+ - added (staged new files)
48
+ - deleted
49
+ - untracked
50
+ - renamed
51
+ - unmerged (conflicts)
52
+ """
53
+ counts = {
54
+ "modified": 0,
55
+ "added": 0,
56
+ "deleted": 0,
57
+ "untracked": 0,
58
+ "renamed": 0,
59
+ "unmerged": 0
60
+ }
61
+
62
+ res = run_git(["status", "--porcelain=v1", "-uall"], cwd=cwd)
63
+ if res.returncode != 0 or not res.stdout.strip():
64
+ return counts
65
+
66
+ for line in res.stdout.splitlines():
67
+ if len(line) < 3:
68
+ continue
69
+ index_code = line[0]
70
+ work_code = line[1]
71
+
72
+ # Check unmerged / conflicts
73
+ if index_code in ('U', 'A', 'D') and work_code == 'U' or (index_code == 'U' and work_code in ('U', 'A', 'D')) or (index_code == 'A' and work_code == 'A') or (index_code == 'D' and work_code == 'D'):
74
+ counts["unmerged"] += 1
75
+ continue
76
+
77
+ # Untracked
78
+ if index_code == '?' and work_code == '?':
79
+ counts["untracked"] += 1
80
+ continue
81
+
82
+ # Added / New in index
83
+ if index_code == 'A':
84
+ counts["added"] += 1
85
+ if work_code == 'M':
86
+ counts["modified"] += 1
87
+ elif work_code == 'D':
88
+ counts["deleted"] += 1
89
+ continue
90
+
91
+ # Deleted
92
+ if index_code == 'D' or work_code == 'D':
93
+ counts["deleted"] += 1
94
+ continue
95
+
96
+ # Renamed
97
+ if index_code == 'R' or work_code == 'R':
98
+ counts["renamed"] += 1
99
+ continue
100
+
101
+ # Modified
102
+ if index_code == 'M' or work_code == 'M':
103
+ counts["modified"] += 1
104
+ continue
105
+
106
+ return counts
107
+
108
+
109
+ def calculate_repo_health(
110
+ detached: bool,
111
+ unmerged_count: int,
112
+ behind_count: int,
113
+ diverged: bool,
114
+ modified_count: int,
115
+ untracked_count: int,
116
+ stash_count: int,
117
+ is_protected: bool
118
+ ) -> Tuple[int, str, str]:
119
+ """
120
+ Computes a repository health score between 0 and 100 with a label and color style.
121
+ Returns: (score, label, color_style)
122
+ """
123
+ score = 100
124
+
125
+ # Severe issues
126
+ if unmerged_count > 0:
127
+ score -= min(40, unmerged_count * 20)
128
+ if detached:
129
+ score -= 25
130
+ if diverged:
131
+ score -= 20
132
+ elif behind_count > 0:
133
+ score -= min(15, behind_count * 3)
134
+
135
+ # Worktree hygiene
136
+ total_dirty = modified_count + untracked_count
137
+ if total_dirty > 20:
138
+ score -= 15
139
+ elif total_dirty > 10:
140
+ score -= 10
141
+ elif total_dirty > 5:
142
+ score -= 5
143
+
144
+ # Stash buildup
145
+ if stash_count > 10:
146
+ score -= 10
147
+ elif stash_count > 5:
148
+ score -= 5
149
+
150
+ # Clamp between 0 and 100
151
+ score = max(0, min(100, score))
152
+
153
+ if score >= 90:
154
+ label = "Excellent (Clean & Ready)"
155
+ color = Style.GREEN
156
+ elif score >= 75:
157
+ label = "Good (Minor Action Needed)"
158
+ color = Style.CYAN
159
+ elif score >= 50:
160
+ label = "Attention Needed"
161
+ color = Style.YELLOW
162
+ else:
163
+ label = "Critical (Conflicts / Detached)"
164
+ color = Style.RED
165
+
166
+ return score, label, color
167
+
168
+
169
+ def get_decision_recommendations(
170
+ branch: str,
171
+ tracking: Optional[str],
172
+ ahead: int,
173
+ behind: int,
174
+ diverged: bool,
175
+ detached: bool,
176
+ worktree: Dict[str, int],
177
+ is_protected: bool
178
+ ) -> Tuple[List[str], List[str]]:
179
+ """
180
+ Generates actionable, contextual push/pull notes and recommended next commands.
181
+ Returns: (status_notes, recommended_commands)
182
+ """
183
+ status_notes = []
184
+ recommended = []
185
+
186
+ total_worktree_changes = sum(worktree.values())
187
+
188
+ # 1. Unmerged / Conflicts
189
+ if worktree.get("unmerged", 0) > 0:
190
+ status_notes.append(colorize(f"❌ {worktree['unmerged']} merge conflict(s) require resolution.", Style.BOLD, Style.RED))
191
+ recommended.append("Resolve conflict markers in affected files, then run: git add .")
192
+ recommended.append("Continue rebase/merge with: git rebase --continue (or git commit)")
193
+ return status_notes, recommended
194
+
195
+ # 2. Detached HEAD
196
+ if detached:
197
+ status_notes.append(colorize("⚠ You are in a detached HEAD state. Commits made here are not on a branch.", Style.YELLOW))
198
+ recommended.append("Create a new branch to keep your work: git switch -c feature/my-new-branch")
199
+ return status_notes, recommended
200
+
201
+ # 3. Remote Tracking & Sync
202
+ if tracking is None:
203
+ status_notes.append("Branch has no remote tracking branch set.")
204
+ if total_worktree_changes > 0:
205
+ recommended.append(f'gitpush "Commit message" {branch} origin')
206
+ else:
207
+ recommended.append(f"git push -u origin {branch}")
208
+ elif diverged:
209
+ status_notes.append(colorize(f"⚠ Branch has diverged from '{tracking}' ({ahead} ahead, {behind} behind).", Style.YELLOW))
210
+ recommended.append("Rebase onto remote changes: git pull --rebase")
211
+ recommended.append(f'Then push your changes: gitpush')
212
+ elif behind > 0:
213
+ status_notes.append(colorize(f"⚠ Remote has {behind} commit(s) you don't have locally.", Style.YELLOW))
214
+ recommended.append("git pull --rebase")
215
+ if ahead > 0 or total_worktree_changes > 0:
216
+ recommended.append(f'gitpush "Commit message"')
217
+ elif ahead > 0:
218
+ if total_worktree_changes > 0:
219
+ status_notes.append(f"Ready to commit {total_worktree_changes} change(s) and push {ahead} commit(s).")
220
+ recommended.append(f'gitpush "Commit message"')
221
+ else:
222
+ status_notes.append(colorize(f"✔ Ready to push {ahead} local commit(s) to '{tracking}'.", Style.GREEN))
223
+ recommended.append("gitpush")
224
+ else:
225
+ # Synced with remote
226
+ if total_worktree_changes > 0:
227
+ status_notes.append(f"You have {total_worktree_changes} uncommitted file change(s).")
228
+ recommended.append(f'gitpush "Describe your changes"')
229
+ else:
230
+ status_notes.append(colorize("✔ Repository is clean and in sync with remote.", Style.GREEN))
231
+ recommended.append("No action needed (all changes committed and pushed).")
232
+
233
+ # Protected branch advice
234
+ if is_protected and (ahead > 0 or total_worktree_changes > 0):
235
+ status_notes.append(colorize(f"ℹ '{branch}' is a protected branch.", Style.CYAN))
236
+ if not any("git switch -c" in r for r in recommended):
237
+ recommended.append(f"Tip: If contributing a feature, branch off: git switch -c feature/new-idea")
238
+
239
+ return status_notes, recommended
240
+
241
+
242
+ def format_status_dashboard(cwd: Optional[str] = None, verbose: bool = False) -> str:
243
+ """
244
+ Format the complete decision-support status dashboard.
245
+ """
246
+ if not is_git_repo(cwd):
247
+ return colorize("❌ Not a Git repository (or any of the parent directories).", Style.RED)
248
+
249
+ repo_name = get_repo_name(cwd)
250
+ branch = get_current_branch(cwd)
251
+ detached = is_detached_head(cwd)
252
+ tracking = get_tracking_branch(cwd)
253
+ is_prot = is_protected_branch(branch)
254
+
255
+ # Remote sync status
256
+ remote = "origin"
257
+ remote_branch = branch
258
+ if tracking and "/" in tracking:
259
+ remote, remote_branch = tracking.split("/", 1)
260
+
261
+ behind, ahead = get_ahead_behind(remote, remote_branch, cwd)
262
+ diverged = (ahead > 0 and behind > 0)
263
+
264
+ # Worktree breakdown
265
+ worktree = get_worktree_breakdown(cwd)
266
+ stash_count = get_stash_count(cwd)
267
+ diff_summary = get_diffstat_summary(cwd)
268
+
269
+ # Health score
270
+ score, health_label, health_color = calculate_repo_health(
271
+ detached=detached,
272
+ unmerged_count=worktree.get("unmerged", 0),
273
+ behind_count=behind,
274
+ diverged=diverged,
275
+ modified_count=worktree["modified"],
276
+ untracked_count=worktree["untracked"],
277
+ stash_count=stash_count,
278
+ is_protected=is_prot
279
+ )
280
+
281
+ # Decision recommendations
282
+ status_notes, recommendations = get_decision_recommendations(
283
+ branch=branch,
284
+ tracking=tracking,
285
+ ahead=ahead,
286
+ behind=behind,
287
+ diverged=diverged,
288
+ detached=detached,
289
+ worktree=worktree,
290
+ is_protected=is_prot
291
+ )
292
+
293
+ lines = []
294
+ lines.append("")
295
+ lines.append(f"{colorize('Repository:', Style.BOLD)} {colorize(repo_name, Style.GREEN)}")
296
+
297
+ branch_styled = branch
298
+ if detached:
299
+ branch_styled = colorize(f"{branch} [DETACHED HEAD ⚠️]", Style.BOLD, Style.RED)
300
+ elif is_prot:
301
+ branch_styled = colorize(f"{branch} [Protected]", Style.BOLD, Style.YELLOW)
302
+ else:
303
+ branch_styled = colorize(branch, Style.CYAN)
304
+
305
+ lines.append(f"{colorize('Branch:', Style.BOLD)} {branch_styled}")
306
+ tracking_str = tracking if tracking else colorize("None (no tracking remote)", Style.DIM)
307
+ lines.append(f"{colorize('Tracking:', Style.BOLD)} {tracking_str}")
308
+ lines.append("")
309
+
310
+ # WORKTREE section
311
+ lines.append(colorize("WORKTREE", Style.BOLD))
312
+ lines.append(colorize("────────────────────────", Style.DIM))
313
+ total_worktree = sum(worktree.values())
314
+ if total_worktree == 0:
315
+ lines.append(colorize("Clean (no changes)", Style.DIM))
316
+ else:
317
+ if worktree["modified"] > 0:
318
+ lines.append(f"Modified {colorize(str(worktree['modified']), Style.CYAN)}")
319
+ if worktree["added"] > 0:
320
+ lines.append(f"Added {colorize(str(worktree['added']), Style.GREEN)}")
321
+ if worktree["deleted"] > 0:
322
+ lines.append(f"Deleted {colorize(str(worktree['deleted']), Style.RED)}")
323
+ if worktree["renamed"] > 0:
324
+ lines.append(f"Renamed {colorize(str(worktree['renamed']), Style.CYAN)}")
325
+ if worktree["untracked"] > 0:
326
+ lines.append(f"Untracked {colorize(str(worktree['untracked']), Style.YELLOW)}")
327
+ if worktree["unmerged"] > 0:
328
+ lines.append(f"Conflicts {colorize(str(worktree['unmerged']), Style.BOLD, Style.RED)}")
329
+
330
+ # Line stat addition/deletion
331
+ if diff_summary["total_additions"] > 0 or diff_summary["total_deletions"] > 0:
332
+ adds = colorize(f"+{diff_summary['total_additions']}", Style.GREEN)
333
+ dels = colorize(f"-{diff_summary['total_deletions']}", Style.RED)
334
+ lines.append(f"Line Changes {adds}, {dels}")
335
+
336
+ if stash_count > 0:
337
+ lines.append(f"Stashes {stash_count} saved")
338
+ lines.append("")
339
+
340
+ # COMMITS section
341
+ lines.append(colorize("COMMITS", Style.BOLD))
342
+ lines.append(colorize("────────────────────────", Style.DIM))
343
+ ahead_str = colorize(str(ahead), Style.GREEN) if ahead > 0 else "0"
344
+ behind_str = colorize(str(behind), Style.RED) if behind > 0 else "0"
345
+ lines.append(f"Ahead {ahead_str}")
346
+ lines.append(f"Behind {behind_str}")
347
+ lines.append("")
348
+
349
+ # PUSH / PULL section (Decision Support)
350
+ lines.append(colorize("PUSH / STATUS", Style.BOLD))
351
+ lines.append(colorize("────────────────────────", Style.DIM))
352
+ for note in status_notes:
353
+ lines.append(note)
354
+ lines.append("")
355
+
356
+ # Recommended Action
357
+ lines.append(colorize("Recommended:", Style.BOLD))
358
+ for rec in recommendations:
359
+ lines.append(f" {colorize(rec, Style.CYAN)}")
360
+ lines.append("")
361
+
362
+ # Repository Health Score
363
+ health_bar_filled = int(score / 10)
364
+ health_bar = f"[{'█' * health_bar_filled}{'░' * (10 - health_bar_filled)}]"
365
+ lines.append(colorize("Health:", Style.BOLD))
366
+ lines.append(f" {colorize(f'{score}/100', Style.BOLD, health_color)} {colorize(health_bar, health_color)} {colorize(health_label, Style.DIM)}")
367
+ lines.append("")
368
+
369
+ return "\n".join(lines)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpush-tool
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Supercharged Git push tool with automatic GitHub repo creation and pushing
5
5
  Home-page: https://github.com/inevitablegs/gitpush
6
6
  Author: Ganesh Sonawane
@@ -38,6 +38,7 @@ A supercharged Git CLI and safety tool that simplifies repository creation and p
38
38
 
39
39
  ## ✨ Features
40
40
 
41
+ - **Decision-Support Status Dashboard (`gitpush status`):** Actionable repository overview showing worktree breakdown, commit sync, intelligent next-step recommendations, and a 0-100 Repo Health score.
41
42
  - **Pre-Push Safety & Diffstat Preview:** See changed files, line additions/deletions (`+X, -Y`), ahead/behind status, and pending commits before pushing.
42
43
  - **Dangerous Git Operation Guard:** Analyzes and protects against risky actions (`reset --hard`, force pushes, deleting unmerged branches, dirty `git clean`, detached HEADs).
43
44
  - **Protected Branch Safeguard:** Automatically detects and alerts when pushing or force-pushing to `main`, `master`, `prod`, `release`, or `dev`.
@@ -48,6 +49,40 @@ A supercharged Git CLI and safety tool that simplifies repository creation and p
48
49
 
49
50
  ---
50
51
 
52
+ ## 📊 Decision-Support Status Dashboard (`gitpush status`)
53
+
54
+ ```text
55
+ Repository: my-project
56
+ Branch: feature/payment
57
+ Tracking: origin/feature/payment
58
+
59
+ WORKTREE
60
+ ────────────────────────
61
+ Modified 4
62
+ Added 2
63
+ Deleted 1
64
+ Untracked 3
65
+ Line Changes +142, -18
66
+
67
+ COMMITS
68
+ ────────────────────────
69
+ Ahead 3
70
+ Behind 1
71
+
72
+ PUSH / STATUS
73
+ ────────────────────────
74
+ ⚠ Remote has 1 commit(s) you don't have locally.
75
+
76
+ Recommended:
77
+ git pull --rebase
78
+ gitpush "Commit message"
79
+
80
+ Health:
81
+ 82/100 [████████░░] Good (Minor Action Needed)
82
+ ```
83
+
84
+ ---
85
+
51
86
  ## 🛡️ Pre-Push Warning & Inspection
52
87
 
53
88
  Whenever you run `gitpush`, you get an instant, clear breakdown of your repository state:
@@ -8,10 +8,12 @@ gitpush/__doc__.py
8
8
  gitpush/__init__.py
9
9
  gitpush/cli.py
10
10
  gitpush/safety.py
11
+ gitpush/status.py
11
12
  gitpush_tool.egg-info/PKG-INFO
12
13
  gitpush_tool.egg-info/SOURCES.txt
13
14
  gitpush_tool.egg-info/dependency_links.txt
14
15
  gitpush_tool.egg-info/entry_points.txt
15
16
  gitpush_tool.egg-info/top_level.txt
16
17
  tests/test_cli.py
17
- tests/test_safety.py
18
+ tests/test_safety.py
19
+ tests/test_status.py
@@ -1,3 +1,4 @@
1
1
  [console_scripts]
2
2
  gitpush = gitpush.cli:run
3
3
  gitpush-guard = gitpush.cli:run
4
+ gitpush-status = gitpush.cli:run
@@ -2,21 +2,18 @@ from setuptools import setup, find_packages
2
2
  from pathlib import Path
3
3
 
4
4
  # Read long description
5
- long_desc_file = Path(__file__).parent / "LONG_DESCRIPTION.md"
6
- if not long_desc_file.exists():
7
- long_desc_file = Path(__file__).parent / "README.md"
8
- long_description = long_desc_file.read_text(encoding="utf-8") if long_desc_file.exists() else ""
9
-
5
+ long_description = (Path(__file__).parent / "LONG_DESCRIPTION.md").read_text(encoding="utf-8")
10
6
 
11
7
  setup(
12
8
  name="gitpush-tool",
13
- version="0.4.0",
9
+ version="0.4.1",
14
10
  packages=find_packages(),
15
11
  install_requires=[],
16
12
  entry_points={
17
13
  "console_scripts": [
18
14
  "gitpush=gitpush.cli:run",
19
- "gitpush-guard=gitpush.cli:run"
15
+ "gitpush-guard=gitpush.cli:run",
16
+ "gitpush-status=gitpush.cli:run"
20
17
  ],
21
18
  },
22
19
  author="Ganesh Sonawane",
File without changes
File without changes
@@ -0,0 +1,113 @@
1
+ import unittest
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ # Add package root to sys.path
10
+ sys.path.insert(0, str(Path(__file__).parent.parent))
11
+
12
+ from gitpush.status import (
13
+ get_tracking_branch,
14
+ get_stash_count,
15
+ get_worktree_breakdown,
16
+ calculate_repo_health,
17
+ get_decision_recommendations,
18
+ format_status_dashboard
19
+ )
20
+
21
+
22
+ class TestStatusDashboard(unittest.TestCase):
23
+
24
+ def setUp(self):
25
+ self.test_dir = tempfile.mkdtemp()
26
+ subprocess.run(["git", "init"], cwd=self.test_dir, capture_output=True, check=True)
27
+ subprocess.run(["git", "config", "user.name", "Test User"], cwd=self.test_dir, capture_output=True, check=True)
28
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=self.test_dir, capture_output=True, check=True)
29
+ subprocess.run(["git", "branch", "-M", "feature/payment"], cwd=self.test_dir, capture_output=True, check=True)
30
+
31
+ # Initial commit
32
+ init_file = os.path.join(self.test_dir, "init.txt")
33
+ with open(init_file, "w") as f:
34
+ f.write("init\n")
35
+ subprocess.run(["git", "add", "."], cwd=self.test_dir, capture_output=True, check=True)
36
+ subprocess.run(["git", "commit", "-m", "initial commit"], cwd=self.test_dir, capture_output=True, check=True)
37
+
38
+ def tearDown(self):
39
+ shutil.rmtree(self.test_dir, ignore_errors=True)
40
+
41
+ def test_worktree_breakdown(self):
42
+ # Create modified, added, untracked, deleted files
43
+ f1 = os.path.join(self.test_dir, "init.txt")
44
+ with open(f1, "w") as f:
45
+ f.write("init modified\n")
46
+
47
+ f2 = os.path.join(self.test_dir, "new_staged.txt")
48
+ with open(f2, "w") as f:
49
+ f.write("staged\n")
50
+ subprocess.run(["git", "add", "new_staged.txt"], cwd=self.test_dir, check=True)
51
+
52
+ f3 = os.path.join(self.test_dir, "untracked.txt")
53
+ with open(f3, "w") as f:
54
+ f.write("untracked\n")
55
+
56
+ counts = get_worktree_breakdown(cwd=self.test_dir)
57
+ self.assertEqual(counts["modified"], 1)
58
+ self.assertEqual(counts["added"], 1)
59
+ self.assertEqual(counts["untracked"], 1)
60
+
61
+ def test_health_calculation(self):
62
+ score_clean, label_clean, _ = calculate_repo_health(
63
+ detached=False,
64
+ unmerged_count=0,
65
+ behind_count=0,
66
+ diverged=False,
67
+ modified_count=0,
68
+ untracked_count=0,
69
+ stash_count=0,
70
+ is_protected=False
71
+ )
72
+ self.assertEqual(score_clean, 100)
73
+ self.assertIn("Excellent", label_clean)
74
+
75
+ score_dirty, label_dirty, _ = calculate_repo_health(
76
+ detached=True,
77
+ unmerged_count=1,
78
+ behind_count=3,
79
+ diverged=True,
80
+ modified_count=6,
81
+ untracked_count=6,
82
+ stash_count=6,
83
+ is_protected=True
84
+ )
85
+ self.assertTrue(score_dirty < 50)
86
+
87
+ def test_decision_recommendations(self):
88
+ # Test behind remote recommendation
89
+ notes, recs = get_decision_recommendations(
90
+ branch="feature/payment",
91
+ tracking="origin/feature/payment",
92
+ ahead=0,
93
+ behind=2,
94
+ diverged=False,
95
+ detached=False,
96
+ worktree={"modified": 0, "added": 0, "deleted": 0, "untracked": 0, "renamed": 0, "unmerged": 0},
97
+ is_protected=False
98
+ )
99
+ self.assertTrue(any("git pull --rebase" in r for r in recs))
100
+ self.assertTrue(any("Remote has" in n for n in notes))
101
+
102
+ def test_format_status_dashboard(self):
103
+ dashboard = format_status_dashboard(cwd=self.test_dir)
104
+ self.assertIn("Repository:", dashboard)
105
+ self.assertIn("feature/payment", dashboard)
106
+ self.assertIn("WORKTREE", dashboard)
107
+ self.assertIn("COMMITS", dashboard)
108
+ self.assertIn("Health:", dashboard)
109
+ self.assertIn("Recommended:", dashboard)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ unittest.main()
@@ -1 +0,0 @@
1
- __version__ = "0.4.0"
@@ -1,25 +0,0 @@
1
- import unittest
2
- import sys
3
- import subprocess
4
- from pathlib import Path
5
-
6
- # Add package root to sys.path
7
- sys.path.insert(0, str(Path(__file__).parent.parent))
8
-
9
- from gitpush.cli import handle_guard_command
10
-
11
-
12
- class TestCLI(unittest.TestCase):
13
-
14
- def test_guard_help_when_no_args(self):
15
- code = handle_guard_command([])
16
- self.assertEqual(code, 1)
17
-
18
- def test_guard_dry_check(self):
19
- # Run git status through guard with -y
20
- code = handle_guard_command(["status"], auto_confirm=True)
21
- self.assertEqual(code, 0)
22
-
23
-
24
- if __name__ == "__main__":
25
- unittest.main()
@@ -1,169 +0,0 @@
1
- import unittest
2
- import os
3
- import sys
4
- import tempfile
5
- import shutil
6
- import subprocess
7
- from pathlib import Path
8
-
9
- # Add package root to sys.path
10
- sys.path.insert(0, str(Path(__file__).parent.parent))
11
-
12
- from gitpush.safety import (
13
- is_git_repo,
14
- get_repo_name,
15
- get_repo_root,
16
- get_current_branch,
17
- is_detached_head,
18
- is_protected_branch,
19
- get_ahead_behind,
20
- get_diffstat_summary,
21
- DangerousOperationAnalyzer,
22
- format_danger_box,
23
- format_prepush_summary
24
- )
25
-
26
-
27
- class TestSafetyEngine(unittest.TestCase):
28
-
29
- def setUp(self):
30
- # Create a temporary directory and initialize a git repo for testing
31
- self.test_dir = tempfile.mkdtemp()
32
- subprocess.run(["git", "init"], cwd=self.test_dir, capture_output=True, check=True)
33
- subprocess.run(["git", "config", "user.name", "Test User"], cwd=self.test_dir, capture_output=True, check=True)
34
- subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=self.test_dir, capture_output=True, check=True)
35
- subprocess.run(["git", "branch", "-M", "main"], cwd=self.test_dir, capture_output=True, check=True)
36
- # Create initial root commit
37
- init_file = os.path.join(self.test_dir, "init.txt")
38
- with open(init_file, "w") as f:
39
- f.write("init\n")
40
- subprocess.run(["git", "add", "."], cwd=self.test_dir, capture_output=True, check=True)
41
- subprocess.run(["git", "commit", "-m", "initial commit"], cwd=self.test_dir, capture_output=True, check=True)
42
-
43
- def tearDown(self):
44
- shutil.rmtree(self.test_dir, ignore_errors=True)
45
-
46
- def test_is_git_repo(self):
47
- self.assertTrue(is_git_repo(cwd=self.test_dir))
48
- non_repo = tempfile.mkdtemp()
49
- try:
50
- self.assertFalse(is_git_repo(cwd=non_repo))
51
- finally:
52
- shutil.rmtree(non_repo, ignore_errors=True)
53
-
54
- def test_repo_name_and_root(self):
55
- root = get_repo_root(cwd=self.test_dir)
56
- self.assertIsNotNone(root)
57
- self.assertEqual(os.path.abspath(root), os.path.abspath(self.test_dir))
58
- name = get_repo_name(cwd=self.test_dir)
59
- self.assertEqual(name, os.path.basename(self.test_dir))
60
-
61
- def test_protected_branch_detection(self):
62
- self.assertTrue(is_protected_branch("main"))
63
- self.assertTrue(is_protected_branch("master"))
64
- self.assertTrue(is_protected_branch("prod"))
65
- self.assertTrue(is_protected_branch("production"))
66
- self.assertTrue(is_protected_branch("release"))
67
- self.assertTrue(is_protected_branch("dev"))
68
- self.assertFalse(is_protected_branch("feature/login"))
69
- self.assertFalse(is_protected_branch("bugfix-123"))
70
-
71
- def test_diffstat_summary(self):
72
- # Initially clean
73
- diff = get_diffstat_summary(cwd=self.test_dir)
74
- self.assertFalse(diff["has_changes"])
75
-
76
- # Create a file
77
- f1 = os.path.join(self.test_dir, "hello.txt")
78
- with open(f1, "w") as f:
79
- f.write("Line 1\nLine 2\nLine 3\n")
80
-
81
- diff = get_diffstat_summary(cwd=self.test_dir)
82
- self.assertTrue(diff["has_changes"])
83
- self.assertEqual(diff["untracked_count"], 1)
84
- self.assertEqual(diff["total_additions"], 3)
85
-
86
- # Commit initial file
87
- subprocess.run(["git", "add", "."], cwd=self.test_dir, check=True)
88
- subprocess.run(["git", "commit", "-m", "Init"], cwd=self.test_dir, check=True)
89
-
90
- # Modify file
91
- with open(f1, "w") as f:
92
- f.write("Line 1 modified\nLine 2\nLine 3\nLine 4 added\n")
93
-
94
- diff = get_diffstat_summary(cwd=self.test_dir)
95
- self.assertTrue(diff["has_changes"])
96
- self.assertEqual(diff["modified_count"], 1)
97
- self.assertTrue(diff["total_additions"] > 0)
98
-
99
- def test_reset_hard_analysis(self):
100
- # Create 3 commits
101
- f = os.path.join(self.test_dir, "file.txt")
102
- for i in range(1, 4):
103
- with open(f, "a") as fp:
104
- fp.write(f"commit {i}\n")
105
- subprocess.run(["git", "add", "."], cwd=self.test_dir, check=True)
106
- subprocess.run(["git", "commit", "-m", f"commit {i}"], cwd=self.test_dir, check=True)
107
-
108
- # Make uncommitted changes
109
- with open(f, "a") as fp:
110
- fp.write("uncommitted work\n")
111
-
112
- report = DangerousOperationAnalyzer.analyze_reset_hard("HEAD~2", cwd=self.test_dir)
113
- self.assertTrue(report.is_dangerous)
114
- self.assertEqual(report.lost_items.get("commits"), 2)
115
- self.assertEqual(report.lost_items.get("modified_files"), 1)
116
-
117
- # Format danger box output
118
- box = format_danger_box(report, repo_name="test-repo")
119
- self.assertIn("git reset --hard HEAD~2", box)
120
- self.assertIn("2 commit(s)", box)
121
- self.assertIn("1 modified/staged file(s)", box)
122
- self.assertIn("test-repo", box)
123
-
124
- def test_force_push_analysis(self):
125
- report = DangerousOperationAnalyzer.analyze_force_push("origin", "main", cwd=self.test_dir)
126
- self.assertTrue(report.is_dangerous)
127
- self.assertIn("PROTECTED BRANCH", report.risks[0])
128
-
129
- def test_clean_analysis(self):
130
- # Create untracked file
131
- untracked = os.path.join(self.test_dir, "untracked.txt")
132
- with open(untracked, "w") as fp:
133
- fp.write("untracked\n")
134
-
135
- report = DangerousOperationAnalyzer.analyze_clean(force=True, remove_dirs=True, cwd=self.test_dir)
136
- self.assertTrue(report.is_dangerous)
137
- self.assertEqual(report.lost_items.get("untracked_files"), 1)
138
-
139
- def test_branch_delete_analysis(self):
140
- # Create a branch with an unmerged commit
141
- subprocess.run(["git", "checkout", "-b", "feature-x"], cwd=self.test_dir, check=True)
142
- f = os.path.join(self.test_dir, "feat.txt")
143
- with open(f, "w") as fp:
144
- fp.write("feature\n")
145
- subprocess.run(["git", "add", "."], cwd=self.test_dir, check=True)
146
- subprocess.run(["git", "commit", "-m", "feature commit"], cwd=self.test_dir, check=True)
147
- subprocess.run(["git", "checkout", "main"], cwd=self.test_dir, check=True)
148
-
149
- report = DangerousOperationAnalyzer.analyze_branch_delete("feature-x", force=True, cwd=self.test_dir)
150
- self.assertTrue(report.is_dangerous)
151
- self.assertEqual(report.lost_items.get("unmerged_commits"), 1)
152
-
153
- def test_arbitrary_command_analyzer(self):
154
- # Test command parsing
155
- rep1 = DangerousOperationAnalyzer.analyze_arbitrary_command(["git", "reset", "--hard", "HEAD~3"], cwd=self.test_dir)
156
- self.assertIsNotNone(rep1)
157
- self.assertIn("reset --hard", rep1.operation_name)
158
-
159
- rep2 = DangerousOperationAnalyzer.analyze_arbitrary_command(["git", "clean", "-fd"], cwd=self.test_dir)
160
- self.assertIsNotNone(rep2)
161
- self.assertIn("Clean", rep2.operation_name)
162
-
163
- rep3 = DangerousOperationAnalyzer.analyze_arbitrary_command(["git", "push", "--force", "origin", "main"], cwd=self.test_dir)
164
- self.assertIsNotNone(rep3)
165
- self.assertIn("Force Push", rep3.operation_name)
166
-
167
-
168
- if __name__ == "__main__":
169
- unittest.main()
File without changes
File without changes
File without changes