3
0
0

Git 版本控制实战:从本地仓库到团队协作

2026-10-19
2026-10-21

📝 笔记摘要:Git 版本控制实战

Git 是分布式版本控制系统,仓库完整地存在每个人手里,团队之间靠 push 和 pull 汇合。这篇顺着工作流往下走:本地仓库怎么建、一次提交经过哪几个区域、分支和标签怎么用、远程仓库怎么配、冲突怎么处理、撤销回退该敲哪条命令。

原笔记里是一份命令清单,命令都在,但没说每条命令在整条链路里的位置。这篇补了几处清单里没有的:.git 目录到底存了什么、rebase 和 merge 什么时候用哪个、--force 和 --force-with-lease 差在哪、已经推给别人的提交为什么不能 reset。

1. Git 是什么

Git 是 Linus Torvalds 在 2005 年为了管理 Linux 内核代码写的分布式版本控制系统,GNU GPL v2 协议。它管的不只是代码,配置文件、部署脚本、文档都能塞进去,所以团队里「代码放 Git、文档放 Wiki」这种割裂在 Git 项目里很少出现。

分布式是理解 Git 的钥匙
  每个克隆下来的仓库都是完整的,带着全部历史,不是只有最新一版
  提交在本地完成,断网照样写代码、照样提交
  团队之间的汇合靠 push 和 pull,不是靠中央服务器分配
提交是一个快照,不是一个差异
  每次 commit 存下的是当时整个项目的状态
  所以历史里能看到任何一个中间版本,不只是最终结果
分支就是一个指针
  它不复制任何文件,只记着「我指向哪个 commit」
  建分支几乎不花时间,这是 Git 比 SVN 顺手得多的根源

Git 和 SVN 的模型差异放在 SVN 那篇单独讲,那边会拿集中式和分布式摆在一起对照。这里先把 Git 自己的链路走通。

2. 三个区域:一次提交经过哪几站

Git 四个区域横向流转图:工作区经过 git add 进入暂存区,暂存区经过 git commit 进入本地仓库,本地仓库经过 git push 进入远程仓库,三段箭头下方分别标注反向的 git restore、git reset、git fetch

这张图是整个 Git 的地图。工作区是你正在改的文件,暂存区是「下一次提交要带走的清单」,本地仓库是真正记进历史的地方,远程仓库是团队共享的那一份。

git add 把工作区的改动放进暂存区
  加进去还能撤回来,暂存区是可反悔的
git commit 把暂存区打包成一个 commit,写进本地仓库
git push 把本地仓库的 commit 推给远程
git fetch 把远程的变动取回来,放进本地仓库,不动你的工作区
git restore 把工作区或暂存区的文件恢复回原样
git reset 移动分支指针,撤掉 commit 或撤出暂存区

四个命令的关系是:add 决定「这次带什么」,commit 决定「记成什么样」,push 决定「给谁看」,fetch 决定「拿别人的回来」。

3. 安装

Windows 上装 Git for Windows,官网 gitforwindows.org,一路下一步。装完在任意目录点右键,能看到 Git GUI Here 和 Git Bash Here 两项就说明装好了。

Windows 文件资源管理器右键菜单截图,菜单中显示 Git GUI Here 和 Git Bash Here 两个 Git 快捷选项

Git Bash 是 Git 自带的 MinGW 环境,能用 Linux 风格命令,路径还是 Windows 的 C:\ 盘。Git GUI 是图形界面,够用但不熟;命令行加 IDEA 的组合最省事。

装完先配身份
  不配的话第一次 commit 会直接报错,因为 commit 里必须记录作者
git config --global user.name "你的名字"
git config --global user.email "你的邮箱"
  这个邮箱要和代码托管平台上的账号邮箱一致,私有仓库的提交关联才认得出
改错了也能改
git config --global --list 看当前全部配置

4. 初始化与克隆

手上还没有代码,用 clone;手上已经有一份代码,想让它开始被 Git 管,用 init。

git init 在当前目录新建一个 Git 仓库
git init    在当前目录初始化
git init 项目名 新建一个目录并在里面初始化
git init -b main 同时把默认分支命名成 main,不指定就是 master
git clone 把远程仓库整个复制到本地
git clone https://gitee.com/账号/仓库.git
git clone --depth 1 https://gitee.com/账号/仓库.git 只拉最近一次提交
  只需要看代码不需要历史时用它,体积能小一个量级
