【05】Git入门

20 阅读15分钟

[26/3/17]Git入门

【【GeekHour】一小时Git教程】 www.bilibili.com/video/BV1HM…

基础配置

以下配置只需要执行一次

  1. 用户名配置:git config --global user.name "用户名"
  2. 邮箱配置:git config --global user.email 邮箱地址
  3. 保存用户名和密码:git config --global credential.helper store

创建仓库

  1. 本地创建一个新仓库:git init
  2. 从远程克隆一个仓库:git clone 仓库地址 (仓库地址结尾是.git)

基础概念

三个区:

  1. 工作区
  2. 暂存区
  3. 本地仓库

一般的工作流:你修改了一些文件,先从工作区 add 到暂存区,等你工作到差不多了,add 的内容足够多了,然后再用 commit 提交到本地仓库。

文件的四种状态

  1. 未跟踪( Untrack ):新创建的,都没有 add 的文件
  2. 未修改( Unmodified ):add 完成之后没有修改的
  3. 已修改( Modified ):add 之后修改的
  4. 已暂存( Staged ):刚刚 add 的

基础操作

  1. git status :查看仓库状态
  2. git add 文件名 :将文件加入暂存区(文件名可以使用通配符或者文件夹,例如 . 表示当前文件夹,意思就是把当前文件夹下的所有文件都提交到暂存区)
  3. git commit -m "提交信息" :将暂存区的文件提交到仓库
  4. git log [--oneline] :查看提交记录,可以获取到版本 ID !

回退版本 reset

版本 ID 通过 log 命令查看,还有一个特殊的版本 ID HEAD 表示最新版本,此外还有上一个版本可以用 HEAD^ 或者 HEAD~ 表示;此外还有,前 2 个版本可以这样写:HEAD~2

  1. git reset --soft 版本ID :回退,并保留工作区和暂存区的所有修改内容
  2. git reset --hard 版本ID :回退,并丢弃工作区和暂存区的所有修改内容
  3. git reset --mixed 版本ID :(无参数时默认)回退,只保留工作区的内容,但是丢弃暂存区

查看差异 diff

查看不同区、不同版本、不同分支下的内容差异

不加参数则默认查看的是工作区和暂存区的差异

  1. git diff :默认比较工作区和暂存区的差异
  2. git diff 版本ID :比较工作区和特定版本的差异
  3. git diff --cached :比较暂存区和版本库的差异
  4. git diff 版本ID1 版本ID2:比较指定版本的不同
  5. git diff 版本ID1 版本ID2 文件名 :只比较这一个文件的不同版本的差异
  6. 分支的差异的比较和版本差异比较相似

删除文件 rm

  1. 先用系统命令删除一个文件,然后为了更新这个状态,你需要将这个文件再次 add 到暂存区,就能够更新这个文件被删除的状态了!(或者你直接用 git status 看到它提示你这个文件被删除,需要用什么命令进行更新就好了)
  2. git rm 文件名 :用 git 提供的删除命令进行操作
  3. git rm --cached 文件名 :将这个文件从暂存区移除,但是不删除文件本体

忽略文件 .gitignore

需要忽略的文件:系统或者软件自动生成的文件,编译的中间文件和结果文件,运行时产生的日志缓存临时文件,带有隐私信息的文件

  1. .gitignore 这个文件里,一行写一条忽略规则,且忽略规则是从上到下的顺序进行匹配的
  2. 忽略文件里的空行会被忽略,# 开头的行被视为注释
  3. 最简单的,写一个完整的文件名
  4. 忽略文件夹,必须以 / 结尾,例如:temp/
  5. 可以使用通配符,比如说 *.log
  6. 使用 ** 表示任意的目录
  7. 使用 ! 表示取反

例子:

# 忽略所有的.a文件(默认就是任意文件夹下的)
*.a

# 跟踪所有的 lib.a 文件,即使有前面的忽略规则
!lib.a

# 只当前目录下的TODO文件(因为开头是/)
/TODO

# 忽略任何文件夹下,名为 build 的文件夹(结尾/表示文件夹)
build/

# 忽略特定层级的txt文件,但是doc/xxx/a.txt不会被忽略
doc/*.txt

# 忽略doc文件夹以及其所有子目录下的 pdf 文件
doc/**/*.pdf
# 补充 ** 表示任意层级的文件夹,包括当前目录,类似于 * 匹配0个字符

