Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable changes to this project will be documented in this file.

### Added
- **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.
- **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.
- **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.
- **Remediation Guidelines**: Added a comprehensive [HYGIENE.md](HYGIENE.md) guide with actionable Git commands to help users improve their project's hygiene score.

### Fixed
- **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.
Expand Down
168 changes: 168 additions & 0 deletions HYGIENE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Git Hygiene Guidelines

This document explains the hygiene metrics analyzed by `git-graphable` and provides actionable steps to remediate common issues.

## 1. Process Integrity

### Direct Pushes to Protected Branches
**Detection:** Commits made directly to `production_branch` or `development_branch` without a merge commit.
**Why it matters:** Bypassing the Pull Request process avoids code review and CI checks, increasing the risk of bugs.
**Remediation:**
1. **Stop**: Do not push directly to main/master/develop.
2. **Fix History** (if not yet shared/stable):
```bash
# Move the commit to a new branch
git checkout -b feature/my-fix

# Reset main back to before the direct push
git checkout main
git reset --hard <commit-before-push>

# Push the new branch and open a PR
git push -u origin feature/my-fix
```
3. **Protection**: Enable "Branch Protection Rules" in GitHub/GitLab settings to physically prevent direct pushes.

### Conflicting Pull Requests
**Detection:** Open PRs marked as `CONFLICTING` by the VCS provider.
**Why it matters:** Conflicts block merging and indicate divergent development paths that get harder to resolve over time.
**Remediation:**
```bash
git checkout feature/my-branch
git pull origin main
# Resolve conflicts in editor
git add .
git commit -m "Merge branch 'main' into feature/my-branch"
git push
```

### Orphan/Dangling Commits
**Detection:** Commits that are not reachable from any branch or tag.
**Why it matters:** These are often lost code or mistakes.
**Remediation:**
- **Garbage Collect**: `git gc --prune=now`
- **Recover**: If valuable, checkout the SHA and create a branch: `git checkout -b recover-work <sha>`

## 2. Cleanliness

### WIP / Fixup Commits
**Detection:** Commit messages containing "wip", "todo", "fixup", "temp".
**Why it matters:** messy history makes debugging and `git bisect` difficult.
**Remediation:**
- **Interactive Rebase**: Squash WIP commits into meaningful units.
```bash
git rebase -i HEAD~n # where n is number of commits back
# Change 'pick' to 'squash' or 'fixup' for the WIP commits
```

### Stale Branches
**Detection:** Feature branches with no activity for `stale_days` (default: 30).
**Why it matters:** Clutters the repository and indicates abandoned work.
**Remediation:**
- **Delete Local**: `git branch -d branch-name`
- **Delete Remote**: `git push origin --delete branch-name`
- **Archive**: Tag it if you need to keep it: `git tag archive/branch-name branch-name`

## 3. Connectivity & Flow

### Long-Running Branches
**Detection:** Feature branches that have diverged from the base for more than `long_running_days` (default: 14).
**Why it matters:** Increases the risk of massive merge conflicts (The "Merge Hell").
**Remediation:**
- **Merge Often**: Merge `main` into your feature branch frequently.
- **Ship Smaller**: Break large features into smaller, mergeable PRs.

### Divergence (Behind Base)
**Detection:** Feature branches that are missing commits from the base branch (`main`).
**Why it matters:** You are testing against outdated code.
**Remediation:**
```bash
git checkout feature/my-feature
git pull origin main
git push
```

### Redundant Back-Merges
**Detection:** Merging `main` into a feature branch, but doing it recursively or unnecessarily often creates a "railroad track" history.
**Why it matters:** Makes history hard to read.
**Remediation:**
- Use `git rebase main` instead of `git merge main` for feature branches (if your team policy allows rewriting feature branch history).

## 4. Collaboration

### Contributor Silos
**Detection:** Long sequences of commits on a branch by a single author without interaction from others.
**Why it matters:** Risk of "Bus Factor". No code review or shared knowledge.
**Remediation:**
- **Pair Program**: Involve others earlier.
- **Early PRs**: Open a Draft PR to get feedback before the feature is done.

### Collaboration Gaps
**Detection:** The Git commit author does not match the assignee of the linked Issue Tracker ticket.
**Remediation:**
- Update the Issue Tracker ticket to assign it to the actual developer.
- Configure `author_mapping` in `.git-graphable.toml` if names just don't match (e.g., "John Doe" vs "jdoe").

