技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTGIT

Git 玩不明白,怎么办?安排!

所属馆藏
Git
文件格式
Markdown
原始路径
git/01-Git 玩不明白,怎么办?安排!
本文目录6 个章节
  1. 前言
  2. Git 体系介绍
  3. 命令概览
  4. 命令解析
  5. 总结:先判断要改哪一层
  6. 参考资料

Git 玩不明白,怎么办?安排!

Category(分类): git Status: 未知

前言

在某个月黑风高的晚上,一脸愁容的女朋友突然跟我说,Git 老搞不明白,有什么比较好的经验可以分享下吗?说时迟,那时快,二话不说,立马开始奋笔疾书……

在平时的 Coding 过程中,我们需要具备一定的 Git 操作能力。但总会有一些场景:命令一时想不起来,或者不确定某个命令到底会修改工作区、暂存区还是提交历史,只好再次打开搜索引擎查一番。与其把时间消磨在一次次重复搜索中,不如把常用命令背后的模型和使用场景整理清楚。

本文保留原文的案例、命令和截图,并对已经过时、容易误解或存在错误的说法补充当前 Git 的实践。示例中的 master 是历史文章中常见的分支名;现在的项目可能使用 maintrunk 或其他默认分支,实际操作时应以仓库配置和团队规范为准。

当然,除了了解命令的写法,我们也应该了解 Git 的整体结构。这样才能知道每天执行的命令究竟修改了哪一层。

如有表述不当之处,感谢指正。

Git 体系介绍

开局一张图,结论先靠猜。下面的截图是原文配图,界面和分支名可能已经过时:

Git 区域与工作流示意图

Git 区域理解

  • 远程仓库区:托管在服务器上的仓库,用于共享提交、代码评审、备份和协作。它不是本地 .git 目录的实时镜像,网络操作也不会自动把所有内容同步到工作区。
  • 远程跟踪分支:例如 origin/mainorigin/feature/login。它们是本地对远程引用最近一次已知位置的记录,不是“远程仓库各分支代码的完整副本”。通常由 git fetch 更新。它们通常以 refs/remotes/<remote>/<branch> 的形式存在,但引用也可能被打包,不应依赖某个具体文件一定存在。
  • 本地分支:例如 mainfeature/login。本地分支本质上是指向某个提交的可移动引用,通常对应 refs/heads/;提交中的文件内容则存放在 Git 对象库中,而不是直接写进分支文件。
  • 暂存区(Index):执行 git add 后,下一次提交准备写入的快照。它通常由 .git/index 表示,是工作区和下一次提交之间的可编辑中间层。
  • 工作区(Working tree):编辑器打开并修改文件的目录,例如 VS Code 当前打开的项目。
  • HEAD:当前检出的提交或分支。通常 HEAD 指向当前分支,而当前分支再指向一个提交;处于 detached HEAD 状态时,HEAD 可以直接指向某个提交。

这些区域不是几个互相搬运文件的普通文件夹。Git 命令主要是在更新引用、对象、索引和工作区之间的状态:

工作区 --git add--> 暂存区 --git commit--> 本地提交图
   ^                                      |
   |                                      | git fetch 更新远程跟踪分支
   |                                      v
   +----------- git restore / merge / rebase

本地分支 --git push--> 远程仓库
远程仓库 --git fetch--> origin/main 等远程跟踪分支

stash

除此之外,还有一个特殊的本地保存机制:git stash。有时我们已经改了一半代码,却突然需要切换到另一个分支处理紧急问题,又不想创建一个质量很差的临时提交,就可以把当前改动暂时保存起来,处理完其他事情后再恢复。

原文使用的是:

git stash save "临时存一下"

save 是历史写法,现代 Git 更推荐:

git stash push -m "临时存一下"

stash 是本地临时记录,不是远程备份,也不适合替代长期提交。默认主要保存已跟踪文件的工作区和索引状态;未跟踪文件需要 -u,被 .gitignore 忽略的文件还需要 -a。使用前后要通过 git stash list 确认记录,避免过一段时间后忘记自己保存过什么。

笔者仍然不建议把 stash 当作长期储存。若改动已经具有独立意义,临时提交、临时分支或提交一个明确的 WIP 版本通常更容易追踪。stash 很有用,但要及时 apply/pop 并清理无用记录。

Git 简单工作流理解

