Git高频命令与注意事项

0 阅读6分钟

一、问题回顾

在 feature/branchA 分支的基础上 checkout feature/branchA-bak 分支,由于需求涉及到model-A 和 model-B两个模块的开发,但是需要先发布上线 model-A的代码,暂不上线 model-B的代码,所以想在 checkout feature/branchA-bak 分支 上回退 model-B模块的代码和master分支保持一致,只发布model-A 的模块代码,但是在执行模块回退的git命令犯了一个错误:

git checkout master -- model-B

在feature/voip-add-node-bak 分支上执行 git checkout master -- model-B 命令,然后本地解决一些代码依赖等冲突后执行 commit: Revert model-B to master on bak branch 然后push了,由于需求改动的文件比较多,几十上百个改动文件,将近两万行代码,没有仔细比对commit 代码是否合理就执行了push操作,造成 model-B 模块回退的时间较早,之前很多功能和配置都被回退了,上线时直接把feature/voip-add-node-bak 分支代码合并到master分支上,造成master分支代码也回退了。

分析:

git checkout master -- model-B 这条命令的意思是: "用本地master分支中的model-B目录,覆盖当前工作区中的model-B目录", feature/branchA-bak 分支上master代码很可能不是最新的,可能落后于代码仓库上master分支几周的时间,因此 真实回退的commit-id基准不可控。正确的做法应该是:

# 查看远程master的最新commit
git log origin/master --oneline -1
​
# 使用具体的commit哈希
git checkout commit-id -- model-B

修复master分支命令:

# 安全地撤销错误的合并
git revert -m 1 bad123
git push origin master
# 优点:保留历史,团队友好

或者点击一键回滚,回滚代码到上次确认上线的版本。

二、日常开发高频命令

我们现在是多分支开发,经常涉及切换分支,merge 分支代码等,基于我们的使用场景,分析一些重要的git 命令 以及容易忽略错误使用的命令,深刻理解git 的原理的技巧,减少误操作,比如 git rebae、 git merge 、 git reset、 git stash、 git store等等。

一、Git本质:理解这三个核心概念

1.Git的状态与区域

image.png

工作区 (Working Directory) → 暂存区 (Staging Area) → 本地仓库 (Local Repository)  →  远程仓库 (Remote Repository)
       ↓                               ↓                           ↓                           ↓
   你编辑的文件                   git add后的文件              git commit后的提交           git push后的共享代码

五个工作区

  • 工作区:就是你看到的项目文件,可以直接编辑。
  • 暂存区:一个中间层,让你决定哪些修改要进入下一个版本。
  • 本地仓库:保存了项目完整历史的地方,本地远程仓库是远程分支在本地的引用,属于本地仓库的一部分。
  • 远程仓库:团队共享的代码仓库。

2. Git对象模型:一切皆对象

Git底层有四种核心对象,理解它们能解开很多谜团:

对象类型作用存储内容不存储内容典型结构/字段是否去重常用查看命令典型场景
Blob(Binary Large Object)存文件内容本身文件字节内容文件名、路径、目录结构、作者、时间、提交信息原始字节流相同内容只存一份(SHA-1/SHA-256)git hash-object``git cat-file -p <blob>文件内容入库、内容去重
Tree目录结构快照文件名、权限(mode)、对象类型(blob/tree)、对象哈希文件内容、作者、提交信息100644 blob <hash> filename``040000 tree <hash> dirname间接去重(结构相同共享 tree)git cat-file -p <tree>git ls-tree <tree>构建项目快照
Commit存一次提交记录根 tree 指针、父 commit、作者、提交者、时间、提交信息文件内容、目录结构tree <hash>``parent <hash>``author ...``committer ...间接去重(共享 tree/blob)git cat-file -p <commit>构建历史链
Tag(标签)给 commit 起永久名字指向对象、标签作者、时间、说明、签名文件内容、目录结构object <hash>``type commit``tag v1.0不去重git cat-file -p <tag>版本发布、里程碑标记
# 查看Git对象类型
git cat-file -t <hash>  # 查看对象类型
git cat-file -p <hash>  # 查看对象内容# 四种对象:
# 1. blob: 存储文件内容(文件名和路径信息不在这里)
# 2. tree: 存储目录结构,指向blob和其他tree
# 3. commit: 存储提交信息,指向一个tree和父commit
# 4. tag: 指向特定commit的标签

