axm-git 0.2.0__tar.gz → 0.3.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.
- {axm_git-0.2.0 → axm_git-0.3.0}/.gitignore +1 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/PKG-INFO +9 -4
- {axm_git-0.2.0 → axm_git-0.3.0}/README.md +7 -3
- {axm_git-0.2.0 → axm_git-0.3.0}/docs/explanation/architecture.md +2 -2
- {axm_git-0.2.0 → axm_git-0.3.0}/docs/index.md +2 -2
- {axm_git-0.2.0 → axm_git-0.3.0}/docs/reference/cli.md +1 -1
- {axm_git-0.2.0 → axm_git-0.3.0}/docs/tutorials/getting-started.md +1 -1
- {axm_git-0.2.0 → axm_git-0.3.0}/pyproject.toml +2 -1
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/_version.py +2 -2
- axm_git-0.3.0/src/axm_git/core/__init__.py +17 -0
- axm_git-0.3.0/src/axm_git/core/identity.py +162 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/core/runner.py +1 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/await_merge.py +21 -7
- axm_git-0.3.0/src/axm_git/hooks/commit_phase.py +342 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/preflight.py +17 -6
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/commit.py +86 -51
- axm_git-0.3.0/tests/hooks/test_commit_phase_retry.py +137 -0
- axm_git-0.3.0/tests/hooks/test_commit_phase_staging.py +136 -0
- axm_git-0.3.0/tests/test_commit_edge.py +126 -0
- axm_git-0.3.0/tests/test_commit_identity.py +268 -0
- axm_git-0.3.0/tests/test_commit_phase_identity.py +357 -0
- axm_git-0.3.0/tests/test_commit_phase_result.py +175 -0
- axm_git-0.3.0/tests/test_identity_helpers.py +171 -0
- axm_git-0.3.0/tests/unit/core/test_identity.py +307 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_commit_phase.py +130 -0
- axm_git-0.3.0/tests/unit/hooks/test_commit_phase_format.py +153 -0
- axm_git-0.3.0/tests/unit/hooks/test_commit_phase_skip_hooks.py +197 -0
- axm_git-0.3.0/tests/unit/hooks/test_resolve_pr_ref.py +31 -0
- axm_git-0.3.0/tests/unit/hooks/test_truncate_diff.py +26 -0
- axm_git-0.2.0/src/axm_git/core/__init__.py +0 -1
- axm_git-0.2.0/src/axm_git/hooks/commit_phase.py +0 -171
- {axm_git-0.2.0 → axm_git-0.3.0}/.python-version +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/CONTRIBUTING.md +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/LICENSE +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/Makefile +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/docs/howto/index.md +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/mkdocs.yml +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/__init__.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/core/branch_naming.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/core/phase_commit.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/core/semver.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/__init__.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/_resolve.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/branch_delete.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/create_branch.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/create_pr.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/merge_squash.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/pull.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/push.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/worktree_add.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/worktree_remove.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/py.typed +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/__init__.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/branch.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/commit_preflight.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/pr.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/push.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/tag.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/worktree.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/__init__.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/conftest.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/functional/__init__.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/functional/test_flows.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/functional/test_hook_discovery.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/functional/test_worktree_roundtrip.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/test_version.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/__init__.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/__init__.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/test_branch_naming.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/test_phase_commit.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/test_runner.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/test_semver.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/__init__.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test__resolve.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_await_merge.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_branch_delete.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_commit_phase_retry.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_create_branch.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_create_pr.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_merge_squash.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_preflight.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_pull.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_push.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_worktree_add.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_worktree_remove.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/test_tag_helpers.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/__init__.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_branch.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_commit.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_commit_preflight.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_pr.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_push.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_tag.py +0 -0
- {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_worktree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: axm-git
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Git workflow automation for AXM agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/axm-protocols/axm-git
|
|
6
6
|
Project-URL: Documentation, https://axm-protocols.github.io/axm-git/
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Typing :: Typed
|
|
17
17
|
Requires-Python: >=3.12
|
|
18
18
|
Requires-Dist: axm>=0.1.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
|
|
21
22
|
<p align="center">
|
|
@@ -28,8 +29,8 @@ Description-Content-Type: text/markdown
|
|
|
28
29
|
|
|
29
30
|
<p align="center">
|
|
30
31
|
<a href="https://github.com/axm-protocols/axm-forge/actions/workflows/ci.yml"><img src="https://github.com/axm-protocols/axm-forge/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
31
|
-
<a href="https://
|
|
32
|
-
<a href="https://
|
|
32
|
+
<a href="https://forge.axm-protocols.io/audit/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/axm-protocols/axm-forge/gh-pages/badges/axm-git/axm-audit.json" alt="axm-audit"></a>
|
|
33
|
+
<a href="https://forge.axm-protocols.io/init/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/axm-protocols/axm-forge/gh-pages/badges/axm-git/axm-init.json" alt="axm-init"></a>
|
|
33
34
|
<a href="https://github.com/axm-protocols/axm-forge/actions/workflows/axm-quality.yml"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/axm-protocols/axm-forge/gh-pages/badges/axm-git/coverage.json" alt="Coverage"></a>
|
|
34
35
|
<a href="https://pypi.org/project/axm-git/"><img src="https://img.shields.io/pypi/v/axm-git" alt="PyPI"></a>
|
|
35
36
|
<img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="Python 3.12+">
|
|
@@ -42,13 +43,14 @@ Description-Content-Type: text/markdown
|
|
|
42
43
|
|
|
43
44
|
- 🔍 **Preflight** — Structured working tree status with diff summary
|
|
44
45
|
- 🌿 **Branch** — Create or checkout branches with one call
|
|
45
|
-
- 📦 **Commit** — Batched atomic commits with auto-retry on pre-commit fixes
|
|
46
|
+
- 📦 **Commit** — Batched atomic commits with auto-retry on pre-commit fixes and optional author identity injection
|
|
46
47
|
- 🏷️ **Tag** — One-shot semver tagging from Conventional Commits
|
|
47
48
|
- 🚀 **Push** — Push with dirty-check, auto-upstream detection, and force support
|
|
48
49
|
- 🌲 **Worktree** — Add, remove, or list git worktrees
|
|
49
50
|
- 🔀 **PR** — Create GitHub pull requests with optional auto-merge
|
|
50
51
|
- 🧭 **Error Recovery** — When called on a non-git directory, tools suggest nearby git repos
|
|
51
52
|
- 🪝 **Hooks** — Lifecycle hook actions (preflight, create-branch, branch-delete, commit-phase, merge-squash, worktree-add, worktree-remove, push, create-pr, await-merge, pull-main) with `enabled` guard, auto-discovered via entry-points
|
|
53
|
+
- 🪪 **Identity** — Resolve git author from `git-profiles.toml` with schedule-based or explicit profile selection
|
|
52
54
|
- 🔎 **Phase Lookup** — `get_phase_commit()` retrieves commit hashes for protocol phases
|
|
53
55
|
|
|
54
56
|
## Installation
|
|
@@ -118,6 +120,7 @@ Execute one or more atomic commits with pre-commit hook handling.
|
|
|
118
120
|
|---|---|---|
|
|
119
121
|
| `path` | `.` | Project root directory |
|
|
120
122
|
| `commits` | *required* | List of commit specs (see below) |
|
|
123
|
+
| `profile` | `None` | Identity profile name — overrides schedule-based resolution from `git-profiles.toml` |
|
|
121
124
|
|
|
122
125
|
Each commit spec:
|
|
123
126
|
|
|
@@ -129,6 +132,8 @@ Each commit spec:
|
|
|
129
132
|
|
|
130
133
|
When a pre-commit hook auto-fixes files (e.g. ruff `--fix`), the tool re-stages and retries once automatically.
|
|
131
134
|
|
|
135
|
+
Identity is resolved once per call (not per commit). When resolved, each commit includes `--author="Name <email>"`. The result includes an `author` key (`{name, email}` or `null`).
|
|
136
|
+
|
|
132
137
|
### `git_tag`
|
|
133
138
|
|
|
134
139
|
Compute the next semver version from Conventional Commits, create and push the tag.
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://github.com/axm-protocols/axm-forge/actions/workflows/ci.yml"><img src="https://github.com/axm-protocols/axm-forge/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
11
|
-
<a href="https://
|
|
12
|
-
<a href="https://
|
|
11
|
+
<a href="https://forge.axm-protocols.io/audit/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/axm-protocols/axm-forge/gh-pages/badges/axm-git/axm-audit.json" alt="axm-audit"></a>
|
|
12
|
+
<a href="https://forge.axm-protocols.io/init/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/axm-protocols/axm-forge/gh-pages/badges/axm-git/axm-init.json" alt="axm-init"></a>
|
|
13
13
|
<a href="https://github.com/axm-protocols/axm-forge/actions/workflows/axm-quality.yml"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/axm-protocols/axm-forge/gh-pages/badges/axm-git/coverage.json" alt="Coverage"></a>
|
|
14
14
|
<a href="https://pypi.org/project/axm-git/"><img src="https://img.shields.io/pypi/v/axm-git" alt="PyPI"></a>
|
|
15
15
|
<img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="Python 3.12+">
|
|
@@ -22,13 +22,14 @@
|
|
|
22
22
|
|
|
23
23
|
- 🔍 **Preflight** — Structured working tree status with diff summary
|
|
24
24
|
- 🌿 **Branch** — Create or checkout branches with one call
|
|
25
|
-
- 📦 **Commit** — Batched atomic commits with auto-retry on pre-commit fixes
|
|
25
|
+
- 📦 **Commit** — Batched atomic commits with auto-retry on pre-commit fixes and optional author identity injection
|
|
26
26
|
- 🏷️ **Tag** — One-shot semver tagging from Conventional Commits
|
|
27
27
|
- 🚀 **Push** — Push with dirty-check, auto-upstream detection, and force support
|
|
28
28
|
- 🌲 **Worktree** — Add, remove, or list git worktrees
|
|
29
29
|
- 🔀 **PR** — Create GitHub pull requests with optional auto-merge
|
|
30
30
|
- 🧭 **Error Recovery** — When called on a non-git directory, tools suggest nearby git repos
|
|
31
31
|
- 🪝 **Hooks** — Lifecycle hook actions (preflight, create-branch, branch-delete, commit-phase, merge-squash, worktree-add, worktree-remove, push, create-pr, await-merge, pull-main) with `enabled` guard, auto-discovered via entry-points
|
|
32
|
+
- 🪪 **Identity** — Resolve git author from `git-profiles.toml` with schedule-based or explicit profile selection
|
|
32
33
|
- 🔎 **Phase Lookup** — `get_phase_commit()` retrieves commit hashes for protocol phases
|
|
33
34
|
|
|
34
35
|
## Installation
|
|
@@ -98,6 +99,7 @@ Execute one or more atomic commits with pre-commit hook handling.
|
|
|
98
99
|
|---|---|---|
|
|
99
100
|
| `path` | `.` | Project root directory |
|
|
100
101
|
| `commits` | *required* | List of commit specs (see below) |
|
|
102
|
+
| `profile` | `None` | Identity profile name — overrides schedule-based resolution from `git-profiles.toml` |
|
|
101
103
|
|
|
102
104
|
Each commit spec:
|
|
103
105
|
|
|
@@ -109,6 +111,8 @@ Each commit spec:
|
|
|
109
111
|
|
|
110
112
|
When a pre-commit hook auto-fixes files (e.g. ruff `--fix`), the tool re-stages and retries once automatically.
|
|
111
113
|
|
|
114
|
+
Identity is resolved once per call (not per commit). When resolved, each commit includes `--author="Name <email>"`. The result includes an `author` key (`{name, email}` or `null`).
|
|
115
|
+
|
|
112
116
|
### `git_tag`
|
|
113
117
|
|
|
114
118
|
Compute the next semver version from Conventional Commits, create and push the tag.
|
|
@@ -91,7 +91,7 @@ graph TD
|
|
|
91
91
|
Each tool exposes an `execute(*, path, ..., **kwargs) → ToolResult` method with explicit typed parameters:
|
|
92
92
|
|
|
93
93
|
- **`GitTagTool`** — Full tag workflow: check clean tree, check CI, compute semver bump, create tag, verify hatch-vcs, push.
|
|
94
|
-
- **`GitCommitTool`** — Stage files, commit with pre-commit hooks, auto-retry on linter fixes. Supports batched commits.
|
|
94
|
+
- **`GitCommitTool`** — Stage files, commit with pre-commit hooks, auto-retry on linter fixes. Supports batched commits. Each commit spec is processed by `_process_single_commit()` (validate → stage → commit → record).
|
|
95
95
|
- **`GitPreflightTool`** — Parse `git status --porcelain` and `git diff --stat` into structured data.
|
|
96
96
|
- **`GitBranchTool`** — Create or checkout a branch. Supports `from_ref` (branch from tag/commit) and `checkout_only` (switch without creating).
|
|
97
97
|
- **`GitPushTool`** — Push with dirty-check guard, auto-upstream detection, custom remote, and force-push support.
|
|
@@ -100,7 +100,7 @@ Each tool exposes an `execute(*, path, ..., **kwargs) → ToolResult` method wit
|
|
|
100
100
|
|
|
101
101
|
Shared logic used by multiple tools:
|
|
102
102
|
|
|
103
|
-
- **`runner.py`** — `run_git()` and `run_gh()` subprocess wrappers, `gh_available()` auth check, `detect_package_name()` from `pyproject.toml`. Also provides `suggest_git_repos()` (scans for child directories that are git repos) and `not_a_repo_error()` (enriches "not a git repository" errors with suggestions for nearby repos).
|
|
103
|
+
- **`runner.py`** — `find_git_root()` locates the repository root via `rev-parse --show-toplevel`, `run_git()` and `run_gh()` subprocess wrappers, `gh_available()` auth check, `detect_package_name()` from `pyproject.toml`. Also provides `suggest_git_repos()` (scans for child directories that are git repos) and `not_a_repo_error()` (enriches "not a git repository" errors with suggestions for nearby repos).
|
|
104
104
|
- **`semver.py`** — `parse_tag()` for version parsing, `compute_bump()` for Conventional Commits analysis (returns `VersionBump` with next version + reason).
|
|
105
105
|
- **`phase_commit.py`** — `get_phase_commit()` looks up the commit hash for a given protocol phase name by searching git log.
|
|
106
106
|
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://github.com/axm-protocols/axm-forge/actions/workflows/ci.yml"><img src="https://github.com/axm-protocols/axm-forge/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
10
|
-
<a href="https://
|
|
11
|
-
<a href="https://
|
|
10
|
+
<a href="https://forge.axm-protocols.io/audit/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/axm-protocols/axm-forge/gh-pages/badges/axm-git/axm-audit.json" alt="axm-audit"></a>
|
|
11
|
+
<a href="https://forge.axm-protocols.io/init/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/axm-protocols/axm-forge/gh-pages/badges/axm-git/axm-init.json" alt="axm-init"></a>
|
|
12
12
|
<a href="https://github.com/axm-protocols/axm-forge/actions/workflows/axm-quality.yml"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/axm-protocols/axm-forge/gh-pages/badges/axm-git/coverage.json" alt="Coverage"></a>
|
|
13
13
|
<a href="https://pypi.org/project/axm-git/"><img src="https://img.shields.io/pypi/v/axm-git" alt="PyPI"></a>
|
|
14
14
|
<img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+" />
|
|
@@ -27,4 +27,4 @@ Hook actions auto-discovered via the `axm.hooks` entry-point group by `HookRegis
|
|
|
27
27
|
|
|
28
28
|
## Python API
|
|
29
29
|
|
|
30
|
-
Auto-generated API reference is available under [Python API](
|
|
30
|
+
Auto-generated API reference is available under [Python API](../../reference/axm_git/index.md).
|
|
@@ -114,4 +114,4 @@ The tool verifies the tree is clean before pushing, and automatically sets the u
|
|
|
114
114
|
## Next Steps
|
|
115
115
|
|
|
116
116
|
- [Architecture](../explanation/architecture.md) — How the project is structured
|
|
117
|
-
- [API Reference](
|
|
117
|
+
- [API Reference](../../reference/axm_git/index.md) — Full API documentation
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
|
|
17
17
|
dependencies = [
|
|
18
18
|
"axm>=0.1.0",
|
|
19
|
+
"pydantic>=2.0",
|
|
19
20
|
]
|
|
20
21
|
|
|
21
22
|
[project.entry-points."axm.tools"]
|
|
@@ -73,7 +74,7 @@ dev = [
|
|
|
73
74
|
"pytest>=8.0",
|
|
74
75
|
"pytest-cov>=4.0",
|
|
75
76
|
"pytest-mock>=3.0",
|
|
76
|
-
"ruff
|
|
77
|
+
"ruff==0.15.9",
|
|
77
78
|
"mypy>=1.14",
|
|
78
79
|
"pre-commit>=4.0",
|
|
79
80
|
"pip-audit>=2.0",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Core logic — subprocess runners and semver computation."""
|
|
2
|
+
|
|
3
|
+
from axm_git.core.identity import (
|
|
4
|
+
GitIdentity,
|
|
5
|
+
GitProfileConfig,
|
|
6
|
+
author_args,
|
|
7
|
+
load_config,
|
|
8
|
+
resolve_identity,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"GitIdentity",
|
|
13
|
+
"GitProfileConfig",
|
|
14
|
+
"author_args",
|
|
15
|
+
"load_config",
|
|
16
|
+
"resolve_identity",
|
|
17
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Git identity resolution with schedule-based profile switching."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from datetime import datetime, time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"GitIdentity",
|
|
14
|
+
"GitProfileConfig",
|
|
15
|
+
"author_args",
|
|
16
|
+
"load_config",
|
|
17
|
+
"resolve_identity",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
_AXM_WORKSPACE_PREFIX = "/Users/gabriel/Documents/Code/python/axm-workspaces/axm-"
|
|
21
|
+
_DEFAULT_CONFIG_PATH = Path.home() / "axm" / "git-profiles.toml"
|
|
22
|
+
|
|
23
|
+
_DAY_MAP: dict[str, int] = {
|
|
24
|
+
"mon": 0,
|
|
25
|
+
"tue": 1,
|
|
26
|
+
"wed": 2,
|
|
27
|
+
"thu": 3,
|
|
28
|
+
"fri": 4,
|
|
29
|
+
"sat": 5,
|
|
30
|
+
"sun": 6,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GitIdentity(BaseModel):
|
|
35
|
+
"""A git author identity."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
email: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ScheduleRule(BaseModel):
|
|
42
|
+
"""A time-based rule mapping to a profile."""
|
|
43
|
+
|
|
44
|
+
profile: str
|
|
45
|
+
days: list[str]
|
|
46
|
+
start: str
|
|
47
|
+
end: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Schedule(BaseModel):
|
|
51
|
+
"""Schedule configuration with rules."""
|
|
52
|
+
|
|
53
|
+
rules: list[ScheduleRule] = []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GitProfileConfig(BaseModel):
|
|
57
|
+
"""Full git-profiles.toml configuration."""
|
|
58
|
+
|
|
59
|
+
default: GitIdentity
|
|
60
|
+
profiles: dict[str, GitIdentity] = {}
|
|
61
|
+
schedule: Schedule = Schedule()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load_config(config_path: Path | None = None) -> GitProfileConfig | None:
|
|
65
|
+
"""Load and validate a git-profiles TOML config file.
|
|
66
|
+
|
|
67
|
+
Returns ``None`` if the file is missing, empty, or invalid.
|
|
68
|
+
"""
|
|
69
|
+
path = config_path or _DEFAULT_CONFIG_PATH
|
|
70
|
+
try:
|
|
71
|
+
data = path.read_bytes()
|
|
72
|
+
if not data:
|
|
73
|
+
return None
|
|
74
|
+
parsed: dict[str, Any] = tomllib.loads(data.decode())
|
|
75
|
+
return GitProfileConfig.model_validate(parsed)
|
|
76
|
+
except (OSError, tomllib.TOMLDecodeError, ValueError, KeyError):
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _matches_schedule(rule: ScheduleRule, now: datetime) -> bool:
|
|
81
|
+
"""Check if *now* falls within the rule's day + time window.
|
|
82
|
+
|
|
83
|
+
Start is inclusive, end is exclusive.
|
|
84
|
+
"""
|
|
85
|
+
weekday = now.weekday()
|
|
86
|
+
if weekday not in [_DAY_MAP[d] for d in rule.days]:
|
|
87
|
+
return False
|
|
88
|
+
current_time = now.time()
|
|
89
|
+
start = time.fromisoformat(rule.start)
|
|
90
|
+
end = time.fromisoformat(rule.end)
|
|
91
|
+
return start <= current_time < end
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_axm_workspace(path: Path) -> bool:
|
|
95
|
+
"""Return ``True`` if *path* resolves under an axm workspace."""
|
|
96
|
+
resolved = str(path.resolve())
|
|
97
|
+
return resolved.startswith(_AXM_WORKSPACE_PREFIX)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_by_override(
|
|
101
|
+
config: GitProfileConfig,
|
|
102
|
+
profile_override: str | None,
|
|
103
|
+
) -> GitIdentity | None:
|
|
104
|
+
"""Resolve identity from an explicit profile override.
|
|
105
|
+
|
|
106
|
+
Returns the matching identity, or ``None`` when *profile_override*
|
|
107
|
+
is ``None`` or names an unknown profile.
|
|
108
|
+
"""
|
|
109
|
+
if profile_override is None:
|
|
110
|
+
return None
|
|
111
|
+
if profile_override == "default":
|
|
112
|
+
return config.default
|
|
113
|
+
return config.profiles.get(profile_override)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _resolve_by_schedule(
|
|
117
|
+
config: GitProfileConfig,
|
|
118
|
+
workspace_path: Path,
|
|
119
|
+
now: datetime,
|
|
120
|
+
) -> GitIdentity | None:
|
|
121
|
+
"""Resolve identity from schedule rules for AXM workspaces.
|
|
122
|
+
|
|
123
|
+
Returns ``None`` when the path is outside AXM workspaces or no
|
|
124
|
+
schedule rule matches.
|
|
125
|
+
"""
|
|
126
|
+
if not _is_axm_workspace(workspace_path):
|
|
127
|
+
return None
|
|
128
|
+
for rule in config.schedule.rules:
|
|
129
|
+
if _matches_schedule(rule, now) and rule.profile in config.profiles:
|
|
130
|
+
return config.profiles[rule.profile]
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def resolve_identity(
|
|
135
|
+
workspace_path: Path,
|
|
136
|
+
*,
|
|
137
|
+
now: datetime | None = None,
|
|
138
|
+
profile_override: str | None = None,
|
|
139
|
+
config_path: Path | None = None,
|
|
140
|
+
) -> GitIdentity | None:
|
|
141
|
+
"""Resolve the git identity for the given workspace.
|
|
142
|
+
|
|
143
|
+
Returns ``None`` when no config is available or an unknown profile
|
|
144
|
+
is requested via *profile_override*.
|
|
145
|
+
"""
|
|
146
|
+
config = load_config(config_path)
|
|
147
|
+
if config is None:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
override = _resolve_by_override(config, profile_override)
|
|
151
|
+
if profile_override is not None:
|
|
152
|
+
return override
|
|
153
|
+
|
|
154
|
+
effective_now = now or datetime.now()
|
|
155
|
+
return _resolve_by_schedule(config, workspace_path, effective_now) or config.default
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def author_args(identity: GitIdentity | None) -> list[str]:
|
|
159
|
+
"""Build ``--author`` arguments for a git command."""
|
|
160
|
+
if identity is None:
|
|
161
|
+
return []
|
|
162
|
+
return ["--author", f"{identity.name} <{identity.email}>"]
|
|
@@ -16,12 +16,31 @@ from axm.hooks.base import HookResult
|
|
|
16
16
|
from axm_git.core.runner import gh_available, run_gh
|
|
17
17
|
from axm_git.hooks._resolve import _resolve_working_dir
|
|
18
18
|
|
|
19
|
-
__all__ = ["AwaitMergeHook"]
|
|
19
|
+
__all__ = ["AwaitMergeHook", "_resolve_pr_ref"]
|
|
20
20
|
|
|
21
21
|
_DEFAULT_TIMEOUT = 600 # 10 minutes
|
|
22
22
|
_DEFAULT_INTERVAL = 30 # seconds
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _resolve_pr_ref(
|
|
26
|
+
params: dict[str, Any],
|
|
27
|
+
context: dict[str, Any],
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Resolve a PR reference from params or context.
|
|
30
|
+
|
|
31
|
+
Checks ``pr_number`` then ``pr_url`` in *params* first,
|
|
32
|
+
falling back to *context*. Returns ``None`` when no
|
|
33
|
+
reference is found.
|
|
34
|
+
"""
|
|
35
|
+
ref = (
|
|
36
|
+
params.get("pr_number")
|
|
37
|
+
or params.get("pr_url")
|
|
38
|
+
or context.get("pr_number")
|
|
39
|
+
or context.get("pr_url")
|
|
40
|
+
)
|
|
41
|
+
return ref if ref else None
|
|
42
|
+
|
|
43
|
+
|
|
25
44
|
@dataclass
|
|
26
45
|
class AwaitMergeHook:
|
|
27
46
|
"""Poll a PR until merged or timeout.
|
|
@@ -51,12 +70,7 @@ class AwaitMergeHook:
|
|
|
51
70
|
|
|
52
71
|
working_dir = _resolve_working_dir(params, context)
|
|
53
72
|
|
|
54
|
-
pr_ref = (
|
|
55
|
-
params.get("pr_number")
|
|
56
|
-
or params.get("pr_url")
|
|
57
|
-
or context.get("pr_number")
|
|
58
|
-
or context.get("pr_url")
|
|
59
|
-
)
|
|
73
|
+
pr_ref = _resolve_pr_ref(params, context)
|
|
60
74
|
if not pr_ref:
|
|
61
75
|
return HookResult.fail("no pr_number or pr_url in params or context")
|
|
62
76
|
|