git原理和基本使用

7 阅读12分钟

这篇文章仅记录了本人在学习git时遇到的难以理解的指令,并且用图示表示出了不同指令的工作流。文中未详细介绍的指令可查看其他的文章。

Git 的工作区域

本小节内容来自这篇文章

image.png Workspace:工作区,就是平时进行开发改动的地方,是当前看到最新的内容,在开发的过程也就是对工作区的操作

Index:暂存区,当执行 git add 的命令后,工作区的文件就会被移入暂存区,暂存区标记了当前工作区中那些内容是被 Git 管理的,当完成某个需求或者功能后需要提交代码,第一步就是通过 git add 先提交到暂存区。

Repository:本地仓库,位于自己的电脑上,通过 git commit 提交暂存区的内容,会进入本地仓库。

Remote:远程仓库,用来托管代码的服务器,远程仓库的内容能够被分布在多个地点的处于协作关系的本地仓库修改,本地仓库修改完代码后通过 git push 命令同步代码到远程仓库。

Git 的工作流程

  1. git init之后会出现一个.git的文件夹,结构如下:

    image.png

    HEAD文件中存储的是当前分支

    refs文件夹中存储的是所有分支

  2. 在.git的同级目录创建a.txt,内容为111。

  3. git add a.txt之后发现objects文件夹里面多了一个d1文件夹,并且里面有一个文件,文件的内容为111二进制编码之后的内容。此处的d1和d06ad...是根据文件内容计算出来的一个hash值。

    image.png

  4. git commit -m "first"之后发现又多了两个文件夹,分别是: image.png image.png

    第一个文件的类型为:tree,存储的内容为: image.png

    第一个文件的类型为:commit,存储的内容为: image.png

  5. 目前为止,git的结构可以表示为:

image.png

  1. 如果有新的commit,则结构为:

image.png

Git 的基本操作

git add

git commit

git stash

「将工作区 / 暂存区的未完成修改临时“打包” 存入stash栈」,让工作区回到干净状态(与当前分支的最新提交一致);后续可随时从stash栈中恢复这些修改,完美解决 “切换分支/拉取代码/合并分支时,工作区有未完成修改不想提交(提交半成品不规范)”的问题。

语法:

# 1. 暂存未提交修改(推荐写法,Git 2.13+) 
git stash push [-m "备注信息"] # -m 给stash加备注,便于区分多个stash 

# 2. 查看stash栈中的所有暂存记录 
git stash list 

# 3. 恢复最新的stash修改(恢复后保留stash记录) 
git stash apply 

# 4. 恢复最新的stash修改(恢复后删除该stash记录,最常用) 
git stash pop 

# 5. 恢复指定的stash修改(如stash@{1}) 
git stash apply stash@{n} # n是索引,0=最新,1=上一个,依此类推 
git stash pop stash@{n} 

# 6. 查看某个stash的具体修改内容 
git stash show stash@{n} # 简洁版:仅显示文件变更 
git stash show -p stash@{n} # 详细版:显示具体代码行修改 

# 7. 删除指定的stash记录 
git stash drop stash@{n} 

# 8. 清空所有stash记录 
git stash clear 

# 9. 暂存时包含未跟踪文件(新创建的文件,默认不暂存) 
git stash push -u # -u = --include-untracked 

# 10. 暂存时包含忽略文件(.gitignore中的文件,如node_modules) 
git stash push -a # -a = --all 

# 11. 从stash创建新分支(恢复时冲突,避免覆盖当前修改) 
git stash branch <新分支名> stash@{n}

git merge

将源分支(如feature)的全部提交修改整合到目标分支(当前分支,如master),最终生成「快进提交」(线性历史)或「merge 提交」(保留分支合并记录)。

前提场景:

在master分支上已经创建了A、B提交。基于B创建了feature分支,并创建了C提交,如图:

image.png

场景1:此时在master分支上执行:git merge feature,则会直接把master指针移到feature最新提交,无额外merge节点。

场景2:如果master和feature都有新的提交,如图:

image.png

此时在master分支上执行:git merge feature,合并时生成专门的merge提交(F),保留分支历史。

image.png

git rebase

变基操作,这里的基指的是基底(一个分支的生长起点 / 地基)。git rebase是把一个分支(如 feature)的所有提交,从原来的“基底提交”上“剪下来”,按顺序“粘贴”到目标分支(如 main)的最新提交后面,生成提交副本(原提交保留但无分支指向),最终让分支历史从“分叉状”变成“线性”。

