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.
Files changed (92) hide show
  1. {git_graphable-0.6.0 → git_graphable-0.7.0}/CHANGELOG.md +13 -0
  2. git_graphable-0.7.0/HYGIENE.md +168 -0
  3. {git_graphable-0.6.0 → git_graphable-0.7.0}/PKG-INFO +8 -5
  4. {git_graphable-0.6.0 → git_graphable-0.7.0}/README.md +7 -4
  5. {git_graphable-0.6.0 → git_graphable-0.7.0}/USAGE.md +2 -1
  6. {git_graphable-0.6.0 → git_graphable-0.7.0}/pyproject.toml +7 -1
  7. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/bare_cli.py +31 -14
  8. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/core.py +11 -1
  9. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/default_config.toml +1 -1
  10. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlights/hygiene.py +45 -15
  11. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/hygiene.py +96 -11
  12. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/__init__.py +1 -0
  13. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/jira.py +19 -2
  14. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/script.py +11 -10
  15. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/script.py +4 -4
  16. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/rich_cli.py +20 -1
  17. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styling/base.py +1 -1
  18. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styling/html.py +6 -3
  19. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/test_jira_engine.py +5 -1
  20. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/test_script.py +13 -12
  21. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/prs/test_script.py +13 -14
  22. {git_graphable-0.6.0 → git_graphable-0.7.0}/.gemini/GEMINI.md +0 -0
  23. {git_graphable-0.6.0 → git_graphable-0.7.0}/.gemini/code-ordering.md +0 -0
  24. {git_graphable-0.6.0 → git_graphable-0.7.0}/.github/dependabot.yml +0 -0
  25. {git_graphable-0.6.0 → git_graphable-0.7.0}/.github/workflows/ci.yml +0 -0
  26. {git_graphable-0.6.0 → git_graphable-0.7.0}/.github/workflows/pages.yml +0 -0
  27. {git_graphable-0.6.0 → git_graphable-0.7.0}/.github/workflows/publish.yml +0 -0
  28. {git_graphable-0.6.0 → git_graphable-0.7.0}/.gitignore +0 -0
  29. {git_graphable-0.6.0 → git_graphable-0.7.0}/.python-version +0 -0
  30. {git_graphable-0.6.0 → git_graphable-0.7.0}/Justfile +0 -0
  31. {git_graphable-0.6.0 → git_graphable-0.7.0}/LICENSE +0 -0
  32. {git_graphable-0.6.0 → git_graphable-0.7.0}/SECURITY.md +0 -0
  33. {git_graphable-0.6.0 → git_graphable-0.7.0}/STYLING.md +0 -0
  34. {git_graphable-0.6.0 → git_graphable-0.7.0}/action.yml +0 -0
  35. {git_graphable-0.6.0 → git_graphable-0.7.0}/examples/EXAMPLES.md +0 -0
  36. {git_graphable-0.6.0 → git_graphable-0.7.0}/examples/generate_demos.py +0 -0
  37. {git_graphable-0.6.0 → git_graphable-0.7.0}/examples/index_template.html +0 -0
  38. {git_graphable-0.6.0 → git_graphable-0.7.0}/examples/publish_demos.py +0 -0
  39. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/__init__.py +0 -0
  40. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/cli.py +0 -0
  41. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/cli_utils.py +0 -0
  42. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/commands.py +0 -0
  43. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/github.py +0 -0
  44. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlighter.py +0 -0
  45. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlights/core.py +0 -0
  46. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlights/external.py +0 -0
  47. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/highlights/visual.py +0 -0
  48. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/base.py +0 -0
  49. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/github.py +0 -0
  50. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/issues/gitlab.py +0 -0
  51. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/models.py +0 -0
  52. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/parser.py +0 -0
  53. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/__init__.py +0 -0
  54. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/base.py +0 -0
  55. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/github.py +0 -0
  56. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/prs/gitlab.py +0 -0
  57. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styler.py +0 -0
  58. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styling/generic.py +0 -0
  59. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/styling/mermaid.py +0 -0
  60. {git_graphable-0.6.0 → git_graphable-0.7.0}/src/git_graphable/templates.py +0 -0
  61. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/__init__.py +0 -0
  62. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/conftest.py +0 -0
  63. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/highlights/__init__.py +0 -0
  64. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/highlights/test_external.py +0 -0
  65. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/highlights/test_hygiene.py +0 -0
  66. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/highlights/test_visual.py +0 -0
  67. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/__init__.py +0 -0
  68. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/test_github.py +0 -0
  69. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/issues/test_gitlab.py +0 -0
  70. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/prs/__init__.py +0 -0
  71. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/prs/test_github.py +0 -0
  72. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/prs/test_gitlab.py +0 -0
  73. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/styling/__init__.py +0 -0
  74. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/styling/test_generic.py +0 -0
  75. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/styling/test_mermaid.py +0 -0
  76. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_bare_cli.py +0 -0
  77. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_cli.py +0 -0
  78. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_cli_utils.py +0 -0
  79. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_commands.py +0 -0
  80. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_config.py +0 -0
  81. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_core.py +0 -0
  82. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_examples_html.py +0 -0
  83. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_github.py +0 -0
  84. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_highlighter.py +0 -0
  85. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_hygiene_scorer.py +0 -0
  86. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_interactive_html.py +0 -0
  87. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_models.py +0 -0
  88. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_parser.py +0 -0
  89. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_rich_cli.py +0 -0
  90. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_styler.py +0 -0
  91. {git_graphable-0.6.0 → git_graphable-0.7.0}/tests/test_ui_interactive.py +0 -0
  92. {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.6.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 ensures custom scripts only run from trusted sources (use `--trust` to authorize local configs).
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.6.0
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 ensures custom scripts only run from trusted sources (use `--trust` to authorize local configs).
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.6.0
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
- * `--trust`: Trust configuration files (.git-graphable.toml, pyproject.toml) found in the repository. Required to execute custom scripts from these sources.
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.6.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
- "--check",
155
- action="store_true",
156
- help="Exit with non-zero if hygiene score is below threshold",
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
- "--hygiene-output",
161
- help="Path to save hygiene summary as JSON",
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
- "--trust",
166
+ "--check",
165
167
  action="store_true",
166
- help="Trust configuration files found in the repository",
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
- "--penalty",
173
+ "--ignore",
170
174
  action="append",
171
175
  default=[],
172
- help="Override hygiene penalty (format: metric:value, e.g. direct_push_penalty:20)",
176
+ help="Ignore hygiene rules for specific SHAs (format: sha:rule)",
173
177
  )
174
178
  p.add_argument(
175
- "--style",
176
- action="append",
177
- default=[],
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.add_tag(Tag.BEHIND.value)
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.add_tag(Tag.ORPHAN.value)
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.add_tag(f"{Tag.COLOR.value}{color}")
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.add_tag(Tag.LONG_RUNNING.value)
113
- for parent, _ in graph.internal_depends_on(commit):
114
- if parent in unique_commits or parent in base_reach:
115
- commit.set_edge_attribute(
116
- parent, Tag.EDGE_LONG_RUNNING.value, True
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
- keywords = [k.lower() for k in config.wip_keywords]
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
- message = commit.reference.message.lower()
127
- if any(k in message for k in keywords):
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.add_tag(Tag.BACK_MERGE.value)
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.add_tag(Tag.CONTRIBUTOR_SILO.value)
241
+ if not _should_ignore(tip, "silo", config):
242
+ tip.add_tag(Tag.CONTRIBUTOR_SILO.value)