Git常用命令

摘要

基本概念

Git的5种工作区域

  • 工作目录:用于新增、修改、删除文件,实际我们用于编写代码的目录

  • 暂存区:执行add命令可以将工作目录对应的文件提交到暂存区,只有加入到暂存区的文件才会参与版本控制,其实际为一堆索引文件,保存在.git/objects目录下,记录每个文件的快照(hash)

  • 本地版本库:执行commit命令可以将暂存区的文件提交到本地版本库,每一次提交都会记录版本日志,其实际保存位置也是.git/objects目录下的快照文件,每一次commit如果文件发生变化都会生成新的快照,.git/refs/heads/下记录每个分支的最新一次commit的版本号

  • 远程跟踪区:.git/refs/remotes/origin/下记录每个分支的最新一次更新后的远程版本号,执行fetch\pull\push时都会更新为最新的远程版本号。如果只执行fetch仅仅会更新远程跟踪区,并不会更新本地目录,执行pull命令会同时更新远程跟踪区和本地目录

  • 远程版本库:例如github

文件的状态

  • Untracked:未跟踪的文件,尚未加入过暂存区的文件

1
2
3
4
5
6
7
8
9
10
➜  git:(release) touch a.txt
➜ git:(release) ✗ git status
位于分支 release
您的分支与上游分支 'origin/release' 一致。

未跟踪的文件:
(使用 "git add <文件>..." 以包含要提交的内容)
a.txt

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)
  • 要提交的变更[新增],加入暂存区

1
2
3
4
5
6
7
8
➜  git:(release) ✗ git add .
➜ git:(release) ✗ git status
位于分支 release
您的分支与上游分支 'origin/release' 一致。

要提交的变更:
(使用 "git restore --staged <文件>..." 以取消暂存)
新文件: a.txt
  • 已提交版本库待发布到远端,将暂存区的文件加入本地版本库

1
2
3
4
5
6
7
8
9
10
➜  git:(release) ✗ git commit -m 'add a.txt'
[release 9292bb2] add a.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 a.txt
➜ git:(release) git status
位于分支 release
您的分支领先 'origin/release' 共 1 个提交。
(使用 "git push" 来发布您的本地提交)

无文件要提交,干净的工作区
  • 尚未暂存以备提交的变更,加入过暂存区的文件发生修改

1
2
3
4
5
6
7
8
9
10
11
12
➜  git:(release) echo "hello" >> a.txt
➜ git:(release) ✗ git status
位于分支 release
您的分支领先 'origin/release' 共 1 个提交。
(使用 "git push" 来发布您的本地提交)

尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git restore <文件>..." 丢弃工作区的改动)
修改: a.txt