假设现在的分支情况是:

image.png

执行变基操作:

1. 切到要变基的分支(feature) 
git checkout feature 

# 2. 执行变基:以master的最新提交为新基底 
git rebase master

变基的逻辑

  1. Git 先找到 featuremaster共同祖先提交(这里是 B);
  2. 提取 feature 从共同祖先(B)到最新提交(E)的所有变更(C、E 的修改内容),保存为临时补丁;
  3. feature 分支的 HEAD 指针移到 master 的最新提交(D)上;
  4. 按顺序把临时补丁贴到 D 后面,生成提交副本 C'、E' (C' 是 C 的副本,E' 是 E 的副本,哈希值与原提交不同);
  5. 原提交 C、E 变成 “游离提交”(无分支指向,后续会被 Git 清理)。

则为: master:A->B->D;feature:A->B->D->C'->E'

image.png

合并变基后的分支:

git checkout master 
git merge feature # 快进合并,master指针移到E'

image.png

git pull

git fetch

git branch

git checkout

操作类型命令格式作用
切换分支git checkout <分支名>切换到已存在的分支
创建并切换分支git checkout -b <新分支名>等价于 git branch <新分支名> + git checkout <新分支名>
切换到指定提交版本git checkout <commit-hash>进入 “分离头指针” 状态,查看历史版本
恢复工作区文件git checkout -- <文件名>撤销工作区对文件的修改(未暂存的修改)
从暂存区恢复文件git checkout HEAD -- <文件名>撤销暂存区 + 工作区的修改
切换到远程分支git checkout -b <本地分支名> origin/<远程分支名>基于远程分支创建并切换本地分支

场景1:假设在master分支上已经创建了A、B提交。基于B使用git checkout -b dev创建了dev分支,并创建了C提交,如图:

image.png

前提场景:

现在只有master分支,并且提交了A、B、C,如图:

image.png

使用git log --oneline可查看每次提交的<commit-hash>

image.png

现在C有bug,怀疑是B引入的,那么想要切换到A分支,可以使用git checkout 1bd2fc6查看当时的代码。

image.png

注意:这是分离头指针操作,可以查看、测试A的代码,但不要直接提交修改(提交的修改会成为「游离提交」,切换回分支后会丢失)。若需修改则可重建分支进行修改。

场景1:如果此时,我在工作区修改了一个a.txt文件,并未提交到暂存区(未git add),但想撤回对a.txt文件的修改,那么可以使用git checkout -- a.txt恢复到最新C提交中的a.txt文件内容。

场景2:如果此时,在工作区修改了a.txt,并且提交到了暂存区(git add,但未git commit),想撤销暂存并恢复文件到提交C的版本,可以使用git checkout HEAD -- a.txt

场景3:同事推送了origin/test分支到远程仓库,你本地没有该分支,需要拉取并切换到该分支开发,执行git checkout -b test origin/test,该操作会在本地新建一个test分支,并且关联到同事的origin/test。

git reset

「移动当前分支的HEAD指针」,并通过不同模式控制是否保留修改——它仅作用于本地仓库,不会自动同步远程分支。

模式HEAD 指针暂存区(Staging)工作区(Working Dir)核心用途风险等级
--soft回退保留修改保留修改修改最近一次提交、合并多次提交低(无数据丢失)
--mixed(默认)回退清空修改保留修改撤销暂存(git add)、重新选择提交文件
--hard回退清空修改清空修改彻底放弃所有未提交 / 已提交的修改高(不可逆)

通用语法:

git reset [--soft/--mixed/--hard] <目标版本> 
# 目标版本可填:HEAD~n(当前提交往前n个)、commit-hash(提交哈希)、分支名

场景前提:

在master分支创建了A,B,C提交历史

image.png

场景1:想要撤销C的提交,但在工作区和暂存区保留C的修改,使用git reset --soft HEAD~1,C成为了游离分支

image.png

场景2:修改了文件a.txt,并且git add了,但此时想撤销暂存区的内容,使用git reset --mixed HEAD,仅暂存区清空,修改回到工作区

场景3:想撤销C的提交,并且放弃暂存区的暂存和工作区的修改,使用git reset --hard HEAD~1,C变成游离提交,并且工作区和暂存区的修改都没清空,不可逆

git revert

本质是「生成反向提交」——它不会删除任何历史提交,而是新增一个与目标提交“反向操作”的提交,抵消目标提交的修改。生成一条全新的反向提交,仅抵消 <提交标识> 这个提交本身做的所有修改;既不会回到该提交的状态,也不会撤销该提交之后的任何提交