日常工作中,Git 的常见流程大致如下,不同公司和项目会有差异:

  1. 从项目实际的默认分支(历史上常见 master,现在也常见 main)获取最新代码。
  2. 创建并切换到自己的 feature 分支进行开发。现代 Git 可以使用 git switch -c,历史文章常用 git checkout -b
  3. 开发完一个功能点后,通过 git add 将确认过的改动放入暂存区。
  4. 执行 git commit 生成本地提交。
  5. 使用 git push -u origin <branch> 首次推送并建立上游关系。
  6. 通过 Pull Request(PR)、Merge Request(MR)和代码评审流程合入集成分支。
  7. 提交 PR/MR 前,根据团队策略使用 git fetch 后的 mergerebase 同步目标分支,解决冲突并运行测试。

不建议把“先把 master 合并到开发分支”当作所有项目的固定规则。团队可能要求 merge,也可能要求 rebase;关键是遵循分支保护、CI、评审和发布规范,不要擅自在公共分支上改写历史。

命令概览

  • git stash:临时保存未完成的本地改动
  • git clone:克隆远程仓库
  • git init:初始化本地仓库
  • git remote:管理远程仓库地址
  • git branch:创建、查看、删除分支引用
  • git switch:切换和创建分支,现代推荐写法
  • git checkout:历史上同时承担分支切换和文件恢复,仍然有效
  • git add:更新暂存区
  • git commit:创建本地提交
  • git rm:删除文件或取消跟踪
  • git push:把本地引用更新请求发送到远程
  • git pull:fetch 后整合远程分支
  • git fetch:只获取远程对象和引用,不自动整合当前分支
  • git merge:合并分支历史
  • git log:查看提交历史
  • git diff:查看差异
  • git reset:移动当前分支或更新索引,使用前要确认影响范围
  • git restore:现代的文件恢复和取消暂存命令
  • git reflog:查看本地引用移动记录
  • git revert:创建新提交抵消已有提交
  • git cherry-pick:把指定提交的变更复制到当前分支
  • git tag:创建和管理标签
  • git rebase:重新应用提交,整理或改变提交基线

乍一看,眼花缭乱,当场决定放弃,还是用可视化工具吧。莫慌,下面按场景逐一介绍。图形化工具可以降低操作门槛,但不能替代对分支、索引和提交历史的理解。

命令解析

一般来说,本地想使用 Git 管理文件,首先需要有一个仓库。常见方式是从 GitHub、GitLab、Gitea 或企业代码平台克隆已有仓库;也可以在本地目录执行 git init,再通过 git remote add 关联远程仓库。

git stash:临时保存未完成的改动

常用命令如下:

# 现代写法:保存已跟踪文件的改动并添加说明
git stash push -m "临时存一下"

# 同时保存未跟踪文件
git stash push -u -m "临时存一下未跟踪文件"

# 查看所有 stash 记录
git stash list

# 应用最近一条并在成功后删除该记录
git stash pop

# 应用指定记录但保留它
git stash apply stash@{0}

# 删除指定记录
git stash drop stash@{0}

# 清空所有 stash,执行前务必确认
git stash clear

原文中的 git stash save 'xxx' 仍可能在现有 Git 版本中工作,但已经是过时写法;新文档应优先使用 git stash push -m 'xxx'pop 通常在应用成功后删除记录;如果应用发生冲突,记录不一定会被删除,应先查看状态并处理冲突。

stash 的内部引用通常是 refs/stash,但这是实现细节。使用 git stash listshowapplypopdrop 管理它,比直接操作 .git 内部文件安全得多。

git clone

最基础的用法是:

git clone <仓库地>

克隆完成后,Git 会根据远程仓库配置检出默认分支。默认分支不一定叫 master,不能把“clone 后一定停留在 master”当作固定行为。

如果想克隆后直接检出指定分支,可以使用:

git clone --branch branch1 <仓库地>
# 只需要该分支及其相关历史时,可按需使用:
git clone --branch branch1 --single-branch <仓库地>

--branch 既可以指定分支,也可以指定某个可检出的标签;短写法 -b 仍然有效。如果指定的是标签,克隆后通常会处于 detached HEAD 状态。需要继续开发时,应创建新分支。

git init

如果是一个尚未纳入版本管理的本地目录,可以执行:

cd 项目目录
git init
git status

git init 只是在本地创建 Git 仓库,并不自动创建或关联远程仓库。需要与远程协作时,可以在代码托管平台创建仓库,再执行 git remote add;也可以直接使用已有远程地址。

如果使用 GitHub HTTPS 地址,账号密码认证已经被移除,应使用个人访问令牌(PAT)、Git Credential Manager 或平台支持的 OAuth/企业认证;不要把 PAT 直接写进远程 URL 或提交到仓库。SSH 则应保护好私钥并为私钥设置口令。

