fix: 代码同步前隐藏指向非 commit 对象的引用,规避 libgit2 导入报错 - #2718
Conversation
📝 Walkthrough修复
WalkthroughGit 服务在临时仓库导入前隐藏指向非 commit 对象的直接引用。导入完成或失败后,流程通过 ChangesGit 引用导入流程
Estimated code review effort: 3 (中等) | ~20 分钟 Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/one_dragon/envs/git_service.py`:
- Around line 893-896: 更新调用方与 _hide_non_commit_refs 的隐藏引用流程,使每个已删除引用立即追加到调用方持有的
hidden_refs 列表,而不是仅在循环成功结束后返回结果;确保后续读取或删除失败时,finally 仍能使用已记录的引用恢复全部已删除引用。
- Around line 1174-1175: 更新隐藏引用恢复逻辑,跳过包含在 affected_refs 中的引用,避免 finally
阶段以旧目标覆盖导入写入的新目标。保留其他隐藏引用的恢复行为,并在相关导入测试中增加同名非 commit 目标引用场景,验证最终引用仍指向新对象。
- Around line 943-948: Remove the restore_repo.free() call in the hidden_refs
cleanup within the repository import flow, since _open_repo() may return the
cached self._repo object. Preserve the existing _restore_hidden_refs behavior
and add a regression test that performs a repository operation after importing
while hidden references exist.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: b6e399c0-4299-4a79-9853-047d344a18c6
📒 Files selected for processing (1)
src/one_dragon/envs/git_service.py
| hidden_refs: list[tuple[str, Oid]] = [] | ||
| try: | ||
| log.info(f'开始导入临时 Git 仓库: {remote_path}') | ||
| hidden_refs = self._hide_non_commit_refs(active_repo) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
确保隐藏过程部分失败时也能恢复已删除引用。
_hide_non_commit_refs() 在循环结束后才返回 hidden。
如果一个引用已在 Line 1162 删除,而后续引用读取或删除失败,Line 896 不会完成赋值。
此时 finally 中的 hidden_refs 仍为空,已删除的引用不会恢复。
让调用方持有并传入记录列表,或在 _hide_non_commit_refs() 内部捕获失败后立即恢复已删除引用。
Also applies to: 1150-1162
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/one_dragon/envs/git_service.py` around lines 893 - 896, 更新调用方与
_hide_non_commit_refs 的隐藏引用流程,使每个已删除引用立即追加到调用方持有的 hidden_refs
列表,而不是仅在循环成功结束后返回结果;确保后续读取或删除失败时,finally 仍能使用已记录的引用恢复全部已删除引用。
| finally: | ||
| if hidden_refs: | ||
| try: | ||
| restore_repo = self._open_repo() | ||
| self._restore_hidden_refs(restore_repo, hidden_refs) | ||
| restore_repo.free() |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- target methods and surrounding flow ---'
sed -n '840,970p' src/one_dragon/envs/git_service.py
printf '%s\n' '--- repository cache and free/open usages ---'
rg -n -C 3 'self\._repo|_open_repo|\.free\(\)|_hide_non_commit_refs|_restore_hidden_refs|_import_fetch_result' src/one_dragon/envs/git_service.py
printf '%s\n' '--- related tests ---'
rg -n 'hidden_refs|non_commit|import_fetch|fetch_result|git_service|GitService' . -g '*.py' -g '*.yaml' -g '*.yml' | head -250Repository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 21975
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- hidden-reference helpers ---'
sed -n '1128,1182p' src/one_dragon/envs/git_service.py
printf '%s\n' '--- class lifecycle and callers after import ---'
sed -n '564,625p' src/one_dragon/envs/git_service.py
sed -n '1070,1150p' src/one_dragon/envs/git_service.py
printf '%s\n' '--- dependency declarations ---'
rg -n -C 2 'pygit2|libgit2' pyproject.toml requirements*.txt poetry.lock uv.lock setup.cfg setup.py 2>/dev/null || true
printf '%s\n' '--- all GitService construction and fetch call sites ---'
rg -n -C 4 'GitService\(|fetch_latest_code\(' src tools -g '*.py'Repository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 16920
🌐 Web query:
pygit2 1.19.3 Repository.free documentation cached repository object behavior
💡 Result:
In pygit2 1.19.3, the Repository.free method is used to release the handles to the underlying Git database [1]. It explicitly releases these handles without deallocating the repository object itself [1]. Regarding cached objects, pygit2 maintains an internal cache for objects loaded from the repository [2]. While Repository.free cleans up the connection to the database, it is distinct from the library's global caching settings [2]. The global caching behavior—which can be configured via pygit2.settings (such as cache_max_size and cache_object_limit)—is repository-specific in its implementation, but these settings manage how memory is utilized across all repositories [2][3]. Disabling or clearing caches is managed through these global settings rather than the Repository.free method [2].
Citations:
- 1: https://www.pygit2.org/repository.html
- 2: https://www.pygit2.org/settings.html
- 3: https://github.com/libgit2/pygit2/blob/master/pygit2/settings.py
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from __future__ import annotations
import ast
from pathlib import Path
path = Path("src/one_dragon/envs/git_service.py")
tree = ast.parse(path.read_text(encoding="utf-8"))
class_node = next(
node for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "GitService"
)
methods = {
node.name: node
for node in class_node.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
}
open_repo = methods["_open_repo"]
import_result = methods["_import_fetch_result"]
def calls(method: ast.AST, name: str) -> list[ast.Call]:
return [
node for node in ast.walk(method)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == name
]
open_returns_cache = any(
isinstance(node, ast.Return)
and isinstance(node.value, ast.Attribute)
and isinstance(node.value.value, ast.Name)
and node.value.value.id == "self"
and node.value.attr == "_repo"
for node in ast.walk(open_repo)
)
free_calls = [
node for node in ast.walk(import_result)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "free"
]
finally_nodes = [
node for node in ast.walk(import_result)
if isinstance(node, ast.Try) and node.finalbody
]
finally_free_lines = [
node.lineno
for try_node in finally_nodes
for node in ast.walk(ast.Module(body=try_node.finalbody, type_ignores=[]))
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "free"
]
print(f"_open_repo returns self._repo: {open_returns_cache}")
print(f"_import_fetch_result free() call lines: {[node.lineno for node in free_calls]}")
print(f"finally free() call lines: {finally_free_lines}")
print(f"finally clears self._repo: {any(
isinstance(node, ast.Assign)
and any(
isinstance(target, ast.Attribute)
and isinstance(target.value, ast.Name)
and target.value.id == 'self'
and target.attr == '_repo'
for target in node.targets
)
for try_node in finally_nodes
for node in ast.walk(ast.Module(body=try_node.finalbody, type_ignores=[]))
)}")
PYRepository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 346
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from __future__ import annotations
import ast
from pathlib import Path
path = Path("src/one_dragon/envs/git_service.py")
tree = ast.parse(path.read_text(encoding="utf-8"))
class_node = next(
node for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "GitService"
)
methods = {
node.name: node
for node in class_node.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
}
open_repo = methods["_open_repo"]
import_result = methods["_import_fetch_result"]
open_returns_cache = any(
isinstance(node, ast.Return)
and isinstance(node.value, ast.Attribute)
and isinstance(node.value.value, ast.Name)
and node.value.value.id == "self"
and node.value.attr == "_repo"
for node in ast.walk(open_repo)
)
free_calls = [
node.lineno
for node in ast.walk(import_result)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "free"
]
finally_nodes = [
node for node in ast.walk(import_result)
if isinstance(node, ast.Try) and node.finalbody
]
finally_free_lines = [
node.lineno
for try_node in finally_nodes
for node in ast.walk(ast.Module(body=try_node.finalbody, type_ignores=[]))
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "free"
]
finally_clears_cache = any(
isinstance(node, ast.Assign)
and any(
isinstance(target, ast.Attribute)
and isinstance(target.value, ast.Name)
and target.value.id == "self"
and target.attr == "_repo"
for target in node.targets
)
for try_node in finally_nodes
for node in ast.walk(ast.Module(body=try_node.finalbody, type_ignores=[]))
)
print(f"_open_repo returns self._repo: {open_returns_cache}")
print(f"_import_fetch_result free() call lines: {free_calls}")
print(f"finally free() call lines: {finally_free_lines}")
print(f"finally clears self._repo: {finally_clears_cache}")
PYRepository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 340
不要释放仍由 self._repo 缓存的仓库对象。
当 hidden_refs 非空时,_open_repo() 返回缓存的 Repository。restore_repo.free() 释放其底层句柄,但 self._repo 仍指向该对象。后续 Git 操作可能失败。删除该 free() 调用,或在释放前清空 self._repo 并重新打开仓库。添加隐藏引用存在时导入后继续访问仓库的回归测试。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/one_dragon/envs/git_service.py` around lines 943 - 948, Remove the
restore_repo.free() call in the hidden_refs cleanup within the repository import
flow, since _open_repo() may return the cached self._repo object. Preserve the
existing _restore_hidden_refs behavior and add a regression test that performs a
repository operation after importing while hidden references exist.
| for ref_name, target in hidden: | ||
| repo.references.create(ref_name, target, force=True) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
file="src/one_dragon/envs/git_service.py"
printf '%s\n' '--- target methods and symbols ---'
rg -n -C 8 '_import_fetch_result|_hide_non_commit_refs|_restore_hidden_refs|affected_refs|ReferenceType|_open_repo' "$file"
printf '%s\n' '--- relevant file ranges ---'
sed -n '1080,1205p' "$file"
printf '%s\n' '--- changed-file summary ---'
git diff --stat -- "$file"Repository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 24828
🏁 Script executed:
#!/bin/bash
set -eu
file="src/one_dragon/envs/git_service.py"
printf '%s\n' '--- repository cache lifecycle ---'
sed -n '560,625p' "$file"
printf '%s\n' '--- all affected_refs uses ---'
rg -n -C 5 'affected_refs|original_ref_targets|hidden_refs' "$file"
printf '%s\n' '--- pygit2 dependency declarations ---'
rg -n -C 3 'pygit2|GitPython' pyproject.toml requirements*.txt setup.cfg setup.py 2>/dev/null || true
printf '%s\n' '--- related tests ---'
rg -n -C 4 '_hide_non_commit_refs|_restore_hidden_refs|_import_fetch_result|non.?commit|checkpoint' . -g '!*dist*' -g '!*.pyc' 2>/dev/null || true
printf '%s\n' '--- deterministic state-transition probe ---'
python3 - <<'PY'
# Model the exact hide/fetch/restore ordering in the reviewed methods.
refs = {
"refs/remotes/origin/main": "old-non-commit",
"refs/tags/v1": "old-commit",
}
affected_refs = {"refs/remotes/origin/main"}
hidden = []
for name, target in list(refs.items()):
if target == "old-non-commit":
hidden.append((name, target))
del refs[name]
# The refspec destination is recreated by fetch with the newly fetched commit.
for name in affected_refs:
refs[name] = "new-commit"
# _restore_hidden_refs(..., force=True) recreates every hidden ref.
for name, target in hidden:
refs[name] = target
print(refs)
assert refs["refs/remotes/origin/main"] == "old-non-commit"
assert refs["refs/remotes/origin/main"] != "new-commit"
print("A hidden ref that is also an affected fetch destination is overwritten.")
PYRepository: OneDragon-Anything/ZenlessZoneZero-OneDragon
Length of output: 11761
跳过受本次导入更新的隐藏引用恢复
当 affected_refs 中的引用原先指向非 commit 对象时,fetch 或后续导入会写入新目标。finally 中的 force=True 会用旧目标覆盖新目标,导致导入结果失效。
让恢复逻辑跳过 affected_refs 中的引用,并添加同名非 commit 目标引用的回归测试,确保最终引用指向新对象。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/one_dragon/envs/git_service.py` around lines 1174 - 1175,
更新隐藏引用恢复逻辑,跳过包含在 affected_refs 中的引用,避免 finally
阶段以旧目标覆盖导入写入的新目标。保留其他隐藏引用的恢复行为,并在相关导入测试中增加同名非 commit 目标引用场景,验证最终引用仍指向新对象。
为什么改(why)
用户反馈代码同步报「暂时无法获取更新」,日志显示所有代码源都失败:
ValueError: object is not a committish。根因是 libgit2 本地 transport 的 bug:导入 fetch 结果时会枚举正式仓库全部引用做 revwalk hide,若某个引用指向非 commit 对象(tree/blob),会报
object is not a committish导致导入失败。libgit2 的容错只处理了GIT_ENOTFOUND和GIT_ERROR_INVALID,漏了实际返回的GIT_EINVALIDSPEC(注释写明"may not be a commit"却跳不过去),上游 main 分支同样未修复,应用层必须规避。实际触发来源:用户机器上有 OpenAI Codex CLI 产生的
refs/codex/turn-diffs/checkpoints/...引用直接指向 tree 对象,撞上该 bug 后代码同步必然失败。改动要点(what)
_hide_non_commit_refs:导入前扫描正式仓库全部引用,删除指向非 commit 对象的引用并记录原名与目标_restore_hidden_refs:导入完成后按记录重建被删除的引用(对象本身留在对象库,不丢数据)_import_fetch_result在 try/finally 中调用上述两方法,成功与失败路径都会恢复注意:不能只改名,libgit2 枚举的是全部引用,改名后仍会被枚举,必须删除、导入后重建。
复现方法
预期结果(修复前):
修复后:代码同步正常完成,
refs/tags/weird在导入后自动恢复。用户机器排查命令:
git for-each-ref --format='%(objecttype) %(refname)',objecttype 不是commit的行就是问题引用。