平常工作经常需要与Git打交道,记录一些常用的命令和使用场景。

GitHub配置公钥用于下载代码仓

如果代码要同步到远程仓库,需要在远程代码仓库托管平台(如Gitlab, GitHub, Gitee)要添加本地公钥,基本步骤是本地生成一对RSA密钥,然后将公钥添加到远程仓库代码托管平台。
下面以Github为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.配置用户名和邮箱地址,提交代码时的用户,与代码托管平台如github的账户无关联
git config --global user.name "xxx"
git config --global user.email "xxx@xx.com"

# 2.本地生成RSA私钥对
ssh-keygen -t rsa -C "example@xx.com"
# 或者采用
ssh-keygen -m PEM -t rsa -b 4096 -C "example@xx.com"

# 3.代码托管平台添加rsa公钥id_rsa.pub

# 4.验证配置结果
$ ssh -T git@github.com
Hi TsukasaMoe! You've successfully authenticated, but GitHub does not provide shell access.

1.第一次同步远程仓库代码

生成新密钥,在GitHub配置好公钥后,第一次用git clone拉取代码仓时可能会遇到下面提示:

1
2
3
The authenticity of host 'github.com (20.205.243.166)' can't be established.
ECDSA key fingerprint is SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

原因是rsa密钥对所在目录~/.ssh下没有known_hosts文件,输入yes即可生成。

2.Git bash中文乱码问题

如果有中文乱码问题可以设置下编码格式:

  1. Git bash 中右键,选择“Option”
  2. 选择“Text”,设置字符编码为UTF-8

基本配置

global:配置文件在~/.gitconfig文件下
system:用户级别
local:项目级别,在当前项目的.git/config文件下

配置优先级别:local>system>global

1
2
3
git config --local user.name "xxx"
git config --local user.email "xxx@xx.com"

分支 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
# 查看分支
git branch
git branch -a
git branch -av

# 克隆仓库到本地
git clone git@github.com:square/okhttp.git

# 查看分支最后一次提交
git branch -v
# 查看本地分支关联的远程仓库
git branch -vv
git remote show origin
cat .git/config

# 创建分支
git branch mybranch

# 重命名分支
git branch -m mybranch dev

# 切换分支
git checkout mybranch

# 删除分支
git branch -d mybranch
# 强制删除分支
git branch -D mybranch

# 关联当前分支到远程分支
git branch --set-upstream-to=origin<分支名称>
# 示例:git branch --set-upstream debug origin/debug

# 创建并切换分支:https://www.cnblogs.com/mcat/p/5831212.html
git checkout -b mybranch

# 拉取远程分支最新内容
git pull

标签 git tag

项目发布时,一般会打标签标明版本,针对整个项目,与具体分支无关。

.git/refs/tags下记录了创建的标签

简单标签:只存储commit的sha1值
带注释的标签:存储了带注释的信息,包含commit的sha1值(创建一个新的commit)

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
# 打简单标签
git tag v1.0

# 打标签加注释
git tag -a xxx -m "xxx"

# 误打标签
git tag v2.0 v1.0

# 查询标签
git tag -l
git show v1.0

# 支持通配符进行查询
git tag -l '1.*'

# 删除标签,远程仓库可以直接在托管平台删除或创建标签
git push origin :refs/tags/v1.0
# 将远程的标签删除后,本地无法感知,可通过下面命令手动删除
git tag -d v1.0

# 推送所有tags变更到远端
git push --tags
# 只推送某些标签到远端
git push origin v1.0
git push origin v1.0 v2.0
# 完整写法
git push origin refs/tags/v1.0:refs/tags/v1.0

# 只获取远程标签
git fetch origin tag v1.0
git fetch origin tag

分支合并与冲突

分支:一个commit链,一条工作线。

1.合并无冲突场景:
如果一个分支靠前(dev),另一个分支落后(master),master合并dev分支后直接追赶上dev分支,成为fast forward。可以用git log查看commit id变化情况。

fast forward本质是分支指针的移动。

  • 两个分支fast forward归于一点,指向同一个commit
  • 没有分支信息(丢失分支信息)

git在merge时,默认使用fast forward,也可以禁止:git merge –no-ff <分支>

  • 两个分支的fast forward不会归于一点,主动合并的分支会前进一步,指向最新的commit
  • 分支信息完整(不会丢失分支信息),可以用git log –graph查看

2.合并有冲突:
需要解决冲突:git add xxx, git commit -m “xxx”
注意:master在merge时,遇到冲突并解决后,会进行2次提交,1次是最终提交,另一次是将dev分支的提交信息也拿过来。