git remote

git remote 用于管理远程仓库名称和地址:

# 查看远程名称
git remote

# 查看名称、抓取地址和推送地址
git remote -v

# 添加远程仓库
git remote add origin <仓库地>

# 修改远程地址
git remote set-url origin <新地>

# 删除本地的远程配置,不会删除服务器上的仓库
git remote remove origin

上面命令前的缩进只是排版问题,实际复制时不需要多余空格。origin 只是约定俗成的名称,可以改成其他名称;它不代表某个特殊服务器。历史文章中的 git remote rm origingit remote remove origin 的别名。

git branch、git switch 和 git checkout

拿到一个项目后,先查看当前分支和远程跟踪分支:

# 查看本地分支
git branch

# 查看远程跟踪分支
git branch --remotes

# 查看本地和远程跟踪分支
git branch --all

现代 Git 推荐使用 switch 表达分支操作:

# 创建并切换到新分支
git switch -c feature/login

# 切换到已有分支
git switch feature/login

# 从远程跟踪分支创建本地分支并设置上游
git switch --track origin/feature/login

# 查看分支及其上游和最近提交
git branch -vv

# 删除已经合并的本地分支
git branch --delete feature/login

# 强制删除未合并分支,可能丢失该引用
git branch --delete --force feature/login

# 重命名当前分支
git branch --move new-name

历史文章常见写法仍然有效:

git checkout -b feature/login
git checkout feature/login

checkout 同时承担分支切换和文件恢复两种语义,Git 2.23 之后可以优先使用 switch 切分支、restore 恢复文件,减少歧义。

git add

在某个分支修改代码后,先把需要提交的内容放入暂存区:

# 添加一个或多个指定文件
git add path/to/file1 path/to/file2

# 交互式选择部分改动
git add --patch

# 添加当前目录及其子目录中的变更,执行目录会影响范围
git add .

# 从仓库根目录添加所有工作区变更,包括新增、修改和删除
git add -A

# 检查暂存区内容
git status --short
git diff --staged

原文说 git add . 是“当前目录下的所有文件改动”,这个说法需要补充范围限制:它受当前执行目录影响,可能不会覆盖仓库其他目录。git add -A 的范围更接近整个工作区,但也要先确认不会把密钥、.env、构建产物或依赖目录加入提交。

git commit

文件进入暂存区后,可以创建本地提交:

# 打开编辑器填写提交信息
git commit

# 直接指定提交信息
git commit -m "feat: 完成功能"

# 仅对已跟踪文件的修改和删除自动暂存后提交
git commit -am "fix: 修复校验逻辑"

# 修改最近一次提交,通常会改变提交 ID
git commit --amend
# 只修改内容、不重新编辑提交信息
git commit --amend --no-edit

-a 不是 -Agit commit -am 不会包含未跟踪的新文件,新文件仍然要先执行 git add。提交前建议检查:

git diff --staged
git status --short

不要把密码、PAT、私钥、云服务密钥或 .env 内容提交到历史中。若密钥已经提交或推送,仅添加 .gitignore 并不能让历史中的密钥失效,应立即吊销并轮换,必要时再清理历史。

git rm

git rm 会同时更新工作区和暂存区,适合删除已经被 Git 跟踪的文件:

# 删除文件并将删除动作加入暂存区
git rm path/to/file

# 删除已跟踪目录
git rm -r dist

如果 .env 已经被跟踪,但希望保留本地文件、以后不再跟踪,应使用:

git rm --cached .env
echo .env >> .gitignore
git add .gitignore
git commit -m "chore: 停止跟踪本地环境文件"

git rm --cached 只取消跟踪,不会删除工作区文件;普通的 git rm .env 则会把文件从工作区也删除。.gitignore 只会影响尚未被跟踪的文件,不能自动删除已经存在于 Git 历史中的敏感内容。

git push

首次推送新分支并建立上游关系:

git switch -c feature/login
git push --set-upstream origin feature/login
# 后续在该分支上通常可以直接使用 git push / git pull
git push

-u--set-upstream 等价。推送不是“把本地文件夹上传”这么简单,而是请求远程仓库更新某个引用;远程仓库可能因为分支保护、权限、非快进或 CI 策略拒绝请求。

如果远程已经存在同名分支:

# 先获取远程状态
git fetch origin

# 查看本地和远程的分叉情况
git log --oneline --graph --decorate HEAD..origin/feature/login
git log --oneline --graph --decorate origin/feature/login..HEAD