## 5. Consistency (Issue Tracking)

### Issue / Git Desync
**Detection:**
- Ticket is `OPEN` but PR is `MERGED`.
- Ticket is `CLOSED` but PR is `OPEN`.
**Remediation:**
- **Sync Status**: Manually update the ticket status.
- **Automation**: Configure GitHub/Jira to auto-close issues when PRs are merged.

### Release Inconsistencies
**Detection:** Ticket is marked `RELEASED` but the commit is not included in any Git Tag.
**Remediation:**
- **Cut a Release**: Create a Git tag for the deployment.
```bash
git tag v1.0.0
git push --tags
```

### Longevity Mismatch
**Detection:** A large time gap (>14 days) exists between when a ticket was created and when work (commits) started.
**Why it matters:** Planning failure or stale requirements.
**Remediation:**
- **Review Backlog**: Don't open tickets until work is ready to start.
- **Re-evaluate**: If a ticket sits for 2 weeks, check if requirements have changed before starting code.

## 6. Ignoring Hygiene Rules

Sometimes a commit or branch is flagged for a reason that is acceptable or intended. You can selectively ignore these rules.

### Via Configuration (`.git-graphable.toml`)

Add an `[git-graphable.ignore]` section to your configuration:

```toml
[git-graphable.ignore]
# Ignore specific rules for specific SHAs (prefix or full SHA)
"9bd5377" = ["wip", "direct_push"]
"abc1234" = ["all"] # Ignore all hygiene rules for this commit
```

### Via CLI

Use the `--ignore` flag:

```bash
# Ignore WIP rule for a specific SHA
uv run git-graphable analyze . --ignore 9bd5377:wip

# Ignore multiple items
uv run git-graphable analyze . --ignore 9bd5377:wip --ignore abc1234:all
```