两者的区别
  init 只长出一个 .git 目录,历史从零开始
  clone 下来的目录自带完整历史,而且已经和远程仓库建立了追踪关系

执行 git init 之后,项目根目录会多出一个 .git 文件夹。这个文件夹就是本地仓库本体,它不进任何提交,也不该手动去改。

5. 一次完整的提交

三个命令按顺序来,缺一个环节都容易出岔子。

# 看看现在什么状态,哪些改了、哪些没被跟踪
git status

# 把改动放进暂存区
git add .
git add src/main/java/OrderService.java
git add -p          # 同一个文件改了多处,逐块确认要不要进这次提交

# 检查一下这次要提交什么
git diff --cached

# 打包成一个 commit
git commit -m "fix: 订单超时未释放库存"

commit message 要写清楚
  格式一般是 类型: 简短描述,类型用 feat、fix、docs、refactor、chore 这些
  一行写完,细节展开成正文空一行再写
  以后 git log --oneline 只显示第一行,描述要能独立看懂
不要用 git add . 提交所有东西
  IDE 里那个 Commit 按钮基本等价于 git add .,会把所有改动一股脑塞进去
  调试日志、临时配置、IDE 的 *.iml 都会被带进去
  用 git status 看清楚再 add,宁可多敲两次
git commit -a 也不干净
  它只自动暂存已跟踪文件的修改,新文件还是得手动 add
提交粒度
  一次提交只干一件事,反过来一次干几件事
  出问题时 git revert 只能整条撤掉,混在一起就没法拆

6. .git 目录里到底存了什么

项目根目录下 .git 文件夹的目录结构截图,展开后可见 config、description、hooks、info、objects、refs、HEAD 等子目录和文件

初始化之后 .git 里是这个结构:

config  仓库级别的配置,远程地址、用户信息、分支追踪关系都写这儿
HEAD  当前在哪个分支,本质是一个文本文件
objects/  所有文件内容和提交对象都在这,按哈希分目录存放
refs/  分支和标签的指针
logs/  HEAD 和分支指针的移动日志,git reflog 读的就是它
hooks/  钩子脚本目录,可以挂 pre-commit 做提交前检查
index  暂存区的实际文件,git add 就是往这写

这里能看出 Git 的一个设计:暂存区是一个真实存在的文件,叫 index。所以「暂存区能撤销」不是比喻,它就是磁盘上的一份快照,git reset 是把它换回旧版本。

7. 分支

分支模型图:master、develop、feature/login、hotfix 四条横向分支线,feature 做完合并回 develop,发版时 develop 合并到 master,线上出问题从 master 拉 hotfix 再回合 develop

分支和 tag 的根本区别
  分支可以继续提交、可以合并、可以移动,是活的
  tag 是钉死某个 commit 的书签,推上去别人改不了,适合标版本
  所以 tag 用在发布版本上,分支用在日常开发上
建分支
git branch feature/login    建了但不切过去
git switch -c feature/login  建了并且切过去(Git 2.23 以后的写法)
git checkout -b feature/login 旧写法,效果一样
git branch -a 看本地和远程的全部分支
切分支
git switch feature/login
git switch - 切回上一个分支
删分支
git branch -d feature/login  分支没合进主干时删不掉,得用 -D 强删
git push origin --delete feature/login 删远程分支

7.1 merge 还是 rebase

两个分支都有改动,合并有两种做法,结果不一样。

git merge 生成一个合并提交,把两条历史都保留下来
  历史长成一条线加一个分叉合并节点,谁合的、什么时候合的都查得到
  公共分支上用 merge 最省心
git merge feature/login
git merge --no-ff feature/login 即使能快进也强制留一个合并节点
git rebase 把自己的提交搬过去重放一遍,历史变成一条直线
  提交哈希会变,因为父提交变了
  只在自己还没推出去的分支上用
git rebase develop 把自己的提交搬到 develop 最新位置之后
git rebase -i develop 交互式,可以编辑、合并、删除中间的提交

rebase 之后不能直接 push。因为提交哈希全变了,直接推会被远程拒绝。正确做法是 git push --force-with-lease,它会在推送前检查远程有没有别人的新提交,有就拒绝,避免覆盖掉同事的工作。

7.2 摘单个提交

只想要某个提交里的改动,不要整条分支:

# 把某个提交的内容摘到当前分支
git cherry-pick 8a3f21c

# 摘多个不连续的提交
git cherry-pick 8a3f21c 4b7e9d2

# 遇到冲突解完继续
git cherry-pick --continue

# 放弃这次摘取
git cherry-pick --abort

cherry-pick 是「复制」,不是「移动」。原提交还在原分支上,摘过来的是一个新的提交。

8. 标签

# 在当前提交上打个轻量标签
git tag v1.0

# 在指定提交上打标签
git tag v1.0 8a3f21c

# 打带说明的附注标签,发布版本用这种
git tag -a v1.0 -m "第一个可用版本"

# 看标签详情
git show v1.0

# 推单个标签
git push origin v1.0

# 推所有标签
git push origin --tags

# 删标签
git tag -d v1.0
git push origin --delete v1.0

# 从某个标签拉分支
git switch -c hotfix/v1.0 v1.0

轻量标签 vs 附注标签
  轻量标签就是一个指针,附注标签是带说明和签名的对象
  发版用附注标签,带上说明,将来查这个版本是什么来路
tag 推上去就基本动不了
  要改只能删了重建,那会破坏所有引用这个 tag 的下游
  所以 tag 要在发布那一刻打,不要提前打

9. 远程仓库

远程仓库选择与协议图:上方三栏分别介绍 Gitee、GitHub、自建 GitLab 的特点和适用场景,下方对比 HTTP 加账号密码与 SSH 密钥两种推送协议,并附 SSH 三步配置说明

托管平台就三个常见的:Gitee 国内访问快,GitHub 生态最全,自建 GitLab 代码完全在自己手里。选好了之后,克隆地址有两种形式。

HTTP(S) + 账号密码
https://gitee.com/账号/仓库名.git
  每次推送都要输凭证,CI 里得存 personal access token
SSH 密钥
git@gitee.com:账号/仓库名.git
  配好公钥就直接推,不用再输密码
两种地址随时能切
git remote set-url origin git@gitee.com:账号/仓库名.git
git remote -v 看当前配的是哪个

9.1 SSH 密钥怎么配

# 1. 生成密钥对
ssh-keygen -t ed25519 -C "你的邮箱"

# 2. 打印公钥,复制到平台的「SSH 公钥」设置里
cat ~/.ssh/id_ed25519.pub

# 3. 把远程地址换成 SSH 形式
git remote set-url origin git@gitee.com:账号/仓库名.git

# 4. 测一下通不通
ssh -T git@gitee.com

公钥贴到平台,私钥只留自己电脑上
  私钥泄露等于把仓库权限送人,没有任何补救余地
一把钥匙多平台复用
  同一对密钥可以贴到 Gitee、GitHub、GitLab 三个地方,不用一把平台一把
  嫌乱就单独给工作平台配一把
换机器或者重装系统
  把 ~/.ssh/id_ed25519 拷过去就能用,不用重新生成

9.2 在 IDEA 里接上远程仓库

本地有代码、想推上去的时候,IDEA 走这几步。先在 VCS 菜单里把项目变成 Git 仓库:

IDEA 界面截图,VCS 菜单展开后光标停在 Import Version Control 子菜单的 Create Git Repository 选项上

然后在 VCS → Git → Remotes 里添加远程仓库连接信息:

IDEA 的 Define Remote 对话框截图,Name 输入框填了 origin,URL 输入框留空等待填入远程仓库地址,下方有 OK 和 Cancel 两个按钮

Name 填什么
  默认给 origin,命令行里 git push origin xxx 就是它
  名字可以自己起,起成公司或项目代号更清楚
URL 从平台复制
  仓库页面上有个「克隆/下载」按钮,HTTP 和 SSH 两个地址都在那儿
认证
  推的时候 IDEA 会弹窗要账号密码,或者要 token,填一次就记住了

10. 日常同步

# 第一次把远程仓库取到本地
git clone https://gitee.com/账号/仓库.git

# 只取远程变动,不动工作区,也不自动合并
git fetch origin

# 取回来并且合并进当前分支
git pull origin develop

# 推送当前分支
git push origin develop

# 推送所有分支
git push origin --all

# 清理远程已经删掉的分支引用
git fetch --prune

fetch 和 pull 的差别只差一步 merge
  fetch 只下载,pull 是 fetch 加 merge
  拉下来发现冲突时,fetch 让你自己决定什么时候合,更可控