修改尚未加入提交(使用 "git add" 和/或 "git commit -a"
  • 要提交的变更[修改],加入过暂存区的文件重新加入暂存区

1
2
3
4
5
6
7
8
9
➜  git:(release) ✗ git add .
➜ git:(release) ✗ git status
位于分支 release
您的分支领先 'origin/release' 共 1 个提交。
(使用 "git push" 来发布您的本地提交)

要提交的变更:
(使用 "git restore --staged <文件>..." 以取消暂存)
修改: a.txt

切换分支时值得注意的地方

  • 只要文件没有被commit,无论是新增还是修改,切换分支时,文件的状态都会被带到切换后的分支

  • 所以切换分支前,一定要执行commit

HEAD指针和分支指针

  • 我们在查看git的log时会看到类似于* 6d93a15 (HEAD -> master) message这样的信息,6d93a15就是commit时的版本号,master是分支名称,HEAD就是HEAD指针,实际上这里的master也是一个指针,他就是分支指针

  • 分支指针永远指向当前分支最新的一次提交版本,分支指针对应版本号保存在.git/refs/heads/目录下对应的分支文件中

  • HEAD指针表示我们当前的工作目录是基于哪个版本checkout出来的,通常情况下HEAD指针指向分支指针,但当我们通过命令git checkout <commit号>切换到某个版本时,HEAD指针就不再指向分支指针,这个情况有个名字叫作detached HEAD(头分离)HEAD指针对应的版本号保存在.git/HEAD文件中,这也是为什么我们每次进入项目,git都知道我们当前所在分支或版本号是什么。

  • 参考资料:https://www.zsythink.net/archives/3412/

问题与方法

1.别人在远程仓库中创建了新的branch,我本地执行git branch -a却看不到,如何才能看到并checkout呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1.先要获取远端全部信息
git fetch origin
# git fetch 远程名称,通过git remote查看,一般就是origin。
git fetch # 也可以不加远程名称
# 该命令无论在哪个分支上执行,都会更新本地所有的远程分支
# git fetch 完成了仅有的但是很重要的两步:
# 1.从远程仓库下载本地仓库中缺失的提交记录
# 2.更新远程分支指针(如 o/master)
# git fetch 实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。
# git fetch 并不会改变你本地仓库的状态。它不会更新你的 本地 分支,也不会修改你磁盘上的文件。

# 2.再查看分支信息
git branch -a

# 3.checkout到本地并切换到新创建的分支
git checkout -b release(本地分支名) origin/release(远程分支名)
控制台输出:
分支 'release' 设置为跟踪来自 'origin' 的远程分支 'release'
切换到一个新分支 'release'

# 4.查看当前分支跟踪的远程分支
git rev-parse --abbrev-ref --symbolic-full-name @{u} # @{u} 是 @{upstream} 的简写
控制台输出:
origin/release

2.如何查看本地分支与远程分支的区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.先要获取远端全部信息
git fetch origin
# 或者
git fetch

# 2.切换到待比较的本地分支,如master
git checkout master

# 3.比较当前分支与origin/master之间的不同,--stat只显示哪些文件有不同,如果要查看每个文件不同的详细信息就去掉--stat
git diff origin/master --stat

# 4.比较任意两个分支的不同,--stat只显示哪些文件有不同,如果要查看每个文件不同的详细信息就去掉--stat
git diff origin/master..master --stat

3.如何查看本地两个分支之间的区别?

1
2
3
4
5
# 1.比较任意两个分支的区别
git diff master..dev --stat

# 2.比较当前分支与master分支的区别
git diff master --stat

4.如何查看本地的发生了哪些更改?

1
2
3
4
5
6
7
8
9
10
11
# 当前工作目录的索引和上次提交索引之间的差异,只有已经被commit过的文件才会被比较,如果是新增的文件则看不到
git diff --stat

# 可以通过`git status`命令查看本地都有哪些变化,包含新增未加入暂存区的,新增已加入暂存区的,已提交过但有改动的,等等
git status

# 查看下次执行`git commit`时会被提交的文件
git diff --cached --stat

# 查看下次执行`git commit -a` 时会被提交的文件,-a表示先add再commit
git diff HEAD --stat

5.如何提交本地仓库?

1
2
3
4
5
6
7
8
# 本地无论是新增文件或修改文件,都要add后才能commit

# 1.先add再commit
git add .
git commit -m 'message'

# 2.add同时commit,注意,如果存在尚未跟踪的文件,需要使用 "git add" 建立跟踪
git commit -a -m 'message'

6.git reset–如何回滚到指定版本或分支?

  • git reset的作用是修改HEAD的位置,即将HEAD指向的位置改变为之前存在的某个版本,如果想恢复到之前某个提交的版本,且那个版本之后提交的版本我们都不要了,就可以用这种方法。

  • 注意,如果reset包含了已经发布(git push)的的版本,此时如果用git push会报错,因为我们本地库HEAD指向的版本比远程库的要旧,需要执行命令git push -f强制更新远程

  • 注意参数--hard有和没有的区别,有–hard,则完全回退到上一版本,丢弃所有其它修改,清空暂存区,同步工作目录到指定版本。没有–hard,则该版本之后的变化会变为Modified状态保留在工作目录,只清空暂存区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1.回滚到上一个版本
git reset --hard HEAD^ # 完全回退到上一个版本,丢弃所有其它修改,清空暂存区,同步工作目录到指定版本,也就是说,上一次提交之后新增或修改的文件内容都没有了
git reset HEAD~1 # 则该版本之后的变化会变为Modified状态保留在工作目录,只清空暂存区,也就是说,上一次提交之后新增或修改的文件内容得以保留

# 2.回滚到上上个版本
git reset --hard HEAD^^
git reset HEAD~2

# 3.回滚到指定版本的分支
git reset --hard 版本号

# 3.1 查看版本号
git log --oneline

# 4.回滚到指定分支
git reset --hard 分支名称

# 4.1 回滚到与本地远程分支一样的状态,一般本地仓库搞坏了会这么做
git reset --hard origin/master

# 5. `git log`看不到reset的历史,可以通过如下命令查看
git reflog

7..gitignore–在git中如果想忽略掉某个文件,不让这个文件提交到版本库中,要怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 在工作目录下创建 .gitignore 文件
# 其格式为:
# 此为注释 – 将被 Git 忽略
*.a # 忽略所有 .a 结尾的文件
!lib.a # 但 lib.a 除外
/TODO # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
build/ # 忽略 build/ 目录下的所有文件
doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt

# 忽略文件默认为当前项目目录下的.gitignore文件,也可以通过如下命令指定文件路径和名称
git config core.excludesfile .gitignore_dev

# 也可配置为全局文件,这样就不需要为每个项目都创建.gitignore 文件
git config --global core.excludesfile ~/.gitignore_global


# .gitignore 文件 只对git add起作用,如果有文件在被加入到.gitignore 文件前就已经被commit了可以使用如下方法
# 1.删除暂存区、分支上内容,本地保留。解除该文件的追踪关系,脱离版本控制。
git rm --cached 文件名 # 删除文件
git rm -r --cached 文件夹 # 删除文件夹 -r 表示允许递归删除
git rm -r --cached . # 删除当前目录下全部文件的暂存区

git rm -r 文件夹/文件名 #删除本地、暂存区、分支上内容,如果该文件不需要在本地保留,就可以测底删除

# 2.重新加入暂存区
git add . # 将当前目录下所有文件加入暂存区
git add file/dir #将指定文件或目录加入暂存区,支持通配符

# 3.查看暂存区内容
git diff --cached

# 4.提交
git commit -m "message" # 将暂存区内容提交到本地版本库
git commit -am "message" # 先提交暂存区再提交到本地版本库

8.git commit --amend–commit后发现有内容要修改或者注释写错了,但是不想创建新的一次commit,要怎么办呢?

  • 以下命令如果直接合并到已经push过的版本,再次git push时会提示"更新被拒绝,因为您当前分支的最新提交落后于其对应的远程分支",此时可以执行git push -f强行发布即可。

1
2
3
4
5
# 覆盖上一次提交,这样不会产生新的提交
git commit --amend -am "注释"

# 在上次提交中附加一些内容,保持提交日志不变
git commit -a --amend --no-edit

9.git log–如何查看提交日志?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 显示版本历史,如果有用git reset --hard xxxxx回退操作,则只会显示到xxx之前的历史
git log # 显示当前分支之前的全部日志
git log --all # --all 显示全部分支
git log --oneline # 单行显示

git log --oneline --all --graph # 图形化显示全部分支log

# 在所有提交日志中搜索包含「homepage」的提交
git log --all --grep='homepage' #模糊匹配

# 获取某人的提交日志
git log --author="hanqf" #模糊匹配

# 查看完整版本历史,也就是说即便有git reset也会显示
git reflog

10.如何创建本地仓库并绑定到远程仓库?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1.首先要在对应的git服务器创建一个新的仓库,一般的git服务器创建新仓库后都会提示你如何绑定该仓库的

# 2. 创建本地仓库
git init #将当前目录加入版本控制
git init dir #将dir加入版本控制
# 默认使用 'master' 作为初始分支的名称,如果要修改分支名称,可以执行如下命令:
git branch -m <name>
# 也可以通过设置git config来配置默认的名称,除了 'master' 之外,通常选定的名字有 'main'、'trunk' 和 'development'。
git config --global init.defaultBranch <名称>

# 3. 创建新的文件后提交到本地仓库
git add . && git commit -m 'message'
git commit -am "备注"

# 4.绑定远程仓库
git remote add origin https://xxxxx (远程仓库地址)

# 5.提交本地仓库的变更到远程仓库,这里要注意github的主分支名称已经变更为main,所以提交github时要将master替换为main
git pull --rebase origin master #获取远程库与本地同步合并(如果远程库不为空必须做这一步,否则后面的提交会失败)
git push -u origin master #提交master到其远程仓库,第一次提交加上-u,以后就不用了
git push origin master
# 或者
git push # 将当前分支提交到其对应的远程仓库

11.远程仓库地址变更后如何更新?

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1.命令行修改
git remote set-url origin [NEW_URL]

# 2.手工编辑.git目录下的config文件
[remote "origin"]
url = https://xxxxxx (修改为新的地址)
fetch = +refs/heads/*:refs/remotes/origin/*

# 3.查看修改后的地址
git remote get-url origin #只会显示可以fetch的地址

# 4.查看远程仓库地址
git remote -v # 会显示push和fetch的地址

12.如何将本地仓库同时绑定到多个远程仓库?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.按照`10.如何创建本地仓库并绑定到远程仓库?`中的步骤完成第一个仓库的绑定

# 2.手工编辑.git目录下的config文件
[remote "origin"]
url = https://xxxxxxxx1 # 此处为第一个远程仓库的地址,即可以push又可以fetch
url = https://xxxxxxxx2 # 在此添加第二个仓库的地址,以此类推,可以添加多个,只可以push,只能备份使用
fetch = +refs/heads/*:refs/remotes/origin/*

# 3.保存后再次执行,这里要注意github的主分支名称已经变更为main,所以提交github时要将master替换为main
git push -u origin master # 提交master到其远程仓库,只有第一次执行时需要加上 -u ,以后只需要:git push origin master
# 或者
git push # 将当前分支提交到其对应的远程仓库

# 4.查看远程仓库地址
git remote -v

13.如何在git pull时不用每次都输入密码?

1
2
3
4
5
# 进入项目目录
git config --global credential.helper store

# 执行 git pull 并输入密码,此时当前项目就会记住密码了,下次再执行时就不用密码了
git pull

14.如果文件已经git add到暂存区,但是尚未commit,此时如何将文件从暂存区中移除?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 如果文件还没有放入暂存区
git checkout -- <文件名> #放弃对某一个文件已经做出的修改,回到head版本的该文件

# 如果文件已经`git add`到暂存区,但是尚未commit
# 移出单个文件
git restore --staged <文件路径>

# 移出本次全都add到暂存区的文件
git reset . # 重置本次全部索引,.代表当前目录

# 可以通过git status查看文件路径
git status

# 如果已经commit,可以通过如下方式移除
# 删除暂存区、分支上内容,本地保留。解除该文件的追踪关系,脱离版本控制。
git rm --cached <文件名> # 删除文件
git rm -r --cached <文件夹> # 删除文件夹 -r 表示允许递归删除
git rm -r --cached . # 删除当前目录下全部文件的暂存区,.代表当前目录

15.git config–如何设置和查看git配置信息?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 使用git config命令进行设置和查看

# 全局设置用户名和邮箱,全局配置是保存在 ~/.gitconfig中的
git config --global user.name "你的用户名"
git config --global user.email "你的邮箱"

# 只对当前项目有效,项目根目录下执行,去掉--global,项目配置优先级更高,项目配置保存在.git/config中,可以直接修改
git config user.name "你的用户名"
git config user.email "你的邮箱"

# 修改你的用户名和邮箱
git config --global --replace-all user.name "你的用户名"
git config --global --replace-all user.email "你的邮箱"

# 查看配置
git config --list # 此时项目配置和全局配置都会显示

git config --global --list # 只显示全局配置,全局配置是保存在 ~/.gitconfig中的

git config user.name # 查看某个配置的值

# 修改全局配置的值,--replace-all会匹配所有行,也可以直接在~/.gitconfig中修改
git config --global --replace-all user.name "你的用户名"
git config --global --replace-all user.email "你的邮箱"

# 修改项目配置的值,--replace-all会匹配所有行,也可以直接在.git/config中修改
git config --replace-all user.name "你的用户名"
git config --replace-all user.email "你的邮箱"

16.提交的文件太大(默认是1M),导致push失败怎么办?

1
2
3
4
5
6
# 加大缓冲区大小(http.postBuffer的参数)

# 524288000 的单位代表 B,524288000B 也就是 500MB。
git config http.postBuffer 524288000
# 或全局设置
git config --global http.postBuffer 524288000

17.clone代码最简单的方式是通过https的形式,不过一般这样做需要输入用户名和密码,如果不想输入用户名和密码可以使用git@xxxx的形式,如何实现呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 这种方式就是基于ssh的密钥证书来实现
# 生成本地ssh密钥对,按照提示完成三次回车,即可生成 ssh key。此时会在~/.ssh/下创建id_rsa.pub(公钥)和id_rsa(私钥)
ssh-keygen -b 4096 -t rsa # -t 指定生成密钥的算法类型,生成的文件名称就是以其开头的,如这里就是id_rsa
# 支持的密钥算法类型包含:rsa,ed25519,等等,具体参见各个git服务器的配置说明

# 此时cat id_rsa.pub 可以看到公钥的内容
# 形式为:sshkey的算法+密文+sshkey的标签,此时sshkey算法为ssh-rsa,sshkey标签为机器名称,若要指定sshkey标签可以通过-C指定
ssh-keygen -b 4096 -t rsa -C "xxxxxxx"
# 注意这里-C可以指定任意值,网上说必须指定邮箱其初衷仅仅是为了便于辨识

# 将公钥文件内容复制到对应的git服务器配置ssh-key的地方,具体使用方式可以参见各个git服务器的说明。
# Github: settings--SSH and GPG keys--SSH keys--New SSH key
# 测试连接是否正常
ssh -T git@github.com
# Coding: 个人帐号设置--个人设置--SSH 公钥--新增公钥
# 测试连接是否正常
ssh -T git@e.coding.net

# Gitee: 设置--安全设置--SSH公钥
# 测试连接是否正常
ssh -T git@gitee.com

# clone代码示例
git clone git@github.com:hanqunfeng/reactive-redis-cache-annotation-spring-boot-starter.git

18.git branch–如何创建分支\切换分支\查看分支\删除分支\发布分支?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 从当前分支创建release分支,但是不切换
git branch release

git checkout release # 从当前分支切换到release分支
git checkout <commit号> # 从当前分支切换到commit号对应的版本
# 注意,当切换到某个版本号对应的版本时就会发生“头分离”,即HEAD指针不会再指向分支指针,而是指向了那个版本

# 从当前分支创建并切换到release分支,相当于执行git branch release && git checkout release两个命令
git checkout -b release

# 从dev分支创建release分支,但是不切换
git branch release dev
# 从dev分支创建release分支,并切换
git checkout -b release dev

# 从指定版本创建release分支,版本号通过git log --oneline查看
git branch release 799fb04
git checkout -b release 799fb04


# 从远程分支origin/release创建release分支,并切换
git checkout -b release origin/release

#跳到上一个分支,比如当前是master分支,是从dev分支通过git checkout master命令切换过来的,那么该命令会重新回到dev分支
git checkout -

# 查看分支
git branch #列出所有本地分支,标*的就是当前所在的分支,.git/HEAD 文件中存放的就是当前分支信息
git branch --remote #列出远程仓库中的所有分支
git branch -a #同时显示本地和远程的所有分支

cat .git/HEAD #查看当前所在分支
git symbolic-ref HEAD #查看当前所在分支,本地分支


# 删除分支
# 删除release分支,不能在release下删除自己
git branch -d release

# 假设我们通过master创建了release分支,此时release分支发生了变更并执行了commit,但是并没有将更改merge回master,此时删除release分支需要执行如下命令
git branch -D release # -D 强制删除

# 发布分支
git push --set-upstream origin <new branch name> #推新分支到远程仓库并在本地分支和远程分支之间建立关联
# 也可以分两步完成,但推荐上面的方式
git push origin <new branch name> #推新分支到远程仓库
git branch --set-upstream-to=origin/<new branch name> <new branch name> #关联本地分支到远程分支,否则每一次pull或push的时候都需要明确指定本地和远端分支。

# 查看当前分支跟踪的远程分支
git rev-parse --abbrev-ref --symbolic-full-name @{u} # @{u} 是 @{upstream} 的简写

19.merge还是rebase? 如何合并分支?

一般开发流程:

  1. 更新主分支 git pull

  2. 从主分支创建一个开发分支 git checkout -b dev,每个开发人员都会创建一个自己的开发分支

  3. 开发分支完成测试后合并回主分支,这里推荐使用git rebase,开发分支的log会被移动到主分支的顶端,这样主分支始终是一条线

  4. 这样主分支在保持干净的同时还保留了每一次commit的log,便于开发人员追溯历史

  5. 也可以使用git merge --no-ff,其也会保留每次的log到主分支上

一般发布流程:

  1. 将主分支测试完成后合并到发布分支,一般都是由一个专门的人员进行合并,此时推荐使用git merge,这样每次一发布的版本都会产生一个新的节点,而主分支上的多次commit的log不会被记录到发布分支上

  2. 这样发布分支看起来就是每一次大的发布才会合并一次并记录log,log中可以编辑本次发布的内容。

示例准备,初始化一个待合并的目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
mkdir git_test
cd git_test
touch 1.txt
➜ git init
[master(根提交) 5b91fc1] init...
1 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 1.txt
➜ git:(master) git add . && git commit -m 'init...'
➜ git:(master) git checkout -b dev
➜ git:(dev) echo "dev1" >> a.txt
➜ git:(dev) ✗ git add . && git commit -m 'dev1'
[dev 8d57739] dev1
1 file changed, 1 insertion(+)
create mode 100644 a.txt
➜ git:(dev) ✗ git checkout master
切换到分支 'master'
➜ git:(master) ✗ echo "master1" >> a.txt
➜ git:(master) ✗ git add . && git commit -m 'master1'
[master fa2400a] master1
1 file changed, 1 insertion(+)
create mode 100644 a.txt
➜ git:(master) ✗ git checkout dev
切换到分支 'dev'
➜ git:(dev) ✗ echo "dev2" >> a.txt
➜ git:(dev) ✗ git add . && git commit -m 'dev2'
[dev 0006560] dev2
1 file changed, 1 insertion(+)
➜ git:(dev) ✗ git checkout master
切换到分支 'master'

merge

将dev分支merge到当前分支

1
git merge dev
  • merge会将dev分支和当前分支合并后创建一个新的节点放到当前分支最顶端,所以解决完冲突,需要执行如下命令创建一个新的commit

1
git add . && git commit -m 'merge dev'
  • merge的log会按照时间顺序显示

  • merge后如果删除了dev分支,则在log中就看不到这个分支信息了,但是日志内容还在

merge示例
1
2
3
4
5
6
7
8
9
➜ git:(master) ✗ git merge dev
冲突(add/add):合并冲突于 a.txt
自动合并 a.txt
自动合并失败,修正冲突然后提交修正的结果。
➜ git:(master) ✗ vim a.txt
➜ git:(master) ✗ git add . && git commit -m 'master merge release'
➜ git:(master) git log --oneline # 此时才能看到日志
➜ git:(master) git branch -D dev
已删除分支 dev(曾为 0006560)。


删除dev分支后

rebase

将dev分支rebase到当前分支

1
git rebase dev
  • rebase会将dev分支的每一个节点转移到当前分支最顶端,而不会产生一个新的节点,这样rebase后的分支节点看起来就是一条线

  • 解决完冲突,执行git add .git rebase --continue,不会产生额外的commit

  • rebase后如果删除了dev分支,则在log中就看不到这个分支信息了,但是日志内容还在,这样看起来就是主分支自己的节点,没有产生过新的分支

  • 如果要放弃本次合并,可以运行git rebase --abort

rebase示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  git:(master) git rebase dev
冲突(add/add):合并冲突于 a.txt
自动合并 a.txt
error: 不能应用 0657382... master1
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
不能应用 0657382... master1
➜ git:(e4410b4) ✗ vim a.txt
➜ git:(e4410b4) ✗ git add .
➜ git:(e4410b4) ✗ git rebase --continue
[分离头指针 bd01ea0] master1
1 file changed, 1 insertion(+)
成功变基并更新 refs/heads/master。
➜ git:(master) git log --oneline
➜ git:(master) git branch -D dev
已删除分支 dev(曾为 e4410b4)。


删除dev分支后

20.git rebase -i–我commit了很多次,发现都是干的一件事,此时如果直接rebase到主分支会有很多没必要的log,是否可以合并这些commit为一个呢?

1
2
3
4
5
6
7
# git rebase -i 即可以合并多次提交,也可以合并分支,-i表示可以编辑提交过程,编辑过程中将后面的提交命令修改为s即可,保存后会提示你是否重新编辑log内容,按需编写即可。
git rebase -i commit号 #将commit号到最新的提交合并为一个提交,需要在弹出的交互页面中编辑合并过程
git rebase -i start_commit号 end_commit号 #合并指定commit号之间的的提交
git rebase -i HEAD~3 #合并最新的三次提交,编辑页面将后两次命令修改为s,wq后编辑新的commit

# 该命令也可以合并分支
git rebase -i 分支名称 #将指定分支合并到当前分支,合并时将指定分支的多个提交合并为一次提交,需要在弹出的交互页面中编辑合并过程
  • 如果修改的提交节点距离结束的提交节点中间有多个节点,而上一次和下一次都需要合并文件,这个过程就要进行多次。个人感觉这个过程虽然可以重新整理提交节点,使节点更准确清晰,但是如果修改的点比较远,文件内容变化复杂,这个多次合并的过程还是比较痛苦的,不推荐在这种情况下使用。

  • 推荐的使用场景为,针对同一个功能进行修改,在commit后尚未进行pushsa时发现有些内容需要变化,如果此时没有进行commit,可以执行git commit --amend -am "注释",如果已经执行了commit,则可以执行git rebase -i HEAD~2

示例

1
git rebase -i HEAD~3
  • 注意这里会进入两次编辑页面,一次是多个提交的处理方式,按提示进行修改即可,一般保留第一行的命令pick,后面的行命令修改为s,然后wq保存。

  • 注意合并多个分支时如果包含已经发布过的分支,就要调整顺序,将已经发布的最后一个分支放到第一行,否则再次发布时会提示需要先执行git pull,这是因为按照上面的逻辑只有第一行的是提交操作,后面的不会产生新的提交,所以版本号就会落后于远端分支,但是即便调整顺序,也会提示版本偏离,还是要先执行git pull,待手工合并冲突后再次提交一个新的commit,所以,最好的方法就是不要包含已经发布过的分支,只针对未发布的分支进行合并。
    编辑后的:

  • 第二个页面是要求你编辑本次合并的log说明,此时每次的log都会显示在这个页面,去掉不需要的,重新编辑一下保存即可。

    编辑后的:

查看日志:git log --oneline

21.git revert–如何撤销指定的提交呢,就是把这次提交回滚到其前一次提交的状态,但是又不影响其之后的提交?

  • git revert 是回滚某个commit ,不是回滚“到”某个

  • git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit。

  • git reset 撤销到某次提交, git revert 撤销某次提交,撤销并不意味着删除本次提交,其log里仍然会有这次提交,只不过revert后会产生一个新的节点用于提交,而reset是会删除之前的log。

  • 需要回滚到上一个版本时,可以通过git reset HEAD~1实现,也可以通过执行git revert HEAD实现。区别就是reset后再次发布需要git push -f,而revert不需要-f参数,因为revert时会在顶端产生一个新的节点,其版本号一定比远端新的。

  • 还有一点需要注意,例如dev是从 master checkout出来的分支,然后针对dev执行reset时,如果reset的版本master里也包含,此时再将dev merge 回 master时,则master里会保留本应该被reset的内容,而执行revert则不会出现这个问题,因为revert是用一次逆向的commit“中和”之前的提交。

1
2
3
4
5
6
7
8
9
# 撤销并commit一次新的提交
git revert HEAD #撤销前一次 commit
git revert HEAD^ #撤销前前一次 commit
git revert HEAD~3 #撤销倒数第四次提交
git revert commit号 #撤销指定的版本,撤销也会作为一次提交进行保存。

# 如果只撤销,即只改变本地目录和索引,但不执行commit,可以加上-n参数,然后手工执行commit操作
git revert -n HEAD~3
git revert -n <commit1>..<commit2> # 撤销多个版本,commit1 > x >= commit2,此时要加上-n参数,否则每个撤销都会创建一个新的commit号

22.如何打tag

1
2
3
4
5
6
7
8
9
git tag -a "prod_x.x.x" -m "message" #打标签, -a是标签名 -m注释
git push origin "prod_x.x.x" #将这个新标签推送到远程仓库

git tag #查看标签信息
git show prod_x.x.x #显示被打标签的commit的详细信息

git checkout -b hotfix_x.x.x prod_x.x.x #从prod_x.x.x标签的版本上开一个新的修复分支
git push origin hotfix_x.x.x #将hotfix_x.x.x分支推送到远程仓库
git branch --set-upstream-to=origin/hotfix_x.x.x hotfix_x.x.x #关联本地分支到远程分支

23.git pull = git fetch + git merge这种说法对吗?

  • 先说答案,git pull != git fetch + git merge,但是确实很像。

  • 其实更好的比较是git pull --rebasegit fetch + git merge的区别,其相当于git fetch + git rebase

为了说清楚这个问题,我们需要先明白git的三个仓库:

  1. 本地仓库:如master,修改代码,提交暂存区,然后commit到的就是本地仓库

  2. 本地远程跟踪仓库:如orign/master,这个仓库不能用来直接提交代码,其用来保存上一次从远程仓库拉取到的最新版本,如果不主动执行git fetch,git pull,git push等,是不会更新这个版本的。

  3. 远程仓库:git服务器,如github。执行git push时会将本地仓库的版本发布到远程仓库。

  • git fetch的作用是将远程仓库(如github)的版本与本地远程跟踪仓库(如orign/master)的版本保持一致。之后我们在master分支下执行git merge orign/master就会将远程仓库的代码合并到本地仓库,所以合并后会产出一个新的版本,我们可以发布这个版本到远程仓库。

  • git pull的作用很像git fetch + git merge,区别就是,git pull时如果没有冲突,就不会产生新的版本。

24.如何确定开发–>测试–>发布流程?

开发A模型:基于master的主分支开发模型

  • 使用git rebase orign将远程分支内容合并到当前master主分支,保证本地master始终是一条线

  • 模型结构简单,始终记住使用git fetch + git rebase或者git pull --rebase更新主分支版本

开发B模型:基于dev的子分支开发模型

  • 增加了一个dev子分支,合并回master主分支时可以通过git rebase -i的方式合并多个commit为一个commit,减少版本号数量

  • 每次开发时要创建新的分支,开发完成后删除该分支,虽略显反锁,但也更加灵活,比如上一次的开发任务还没有完成,有一个紧急的任务需要马上处理,此时只需要从master分支checkout出一个子分支进行开发即可。

  • 使用git pull --rebase更新远程版本,使用git rebase dev合并子分支,总的目标依旧是保证本地master始终是一条线

测试模型:基于A\B模型的master的主分支

  • 开发完成后,从master分支中checkout出一个release分支用于测试

  • 测试过程中如果有bug,则由开发人员通过开发模型进行修复,修复后merge到release中

  • 测试通过后,将release分支内容merge到prod分支中(prod表示生产分支,第一次时从release中checkout创建)

  • release分支仅仅为了本次发布内容而创建的测试分支,所以发布后可以删除release分支

发布模型

  • release分支测试通过够会merge到prod生产分支,此时代码可以发布上线,上线前需要为prod打tag

  • 只有打了tag的代码才能部署上线,如果线上代码发现bug,则从对应的tag中checkout一个hotfix分支,用于bug修复

  • bug修复后从hotfix分支checkout出hotfix_release分支进行测试,测试过程按照测试模型进行

  • 测试通过后按照发布模型进行

  • 发布后需要将hotfix_release分支merge到master开发主分支,之后可以删除本次hotfix分支和hotfix_release分支

说明

  • 本地和远程始终存在的分支是master开发主分支和prod发布主分支

  • 每次上线前一定要打tag,tag对应的就是当前生产环境

  • 测试分支和修复分支根据需要进行创建,用后可以删除。