如果一个分支落后(dev),另一个分支领先(master),则落后的分支可以直接通过git merge合并领先分支,不会报冲突,也是采用fast forward的方式合并。

.git文件夹下的HEAD文件记录了当前分支的Head

版本穿梭

回退和前进到对应的commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 添加到暂存区并提交
git commit -am "xxxx"

# 修改上一次提交的commit注释
git commit --amend -m "xxxx"

# 回退到上两次commit
git reset --hard HEAD^^

# 回退到上n次commit
git reset --hard HEAD~n

# 回退到某个commit,通过sha1值,无需写完整的commit id
git reset --hard 4c03f5c6

git reset –hard的后悔药:git reflog
git reflog记录了git的所有操作,包括commit,reset等,找到需要回退到的commit id,使用git reset –hard进行回退。

git checkout的放弃与游离操作

取消工作区的修改,暂存区的修改会保留
git checkout filename

git reset HEAD filename,将之前增加到暂存区的内容,回退到工作区。

git checkout还可以进行版本穿梭(游离状态)

git checkout

  • 修改后必须commit,提交后可以用git branch mybranch <刚才生成的commit id>,生成新分支
  • 可以用git checkout -b <新分支名称>创建新分支,再进行修改等操作

git log操作

1
2
3
4
5
6
7
8
9
# 查看倒数第n次提交 git log -n
git log -1

# 查看远程仓库分支的日志
git log refs/remotes/origin/master

# 查看tree和parent对象
git log --pretty=raw <commit>
git log --pretty=raw

git blame:查询文件编辑历史

1
2
3
# 查看文件的所有提交commit id,以及每一行的作者
git blame 1.txt

diff: 比较文件
git diff: 比较工作区与暂存区中的文件差异,通常用IDE或者beyond compare等软件进行差异比较更直观。
git diff –cached:比较对象区和暂存区的差异

1
2
3
# 对象区和暂存区的差异
git diff --cached <commit id>
git diff --cached HEAD

个人远程仓库回退

工作中每个人有自己的远程仓库,有时候自己作死做了一些危险的操作,导致个人远程仓库的master分支与主仓库的有差别,不能够发起merge request,需要重写覆盖,下面命令不要在项目的远程仓库执行。

1
2
3
4
5
6
7
8
9
10
# 找到需要回退commitId
git reflog

# 强制回退
git reset --hard {commiId}

# 推送到远程仓库
git push -u {远程仓库名} {远程仓库分支} -f
git push -f

删除github对象区的某个文件

git rm: 如果文件已经在对象区,执行删除操作会放到暂存区。
如果想要放回到工作区:git restore --staged <file>
放回工作区后,如果不想删除文件了:git restore <file>
放回工作区后,如果想要放回暂存区,可以执行:git add/rm <file>

git rm --cache: 仅从索引中(暂存区)删除文件,本地文件(工作区中)保留,适用于不将该文件被版本控制。
rm: 本地删除文件,但是没有将其从git的记录中删除。

1
2
3
4
5
6
git rm --cached 1.txt
git commit -m "变更描述"
git push origin master

git rm 1.txt

保存现场 git stash

如果两个分支不在同一个commit上,暂存区有内容则不能切换分支,可以先commit或者stash后再切换,恢复现场的内容会进入工作区。

如果两个分支在同一个commit上,在commit之前,可以checkout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 临时保存到栈中
git stash

# 查看有哪些临时保存内容
git stash list

# 还原最新保存的内容,该保存内容会删除(出栈)
git stash pop

# 还原保存的内容,但是不会将该内容删除(出栈)
git stash apply
git stash apply stash{1}

# 删除保存的内容
git stash drop stash{0}

# 保存现场时可以指定名字
git stash save "mystash"

远程仓库 git remote

git push:本地提交推送到远程仓库
git pull:远程仓库到本地,拉取fetch+合并merge。

git stash冲突:保存现场后修改了内容并commit,然后再还原现场导致的冲突场景。解决冲突后需要git add,commit。

1
2
3
4
5
6
7
8
9
10
11
12
# 设置远程仓库名称为origin,地址(或者https协议)
git remote add origin git@github.com:TsukasaMoe/tsukasamoe.github.io.git

# 推送本地master分支到远程仓库origin,-u表示后续不用加参数,直接git push即可
git push -u origin master

# 查看远程仓库有哪些
git remote show

# 查看具体的远程仓库origin信息,包括查看远程仓库是否有更新
git remote show origin

本地分支与远程分支关联操作

