前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于 Git 的那些事

关于 Git 的那些事

作者头像
Chor
修改2020-06-01 17:20:46
1.1K0
修改2020-06-01 17:20:46
举报
文章被收录于专栏:前端之旅前端之旅

主要记录学习 Git 过程中的一些坑。重点在于各种 Git 操作,但也不限于此,一些和 Git 相关的东西都会记录一下。

本文的思维导图如下:

1. Git 安装和升级

安装应该不用多说了,主要讲一下升级。Linux 和 Mac 都有包管理器,升级是很方便的,关键是 windows 并没有这类东西,那么怎么升级呢?

这里首先要通过 git version 查看你当前的 Git 版本是多少,如果版本:

  • <= 2.14.1 :不要多想,老老实实卸载旧版本,安装新版本吧。。。
  • 2.14.2 ~ 2.16.1 :直接 git update 升级
  • >= 2.16.1(2):直接 git update-git-for-windows 升级

通过命令升级的时候可能半天没反应,最后提示你 using proxy as per lookup,如图所示:

2. Git 乱码问题解决方案

以下操作基于 win10。

这个问题有点奇怪,直接使用 Git Bash 是不会出现乱码的,但是使用 windows terminal 后,在 git log 的时候中文会显示为八进制。你的问题可能和我不一样,也许是 git commit 或者 git status 的时候乱码。总之,我们可以统一设置:

代码语言:javascript
复制
git config --global gui.encoding utf-8
git config --global core.quotepath false
git config --global i18n.commitencoding utf-8
git config --global i18n.logoutputencoding utf-8

最后执行:

代码语言:javascript
复制
export LESSCHARSET=utf-8

然后新建一个环境变量,键名 LESSCHARSET,键值 utf-8。(这一步很关键,否则你新开窗口还是会乱码的)

注意:具体的设置可能因为系统(我是 win10)或默认编码不同而不同。我在解决这个问题的时候查看了很多博客,有的地方会说 git config --global i18n.logoutputencoding utf-8 这一步应该是设置为 gbk,不过我这样设置之后连 Git bash 也乱码了……(尽管 win10 的默认编码应该是 gbk 没错),所以在设置上还是得看你自己系统的情况,可能得多试几次。

2020-05-27 更

这个 windows terminal 果然不让人省心,今天发现 ls 指令也会有乱码的情况,而且目录不会高亮。最后在这个 issue 下找到了答案,两种解决方法:

① 编辑 git/etc/bash/bashrc 文件,末尾添加:

代码语言:javascript
复制
export LANG="zh_CN.UTF-8"
export LC_ALL="zh_CN.UTF-8"

② 编辑 windows terminalsetting.json 文件:

代码语言:javascript
复制
"commandline": "D:\\Git\\bin\\bash.exe"
// 改为 
"commandline": "D:\\Git\\bin\\bash.exe -li"

-li 参数可以让 git bash 以正确的配置启动。估计前面所有问题都和这个有关,因为我的 git 没有安装在默认路径,可能导致某些配置读取不了,出现各种奇葩的问题。

3. 初始配置

显示配置:

代码语言:javascript
复制
git config -l/--list
git config --global -l/--list

进行配置:

代码语言:javascript
复制
git config --global username 'xxx'
git config --global useremail 'xxx'

这里的参数可以是 local(对本仓库生效),global(对登录用户的所有仓库生效) 或 system(对所有用户的所有仓库生效),在优先级上依次递减。

4. 本地仓库操作

4.1 工作区相关

生成项目文件夹(本地仓库),同时生成记录重要信息的隐藏文件夹 .git

代码语言:javascript
复制
git init project

查看项目文件状态,红色表示文件还在工作区,绿色表示文件还在暂存区,不显示表示都到了历史区:

代码语言:javascript
复制
git status

撤销本地文件的修改,使之回退到最近一次 git add 的状态:

代码语言:javascript
复制
git checkout -- <file>

上面这个命令和切换分支非常像(多了 --),容易混淆。为此,git 2.23 之后引入了专门的回退命令和切换分支命令,用来分离 git checkout 的职责。

我们可以用下面命令进行工作区文件的回退:

代码语言:javascript
复制
git restore <file>

有时候我们在工作区做一些修改的时候,可能临时接手了其它更紧急的任务,那么这时候就得把当前所做的更改做一个状态保存:

代码语言:javascript
复制
git stash

