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.
Files changed (94) hide show
  1. {axm_git-0.2.0 → axm_git-0.3.0}/.gitignore +1 -0
  2. {axm_git-0.2.0 → axm_git-0.3.0}/PKG-INFO +9 -4
  3. {axm_git-0.2.0 → axm_git-0.3.0}/README.md +7 -3
  4. {axm_git-0.2.0 → axm_git-0.3.0}/docs/explanation/architecture.md +2 -2
  5. {axm_git-0.2.0 → axm_git-0.3.0}/docs/index.md +2 -2
  6. {axm_git-0.2.0 → axm_git-0.3.0}/docs/reference/cli.md +1 -1
  7. {axm_git-0.2.0 → axm_git-0.3.0}/docs/tutorials/getting-started.md +1 -1
  8. {axm_git-0.2.0 → axm_git-0.3.0}/pyproject.toml +2 -1
  9. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/_version.py +2 -2
  10. axm_git-0.3.0/src/axm_git/core/__init__.py +17 -0
  11. axm_git-0.3.0/src/axm_git/core/identity.py +162 -0
  12. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/core/runner.py +1 -0
  13. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/await_merge.py +21 -7
  14. axm_git-0.3.0/src/axm_git/hooks/commit_phase.py +342 -0
  15. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/preflight.py +17 -6
  16. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/commit.py +86 -51
  17. axm_git-0.3.0/tests/hooks/test_commit_phase_retry.py +137 -0
  18. axm_git-0.3.0/tests/hooks/test_commit_phase_staging.py +136 -0
  19. axm_git-0.3.0/tests/test_commit_edge.py +126 -0
  20. axm_git-0.3.0/tests/test_commit_identity.py +268 -0
  21. axm_git-0.3.0/tests/test_commit_phase_identity.py +357 -0
  22. axm_git-0.3.0/tests/test_commit_phase_result.py +175 -0
  23. axm_git-0.3.0/tests/test_identity_helpers.py +171 -0
  24. axm_git-0.3.0/tests/unit/core/test_identity.py +307 -0
  25. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_commit_phase.py +130 -0
  26. axm_git-0.3.0/tests/unit/hooks/test_commit_phase_format.py +153 -0
  27. axm_git-0.3.0/tests/unit/hooks/test_commit_phase_skip_hooks.py +197 -0
  28. axm_git-0.3.0/tests/unit/hooks/test_resolve_pr_ref.py +31 -0
  29. axm_git-0.3.0/tests/unit/hooks/test_truncate_diff.py +26 -0
  30. axm_git-0.2.0/src/axm_git/core/__init__.py +0 -1
  31. axm_git-0.2.0/src/axm_git/hooks/commit_phase.py +0 -171
  32. {axm_git-0.2.0 → axm_git-0.3.0}/.python-version +0 -0
  33. {axm_git-0.2.0 → axm_git-0.3.0}/CONTRIBUTING.md +0 -0
  34. {axm_git-0.2.0 → axm_git-0.3.0}/LICENSE +0 -0
  35. {axm_git-0.2.0 → axm_git-0.3.0}/Makefile +0 -0
  36. {axm_git-0.2.0 → axm_git-0.3.0}/docs/howto/index.md +0 -0
  37. {axm_git-0.2.0 → axm_git-0.3.0}/mkdocs.yml +0 -0
  38. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/__init__.py +0 -0
  39. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/core/branch_naming.py +0 -0
  40. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/core/phase_commit.py +0 -0
  41. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/core/semver.py +0 -0
  42. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/__init__.py +0 -0
  43. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/_resolve.py +0 -0
  44. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/branch_delete.py +0 -0
  45. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/create_branch.py +0 -0
  46. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/create_pr.py +0 -0
  47. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/merge_squash.py +0 -0
  48. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/pull.py +0 -0
  49. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/push.py +0 -0
  50. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/worktree_add.py +0 -0
  51. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/hooks/worktree_remove.py +0 -0
  52. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/py.typed +0 -0
  53. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/__init__.py +0 -0
  54. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/branch.py +0 -0
  55. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/commit_preflight.py +0 -0
  56. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/pr.py +0 -0
  57. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/push.py +0 -0
  58. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/tag.py +0 -0
  59. {axm_git-0.2.0 → axm_git-0.3.0}/src/axm_git/tools/worktree.py +0 -0
  60. {axm_git-0.2.0 → axm_git-0.3.0}/tests/__init__.py +0 -0
  61. {axm_git-0.2.0 → axm_git-0.3.0}/tests/conftest.py +0 -0
  62. {axm_git-0.2.0 → axm_git-0.3.0}/tests/functional/__init__.py +0 -0
  63. {axm_git-0.2.0 → axm_git-0.3.0}/tests/functional/test_flows.py +0 -0
  64. {axm_git-0.2.0 → axm_git-0.3.0}/tests/functional/test_hook_discovery.py +0 -0
  65. {axm_git-0.2.0 → axm_git-0.3.0}/tests/functional/test_worktree_roundtrip.py +0 -0
  66. {axm_git-0.2.0 → axm_git-0.3.0}/tests/test_version.py +0 -0
  67. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/__init__.py +0 -0
  68. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/__init__.py +0 -0
  69. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/test_branch_naming.py +0 -0
  70. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/test_phase_commit.py +0 -0
  71. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/test_runner.py +0 -0
  72. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/core/test_semver.py +0 -0
  73. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/__init__.py +0 -0
  74. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test__resolve.py +0 -0
  75. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_await_merge.py +0 -0
  76. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_branch_delete.py +0 -0
  77. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_commit_phase_retry.py +0 -0
  78. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_create_branch.py +0 -0
  79. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_create_pr.py +0 -0
  80. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_merge_squash.py +0 -0
  81. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_preflight.py +0 -0
  82. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_pull.py +0 -0
  83. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_push.py +0 -0
  84. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_worktree_add.py +0 -0
  85. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/hooks/test_worktree_remove.py +0 -0
  86. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/test_tag_helpers.py +0 -0
  87. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/__init__.py +0 -0
  88. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_branch.py +0 -0
  89. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_commit.py +0 -0
  90. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_commit_preflight.py +0 -0
  91. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_pr.py +0 -0
  92. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_push.py +0 -0
  93. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_tag.py +0 -0
  94. {axm_git-0.2.0 → axm_git-0.3.0}/tests/unit/tools/test_worktree.py +0 -0
@@ -23,6 +23,7 @@ ENV/
23
23
  coverage.xml
24
24
  coverage.json
25
25
  htmlcov/
26
+ coverage_html/
26
27
  .tox/
27
28
 
28
29
  # Type checking & Linting
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axm-git
3
- Version: 0.2.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://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/axm-audit.json" alt="axm-audit"></a>
32
- <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/axm-init.json" alt="axm-init"></a>
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://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/axm-audit.json" alt="axm-audit"></a>
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/axm-init.json" alt="axm-init"></a>
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://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/axm-audit.json" alt="axm-audit"></a>
11
- <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/axm-init.json" alt="axm-init"></a>
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](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](../reference/api/) — Full API documentation
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>=0.8",
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.2.0'
22
- __version_tuple__ = version_tuple = (0, 2, 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}>"]
@@ -15,6 +15,7 @@ logger = logging.getLogger(__name__)
15
15
  __all__ = [
16
16
  "detect_package_name",
17
17
  "find_git_root",
18
+ "find_git_root",
18
19
  "gh_available",
19
20
  "not_a_repo_error",
20
21
  "run_gh",
@@ -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