### Supported Rule Names
- `wip`
- `direct_push`
- `divergence`
- `orphan`
- `stale`
- `long_running`
- `back_merge`
- `silo`
- `all` (ignores everything for that SHA)
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ Check out the tool in action with our **[Live Interactive Demos](https://thetrue
- **Automatic Visualization**: Generates and opens an image (PNG) automatically if no output is specified.
- **Advanced Highlighting**: Visualize author patterns, topological distance, and specific merge paths.
- **VCS Integration**: Highlight commits based on pull request/merge request status using `gh` (GitHub) or `glab` (GitLab) CLIs.
- **Hygiene Analysis**: Automatically detect WIP commits, direct pushes to protected branches, squashed PRs, back-merges, and contributor silos.
- **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.
- **Issue Tracker Integration**: Connect to Jira, GitHub Issues, GitLab Issues, or custom scripts to highlight status desyncs.
- **Security First**: Configuration trust mechanism enforces security by requiring explicit authorization (use `--trust`) to execute custom scripts or send credentials from repository-local configs.
- **Selective Ignores**: Suppress specific hygiene rules for given commit SHAs using the configuration file or `--ignore` CLI flag.
- **Remediation Guide**: Detailed guidelines in [HYGIENE.md](HYGIENE.md) help you reach a 100% score.
- **Dynamic Badges**: Host live Shields.io badges for Git Hygiene and Code Coverage on GitHub Pages.

## Installation
Expand Down Expand Up @@ -137,6 +139,7 @@ Identify problematic patterns like direct pushes to `main`, messy WIP commits, b
```bash
uv run git-graphable analyze . --highlight-direct-pushes --highlight-wip --highlight-squashed --highlight-back-merges --highlight-silos
```
> **Tip:** See [HYGIENE.md](HYGIENE.md) for a detailed guide on how to remediate these issues and improve your score.

### PR Status Highlighting
View the current state of all PRs in your repository graph:
Expand Down
1 change: 1 addition & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Analyze git history and generate a graph. This is the default command; if no com
* `--min-score INTEGER`: Minimum hygiene score required for --check
* `--bare`: Force bare mode (no rich output)
* `--hygiene-output TEXT`: Path to save hygiene summary as JSON.
* `--ignore TEXT`: Ignore hygiene rules for specific SHAs (format: `sha:rule`). Use `all` as the rule to ignore all checks for a commit.
* `--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.
* `--help`: Show this message and exit.

Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,8 @@ skip_covered = true
markers = [
"ui: UI and browser-based tests",
]

[tool.git-graphable.ignore]
"9bd5377" = ["wip"]
"7a2409e" = ["wip"]
"24b44fb" = ["wip"]
46 changes: 32 additions & 14 deletions src/git_graphable/bare_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,31 +151,34 @@ def add_analyze_args(p):
help="Threshold in days for longevity mismatch detection",
)
p.add_argument(
"--check",
action="store_true",
help="Exit with non-zero if hygiene score is below threshold",
"--penalty",
action="append",
default=[],
help="Override hygiene penalty (format: metric:value, e.g. direct_push_penalty:20)",
)
p.add_argument("--min-score", type=int, help="Minimum score for --check")
p.add_argument(
"--hygiene-output",
help="Path to save hygiene summary as JSON",
"--style",
action="append",
default=[],
help="Override visual style (format: key:property:value, e.g. critical:stroke:teal)",
)
p.add_argument(
"--trust",
"--check",
action="store_true",
help="Trust configuration files found in the repository",
help="Exit with non-zero if hygiene score is below threshold",
)
p.add_argument("--min-score", type=int, help="Minimum score for --check")
p.add_argument("--hygiene-output", help="Path to save hygiene summary as JSON")
p.add_argument(
"--penalty",
"--ignore",
action="append",
default=[],
help="Override hygiene penalty (format: metric:value, e.g. direct_push_penalty:20)",
help="Ignore hygiene rules for specific SHAs (format: sha:rule)",
)
p.add_argument(
"--style",
action="append",
default=[],
help="Override visual style (format: key:property:value, e.g. critical:stroke:teal)",
"--trust",
action="store_true",
help="Trust configuration files found in the repository",
)

add_analyze_args(analyze_parser)
Expand Down Expand Up @@ -294,12 +297,23 @@ def add_analyze_args(p):
}
if args.penalty
else {},
"ignore": {},
"theme": parse_style_overrides(args.style) if args.style else {},
"min_hygiene_score": args.min_score,
"hygiene_output": args.hygiene_output,
"trust": args.trust,
}

if args.ignore:
ignore_dict = {}
for item in args.ignore:
if ":" in item:
key, val = item.split(":", 1)
if key not in ignore_dict:
ignore_dict[key] = []
ignore_dict[key].append(val)
overrides["ignore"] = ignore_dict

try:
results = convert_command(
args.path,
Expand Down Expand Up @@ -328,6 +342,10 @@ def add_analyze_args(p):
)
for deduction in hygiene.get("deductions", []):
print(f" - {deduction['message']} (-{deduction['amount']}%)")
for item in deduction.get("items", []):
print(f" * {item}")

print("\nSee HYGIENE.md for remediation guidelines.")

if args.check:
min_s = args.min_score or 80
Expand Down
10 changes: 10 additions & 0 deletions src/git_graphable/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class GitLogConfig:
longevity_threshold_days: int = (
14 # Max diff between Issue created and first commit
)
ignore: Dict[str, List[str]] = field(default_factory=dict)
trusted: bool = True # True if explicitly provided via CLI or from a trusted source
trust: bool = False # CLI override to force trust
hygiene_weights: HygieneWeights = field(default_factory=HygieneWeights)
Expand Down Expand Up @@ -222,6 +223,8 @@ def from_toml(cls, file_path: str) -> "GitLogConfig":
weights_data = config_data.pop("hygiene_weights", {})
# Handle nested theme
theme_data = config_data.pop("theme", {})
# Handle nested ignore
ignore_data = config_data.pop("ignore", {})

config = cls(
**{
Expand All @@ -235,6 +238,9 @@ def from_toml(cls, file_path: str) -> "GitLogConfig":
if hasattr(config.hygiene_weights, k):
setattr(config.hygiene_weights, k, v)

if ignore_data:
config.ignore = ignore_data

if theme_data:
for k, v in theme_data.items():
if hasattr(config.theme, k):
Expand Down Expand Up @@ -293,6 +299,10 @@ def merge(self, other: Dict[str, Any]) -> "GitLogConfig":
setattr(current_style, s_key, s_val)
else:
setattr(new_config.theme, t_key, t_val)
elif key == "ignore" and isinstance(value, dict):
# Merge ignore if provided as dict
for i_key, i_val in value.items():
new_config.ignore[i_key] = i_val
elif isinstance(value, list) and not value:
# Special case for lists: only override if not empty
continue
Expand Down
Loading