push 之前先 pull
  别人推过东西你直接推会被拒,报 non-fast-forward
  先 pull 把他的改动并进来,再 push

11. 冲突

两个人改了同一个文件的同一部分,Git 不知道该听谁的,只能把两边都摆出来让你定。

<<<<<<< HEAD
这是你这边改的内容
=======
这是同事那边改的内容
>>>>>>> feature/login

处理步骤:

1. 打开冲突文件,把三段标记之间的内容手动改成最终该有的样子
2. 把 <<<<<<<、=======、>>>>>>> 三行标记全删掉
3. git add 这个文件,标记成「已解决」
4. git commit 收尾
不想手工解,用工具
git mergetool 调三方对比工具,IDE 装了就用 IDE 的
git checkout --ours 文件 冲突这块全用自己这版
git checkout --theirs 文件 冲突这块全用对方那版
一次解完所有冲突
git merge --continue 合并中途不想继续了用 git merge --abort 整个撤销

提示:冲突多说明提交太碎。养成每天 push 一次的习惯,冲突范围就小很多。

12. 看历史

# 提交历史
git log
git log --oneline --graph --all     # 带分支图,一眼看清结构
git log --stat                      # 每次提交动了哪些文件
git log -S "关键字"                 # 搜某段代码什么时候进来的
git log --follow src/Order.java     # 文件改过名的历史也能追
git log [tag] HEAD --grep feature   # 按提交说明筛

# 看差异
git diff                 # 工作区 vs 暂存区
git diff --cached        # 暂存区 vs 上一次提交
git diff HEAD            # 工作区 vs 当前最新提交
git diff main...develop  # 两个分支的差异
git diff --shortstat "@{0 day ago}"   # 今天写了多少行

# 看某一次提交
git show 8a3f21c
git show --name-only 8a3f21c
git show 8a3f21c:src/Order.java        # 那个提交时这个文件长什么样

# 查责任
git blame src/Order.java    # 每一行是谁改的
git shortlog -sn             # 各人提交次数排行
git reflog                   # 指针的移动记录,误删的提交能在这捞回来

这一堆命令在 IDEA 里点几下就有对应的图形入口。Version Control 面板的 Log 标签是提交树,左边是历史,右边是选中提交改动的文件和差异内容,双击某个提交能看到完整 diff。命令行的好处是有图形界面没有的东西,比如 git rebase -i 和 git reflog。

reflog 是后悔药
  它记录了 HEAD 和每个分支指针的每一次移动,连被 reset 掉的提交都在
  git reflog 找到那一条,git reset --hard <commit> 就能回去
  注意 reflog 只存在本地,仓库被彻底删掉就没了

13. 撤销与回退

撤销回退决策卡:六个场景分别对应 git restore、git restore --staged、git stash、git commit --amend、git revert、git reset --hard,下方提示已推送的提交用 revert、回退过头用 reflog 找回

改错了还没 add git restore 文件,把文件恢复成上次提交的样子
add 错了 git restore --staged 文件,文件留在工作区,不进这次提交
提交信息写错了还没推 git commit --amend,覆盖上一次提交
提交内容错了已经推了 git revert <commit>,造一个新提交抵消它
想彻底回到某个点 git reset --hard <commit>,三个区域一起退
改了一半要去拉代码 git stash 收起来,git stash pop 再掏回来

reset 的三个档位区别在「退到哪儿就停」:

git reset --soft <commit> 指针退,暂存区和工作区都不动,改动还在暂存区里
git reset --mixed <commit> 指针和暂存区退,工作区不动,改动回到未暂存状态(默认档)
git reset --hard <commit> 三个全退,工作区里的改动直接消失

提示:--hard 会真删东西,跑之前先 git stash 或者 git status 看一眼。

已经推给别人的提交不能用 reset
  reset 重写历史,队友下次 pull 会发现历史对不上,直接报错
  已经推出去的提交只能用 revert,让它保持原样、另外加一条抵消的提交

14. .gitignore

编译产物、依赖目录、IDE 配置、本地密钥,这些东西不该进仓库。.gitignore 就是一份忽略清单。

IDEA 设置界面的插件市场截图,搜索框输入 ig,搜索结果第一位是 .ignore 插件,右侧面板列出它支持的各类 .gitignore 模板