三个分支:

  • 远程分支
  • 追踪分支:本地的远程分支,或者说感知分支
  • 本地分支:需要通过追踪分支感知远程分支

本地创建分支dev,在dev中提交了一些修改后,不能直接git push,解决办法有两种:

1
2
3
4
5
6
7
8
# 1.本地创建dev分支,在dev中提交了一些修改后,首次push需要先创建远程仓库,并指定远程仓库
# dev分支是否有修改不是必须的,可以直接推送新分支到远程仓库
git remote add origin git@github.com:TsukasaMoe/tsukasamoe.github.io.git
git push -u origin dev

# 2.将dev分支关联到远程仓库origin
git push --set-upstream origin dev

首次拉取远程仓库时,可以进行本地分支关联远程分支

1
2
3
4
5
6
7
8
# 方法一:创建分支dev并和远程仓库的dev分支关联
git checkout -b dev origin/dev
# 方法二:创建追踪分支
git checkout -b test --track origin/dev
git checkout --track origin/dev

# 如果远程有新分支dev2,可以一步到位进行关联,src:desc
git pull origin dev2:dev

删除远程分支

1
2
3
4
5
6
7
8
9
10
11
12
# 删除本地分支
git branch -d 分支名

# 将本地分支推送到远程仓库origin下的目标分支
git push origin 本地分支:目标分支

# 删除远端分支
# 方法一:将远程仓库下的test分支删除,可以理解为本地分支为空,即删除远程分支test
git push origin :test

# 方法二
git push origin --delete 远程分支

本地通过.git/refs/remote/origin目录进行远端分支的感知,创建一个感知远程仓库分支的本地分支

git fetch origin master:refs/remotes/origin/test

1
2
3
4
5
6
# 检测本地没有关联的远程分支
git remote prune origin --dry-run

# 清理无效的追踪分支
git remote prune origin

别名 alias

可以给git的命令取别名,也可以在.gitconfig下配置
命令:

1
2
3
# 给checkout命令创建别名ch
git config --global alias.ch checkout

垃圾回收 git gc

objects、refs中记录了很多commit的sha1值,执行命令git gc压缩.git/refs/到.git/packed-refs,同时.git/objects被压缩后会有2个子目录info、pack

1
git gc

GUI工具

默认有GUI和gitk等等

1
2
3
4
5
# 打开gitk工具
gitk

# 打开git gui工具
git gui

子模块 git submodule

子模块submodule应用场景:
一个仓库中引用另一个仓库的代码。两个子模块之间存在引用关系,例如A模块依赖B模块,那么此时虽然可以通过B模块打个jar包给A模块的开发人员使用,假如此时B模块有功能更新则A模块无法用到最新的功能,或者要依赖于B模块先完成开发,不利于两个模块的并行开发。

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
# 当前项目添加引用子模块submodule的仓库地
git submodule add git@github.com:AobingJava/JavaFamily.git
# 从工作区添加到暂存区并提交
git add .
# 或者采用交互式命令
git add -i
git commit -m "添加子模块submodule"
# 推送到远程仓库
git push

# A模块引用B模块,要获取B模块的更新,需要进入到子模块B目录下进行git pull
cd B
git pull
# 此时本地能够感知B库的更新,但是A的远程仓库没法感知B的更新,需要推送到远程端
git commit -am "更新B模块的变化到A的远程仓库"
git push

# 拉取多个子模块的更新场景
git submodule foreach git pull
# 同上,A的远程要感知其他子模块的更新,需要推送到远端
git commit -am "更新所有子模块的变化到A的远程仓库"
git push

# 拉取一个带submodule的项目
git clone git@github.com:TsukasaMoe/tsukasamoe.github.io.git --recursive

# 删除子模块submodule:暂存区、工作区删除后提交(对象区删除),推送到远端
git rm --cached B .gitmodule
rm -rf B .gitmodule
git add .
git commit -m "删除子模块B"
git push

git裸库:没有工作区的工作仓库,存在于服务端。
创建裸库:git init --bare

git subtree

与submodule应用场景类似,但是要双向修改那么可以用subtree

1
2
3
4
5
6
7
8
9
10
11
# 父工程中添加子工程
git subtree add -P 子工程仓库地址 子工程仓库下的某个分支名
# 示例
git subtree add -P git@github.com:AobingJava/JavaFamily.git master
# 或者先添加一个子仓库
git remote add subProjRep git@github.com:AobingJava/JavaFamily.git
git subtree add -P subProjRep master

# 更新子工程代码
git subtree pull -P 子工程名称 subProjRep master