3. git的本质:指针+快照系统

commit:

  • 生成快照(tree + blob)
  • 分支指针前移一格

checkout:

  • HEAD 换个指针
  • 工作区重建快照

reset:

  • 分支指针往回挪
  • 可选是否重建暂存区和工作区
# 查看分支文件
cat .git/HEAD                     # 当前HEAD指向
cat .git/refs/heads/main          # main分支指向的commit# 创建分支的本质
echo $(git rev-parse HEAD) > .git/refs/heads/new-branch

分支不是文件的拷贝,只是一个指向某个提交的指针。切换分支时,Git只是替换工作目录中的文件。

切换分支时,Git 会:

  1. 移动 HEAD 指向新的分支
  2. 更新 index(暂存区)
  3. 用目标 commit 的 tree 重建工作区文件

二、高频命令深度解析

1. git add:不仅仅是添加文件

# 常见用法
git add file.txt          # 添加单个文件
git add .                 # 添加所有修改(慎用!)
git add -p               # 交互式添加(推荐)# 本质:将工作区的修改保存为blob对象,并更新暂存区

坑点

  • git add . 会添加所有修改,包括你不想提交的调试代码
  • 已添加到.:的文件不会被add,但已跟踪文件的修改会被add

安全做法

# 先查看状态,再选择性添加
git status -s            # 简洁状态
git diff                 # 查看具体修改
git add -p               # 交互式添加,逐块确认

2. git commit:创建版本快照

# 常用方式
git commit -m "message"          # 简单提交
git commit --amend               # 修改最近提交
git commit --allow-empty         # 创建空提交# 本质:创建一个commit对象,包含tree、父提交、作者信息和提交信息

深入理解

# 查看提交的完整信息
git show --stat                   # 查看提交的统计信息
git show --name-only              # 查看提交修改的文件名
git show -p                       # 查看提交的具体差异# 修改提交信息
git commit --amend -m "新的提交信息"
# 注意:这会创建一个新的commit对象,替换旧的

坑点

  • git commit --amend重写历史,已推送到远程的提交不要使用
  • 提交信息不清晰会给团队协作带来麻烦

3. git checkout vs git switch/git restore

Git 2.23引入了更明确的命令:

# 传统方式(容易混淆)
git checkout feature      # 切换分支
git checkout -- file     # 丢弃工作区修改# 新命令(语义更清晰)
git switch feature       # 切换分支
git restore file        # 恢复文件
git restore --staged file # 从暂存区移除

本质区别

  • checkout:多功能命令,可以切换分支、恢复文件等
  • switch:专门用于切换分支
  • restore:专门用于恢复文件

坑点

  • git checkout -- .永久丢失所有未提交的修改
  • 恢复前一定要先用 git status 确认
  • 切换分支,能用 switch,就别用 checkout

使用 checkout 的风险点:

1)意外覆盖未提交的修改:

# 假设你在修改一个文件,还没提交
echo "new changes" > important.txt
​
# 危险操作:checkout 到另一个分支
git checkout other-branch
# ❌ 如果 other-branch 也有 important.txt,Git 会尝试合并
# ❌ 如果合并冲突,可能造成修改丢失或混乱# 安全操作:使用 switch
git switch other-branch
# ⚠️ 同样会检查未提交修改,但语义更清晰

2)与恢复文件操作混淆:

# 危险:想切换分支,但写错了命令
git checkout .      # 本意是切换分支,实际恢复了所有文件!
# ❌ 这将丢弃所有未暂存的修改!# 安全:分开使用不同命令
git switch main     # 切换分支
git restore .       # 恢复文件(如果需要)

3)意外分离 HEAD:

# 危险:检查历史提交
git checkout abc1234  # 分离 HEAD 到指定提交
# ❌ 现在处于 "detached HEAD" 状态
# ❌ 在此状态下提交会产生悬空提交(易丢失)# 安全:明确意图
git switch --detach abc1234  # 明确表示要分离 HEAD
# 或使用更安全的查看方式
git show abc1234
git log --oneline -10

4)分支名与文件名冲突:

# 假设有一个分支叫 "docs",也有一个文件叫 "docs"# 危险:想切换分支,但可能被误解
git checkout docs
# 这到底是切换分支还是恢复文件?Git 优先解释为分支名
# 但如果分支不存在,会被解释为恢复文件!# 安全:明确操作
git switch docs        # 明确切换分支
git restore -- docs    # 明确恢复文件

4. git reset:三个级别

# 三级reset的区别
# 假设提交链:A(最早)→ B(中间)→ C(最新,HEAD)# --soft: 只移动HEAD指针
git reset --soft B
# 结果:C的修改在暂存区,可以重新提交# --mixed: 移动HEAD指针并重置暂存区(默认)
git reset B  # 等同于 git reset --mixed B
# 结果:C的修改在工作区,需要重新add# --hard: 彻底重置
git reset --hard B
# 结果:C的修改完全丢弃,不可恢复!

举个例子

场景 1:只有 Commit,没有 Push

# 初始:本地 AB → C,远程只有 AB
#        (C 只在你的电脑上)
​
# 执行 reset
git reset --hard B
​
# 结果:
# 本地:AB (HEAD)   # C 完全消失
# 远程:AB          # 不受影响(本来就没有 C)
​
# 结论:安全!因为没有影响别人

场景 2:已经 Push 了

# 初始:本地 AB → C,远程也是 AB → C
#        (别人可能已经拉取了 C)
​
# 执行 reset
git reset --hard B
​
# 结果:
# 本地:AB (HEAD)   # C 消失
# 远程:AB → C      # 远程依然有 C!
​
# 现在本地和远程不一致!

5. git merge vs git rebase:选择策略

merge的本质

# Merge(合并)是将两个分支的修改历史整合在一起,创建一个新的合并提交(merge commit)。
git merge feature
# 会创建一个新的commit,有两个父提交
​
# 查看合并历史
git log --graph --oneline

场景示例:

# 1. 找到共同祖先(Base)
# main: A ← B ← E
# feature: A ← B ← C ← D
# 共同祖先:B# 2. 计算差异
diff(B → E)  # main 分支的修改
diff(B → D)  # feature 分支的修改# 3. 合并差异
# 如果没有冲突,自动合并
# 如果有冲突,需要手动解决# 4. 创建合并提交
git commit -m "Merge branch 'feature' into main"
​
​
# 场景:两个分支都有新提交
main:    A ← B ← E
feature:       ← C ← D# 执行合并
git checkout main
git merge feature
​
# 结果:创建合并提交 F
main:    A ← B ← E ← F
                    ↖
feature:       ← C ← D
​
特点:
​
创建新的合并提交 F
​
F 有两个父提交:E 和 D
​
保留两个分支的完整历史

rebase的本质

# Rebase(变基)是将一个分支的提交重新应用到另一个分支上,重写历史使其变成线性。
git rebase main
# 1. 找到当前分支和main分支的最近共同祖先
# 2. 提取当前分支的提交,保存为临时文件
# 3. 将当前分支指向目标分支的最新提交
# 4. 依次应用保存的提交

场景示例:

# 1. 保存要变基的提交
git checkout feature
git log --oneline
# C: "Add feature X"
# D: "Fix bug in feature X"# 2. 重置到目标分支
git reset --hard main
​
# 3. 逐个应用保存的提交
git cherry-pick C  # 创建 C'
git cherry-pick D  # 创建 D'# 原来的 C、D 提交被废弃(变成悬空对象)
​
​
​
​
# 初始状态
main:    A ← B ← E
feature:       ← C ← D# 执行变基
git checkout feature
git rebase main
​
# 过程:
# 1. 找到共同祖先 B
# 2. 保存 C、D 的修改为临时补丁
# 3. 将 feature 分支重置到 main 的最新提交 E
# 4. 按顺序应用补丁,创建新的提交 C'、D'# 结果:
main:    A ← B ← E
feature:             ← C' ← D'

Merge vs Rebase:核心区别

特性MergeRebase
历史记录保留分支结构,有合并提交线性历史,无合并提交
提交哈希不改变现有提交的哈希改变提交哈希(重写历史)
冲突处理一次解决所有冲突可能多次解决冲突(每个提交)
安全性安全,不破坏历史危险,重写历史可能造成混乱
适用场景公共分支,团队协作个人分支,整理提交
可视化分支合并的网状结构整洁的线性结构