在任务完成后,想要恢复之前保存的状态,则可以:

代码语言:javascript
复制
git stash apply
// 或者
git stash pop
4.2 暂存区相关
提交文件到暂存区
代码语言:javascript
复制
git add 
git add .
git add -A 
删除暂存区的文件
代码语言:javascript
复制
git rm --cached 
git rm --cached -r .
git rm      // 删除工作区和暂存区的文件 
修改工作区和暂存区的文件名
代码语言:javascript
复制
git mv  
git commit -m'Change the file name'
撤销对暂存区的文件提交

当我们修改文件并进行了 git add 之后,就不能再通过 git restore <file> 直接撤销本地文件的修改了。但可以先通过 git reset HEAD <file> 撤销对暂存区的文件提交(某个文件不提交了),再 git restore <file>

代码语言:javascript
复制
git reset HEAD <file>

同样的,我们可以用新命令代替:

代码语言:javascript
复制
git restore --staged <file>

这两个命令借助于 HEAD 进行恢复,因此运行在“至少提交了一次”的前提下(若没有提交则不存在 HEAD,使用命令是会报错的)。不过忘了也没关系,Git 会根据操作给我们相关提示的。

当我们不加 <file> 参数的时候,会撤销暂存区所有的文件提交(所有文件都不提交了),其实相当于恢复到了最近一次 commit 的状态:

代码语言:javascript
复制
git diff HEAD
比对差异

在急着将工作区的文件提交到暂存区之前,可以先比对两个区的差异:

代码语言:javascript
复制
git diff

如果目标是具体文件,也可以加上参数:

代码语言:javascript
复制
git diff --<file>

同样,在急着将暂存区的文件提交到历史区之前,可能需要先将暂存区与最近一次 commit 进行比对,看看修改了什么东西(--cached 表示暂存区):

代码语言:javascript
复制
git diff --cached
4.3 历史区相关
提交文件到历史区
代码语言:javascript
复制
git commit -m'xxxxx'
git commit
查看 commit 信息
代码语言:javascript
复制
git log
git log --oneline   // 查看简略的提交信息
git log --graph   // 查看图示提交信息
git log --n4        // 查看最近四次的提交信息
git log --all      // 查看所有分支上的提交信息
git reflog         // 查看包含回退在内的提交信息
修改最近一次 commit 信息
代码语言:javascript
复制
git commit --amend
修改以前的 commit 信息

如果要修改以前的 commit 信息,就需要变基(rebase)了。找到要修改的 commit 的前一次 commit id

代码语言:javascript
复制
git rebase -i <commit id>

我们要修改的是 3291b88 这个 commit 的信息,将前面的 pick 指令改为 reword 指令,并保存:

来到另一个界面,在这里进行修改,之后保存退出即可:

合并多个连续 commit

假设想要合并中间的多个连续 commit(2.txt 到 6.txt):

基于这几个 commit 的前一次 commit id 进行变基:

代码语言:javascript
复制
git rebase -i ec7eeb3

在这里会列出第一个 commit 往后的所有 commit,将想要合并的 commit 前面的指令改为 squash 指令,并保存:

编辑 commit 信息,这个信息会成为合并后的总 commit 信息,保存退出:

可以看到,commit 合并成功了:

合并多个非连续 commit

针对上面合并之后的结果,如果我们想要合并第一个和最后一个 commit,那么可以基于第一个 commit 进行变基:

代码语言:javascript
复制
git rebase -i ec7eeb3

rebase 的交互式界面中,只会显示第一个 commit 往后的 commit,但是我们这里需要用到第一个 commit,所以手动写入一个 pick ec7eeb3,并把第三个 commit 调整放到它后面,squash 有压入、塞入的意思,这里可以理解为把第三个 commit 塞入第一个中:

保存后会弹出一些提示,通过 git rebase --continue 再次回到交互式界面中。后面的步骤就和之前一样了:

再来打印看看,发现合并成功了:

版本回退
代码语言:javascript
复制
git reset --hard HEAD^ / HEAD~n
git reset --hard <commit id>
比对版本差异
代码语言:javascript
复制
git diff <commit id1> <commit id2>
4.4 分支相关