子工程修改了,父工程如何感知子工程的修改?

  • 修改子工程之后push到远端
  • 本地将子工程的修改更新到父工程中:git subtree pull -P subProj suProj-origin master
  • 父工程中子工程的更新情况推送到父工程的远程仓库

注意:
–squash参数的作用是合并commit,为了防止子工程干扰父工程,不然git log下父子工程项目下子工程的多次提交会产生干扰,该参数会产生一个新的提交,往前走2步。
产生冲突的原因有:

  • 修改同一个文件同一行
  • 共同祖先不同

使用建议:在做subtree时,如果加squash,以后每次都加,如果不加,每次都不要加,避免后续经常产生合并冲突。

1
2
git remote add --prefix 子工程名称 master --squash
git subtree pull -P 子工程名称 subProjRep master --squash

如何将父工程中子工程的修改推送到真实的子工程仓库上?

  • 父工程下修改子工程内容
  • 推送到父工程以及真实的子工程:
    • 推送到父工程的仓库:git push
    • 推送到子工程的仓库:git subtree push -P subProj subProj-origin master

git cherry-pick

git chery-pick命令用来将某个分支一些提交作为一个新的提交引入到你当前分支上,一个分支上的一些提交可以理解为补丁。
如果在某个分支做了一些提交,发现分支选错了,需要将提交复制到正确的分支上,可以使用cherry-pick。

每次只能复制一个commit,sha1值会改变,可用git log查看。不能跨节点,因为不同祖先会导致冲突。

1
2
3
4
5
6
7
8
9
10
11
# dev分支进行了错误的提交,需要删除掉,删除思路:
# 1.cherry-pick复制到应该编写的分支;2.把写错分支删除(checkout到分支节点,删除分支)

# 1.假如已经在dev中产生了两次提交,commitid分为af2b2c, 14ed1b1c,需要将这2次提交复制到master分支上
git cherry-pick af2b2c
git cherry-pick 14ed1b1c

# 2.原来dev的分支节点为1fab3c,写错分支删除,重新创建dev分支
git checkout 1fab3c
git branch -D dev
git checkout -b dev

变基 git rebase

git rebase和git merge都可以用来整合不同分支的修改。

1
2
3
4
5
6
7
     dev

C1-C2-C4
|
C3->C5

master

如果想要整合dev分支修改到master,使用merge是先切换到master分支,然后进行git merge。
如果用rebase,则是先切换到dev分支,执行git rebase master进行变基,最终会将dev的分叉合入master分支尾部,然后dev指向最新commit节点。

1
2
3
4
5
6
7
# 未在master分支执行git merge的状态

C1-C2-C4 dev
| ⬇
C3->C5->C34'->C6

master

注意:git rebase只在本机操作,不要推送到远程仓库,即不要在主分支master上进行git rebase

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
# 切换到要进行变基的分支
git checkout dev
# 变基
git rebase master

# 如果提示有冲突类似下面
Tsukasa@DESKTOP-O5RIC6N MINGW64 /d/Server/Test/git_rebase (dev)
$ git rebase master

Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
error: could not apply b822ef0... dev修改
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply b822ef0... dev修改

# 根据上述描述,通常由三种策略
# 1.解决冲突后,git add和git commit到对象区后,继续执行git rebase --continue,直到所有冲突被解决,rebase结束
# 2.dev分支的提交冲突是一个commit一个commit进行处理的,某个commit的更新需要忽略,可以执行git rebase --abort
# 3.如果不想执行git rebase了,可以随时执行git rebase --abort回到执行git rebase之前的状态
git rebase --continue
git rebase --skip
git rebase --abort

# git rebase完成后,master指向的节点可能会落后于dev,这时在master分支上执行git merge操作让master分支指向HEAD节点
git checkout master
git merge dev

生产项目分支分配

dev:开发分支
test:开发完毕后,交给测试的分支
master:生产阶段
bugfix:临时修复bug的分支。

dev -> test(merge dev) -> master(merge test) -> …

记录下之前工作时的git协作工作流:

  • 项目远程主仓库,以及每个人在gitlab上克隆主仓库的项目
  • 每个人拉取自己私人远程仓库的代码下来
  • 每个人建立个人远程仓库主分支的跟踪分支,每个开发都在自己的主分支上进行开发
  • 完成个人开发后往私人远程仓库中推送代码,然后在gitlab上发起merge request请求,code review通过后项目leader合入。

参考资料

声明:本站所有文章均为原创或翻译,遵循署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议,如需转载请确保您对该协议有足够了解,并附上作者名 (Tsukasa) 及原文地址