git-aware-coding-agent 1.0.0__py3-none-any.whl
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.
- avos_cli/__init__.py +3 -0
- avos_cli/agents/avos_ask_agent.md +47 -0
- avos_cli/agents/avos_ask_agent_JSON_converter.md +78 -0
- avos_cli/agents/avos_hisotry_agent_JSON_converter.md +92 -0
- avos_cli/agents/avos_history_agent.md +58 -0
- avos_cli/agents/git_diff_agent.md +63 -0
- avos_cli/artifacts/__init__.py +17 -0
- avos_cli/artifacts/base.py +47 -0
- avos_cli/artifacts/commit_builder.py +35 -0
- avos_cli/artifacts/doc_builder.py +30 -0
- avos_cli/artifacts/issue_builder.py +37 -0
- avos_cli/artifacts/pr_builder.py +50 -0
- avos_cli/cli/__init__.py +1 -0
- avos_cli/cli/main.py +504 -0
- avos_cli/commands/__init__.py +1 -0
- avos_cli/commands/ask.py +541 -0
- avos_cli/commands/connect.py +363 -0
- avos_cli/commands/history.py +549 -0
- avos_cli/commands/hook_install.py +260 -0
- avos_cli/commands/hook_sync.py +231 -0
- avos_cli/commands/ingest.py +506 -0
- avos_cli/commands/ingest_pr.py +239 -0
- avos_cli/config/__init__.py +1 -0
- avos_cli/config/hash_store.py +93 -0
- avos_cli/config/lock.py +122 -0
- avos_cli/config/manager.py +180 -0
- avos_cli/config/state.py +90 -0
- avos_cli/exceptions.py +272 -0
- avos_cli/models/__init__.py +58 -0
- avos_cli/models/api.py +75 -0
- avos_cli/models/artifacts.py +99 -0
- avos_cli/models/config.py +56 -0
- avos_cli/models/diff.py +117 -0
- avos_cli/models/query.py +234 -0
- avos_cli/parsers/__init__.py +21 -0
- avos_cli/parsers/artifact_ref_extractor.py +173 -0
- avos_cli/parsers/reference_parser.py +117 -0
- avos_cli/services/__init__.py +1 -0
- avos_cli/services/chronology_service.py +68 -0
- avos_cli/services/citation_validator.py +134 -0
- avos_cli/services/context_budget_service.py +104 -0
- avos_cli/services/diff_resolver.py +398 -0
- avos_cli/services/diff_summary_service.py +141 -0
- avos_cli/services/git_client.py +351 -0
- avos_cli/services/github_client.py +443 -0
- avos_cli/services/llm_client.py +312 -0
- avos_cli/services/memory_client.py +323 -0
- avos_cli/services/query_fallback_formatter.py +108 -0
- avos_cli/services/reply_output_service.py +341 -0
- avos_cli/services/sanitization_service.py +218 -0
- avos_cli/utils/__init__.py +1 -0
- avos_cli/utils/dotenv_load.py +50 -0
- avos_cli/utils/hashing.py +22 -0
- avos_cli/utils/logger.py +77 -0
- avos_cli/utils/output.py +232 -0
- avos_cli/utils/sanitization_diagnostics.py +81 -0
- avos_cli/utils/time_helpers.py +56 -0
- git_aware_coding_agent-1.0.0.dist-info/METADATA +390 -0
- git_aware_coding_agent-1.0.0.dist-info/RECORD +62 -0
- git_aware_coding_agent-1.0.0.dist-info/WHEEL +4 -0
- git_aware_coding_agent-1.0.0.dist-info/entry_points.txt +2 -0
- git_aware_coding_agent-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Hook install command for setting up automatic commit sync on git push.
|
|
2
|
+
|
|
3
|
+
Installs a pre-push git hook that automatically syncs pushed commits
|
|
4
|
+
to Avos Memory. This enables real-time team synchronization without
|
|
5
|
+
requiring manual `avos ingest` runs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import stat
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from avos_cli.config.manager import load_config
|
|
14
|
+
from avos_cli.exceptions import (
|
|
15
|
+
AvosError,
|
|
16
|
+
ConfigurationNotInitializedError,
|
|
17
|
+
)
|
|
18
|
+
from avos_cli.services.git_client import GitClient
|
|
19
|
+
from avos_cli.utils.logger import get_logger
|
|
20
|
+
from avos_cli.utils.output import print_error, print_info, print_success, print_warning
|
|
21
|
+
|
|
22
|
+
_log = get_logger("commands.hook_install")
|
|
23
|
+
|
|
24
|
+
_PRE_PUSH_HOOK_SCRIPT = """\
|
|
25
|
+
#!/bin/sh
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Avos Memory Auto-Sync Hook
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# Installed by: avos hook-install
|
|
30
|
+
# Purpose: Automatically sync pushed commits to Avos Memory
|
|
31
|
+
#
|
|
32
|
+
# This hook runs before git push and extracts commits being pushed,
|
|
33
|
+
# then inserts them into Avos Memory for team synchronization.
|
|
34
|
+
#
|
|
35
|
+
# To uninstall: rm .git/hooks/pre-push
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
remote="$1"
|
|
39
|
+
url="$2"
|
|
40
|
+
|
|
41
|
+
# Read refs from stdin (format: local_ref local_sha remote_ref remote_sha)
|
|
42
|
+
while read local_ref local_sha remote_ref remote_sha; do
|
|
43
|
+
# Skip if deleting a ref (local_sha is null)
|
|
44
|
+
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
45
|
+
continue
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# For new branches, remote_sha is null - use empty string
|
|
49
|
+
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
50
|
+
remote_sha=""
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Sync commits to Avos Memory (never blocks push)
|
|
54
|
+
avos hook-sync "$remote_sha" "$local_sha" 2>&1 || true
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
# Always allow push to proceed
|
|
58
|
+
exit 0
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class HookInstallOrchestrator:
|
|
63
|
+
"""Orchestrates installation of the pre-push git hook.
|
|
64
|
+
|
|
65
|
+
The hook enables automatic commit sync to Avos Memory on every
|
|
66
|
+
git push, allowing team members to see each other's work in
|
|
67
|
+
real-time via `avos ask` and `avos history`.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
git_client: Local git operations wrapper.
|
|
71
|
+
repo_root: Path to the repository root.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, git_client: GitClient, repo_root: Path) -> None:
|
|
75
|
+
self._git = git_client
|
|
76
|
+
self._repo_root = repo_root
|
|
77
|
+
|
|
78
|
+
def run(self, force: bool = False, quiet: bool = False) -> int:
|
|
79
|
+
"""Install the pre-push hook for automatic commit sync.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
force: If True, overwrite existing hook without prompting.
|
|
83
|
+
quiet: If True, suppress all output (for auto-install from connect).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Exit code: 0 on success, 1 on failure.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
config = load_config(self._repo_root)
|
|
90
|
+
except ConfigurationNotInitializedError:
|
|
91
|
+
if not quiet:
|
|
92
|
+
print_error(
|
|
93
|
+
"[CONFIG_NOT_INITIALIZED] Repository not connected. "
|
|
94
|
+
"Run 'avos connect org/repo' first."
|
|
95
|
+
)
|
|
96
|
+
return 1
|
|
97
|
+
except AvosError as e:
|
|
98
|
+
if not quiet:
|
|
99
|
+
print_error(f"[{e.code}] {e}")
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
hooks_dir = self._get_hooks_dir()
|
|
103
|
+
if hooks_dir is None:
|
|
104
|
+
if not quiet:
|
|
105
|
+
print_error("[REPOSITORY_CONTEXT_ERROR] Could not locate .git/hooks directory.")
|
|
106
|
+
return 1
|
|
107
|
+
|
|
108
|
+
hook_path = hooks_dir / "pre-push"
|
|
109
|
+
|
|
110
|
+
if hook_path.exists() and not force:
|
|
111
|
+
if self._is_avos_hook(hook_path):
|
|
112
|
+
if not quiet:
|
|
113
|
+
print_info("Avos pre-push hook is already installed.")
|
|
114
|
+
return 0
|
|
115
|
+
else:
|
|
116
|
+
if not quiet:
|
|
117
|
+
print_warning(
|
|
118
|
+
"A pre-push hook already exists and was not installed by avos.\n"
|
|
119
|
+
"Use --force to overwrite, or manually integrate avos hook-sync."
|
|
120
|
+
)
|
|
121
|
+
_log.debug("Existing non-avos hook found, skipping auto-install")
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
self._install_hook(hook_path)
|
|
126
|
+
except OSError as e:
|
|
127
|
+
if not quiet:
|
|
128
|
+
print_error(f"[HOOK_INSTALL_ERROR] Failed to install hook: {e}")
|
|
129
|
+
return 1
|
|
130
|
+
|
|
131
|
+
if not quiet:
|
|
132
|
+
print_success(f"Installed pre-push hook: {hook_path}")
|
|
133
|
+
print_info(
|
|
134
|
+
"Commits will now auto-sync to Avos Memory on every git push.\n"
|
|
135
|
+
f"Connected to memory: {config.memory_id}"
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
_log.debug("Auto-installed pre-push hook at %s", hook_path)
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
def _get_hooks_dir(self) -> Path | None:
|
|
142
|
+
"""Get the path to the git hooks directory.
|
|
143
|
+
|
|
144
|
+
Handles both regular repos (.git/hooks) and worktrees.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Path to hooks directory, or None if not found.
|
|
148
|
+
"""
|
|
149
|
+
git_path = self._repo_root / ".git"
|
|
150
|
+
|
|
151
|
+
if git_path.is_dir():
|
|
152
|
+
hooks_dir = git_path / "hooks"
|
|
153
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
154
|
+
return hooks_dir
|
|
155
|
+
|
|
156
|
+
if git_path.is_file():
|
|
157
|
+
try:
|
|
158
|
+
content = git_path.read_text().strip()
|
|
159
|
+
if content.startswith("gitdir:"):
|
|
160
|
+
gitdir = content[7:].strip()
|
|
161
|
+
gitdir_path = Path(gitdir)
|
|
162
|
+
if not gitdir_path.is_absolute():
|
|
163
|
+
gitdir_path = (self._repo_root / gitdir_path).resolve()
|
|
164
|
+
hooks_dir = gitdir_path / "hooks"
|
|
165
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
166
|
+
return hooks_dir
|
|
167
|
+
except (OSError, ValueError):
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def _is_avos_hook(self, hook_path: Path) -> bool:
|
|
173
|
+
"""Check if an existing hook was installed by avos.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
hook_path: Path to the hook file.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
True if the hook contains avos signature.
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
content = hook_path.read_text()
|
|
183
|
+
return "avos hook-sync" in content or "Avos Memory Auto-Sync" in content
|
|
184
|
+
except OSError:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def _install_hook(self, hook_path: Path) -> None:
|
|
188
|
+
"""Write the pre-push hook script and make it executable.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
hook_path: Path where the hook should be installed.
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
OSError: If writing or chmod fails.
|
|
195
|
+
"""
|
|
196
|
+
hook_path.write_text(_PRE_PUSH_HOOK_SCRIPT)
|
|
197
|
+
hook_path.chmod(hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
198
|
+
_log.info("Installed pre-push hook at %s", hook_path)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class HookUninstallOrchestrator:
|
|
202
|
+
"""Orchestrates removal of the pre-push git hook.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
repo_root: Path to the repository root.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def __init__(self, repo_root: Path) -> None:
|
|
209
|
+
self._repo_root = repo_root
|
|
210
|
+
|
|
211
|
+
def run(self) -> int:
|
|
212
|
+
"""Remove the avos pre-push hook if it exists.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Exit code: 0 on success, 1 if hook wasn't avos-installed.
|
|
216
|
+
"""
|
|
217
|
+
git_path = self._repo_root / ".git"
|
|
218
|
+
if git_path.is_dir():
|
|
219
|
+
hook_path = git_path / "hooks" / "pre-push"
|
|
220
|
+
elif git_path.is_file():
|
|
221
|
+
try:
|
|
222
|
+
content = git_path.read_text().strip()
|
|
223
|
+
if content.startswith("gitdir:"):
|
|
224
|
+
gitdir = content[7:].strip()
|
|
225
|
+
gitdir_path = Path(gitdir)
|
|
226
|
+
if not gitdir_path.is_absolute():
|
|
227
|
+
gitdir_path = (self._repo_root / gitdir_path).resolve()
|
|
228
|
+
hook_path = gitdir_path / "hooks" / "pre-push"
|
|
229
|
+
else:
|
|
230
|
+
print_error("[REPOSITORY_CONTEXT_ERROR] Invalid .git file format.")
|
|
231
|
+
return 1
|
|
232
|
+
except OSError:
|
|
233
|
+
print_error("[REPOSITORY_CONTEXT_ERROR] Could not read .git file.")
|
|
234
|
+
return 1
|
|
235
|
+
else:
|
|
236
|
+
print_error("[REPOSITORY_CONTEXT_ERROR] No .git found.")
|
|
237
|
+
return 1
|
|
238
|
+
|
|
239
|
+
if not hook_path.exists():
|
|
240
|
+
print_info("No pre-push hook installed.")
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
content = hook_path.read_text()
|
|
245
|
+
if "avos hook-sync" not in content and "Avos Memory Auto-Sync" not in content:
|
|
246
|
+
print_warning(
|
|
247
|
+
"The existing pre-push hook was not installed by avos.\n"
|
|
248
|
+
"Remove it manually if needed."
|
|
249
|
+
)
|
|
250
|
+
return 1
|
|
251
|
+
except OSError:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
hook_path.unlink()
|
|
256
|
+
print_success("Removed avos pre-push hook.")
|
|
257
|
+
return 0
|
|
258
|
+
except OSError as e:
|
|
259
|
+
print_error(f"[HOOK_UNINSTALL_ERROR] Failed to remove hook: {e}")
|
|
260
|
+
return 1
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Hook sync command for automatic commit memory insertion on git push.
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# IMPORTANT: INGEST PIPELINE BYPASS
|
|
5
|
+
# =============================================================================
|
|
6
|
+
# This module bypasses the avos ingest pipeline and directly calls add_memory().
|
|
7
|
+
#
|
|
8
|
+
# WHY: To enable real-time commit sync on git push without GitHub API polling.
|
|
9
|
+
#
|
|
10
|
+
# CONSTRAINT: This MUST use the exact same data schema as avos ingest:
|
|
11
|
+
# - CommitArtifact model (avos_cli/models/artifacts.py)
|
|
12
|
+
# - CommitBuilder format (avos_cli/artifacts/commit_builder.py)
|
|
13
|
+
# - Hash store deduplication (avos_cli/config/hash_store.py)
|
|
14
|
+
#
|
|
15
|
+
# IF YOU CHANGE avos ingest commit handling, UPDATE THIS MODULE.
|
|
16
|
+
# See: avos_cli/commands/ingest.py::_ingest_commits()
|
|
17
|
+
# =============================================================================
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from avos_cli.artifacts.commit_builder import CommitBuilder
|
|
26
|
+
from avos_cli.config.hash_store import IngestHashStore
|
|
27
|
+
from avos_cli.config.manager import load_config
|
|
28
|
+
from avos_cli.exceptions import (
|
|
29
|
+
AvosError,
|
|
30
|
+
ConfigurationNotInitializedError,
|
|
31
|
+
)
|
|
32
|
+
from avos_cli.models.artifacts import CommitArtifact
|
|
33
|
+
from avos_cli.services.git_client import GitClient
|
|
34
|
+
from avos_cli.services.memory_client import AvosMemoryClient
|
|
35
|
+
from avos_cli.utils.logger import get_logger
|
|
36
|
+
from avos_cli.utils.output import print_info, print_success
|
|
37
|
+
|
|
38
|
+
_log = get_logger("commands.hook_sync")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class HookSyncResult:
|
|
43
|
+
"""Result of hook sync operation.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
processed: Total commits attempted.
|
|
47
|
+
stored: Commits successfully stored in Avos Memory.
|
|
48
|
+
skipped: Commits skipped due to deduplication.
|
|
49
|
+
failed: Commits that failed to store.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
processed: int = 0
|
|
53
|
+
stored: int = 0
|
|
54
|
+
skipped: int = 0
|
|
55
|
+
failed: int = 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class HookSyncOrchestrator:
|
|
59
|
+
"""Orchestrates commit sync on git push via pre-push hook.
|
|
60
|
+
|
|
61
|
+
# =========================================================================
|
|
62
|
+
# INGEST BYPASS NOTICE
|
|
63
|
+
# =========================================================================
|
|
64
|
+
# This orchestrator bypasses the full avos ingest pipeline.
|
|
65
|
+
# It directly inserts commits using the same schema as:
|
|
66
|
+
# avos_cli/commands/ingest.py::_ingest_commits()
|
|
67
|
+
#
|
|
68
|
+
# If the ingest commit schema changes, this module MUST be updated.
|
|
69
|
+
# =========================================================================
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
memory_client: Avos Memory API client.
|
|
73
|
+
git_client: Local git operations wrapper.
|
|
74
|
+
hash_store: Content hash store for deduplication.
|
|
75
|
+
repo_root: Path to the repository root.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
memory_client: AvosMemoryClient,
|
|
81
|
+
git_client: GitClient,
|
|
82
|
+
hash_store: IngestHashStore,
|
|
83
|
+
repo_root: Path,
|
|
84
|
+
) -> None:
|
|
85
|
+
self._memory = memory_client
|
|
86
|
+
self._git = git_client
|
|
87
|
+
self._hash_store = hash_store
|
|
88
|
+
self._repo_root = repo_root
|
|
89
|
+
self._commit_builder = CommitBuilder()
|
|
90
|
+
|
|
91
|
+
def run(self, old_sha: str, new_sha: str) -> int:
|
|
92
|
+
"""Execute the hook sync flow for commits between two SHAs.
|
|
93
|
+
|
|
94
|
+
This is called by the pre-push hook with the old and new SHA
|
|
95
|
+
of the ref being pushed. It extracts commits in the range and
|
|
96
|
+
inserts them into Avos Memory.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
old_sha: Base commit SHA (what remote has). Empty for new branches.
|
|
100
|
+
new_sha: Target commit SHA (what we're pushing).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Exit code: 0 on success, 1 on precondition failure.
|
|
104
|
+
Note: We always return 0 to avoid blocking git push.
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
config = load_config(self._repo_root)
|
|
108
|
+
except ConfigurationNotInitializedError:
|
|
109
|
+
_log.debug("No avos config found, skipping hook sync")
|
|
110
|
+
return 0
|
|
111
|
+
except AvosError as e:
|
|
112
|
+
_log.warning("Failed to load avos config: %s", e)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
repo_slug = config.repo
|
|
116
|
+
memory_id = config.memory_id
|
|
117
|
+
|
|
118
|
+
if not repo_slug or not memory_id:
|
|
119
|
+
_log.debug("Missing repo or memory_id in config, skipping")
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
normalized_old = self._normalize_sha(old_sha)
|
|
123
|
+
normalized_new = self._normalize_sha(new_sha)
|
|
124
|
+
|
|
125
|
+
if not normalized_new:
|
|
126
|
+
_log.debug("No new SHA provided, skipping")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
result = self._sync_commits(normalized_old, normalized_new, repo_slug, memory_id)
|
|
130
|
+
|
|
131
|
+
if result.stored > 0 or result.skipped > 0:
|
|
132
|
+
self._hash_store.save()
|
|
133
|
+
|
|
134
|
+
if result.stored > 0:
|
|
135
|
+
print_success(
|
|
136
|
+
f"[avos] Synced {result.stored} commit(s) to memory "
|
|
137
|
+
f"({result.skipped} skipped, {result.failed} failed)"
|
|
138
|
+
)
|
|
139
|
+
elif result.processed > 0 and result.skipped == result.processed:
|
|
140
|
+
print_info(f"[avos] All {result.skipped} commit(s) already in memory")
|
|
141
|
+
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
def _normalize_sha(self, sha: str) -> str:
|
|
145
|
+
"""Normalize SHA, treating null SHA as empty string.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
sha: SHA string, possibly the null SHA (all zeros).
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Empty string if null SHA, otherwise the original SHA.
|
|
152
|
+
"""
|
|
153
|
+
if not sha:
|
|
154
|
+
return ""
|
|
155
|
+
if sha == "0" * 40:
|
|
156
|
+
return ""
|
|
157
|
+
return sha.strip()
|
|
158
|
+
|
|
159
|
+
def _sync_commits(
|
|
160
|
+
self, old_sha: str, new_sha: str, repo_slug: str, memory_id: str
|
|
161
|
+
) -> HookSyncResult:
|
|
162
|
+
"""Sync commits in the given range to Avos Memory.
|
|
163
|
+
|
|
164
|
+
# INGEST BYPASS: Uses same artifact schema as ingest._ingest_commits()
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
old_sha: Base SHA (exclusive). Empty for new branches.
|
|
168
|
+
new_sha: Target SHA (inclusive).
|
|
169
|
+
repo_slug: Repository identifier 'org/repo'.
|
|
170
|
+
memory_id: Target memory ID.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
HookSyncResult with counts.
|
|
174
|
+
"""
|
|
175
|
+
result = HookSyncResult()
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
commits = self._git.commit_log_range(self._repo_root, old_sha, new_sha)
|
|
179
|
+
except AvosError as e:
|
|
180
|
+
_log.warning("Failed to get commit range: %s", e)
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
if not commits:
|
|
184
|
+
_log.debug("No commits in range %s..%s", old_sha[:8] if old_sha else "ROOT", new_sha[:8])
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
for commit_data in commits:
|
|
188
|
+
result.processed += 1
|
|
189
|
+
try:
|
|
190
|
+
self._sync_single_commit(commit_data, repo_slug, memory_id, result)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
_log.warning("Failed to sync commit %s: %s", commit_data.get("hash", "?")[:8], e)
|
|
193
|
+
result.failed += 1
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
def _sync_single_commit(
|
|
198
|
+
self,
|
|
199
|
+
commit_data: dict[str, str],
|
|
200
|
+
repo_slug: str,
|
|
201
|
+
memory_id: str,
|
|
202
|
+
result: HookSyncResult,
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Sync a single commit to Avos Memory.
|
|
205
|
+
|
|
206
|
+
# INGEST BYPASS: This follows the exact same pattern as
|
|
207
|
+
# avos_cli/commands/ingest.py::_ingest_commits() lines 282-298
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
commit_data: Dict with hash, message, author, date.
|
|
211
|
+
repo_slug: Repository identifier.
|
|
212
|
+
memory_id: Target memory ID.
|
|
213
|
+
result: Result object to update.
|
|
214
|
+
"""
|
|
215
|
+
artifact = CommitArtifact(
|
|
216
|
+
repo=repo_slug,
|
|
217
|
+
hash=commit_data["hash"],
|
|
218
|
+
message=commit_data["message"],
|
|
219
|
+
author=commit_data["author"],
|
|
220
|
+
date=commit_data["date"],
|
|
221
|
+
)
|
|
222
|
+
text = self._commit_builder.build(artifact)
|
|
223
|
+
content_hash = self._commit_builder.content_hash(artifact)
|
|
224
|
+
|
|
225
|
+
if self._hash_store.contains(content_hash):
|
|
226
|
+
result.skipped += 1
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
self._memory.add_memory(memory_id=memory_id, content=text)
|
|
230
|
+
self._hash_store.add(content_hash, "commit", commit_data["hash"])
|
|
231
|
+
result.stored += 1
|