在介绍具体的指令之前,首先要搞清楚 HEADmasterdev 三个指针的作用(不考虑版本回退的情况):

  • 在一开始,默认只有 master 分支(主分支),master 指针始终指向主分支的最近一次 commit,并在每次出现新的 commit 时向前推进;
  • 如果新建分支,则会出现 dev 指针,它始终指向子分支的最近一次 commit,并在每次出现新的 commit 时向前推进;
  • 不管当前处于哪个分支,HEAD 指针都指向该分支的最近一次 commit

假设最初只有一个主分支,第一次 commit 新建文本文件,第二次 commit 修改文件内容。下面结合例子和示意图介绍指令。

查看所有分支
代码语言:javascript
复制
git branch -av

此时只有一个主分支,HEADmaster 指针都指向最近一次 commit

PS:这里一定要注意,判断当前处于哪个分支看右边括号的内容即可,HEAD -> master 表示的并不是 HEAD 指针指向主分支,而是像示意图那样。

参数 -a 代表查看本地和远程所有分支,-v 代表查看分支的同时显示最后一次 commit 的相关信息。如果只想查看本地分支,可以:

代码语言:javascript
复制
git branch -v
基于当前分支的最近一次 commit 创建并切换分支
代码语言:javascript
复制
git branch new_branch
git checkout new_branch
/* 或者 */
git checkout -b new_branch 

此时有了子分支,由于它是基于主分支的最近一次 commit 创建的,所以三个指针指向同一个东西,即 HEAD -> new_branch,master

基于某次 commit 创建并切换分支
代码语言:javascript
复制
git branch new_branch 
git checkout new_branch 
/* 或者 */
git checkout -b new_branch 

如果我们是基于第一次 commit 创建分支的,则指针的变动如下:

基于某个分支创建并切换分支
代码语言:javascript
复制
git checkout -b new_branch old_branch
切换分支
代码语言:javascript
复制
git switch new_branch

git 2.23 之后有了专门的切换分支命令。

合并分支
代码语言:javascript
复制
git merge new_branch
删除分支

直接删除指定分支:

代码语言:javascript
复制
git branch -D new_branch

删除前会先让用户确定分支是否已经合并:

代码语言:javascript
复制
git branch -d new_branch
比对分支差异

假设当前提交情况如下:

要比较两个分支的差异,自然而然想到:

代码语言:javascript
复制
git diff master new_branch

不过,前面我们知道,master 指针指向主分支最近一次 commitdev 指向子分支最近一次 commit,正是这两个指针不同才得以将分支区别开来,而指针又是指向 commit,因此其实可以通过比对两个分支最近一次 commit 的差异,进而比对两个分支的差异:

可以看到,虽然两次 diff 的参数不同,但是结果是一样,这是因为比对分支的差异,在本质上就是比对 commit 的差异

基于某次 commit 切换到“分离头指针”状态
代码语言:javascript
复制
git checkout 

此时,我们会到达某次 commit 之后的状态,我们可以从此次 commit 的状态出发,进行一些试探性的修改。由于处在 'detached HEAD' state(“分离头指针”状态),所以修改不是基于任何具体分支的,一旦切换到某个具体分支,所有的修改都会消失。这适用于实验性的修改,可以避免影响其它任何分支。

其实,Git 给出的文字提示也很清楚。我们还可以注意括号里的内容,一开始是 master 分支,后来直接变成了某一次 的 commit id。同时可以看到,HEAD 不再像此前一样指向某个指针,而是分离了

对应的示意图如下:

当然,我们可以执行下面指令:

代码语言:javascript
复制
git checkout -b new_branch

此时就会真正创建一个分支,我们的修改得以保留。实际上就和之前的 git checkout -b new_branch <commit id> 效果一样。

4.5 Git 的三个重要对象(commit、tree、blob)

① 每一次提交会形成一个 commit 对象,根据 id 查看该 commit 对象的类型和内容:

代码语言:javascript
复制
git cat-file -t 'xxxxxx'   
git cat-file -p 'xxxxxx'

② 该 commit 对象的 tree 对象是一棵文件结构树,它记录了提交时的文件结构快照(有哪些文件夹、哪些文件),根据 id 查看该 tree 对象的类型和内容:

代码语言:javascript
复制
git cat-file -t 'xxxxxx'    // tree
git cat-file -p 'xxxxxx'

tree 对象包含了 tree 对象和 blob 对象,分别指代文件夹和文件,我们可以进一步查看其类型和内容:

代码语言:javascript
复制
git cat-file -t 'xxxxxx'    // tree or blob
git cat-file -p 'xxxxxx'

