Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion src/one_dragon/envs/git_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@
init_repository,
settings,
)
from pygit2.enums import CheckoutStrategy, ConfigLevel, ResetMode, SortMode
from pygit2.enums import (
CheckoutStrategy,
ConfigLevel,
ReferenceType,
ResetMode,
SortMode,
)

from one_dragon.base.config.config_item import ConfigItem
from one_dragon.envs.env_config import EnvConfig
Expand Down Expand Up @@ -884,8 +890,10 @@ def report_progress(progress: float, message: str) -> None:

callbacks = _FetchProgressRemoteCallbacks(report_progress, timeout=None)

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

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

active_repo.remotes.create(remote_name, remote_path)
active_repo.config[f'remote.{remote_name}.tagopt'] = '--no-tags'
remote = active_repo.remotes[remote_name]
Expand Down Expand Up @@ -932,6 +940,18 @@ def report_progress(progress: float, message: str) -> None:
except Exception:
log.error('恢复 Git 导入前状态失败', exc_info=True)
raise
finally:
if hidden_refs:
try:
restore_repo = self._open_repo()
self._restore_hidden_refs(restore_repo, hidden_refs)
restore_repo.free()
Comment on lines +943 to +948

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.

except Exception:
log.error(
'恢复导入前隐藏的非 commit 引用失败,对象仍保留在对象库,'
'可用 git fsck --lost-found 找回',
exc_info=True,
)

def _fetch_remote_once(
self,
Expand Down Expand Up @@ -1117,6 +1137,45 @@ def _is_missing_object_error(error: BaseException) -> bool:
return True
return re.fullmatch(r"'{0,1}[0-9a-f]{40}'{0,1}", message) is not None

@staticmethod
def _hide_non_commit_refs(repo: Repository) -> list[tuple[str, Oid]]:
"""删除指向非 commit 对象的引用并记录原名与目标,规避 libgit2 导入报错。

libgit2 本地 transport 导入 fetch 时会枚举正式仓库的全部引用做 revwalk
hide,若某个引用指向 tree/blob 等非 commit 对象(如 Codex CLI 的
checkpoint 引用),会报 "object is not a committish" 导致代码同步失败。
导入前临时删除这些引用,导入完成后按记录重建;对象本身仍留在对象库,
重建不丢数据。
"""
hidden: list[tuple[str, Oid]] = []
for ref_name in list(repo.references):
ref = repo.references[ref_name]
if ref.type != ReferenceType.DIRECT:
continue
try:
repo[ref.target].peel(Commit)
except Exception:
pass
else:
continue
hidden.append((ref.name, ref.target))
repo.references.delete(ref.name)
if hidden:
log.info(
'导入前临时隐藏 %d 个非 commit 引用: %s',
len(hidden),
', '.join(name for name, _ in hidden),
)
return hidden

@staticmethod
def _restore_hidden_refs(repo: Repository, hidden: list[tuple[str, Oid]]) -> None:
"""按记录重建导入前删除的非 commit 引用。"""
for ref_name, target in hidden:
repo.references.create(ref_name, target, force=True)
Comment on lines +1174 to +1175

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

if hidden:
log.info(f'已恢复 {len(hidden)} 个非 commit 引用')

def _rebuild_repository(
self,
progress_callback: Callable[[float, str], None] | None,
Expand Down
Loading