前言

Git 是程序员每天都要打交道的工具。回想起最初学 Git 的时候,addcommitpush 三连就敢说自己”会用 Git 了”。结果一遇上冲突、分支混乱、或者不小心 push -f 把同事代码搞没了,才开始真正意识到:会用 Git 和用好 Git,完全是两码事

这篇文章记录我在实际项目中积累的 Git 经验,既是对自己的总结,也希望能帮到正在学习 Git 的朋友。

一、理解 Git 的核心模型

很多人学 Git 一开始就记命令,其实先理解它的数据模型,很多操作就自然懂了。

四个区域

1
2
3
4
5
6
7
工作区 (Working Directory)
↓ git add
暂存区 (Staging Area / Index)
↓ git commit
本地仓库 (Local Repository)
↓ git push
远程仓库 (Remote Repository)
  • 工作区:你正在编辑的文件,任何修改都在这里
  • 暂存区git add 后,改动被”标记”为准备提交
  • 本地仓库git commit 后,改动被永久记录到本地 .git 目录
  • 远程仓库git push 后,本地提交同步到 GitHub / GitLab 等远程服务器

关键认知:Git 是一个内容寻址的文件系统。每个文件、每个目录、每个 commit 都被计算出一个 SHA-1 哈希值作为”地址”。你看到的 commit 1a2b3c4 就是那个 commit 对象的内容哈希。

.git 目录里有什么

一个典型项目的 .git 目录结构:

1
2
3
4
5
6
7
8
.git/
├── HEAD # 指向当前分支
├── refs/ # 分支和标签的引用
│ ├── heads/ # 本地分支
│ └── remotes/ # 远程分支
├── objects/ # Git 对象数据库(commit、tree、blob)
├── index # 暂存区
└── config # 仓库配置

理解了这些,你就会知道 git checkout 本质上是移动 HEAD 指针,git reset 本质上是改变分支引用的指向。

二、日常高频命令(正确用法)

git add

1
2
3
4
5
6
7
8
9
10
11
# 添加单个文件
git add src/main.js

# 添加整个目录
git add src/

# 交互式添加(推荐!可以选择性暂存)
git add -p

# 添加所有改动(慎用,容易把不该提交的也加进去)
git add .

经验:养成 git add -p 的习惯。它会逐个 hunk(代码块)让你确认,避免把调试代码、临时注释一起提交上去。

git commit

1
2
3
4
5
6
7
8
# 基础提交
git commit -m "fix: 修复登录页面样式错乱"

# 提交所有已跟踪文件的修改(跳过 add)
git commit -am "feat: 添加搜索功能"

# 修改上一次提交(还没有 push 的时候)
git commit --amend

提交信息规范:强烈推荐 Conventional Commits 格式:

1
2
3
4
5
<type>(<scope>): <subject>

type: feat | fix | docs | style | refactor | test | chore
scope: 可选,影响范围
subject: 简短描述(50 字以内)

示例:

1
2
3
feat(auth): 添加 JWT 登录认证
fix(ui): 修复移动端导航栏溢出
docs(readme): 更新部署文档

规范的提交信息让 git log 一目了然,也能配合工具自动生成 Changelog。

git pull 的正确姿势

1
2
3
4
5
# 默认行为:fetch + merge(会产生额外的 merge commit)
git pull

# 推荐:fetch + rebase(保持提交历史线性)
git pull --rebase

为什么推荐 rebase? merge 会产生一条 “Merge branch ‘main’ into feature” 的提交,让历史图像意大利面条。rebase 则把你的本地提交”接”在远程最新提交之后,历史保持一条直线。

可以在全局配置中设为默认:

1
git config --global pull.rebase true

git push

1
2
3
4
5
6
7
8
# 推送到默认远程分支
git push

# 首次推送新分支
git push -u origin feature/new-feature

# 强制推送(危险!)
git push --force-with-lease

**永远不要用 git push -f**。用 --force-with-lease 替代,它会在推送前检查远程分支是否被其他人更新过,避免覆盖他人代码。

git remote:管理远程仓库

当你 git clone 一个仓库时,Git 会自动把源地址保存为 origin。但很多时候你需要自己建立远程链接——比如本地已有项目、需要推到不同平台、或者想同时备份到多个仓库。

给本地仓库建立远程链接

如果你在本地用 git init 创建了一个新项目,它还没有远程地址:

1
2
3
4
5
6
7
8
# 查看当前配置的远程仓库
git remote -v

# 添加远程仓库
git remote add origin git@github.com:username/repo.git

# 首次推送(设置上游分支)
git push -u origin main

经验-u 参数会把本地分支和远程分支关联起来,之后就可以直接用 git push / git pull 而不需要每次指定远程和分支名。

多远程仓库:同时推送多个平台

有时候你想把代码同时推送到 GitHub 和 Gitee(或者公司的内部 GitLab),有两种方式:

方案一:添加多个 remote 名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 默认的 GitHub
git remote add origin git@github.com:username/repo.git

# 再添加一个 Gitee
git remote add gitee git@gitee.com:username/repo.git

# 查看所有 remote
git remote -v
# origin git@github.com:username/repo.git (fetch)
# origin git@github.com:username/repo.git (push)
# gitee git@gitee.com:username/repo.git (fetch)
# gitee git@gitee.com:username/repo.git (push)

# 分别推送
git push origin main
git push gitee main

优点:灵活控制推到哪个平台。缺点:需要执行两次 push。

方案二:同一个 remote 配置多个 push URL

这是更优雅的方案——push 一次,自动同步到所有远程:

1
2
3
4
5
6
7
8
9
10
11
# 设置 origin 默认 URL
git remote set-url origin git@github.com:username/repo.git

# 添加第二个 push URL
git remote set-url --add origin git@gitee.com:username/repo.git

# 查看结果
git remote -v
# origin git@github.com:username/repo.git (fetch)
# origin git@github.com:username/repo.git (push)
# origin git@gitee.com:username/repo.git (push)

注意有两个 push URL 但只有一个 fetch URL——push 会同时推送到两个地址,pull 只从第一个地址拉取。

1
2
# 一次 push,同时推送到 GitHub 和 Gitee
git push origin main

常用 remote 操作汇总

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 查看远程仓库信息(包含 fetch 和 push URL)
git remote -v

# 添加新的远程仓库
git remote add <名称> <URL>

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

# 为同一个 remote 增加第二个 push 地址
git remote set-url --add origin <第二个URL>

# 删除某个 push URL
git remote set-url --delete origin <要删除的URL>

# 重命名远程仓库
git remote rename origin upstream

# 删除远程仓库
git remote remove gitee

# 查看某个 remote 的详细信息
git remote show origin

# 删除本地已不存在的远程分支引用
git remote prune origin

典型场景

  1. 从 HTTPS 切换到 SSH

    1
    git remote set-url origin git@github.com:username/repo.git
  2. GitHub + Gitee 双备份:用方案二,一次 push 双平台同步。

  3. 公司项目迁移 GitLab 地址

    1
    git remote set-url origin git@gitlab-new.company.com:team/project.git
  4. 为开源项目添加上游仓库

    1
    2
    3
    4
    5
    # clone 的是你自己的 fork
    git remote add upstream git@github.com:original-author/repo.git
    # 同步上游的更新
    git fetch upstream
    git merge upstream/main

经验:多 remote 推送时,各平台的提交历史必须兼容(通常是同一套 commit)。如果某个平台的历史不一样(比如用 git push -f 搞乱了),会导致另一个平台推送失败。

三、分支策略

GitHub Flow(推荐个人和小团队)

这是最简单实用的策略:

1
main ← feature-branch
  1. main 分支始终可部署
  2. 做任何改动都从 main 拉一个新分支
  3. 分支名用描述性的:feature/user-authfix/navbar-overflowdocs/api-guide
  4. 完成后提 PR,review 后合并回 main

Git Flow(适合有固定发布周期的大项目)

1
2
3
main ← develop ← feature-branch

release ← hotfix