如果本地落后远程,应先按照团队策略执行 git merge origin/feature/logingit rebase origin/feature/login,解决冲突、测试后再推送。不要为了绕过非快进拒绝就随意使用 git push --force;个人分支确需改写时,优先使用:

git push --force-with-lease origin feature/login

共享分支应优先通过 PR/MR、分支保护和 git revert 保持可追踪性。

git pull

git pull 通常先执行 fetch,再把远程引用整合到当前分支。它不永远等价于 git fetch && git merge,因为可以选择 rebase 或只允许 fast-forward:

# 使用当前分支配置的上游
git pull

# 明确使用 merge 整合
git pull --no-rebase origin branch1

# 明确使用 rebase 整合
git pull --rebase origin branch1

# 只接受快进;本地和远程分叉时停止,不自动解决
git pull --ff-only origin branch1

git pull origin branch1 会把远程的 branch1 获取并整合到当前本地分支,不等于切换到 branch1,也不一定自动建立上游关系。首次推送建立上游通常使用 git push -u origin branch1

实际工作中,先 fetch 再查看差异往往更稳妥:

git fetch origin
git log --oneline --graph --decorate HEAD..origin/branch1
git diff HEAD..origin/branch1
# 根据团队策略二选一:
git merge origin/branch1
# 或:git rebase origin/branch1

不要把 git pull 当作无条件覆盖本地代码的命令。它会尊重工作区状态和整合策略,可能产生冲突或新的合并提交。

git fetch

git fetch 获取远程仓库的新对象和引用,但不会自动把远程变更合并到当前本地分支,也不会直接改写当前工作区:

# 获取 origin 的默认远程更新
git fetch origin

# 获取指定远程分支
git fetch origin branch1

# 获取所有远程,并清理已经不存在的远程跟踪引用
git fetch --all --prune

fetch 后可以通过 origin/branch1 查看远程跟踪分支,再选择 merge、rebase 或其他处理方式。--prune 清理的是本地已经不存在于远程的 remote-tracking 引用,不是删除远程仓库的分支。

git merge

git merge 把指定分支的历史整合到当前分支。以提交 MR 前同步主线为例:

# 方案一:使用远程跟踪分支,避免先切换到主分支
git switch feature/login
git fetch origin
git merge origin/main

也可以按原文的流程操作:

# 先更新本地主分支
git switch main
git pull --ff-only origin main

# 再回到开发分支合并主线
git switch feature/login
git merge main

merge 不一定总会产生合并提交:如果可以 fast-forward,Git 可能只移动当前分支指针;需要保留一次合并节点时可以根据团队策略使用 --no-ff。发生冲突时,解决文件后检查暂存区并完成合并:

git status
# 编辑冲突文件,删除冲突标记并保留正确内容
git add <已解决文>
git commit                 # 或使用 git merge --continue

# 如果决定放弃本次合并
git merge --abort

git log

git log 用来查看当前分支的提交历史,也可以通过 --all 查看所有本地引用:

# 简洁查看当前分支
git log --oneline

# 查看分支图、标签和远程跟踪分支
git log --oneline --graph --decorate --all

# 查看某个文件的历史
git log --follow -- path/to/file

示例输出如下,提交 ID、分支名和作者信息只是示例:

commit e55c4d273141edff401cbc6642fe21e14681c258 (HEAD -> branch1, origin/branch1)
Author: 陌小路 <44311619+STDSuperman@users.noreply.github.com>
Date:   Mon Aug 1 23:16:11 2022 +0800

    Initial commit

除了查看谁提交了什么,git log 还是排查版本、比较分支、寻找可恢复提交的重要入口。需要查看某个提交具体改了什么,可以使用 git show <commit>

git reset

git reset 既可以移动当前分支的 HEAD,也可以只更新暂存区。常见语法是:

git reset [--soft | --mixed | --hard] [<commit>]

# 路径形式只更新暂存区,不移动 HEAD
git reset [<commit>] -- <pathspec>...

关于 HEAD

  • HEAD:当前检出的提交或当前分支指向的提交。
  • HEAD^:当前提交的第一个父提交,通常等同于 HEAD~1
  • HEAD^^:沿第一个父提交再向前一个提交。
  • HEAD~n:沿第一个父提交向前回退 n 代。

合并提交有多个父提交,^~ 的含义需要结合提交图确认,不要机械套用。

reset 参数

