Git 版本控制实战:从本地仓库到团队协作
📝 笔记摘要: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把暂存区打包成一个 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 两项就说明装好了。
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仓库级别的配置,远程地址、用户信息、分支追踪关系都写这儿
HEAD当前在哪个分支,本质是一个文本文件
objects/所有文件内容和提交对象都在这,按哈希分目录存放
refs/分支和标签的指针
logs/HEAD 和分支指针的移动日志,git reflog读的就是它
hooks/钩子脚本目录,可以挂 pre-commit 做提交前检查
index暂存区的实际文件,git add就是往这写
这里能看出 Git 的一个设计:暂存区是一个真实存在的文件,叫 index。所以「暂存区能撤销」不是比喻,它就是磁盘上的一份快照,git reset 是把它换回旧版本。
7. 分支
分支和 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(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 仓库:
然后在 VCS → Git → Remotes 里添加远程仓库连接信息:
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. 撤销与回退
改错了还没 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 里装个 .ignore 插件,新建文件时可以直接选「新建 gitignore 过滤文件」,它会把常见目录按类型分好,比手写省事:
# 编译输出
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,你在自己的仓库里审完再合并进主分支。开源项目几乎都靠这套流程协作。
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。
