feat(git): 新增隔离分支集成 Skill
This commit is contained in:
@@ -91,7 +91,7 @@
|
||||
1. `branch`(已完成)
|
||||
2. `identity`(已完成,只管理 Git 提交用户名和邮箱)
|
||||
3. `export`(已完成,环境配置默认排除,规范明确要求并再次确认后才可导出)
|
||||
4. `integrate`(待独立重建,不沿用特定组织的分支和审批模型)
|
||||
4. `integrate`(已完成,按项目规范通过隔离 worktree 评估、准备和发布预集成分支)
|
||||
|
||||
`commit-msg` 将作为只读 Git 特殊样本先行完成。涉及提交、合并和远端操作的 Skill 必须保留明确授权边界,并保护脏工作区。
|
||||
|
||||
@@ -234,3 +234,4 @@ plan-change
|
||||
- [ ] 按插件和风险类型继续推进同质批量迁移。
|
||||
- [x] 完成首个同质批量:`md-to-docx`、`xlsx-to-md`、`archive`。
|
||||
- [x] 完成 Git 本地操作批次:`identity`、`export`。
|
||||
- [x] 完成 Git 高风险隔离集成样本:`integrate`。
|
||||
|
||||
@@ -262,7 +262,10 @@
|
||||
"source-b:4ca0940d73b4b626": {
|
||||
"sourcePathHash": "4ca0940d73b4b6264a37e1a344d669367dc313364a0077abf8bcaf97cfed42a1",
|
||||
"sourceSha256": "92b3cea563f544806493245bdf099e2357112ccbce15007593c94d62c26c258c",
|
||||
"status": "pending"
|
||||
"status": "migrated",
|
||||
"target": "plugins/git/skills/integrate",
|
||||
"targetVersion": "0.1.0",
|
||||
"reviewedAt": "2026-08-25"
|
||||
},
|
||||
"source-b:0b917cc63232dff3": {
|
||||
"sourcePathHash": "0b917cc63232dff38c375d3bec3fabe57390201c34c3ce96f7885eb23bca91d7",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""验证预集成 Skill 的文档契约和隔离 Git 行为。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SKILL = ROOT / "plugins/git/skills/integrate/SKILL.md"
|
||||
|
||||
|
||||
class GitIntegrateTests(unittest.TestCase):
|
||||
"""在临时仓库中验证 worktree 集成不会移动输入引用。"""
|
||||
|
||||
def git(self, repo: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
"""执行测试仓库中的 Git 命令并保留失败状态供冲突断言。"""
|
||||
|
||||
return subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True, encoding="utf-8", check=check)
|
||||
|
||||
def make_repo(self, root: Path, conflict: bool = False) -> tuple[Path, str, str]:
|
||||
"""创建具有源、目标分叉的隔离仓库。"""
|
||||
|
||||
repo = root / "repo"; repo.mkdir(); self.git(repo, "init", "-q")
|
||||
self.git(repo, "config", "user.name", "Test User"); self.git(repo, "config", "user.email", "test@example.invalid")
|
||||
(repo / "base.txt").write_text("base\n", encoding="utf-8")
|
||||
self.git(repo, "add", "base.txt"); self.git(repo, "commit", "-q", "-m", "base")
|
||||
self.git(repo, "branch", "target"); self.git(repo, "switch", "-q", "-c", "source")
|
||||
source_file = repo / ("base.txt" if conflict else "source.txt")
|
||||
source_file.write_text("source\n", encoding="utf-8")
|
||||
self.git(repo, "add", "-A"); self.git(repo, "commit", "-q", "-m", "source")
|
||||
source_hash = self.git(repo, "rev-parse", "source").stdout.strip()
|
||||
self.git(repo, "switch", "-q", "target")
|
||||
target_file = repo / ("base.txt" if conflict else "target.txt")
|
||||
target_file.write_text("target\n", encoding="utf-8")
|
||||
self.git(repo, "add", "-A"); self.git(repo, "commit", "-q", "-m", "target")
|
||||
target_hash = self.git(repo, "rev-parse", "target").stdout.strip()
|
||||
return repo, source_hash, target_hash
|
||||
|
||||
def test_isolated_merge_preserves_source_and_target_refs(self) -> None:
|
||||
"""预集成分支发生 merge 后,源和目标引用必须保持原哈希。"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp); repo, source_hash, target_hash = self.make_repo(root)
|
||||
worktree = root / "integration"
|
||||
self.git(repo, "worktree", "add", "-q", "-b", "pre/integration", str(worktree), "target")
|
||||
self.git(worktree, "merge", "--no-edit", "source")
|
||||
self.assertEqual(source_hash, self.git(repo, "rev-parse", "source").stdout.strip())
|
||||
self.assertEqual(target_hash, self.git(repo, "rev-parse", "target").stdout.strip())
|
||||
self.assertTrue((worktree / "source.txt").is_file())
|
||||
self.assertTrue((worktree / "target.txt").is_file())
|
||||
|
||||
def test_conflict_stops_before_automatic_resolution(self) -> None:
|
||||
"""真实冲突应停留在未合并状态,不移动源或目标引用。"""
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp); repo, source_hash, target_hash = self.make_repo(root, conflict=True)
|
||||
worktree = root / "integration"
|
||||
self.git(repo, "worktree", "add", "-q", "-b", "pre/conflict", str(worktree), "target")
|
||||
result = self.git(worktree, "merge", "--no-edit", "source", check=False)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("UU base.txt", self.git(worktree, "status", "--short").stdout)
|
||||
self.assertEqual(source_hash, self.git(repo, "rev-parse", "source").stdout.strip())
|
||||
self.assertEqual(target_hash, self.git(repo, "rev-parse", "target").stdout.strip())
|
||||
|
||||
def test_skill_separates_stages_and_protects_delivery(self) -> None:
|
||||
"""Skill 必须保留阶段授权、环境配置阻断和强推禁用边界。"""
|
||||
|
||||
text = SKILL.read_text(encoding="utf-8")
|
||||
for value in ("assess", "prepare", "publish", "独立 worktree", "环境配置", "私钥"):
|
||||
self.assertIn(value, text)
|
||||
publish = (SKILL.parent / "references/publish.md").read_text(encoding="utf-8")
|
||||
self.assertIn("默认不改用 `--force`", publish)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user