注意最外层、最初始的 tree 是文件结构树,此后的 tree 是文件夹树。比方拿下面这张图来看,第一棵树是文件结构树,内容是各个文件夹和文件,文件直接指代 blob 对象,而文件夹则指代另一棵树/ tree 对象。

5. 远程仓库操作

5.1 公私钥配置

查看本地公私钥配置情况:

代码语言:javascript
复制
ls -al ~/.ssh

生成公私钥:

代码语言:javascript
复制
ssh-keygen -t rsa -b 4096 -C 'your_email@example.com'

之后本地保存私钥,并在 GitHub 个人设置里配置公钥。

5.2 新建项目

第一步,先到 GitHub 创建一个远程仓库,拿到仓库对应的 ssh 或者 http 地址

第二步,创建本地仓库:

代码语言:javascript
复制
git init                          // 初始化当前文件夹为本地仓库
git remote add origin <Address>  // 本地仓库与远程仓库建立关联
git remote -v                   // 查看与本地仓库关联的远程仓库

最后,本地提交一些修改,并推送到远程:

代码语言:javascript
复制
git add -A  
git commit -m'xxxx'    
git push origin master   

PS:注意,如果之前建立远程仓库的时候勾选新建 README.md 文件,并且后来采用的不是 git clone 的方法,那么在这里是不能直接推送到远程的,因为本地与远程内容不一样(本地没有 README.md 文件),直接推送的话会报错。必须得先 git pull 把远程的东西拉下来同步,之后再 git push

5.3 克隆项目

在前面的第二步,可以直接 git clone ,这样就不需要手动初始化本地仓库、建立远程连接以及同步等:

代码语言:javascript
复制
git clone <Address>

不过,这只会克隆远程仓库的 master 分支。以后如果我们想要克隆远程的其它分支,可以:

代码语言:javascript
复制
git  branch  remote_branch_name  origin/remote_branch_name

或者是直接切换到其它分支,Git 默认会帮我们建立一个追踪(track)远程仓库对应分支的分支:

代码语言:javascript
复制
git switch remote_branch_name

如果一开始就只想要克隆某个分支,可以:

代码语言:javascript
复制
git clone -b remote_branch_name <Address>

还有一种情况是,我们直接在本地建立了一个分支,忘记将它和对应的远程分支建立关联,这时候怎么办呢?可以这样:

代码语言:javascript
复制
git branch --set-upstream-to=origin/remote_branch  remote_branch 

6. 多人开发

假设老大(项目负责人)新建远程仓库 X,并添加 AB 两人作为仓库的 contributor,那么 AB 就可以各自 clone 形成一个本地仓库,在做一些修改之后直接 push 到 X 仓库。

6.1 修改不同文件/同一文件不同区域

A 和 B 在同一个分支 remote_branch 上进行开发,A 修改 a 文件,B 修改 b 文件。在 A push 之后,B 是不能直接 push 的,因为本地仓库与远程仓库对应分支的内容还没进行同步。必须先执行下面的操作进行同步:

代码语言:javascript
复制
git pull origin/remote_branch

这个指令其实做了两件事,第一件事是把远程已被 A 更新的 remote_branch 分支拉到本地:

代码语言:javascript
复制
git fetch origin/remote_branch

第二件事是将这个目前最新的分支与本地 B 自己的分支进行合并:

代码语言:javascript
复制
git merge remote_branch origin/remote_branch

最后再进行 push 就不会再报错了。

还有一种情况是修改同一文件的不同区域,因为修改的地方不一样,Git 还是可以帮我们进行 merge 的。

6.2 修改同一文件同一区域

因为上面的修改都是针对不同区域的修改,所以不存在冲突,Git 可以帮我们进行 merge。但是当修改同一文件的同一地方时,情况就不一样了:比如说 A 修改了 a 文件的某个地方,之后 push;B 同时也修改了 a 文件的这个地方,进行 push,之后报错,所以 B 选择先 fetch --> mergepush,但是在 merge 的时候还会报错(Auto merged failed)。

这是因为 A 和 B 都修改了同一个地方,Git 并不知道以谁的修改为准,因此 Git 无法帮我们做合并的工作。此时,就需要 B 手动去处理冲突,之后再 add --> commit --> push 才行。

6.3 修改同一文件的文件名