黄金规则

  • 已推送到远程的提交不要rebase
  • 个人本地分支可以用rebase保持整洁
  • 公共分支用merge保留完整历史

6. git stash:临时保存

# 完整用法
git stash save "描述信息"         # 保存当前工作
git stash list                   # 查看保存列表
git stash apply stash@{1}        # 取回不删除(可重复取)
git stash pop                    # 应用并删除最近保存
git stash drop stash@{2}         # 删除特定保存
git stash show -p stash@{0}      # 查看保存的差异

本质:stash实际上创建了一个特殊的commit,保存在.git/refs/stash中。

坑点

  • 多次stash后容易混乱,记得加描述
  • 默认不包含未跟踪文件,需要使用-u参数
  • stash不会跨越分支边界,在不同分支应用可能产生冲突

安全模式

# 1. 紧急修复bug
git stash save "feature-x 开发中"
git checkout hotfix-branch
​
# 2. 修完bug
git add .
git commit -m "紧急修复"
git checkout feature-branch
​
# 3. 恢复工作
git stash pop
# 如果冲突,手动解决

7. git cherry-pick:复制特定提交到当前分支

cherry-pick的本质

1️⃣ 它到底干了什么?
​
一句话:
​
Cherry-pick = 把某个提交的“改动”复制一份,重新生成一个新提交,贴到当前分支上
​
不是“搬提交”
而是:
​
重放这个提交的 diff
​
2️⃣ 用提交图看本质
​
原始结构:
​
A --- B --- C   (feature)
      \
       X --- Y   (main,当前分支)
​
​
你在 main 上执行:
​
git cherry-pick C
​
​
Git 干的事是:
​
Step 1:计算差异
​
算出:
​
diff = C - B
​
Step 2:应用差异
​
把这个 diff 应用到 Y
​
Step 3:生成新提交
​
创建新提交 C'
​
3️⃣ 结果结构
A --- B --- C   (feature)
      \
       X --- Y --- C'   (main)
​
​
关键点:
​
C' 和 C
​
内容一样
​
哈希不一样
​
历史关系没连起来
​
Git 认为它们是“两个独立提交”
​
4️⃣ 本质对比
命令  本质
merge 连分支历史
rebase  批量 cherry-pick
cherry-pick 复制单个提交

场景示例:

场景 1:紧急修 Bug,不想合整个分支
状态
A --- B --- C --- D   (feature,新功能还没测完)
      \
       X --- Y       (main,线上分支)
​
​
C = Bug 修复
D = 新功能(不稳定)
​
你只想要 C
​
操作
git checkout main
git cherry-pick C
​
结果
A --- B --- C --- D   (feature)
      \
       X --- Y --- C'   (main)
​
​
这就是 cherry-pick 的黄金场景
​
场景 2:跨分支同步小优化
​
你在 dev 分支做了个日志优化提交
测试分支也要
​
git checkout test
git cherry-pick <commit-hash>
​
场景 3:多提交批量搬运
git cherry-pick C D E
​
​
或者区间方式:
​
git cherry-pick B..E
​
​
表示:
​
拿 B 之后到 E 之间的提交

日常坑点:

1️⃣ 只 pick 了“最后一个提交”,忘了依赖提交
场景
A --- B --- C   (feature)
​
​
你只 pick 了 C
​
结果
​
❌ 编译报错
​
❌ 配置缺失
​
❌ 逻辑异常
​
本质
​
C 的变更是建立在 B 之上的
你把**“上层补丁”贴到了“旧底座”上**
​
正确姿势
​
一次性 pick 依赖提交:
​
git cherry-pick B..C
​
​
或显式列出:
​
git cherry-pick B C
​
2️⃣ 后续 merge 分支时,代码被“合进来两次”
场景
​
你:
​
从 feature cherry-pick 到 main
​
之后又 merge feature
​
结果
​
❌ 冲突爆发
​
❌ 或代码重复
​
本质
​
Git 认为:
​
C ≠ C'
​
​
虽然内容相同,但提交历史不相连
​
规避规则
​
用过 cherry-pick 的分支,后续尽量不要再 merge 回来
​
3️⃣ 在旧分支上 pick 新提交
场景
​
你当前 main 不是最新版本
直接 cherry-pick 新提交
​
结果
​
❌ 冲突暴增
​
❌ 引入过时代码
​
标准流程
git checkout main
git pull
git cherry-pick C
​
4️⃣ 批量 pick 中途冲突,不知道 Git 在“暂停状态”
场景
git cherry-pick C D E
​
​
在 D 发生冲突
​
错误操作
​
直接再 cherry-pick
​
或强行切换分支
​
正确操作链
git status
git add .
git cherry-pick --continue
​
​
或放弃本次操作:
​
git cherry-pick --abort5️⃣ 把“实验代码”pick 进主干
场景
​
feature 分支里:
​
一半是调试日志
​
一半是正式修复
​
你直接 pick 最近一次提交
​
结果
​
❌ 测试代码进了生产分支
​
标准习惯
​
先确认提交内容:
​
git log --oneline
git show C
# 确认没问题再 pick