分支类型:

  • main:生产环境代码
  • develop:开发主线
  • feature/*:功能开发
  • release/*:发布准备
  • hotfix/*:紧急修复

我的建议:大多数项目用 GitHub Flow 就够了,Git Flow 分支太多,维护成本高。

分支操作实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建并切换到新分支
git checkout -b feature/payment

# 切换到已有分支
git checkout main

# 查看所有分支
git branch -a

# 删除本地分支
git branch -d feature/old-feature

# 删除远程分支
git push origin --delete feature/old-feature

# 重命名分支
git branch -m old-name new-name

四、合并与变基

merge vs rebase:什么时候用什么

1
2
3
4
5
6
7
8
9
10
11
场景:你在 feature 分支上开发,同事合并了新代码到 main

merge:
main: A---B---C---D
\ \
feature: E---F---M (M 是 merge commit)

rebase:
main: A---B---C---D
\
feature: E'---F' (E' F' 是重新应用的提交)

选择原则

  • rebase:个人分支整理提交历史用。比如 git pull --rebasegit rebase main
  • merge:合并功能分支到主分支用。比如 PR 合并到 main
  • 黄金法则永远不要 rebase 已经推送到远程的公共分支

冲突解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# rebase 过程中遇到冲突
git rebase main
# CONFLICT 出现后:
# 1. 手动编辑冲突文件
# 2. 标记为已解决
git add conflicting-file.js
# 3. 继续 rebase
git rebase --continue

# 如果想放弃 rebase
git rebase --abort

# 查看冲突文件列表
git diff --name-only --diff-filter=U

经验:冲突不可怕,可怕的是不知道怎么处理就乱操作。记住:git status 永远是你的朋友,它会告诉你当前状态和下一步该做什么。

五、踩坑记录与补救措施

1. 提交到了错误的分支

1
2
3
4
5
# 场景:在 main 分支上做了几个提交,应该在新分支上
# 解决:
git branch feature/new-feature # 用当前提交创建新分支
git reset --hard HEAD~3 # main 回退 3 个提交(3 是要移走的提交数)
git checkout feature/new-feature # 切到新分支继续开发

2. commit message 写错了

1
2
3
4
5
# 修改最近一次提交的信息
git commit --amend -m "新的提交信息"

# 如果已经 push 了(仅限个人分支!)
git push --force-with-lease

3. 不小心 git add 了不该提交的文件

1
2
3
4
5
# 从暂存区移除,保留工作区改动
git reset HEAD path/to/file

# 撤销工作区的改动(危险!会丢失修改)
git checkout -- path/to/file

4. 想回到之前的某个版本看看

1
2
3
4
5
6
7
8
# 查看提交历史
git log --oneline --graph --all

# 临时切换到某个提交(进入 "detached HEAD" 状态)
git checkout <commit-hash>

# 回到当前分支
git checkout main

5. “救命!我搞砸了” — git reflog

git reflog 记录了 HEAD 的所有移动历史,是 Git 的”后悔药”:

1
2
3
4
5
6
7
8
git reflog
# 输出:
# 1a2b3c4 HEAD@{0}: commit: fix login bug
# 5d6e7f8 HEAD@{1}: rebase finished
# 9a0b1c2 HEAD@{2}: reset: moving to HEAD~2

# 回到 rebase 之前的状态
git reset --hard HEAD@{2}

经验:只要你的改动曾经被 commit 过(哪怕后来被 reset 了),reflog 都能找回来。默认保留 90 天。

6. .gitignore 不生效

1
2
3
4
5
# 文件已经被 Git 跟踪了,.gitignore 对它无效
# 需要先从缓存中移除:
git rm --cached path/to/file
git commit -m "chore: 停止跟踪某个文件"
# 然后再添加 .gitignore 规则

六、实用技巧

git stash:临时保存工作现场

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 保存当前改动
git stash

# 保存时加说明
git stash push -m "WIP: 支付功能开发中"

# 查看 stash 列表
git stash list

# 恢复最近的 stash
git stash pop

# 恢复指定的 stash
git stash pop stash@{2}

# 丢弃某个 stash
git stash drop stash@{1}

git cherry-pick:摘取特定提交

1
2
3
4
5
6
7
8
# 把其他分支的某个提交"复制"到当前分支
git cherry-pick <commit-hash>

# 摘取多个提交
git cherry-pick <hash1> <hash2> <hash3>

# 摘取一段连续的提交(不含 A,含 B)
git cherry-pick A..B

git bisect:二分法定位 bug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 开始二分查找
git bisect start

# 标记当前为 bad(有问题)
git bisect bad

# 标记某个旧版本为 good(没问题的)
git bisect good <old-commit-hash>

# Git 会自动切换到中间版本,测试后标记:
git bisect good # 或
git bisect bad

# 重复直到找到引入 bug 的提交
# 结束后:
git bisect reset

git alias:偷懒神器

~/.gitconfig 中配置:

1
2
3
4
5
6
7
8
9
[alias]
st = status
co = checkout
br = branch
ci = commit
lg = log --oneline --graph --all --decorate
unstage = reset HEAD --
last = log -1 HEAD
undo = reset --soft HEAD~1

常用 alias 推荐:

1
2
3
4
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.undo "reset --soft HEAD~1"

七、好习惯清单

  1. 提交前先 git diff — 确认自己的改动
  2. git add -p — 选择性暂存,避免垃圾提交
  3. 写规范的 commit message — 6 个月后的你会感谢现在的你
  4. pull --rebase 而不是 pull — 保持历史整洁
  5. --force-with-lease 而不是 -f — 保护同事的心血
  6. 小步提交 — 每个 commit 只做一件事,方便 code review 和回滚
  7. 分支命名规范feature/xxxfix/xxxdocs/xxx
  8. 学会看 git status — 它是你最常用的命令
  9. 定期清理git remote prune origin 清理已删除的远程分支引用
  10. 备份就是 reflog — 只要 commit 过,几乎没有找不回来的代码

总结

Git 的学习曲线确实陡峭,但一旦你理解了它的数据模型(commit → tree → blob),那些”魔法命令”背后的原理就清晰了。

最重要的不是记住多少命令,而是养成好的版本管理习惯。当你把 Git 当作合作伙伴而不是负担时,它会让你的开发流程更加从容。

记住:Git 不会丢你的代码,但你自己可能会。所以,commit early, commit often。

祝编码愉快!🚀