远程仓库

两种推送方式:

  1. HTTPS 方式:每次推送时都需要验证用户名和密码
  2. SSH 方式:不需要每次都验证,但是需要配置密钥

密钥配置

cd ~ 			# 先回到用户根目录
cd .ssh 	# 没有就自己创建一个
ssk-keygen -t rsa -b 4096 # 生成SSH密钥
# 接下来需要输入密钥文件名和密码
# 生成的 id_rsa 是私钥, id_rsa.pub 是公钥
# 请把公钥的文本内容复制粘贴到 Github 的 SSH keys设置中

# 如果使用的是默认文件名,就结束了,否则需要手动创建一个配置
touch config
# congfig文件内容如下5行
# github
Host github.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/私钥名称

从远程仓库开始

  • 克隆仓库 :git clone 仓库链接

同步仓库

  1. 推送更新内容 :git push <remote> <branch>
  2. 拉取更新内容 :git pull <remote>

关联本地仓库到远程仓库

  1. 增加远程链接:git remote add <shortname> <url> 这里的 shortname 一般是 origin,url 是仓库的链接
  2. 指定分支名字:git branch -M main 把分支名修改为 main (如果你的默认分支名就是 main 那么可以不用运行这一行)
  3. 关联:git push -u origin main 让远程的 origin 仓库和本地的 main 分支关联起来,补充:完整版本 git push -u origin main:main
  4. 查看远程仓库:git remote -v

pull 拉取远程仓库

git pull <远程仓库名> <远程分支名>:<本地分支名> pull 后面的参数可以省略

注意:运行 git pull 之后会自动进行一次仓库合并,如果没有冲突就会合并成功,否则会因为冲突而合并失败,需要手动解决冲突

此外,如果远程分支和本地分支名字相同,可以省略冒号和后面的部分:

git pull <远程仓库名> <相同的分支名>

fetch 拉取修改

fetch 只是获取远程仓库的修改,但是并不会自动合并到本地仓库,需要手动合并。

fetch 实例

现在你在 gitee 网页上手动创建了许可证协议,

此时你的云端仓库会增加一次提交(新增许可证)

那么你的本地仓库就会落后一次提交,

此时先运行 git fetch 获取云端仓库的最新进度

然后再运行 git status 查看云端和本地的进度差异

最后跑一下 git pull 把最新进度拉取下来

VS Code 中的 Git 符号含义


分支 Branch / 合并 merge

  1. 查看分支:git branch
  2. 创建新的分支:git branch 新分支名
  3. 切换到新分支:git checkout 新分支名 或者 git switch 新分支名
    注意!checkout 还可以用于恢复文件,所以如果你有文件和新分支名相同,可能会产生歧义!所以更加推荐新命令 switch
  4. 合并分支:首先你要处于主分支(你的目标是把别的分支合并过来) git merge 新分支名 注意这会自动产生一次提交,会自动打开 vim 要你输入提交信息,可以直接 :wq 使用默认的提交信息;此外,分支合并后并不会自动被删除!
  5. 图形化查看分支合并情况:git log --graph --oneline --decorate --all
  6. 删除已经合并的分支:git branch -d 新分支名 这样删除的必须是已经合并的分支
  7. 强制删除未合并分支:git branch -D 新分支名 大写 D 强制删除
  8. 基于特定版本创建分支:git checkout -b 分支名 版本ID 会修改内容,还会直接为你创建一个新分支!-b 意思是以指定的 版本ID 的代码为基础,创建一个新的分支,并且立刻切换过去
  9. 纯粹查看历史: git checkout 版本ID(这在 Git 里叫 Detached HEAD 分离头指针状态。你可以随便看,看完直接 git switch main 就能安全回来)
  10. 补充:可以在创建分支时就立马切换过去 git switch -c hotfix/fix-time-bug 这里的-c 表示 create,就是现在创建!

合并分支时解决冲突

何时出现冲突?

如果两个分支修改了同一个文件的同一行代码,此时才产生冲突。

也就是运行 merge 的时候,会报错,此时:

  1. 可以用 git status 查看冲突文件
  2. 可以用 git diff 查看冲突的内容

