-
Notifications
You must be signed in to change notification settings - Fork 230
fix: 代码同步前隐藏指向非 commit 对象的引用,规避 libgit2 导入报错 #2718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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) | ||
| active_repo.remotes.create(remote_name, remote_path) | ||
| active_repo.config[f'remote.{remote_name}.tagopt'] = '--no-tags' | ||
| remote = active_repo.remotes[remote_name] | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 -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:
💡 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=[]))
)}")
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 不要释放仍由 当 🤖 Prompt for AI Agents |
||
| except Exception: | ||
| log.error( | ||
| '恢复导入前隐藏的非 commit 引用失败,对象仍保留在对象库,' | ||
| '可用 git fsck --lost-found 找回', | ||
| exc_info=True, | ||
| ) | ||
|
|
||
| def _fetch_remote_once( | ||
| self, | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.")
PYRepository: OneDragon-Anything/ZenlessZoneZero-OneDragon Length of output: 11761 跳过受本次导入更新的隐藏引用恢复 当 让恢复逻辑跳过 🤖 Prompt for AI Agents |
||
| if hidden: | ||
| log.info(f'已恢复 {len(hidden)} 个非 commit 引用') | ||
|
|
||
| def _rebuild_repository( | ||
| self, | ||
| progress_callback: Callable[[float, str], None] | None, | ||
|
|
||
There was a problem hiding this comment.
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