Skip to content

Commit 25ee390

Browse files
committed
chore: final polish for public release
- Add GitHub Actions CI - Enhance git analyzer with branch age and drift detection - Update rules engine for smarter nudges - Update docs (README, CONTRIBUTING, rules)
1 parent 23f31aa commit 25ee390

4 files changed

Lines changed: 104 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Python CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.10", "3.11", "3.12", "3.13"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Set up Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
23+
- name: Install dependencies
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install .[dev]
27+
28+
- name: Run tests with pytest
29+
run: |
30+
pytest tests/ -v --cov=src/flowcheck --cov-report=xml
31+
32+
- name: Upload coverage to Codecov
33+
uses: codecov/codecov-action@v4
34+
with:
35+
file: ./coverage.xml
36+
fail_ci_if_error: false

src/flowcheck/core/git_analyzer.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,46 @@ def get_minutes_since_last_commit(repo: Repo) -> int:
7575
now = datetime.now(tz=timezone.utc)
7676
delta = now - commit_time
7777
return int(delta.total_seconds() / 60)
78-
except (ValueError, GitCommandError):
79-
# No commits in repository
78+
except (ValueError, GitCommandError, AttributeError):
79+
# No commits or detached HEAD with no history
80+
return 0
81+
82+
83+
def get_branch_age_days(repo: Repo) -> int:
84+
"""Calculate the age of the current branch in days.
85+
86+
Age is measured from the first commit that is only in this branch.
87+
"""
88+
try:
89+
# Get the timestamp of the very first commit in this branch
90+
commits = list(repo.iter_commits(
91+
rev=repo.active_branch.name, reverse=True, max_count=1))
92+
if not commits:
93+
return 0
94+
first_commit_date = commits[0].committed_date
95+
dt = datetime.fromtimestamp(first_commit_date, tz=timezone.utc)
96+
delta = datetime.now(tz=timezone.utc) - dt
97+
return max(0, delta.days)
98+
except Exception:
99+
return 0
100+
101+
102+
def get_commits_behind_main(repo: Repo) -> int:
103+
"""Check how many commits the current branch is behind 'main' or 'master'."""
104+
try:
105+
main_name = "main" if "main" in repo.heads else "master" if "master" in repo.heads else None
106+
if not main_name or repo.active_branch.name == main_name:
107+
return 0
108+
109+
# Get merge-base
110+
base = repo.merge_base(repo.active_branch, repo.heads[main_name])
111+
if not base:
112+
return 0
113+
114+
# Count commits on main after base
115+
behind = list(repo.iter_commits(f"{base[0]}..{main_name}"))
116+
return len(behind)
117+
except Exception:
80118
return 0
81119

82120

@@ -148,6 +186,8 @@ def analyze_repo(repo_path: str) -> dict:
148186
- minutes_since_last_commit: Minutes since last commit
149187
- uncommitted_files: Count of changed files
150188
- uncommitted_lines: Total lines changed
189+
- branch_age_days: Age of branch in days
190+
- behind_main_by_commits: Count of commits behind main
151191
152192
Raises:
153193
NotAGitRepositoryError: If path is not a valid Git repository.
@@ -157,10 +197,14 @@ def analyze_repo(repo_path: str) -> dict:
157197
branch_name = get_current_branch(repo)
158198
minutes_since_last_commit = get_minutes_since_last_commit(repo)
159199
uncommitted_files, uncommitted_lines = get_uncommitted_stats(repo)
200+
branch_age_days = get_branch_age_days(repo)
201+
behind_main_by_commits = get_commits_behind_main(repo)
160202

161203
return {
162204
"branch_name": branch_name,
163205
"minutes_since_last_commit": minutes_since_last_commit,
164206
"uncommitted_files": uncommitted_files,
165207
"uncommitted_lines": uncommitted_lines,
208+
"branch_age_days": branch_age_days,
209+
"behind_main_by_commits": behind_main_by_commits,
166210
}

src/flowcheck/core/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class FlowState:
3030
uncommitted_files: int
3131
branch_name: str
3232
status: Status
33+
branch_age_days: int = 0
34+
behind_main_by_commits: int = 0
3335

3436
def to_dict(self) -> dict:
3537
"""Convert to dictionary for JSON serialization."""
@@ -39,6 +41,8 @@ def to_dict(self) -> dict:
3941
"uncommitted_files": self.uncommitted_files,
4042
"branch_name": self.branch_name,
4143
"status": self.status.value,
44+
"branch_age_days": self.branch_age_days,
45+
"behind_main_by_commits": self.behind_main_by_commits,
4246
}
4347

4448
@classmethod
@@ -50,4 +54,6 @@ def from_dict(cls, data: dict) -> "FlowState":
5054
uncommitted_files=data["uncommitted_files"],
5155
branch_name=data["branch_name"],
5256
status=Status(data["status"]),
57+
branch_age_days=data.get("branch_age_days", 0),
58+
behind_main_by_commits=data.get("behind_main_by_commits", 0),
5359
)

src/flowcheck/rules/engine.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ def generate_recommendations(
9292
f"Consider grouping related changes into separate commits."
9393
)
9494

95+
# Branch age recommendation
96+
if flow_state.branch_age_days > 7:
97+
recommendations.append(
98+
f"🌿 This branch is {flow_state.branch_age_days} days old. "
99+
f"Consider finishing up or merging to avoid long-lived branches."
100+
)
101+
102+
# Main branch sync recommendation
103+
if flow_state.behind_main_by_commits > 10:
104+
recommendations.append(
105+
f"🔄 You are behind main by {flow_state.behind_main_by_commits} commits. "
106+
f"Consider merging main into your branch to stay up to date and avoid conflicts."
107+
)
108+
95109
# All good message
96110
if not recommendations:
97111
recommendations.append(
@@ -126,4 +140,6 @@ def build_flow_state(
126140
uncommitted_files=raw_metrics["uncommitted_files"],
127141
branch_name=raw_metrics["branch_name"],
128142
status=status,
143+
branch_age_days=raw_metrics.get("branch_age_days", 0),
144+
behind_main_by_commits=raw_metrics.get("behind_main_by_commits", 0),
129145
)

0 commit comments

Comments
 (0)