看到冲突内容之后,需要手动修改再提交 (补充说明:Git 会自动把两个分支对同一个部分修改的内容都添加到那个冲突文件里面,你只需要考虑删除哪些就行)

修改后 add commit 就行,会自动完成合并过程

否则,如果你希望中止这次合并,也可以在提交之前使用:git merge --abort


变基(合并) Rease

语法:git rebase 目标分支

注意:需要先切换到自己想嫁接过去的分支,然后目标分支是其他被嫁接分支。

merge 是如同溪流汇聚一样的合并:

rebase 是把分支拆开插入式的合并:

dev 上变基:我是 dev,我迁移到 main 之后

git switch dev

git rebase main

main 上变基:我是 main,我迁移到 dev 之后

git switch main

git rebase dev

注意拆开的位置就是出现分叉的位置!

比较

然后,变基只是将分叉的位置往后移,并不是说,变基之后两个分支就会合并成一个分支了,还是两个分支,会继续分叉。

比如说,我在 mian 上开发,但是 dev 分支总是变基,不过对我来说没有任何影响,它不论怎么变基,都影响不到 main 上的代码。

补充三大操作

这 3 个指令,就是你的“后悔药”和“急救包”:

1. 终极后悔药:git reflog (引用日志)

痛点场景: 你不小心执行了 git reset --hard,或者 Rebase 彻底搞砸了,分支乱成一锅粥,git log 里连之前的提交记录都找不到了。是不是觉得代码彻底灰飞烟灭了?

神级破解: 敲下 git reflog。 这个命令会记录你在本地仓库执行过的每一次导致 HEAD 指针移动的操作(包括你 reset 掉的、删掉的那些 commit,只要垃圾回收还没触发,它全记着)。 找到你搞砸之前的那次操作的 ID(比如 HEAD@{2}),直接 git reset --hard HEAD@{2},你的整个时空线瞬间完美复原!这是 Git 给你留的最强底牌。

2. 工作现场的“时间静止”:git stash (贮藏)

痛点场景: 你正在 dev 分支上写第 54 道力扣题,写了一半,代码根本编译不过。突然,你发现昨天写的自动化 Shell 脚本有个致命 Bug 需要立刻紧急修复(需要切回 main 拉个 hotfix 分支)。但是你现在的破烂代码既不能 commit(因为没写完),如果不 commit 又切不了分支(Git 会报错拦住你)。

神级破解:

  • git stash:瞬间把你当前工作区和暂存区所有没写完的代码“打包塞进床底”,你的工作区瞬间变得像新克隆下来一样干净。
  • 现在你可以从容地切换分支去修 Bug 了。
  • 修完 Bug 回到 dev 分支,敲击 git stash pop:把你塞在床底的破烂代码原封不动地掏出来,继续刚才的思路接着写!

3. 隔空取物(外科手术式合并):git cherry-pick (摘樱桃)

痛点场景: 你的同学在 feature-A 分支上提交了 5 个 commit。你所在的 main 分支只需要他的第 3 个 commit(比如那是一个 CMake 配置的修复),但你极其不想要他的其他 4 个烂代码。这时候绝对不能用 merge!

神级破解: 在 main 分支上,直接敲击:git cherry-pick <他第3个commit的ID> Git 会极其精准地像狙击枪一样,只把那一个特定的 commit 抓过来,复制一份强行塞进你的当前分支里。这在大型开源项目(比如你以后要去提 PR 的 ncnn)中,提取特定的 Bugfix 极其常用!

Git 工作流

例如:

  1. GitFlow 模型
  2. GitHubFlow 模型

简述一下 GitFlow 工作流 (Git Branching Model)

  1. 原则 1: 主分支永远是可以立马执行得到结果的:main 分支保持 Production Ready(生产环境可用)
  2. 原则 2: 开发新功能就创建一个新分支,开发完合并回去:Feature Branch(功能分支)开发模式。此外,用这样的命名法:feature/add-kalman-filter(开发卡尔曼滤波新功能)或 feature/auto-script。在这个分支上代码怎么乱、怎么崩溃都没关系,完全不污染 main
  3. 原则 3: 维护一个专门用于修复 bug 的分支,修复完合并:Hotfix Branch(热修复分支) 。如果突然发现一个数据越界 Bug。立马从 main 拉出一个 hotfix/fix-array-out-of-bound 分支,修好测试没问题后,立刻 mergemain
  4. 合并分支的时候需要好好处理冲突:Resolve Merge Conflicts

