git

Git 工作流・三

Git 常用技巧说明

Posted by mingfer on May 18, 2019

git 是一个开源的分布式版本控制系统,可以有效、高速地进行很小到非常大项目的版本管理。

对于刚接触 git 的同学来说,大家更多的是将 git 当做 SVN 这样的集中式的版本控制工具在使用。当然这没什么不好的,但是 Git 显然不是这么简单的一个工具。本文将会持续的更新工作中用到的各种关于 Git 的使用技巧。

菜鸟的三板斧

建立一个本地仓库:

1
$ mkdir demo && cd demo && git init

git 本地由三个空间构成:你的工作目录,index 暂存空间和 git 数据库。当我们在本地新增了一个文件之后首先需要 add 这个文件,然后在 commit 文件:

1
2
3
$ echo "test data" > test
$ git add test
$ git commit -m "add a file named test" test

这里在 add 的时候实际上是将 test 文件从本地目录 add 到了 git 数据库,但是此时文件还没有真正的提交。它的快照信息存放在 index 暂存空间中,当我们 commit 的时候,test 文件才真正的提交到了 git 数据库。

这里简单的说一下 .gitignore 文件,git 通过 .gitignore 文件决定哪些东西不需要提交。下面是一个 .gitignore 的示例:

1
2
3
4
5
6
7
8
# 不提交后缀名为 tar.gz 的文件
*.tar.gz
# 但是提交名为 release.tar.gz 的文件
!release.tar.gz
# 不提交 target 目录
target/
# 但是提交 target 目录下的 release.jar 文件
!target/release.jar

git 在进行 add 的时候,可以通过正则匹配进行文件的添加。如:

1
2
3
4
# 添加当前目录下的所有内容
$ git add .
# 添加后缀为 .java 的文件
$ git add *.java

git 在进行 commit 的时候,可以选择一次性提交所有暂存的内容,也可以选择性的提交一部分内容:

1
2
3
4
5
6
7
8
# 一次性提交所有的内容
$ git commit -am "提交所有内容"

# 提交 test 文件夹下的内容
$ git commit -m "提交 test 目录下的内容" test/

# 提交后缀名称为 .java 的内容
$ git commit -m "提交后缀名称为 .java 的内容" *.java

分支的正确姿势

git 提供了一个非常重要的功能——分支,基本上通过 Git 建立的工作流都和分支这个功能紧密结合。因为分支的存在,Git 允许我们在开发不同功能特性的时候做代码上的分隔,甚至在进行最新功能的开发时我们依然能够回到过去的功能代码上修复紧急的 BUG。所以能够真正的使用分支,Git 才算是真正的入门了。

分支的新建和合并非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 从当前的分支建立一个新的分支:
$ git checkout -b branch-a

# 合并一个分支到主分支 master
$ git checkout master
$ git merge --no-ff branch-a

# 查看本地所有的分支
$ git branch

# 查看本地及远程分支
$ git branch -a

# 删除分支
$ git branch -d branch-a

很多时候,我们在功能分支进行开发的时候,还需要解决生产或者测试同事提过来的 BUG 。此时,我们可能需要新建一个新的修复分支,并且切换到该分支上快速的修复 BUG 。但是,如果我们功能分支上的开发还没有完成,代码还不能提交怎么办呢?

如果我们在不提交代码的情况下将分支切换到 BUG 修复的分支,我们未提交的代码也会一并切换过去,这个肯定不是我们乐意看到的。那么,解决的办法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 首先储藏我们未完成的工作
$ git stash

# 然后切换到 master 或者具体的版本分支
$ git checkout master

# 建立我们的修复分支
$ git checkout -b hotfix-xxx

# 提交我们的修复,并在测试通过后合并回主分支
$ git commit -m "修复 BUG:xxxx" xxx
$ git checkout master
$ git merge --no-ff hotfix-xxx
$ git branch -d hotfix-xxx

# 回到我们的功能分支,提取储藏的代码,继续我们的功能开发
$ git checkout feature
$ git stash pop

版本控制中经常会遇到的另外一个问题就是冲突。Git 提供了自动合并的功能,但是有一些两个分支均有改动的地方还是需要我们手动去修改。一般来说,需要我们手动解决冲突的文件都会处于 Changes not staged for commit 的状态,合并冲突的时候有一些简单的技巧:

1
2
3
4
5
# 对于 test 文件,完全接受合并过来的分支的更改,放弃当前分支的更改
$ git chekout -3 test

# 对于 test 文件,完全接受当前的分支更改,放弃合并过来的分支的更改
$ git checkout -2 test

对于两个分支的修改都需要接受的情况,只能认为的去审视修改的地方,然后进行合并了。

注意:冲突的时候,所有合并的内容都是处于暂存区的,我们在解决完冲突之后应该 add 处于 Changes not staged for commit 的状态的文件,然后提交本次合并:

1
2
3
$ git add test
$ git commit
# 由于没有 -m 指定提交信息,此时会打开一个 vi 的文件,如果不需要修改备注,直接 `:q` 退出就可以提交成功了

小技巧:合并的时候,最好确认本分支的代码已经全部提交了或者 stash 了,否则解决冲突后再提交的过程中很容易将自己本分支的修改代码作为合并内容的一部分一并提交了。

远程仓库联动

一般来说,协同开发的项目都会有一个远程仓库,我们可以使用 GitLab 或 GitHub 来作为项目的远程仓库,或者搭建自己的 GitLab 私有远程仓库。当然我们的重点不在于如何建立一个远程仓库,而在于如何使用。

如果远程已经有了一个现成的远程仓库,我们可以直接克隆那个仓库:

1
$ git clone https://github.com/mingfer/mingfer.github.io.git

当然,出于礼貌和便于让大家认识你是谁,请在克隆仓库之后,配置好你的用户名称和邮箱名称:

1
2
$ git config --local user.name mingfer
$ git config --local user.email mingfer.cn@gmail.com

如果是本地已有的一个仓库,想要分享到远程仓库。那么我们先到远程建立一个裸仓库:

image-20190519160937849

创建好仓库之后,一般 GitLab 或 GitHub 都会给你一些如何关联本地仓库的提示:

image-20190519161130326

这里,我们需要用到 remote 命令关联远程仓库,然后通过 push 命令推送代码,通过 fetch 和远程进行状态同步,通过 pull 拉取最新的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 关联远程仓库
$ git remote add origin https://github.com/mingfer/demo-git.git

# 推送本地代码到远程
$ git push -u origin master

# 在远程建立分支之后,fetch 一下
$ git fetch
From https://github.com/mingfer/demo-git
 * [new branch]      test       -> origin/test
# 将远程分支 checkout 到本地分支
$ git checkout -b test origin/test

# 从本地删除远程分支
$ git push origin --delete test

# 有些时候别人删除了远程分支,但是我们本地通过 git branch -a 查看的时候还是能够看到被删除的远程分支依然存在,这时需要
$ git remote prune origin

# 将本地分支 push 成为一个远程分支也非常简单,在所在的分支上直接 push 即可
$ git checkout -b test
$ git push -u origin test

手贱的后悔药

人总会有犯傻和手贱的时候,这时候有一份后悔的机会是至关重要的。很开心的是,Git 给了我们在各个阶段后悔的机会。

当我们写了错误的提交注释,或者注释写到一半就手贱回车提交的时候。我们怎么去更改我们的注释呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 手贱的时候
$ git commit -m "手贱了" test
$ git log
commit 08f3bb192a9a3466ce6abff95b7575738e3ff002 (HEAD -> test)
Author: mingfer <mingfer@gmail.com>
Date:   Sun May 19 16:27:25 2019 +0800

    手贱了

# 后悔的机会,更改最近的一次提交的注释
$ git commit --amend -m "手贱治好了"
$ git log
commit 932b66bc581636d4479a1f3d23436b5834637bc9 (HEAD -> test)
Author: mingfer <mingfer@gmail.com>
Date:   Sun May 19 16:27:25 2019 +0800

    手贱治好了

当我们改了一大堆乱七八糟的文件,自己都搞不清楚干了啥,想要完全丢弃的时候。如下所示,我们想撤销对 test 文件的修改,而且不想要 test-2 目录。注意:我们在使用 reset --hard 在丢弃修改的时候,未提交的文件是无法找回的,请务必慎重执行该命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ git status
On branch test
Your branch is ahead of 'origin/test' by 1 commit.
  (use "git push" to publish your local commits)

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   test

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	test-2/

no changes added to commit (use "git add" and/or "git commit -a")