下面的说明以目标提交为 HEAD^ 为例:

  • --soft:移动 HEAD/当前分支,但不改变暂存区和工作区。适合把最近几个本地提交重新整理后再提交。
  • --mixed:默认模式,移动 HEAD,并把暂存区更新为目标提交;工作区文件保持不变,因此原提交中的改动会回到未暂存状态。
  • --hard:移动 HEAD,同时把暂存区和已跟踪工作区更新为目标提交。它可能覆盖工作区改动,也可能覆盖未跟踪文件;执行前必须确认没有需要保留的数据。

原文中的 -soft-mixed-hard 少了一个连字符,正确写法是 --soft--mixed--hard

假设我们改动 README 后依次完成了工作区修改、git addgit commit,撤回最近一次提交的结果大致是:

  • git reset --soft HEAD^:提交被撤回,但 README 改动仍在暂存区。
  • git reset --mixed HEAD^:提交被撤回,README 改动仍在工作区但不再暂存。
  • git reset --hard HEAD^:提交被撤回,已跟踪的 README 改动也被目标提交覆盖,可能丢失内容。

默认不带模式时相当于 --mixed

git reset HEAD^

场景一:撤销 git add

如果只是误把文件加入暂存区,现代 Git 推荐:

# 取消指定文件的暂存,保留工作区改动
git restore --staged path/to/file

# 历史写法,效果相近
git reset HEAD -- path/to/file

git reset 不带提交且不带路径也常被用来把整个暂存区恢复到 HEAD,但使用前应通过 git diff --staged 确认范围。

场景二:撤销尚未共享的 git commit

如果最近一次提交只存在于个人本地分支,可以根据想保留的状态选择:

# 保留改动并继续处于暂存状态
git reset --soft HEAD^

# 保留改动,但回到未暂存状态
git reset --mixed HEAD^

如果提交已经进入共享分支,不要直接 reset 后强推覆盖他人历史;通常使用 git revert <commit>,或者按照团队的历史改写流程处理。

场景三:回到旧提交检查代码

原文建议直接执行 git reset --hard <commit>。这个命令确实可以让当前分支和工作区回到指定提交,但破坏性很强。仅仅为了查看旧版本时,优先使用 detached HEAD 或新工作树:

# 只查看旧版本,不移动当前分支
git switch --detach <commit-id>

# 看完后回到原分支
git switch feature/login

如果确定要把个人分支退回旧提交,先建立恢复分支,再执行 reset:

git branch recovery/before-hard-reset HEAD
git reset --hard <commit-id>

场景四:误 reset 后找回代码

原文示例中曾经把 mastere62b559... reset 到 36577ea...,然后尝试手工复制代码。更安全的做法是先用 reflog 保存原位置,不要假设 git log 能看到所有曾经存在的提交:

git reflog

# 二选一:假设确认 HEAD@{1} 是 reset 前的位置
git branch recovery/before-reset HEAD@{1}

# 或者使用 reflog 中确认过的提交 ID
git branch recovery/before-reset e62b559633387ab3a5324ead416f09bf347d8e4a

如果只是需要从旧提交恢复某个文件,可以:

git restore --source=<old-commit> -- path/to/file
# 或查看旧提交中的文件内容
git show <old-commit>:path/to/file

确认恢复点后,再切换到恢复分支或按需把当前分支 reset 回去:

git switch recovery/before-reset

“只要不删除 .git 就能找回所有提交”并不准确:reflog 是本地记录,会按配置过期;不可达对象最终可能被垃圾回收。误操作后应尽快建立恢复分支,并避免运行会清理对象的命令。

git reflog

git reflog 用来查看本地引用曾经指向过哪些位置,常用于恢复 reset、rebase、误删分支或 detached HEAD。它不是远程日志,也不是所有仓库成员共享的操作记录:

# 查看 HEAD 的引用移动记录
git reflog

# 查看指定本地分支的 reflog
git reflog show main

# 查看所有本地引用的 reflog(按需使用)
git reflog show --all

典型输出:

36577ea HEAD@{0}: reset: moving to 36577ea
 e62b559 HEAD@{1}: reset: moving to e62b559

找到目标后,优先创建恢复分支:

git branch recovery/restore-work HEAD@{1}

远程仓库不会因为本地 reflog 存在就自动拥有这些提交。若本地 reflog 找不到,还可以在确认风险后了解 git fsck --no-reflogs 等对象恢复手段,但恢复结果不保证完整。

git revert

如果是针对共享的 mainmaster 或 release 分支,为了安全起见通常建议使用 revert。它和 reset 的效果都可能让最终文件内容回到旧状态,但机制不同:reset 移动分支指针,revert 在现有历史上创建一个新的反向提交。

例如 README 新增了一行文字并已经推送,后来需要撤销这次改动:

git revert <commit-id>