IDEA 里装个 .ignore 插件,新建文件时可以直接选「新建 gitignore 过滤文件」,它会把常见目录按类型分好,比手写省事:

IDEA 新建文件对话框截图,New 菜单里选中 .ignore file,右侧子菜单展开列出 Bazaar、CVS、Docker、Git、Maven、Node、Scala、Subversion 等几十种 .gitignore 模板,.gitignore file 一项被高亮
# 编译输出
target/
build/
out/
*.class
*.jar
*.war

# 依赖目录
node_modules/
.venv/
venv/

# IDE 配置
.idea/
*.iml
*.ipr
*.iws
.vscode/

# 日志
*.log
logs/

# 本地配置和密钥
application-local.yml
.env
*.pem

.gitignore 只对没被跟踪的文件生效
  已经提交过的文件,后来加进 .gitignore 也不会自动消失
  得先 git rm --cached 文件 从索引里摘掉,再提交一次
想强行加进去
git add -f 文件
已经推上去了再补 .gitignore
  先改 .gitignore,再 git rm -r --cached . 加 git add .,逐个剔掉历史里不该有的东西
  不过仓库历史已经泄出去了,正确做法是重写历史加强制推送

15. IDEA 里的 Git 集成

IDEA 自带的 Git 插件基本覆盖日常操作,常用入口集中在三个地方:

Commit 面板(左下角 Commit 标签)
  列出所有改动,勾选要提交的文件,写 commit message,点 Commit
  改完先点 Update Project 拉别人的,再提交,能避开大部分冲突
Git 日志(Version Control → Log)
  左边是提交树,右边是改动内容,双击某个提交能看到完整 diff
Terminal 里的 Git
  IDEA 内置终端默认就是 Git Bash,命令直接敲,不用切窗口

SSH 密钥这块 IDEA 不用单独配。它调的是系统的 ssh 命令,密钥按上一节说的放在 ~/.ssh/ 下,IDEA 直接就用上了。真要指定用哪个 ssh 可执行文件、或者配跳板机,去 Settings → Tools → SSH Configurations 改,那里的 SSH executable、Port、Private key 都是通用选项。

IDEA 里的 Terminal 比 Windows Terminal 好用
  自带 Git Bash,路径自动定位到项目根目录,字体和编码也不会出问题
图形界面和命令行不冲突
  两边操作的是同一个 .git 目录,混着用完全没问题
  有些操作只有命令行有,比如 git rebase -i、git reflog、git filter-branch

16. Pull Request

不是团队成员想给你的项目提改动,可以走 Pull Request:对方 fork 你的仓库,改完提一个 PR,你在自己的仓库里审完再合并进主分支。开源项目几乎都靠这套流程协作。

Gitee 平台的 Pull Request 页面截图,展示了一个开源项目的合并请求列表,右侧是发起合并请求的表单区域

PR 的价值
  改动进主干之前有人看一眼,代码评审落到了流程里
  每一版改动都有独立讨论记录,结论跟着代码走
自己项目也能用
  哪怕只有两个人,也可以让对方 fork 一下再提 PR,比直接往主干推稳
合并方式
  GitHub / Gitee 上 PR 合并默认会生成一个 merge commit
  想保持线性历史可以选 squash 或者 rebase 合并

💡 补充几个原文没写的:

全局配置和仓库配置
  --global 写进 ~/.gitconfig,对所有仓库生效
  不带参数写进当前仓库的 .git/config,只对这个仓库生效
  同一台机器上给公司项目和开源项目配两套身份,就靠这个区分
换掉某个文件的历史
  密钥或者密码误提交进仓库了,改文件不够,得改历史
git filter-repo --path 密钥文件 --invert-paths
  改完要强制推送,而且得让所有协作者重新克隆,代价很大
仓库太大拉不动
git clone --filter=blob:none 按需下载文件内容,历史照样完整
git gc --aggressive 本地仓库瘦身,能压掉很多松散对象
分支太多看着乱
git branch --merged develop 看哪些已经合进主干了,可以清掉
多账号切换
  git config --global user.email 改全局身份,切项目前记得改
  或者用 git config user.email 只在当前仓库改

🔥 一句话总结:Git 的全部秘密就是工作区、暂存区、本地仓库、远程仓库这四个区域,add、commit、push、fetch 各管一段;分支是能随便建的指针,tag 是钉死的书签;撤销要分清「没推」和「已推」,没推用 reset,已推只能用 revert。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!