# 对于未跟踪的文件,我们自接删除就好了
$ rm -rf test-2

# 对于已跟踪的文件的修改,我们通过 reset 丢弃
$ git reset --hard HEAD

当我们只想撤销某个文件的修改的时候。

1
$ git checkout -- test

当我们只想撤销某个文件的修改的时候,但是这个文件已经 add 到了暂存区。

1
2
$ git reset HEAD test
$ git checkout -- test

我们需要丢弃最近几次的提交的时候,如下所示,我们想要回到 手贱治好了 那次提交的时候。

1
2
3
4
5
6
7
8
9
10
11
$ git log --oneline
2ac8b87 (HEAD -> test) commit 4
e996414 commit 3
9ac8eba commit 2
932b66b 手贱治好了
4596559 (origin/test, origin/master, master) test

$ git reset --hard 932b66b
$ git log --oneline
932b66b (HEAD -> test) 手贱治好了
4596559 (origin/test, origin/master, master) test

后来我们发现前面的步骤其实是我们又一次的手贱,我们还想重新回到 commit 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 查看我们的操作日志,找到 commit 4 的 commit id : 2ac8b87
$ git reflog
932b66b (HEAD -> test) HEAD@{0}: reset: moving to 932b66b
2ac8b87 HEAD@{1}: commit: commit 4
e996414 HEAD@{2}: commit: commit 3
9ac8eba HEAD@{3}: commit: commit 2
932b66b (HEAD -> test) HEAD@{4}: reset: moving to HEAD
932b66b (HEAD -> test) HEAD@{5}: reset: moving to HEAD
932b66b (HEAD -> test) HEAD@{6}: commit (amend): 手贱治好了
08f3bb1 HEAD@{7}: commit (amend): 手贱了
df307dd HEAD@{8}: commit (amend): 手贱治好了
c84a15f HEAD@{9}: commit (amend): 手贱了
89dd54a HEAD@{10}: commit: 手贱了
4596559 (origin/test, origin/master, master) HEAD@{11}: checkout: moving from master to test
4596559 (origin/test, origin/master, master) HEAD@{12}: commit (initial): test
$ git reset --hard 2ac8b87
$ git log --oneline
2ac8b87 (HEAD -> test) commit 4
e996414 commit 3
9ac8eba commit 2
932b66b 手贱治好了
4596559 (origin/test, origin/master, master) test

我们发现我们本次提交多提交了一些不相关的文件或者少提交了相关的文件,想要重新来过。通过 reset --soft 将 HEAD 移到上一次提交,但是本次提交的索引还存在且还原到了未提交的状态。此时,我们可以选择我们想要的内容重新提交。

1
2
3
4
5
6
7
8
9
10
11
$ git reset --soft HEAD^
$ git status
On branch test
Your branch is ahead of 'origin/test' by 3 commits.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   test

其它骚操作

合并两个不同的项目

开发过程中,有时候远程有了一个初始化的仓库,本地也有一个相关的仓库。例如在 Github 新建一个仓库,初始化了一个 README.md 文件,然后把本地一个写了很久仓库上传。此时使用 git pull 进行远程仓库拉取的时候,会报错 refusing to merge unrelated histories。这是因为 github 的仓库和本地的没有一个共同的 commit ,所以 git 不让提交。此时我们可以使用 --allow-unrelated-histories 选项进行拉取:

1
$ git pull origin master --allow-unrelated-histories

强制远程分支回退

当我们在本地进行了 git reset --hard HEAD~2 操作的时候,在进行 git push 会发现 git 提示我们版本落后了,需要进行 git pull 操作。但是我们此时的需求的确是需要回退我们的 commit,那么可以使用如下命令强制回退远程版本:

1
$ git push origin master --force

查看两次 commit 的文件差异

先使用 git log --oneline 确定我们要比较的两个 commit 的 id,然后进行比较:

  1. 只查看两次 commit 之间更改了哪些文件:git diff 044c04f 667f87a --stat
  2. 查看两次 commit 之间更改的内容:git diff 044c04f 667f87a

Windows 下的用户密码存储

Windows 下存储用户密码之前需要先指定 Windows 下的密码存储助手 wincred

1
$ git config --global credential.helper wincred

然后进行一次 git pullgit push ,在这个操作执行完毕之后,存储用户密码:

1
$ git config --global credential.helper store