假设现在的场景是:A 修改 a 文件的文件名为 a1,之后 push;B 修改 a 文件的文件名为 a2,也想要 push,因为本地和远程不同步,根据之前解释的,到这里自然会报错,所以 B 决定先 pullpush,但实际上,在 pull 这一步的时候会再报一个错,提示无法进行 merge

这和 6.2 其实是一样的情况,A 和 B 都想要修改同一个东西,Git 不知道应该以谁为准。假设 AB 经过协商,最终决定修改为 a2,那么对于 B 来说,他可以把本地的 aa1 删除,再对 a2 进行 add --> commit --> push 的操作。(为什么会有 a1,是因为 pull 操作的 fetch 这一步是没问题的,远程拉下来之后本地会多出此前 A 更新的 a1 文件)

6.4 local,origin 和 upstream

在前面,我们是单人开发或者团队开发,可能只涉及到本地仓库和远程仓库,即 local 和 origin。假设 A 发现了某个第三方开源项目 ThirdParty/project 存在一些 bug,他想要进行修复,那么他就需要 fork 别人的仓库,形成自己的远程仓库 A/project,再克隆这个仓库形成本地仓库。那么,对于本地仓库来说,A/project 就是 origin,而 ThirdParty/project 就是 upstream。

local 和 origin 的同步

这个就是之前讲的情形,origin 是 A 自己的远程仓库,A 具有自由读写的权限,可以直接 pullpush

local 和 upstream 的同步

如果本地仓库想要与原始仓库同步,第一步是先设置上游:

`git bash git remote add upstream

代码语言:javascript
复制
查看当前配置的远程仓库(将包括 origin 和 upstream):

```git bash
git remote -v

之后就可以与原始仓库进行同步了:

代码语言:javascript
复制
 git pull upstream master

不过这里要注意,A 不是项目参与者,对原始仓库是没有写入权限的,因此无法直接进行 git push 操作。那么 A 怎么做呢?他可以先在本地把 bug 修复好,之后先 push 到 orgin 上去,再对原始仓库发起 pull request,等待项目负责人进行 merge。

origin 和 upstream 的同步

设置上游之后,本地可以与原始仓库保持同步,那么 origin 怎么与原始仓库同步呢?我们需要反向 pull request。这里可以以掘金翻译计划这个项目为例。在我们自己的仓库点击 pull request 来到这个界面:

注意这里的 head 和 base,一开始是 origin 指向 upstream,表示我这边更新了东西,想要原始仓库去合并;不过现在的情况是反过来的,我们可以看作是原始仓库更新了东西,想要我们去合并。所以将 head 和 base 改为从 upstream 指向 origin:

可以看到,因为有很长一段时间没有同步更新了,所以多出了整整 800 多个 commit。到了这个界面,就可以正式创建 pr,然后我们自己来 merge 了。这样,origin 和 upstream 就保持了同步。

参考:

玩转 Git 三剑客

廖雪峰 Git 教程

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-05-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. Git 安装和升级
  • 2. Git 乱码问题解决方案
  • 3. 初始配置
  • 4. 本地仓库操作
    • 4.1 工作区相关
      • 4.2 暂存区相关
        • 提交文件到暂存区
        • 删除暂存区的文件
        • 修改工作区和暂存区的文件名
        • 撤销对暂存区的文件提交
        • 比对差异
      • 4.3 历史区相关
        • 提交文件到历史区
        • 查看 commit 信息
        • 修改最近一次 commit 信息
        • 修改以前的 commit 信息
        • 合并多个连续 commit:
        • 合并多个非连续 commit:
        • 版本回退
        • 比对版本差异
      • 4.4 分支相关
        • 查看所有分支
        • 基于当前分支的最近一次 commit 创建并切换分支
        • 基于某次 commit 创建并切换分支
        • 基于某个分支创建并切换分支
        • 切换分支
        • 合并分支
        • 删除分支
        • 比对分支差异
        • 基于某次 commit 切换到“分离头指针”状态
      • 4.5 Git 的三个重要对象(commit、tree、blob)
      • 5. 远程仓库操作
        • 5.1 公私钥配置
          • 5.2 新建项目
            • 5.3 克隆项目
            • 6. 多人开发
              • 6.1 修改不同文件/同一文件不同区域
                • 6.2 修改同一文件同一区域
                  • 6.3 修改同一文件的文件名
                    • 6.4 local,origin 和 upstream
                      • local 和 origin 的同步
                      • local 和 upstream 的同步
                      • origin 和 upstream 的同步
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档