86 lines
3.9 KiB
Python
86 lines
3.9 KiB
Python
"""Git 练习场:在临时目录里自动演示并验证常用操作。
|
||
|
||
覆盖:功能分支 + 合并、rebase 整理、revert 撤销已推送的提交、reset 后用 reflog 找回、cherry-pick 热修复。
|
||
运行:python lab.py(只需要本机安装 git,不会改动你的任何仓库)
|
||
"""
|
||
import subprocess
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
|
||
def git(repo: Path, *args: str) -> str:
|
||
out = subprocess.run(["git", *args], cwd=repo, capture_output=True, text=True,
|
||
env={"GIT_AUTHOR_NAME": "dev", "GIT_AUTHOR_EMAIL": "dev@example.com",
|
||
"GIT_COMMITTER_NAME": "dev", "GIT_COMMITTER_EMAIL": "dev@example.com",
|
||
"HOME": str(repo), "PATH": "/usr/bin:/bin:/usr/local/bin"})
|
||
if out.returncode != 0:
|
||
raise RuntimeError(f"git {' '.join(args)} 失败:{out.stderr}")
|
||
return out.stdout.strip()
|
||
|
||
|
||
def commit(repo: Path, name: str, content: str, msg: str) -> str:
|
||
(repo / name).write_text(content)
|
||
git(repo, "add", name)
|
||
git(repo, "commit", "-q", "-m", msg)
|
||
return git(repo, "rev-parse", "--short", "HEAD")
|
||
|
||
|
||
def log(repo: Path, ref: str = "HEAD") -> list[str]:
|
||
return git(repo, "log", "--format=%s", ref).splitlines()
|
||
|
||
|
||
def main() -> None:
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
repo = Path(tmp)
|
||
git(repo, "init", "-q", "-b", "main")
|
||
commit(repo, "app.txt", "v1\n", "feat: 初始化项目")
|
||
|
||
# 1. 功能分支开发,期间 main 有新提交
|
||
git(repo, "switch", "-q", "-c", "feat/login")
|
||
commit(repo, "login.txt", "login\n", "feat(login): 登录表单")
|
||
commit(repo, "login.txt", "login\nvalidate\n", "fix: 拼写")
|
||
git(repo, "switch", "-q", "main")
|
||
commit(repo, "readme.txt", "readme\n", "docs: 说明文档")
|
||
|
||
# 2. 在功能分支上 rebase 到最新 main:历史变成一条直线,不产生合并提交
|
||
git(repo, "switch", "-q", "feat/login")
|
||
git(repo, "rebase", "-q", "main")
|
||
assert log(repo)[:3] == ["fix: 拼写", "feat(login): 登录表单", "docs: 说明文档"]
|
||
print("rebase 后:", log(repo))
|
||
|
||
# 3. 合并回 main(--no-ff 保留一个合并提交,记录“这里合入了一个功能”)
|
||
git(repo, "switch", "-q", "main")
|
||
git(repo, "merge", "-q", "--no-ff", "feat/login", "-m", "merge: 登录功能")
|
||
assert log(repo)[0] == "merge: 登录功能"
|
||
|
||
# 4. 已推送的错误提交:用 revert 生成一个反向提交,不改写历史
|
||
bad = commit(repo, "app.txt", "v2-bug\n", "feat: 有问题的改动")
|
||
git(repo, "revert", "--no-edit", bad)
|
||
assert (repo / "app.txt").read_text() == "v1\n"
|
||
assert log(repo)[0].startswith('Revert "feat: 有问题的改动"')
|
||
print("revert 后 app.txt =", (repo / "app.txt").read_text().strip())
|
||
|
||
# 5. 误用 reset --hard 丢掉了提交,用 reflog 找回
|
||
lost = commit(repo, "important.txt", "重要数据\n", "feat: 重要改动")
|
||
git(repo, "reset", "-q", "--hard", "HEAD~1")
|
||
assert not (repo / "important.txt").exists()
|
||
reflog = git(repo, "reflog", "--format=%h %gs")
|
||
found = next(line.split()[0] for line in reflog.splitlines() if "feat: 重要改动" in line)
|
||
assert found == lost
|
||
git(repo, "reset", "-q", "--hard", found)
|
||
assert (repo / "important.txt").read_text() == "重要数据\n"
|
||
print("reflog 找回提交", found)
|
||
|
||
# 6. 热修复:在 release 分支修复后 cherry-pick 回 main
|
||
git(repo, "switch", "-q", "-c", "release/1.0", "HEAD~1")
|
||
fix = commit(repo, "hotfix.txt", "fix\n", "fix: 线上紧急修复")
|
||
git(repo, "switch", "-q", "main")
|
||
git(repo, "cherry-pick", fix)
|
||
assert log(repo)[0] == "fix: 线上紧急修复" and (repo / "hotfix.txt").exists()
|
||
print("cherry-pick 后 main:", log(repo)[:2])
|
||
print("Git 练习全部通过")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|