通用语法:

git revert [选项] <提交标识> 
# 提交标识:提交哈希(前7位即可)、HEAD~n(如HEAD~1=上1个提交D)、标签名等 
# 常用选项: 
# -m 1:撤销merge提交时必须,指定“以主分支为基准撤销”(后文详解) 
# --no-edit:跳过编辑器,使用默认提交信息 
# -n:仅修改工作区/暂存区,不自动提交(适合批量撤销)

场景前提:

在master分支创建了A,B,C提交历史

image.png

场景1:想撤销B的提交,但保留C的提交,执行git revert B提交的hash。会生成一个新的提交D,这个D撤销了B所做的事情

image.png

场景2:把feature分支(提交链 A→B→E→F)merge到master分支,生成merge提交M(master: A→B→C→D→M)。

image.png

但merge后发现feature有严重bug,需要撤销M,让master回到merge前的D状态。

此时merge提交有两个父节点

  • 父节点 1(-m 1):主分支(master)的最新提交(D);
  • 父节点 2(-m 2):被合并分支(feature)的最新提交(F)。

撤销 merge 提交时,必须用 -m 指定 “以哪个父节点为基准撤销”(通常选 -m 1,即回到主分支的状态)。执行git revert M提交的hash -m 1,生成新的提交N(N 是反向提交,抵消M,代码回到D的状态)。 image.png

git cherry-pick

把一个/多个提交(来自任意分支)从原分支 “复制” 到当前分支的最新提交后,生成提交副本(哈希值与原提交不同),且完全不影响原分支的提交历史。

通用语法:

# 基础语法:复制单个提交 
git cherry-pick <提交哈希>

前提场景:

image.png

场景:把feature分支的E提交复制到master分支,不合并 D/F。

操作步骤:

# 1. 切换到目标分支(要把提交复制到的分支,这里是main) 
git checkout main 

# 2. 确保main分支是最新的(拉取远程最新) 
git pull origin main 

# 3. 查看feature分支的提交记录,获取E的哈希(前7位即可) 
git log --oneline feature 
# 输出示例: 
# f123456 (feature) F: 完善功能 
# e567890 E: 修复支付bug (要复制的提交,记哈希 e567890) 
# d789012 D: 新增功能 
# b123456 B: 基础功能 
# a789012 A: 初始提交 

# 4. 执行cherry-pick,复制E提交到main分支 
git cherry-pick e567890 # 加 -x 可保留原提交信息:git cherry-pick -x e567890 

# 5. 若无冲突,Git会自动生成新提交 E'(哈希与E不同) 
# 终端提示:[main 9abcdef] E: 修复支付bug 1 file changed, 1 insertion(+) 

# 6. 推送新提交到远程main分支 
git push origin main

最后结果:

image.png

git reflog

它记录了本地仓库中HEAD指针(以及各分支指针)的所有移动轨迹(比如commit/checkout/reset/merge/rebase 等操作),哪怕提交被reset删掉、分支被误删、提交变成游离状态,都能通过reflog找回。

通用语法:

# 基础用法:查看 HEAD 指针的所有操作记录(最常用) 
git reflog 

# 查看指定分支的指针操作记录(比如查看 main 分支的操作) 
git reflog show <分支名> # 例:git reflog show main

场景1:找回误执行git reset --hard删除的提交。

在main 分支执行git reset --hard HEAD~1,误删了提交I(提交链从A→B→C→H→I 回退到A→B→C),需要找回 I 提交。

# 1. 执行 git reflog,找到误操作前的提交哈希(比如 I 的哈希是 e123456git reflog # 输出中找到:e123456 HEAD@{1}: commit: I: a.txt新增I 

# 2. 恢复到该提交(用哈希或索引均可) 
git reset --hard e123456 #用提交哈希(推荐,精准)

场景2:找回误删除的本地分支

执行git branch -D feature误删了本地feature分支,该分支的最新提交是F(哈希f789012),需要恢复这个分支。

# 1. 查看 reflog,找到 feature 分支最后一次操作的提交哈希 
git reflog # 输出中找到:f789012 HEAD@{5}: checkout: moving from main to feature 

# 2. 基于该提交重新创建 feature 分支 
git checkout -b feature f789012

# 3. 验证恢复结果 
git branch # 输出:* feature → 分支恢复 
git log --oneline feature # 输出 feature 分支的提交链 → 完整恢复