Skip to content

fix: 代码同步前隐藏指向非 commit 对象的引用,规避 libgit2 导入报错 - #2718

Closed
kawayiYokami wants to merge 1 commit into
OneDragon-Anything:mainfrom
kawayiYokami:fix/git-import-non-commit-ref
Closed

fix: 代码同步前隐藏指向非 commit 对象的引用,规避 libgit2 导入报错#2718
kawayiYokami wants to merge 1 commit into
OneDragon-Anything:mainfrom
kawayiYokami:fix/git-import-non-commit-ref

Conversation

@kawayiYokami

Copy link
Copy Markdown
Contributor

为什么改(why)

用户反馈代码同步报「暂时无法获取更新」,日志显示所有代码源都失败:
ValueError: object is not a committish

根因是 libgit2 本地 transport 的 bug:导入 fetch 结果时会枚举正式仓库全部引用做 revwalk hide,若某个引用指向非 commit 对象(tree/blob),会报 object is not a committish 导致导入失败。libgit2 的容错只处理了 GIT_ENOTFOUNDGIT_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 枚举的是全部引用,改名后仍会被枚举,必须删除、导入后重建。

复现方法

# 1. 造一个指向 tree 对象的引用(模拟 Codex checkpoint ref)
git tag weird <任意tree对象的sha>

# 2. 触发一次代码同步(应用内「代码同步」或任意走 fetch 的路径)
#    本地分支落后远端时(有新对象要下载)必然复现

预期结果(修复前):

ValueError: object is not a committish
所有代码源均拉取失败

修复后:代码同步正常完成,refs/tags/weird 在导入后自动恢复。

用户机器排查命令:git for-each-ref --format='%(objecttype) %(refname)',objecttype 不是 commit 的行就是问题引用。

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

修复

  • 在代码同步前隐藏指向非 commit 对象的引用,并在导入完成后恢复引用。
  • 使用 try/finally 确保导入成功或失败后均执行引用恢复。
  • 记录恢复失败的引用错误,同时保留对应对象数据。

Walkthrough

Git 服务在临时仓库导入前隐藏指向非 commit 对象的直接引用。导入完成或失败后,流程通过 finally 恢复这些引用,并记录恢复错误。

Changes

Git 引用导入流程

Layer / File(s) Summary
隐藏并恢复非 Commit 引用
src/one_dragon/envs/git_service.py
新增 ReferenceType 导入。临时仓库导入前删除无法解析为 commit 的直接引用并保存其名称和 OID。导入结束后恢复引用;恢复失败时记录错误。

Estimated code review effort: 3 (中等) | ~20 分钟

Suggested reviewers: shadowlemoon

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了在代码同步前隐藏非 commit 引用以规避 libgit2 导入错误这一主要变更。
Description check ✅ Passed 描述详细说明了问题根因、修复方案、触发场景和预期结果,且内容与变更一致。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 84ed14c and cc96d68.

📒 Files selected for processing (1)
  • src/one_dragon/envs/git_service.py

Comment on lines +893 to +896
hidden_refs: list[tuple[str, Oid]] = []
try:
log.info(f'开始导入临时 Git 仓库: {remote_path}')
hidden_refs = self._hide_non_commit_refs(active_repo)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 仍能使用已记录的引用恢复全部已删除引用。

Comment on lines +943 to +948
finally:
if hidden_refs:
try:
restore_repo = self._open_repo()
self._restore_hidden_refs(restore_repo, hidden_refs)
restore_repo.free()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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 -250

Repository: 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:


🏁 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=[]))
)}")
PY

Repository: 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}")
PY

Repository: OneDragon-Anything/ZenlessZoneZero-OneDragon

Length of output: 340


不要释放仍由 self._repo 缓存的仓库对象。

hidden_refs 非空时,_open_repo() 返回缓存的 Repositoryrestore_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.

Comment on lines +1174 to +1175
for ref_name, target in hidden:
repo.references.create(ref_name, target, force=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.")
PY

Repository: 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 目标引用场景,验证最终引用仍指向新对象。

@kawayiYokami
kawayiYokami deleted the fix/git-import-non-commit-ref branch August 11, 2026 15:25
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Aug 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant