git-graphable 0.6.0__tar.gz → 0.7.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.
- {git_graphable-0.6.0 → git_graphable-0.7.0}/CHANGELOG.md +13 -0
- git_graphable-0.7.0/HYGIENE.md +168 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/PKG-INFO +8 -5
- {git_graphable-0.6.0 → git_graphable-0.7.0}/README.md +7 -4
- {git_graphable-0.6.0 → git_graphable-0.7.0}/USAGE.md +2 -1
- {git_graphable-0.6.0 → git_graphable-0.7.0}/pyproject.toml +7 -1
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/bare_cli.py +31 -14
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/core.py +11 -1
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/default_config.toml +1 -1
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlights/hygiene.py +45 -15
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/hygiene.py +96 -11
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/__init__.py +1 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/jira.py +19 -2
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/script.py +11 -10
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/script.py +4 -4
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/rich_cli.py +20 -1
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styling/base.py +1 -1
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styling/html.py +6 -3
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/test_jira_engine.py +5 -1
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/test_script.py +13 -12
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/prs/test_script.py +13 -14
- {git_graphable-0.6.0 → git_graphable-0.7.0}/.gemini/GEMINI.md +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/.gemini/code-ordering.md +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/.github/dependabot.yml +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/.github/workflows/ci.yml +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/.github/workflows/pages.yml +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/.github/workflows/publish.yml +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/.gitignore +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/.python-version +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/Justfile +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/LICENSE +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/SECURITY.md +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/STYLING.md +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/action.yml +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/examples/EXAMPLES.md +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/examples/generate_demos.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/examples/index_template.html +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/examples/publish_demos.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/__init__.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/cli.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/cli_utils.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/commands.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/github.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlighter.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlights/core.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlights/external.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlights/visual.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/base.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/github.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/gitlab.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/models.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/parser.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/__init__.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/base.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/github.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/gitlab.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styler.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styling/generic.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styling/mermaid.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/templates.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/__init__.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/conftest.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/highlights/__init__.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/highlights/test_external.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/highlights/test_hygiene.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/highlights/test_visual.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/__init__.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/test_github.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/test_gitlab.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/prs/__init__.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/prs/test_github.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/prs/test_gitlab.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/styling/__init__.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/styling/test_generic.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/styling/test_mermaid.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_bare_cli.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_cli.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_cli_utils.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_commands.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_config.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_core.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_examples_html.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_github.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_highlighter.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_hygiene_scorer.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_interactive_html.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_models.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_parser.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_rich_cli.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_styler.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_ui_interactive.py +0 -0
- {git_graphable-0.6.0 → git_graphable-0.7.0}/uv.lock +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.7.0] - 2026-03-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Security Trust Enforcement**: Custom scripts (`issue_script`, `pr_script`) and sensitive API integrations (Jira) are now disabled by default for untrusted local configurations. Users must explicitly use `--trust` to enable these features for repository-local configs.
|
|
9
|
+
- **Actionable Hygiene Intelligence**: The hygiene summary now provides specific details (commit hashes, branch names) for every deduction, allowing users to precisely identify and fix hygiene issues.
|
|
10
|
+
- **Selective Hygiene Ignore**: Introduced a SHA-based ignore mechanism to suppress specific hygiene rules. Supported via `.git-graphable.toml` (or `pyproject.toml`) and the `--ignore` CLI flag.
|
|
11
|
+
- **Remediation Guidelines**: Added a comprehensive [HYGIENE.md](HYGIENE.md) guide with actionable Git commands to help users improve their project's hygiene score.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Critical Command Injection Mitigation**: Fixed a vulnerability in `ScriptIssueEngine` where malicious `issue_id` strings could inject shell commands. Switched to `sh -c` with positional arguments for safe execution.
|
|
15
|
+
- **Credential Theft Prevention**: Prevented potential Jira token exposure to untrusted URLs by enforcing the trust model for Jira configurations.
|
|
16
|
+
- **XSS Mitigation**: Fixed a medium-severity XSS vulnerability in interactive HTML export legend generation by properly escaping repository-derived data.
|
|
17
|
+
|
|
5
18
|
## [0.6.0] - 2026-03-06
|
|
6
19
|
|
|
7
20
|
### Added
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Git Hygiene Guidelines
|
|
2
|
+
|
|
3
|
+
This document explains the hygiene metrics analyzed by `git-graphable` and provides actionable steps to remediate common issues.
|
|
4
|
+
|
|
5
|
+
## 1. Process Integrity
|
|
6
|
+
|
|
7
|
+
### Direct Pushes to Protected Branches
|
|
8
|
+
**Detection:** Commits made directly to `production_branch` or `development_branch` without a merge commit.
|
|
9
|
+
**Why it matters:** Bypassing the Pull Request process avoids code review and CI checks, increasing the risk of bugs.
|
|
10
|
+
**Remediation:**
|
|
11
|
+
1. **Stop**: Do not push directly to main/master/develop.
|
|
12
|
+
2. **Fix History** (if not yet shared/stable):
|
|
13
|
+
```bash
|
|
14
|
+
# Move the commit to a new branch
|
|
15
|
+
git checkout -b feature/my-fix
|
|
16
|
+
|
|
17
|
+
# Reset main back to before the direct push
|
|
18
|
+
git checkout main
|
|
19
|
+
git reset --hard <commit-before-push>
|
|
20
|
+
|
|
21
|
+
# Push the new branch and open a PR
|
|
22
|
+
git push -u origin feature/my-fix
|
|
23
|
+
```
|
|
24
|
+
3. **Protection**: Enable "Branch Protection Rules" in GitHub/GitLab settings to physically prevent direct pushes.
|
|
25
|
+
|
|
26
|
+
### Conflicting Pull Requests
|
|
27
|
+
**Detection:** Open PRs marked as `CONFLICTING` by the VCS provider.
|
|
28
|
+
**Why it matters:** Conflicts block merging and indicate divergent development paths that get harder to resolve over time.
|
|
29
|
+
**Remediation:**
|
|
30
|
+
```bash
|
|
31
|
+
git checkout feature/my-branch
|
|
32
|
+
git pull origin main
|
|
33
|
+
# Resolve conflicts in editor
|
|
34
|
+
git add .
|
|
35
|
+
git commit -m "Merge branch 'main' into feature/my-branch"
|
|
36
|
+
git push
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Orphan/Dangling Commits
|
|
40
|
+
**Detection:** Commits that are not reachable from any branch or tag.
|
|
41
|
+
**Why it matters:** These are often lost code or mistakes.
|
|
42
|
+
**Remediation:**
|
|
43
|
+
- **Garbage Collect**: `git gc --prune=now`
|
|
44
|
+
- **Recover**: If valuable, checkout the SHA and create a branch: `git checkout -b recover-work <sha>`
|
|
45
|
+
|
|
46
|
+
## 2. Cleanliness
|
|
47
|
+
|
|
48
|
+
### WIP / Fixup Commits
|
|
49
|
+
**Detection:** Commit messages containing "wip", "todo", "fixup", "temp".
|
|
50
|
+
**Why it matters:** messy history makes debugging and `git bisect` difficult.
|
|
51
|
+
**Remediation:**
|
|
52
|
+
- **Interactive Rebase**: Squash WIP commits into meaningful units.
|
|
53
|
+
```bash
|
|
54
|
+
git rebase -i HEAD~n # where n is number of commits back
|
|
55
|
+
# Change 'pick' to 'squash' or 'fixup' for the WIP commits
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Stale Branches
|
|
59
|
+
**Detection:** Feature branches with no activity for `stale_days` (default: 30).
|
|
60
|
+
**Why it matters:** Clutters the repository and indicates abandoned work.
|
|
61
|
+
**Remediation:**
|
|
62
|
+
- **Delete Local**: `git branch -d branch-name`
|
|
63
|
+
- **Delete Remote**: `git push origin --delete branch-name`
|
|
64
|
+
- **Archive**: Tag it if you need to keep it: `git tag archive/branch-name branch-name`
|
|
65
|
+
|
|
66
|
+
## 3. Connectivity & Flow
|
|
67
|
+
|
|
68
|
+
### Long-Running Branches
|
|
69
|
+
**Detection:** Feature branches that have diverged from the base for more than `long_running_days` (default: 14).
|
|
70
|
+
**Why it matters:** Increases the risk of massive merge conflicts (The "Merge Hell").
|
|
71
|
+
**Remediation:**
|
|
72
|
+
- **Merge Often**: Merge `main` into your feature branch frequently.
|
|
73
|
+
- **Ship Smaller**: Break large features into smaller, mergeable PRs.
|
|
74
|
+
|
|
75
|
+
### Divergence (Behind Base)
|
|
76
|
+
**Detection:** Feature branches that are missing commits from the base branch (`main`).
|
|
77
|
+
**Why it matters:** You are testing against outdated code.
|
|
78
|
+
**Remediation:**
|
|
79
|
+
```bash
|
|
80
|
+
git checkout feature/my-feature
|
|
81
|
+
git pull origin main
|
|
82
|
+
git push
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Redundant Back-Merges
|
|
86
|
+
**Detection:** Merging `main` into a feature branch, but doing it recursively or unnecessarily often creates a "railroad track" history.
|
|
87
|
+
**Why it matters:** Makes history hard to read.
|
|
88
|
+
**Remediation:**
|
|
89
|
+
- Use `git rebase main` instead of `git merge main` for feature branches (if your team policy allows rewriting feature branch history).
|
|
90
|
+
|
|
91
|
+
## 4. Collaboration
|
|
92
|
+
|
|
93
|
+
### Contributor Silos
|
|
94
|
+
**Detection:** Long sequences of commits on a branch by a single author without interaction from others.
|
|
95
|
+
**Why it matters:** Risk of "Bus Factor". No code review or shared knowledge.
|
|
96
|
+
**Remediation:**
|
|
97
|
+
- **Pair Program**: Involve others earlier.
|
|
98
|
+
- **Early PRs**: Open a Draft PR to get feedback before the feature is done.
|
|
99
|
+
|
|
100
|
+
### Collaboration Gaps
|
|
101
|
+
**Detection:** The Git commit author does not match the assignee of the linked Issue Tracker ticket.
|
|
102
|
+
**Remediation:**
|
|
103
|
+
- Update the Issue Tracker ticket to assign it to the actual developer.
|
|
104
|
+
- Configure `author_mapping` in `.git-graphable.toml` if names just don't match (e.g., "John Doe" vs "jdoe").
|
|
105
|
+
|
|
106
|
+
## 5. Consistency (Issue Tracking)
|
|
107
|
+
|
|
108
|
+
### Issue / Git Desync
|
|
109
|
+
**Detection:**
|
|
110
|
+
- Ticket is `OPEN` but PR is `MERGED`.
|
|
111
|
+
- Ticket is `CLOSED` but PR is `OPEN`.
|
|
112
|
+
**Remediation:**
|
|
113
|
+
- **Sync Status**: Manually update the ticket status.
|
|
114
|
+
- **Automation**: Configure GitHub/Jira to auto-close issues when PRs are merged.
|
|
115
|
+
|
|
116
|
+
### Release Inconsistencies
|
|
117
|
+
**Detection:** Ticket is marked `RELEASED` but the commit is not included in any Git Tag.
|
|
118
|
+
**Remediation:**
|
|
119
|
+
- **Cut a Release**: Create a Git tag for the deployment.
|
|
120
|
+
```bash
|
|
121
|
+
git tag v1.0.0
|
|
122
|
+
git push --tags
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Longevity Mismatch
|
|
126
|
+
**Detection:** A large time gap (>14 days) exists between when a ticket was created and when work (commits) started.
|
|
127
|
+
**Why it matters:** Planning failure or stale requirements.
|
|
128
|
+
**Remediation:**
|
|
129
|
+
- **Review Backlog**: Don't open tickets until work is ready to start.
|
|
130
|
+
- **Re-evaluate**: If a ticket sits for 2 weeks, check if requirements have changed before starting code.
|
|
131
|
+
|
|
132
|
+
## 6. Ignoring Hygiene Rules
|
|
133
|
+
|
|
134
|
+
Sometimes a commit or branch is flagged for a reason that is acceptable or intended. You can selectively ignore these rules.
|
|
135
|
+
|
|
136
|
+
### Via Configuration (`.git-graphable.toml`)
|
|
137
|
+
|
|
138
|
+
Add an `[git-graphable.ignore]` section to your configuration:
|
|
139
|
+
|
|
140
|
+
```toml
|
|
141
|
+
[git-graphable.ignore]
|
|
142
|
+
# Ignore specific rules for specific SHAs (prefix or full SHA)
|
|
143
|
+
"9bd5377" = ["wip", "direct_push"]
|
|
144
|
+
"abc1234" = ["all"] # Ignore all hygiene rules for this commit
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Via CLI
|
|
148
|
+
|
|
149
|
+
Use the `--ignore` flag:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Ignore WIP rule for a specific SHA
|
|
153
|
+
uv run git-graphable analyze . --ignore 9bd5377:wip
|
|
154
|
+
|
|
155
|
+
# Ignore multiple items
|
|
156
|
+
uv run git-graphable analyze . --ignore 9bd5377:wip --ignore abc1234:all
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Supported Rule Names
|
|
160
|
+
- `wip`
|
|
161
|
+
- `direct_push`
|
|
162
|
+
- `divergence`
|
|
163
|
+
- `orphan`
|
|
164
|
+
- `stale`
|
|
165
|
+
- `long_running`
|
|
166
|
+
- `back_merge`
|
|
167
|
+
- `silo`
|
|
168
|
+
- `all` (ignores everything for that SHA)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-graphable
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: A powerful Git history visualizer and hygiene linter with CI gating.
|
|
5
5
|
Project-URL: Homepage, https://github.com/TheTrueSCU/git-graphable
|
|
6
6
|
Project-URL: Issues, https://github.com/TheTrueSCU/git-graphable/issues
|
|
@@ -42,9 +42,11 @@ Check out the tool in action with our **[Live Interactive Demos](https://thetrue
|
|
|
42
42
|
- **Automatic Visualization**: Generates and opens an image (PNG) automatically if no output is specified.
|
|
43
43
|
- **Advanced Highlighting**: Visualize author patterns, topological distance, and specific merge paths.
|
|
44
44
|
- **VCS Integration**: Highlight commits based on pull request/merge request status using `gh` (GitHub) or `glab` (GitLab) CLIs.
|
|
45
|
-
- **Hygiene Analysis**: Automatically detect WIP commits, direct pushes to protected branches, squashed PRs, back-merges, and contributor silos.
|
|
45
|
+
- **Hygiene Analysis**: Automatically detect WIP commits, direct pushes to protected branches, squashed PRs, back-merges, and contributor silos. Provides actionable intelligence with exact commit hashes and branch names.
|
|
46
46
|
- **Issue Tracker Integration**: Connect to Jira, GitHub Issues, GitLab Issues, or custom scripts to highlight status desyncs.
|
|
47
|
-
- **Security First**: Configuration trust mechanism
|
|
47
|
+
- **Security First**: Configuration trust mechanism enforces security by requiring explicit authorization (use `--trust`) to execute custom scripts or send credentials from repository-local configs.
|
|
48
|
+
- **Selective Ignores**: Suppress specific hygiene rules for given commit SHAs using the configuration file or `--ignore` CLI flag.
|
|
49
|
+
- **Remediation Guide**: Detailed guidelines in [HYGIENE.md](HYGIENE.md) help you reach a 100% score.
|
|
48
50
|
- **Dynamic Badges**: Host live Shields.io badges for Git Hygiene and Code Coverage on GitHub Pages.
|
|
49
51
|
|
|
50
52
|
## Installation
|
|
@@ -89,7 +91,7 @@ jobs:
|
|
|
89
91
|
fetch-depth: 0 # Required to see full history
|
|
90
92
|
|
|
91
93
|
- name: Generate Git Graph Reports
|
|
92
|
-
uses: TheTrueSCU/git-graphable@v0.
|
|
94
|
+
uses: TheTrueSCU/git-graphable@v0.7.0
|
|
93
95
|
with:
|
|
94
96
|
production_branch: 'main'
|
|
95
97
|
output_dir: 'reports'
|
|
@@ -155,6 +157,7 @@ Identify problematic patterns like direct pushes to `main`, messy WIP commits, b
|
|
|
155
157
|
```bash
|
|
156
158
|
uv run git-graphable analyze . --highlight-direct-pushes --highlight-wip --highlight-squashed --highlight-back-merges --highlight-silos
|
|
157
159
|
```
|
|
160
|
+
> **Tip:** See [HYGIENE.md](HYGIENE.md) for a detailed guide on how to remediate these issues and improve your score.
|
|
158
161
|
|
|
159
162
|
### PR Status Highlighting
|
|
160
163
|
View the current state of all PRs in your repository graph:
|
|
@@ -203,7 +206,7 @@ highlight_critical = true
|
|
|
203
206
|
critical_branches = ["main", "prod"]
|
|
204
207
|
highlight_pr_status = true
|
|
205
208
|
highlight_wip = true
|
|
206
|
-
wip_keywords = ["wip", "todo", "fixme", "temp"]
|
|
209
|
+
wip_keywords = ["wip", "todo", "fixme", "temp", "fixup!", "squash!"]
|
|
207
210
|
highlight_direct_pushes = true
|
|
208
211
|
highlight_squashed = true
|
|
209
212
|
highlight_back_merges = true
|
|
@@ -24,9 +24,11 @@ Check out the tool in action with our **[Live Interactive Demos](https://thetrue
|
|
|
24
24
|
- **Automatic Visualization**: Generates and opens an image (PNG) automatically if no output is specified.
|
|
25
25
|
- **Advanced Highlighting**: Visualize author patterns, topological distance, and specific merge paths.
|
|
26
26
|
- **VCS Integration**: Highlight commits based on pull request/merge request status using `gh` (GitHub) or `glab` (GitLab) CLIs.
|
|
27
|
-
- **Hygiene Analysis**: Automatically detect WIP commits, direct pushes to protected branches, squashed PRs, back-merges, and contributor silos.
|
|
27
|
+
- **Hygiene Analysis**: Automatically detect WIP commits, direct pushes to protected branches, squashed PRs, back-merges, and contributor silos. Provides actionable intelligence with exact commit hashes and branch names.
|
|
28
28
|
- **Issue Tracker Integration**: Connect to Jira, GitHub Issues, GitLab Issues, or custom scripts to highlight status desyncs.
|
|
29
|
-
- **Security First**: Configuration trust mechanism
|
|
29
|
+
- **Security First**: Configuration trust mechanism enforces security by requiring explicit authorization (use `--trust`) to execute custom scripts or send credentials from repository-local configs.
|
|
30
|
+
- **Selective Ignores**: Suppress specific hygiene rules for given commit SHAs using the configuration file or `--ignore` CLI flag.
|
|
31
|
+
- **Remediation Guide**: Detailed guidelines in [HYGIENE.md](HYGIENE.md) help you reach a 100% score.
|
|
30
32
|
- **Dynamic Badges**: Host live Shields.io badges for Git Hygiene and Code Coverage on GitHub Pages.
|
|
31
33
|
|
|
32
34
|
## Installation
|
|
@@ -71,7 +73,7 @@ jobs:
|
|
|
71
73
|
fetch-depth: 0 # Required to see full history
|
|
72
74
|
|
|
73
75
|
- name: Generate Git Graph Reports
|
|
74
|
-
uses: TheTrueSCU/git-graphable@v0.
|
|
76
|
+
uses: TheTrueSCU/git-graphable@v0.7.0
|
|
75
77
|
with:
|
|
76
78
|
production_branch: 'main'
|
|
77
79
|
output_dir: 'reports'
|
|
@@ -137,6 +139,7 @@ Identify problematic patterns like direct pushes to `main`, messy WIP commits, b
|
|
|
137
139
|
```bash
|
|
138
140
|
uv run git-graphable analyze . --highlight-direct-pushes --highlight-wip --highlight-squashed --highlight-back-merges --highlight-silos
|
|
139
141
|
```
|
|
142
|
+
> **Tip:** See [HYGIENE.md](HYGIENE.md) for a detailed guide on how to remediate these issues and improve your score.
|
|
140
143
|
|
|
141
144
|
### PR Status Highlighting
|
|
142
145
|
View the current state of all PRs in your repository graph:
|
|
@@ -185,7 +188,7 @@ highlight_critical = true
|
|
|
185
188
|
critical_branches = ["main", "prod"]
|
|
186
189
|
highlight_pr_status = true
|
|
187
190
|
highlight_wip = true
|
|
188
|
-
wip_keywords = ["wip", "todo", "fixme", "temp"]
|
|
191
|
+
wip_keywords = ["wip", "todo", "fixme", "temp", "fixup!", "squash!"]
|
|
189
192
|
highlight_direct_pushes = true
|
|
190
193
|
highlight_squashed = true
|
|
191
194
|
highlight_back_merges = true
|
|
@@ -77,7 +77,8 @@ Analyze git history and generate a graph. This is the default command; if no com
|
|
|
77
77
|
* `--min-score INTEGER`: Minimum hygiene score required for --check
|
|
78
78
|
* `--bare`: Force bare mode (no rich output)
|
|
79
79
|
* `--hygiene-output TEXT`: Path to save hygiene summary as JSON.
|
|
80
|
-
* `--
|
|
80
|
+
* `--ignore TEXT`: Ignore hygiene rules for specific SHAs (format: `sha:rule`). Use `all` as the rule to ignore all checks for a commit.
|
|
81
|
+
* `--trust`: Trust configuration files (.git-graphable.toml, pyproject.toml) found in the repository. **Required** to execute custom scripts (`issue_script`, `pr_script`) or use sensitive integrations (Jira) from these sources.
|
|
81
82
|
* `--help`: Show this message and exit.
|
|
82
83
|
|
|
83
84
|
---
|
|
@@ -34,7 +34,7 @@ license = "MIT"
|
|
|
34
34
|
name = "git-graphable"
|
|
35
35
|
readme = "README.md"
|
|
36
36
|
requires-python = ">=3.13"
|
|
37
|
-
version = "0.
|
|
37
|
+
version = "0.7.0"
|
|
38
38
|
|
|
39
39
|
[project.optional-dependencies]
|
|
40
40
|
cli = [
|
|
@@ -93,3 +93,9 @@ skip_covered = true
|
|
|
93
93
|
markers = [
|
|
94
94
|
"ui: UI and browser-based tests",
|
|
95
95
|
]
|
|
96
|
+
|
|
97
|
+
[tool.git-graphable.ignore]
|
|
98
|
+
"9bd5377" = ["wip"]
|
|
99
|
+
"7a2409e" = ["wip"]
|
|
100
|
+
"24b44fb" = ["wip"]
|
|
101
|
+
"a9bdbbb" = ["wip"]
|
|
@@ -151,31 +151,34 @@ def run_bare_cli():
|
|
|
151
151
|
help="Threshold in days for longevity mismatch detection",
|
|
152
152
|
)
|
|
153
153
|
p.add_argument(
|
|
154
|
-
"--
|
|
155
|
-
action="
|
|
156
|
-
|
|
154
|
+
"--penalty",
|
|
155
|
+
action="append",
|
|
156
|
+
default=[],
|
|
157
|
+
help="Override hygiene penalty (format: metric:value, e.g. direct_push_penalty:20)",
|
|
157
158
|
)
|
|
158
|
-
p.add_argument("--min-score", type=int, help="Minimum score for --check")
|
|
159
159
|
p.add_argument(
|
|
160
|
-
"--
|
|
161
|
-
|
|
160
|
+
"--style",
|
|
161
|
+
action="append",
|
|
162
|
+
default=[],
|
|
163
|
+
help="Override visual style (format: key:property:value, e.g. critical:stroke:teal)",
|
|
162
164
|
)
|
|
163
165
|
p.add_argument(
|
|
164
|
-
"--
|
|
166
|
+
"--check",
|
|
165
167
|
action="store_true",
|
|
166
|
-
help="
|
|
168
|
+
help="Exit with non-zero if hygiene score is below threshold",
|
|
167
169
|
)
|
|
170
|
+
p.add_argument("--min-score", type=int, help="Minimum score for --check")
|
|
171
|
+
p.add_argument("--hygiene-output", help="Path to save hygiene summary as JSON")
|
|
168
172
|
p.add_argument(
|
|
169
|
-
"--
|
|
173
|
+
"--ignore",
|
|
170
174
|
action="append",
|
|
171
175
|
default=[],
|
|
172
|
-
help="
|
|
176
|
+
help="Ignore hygiene rules for specific SHAs (format: sha:rule)",
|
|
173
177
|
)
|
|
174
178
|
p.add_argument(
|
|
175
|
-
"--
|
|
176
|
-
action="
|
|
177
|
-
|
|
178
|
-
help="Override visual style (format: key:property:value, e.g. critical:stroke:teal)",
|
|
179
|
+
"--trust",
|
|
180
|
+
action="store_true",
|
|
181
|
+
help="Trust configuration files found in the repository",
|
|
179
182
|
)
|
|
180
183
|
|
|
181
184
|
add_analyze_args(analyze_parser)
|
|
@@ -294,12 +297,22 @@ def run_bare_cli():
|
|
|
294
297
|
}
|
|
295
298
|
if args.penalty
|
|
296
299
|
else {},
|
|
300
|
+
"ignore": {},
|
|
297
301
|
"theme": parse_style_overrides(args.style) if args.style else {},
|
|
298
302
|
"min_hygiene_score": args.min_score,
|
|
299
303
|
"hygiene_output": args.hygiene_output,
|
|
300
304
|
"trust": args.trust,
|
|
301
305
|
}
|
|
302
306
|
|
|
307
|
+
if args.ignore:
|
|
308
|
+
ignore_dict = {}
|
|
309
|
+
for item in args.ignore:
|
|
310
|
+
if ":" in item:
|
|
311
|
+
key, val = item.split(":", 1)
|
|
312
|
+
if key not in ignore_dict:
|
|
313
|
+
ignore_dict[key] = []
|
|
314
|
+
ignore_dict[key].append(val)
|
|
315
|
+
overrides["ignore"] = ignore_dict
|
|
303
316
|
|
|
304
317
|
try:
|
|
305
318
|
results = convert_command(
|
|
@@ -329,6 +342,10 @@ def run_bare_cli():
|
|
|
329
342
|
)
|
|
330
343
|
for deduction in hygiene.get("deductions", []):
|
|
331
344
|
print(f" - {deduction['message']} (-{deduction['amount']}%)")
|
|
345
|
+
for item in deduction.get("items", []):
|
|
346
|
+
print(f" * {item}")
|
|
347
|
+
|
|
348
|
+
print("\nSee HYGIENE.md for remediation guidelines.")
|
|
332
349
|
|
|
333
350
|
if args.check:
|
|
334
351
|
min_s = args.min_score or 80
|
|
@@ -164,7 +164,7 @@ class GitLogConfig:
|
|
|
164
164
|
long_running_base: Optional[str] = None
|
|
165
165
|
highlight_wip: bool = False
|
|
166
166
|
wip_keywords: List[str] = field(
|
|
167
|
-
default_factory=lambda: ["wip", "fixup!", "squash!"]
|
|
167
|
+
default_factory=lambda: ["wip", "todo", "fixme", "temp", "fixup!", "squash!"]
|
|
168
168
|
)
|
|
169
169
|
highlight_direct_pushes: bool = False
|
|
170
170
|
highlight_pr_status: bool = False
|
|
@@ -193,6 +193,7 @@ class GitLogConfig:
|
|
|
193
193
|
longevity_threshold_days: int = (
|
|
194
194
|
14 # Max diff between Issue created and first commit
|
|
195
195
|
)
|
|
196
|
+
ignore: Dict[str, List[str]] = field(default_factory=dict)
|
|
196
197
|
trusted: bool = True # True if explicitly provided via CLI or from a trusted source
|
|
197
198
|
trust: bool = False # CLI override to force trust
|
|
198
199
|
hygiene_weights: HygieneWeights = field(default_factory=HygieneWeights)
|
|
@@ -222,6 +223,8 @@ class GitLogConfig:
|
|
|
222
223
|
weights_data = config_data.pop("hygiene_weights", {})
|
|
223
224
|
# Handle nested theme
|
|
224
225
|
theme_data = config_data.pop("theme", {})
|
|
226
|
+
# Handle nested ignore
|
|
227
|
+
ignore_data = config_data.pop("ignore", {})
|
|
225
228
|
|
|
226
229
|
config = cls(
|
|
227
230
|
**{
|
|
@@ -235,6 +238,9 @@ class GitLogConfig:
|
|
|
235
238
|
if hasattr(config.hygiene_weights, k):
|
|
236
239
|
setattr(config.hygiene_weights, k, v)
|
|
237
240
|
|
|
241
|
+
if ignore_data:
|
|
242
|
+
config.ignore = ignore_data
|
|
243
|
+
|
|
238
244
|
if theme_data:
|
|
239
245
|
for k, v in theme_data.items():
|
|
240
246
|
if hasattr(config.theme, k):
|
|
@@ -293,6 +299,10 @@ class GitLogConfig:
|
|
|
293
299
|
setattr(current_style, s_key, s_val)
|
|
294
300
|
else:
|
|
295
301
|
setattr(new_config.theme, t_key, t_val)
|
|
302
|
+
elif key == "ignore" and isinstance(value, dict):
|
|
303
|
+
# Merge ignore if provided as dict
|
|
304
|
+
for i_key, i_val in value.items():
|
|
305
|
+
new_config.ignore[i_key] = i_val
|
|
296
306
|
elif isinstance(value, list) and not value:
|
|
297
307
|
# Special case for lists: only override if not empty
|
|
298
308
|
continue
|
|
@@ -9,7 +9,7 @@ highlight_critical = true
|
|
|
9
9
|
critical_branches = ["main", "prod"]
|
|
10
10
|
highlight_pr_status = true
|
|
11
11
|
highlight_wip = true
|
|
12
|
-
wip_keywords = ["wip", "todo", "fixme", "temp"]
|
|
12
|
+
wip_keywords = ["wip", "todo", "fixme", "temp", "fixup!", "squash!"]
|
|
13
13
|
highlight_direct_pushes = true
|
|
14
14
|
highlight_squashed = true
|
|
15
15
|
highlight_back_merges = true
|
|
@@ -11,6 +11,20 @@ from ..models import Tag
|
|
|
11
11
|
from .visual import find_node
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _should_ignore(commit: GitCommit, rule: str, config: GitLogConfig) -> bool:
|
|
15
|
+
"""Check if a commit should be ignored for a specific hygiene rule."""
|
|
16
|
+
if not config.ignore:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
# Check exact SHA or prefix
|
|
20
|
+
for key, rules in config.ignore.items():
|
|
21
|
+
if commit.reference.hash.startswith(key):
|
|
22
|
+
if rule in rules or "all" in rules:
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
14
28
|
def _apply_divergence_highlights(
|
|
15
29
|
graph: Graph[GitCommit], config: GitLogConfig, force: bool = False
|
|
16
30
|
):
|
|
@@ -37,7 +51,8 @@ def _apply_divergence_highlights(
|
|
|
37
51
|
branch_reach.add(commit)
|
|
38
52
|
|
|
39
53
|
if base_reach - branch_reach:
|
|
40
|
-
commit
|
|
54
|
+
if not _should_ignore(commit, "divergence", config):
|
|
55
|
+
commit.add_tag(Tag.BEHIND.value)
|
|
41
56
|
|
|
42
57
|
|
|
43
58
|
def _apply_orphan_highlights(
|
|
@@ -52,7 +67,8 @@ def _apply_orphan_highlights(
|
|
|
52
67
|
|
|
53
68
|
for commit in graph:
|
|
54
69
|
if commit not in branch_reachable:
|
|
55
|
-
commit
|
|
70
|
+
if not _should_ignore(commit, "orphan", config):
|
|
71
|
+
commit.add_tag(Tag.ORPHAN.value)
|
|
56
72
|
|
|
57
73
|
|
|
58
74
|
def _apply_stale_highlights(
|
|
@@ -72,8 +88,10 @@ def _apply_stale_highlights(
|
|
|
72
88
|
commit.add_tag(f"{Tag.STALE_COLOR.value}{color}")
|
|
73
89
|
|
|
74
90
|
# ONLY apply visual highlight if explicitly requested
|
|
75
|
-
if config.highlight_stale
|
|
76
|
-
commit
|
|
91
|
+
if config.highlight_stale and not _should_ignore(
|
|
92
|
+
commit, "stale", config
|
|
93
|
+
):
|
|
94
|
+
commit.add_tag(Tag.COLOR.value)
|
|
77
95
|
|
|
78
96
|
|
|
79
97
|
def _apply_long_running_highlights(
|
|
@@ -109,22 +127,30 @@ def _apply_long_running_highlights(
|
|
|
109
127
|
|
|
110
128
|
if age_sec > threshold_sec:
|
|
111
129
|
for commit in unique_commits:
|
|
112
|
-
commit
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
if not _should_ignore(commit, "long_running", config):
|
|
131
|
+
commit.add_tag(Tag.LONG_RUNNING.value)
|
|
132
|
+
for parent, _ in graph.internal_depends_on(commit):
|
|
133
|
+
if parent in unique_commits or parent in base_reach:
|
|
134
|
+
commit.set_edge_attribute(
|
|
135
|
+
parent, Tag.EDGE_LONG_RUNNING.value, True
|
|
136
|
+
)
|
|
118
137
|
|
|
119
138
|
|
|
120
139
|
def _apply_wip_highlights(
|
|
121
140
|
graph: Graph[GitCommit], config: GitLogConfig, force: bool = False
|
|
122
141
|
):
|
|
123
142
|
"""Highlight commits with WIP/TODO keywords in message."""
|
|
124
|
-
|
|
143
|
+
import re
|
|
144
|
+
|
|
145
|
+
# Use word boundaries to avoid matching keywords inside other words (e.g. 'swiping')
|
|
146
|
+
# We also ignore cases.
|
|
147
|
+
patterns = [re.compile(rf"\b{re.escape(k)}\b", re.IGNORECASE) for k in config.wip_keywords]
|
|
148
|
+
|
|
125
149
|
for commit in graph:
|
|
126
|
-
|
|
127
|
-
|
|
150
|
+
if _should_ignore(commit, "wip", config):
|
|
151
|
+
continue
|
|
152
|
+
message = commit.reference.message
|
|
153
|
+
if any(p.search(message) for p in patterns):
|
|
128
154
|
commit.add_tag(Tag.WIP.value)
|
|
129
155
|
|
|
130
156
|
|
|
@@ -139,6 +165,8 @@ def _apply_direct_push_highlights(
|
|
|
139
165
|
}
|
|
140
166
|
|
|
141
167
|
for commit in graph:
|
|
168
|
+
if _should_ignore(commit, "direct_push", config):
|
|
169
|
+
continue
|
|
142
170
|
if len(commit.reference.parents) > 1:
|
|
143
171
|
continue
|
|
144
172
|
for branch in commit.reference.branches:
|
|
@@ -181,7 +209,8 @@ def _apply_back_merge_highlights(
|
|
|
181
209
|
has_base_parent = any(p in base_reach for p in parents)
|
|
182
210
|
has_non_base_parent = any(p not in base_reach for p in parents)
|
|
183
211
|
if has_base_parent and has_non_base_parent:
|
|
184
|
-
commit
|
|
212
|
+
if not _should_ignore(commit, "back_merge", config):
|
|
213
|
+
commit.add_tag(Tag.BACK_MERGE.value)
|
|
185
214
|
|
|
186
215
|
|
|
187
216
|
def _apply_silo_highlights(
|
|
@@ -209,4 +238,5 @@ def _apply_silo_highlights(
|
|
|
209
238
|
if len(unique_commits) >= config.silo_commit_threshold:
|
|
210
239
|
authors = {c.reference.author for c in unique_commits}
|
|
211
240
|
if len(authors) <= config.silo_author_count:
|
|
212
|
-
tip
|
|
241
|
+
if not _should_ignore(tip, "silo", config):
|
|
242
|
+
tip.add_tag(Tag.CONTRIBUTOR_SILO.value)
|