三、多分支开发实战模式

1. 分支策略设计

main/master (保护分支)
    ↑
develop (集成分支)
    ↑
feature/xxx (短期功能分支,合并后删除)
    ↑
hotfix/xxx (紧急修复分支)

2. 日常开发工作流

# 晨间同步
git switch main
git fetch origin
git pull --rebase  # 使用rebase保持线性历史# 开始新功能
git switch -c feature/new-feature
# ... 开发 ...
git add -p
git commit -m "feat: 实现新功能"# 同步主分支更新
git fetch origin
git rebase origin/main  # 将主分支更新整合到当前分支# 完成功能
git switch main
git merge --no-ff feature/new-feature  # 保留合并记录
git branch -d feature/new-feature      # 删除功能分支

3. 代码审查与合并

# 创建合并请求前
git fetch origin
git rebase origin/main  # 确保基于最新代码# 解决冲突
git mergetool           # 使用可视化工具解决冲突
git rebase --continue   # 继续rebase# 推送
git push origin feature/new-feature
# 然后在GitLab/GitHub创建Merge Request

四、常见坑点与解决方案

坑点1:在错误的分支上提交

场景:在main分支开发了新功能,应该提交到feature分支。

# 错误:在main分支提交
git switch main
# ... 开发 ...
git add . && git commit -m "新功能"# 解决方案1:创建新分支并移动提交
git switch -c feature-new  # 新分支包含刚才的提交
git switch main
git reset --hard HEAD~1    # main分支回退,丢弃提交# 解决方案2:使用cherry-pick
commit_hash=$(git log -1 --pretty=format:"%H")
git switch -c feature-new
git cherry-pick $commit_hash
git switch main
git reset --hard HEAD~1

坑点2:提交了敏感信息

场景:不小心提交了密码、密钥等敏感信息。

# 如果还没推送
git rm --cached config/secrets.yml
echo "config/secrets.yml" >> .gitignore
git commit --amend
​
# 如果已经推送
# 1. 立即更改密码/密钥
# 2. 使用BFG或git filter-repo清理历史
# 3. 强制推送(需要团队协调)
git push --force-with-lease

坑点3:rebase冲突地狱

场景:rebase过程中出现大量冲突,陷入困境。

# 错误处理:盲目继续
git rebase main
# 冲突...
git add . && git rebase --continue  # 可能引入错误# 正确做法:理解rebase过程
git rebase main
# 遇到冲突时:
# 1. 查看冲突文件
git status
# 2. 解决冲突
git mergetool
# 3. 标记已解决
git add resolved-file.js
# 4. 继续
git rebase --continue# 如果想放弃
git rebase --abort

坑点4:强制推送的灾难

场景:使用git push --force覆盖了团队的工作。

# 永远不要这样做:
git push --force
​
# 相对安全:
git push --force-with-lease  # 检查远程是否有其他人推送# 最佳实践:避免强制推送
# 1. 使用保护分支
# 2. 通过PR/MR合并代码
# 3. 使用revert而不是reset

坑点5:stash遗忘症

场景:暂存了工作,几天后忘记了。

# 预防措施:描述性保存
git stash save "feature-login: user auth implementation $(date +%Y-%m-%d)"# 每日检查
alias gsl='git stash list'# 设置提醒(在.bashrc或.zshrc中)
export PROMPT_COMMAND='stash_count=$(git stash list 2>/dev/null | wc -l); if [ $stash_count -gt 0 ]; then echo "你有 $stash_count 个暂存的修改"; fi'