Git 会创建一个新的 Revert "原提交信息" 提交,原提交仍然保留在历史中。可以使用 --no-edit 接受默认提交信息:

git revert --no-edit <commit-id>

git revert 通常要求工作区干净;如果发生冲突,需要手动解决并使用 git add 后继续。多个提交可以逐个指定,也可以使用范围,但复杂历史和相互依赖的提交可能需要确认撤销顺序:

git revert <commit-a> <commit-b>
git revert <oldest-commit>^..<newest-commit>

如果要回滚一个 MR/PR,GitLab、GitHub 等平台可能提供按钮,但按钮行为仍然会落到创建反向提交或打开回滚请求,具体以平台和权限策略为准。

合并提交有多个父提交,revert 时需要指定主线父提交:

git revert -m 1 <merge-commit>

这里的 1 不是永远代表“主分支”,必须根据 git show --summary <merge-commit> 和提交图确认父提交编号。revert 合并提交后,原功能分支的提交对象仍然是当前历史的祖先;再次直接 merge 可能不会自动重新引入旧变更。若确认要整体恢复,通常需要 revert 那个 revert 提交,或者用新的提交修复后重新发起合并请求。

git cherry-pick

git cherry-pick 把一个或多个已有提交引入当前分支,并通常为每个变更创建新的提交。它适合把一个独立修复带到 release 分支,或者把功能分支中的某个提交选择性带到另一条分支。

原文曾把来源分支名和多个 commit ID 混在一个命令中,但 cherry-pick 没有“指定来源分支”的这种通用参数。Git 会把不符合语义的参数当作提交或修订版本解析,容易产生错误。正确流程是先找到提交,再切换到目标分支:

# 在来源分支或所有引用中查找提交
git log --oneline --all --decorate

# 切换到目标开发分支
git switch feature/login

# 复制一个或多个指定提交
git cherry-pick <commit-id-1> <commit-id-2>

cherry-pick 不会把来源分支本身合并进来。新提交的父提交属于当前分支,因此新提交 ID 通常不同;作者信息通常会保留,提交者信息和提交时间会重新生成。来源分支不会因为 cherry-pick 被删除或回滚。

如果误在 master 上提交了尚未推送的功能,先保留当前提交,再把主分支恢复到正确基线:

# 当前仍在包含误提交的 master 上,创建分支保存当前提交
git switch -c feature/recover-mistake

# 回到 master,确认 origin/master 是正确基线后再回退
git switch master
git fetch origin
git reset --hard origin/master

# 这个分支已经包含原来的功能提交,不需要再次 cherry-pick
git switch feature/recover-mistake
git log --oneline -n 5
git push --set-upstream origin feature/recover-mistake

如果目标开发分支已经存在于正确基线之上,则切到该分支后再执行 git cherry-pick <功能提交>;不要把已经包含该提交的分支再次 cherry-pick 到自己身上。如果误提交已经推送到共享的 master,不要擅自 reset 和 force push,应根据团队规则使用 revert 或提交修复请求。

发生 cherry-pick 冲突时:

# 解决冲突后只暂存确认过的文件
git add <已解决文>
git cherry-pick --continue

# 放弃整个 cherry-pick 序列
git cherry-pick --abort

# 忘记当前序列状态,但保留工作区和索引现状
git cherry-pick --quit

--quit 只是清除 sequencer 状态,不会像 --abort 一样恢复到操作前;使用前要理解两者区别。

git tag

标签是对某个对象的命名引用,常用于版本发布。可以查看 Vite 或其他开源项目的官方仓库,了解版本标签如何对应发布版本。

Git 标签主要有轻量标签和附注标签两种形式。

轻量标签

git tag v1.0.0

轻量标签只是一个直接指向对象(通常是提交)的引用,不是一个会随提交前进的分支,也不是天然不可修改的版本锁。可以显式指定目标提交:

git tag v1.0.0 <commit-id>

原文把轻量标签比作“不可变的分支”,这个比喻容易误导:标签默认不会随新的提交移动,但拥有权限的人仍可以使用 git tag -f 移动它。已经发布给别人使用的标签通常不应随意改写。

检出标签时会进入 detached HEAD:

检出标签后的 detached HEAD 提示

现代写法:

git switch --detach v1.0.0

如果要在这个版本上继续开发,应创建分支保存新提交:

git switch -c hotfix/from-v1.0.0

附注标签

git tag --annotate v1.0.1 --message "发布正式版 1.0.1"

简写仍然常用:

git tag -a v1.0.1 -m "发布正式版 1.0.1"