细节补充:什么时候用 merge 什么时候用 rebase?

Rebase 最经典的用途:“让我的功能分支基于最新的主分支开发”。

假设这样一个场景,你在开发新功能(main 分支 和 featrue 分支);

但是你的老板要你做一下演示,所以直接 switch 到 main:

git switch main # 切换到主分支

不过你在做演示的时候发现了一个 BUG,所以必须要解决,

于是你创建了一个 fixhot-bug-name 分支,并在在这个分支上修复 BUG:

git branch fixhot-bug-name # 新建分支

git switch fixhot-bug-name # 切换过去修 BUG

因为 BUG 会影响后续开发,所以必须优先解决,修复完成之后,

使用 merge 合并分支,并且记得需要解决冲突问题:

git switch main # 是别的分支合并过来,所以切到 main

git merge fixhot-bug-name # 合并 BUG 修复之后的分支

此时会面临冲突,需要手动修复,这里可以查看冲突;

git status # 查看哪些文件有冲突

git diff # 查看具体的冲突内容

手动解决冲突,然后继续合并(再次提交一次):

git add . # 解决冲突时会修改内容所以要重新 add

git commit -m "修复-XX-BUG" # 提交成功才算合并完成

最后可以把这个 hotfix 分支删除:

git branch -d fixhot-bug-name # 删除这个分支

此时想起来,你的 feature 分支是基于有 BUG 的版本开发的,所以需要变基:

git switch featrue # 切换过来

git rebase main # 变基

因为这也是一次合并,所以也可能会有冲突,所以需要手动解决,然后:

git add . # 解决冲突时会修改内容所以要重新 add

git rebase --continue # 继续变基

然后就可以继续开发了!

(补充,如果你的分支在 rebase 之前已经 push 过一次,那么很遗憾,你下次 push 的时候必须使用强制推送:git push -f origin feature

最后补充一句:

“绝不要对已经 Push 到远程公共仓库(且有别人在用)的分支执行 Rebase!”

因为 Rebase 的本质是**“篡改历史”**。你在本地怎么篡改自己的 feature 分支都没人管,这叫“打扮干净再出门”。

但如果你把 main 分支 Rebase 了,还强制推送到云端,你实验室的其他同学一拉代码,会发现他们的整个时空线全乱了,这会导致灾难性的代码丢失。

所以你的直觉是对的:Rebase 永远只用在你自己本地还没合并的 feature 分支上,用来去同步 main 的最新代码。 只要遵守这一条,你在 Git 的世界里就可以横着走了。

继续补充:

以后你的每一次修 Bug,都应该是这种肌肉记忆:

  1. git switch main (确保基于最新主分支)
  2. git switch -c hotfix/fix-time-bug (拉出紧急修复分支)
  3. 在 VSCode 里把那几行代码改掉。
  4. git add .git commit -m "fix: 修正xxx问题"

遇到冲突的实例:

  1. git merge 分支名:触发冲突
  2. git status:先看文件情况,看是哪个文件冲突
  3. git diff:再看内容情况,看哪些内容冲突
  4. 打开冲突的文件进行修改
  5. git add .:需要把刚刚修改的文件重新暂存
  6. git merge --continue:继续合并(会弹出一个文本编辑器让你写提交信息,但是默认信息就已经足够了,直接关闭这个文本编辑器就足够了)

最后一步也可以这样(传统做法,continue 更加现代化)git commit -m "Merge branch 'feature/test-conflict' and resolve conflicts":最后再提交一次就好了

出现冲突时也可以放弃合并:git merge --abort

冲突部分的内容实例:

<<<<<<< HEAD
echo "Start LeetCode Program!"

=======
echo "Hello LeetCode!"
>>>>>>> feature/test-conflict
printf "Time: %s\n" "$(date '+%Y-%m-%d %H:%M:%S')"

其中 <<<<<<<======= 是 HEAD 这个分支的人做出的修改

然后 =======>>>>>>> 是 feature/test-conflict 这个分支的人对于同一行做出的修改