五、高级技巧与内部原理

1. 理解.git目录结构

.git/
├── HEAD                    # 当前所在分支
├── config                  # 仓库配置
├── index                   # 暂存区
├── objects/               # Git对象存储
│   ├── [0-9a-f][0-9a-f]/  # 对象文件
│   └── pack/              # 打包的对象
├── refs/
│   ├── heads/             # 分支指针
│   ├── tags/              # 标签指针
│   └── remotes/           # 远程跟踪分支
├── logs/                  # 引用日志
└── hooks/                 # 钩子脚本

2. 使用reflog恢复误操作

# 查看所有操作历史
git reflog --date=iso
# 输出示例:
# abc1234 HEAD@{2023-10-01 10:30:00}: commit: 添加新功能
# def5678 HEAD@{2023-10-01 09:15:00}: checkout: 从main切换到feature# 恢复误删的分支
git branch feature-lost HEAD@{2}
​
# 恢复hard reset前的状态
git reset --hard HEAD@{5}

关键点:reflog记录了HEAD的所有移动,是Git的"时间机器",默认保存90天。

3. 二分查找定位问题

# 找到引入bug的提交
git bisect start
git bisect bad HEAD          # 当前版本有问题
git bisect good v1.0.0       # 这个版本没问题# Git会自动切换到中间提交,你测试后标记:
git bisect good              # 这个提交没问题
# 或
git bisect bad               # 这个提交有问题# 直到找到第一个坏提交
git bisect reset             # 结束二分查找

六、团队协作最佳实践

1. 分支保护策略

# GitLab示例 (.gitlab-ci.yml)
workflow:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: never  # 禁止直接推送到main

# 分支保护设置:
# - 要求至少2个批准
# - 要求CI通过
# - 禁止强制推送
# - 要求线性历史

2. 提交信息规范

# 使用Conventional Commits
<type>(<scope>): <subject>
​
<body>
​
<footer>
​
# 类型:
# feat:     新功能
# fix:      修复bug
# docs:     文档更新
# style:    代码格式
# refactor: 重构
# test:     测试
# chore:    维护

3. 代码审查清单

## 合并前检查### 代码质量
- [ ] 编译是否通过?
- [ ] 测试是否通过?
- [ ] 是否有明显的性能问题?
- [ ] 是否符合编码规范?
​
### Git相关
- [ ] 提交信息是否清晰?
- [ ] 是否包含不相关的修改?
- [ ] 是否基于最新主分支?
- [ ] 是否有合并冲突?
​
### 业务逻辑
- [ ] 是否处理了边界情况?
- [ ] 是否有安全风险?
- [ ] 是否有文档更新?

4. 自动化检查脚本

#!/bin/bash
# pre-commit-check.shecho "=== 提交前检查 ==="# 1. 检查是否有未跟踪的敏感文件
sensitive_files=$(git status --porcelain | grep -E ".(key|pem|env|secret)" || true)
if [ -n "$sensitive_files" ]; then
    echo "❌ 发现敏感文件:"
    echo "$sensitive_files"
    exit 1
fi# 2. 检查提交信息格式
if [ -n "$1" ]; then
    if ! echo "$1" | grep -qE "^(feat|fix|docs|style|refactor|test|chore)((.+))?: .+"; then
        echo "❌ 提交信息格式不正确"
        echo "格式应为: <type>(<scope>): <subject>"
        echo "示例: feat(auth): 添加用户登录功能"
        exit 1
    fi
fiecho "✅ 检查通过"

七、总结

在团队开发密集,多分支开发的瀑布式开发的场景中,新建分支、合并分支、解决代码冲突等属于高频操作,特别是需求涉及到文件修改较多多,commit、push和代码cr会被多个文件分散注意力,很容易把一些误操作的代码变更合并到master分支上,从而酿成生产事故。因此在日常开发中,每次comit 和push 代码时,需要明确自己提交的文件内容,特别是不要把本地debug修改的一些配置文件内容提交上去,此外代码cr时,要认真基于代码的模块逐个确认,切不可有在预发环境测试过没问题了就麻痹大意,在发布上线之前,最好把master分支最新代码合并到发布分支,然后发布预发观察验证系统的功能是否正常,错误日志是否正常等。