附注标签是 Git 数据库中的完整 tag 对象,包含创建时间、打标签者、邮件地址、说明信息,也可以进行密码学签名。现代 Git 支持根据 gpg.format 使用 GPG、SSH 或其他签名后端,具体取决于本机和团队配置:

# 使用默认签名后端创建签名标签
git tag -s v1.0.1 -m "发布正式版 1.0.1"

# 查看标签对象和指向的提交
git show v1.0.1

# 验证签名标签
git verify-tag v1.0.1

从信息丰富度和发布可追踪性来说,附注标签通常更适合正式版本;轻量标签可以用于本地、临时或私有标记。最终仍应遵循项目发布规范。

对比标签信息

轻量标签的 git show 主要显示它指向的提交及其差异;附注标签会先显示 tag 对象,再显示目标提交。例如原文中的输出可整理为:

commit dcbd335be87f51eaa0cc1852400e64e9f46e84d8 (HEAD -> test-branch1, tag: v1.0.2, tag: v1.0.1)
Author: STDSuperman <2750556766@qq.com>
Date:   Tue Aug 16 22:54:36 2022 +0800

    xx

diff --git a/README.md b/README.md
index 715766a..b4cdea6 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
-# git-practice
+# git-practice
+
+test tag

附注标签的输出会额外包含类似内容:

tag v1.0.1
Tagger: STDSuperman <2750556766@qq.com>
Date:   Tue Aug 16 22:58:27 2022 +0800

发布正式版 1.0.1

推送和删除标签

标签不会因为普通 git push 自动全部上传,常见做法是显式推送:

# 推送一个标签
git push origin v1.0.1

# 推送当前分支可达的附注标签,不会推送所有轻量标签
git push origin main --follow-tags

# 推送本地所有标签,执行前确认范围
git push origin --tags

# 删除本地标签
git tag --delete v1.0.1

# 删除远程标签
git push origin --delete v1.0.2

# 历史写法:向远程推送空引用以删除标签
git push origin :refs/tags/v1.0.1

原文示例中的 v1.0.1v1.0.2 有混用,实际操作时要确认标签名称。正式发布的标签一旦被消费就应视为公开承诺,不要为了“修正”同名标签而随意移动;如果确需重打标签,应提前通知使用者并按照平台规范处理。

git rebase

原文把 rebase 归纳为两个主要用途:分支整合和提交历史整理。这两个方向都成立,但 rebase 会重新生成被重放提交,导致提交 ID 改变,通常只应作用于个人分支、未共享历史或已经取得团队同意的分支。公共分支应优先使用合并请求、分支保护和明确的协作策略。

分支整合

假设 master(现在也可能是 main)有一个 txt 文件:

test1
test2

然后从它创建两个分支 dev1dev2,分别修改相同的两行:

dev1 分支第一次提交:

testdev-1
test2

dev1 分支第二次提交:

testdev-1
testdev1-2

dev2 分支第一次提交:

testdev2-1
test2

dev2 分支第二次提交:

testdev2-1
testdev2-2

两个分支分别提交两次后,分支结构大致如下:

dev1 与 dev2 的分支结构

现在希望把 dev2 的结果作为基础,重新应用 dev1 的两个提交。先切到 dev1,然后执行:

git switch dev1
git rebase dev2

这里的语义是:找到 dev1dev2 的共同祖先,把 dev1 独有的提交逐个重新应用到 dev2 的最新提交之上。它不是把两个分支简单“揉成一个”,而是改变 dev1 的提交基线。

由于两个分支都修改了相同位置,rebase 可能在应用 dev1 的第一个提交时发生冲突:

rebase 第一次冲突提示

冲突解决流程:

git status
# 编辑冲突文件,删除 <<<<<<<、=======、>>>>>>> 标记并保留正确内容
git add path/to/file

git rebase --continue

原文在这里建议执行 git commit,这是 rebase 场景下容易出错的地方。通常不需要手动创建提交,git rebase --continue 会根据原提交信息继续重放并创建提交;如果 Git 打开提交信息编辑器,确认后保存退出即可。

在这个例子中,可以选择保留 dev1 对第一行的修改、保留 dev2 对第二行的修改:

testdev-1
testdev2-2

暂存解决结果后继续。如果第二个 dev1 提交仍然冲突,再次解决并继续:

rebase 第二次冲突提示

第二次冲突可以把最终结果整理为:

testdev-1
testdev2-2

然后执行:

git add path/to/file
git rebase --continue

如果发现选错了基线或不想继续:

git rebase --abort

完成后会看到类似提示:

