"""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()