Successfully rebased and updated refs/heads/dev1.

最终分支图大致如下:

rebase 完成后的分支结构

ourstheirs 在 rebase 冲突中的直觉含义可能与普通 merge 相反:当前基础通常是 dev2,正在重放的是 dev1 的提交。不要只凭冲突标记的名称选择一侧,应结合提交目的和最终文件内容判断。

如果使用 git merge dev2,Git 会以一次三方合并处理分支;非 fast-forward 时通常会创建合并提交并保留两条分支线,fast-forward 时则可能只移动分支指针。merge 和 rebase 没有绝对的优劣:前者保留拓扑,后者通常得到更线性的历史,但会改写被重放提交的身份。

提交历史整理

开发一个需求的过程中,可能产生“修正拼写”“临时调试”“测试提交”等不适合保留在公共历史中的提交。交互式 rebase 可以重新排序、修改提交信息、合并提交或删除提交,但只应在尚未被他人依赖的历史上使用。

这里保留原文构造的提交示意:

需要整理的提交历史

原文的目标包括:

  • xxx 改成更清晰的提交信息;
  • 删除“移除空行”那次提交;
  • 把测试提交合并到上一个提交。

首先找到目标范围之前的提交,复制其提交 ID,然后执行交互式 rebase:

git log --oneline
git rebase -i fce08458e27262edcb9c551db08eb152919efa31

-i 表示 interactive,选项应放在提交参数之前;上面的写法是更清晰、可复用的形式。

编辑器中常见命令:

pick    保留提交
reword  保留改动,只修改提交信息
edit    应用提交后暂停,允许修改内容
squash  合并到前一个提交,并保留两个提交信息后打开编辑器
fixup   合并到前一个提交,丢弃当前提交信息
drop    删除该提交

交互式 rebase 的 todo 文件大致如下:

交互式 rebase 编辑页面

把原文需求整理成:

reword 0341ec3 xxx
drop   22c31d1 移除空行
squash 57fde50 feat: 测试
pick   1a7f146 feat: 修改第二行代码

实际编辑时应保留 Git 生成的完整提交 ID 和格式;上面只是展示动作含义。drop 行可以保留原提交信息,不需要为了避免报错而手动删除消息;不要改动 todo 文件中的注释和命令格式。

保存并退出 Vim 通常是:按 Esc,输入 :wq,再回车。也可以配置 VS Code 等编辑器:

git config --global core.editor "code --wait"

执行 reword 后,Git 会打开新的提交信息编辑器:

reword 修改提交信息

例如把 xxx 改成:

feat: 功能改动

执行 squash 后,Git 会打开合并提交信息的编辑器:

squash 合并提交信息

保留需要的提交信息并保存退出。若重放过程中产生冲突,正确流程是:

git status
# 解决冲突
git add <已解决文>
git rebase --continue

# 放弃整个交互式 rebase
git rebase --abort

不需要在普通 rebase 冲突处理中额外执行 git commit--continue 会继续重放当前提交。完成后会出现类似:

Successfully rebased and updated refs/heads/dev3.

最后的历史图如下:

交互式 rebase 完成后的提交图

如果这些提交已经推送到个人远程分支,整理后通常需要:

git push --force-with-lease origin feature/login

--force-with-lease 比无条件的 --force 更能避免覆盖他人刚推送的更新,但仍然会改写远程历史;公共分支和多人共享分支不要擅自这样做。

总结:先判断要改哪一层

Git 命令很多,但可以先问自己三个问题:

  1. 只是工作区改错了吗? 使用 git restore、stash 或临时提交。
  2. 只是暂存区不对吗? 使用 git restore --staged,历史写法是路径形式的 git reset
  3. 本地提交历史需要整理吗? 未共享时可以考虑 reset 或交互式 rebase;已经共享时优先使用 revert 或团队认可的合并流程。

还需要记住:

  • git fetch 只获取远程更新,git pull 还会执行整合;
  • git merge 通常保留分支拓扑,git rebase 会重新生成被重放提交;
  • git cherry-pick 复制的是提交变更,不是“指定来源分支”的参数;
  • 轻量标签和附注标签都能指向提交,但正式发布通常优先使用附注标签,并谨慎处理签名和标签改写;
  • reflog 是本地恢复入口,但会过期,不能替代远程备份;
  • .gitignore 不能消除已经提交的秘密,泄露的密钥必须立即吊销和轮换;
  • 执行 reset --hardclean、force push、删除标签或清空 stash 前,都要确认影响范围。

优雅,永不过时;命令会变,分层思维更重要。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS