常用命令

git官方文档

git基础

简介 - Git教程 - 廖雪峰的官方网站

创建版本库

版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

首先,选择一个合适的地方,创建一个空目录。

1
2
3
4
$ mkdir learngit
$ cd learngit
$ pwd
/Users/michael/learngit

第二步,通过git init命令把这个目录变成Git可以管理的仓库:

1
2
$ git init
Initialized empty Git repository in /Users/michael/learngit/.git/

init之后发现目录下多了一个隐藏的.git目录,这个目录是Git来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件。

把文件添加到版本库:
首先这里再明确一下,所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等,Git也不例外。版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。
不幸的是,Microsoft的Word格式是二进制格式,因此,版本控制系统是没法跟踪Word文件的改动的,前面我们举的例子只是为了演示,如果要真正使用版本控制系统,就要以纯文本方式编写文件。
因为文本是有编码的,比如中文有常用的GBK编码,日文有Shift_JIS编码,如果没有历史遗留问题,强烈建议使用标准的UTF-8编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。

通过vivim创建一个readme.txt文件,输入一些内容。

1
2
Git is a version control system.
git is a free soft ware

一定要在learngit目录下创建,就是该文件必须要在git仓库中。

  1. 用命令git add告诉Git,把文件添加到仓库:
    git add readme.txt
    执行上面的命令,没有任何显示,这就对了,Unix的哲学是“没有消息就是好消息”,说明添加成功。

  2. 用命令git commit告诉Git,把文件提交到仓库:

1
2
3
4
$ git commit -m "wrote a readme file"
[master (root-commit) eaadf4e] wrote a readme file
 1 file changed, 2 insertions(+)
 create mode 100644 readme.txt

简单解释一下git commit命令,-m后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
git commit命令执行成功后会告诉你,1 file changed:1个文件被改动(我们新添加的readme.txt文件);2 insertions:插入了两行内容(readme.txt有两行内容)。

为什么Git添加文件需要addcommit一共两步呢?因为commit可以一次提交很多文件,所以你可以多次add不同的文件,比如:

1
2
3
$ git add file1.txt
$ git add file2.txt file3.txt
$ git commit -m "add 3 files."

时光机穿梭

我们已经成功地添加并提交了一个readme.txt文件,现在是时候继续工作了,于是我们继续修改readme.txt文件,修改如下:

1
2
Git is a distributed version control system.
Git is free software.

现在,运行git status命令看看结果:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
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:   readme.txt

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

git status命令可以让我们时刻掌握仓库当前的状态,上面的命令输出告诉我们,readme.txt被修改过了,但还没有准备提交的修改。

虽然Git告诉我们readme.txt被修改了,但如果能看看具体修改了什么内容,自然是很好的。比如你休假两周从国外回来,第一天上班时,已经记不清上次怎么修改的readme.txt,所以,需要用git diff这个命令看看:

1
2
3
4
5
6
7
8
9
$ git diff readme.txt 
diff --git a/readme.txt b/readme.txt
index 46d49bf..9247db6 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
-Git is a version control system.
+Git is a distributed version control system.
 Git is free software.

git diff顾名思义就是查看difference,显示的格式正是Unix通用的diff格式,可以从上面的命令输出看到,我们在第一行添加了一个distributed单词。
知道了对readme.txt作了什么修改后,再把它提交到仓库就放心多了,提交修改和提交新文件是一样的两步,第一步是git add
$ git add readme.txt

同样没有任何输出。在执行第二步git commit之前,我们再运行git status看看当前仓库的状态:

1
2
3
4
5
6
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   readme.txt

git status告诉我们,将要被提交的修改包括readme.txt,下一步,就可以放心地提交了:

1
2
3
$ git commit -m "add distributed"
[master e475afc] add distributed
 1 file changed, 1 insertion(+), 1 deletion(-)

提交后,我们再用git status命令看看仓库的当前状态:

1
2
3
$ git status
On branch master
nothing to commit, working tree clean

Git告诉我们当前没有需要提交的修改,而且,工作目录是干净(working tree clean)的。

小结:

  • 要随时掌握工作区的状态,使用git status命令。
  • 如果git status告诉你有文件被修改过,用git diff可以查看修改内容。

版本回退

现在,你已经学会了修改文件,然后把修改提交到Git版本库,现在,再练习一次,修改readme.txt文件如下:

1
2
Git is a distributed version control system.
Git is free software distributed under the GPL.

然后提交。

像这样,你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩RPG游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打Boss之前,你会手动存盘,以便万一打Boss失败了,可以从最近的地方重新开始。Git也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为commit。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

使用git log可以告诉我们历史记录:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ git log
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master)
Author: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 21:06:15 2018 +0800

    append GPL

commit e475afc93c209a690c39c13a46716e8fa000c366
Author: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 21:03:36 2018 +0800

    add distributed

commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 20:59:18 2018 +0800

    wrote a readme file

git log命令显示从最近到最远的提交日志,我们可以看到3次提交,最近的一次是append GPL,上一次是add distributed,最早的一次是wrote a readme file。
如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--pretty=oneline参数:

1
2
3
4
$ git log --pretty=oneline
1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master) append GPL
e475afc93c209a690c39c13a46716e8fa000c366 add distributed
eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0 wrote a readme file

前边一大串类似1094adb...的是commit id(版本号),Git的commit id不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示。
每提交一个新版本,实际上Git就会把它们自动串成一条时间线。如果使用可视化工具查看Git历史,就可以更清楚地看到提交历史的时间线。
好了,现在我们启动时光穿梭机,准备把readme.txt回退到上一个版本,也就是add distributed的那个版本,怎么做呢?
首先,Git必须知道当前版本是哪个版本,在Git中,用HEAD表示当前版本,也就是最新的提交1094adb…(注意我的提交ID和你的肯定不一样),上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来,所以写成HEAD~100。
现在,我们要把当前版本append GPL回退到上一个版本add distributed,就可以使用git reset命令:

1
2
$ git reset --hard HEAD^
HEAD is now at e475afc add distributed

看看readme.txt的内容是不是版本add distributed:

1
2
3
$ cat readme.txt
Git is a distributed version control system.
Git is free software.

果然被还原了。
还可以继续回退到上一个版本wrote a readme file,不过且慢,然我们用git log再看看现在版本库的状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ git log
commit e475afc93c209a690c39c13a46716e8fa000c366 (HEAD -> master)
Author: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 21:03:36 2018 +0800

    add distributed

commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 20:59:18 2018 +0800

    wrote a readme file

最新的那个版本append GPL已经看不到了!好比你从21世纪坐时光穿梭机来到了19世纪,想再回去已经回不去了,肿么办?
办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个append GPL的commit id是1094adb…,于是就可以指定回到未来的某个版本:

1
2
$ git reset --hard 1094a
HEAD is now at 83b0afe append GPL

版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。
再小心翼翼地看看readme.txt的内容:

1
2
3
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.

果然,又回到了最新的版本。
Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD指针,当你回退版本的时候,Git仅仅是把HEAD从指向append GPL:

1
2
3
4
5
6
7
8
9
┌────┐
│HEAD│
└────┘
   └──> ○ append GPL
        ○ add distributed
        ○ wrote a readme file

改为指向add distributed:

1
2
3
4
5
6
7
8
9
┌────┐
│HEAD│
└────┘
   │    ○ append GPL
   │    │
   └──> ○ add distributed
        ○ wrote a readme file

然后顺便把工作区的文件更新了。所以你让HEAD指向哪个版本号,你就把当前版本定位在哪。
现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的commit id怎么办?
在Git中,总是有后悔药可以吃的。当你用$ git reset --hard HEAD^回退到add distributed版本时,再想恢复到append GPL,就必须找到append GPL的commit id。Git提供了一个命令git reflog用来记录你的每一次命令:

1
2
3
4
5
$ git reflog
e475afc HEAD@{1}: reset: moving to HEAD^
1094adb (HEAD -> master) HEAD@{2}: commit: append GPL
e475afc HEAD@{3}: commit: add distributed
eaadf4e HEAD@{4}: commit (initial): wrote a readme file

终于舒了口气,从输出可知,append GPL的commit id是1094adb,现在,你又可以乘坐时光机回到未来了。

小结:

  • HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id
  • 穿梭前,用git log可以查看提交历史,以便确定要回退到哪个版本。
  • 要重返未来,用git reflog查看命令历史,以便确定要回到未来的哪个版本。

工作区和暂存区

Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念。
工作区(Working Directory):
就是你在电脑里能看到的目录,比如我的learngit文件夹就是一个工作区。

版本库(Repository):
工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD

分支和HEAD的概念我们以后再讲。
前面讲了我们把文件往Git版本库里添加的时候,是分两步执行的:
第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit就是往master分支上提交更改。
你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。

俗话说,实践出真知。现在,我们再练习一遍,先对readme.txt做个修改,比如加上一行内容:

1
2
3
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.

然后,在工作区新增一个LICENSE文本文件(内容随便写)。

先用git status查看一下状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ git status
On branch master
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:   readme.txt

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

    LICENSE

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

Git非常清楚地告诉我们,readme.txt被修改了,而LICENSE还从来没有被添加过,所以它的状态是Untracked
现在,使用两次命令git add,把readme.txtLICENSE都添加后,用git status再查看一下:

1
2
3
4
5
6
7
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   LICENSE
    modified:   readme.txt

现在,暂存区的状态就变成这样了:

所以,git add命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit就可以一次性把暂存区的所有修改提交到分支。

1
2
3
4
$ git commit -m "understand how stage works"
[master e43a48b] understand how stage works
 2 files changed, 2 insertions(+)
 create mode 100644 LICENSE

一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的:

1
2
3
$ git status
On branch master
nothing to commit, working tree clean

现在版本库变成了这样,暂存区就没有任何内容了:

管理修改

为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。
你会问,什么是修改?比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。
为什么说Git管理的是修改,而不是文件呢?我们还是做实验。第一步,对readme.txt做一个修改,比如加一行内容:

1
2
3
4
5
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes.

然后,添加:

1
2
3
4
5
6
7
8
$ git add readme.txt
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       modified:   readme.txt
#

然后,再修改readme.txt:

1
2
3
4
5
$ cat readme.txt 
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.

提交:

1
2
3
$ git commit -m "git tracks changes"
[master 519219b] git tracks changes
 1 file changed, 1 insertion(+)

提交后,再看看状态:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
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:   readme.txt

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

咦,怎么第二次的修改没有被提交?
别激动,我们回顾一下操作过程:
第一次修改 -> git add -> 第二次修改 -> git commit
你看,我们前面讲了,Git管理的是修改,当你用git add命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。
提交后,用git diff HEAD -- readme.txt命令可以查看工作区和版本库里面最新版本的区别:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ git diff HEAD -- readme.txt 
diff --git a/readme.txt b/readme.txt
index 76d770f..a9c5755 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,4 @@
 Git is a distributed version control system.
 Git is free software distributed under the GPL.
 Git has a mutable index called stage.
-Git tracks changes.
+Git tracks changes of files.

可见,第二次修改确实没有被提交。
那怎么提交第二次修改呢?你可以继续git add再git commit,也可以别着急提交第一次修改,先git add第二次修改,再git commit,就相当于把两次修改合并后一块提交了:
第一次修改 -> git add -> 第二次修改 -> git add -> git commit

小结:
每次修改,如果不用git add到暂存区,那就不会加入到commit中。

撤销修改

自然,你是不会犯错的。不过现在是凌晨两点,你正在赶一份工作报告,你在readme.txt中添加了一行:

1
2
3
4
5
6
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
My stupid boss still prefers SVN.

在你准备提交前,一杯咖啡起了作用,你猛然发现了stupid boss可能会让你丢掉这个月的奖金!
既然错误发现得很及时,就可以很容易地纠正它。你可以删掉最后一行,手动把文件恢复到上一个版本的状态。如果用git status查看一下:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
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:   readme.txt

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

你可以发现,Git会告诉你,git checkout -- file可以丢弃工作区的修改:
git checkout -- readme.txt
命令git checkout -- readme.txt意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:
一种是readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次git commit或git add时的状态。
现在,看看readme.txt的文件内容:

1
2
3
4
5
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.

git checkout -- file命令中的–很重要,没有--,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到git checkout命令。

现在假定是凌晨3点,你不但写了一些胡话,还git add到暂存区了:

1
2
3
4
5
6
7
8
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
My stupid boss still prefers SVN.

$ git add readme.txt

庆幸的是,在commit之前,你发现了这个问题。用git status查看一下,修改只是添加到了暂存区,还没有提交:

1
2
3
4
5
6
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   readme.txt

Git同样告诉我们,用命令git reset HEAD <file>可以把暂存区的修改撤销掉(unstage),重新放回工作区:

1
2
3
$ git reset HEAD readme.txt
Unstaged changes after reset:
M    readme.txt

git reset命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本。
再用git status查看一下,现在暂存区是干净的,工作区有修改:

1
2
3
4
5
6
7
$ git status
On branch master
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:   readme.txt

还记得如何丢弃工作区的修改吗?

1
2
3
4
5
$ git checkout -- readme.txt

$ git status
On branch master
nothing to commit, working tree clean

现在,假设你不但改错了东西,还从暂存区提交到了版本库,怎么办呢?还记得版本回退一节吗?可以回退到上一个版本。不过,这是有条件的,就是你还没有把自己的本地版本库推送到远程。还记得Git是分布式版本控制系统吗?我们后面会讲到远程版本库,一旦你把stupid boss提交推送到远程版本库,你就真的惨了。

小结:

  • 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout -- file
  • 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD <file>,就回到了场景1,第二步按场景1操作。
  • 场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。

删除文件

在Git中,删除也是一个修改操作,我们实战一下,先添加一个新文件test.txt到Git并且提交:

1
2
3
4
5
6
$ git add test.txt

$ git commit -m "add test.txt"
[master b84166e] add test.txt
 1 file changed, 1 insertion(+)
 create mode 100644 test.txt

一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用rm命令删了:
$ rm test.txt
这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    deleted:    test.txt

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

现在你有两个选择,一是确实要从版本库中删除该文件,那就用命令git rm删掉,并且git commit:

1
2
3
4
5
6
7
$ git rm test.txt
rm 'test.txt'

$ git commit -m "remove test.txt"
[master d46f35e] remove test.txt
 1 file changed, 1 deletion(-)
 delete mode 100644 test.txt

现在,文件就从版本库中被删除了。

另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:
$ git checkout -- test.txt

git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。

小结:
命令git rm用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。
因为你根本就没有提交到版本库,人家怎么知道你改了什么。

远程仓库

  1. 创建SSH Key
    在用户主目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsa和id_rsa.pub这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开终端Terminal(Windows下打开Git Bash),创建SSH Key:
    ssh-keygen -t rsa -C "youremail@example.com"
    你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。
    如果一切顺利的话,可以在用户主目录里找到.ssh目录,里面有id_rsa和id_rsa.pub两个文件,这两个就是SSH Key的秘钥对,id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,可以放心地告诉任何人。

  2. 登陆GitHub,打开“Settings”,“SSH Keys”页面:
    然后,点“New SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容,点“Add SSH Key”,你就应该看到已经添加的Key。
    为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。

当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。
最后友情提示,在GitHub上免费托管的Git仓库,任何人都可以看到喔(但只有你自己才能改)。所以,不要把敏感信息放进去。
如果你不想让别人看到Git库,有两个办法,一个是交点保护费,让GitHub把公开的仓库变成私有的,这样别人就看不见了(不可读更不可写)。另一个办法是自己动手,搭一个Git服务器,因为是你自己的Git服务器,所以别人也是看不见的。这个方法我们后面会讲到的,相当简单,公司内部开发必备。
确保你拥有一个GitHub账号后,我们就即将开始远程仓库的学习。

添加远程仓库

现在的情景是,你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。

首先,登陆GitHub,然后找到新建仓库的按钮(因为github版本变化,按钮位置可能会变化),根据提示创建一个仓库learngit
目前,在GitHub上的这个learngit仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。
现在,我们根据GitHub的提示,在本地的learngit仓库下运行命令:
git remote add origin git@github.com:ayanamiq/learngit.git

添加后,远程库的名字就是origin,这是Git默认的叫法,也可以改成别的,但是origin这个名字一看就知道是远程库。
下一步,就可以把本地库的所有内容推送到远程库上:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ git push -u origin master
Counting objects: 20, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (20/20), 1.64 KiB | 560.00 KiB/s, done.
Total 20 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), done.
To github.com:michaelliao/learngit.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。
由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。
推送成功后,可以立刻在GitHub页面中看到远程库的内容已经和本地一模一样。

从现在起,只要本地作了提交,就可以通过命令。
git push origin master
把本地master分支的最新修改推送至GitHub,现在,你就拥有了真正的分布式版本库!

  • SSH警告
    当你第一次使用Git的clone或者push命令连接GitHub时,会得到一个警告:
1
2
3
The authenticity of host 'github.com (xx.xx.xx.xx)' can't be established.
RSA key fingerprint is xx.xx.xx.xx.xx.
Are you sure you want to continue connecting (yes/no)?

这是因为Git使用SSH连接,而SSH连接在第一次验证GitHub服务器的Key时,需要你确认GitHub的Key的指纹信息是否真的来自GitHub的服务器,输入yes回车即可。
Git会输出一个警告,告诉你已经把GitHub的Key添加到本机的一个信任列表里了:
Warning: Permanently added 'github.com' (RSA) to the list of known hosts.

这个警告只会出现一次,后面的操作就不会有任何警告了。
如果你实在担心有人冒充GitHub服务器,输入yes前可以对照GitHub的RSA Key的指纹信息是否与SSH连接给出的一致。

小结:

  • 要关联一个远程库,使用命令git remote add origin git@server-name:path/repo-name.git
  • 关联后,使用命令git push -u origin master第一次推送master分支的所有内容;

此后,每次本地提交后,只要有必要,就可以使用命令git push origin master推送最新修改;
分布式版本系统的最大好处之一是在本地工作完全不需要考虑远程库的存在,也就是有没有联网都可以正常工作,而SVN在没有联网的时候是拒绝干活的!当有网络的时候,再把本地提交推送一下就完成了同步,真是太方便了!

修改远程仓库

今天拉coding代码,发现coding仓库地址竟然变了。

进入项目根目录,查看远程仓库地址
git remote -v
设置远程仓库地址
git remote set-url origin http://192.168.100.235:9797/john/git_test.git
删除远程仓库
git remote rm origin
重新添加
git remote add origin http://192.168.100.235:9797/john/git_test.git

从远程库克隆

上次我们讲了先有本地库,后有远程库的时候,如何关联远程库。
现在,假设我们从零开发,那么最好的方式是先创建远程库,然后,从远程库克隆。

首先,登陆GitHub,创建一个新的仓库,名字叫gitskills。
现在,远程库已经准备好了,下一步是用命令git clone克隆一个本地库:

1
2
3
4
5
$ git clone git@github.com:michaelliao/gitskills.git
Cloning into 'gitskills'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 3
Receiving objects: 100% (3/3), done.

注意把Git库的地址换成你自己的,然后进入gitskills目录看看,已经有README.md文件了:

1
2
3
$ cd gitskills
$ ls
README.md

如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。
你也许还注意到,GitHub给出的地址不止一个,还可以用https://github.com/michaelliao/gitskills.git这样的地址。
实际上,Git支持多种协议,默认的git://使用ssh,但也可以使用https等其他协议。
使用https除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http端口的公司内部就无法使用ssh协议而只能用https。

小结:

  • 要克隆一个仓库,首先必须知道仓库的地址,然后使用git clone命令克隆。
  • Git支持多种协议,包括https,但通过ssh支持的原生git协议速度最快。

分支管理

分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN。
如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了Git又学会了SVN!

分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
其他版本控制系统如SVN等都有分支管理,但是用过之后你会发现,这些版本控制系统创建和切换分支比蜗牛还慢,简直让人无法忍受,结果分支功能成了摆设,大家都不去用。
但Git的分支是与众不同的,无论创建、切换和删除分支,Git在1秒钟之内就能完成!无论你的版本库是1个文件还是1万个文件。

分支的创建与合并

在版本回退里,你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master分支。HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。

一开始,master分支是一条线,git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点:

当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:

你看,Git创建一个分支很快,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化!
不过,从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:

假如我们在dev上的工作完成了,就可以把dev合并到master上。Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:

所以Git合并分支也很快!就改改指针,工作区内容也不变!

合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支:

真是太神奇了,你看得出来有些提交是通过分支完成的吗?

下面开始实战。
首先,我们创建dev分支,然后切换到dev分支:

1
2
$ git checkout -b dev
Switched to a new branch 'dev'

git checkout命令加上-b参数表示创建并切换,相当于以下两条命令:

1
2
3
$ git branch dev
$ git checkout dev
Switched to branch 'dev'

然后,用git branch命令查看当前分支:

1
2
3
$ git branch
* dev
  master

git branch命令会列出所有分支,当前分支前面会标一个*号。

然后,我们就可以在dev分支上正常提交,比如对readme.txt做个修改,加上一行:
Creating a new branch is quick.

然后提交:

1
2
3
4
$ git add readme.txt 
$ git commit -m "branch test"
[dev b17d20e] branch test
 1 file changed, 1 insertion(+)

现在,dev分支的工作完成,我们就可以切换回master分支:

1
2
$ git checkout master
Switched to branch 'master'

切换回master分支后,再查看一个readme.txt文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变:

现在,我们把dev分支的工作成果合并到master分支上:

1
2
3
4
5
$ git merge dev
Updating d46f35e..b17d20e
Fast-forward
 readme.txt | 1 +
 1 file changed, 1 insertion(+)

git merge命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。
注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward,我们后面会讲其他方式的合并。
合并完成后,就可以放心地删除dev分支了:

1
2
$ git branch -d dev
Deleted branch dev (was b17d20e).

删除后,查看branch,就只剩下master分支了:

1
2
$ git branch
* master

因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。
小结:
Git鼓励大量使用分支:

1
2
3
4
5
6
- 查看分支:git branch
- 创建分支:git branch <name>
- 切换分支:git checkout <name> 或 git switch <name>
- 创建+切换分支:git checkout -b <name> 或 git switch -c <name>
- 合并某分支到当前分支:git merge <name>
- 删除分支:git branch -d <name>

解决冲突

人生不如意之事十之八九,合并分支往往也不是一帆风顺的。
准备新的feature1分支,继续我们的新分支开发:

1
2
$ git checkout -b feature1
Switched to a new branch 'feature1'

修改readme.txt最后一行,改为:
Creating a new branch is quick AND simple.

在feature1分支上提交:

1
2
3
4
5
$ git add readme.txt

$ git commit -m "AND simple"
[feature1 14096d0] AND simple
 1 file changed, 1 insertion(+), 1 deletion(-)

切换到master分支:

1
2
3
4
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

Git还会自动提示我们当前master分支比远程的master分支要超前1个提交。
在master分支上把readme.txt文件的最后一行改为:
Creating a new branch is quick & simple.
提交:

1
2
3
4
$ git add readme.txt 
$ git commit -m "& simple"
[master 5dc6824] & simple
 1 file changed, 1 insertion(+), 1 deletion(-)

现在,master分支和feature1分支各自都分别有新的提交,变成了这样:

这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:

1
2
3
4
$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

果然冲突了!Git告诉我们,readme.txt文件存在冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:   readme.txt

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

我们可以直接查看readme.txt的内容:

1
2
3
4
5
6
7
8
9
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1

git用<<<<<<<=======>>>>>>>标记出不同分支的内容,我们修改如下后保存:
Creating a new branch is quick and simple.
再提交:

1
2
3
$ git add readme.txt 
$ git commit -m "conflict fixed"
[master cf810e4] conflict fixed

现在,master分支和feature1分支变成了下图所示:

用带参数的git log也可以看到分支的合并情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ git log --graph --pretty=oneline --abbrev-commit
*   cf810e4 (HEAD -> master) conflict fixed
|\
| * 14096d0 (feature1) AND simple
* | 5dc6824 & simple
|/
* b17d20e branch test
* d46f35e (origin/master) remove test.txt
* b84166e add test.txt
* 519219b git tracks changes
* e43a48b understand how stage works
* 1094adb append GPL
* e475afc add distributed
* eaadf4e wrote a readme file

最后,删除feature1分支:

1
2
$ git branch -d feature1
Deleted branch feature1 (was 14096d0).

工作完成。

小结:

  • 当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。
  • 解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。
  • 用git log –graph命令可以看到分支合并图。

分支管理策略

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。
下面我们实战一下--no-ff方式的git merge
首先,仍然创建并切换dev分支:

1
2
$ git checkout -b dev
Switched to a new branch 'dev'

修改readme.txt文件,并提交一个新的commit:

1
2
3
4
$ git add readme.txt 
$ git commit -m "add merge"
[dev f52c633] add merge
 1 file changed, 1 insertion(+)

现在,我们切换回master:

1
2
$ git checkout master
Switched to branch 'master'

准备合并dev分支,请注意--no-ff参数,表示禁用Fast forward

1
2
3
4
$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
 readme.txt | 1 +
 1 file changed, 1 insertion(+)

因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。
合并后,我们用git log看看分支历史:

1
2
3
4
5
6
7
$ git log --graph --pretty=oneline --abbrev-commit
*   e1e9c68 (HEAD -> master) merge with no-ff
|\
| * f52c633 (dev) add merge
|/
*   cf810e4 conflict fixed
...

可以看到,不使用Fast forward模式,merge后就像这样:

分支策略

在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;
你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。
所以,团队合作的分支看起来就像这样:

小结:

  • Git分支十分强大,在团队开发中应该充分应用。
  • 合并分支时,加上–no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。

bug分支

软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101来修复它,但是,等等,当前正在dev上进行的工作还没有提交:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ git status
On branch dev
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   hello.py

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:   readme.txt

并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?
幸好,Git还提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:

1
2
$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge

现在,用git status查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。
首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支:

1
2
3
4
5
6
7
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)

$ git checkout -b issue-101
Switched to a new branch 'issue-101'

现在修复bug,需要把“Git is free software …”改为“Git is a free software …”,然后提交:

1
2
3
4
$ git add readme.txt 
$ git commit -m "fix bug 101"
[issue-101 4c805e2] fix bug 101
 1 file changed, 1 insertion(+), 1 deletion(-)

修复完成后,切换到master分支,并完成合并,最后删除issue-101分支:

1
2
3
4
5
6
7
8
9
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)

$ git merge --no-ff -m "merged bug fix 101" issue-101
Merge made by the 'recursive' strategy.
 readme.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

太棒了,原计划两个小时的bug修复只花了5分钟!现在,是时候接着回到dev分支干活了!

1
2
3
4
5
6
$ git checkout dev
Switched to branch 'dev'

$ git status
On branch dev
nothing to commit, working tree clean

工作区是干净的,刚才的工作现场存到哪去了?用git stash list命令看看:

1
2
$ git stash list
stash@{0}: WIP on dev: f52c633 add merge

工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:
一是用git stash apply恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除;
另一种方式是用git stash pop,恢复的同时把stash内容也删了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ git stash pop
On branch dev
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   hello.py

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:   readme.txt

Dropped refs/stash@{0} (5d677e2ee266f39ea296182fb2354265b91b3b2a)

再用git stash list查看,就看不到任何stash内容了:
$ git stash list

你可以多次stash,恢复的时候,先用git stash list查看,然后恢复指定的stash,用命令:
$ git stash apply stash@{0}

清除所有的stash:
先查看所有的stash:git stash list
清除所有stash:git stash clear
选择清除:git stash drop stash@{0}这是删除第一个队列

在master分支上修复了bug后,我们要想一想,dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。
那怎么在dev分支上修复同样的bug?重复操作一次,提交不就行了?
有木有更简单的方法?有!
同样的bug,要在dev上修复,我们只需要把4c805e2 fix bug 101这个提交所做的修改“复制”到dev分支。注意:我们只想复制4c805e2 fix bug 101这个提交所做的修改,并不是把整个master分支merge过来。
为了方便操作,Git专门提供了一个cherry-pick命令,让我们能复制一个特定的提交到当前分支:

1
2
3
4
5
6
$ git branch
* dev
  master
$ git cherry-pick 4c805e2
[master 1d4b803] fix bug 101
 1 file changed, 1 insertion(+), 1 deletion(-)

Git自动给dev分支做了一次提交,注意这次提交的commit是1d4b803,它并不同于master的4c805e2,因为这两个commit只是改动相同,但确实是两个不同的commit。用git cherry-pick,我们就不需要在dev分支上手动再把修bug的过程重复一遍。

有些聪明的童鞋会想了,既然可以在master分支上修复bug后,在dev分支上可以“重放”这个修复过程,那么直接在dev分支上修复bug,然后在master分支上“重放”行不行?当然可以,不过你仍然需要git stash命令保存现场,才能从dev分支切换到master分支。

小结:

  • 修复bug时,我们会通过创建新的bug分支进行修复,然后合并,最后删除;
  • 当手头工作没有完成时,先把工作现场git stash一下,然后去修复bug,修复后,再git stash pop,回到工作现场。
  • 在master分支上修复的bug,想要合并到当前dev分支,可以用git cherry-pick <commit>命令,把bug提交的修改“复制”到当前分支,避免重复劳动。

部分参考

Feature分支

软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。
现在,你终于接到了一个新任务:开发代号为 Vulcan 的新功能,该功能计划用于下一代星际飞船。
于是准备开发:

1
2
$ git checkout -b feature-vulcan
Switched to a new branch 'feature-vulcan'

5分钟后,开发完毕:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ git add vulcan.py

$ git status
On branch feature-vulcan
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   vulcan.py

$ git commit -m "add feature vulcan"
[feature-vulcan 287773e] add feature vulcan
 1 file changed, 2 insertions(+)
 create mode 100644 vulcan.py

切回dev,准备合并:
$ git checkout dev
一切顺利的话,feature分支和bug分支是类似的,合并,然后删除。

但是!
就在此时,接到上级命令,因经费不足,新功能必须取消!
虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:

1
2
3
$ git branch -d feature-vulcan
error: The branch 'feature-vulcan' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature-vulcan'.

销毁失败。Git友情提醒,feature-vulcan分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D参数。。

现在我们强行删除:

1
2
$ git branch -D feature-vulcan
Deleted branch feature-vulcan (was 287773e).

终于删除成功。

小结:

  • 开发一个新feature,最好新建一个分支;
  • 如果要丢弃一个没有被合并过的分支,可以通过git branch -D <name>强行删除。

多人协作

当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin。

要查看远程库的信息,用git remote:

1
2
$ git remote
origin

或者,用git remote -v显示更详细的信息:

1
2
3
$ git remote -v
origin  git@github.com:michaelliao/learngit.git (fetch)
origin  git@github.com:michaelliao/learngit.git (push)

上面显示了可以抓取和推送的origin的地址。如果没有推送权限,就看不到push的地址。

推送分支(本地到远程)

推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
$ git push origin master

如果要推送其他分支,比如dev,就改成:
$ git push origin dev

但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?

  • master分支是主分支,因此要时刻与远程同步;
  • dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
  • bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
  • feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。

总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!

抓取分支

多人协作时,大家都会往master和dev分支上推送各自的修改。
现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆:

1
2
3
4
5
6
7
$ git clone git@github.com:michaelliao/learngit.git
Cloning into 'learngit'...
remote: Counting objects: 40, done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 40 (delta 14), reused 40 (delta 14), pack-reused 0
Receiving objects: 100% (40/40), done.
Resolving deltas: 100% (14/14), done.

当你的小伙伴从远程库clone时,默认情况下,你的小伙伴只能看到本地的master分支。不信可以用git branch命令看看:

1
2
$ git branch
* master

现在,你的小伙伴要在dev分支上开发,就必须创建远程origin的dev分支到本地,于是他用这个命令创建本地dev分支:
$ git checkout -b dev origin/dev
现在,他就可以在dev上继续修改,然后,时不时地把dev分支push到远程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ git add env.txt

$ git commit -m "add env"
[dev 7a5e5dd] add env
 1 file changed, 1 insertion(+)
 create mode 100644 env.txt

$ git push origin dev
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 308 bytes | 308.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
   f52c633..7a5e5dd  dev -> dev

你的小伙伴已经向origin/dev分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ cat env.txt
env

$ git add env.txt

$ git commit -m "add new env"
[dev 7bd91f1] add new env
 1 file changed, 1 insertion(+)
 create mode 100644 env.txt

$ git push origin dev
To github.com:michaelliao/learngit.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@github.com:michaelliao/learngit.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

推送失败,因为你的小伙伴的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git已经提示我们,先用git pull把最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突,再推送:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ git pull
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

    git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=origin/<branch> dev

git pull也失败了,原因是没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置dev和origin/dev的链接:

1
2
$ git branch --set-upstream-to=origin/dev dev
Branch 'dev' set up to track remote branch 'dev' from 'origin'.

再pull:

1
2
3
4
$ git pull
Auto-merging env.txt
CONFLICT (add/add): Merge conflict in env.txt
Automatic merge failed; fix conflicts and then commit the result.

这回git pull成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ git commit -m "fix env conflict"
[dev 57c53ab] fix env conflict

$ git push origin dev
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 621 bytes | 621.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
   7a5e5dd..57c53ab  dev -> dev

因此,多人协作的工作模式通常是这样:

  1. 首先,可以试图用git push origin <branch-name>推送自己的修改;
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用git push origin <branch-name>推送就能成功!

如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>
这就是多人协作的工作模式,一旦熟悉了,就非常简单。

小结

  • 查看远程库信息,使用git remote -v
  • 本地新建的分支如果不推送到远程,对其他人就是不可见的;
  • 从本地推送分支,使用git push origin branch-name,如果推送失败,先用git pull抓取远程的新提交;
  • 在本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致;
  • 建立本地分支和远程分支的关联,使用git branch --set-upstream branch-name origin/branch-name
  • 从远程抓取分支,使用git pull,如果有冲突,要先处理冲突。

Rebase

在上一节我们看到了,多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的童鞋不得不先pull,在本地合并,然后才能push成功。
每次合并再push后,分支变成了这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ git log --graph --pretty=oneline --abbrev-commit
* d1be385 (HEAD -> master, origin/master) init hello
*   e5e69f1 Merge branch 'dev'
|\
| *   57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
| |/
* |   12a631b merged bug fix 101
|\ \
| * | 4c805e2 fix bug 101
|/ /
* |   e1e9c68 merge with no-ff
|\ \
| |/
| * f52c633 add merge
|/
*   cf810e4 conflict fixed

总之看上去很乱,有强迫症的童鞋会问:为什么Git的提交历史不能是一条干净的直线?
其实是可以做到的!
Git有一种称为rebase的操作,有人把它翻译成“变基”。
先不要随意展开想象。我们还是从实际问题出发,看看怎么把分叉的提交变成直线。
在和远程分支同步后,我们对hello.py这个文件做了两次提交。用git log命令看看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ git log --graph --pretty=oneline --abbrev-commit
* 582d922 (HEAD -> master) add author
* 8875536 add comment
* d1be385 (origin/master) init hello
*   e5e69f1 Merge branch 'dev'
|\
| *   57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
...

注意到Git用(HEAD -> master)(origin/master)标识出当前分支的HEAD和远程origin的位置分别是582d922 add authord1be385 init hello,本地分支比远程分支快两个提交。

现在我们尝试推送本地分支:

1
2
3
4
5
6
7
8
9
$ git push origin master
To github.com:michaelliao/learngit.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'git@github.com:michaelliao/learngit.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

很不幸,失败了,这说明有人先于我们推送了远程分支。按照经验,先pull一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ git pull
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:michaelliao/learngit
   d1be385..f005ed4  master     -> origin/master
 * [new tag]         v1.0       -> v1.0
Auto-merging hello.py
Merge made by the 'recursive' strategy.
 hello.py | 1 +
 1 file changed, 1 insertion(+)

再用git status看看状态:

1
2
3
4
5
6
$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交。
用git log看看:

1
2
3
4
5
6
7
8
9
$ git log --graph --pretty=oneline --abbrev-commit
*   e0ea545 (HEAD -> master) Merge branch 'master' of github.com:michaelliao/learngit
|\
| * f005ed4 (origin/master) set exit=1
* | 582d922 add author
* | 8875536 add comment
|/
* d1be385 init hello
...

对强迫症童鞋来说,现在事情有点不对头,提交历史分叉了。如果现在把本地分支push到远程,
有没有问题?有!
什么问题?不好看!
有没有解决方法?有!

这个时候,rebase就派上了用场。我们输入命令git rebase试试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ git rebase
First, rewinding head to replay your work on top of it...
Applying: add comment
Using index info to reconstruct a base tree...
M    hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py
Applying: add author
Using index info to reconstruct a base tree...
M    hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py

输出了一大堆操作,到底是啥效果?再用git log看看:

1
2
3
4
5
6
$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master) add author
* 3611cfe add comment
* f005ed4 (origin/master) set exit=1
* d1be385 init hello
...

原本分叉的提交现在变成一条直线了!这种神奇的操作是怎么实现的?其实原理非常简单。我们注意观察,发现Git把我们本地的提交“挪动”了位置,放到了f005ed4 (origin/master) set exit=1之后,这样,整个提交历史就成了一条直线。rebase操作前后,最终的提交内容是一致的,但是,我们本地的commit修改内容已经变化了,它们的修改不再基于d1be385 init hello,而是基于f005ed4 (origin/master) set exit=1,但最后的提交7e61ed4内容是一致的。

这就是rebase操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。最后,通过push操作把本地分支推送到远程:

1
2
3
4
5
6
7
8
9
Mac:~/learngit michael$ git push origin master
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 576 bytes | 576.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To github.com:michaelliao/learngit.git
   f005ed4..7e61ed4  master -> master

再用git log看看效果:

1
2
3
4
5
6
$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master, origin/master) add author
* 3611cfe add comment
* f005ed4 set exit=1
* d1be385 init hello
...

远程分支的提交历史也是一条直线。

小结

  • rebase操作可以把本地未push的分叉提交历史整理成直线;
  • rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。

其他rebase的说明

参考地址:http://gitbook.liuhui998.com/4_2.html
假设你现在基于远程分支"origin",创建一个叫"mywork"的分支。
$ git checkout -b mywork origin

现在我们在这个分支做一些修改,然后生成两个提交(commit):

1
2
3
4
5
$ vi file.txt
$ git commit
$ vi otherfile.txt
$ git commit
...

但是与此同时,有些人也在"origin"分支上做了一些修改并且做了提交了。这就意味着"origin"和"mywork"这两个分支各自"前进"了,它们之间"分叉"了。

在这里,你可以用"pull"命令把"origin"分支上的修改拉下来并且和你的修改合并; 结果看起来就像一个新的"合并的提交"(merge commit):

但是,如果你想让"mywork"分支历史看起来像没有经过任何合并一样,你也许可以用 git rebase:

1
2
$ git checkout mywork
$ git rebase origin

这些命令会把你的"mywork"分支里的每个提交(commit)取消掉,并且把它们临时 保存为补丁(patch)(这些补丁放到".git/rebase"目录中),然后把"mywork"分支更新 到最新的"origin"分支,最后把保存的这些补丁应用到"mywork"分支上。

当’mywork’分支更新之后,它会指向这些新创建的提交(commit),而那些老的提交会被丢弃。 如果运行垃圾收集命令(pruning garbage collection), 这些被丢弃的提交就会删除。(请查看 git gc)

现在我们可以看一下用合并(merge)和用rebase所产生的历史的区别:

在rebase的过程中,也许会出现冲突(conflict). 在这种情况,Git会停止rebase并会让你去解决 冲突;在解决完冲突后,用"git-add"命令去更新这些内容的索引(index), 然后,你无需执行git-commit,只要执行:
$ git rebase --continue
这样git会继续应用(apply)余下的补丁。

在任何时候,你可以用–abort参数来终止rebase的行动,并且"mywork" 分支会回到rebase开始前的状态。
$ git rebase --abort

标签管理

发布一个版本时,我们通常先在版本库中打一个标签(tag),这样就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。
Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
Git有commit,为什么还要引入tag?
“请把上周一的那个版本打包发布,commit号是6a5819e…”
“一串乱七八糟的数字不好找!”
如果换一个办法:
“请把上周一的那个版本打包发布,版本号是v1.2”
“好的,按照tag v1.2查找commit就行!”
所以,tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。

创建标签

在Git中打标签非常简单,首先,切换到需要打标签的分支上:

1
2
3
4
5
$ git branch
* dev
  master
$ git checkout master
Switched to branch 'master'

然后,敲命令git tag <name>就可以打一个新标签:
$ git tag v1.0

可以用命令git tag查看所有标签:

1
2
$ git tag
v1.0

默认标签是打在最新提交的commit上的。有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?
方法是找到历史提交的commit id,然后打上就可以了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ git log --pretty=oneline --abbrev-commit
12a631b (HEAD -> master, tag: v1.0, origin/master) merged bug fix 101
4c805e2 fix bug 101
e1e9c68 merge with no-ff
f52c633 add merge
cf810e4 conflict fixed
5dc6824 & simple
14096d0 AND simple
b17d20e branch test
d46f35e remove test.txt
b84166e add test.txt
519219b git tracks changes
e43a48b understand how stage works
1094adb append GPL
e475afc add distributed
eaadf4e wrote a readme file

比方说要对add merge这次提交打标签,它对应的commit id是f52c633,敲入命令:
$ git tag v0.9 f52c633
再用命令git tag查看标签:

1
2
3
$ git tag
v0.9
v1.0

注意,标签不是按时间顺序列出,而是按字母排序的。可以用git show <tagname>查看标签信息:

1
2
3
4
5
6
7
8
9
$ git show v0.9
commit f52c63349bc3c1593499807e5c8e972b82c8f286 (tag: v0.9)
Author: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 21:56:54 2018 +0800

    add merge

diff --git a/readme.txt b/readme.txt
...

可以看到,v0.9确实打在add merge这次提交上。

还可以创建带有说明的标签,用-a指定标签名,-m指定说明文字:
$ git tag -a v0.1 -m "version 0.1 released" 1094adb

用命令git show <tagname>可以看到说明文字:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ git show v0.1
tag v0.1
Tagger: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 22:48:43 2018 +0800

version 0.1 released

commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (tag: v0.1)
Author: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 21:06:15 2018 +0800

    append GPL

diff --git a/readme.txt b/readme.txt
...

注意:标签总是和某个commit挂钩。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。

小结:

  • 命令git tag <tagname>用于新建一个标签,默认为HEAD,也可以指定一个commit id;
  • 命令git tag -a <tagname> -m "blablabla..."可以指定标签信息;
  • 命令git tag可以查看所有标签。

操作标签

如果标签打错了,也可以删除:

1
2
$ git tag -d v0.1
Deleted tag 'v0.1' (was f15b0dd)

因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
如果要推送某个标签到远程,使用命令git push origin <tagname>

1
2
3
4
$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
 * [new tag]         v1.0 -> v1.0

或者,一次性推送全部尚未推送到远程的本地标签:

1
2
3
4
$ git push origin --tags
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
 * [new tag]         v0.9 -> v0.9

如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:

1
2
$ git tag -d v0.9
Deleted tag 'v0.9' (was f52c633)

然后,从远程删除。删除命令也是push,但是格式如下:

1
2
3
$ git push origin :refs/tags/v0.9
To github.com:michaelliao/learngit.git
 - [deleted]         v0.9

要看看是否真的从远程库删除了标签,可以登陆GitHub查看。

小结:

  • 命令git push origin <tagname>可以推送一个本地标签;
  • 命令git push origin --tags可以推送全部未推送过的本地标签;
  • 命令git tag -d <tagname>可以删除一个本地标签;
  • 命令git push origin :refs/tags/<tagname>可以删除一个远程标签。

自定义git

在安装Git一节中,我们已经配置了user.name和user.email,实际上,Git还有很多可配置项。
比如,让Git显示颜色,会让命令输出看起来更醒目:
$ git config --global color.ui true

这样,Git会适当地显示不同的颜色,比如git status命令,文件名就会标上颜色。

忽略特殊文件

有些时候,你必须把某些文件放到Git工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次git status都会显示Untracked files …,有强迫症的童鞋心里肯定不爽。
好在Git考虑到了大家的感受,这个问题解决起来也很简单,在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。
不需要从头写.gitignore文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:
https://github.com/github/gitignore

忽略文件的原则是:

  • 忽略操作系统自动生成的文件,比如缩略图等;
  • 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
  • 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

举个例子:
假设你在Windows下进行Python开发,Windows会自动在有图片的目录下生成隐藏的缩略图文件,如果有自定义目录,目录下就会有Desktop.ini文件,因此你需要忽略Windows自动生成的垃圾文件:

1
2
3
4
# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini

然后,继续忽略Python编译产生的.pyc、.pyo、dist等文件或目录:

1
2
3
4
5
6
7
# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build

加上你自己定义的文件,最终得到一个完整的.gitignore文件,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini

# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build

# My configurations:
db.ini
deploy_key_rsa

最后一步就是把.gitignore也提交到Git,就完成了!当然检验.gitignore的标准是git status命令是不是说working directory clean。

使用Windows的童鞋注意了,如果你在资源管理器里新建一个.gitignore文件,它会非常弱智地提示你必须输入文件名,但是在文本编辑器里“保存”或者“另存为”就可以把文件保存为.gitignore了。

有些时候,你想添加一个文件到Git,但发现添加不了,原因是这个文件被.gitignore忽略了:

1
2
3
4
$ git add App.class
The following paths are ignored by one of your .gitignore files:
App.class
Use -f if you really want to add them.

如果你确实想添加该文件,可以用-f强制添加到Git:
$ git add -f App.class

或者你发现,可能是.gitignore写得有问题,需要找出来到底哪个规则写错了,可以用git check-ignore命令检查:

1
2
$ git check-ignore -v App.class
.gitignore:3:*.class    App.class

Git会告诉我们,.gitignore的第3行规则忽略了该文件,于是我们就可以知道应该修订哪个规则。

因为.gitignore只能忽略那些没有被管理的文件。一旦文件被提交过,或者.gitignore文件是后来创建的,.gitignore是无法忽略这些文件的。这个时候需要执行:
git rm -r --cached 文件或文件夹名字
注:一定要加文件名或文件夹名。
然后正常commit、push即可。

小结:

  • 忽略某些文件时,需要编写.gitignore;
  • .gitignore文件本身要放到版本库里,并且可以对.gitignore做版本管理!

配置别名

有没有经常敲错命令?比如git status?status这个单词真心不好记。
如果敲git st就表示git status那就简单多了,当然这种偷懒的办法我们是极力赞成的。
我们只需要敲一行命令,告诉Git,以后st就表示status:
$ git config --global alias.st status
好了,现在敲git st看看效果。

当然还有别的命令可以简写,很多人都用co表示checkout,ci表示commit,br表示branch:

1
2
3
$ git config --global alias.co checkout
$ git config --global alias.ci commit
$ git config --global alias.br branch

以后提交就可以简写成:
$ git ci -m "bala bala bala..."

--global参数是全局参数,也就是这些命令在这台电脑的所有Git仓库下都有用。

在撤销修改一节中,我们知道,命令git reset HEAD file可以把暂存区的修改撤销掉(unstage),重新放回工作区。既然是一个unstage操作,就可以配置一个unstage别名:
$ git config --global alias.unstage 'reset HEAD'
当你敲入命令:
$ git unstage test.py

实际上Git执行的是:
$ git reset HEAD test.py

配置一个git last,让其显示最后一次提交信息:
$ git config --global alias.last 'log -1'

这样,用git last就能显示最近一次的提交:

1
2
3
4
5
6
7
$ git last
commit adca45d317e6d8a4b23f9811c3d7b7f0f180bfe2
Merge: bd6ae48 291bea8
Author: Michael Liao <askxuefeng@gmail.com>
Date:   Thu Aug 22 22:49:22 2013 +0800

    merge & fix hello.py

甚至还有人丧心病狂地把lg配置成了:

1
git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

自行体会git lg的效果。

配置文件

配置Git的时候,加上–global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用。
配置文件放哪了?每个仓库的Git配置文件都放在.git/config文件中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ cat .git/config 
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
[remote "origin"]
    url = git@github.com:michaelliao/learngit.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
    remote = origin
    merge = refs/heads/master
[alias]
    last = log -1

别名就在[alias]后面,要删除别名,直接把对应的行删掉即可。
而当前用户的Git配置文件放在用户主目录下的一个隐藏文件.gitconfig中:

1
2
3
4
5
6
7
8
9
$ cat .gitconfig
[alias]
    co = checkout
    ci = commit
    br = branch
    st = status
[user]
    name = Your Name
    email = your@email.com

配置别名也可以直接修改这个文件,如果改错了,可以删掉文件重新通过命令配置。

搭建Git服务器

在远程仓库一节中,我们讲了远程仓库实际上和本地仓库没啥不同,纯粹为了7x24小时开机并交换大家的修改。GitHub就是一个免费托管开源代码的远程仓库。但是对于某些视源代码如生命的商业公司来说,既不想公开源代码,又舍不得给GitHub交保护费,那就只能自己搭建一台Git服务器作为私有仓库使用。
搭建Git服务器需要准备一台运行Linux的机器,强烈推荐用Ubuntu或Debian,这样,通过几条简单的apt命令就可以完成安装。
假设你已经有sudo权限的用户账号,下面,正式开始安装。
第一步,安装git:
$ sudo apt-get install git

第二步,创建一个git用户,用来运行git服务:
$ sudo adduser git

第三步,创建证书登录:
收集所有需要登录的用户的公钥,就是他们自己的id_rsa.pub文件,把所有公钥导入到/home/git/.ssh/authorized_keys文件里,一行一个。
第四步,初始化Git仓库:
先选定一个目录作为Git仓库,假定是/srv/sample.git,在/srv目录下输入命令:
$ sudo git init --bare sample.git

Git就会创建一个裸仓库,裸仓库没有工作区,因为服务器上的Git仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常都以.git结尾。然后,把owner改为git:
$ sudo chown -R git:git sample.git

第五步,禁用shell登录:

出于安全考虑,第二步创建的git用户不允许登录shell,这可以通过编辑/etc/passwd文件完成。找到类似下面的一行:
git:x:1001:1001:,,,:/home/git:/bin/bash

改为:
git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell
这样,git用户可以正常通过ssh使用git,但无法登录shell,因为我们为git用户指定的git-shell每次一登录就自动退出。

第六步,克隆远程仓库:
现在,可以通过git clone命令克隆远程仓库了,在各自的电脑上运行:

1
2
3
$ git clone git@server:/srv/sample.git
Cloning into 'sample'...
warning: You appear to have cloned an empty repository.

管理公钥
如果团队很小,把每个人的公钥收集起来放到服务器的/home/git/.ssh/authorized_keys文件里就是可行的。如果团队有几百号人,就没法这么玩了,这时,可以用Gitosis来管理公钥。

这里我们不介绍怎么玩Gitosis了,几百号人的团队基本都在500强了,相信找个高水平的Linux管理员问题不大。

管理权限
有很多不但视源代码如生命,而且视员工为窃贼的公司,会在版本控制系统里设置一套完善的权限控制,每个人是否有读写权限会精确到每个分支甚至每个目录下。因为Git是为Linux源代码托管而开发的,所以Git也继承了开源社区的精神,不支持权限控制。不过,因为Git支持钩子(hook),所以,可以在服务器端编写一系列脚本来控制提交等操作,达到权限控制的目的。Gitolite就是这个工具。这里我们也不介绍Gitolite了,不要把有限的生命浪费到权限斗争中。

小结:

  • 搭建Git服务器非常简单,通常10分钟即可完成;
  • 要方便管理公钥,用Gitosis;
  • 要像SVN那样变态地控制权限,用Gitolite。

git异常

Failed to connect to github.com port 443 after 21087 ms: Timed out

1
2
fatal: unable to access 'https://github.com/adityatelange/hugo-PaperMod.git/': Failed to connect to github.com port 443 after 21087 ms: Timed out
fatal: clone of 'https://github.com/adityatelange/hugo-PaperMod.git' into submodule path 'D:/hugo_site/quickstart/themes/PaperMod' failed

情况:不挂梯子上不去github,挂了梯子github能上,但是拉代码不行。

原因:说明在在拉取或推送代码时并没有使用梯子进行代理。

首先你得知道你的梯子代理的端口号是多少。

windows:win+s搜索proxy,梯子开启后可以即可看到代理的端口号。

解决办法:配置代理

1
git config --global http.proxy socks5 127.0.0.1:10809
1
git config --global https.proxy socks5 127.0.0.1:10809
1
git config --global http.proxy 127.0.0.1:10809
1
git config --global https.proxy 127.0.0.1:10809

Failed to connect to 127.0.0.1 port 1181: Connection refused

可能使用了代理,逐一检查:

1
git config --global http.proxy

果然出现
http://127.0.0.1:1181

解决方法:取消代理

1
git config --global --unset http.proxy
1
git config --global --unset https.proxy

Please make sure you have the correct access rights and the repository exists

1.重新设置git的名字和邮箱

1
2
git config --global user.name "yourname"
git config --global user.email“your@email.com"

注:yourname是你要设置的名字,your@email是你要设置的邮箱,建议使用github的用户名和邮箱。

2.删除.ssh文件夹
默认的路径是:
C:\Users\wjy\.ssh
直接删除该文件夹。因为远程ssh文件夹下包含了邮箱信息等。

3.git执行命令
ssh-keygen -t rsa -C "your@email.com"
填写刚才设置的邮箱

4.接下来请一路回车
然后系统会自动在.ssh文件夹下生成两个文件,id_rsa和id_rsa.pub,用记事本打开id_rsa.pub,将内容全部复制。

5.打开github-settings
单机右上角头像–settings,选择SSH-GPG keys,选择New SSH Key,title随便起,Key填写刚才复制id_rsa.pub中的内容,然后点击add ssh key。

6.git执行命令
ssh -T git@github.com
然后跳出一堆话,输入 yes

7.重启git bush,就可以提交了。

fatal: refusing to merge unrelated histories

起因是我修改了git之前错误的提交人邮箱。
然后再次拉代码时提示:
fatal: refusing to merge unrelated histories

解决:
切换想要提交的分支
git checkout hexoBranch

然后执行
git pull origin hexoBranch --allow-unrelated-histories

然后执行:
git push origin hexoBranch

报错:
Updates were rejected because the tip of your current branch is behind
意思就是本地分支的版本落后了。

此时可以使用强制push的方法:
git push -u origin master -f

这样会使远程修改丢失,一般是不可取的,尤其是多人协作开发的时候。

push前先将远程repository修改pull下来
git pull origin master
git push -u origin master

若不想merge远程和本地修改,可以先创建新的分支:
git branch [name]
然后push
git push -u origin [name]

push之后发现还是无法pull,于是再次依次执行
git add .
git commit -m 'fix branch'
git push

然后再次pull,竟然成功了。一头雾水

git fetch & pull详解


简单概括:
git fetch是将远程主机的最近内容拉到本地,用户检查后决定是否合并到本机分支中。
而git pull则是将远程主机的最新内容拉下来后立即合并,即:git pull = git fetch + git merge,但是这样会产生冲突,需要手动解决。

分支

在git中,分支用来标记特定代码的提交,每一个分支通过SHA1sum值来标识,所以对分支的操作时轻量的,你改变的仅仅是SHA1sum值。
如下图所示,当前有2个分支,A,C,E属于master分支,而A,B,D,F属于dev分支。

1
2
3
A----C----E(master)
 \
  B---D---F(dev)

两个分支的指针分别指向E和F,对上述做如下操作:

1
2
git checkout master  //选择or切换到master分支
git merge dev        //将dev分支合并到当前分支(master)中

合并完成后:

1
2
3
A---C---E---G(master)
 \         /
  B---D---F(dev)

现在ABCDEFG属于master,G是一次合并后的结果,是将E和F的代码合并后的结果,可能会出现冲突。而ABDF依然属于dev分支。可以继续在dev的分支上进行开发:

1
2
3
A---C---E---G---H(master)
 \         /
  B---D---F---I(dev)

分支的基本操作:

 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
git branch //查看本地所有分支 

git branch -r //查看远程所有分支

git branch -a //查看本地和远程的所有分支

git branch <branchname> //新建分支

git branch -d <branchname> //删除本地分支

git branch -d -r <branchname> //删除远程分支,删除后还需推送到服务器
git push origin:<branchname>  //删除后推送至服务器

git push origin --delete [branch_name] 一次操作删除远程分支

git branch -m <oldbranch> <newbranch> //重命名本地分支
/**
*重命名远程分支:
*1、删除远程待修改分支
*2、push本地新分支到远程服务器
*/

//git中一些选项解释:

-d
--delete:删除

-D
--delete --force的快捷键

-f
--force:强制

-m
--move:移动或重命名

-M
--move --force的快捷键

-r
--remote:远程

-a
--all:所有

git fetch的用法

git fetch 命令:
git fetch <远程主机名> //这个命令将某个远程主机的更新全部取回本地
如果只想取回特定分支的更新,可以指定分支名。
git fetch <远程主机名> <分支名> //注意之间有空格
如取回origin 主机的master 分支:
git fetch origin master
取回更新后,会返回一个fetch_head,意思是某个branch在服务器上的最新状态,我们可以通过这个fetch_head查看刚取回的更新信息:
git log -p fetch_head
可以看到返回的信息包括更新的文件名,更新的作者和时间,以及更新的代码(19行红色[删除]和绿色[新增]部分)。
我们可以通过这些信息来判断是否产生冲突,以确定是否将更新merge到当前分支。

git pull

git pull 的过程可以理解为:

1
2
git fetch origin master //从远程主机的master分支拉取最新内容 
git merge FETCH_HEAD    //将拉取下来的最新内容合并到当前所在的分支中

即将远程主机的某个分支的更新取回,并与本地指定的分支合并,完整格式可表示为:
git pull <远程主机名> <远程分支名>:<本地分支名>
如果远程分支是与当前分支合并,则冒号后面的部分可以省略:
git pull origin next

git init 和 git init –bare(裸库)

git init
初始化一个仓库,刚创建时,里面只包含一个.git文件夹,用于控制当前仓库。
适用于本地仓库初始化,有完整的Git命令集,可以提交工作空间的代码和文件。
当你创建一个普通库时,在工作目录下,除了.git目录之外,你还可以看到库中所包含的所有源文件。你拥有了一个可以进行浏览和修改(add, commit, delete等)的本地库。

git init –bare(裸露的)
创建一个裸库,刚创建时,里面包含.git文件夹里的内容,而不是.git文件夹。这个仓库只保存git历史提交的版本信息,而不允许用户在上面进行各种git操作(如:add、commit、push等操作),可以使用git show命令查看提交内容。
适用于远程仓库初始化,默认没有工作空间。
当你创建一个裸库时,在工作目录下,只有一个.git目录,而没有类似于本地库那样的文件结构可供你直接进行浏览和修改。但是你仍旧可以用git show命令来进行浏览。

一般来说,一个裸库往往被创建用于作为大家一起工作的共享库,每一个人都可以往里面push自己的本地修改。一个惯用的命名方式是在库名后加上.git,举个例子:

1
2
3
# mkdir example.git
# cd example.git
# git init --bare

这样你便拥有了一个叫做example的共享库。在你自己的本地机器上,你可以用git remote add命令做初始化check-in:

1
2
3
4
5
6
7
8
// assume there're some initial files you want to push to the bare repo you just created,
// which are placed under example directory
# cd example
# git init
# git add *
# git commit -m "My initial commit message"
# git remote add origin git@example.com:example.git
# git push -u origin master

项目团队里面的每个人都可以clone这个库,然后完成本地修改之后,往这个库中push自己的代码。

1
2
# git clone git@example.com:example.git
# cd example

简单来说:

  1. git init 创建本地仓库(在工程目录下创建)
  2. git init –bare 创建远端仓库(在服务器或者工程目录以外路径都可以创建的备份仓库)
  3. 工程commit到1中,push到2中;

使用裸库,理想状态是,工作空间和裸库分离。那么如何将工作空间和裸库关联呢?通过hook。
hook(钩子),就是可执行的脚本,可以用任何语言编写,一般来说用 shell 编写。
官方文档介绍

我们在裸库中找到hooks文件夹
cd hooks

创建并编辑post-receive
vim post-receive
输入内容:

1
2
#!/bin/sh
git --work-tree=#工作空间# --git-dir=#Git仓库地址# checkout -f

其他

Linux 服务器上记住GIT用户名密码

git config --global credential.helper store
执行下面命令,检查是否生效
cat ~/.gitconfig
得到结果

1
2
[credential]
    helper = store

cherry-pick

合并代码时,如果使用git merge dev,这样会将dev分支的所有提交都合并到master分支。
如果我想只合并某一次提交,可以使用git cherry-pick

先使用git log查看提交历史,找到想合并提交的id,复制下来。只复制前边一部分即可。q可以退出git log
检出到想要合并代码的分支:
git checkout master

合并某次提交:
git cherry-pick 你复制的id
如果没有出现冲突,那么就是完成了合并。
如果出现冲突:

  • 先解决冲突
  • git add 将解决了冲突的文件添加到暂存区
  • git cherry-pick –continue就行

撤销merge

现有分支A和B,不小心将B分支的代码全部合并到A上了,如何撤销。
我们停留在A分支上,执行git reflog,可以看到git执行的全部操作,最上边是最后一次的执行。找到我们从B分支切换到A分支的操作,前边可以看到版本号。

执行
git reset --hard (merge前的版本号)

撤销commit

参考

git reset --soft HEAD^
HEAD^的意思是上一个版本,也可以写成HEAD~1
如果你进行了2次commit,想都撤回,可以使用HEAD~2

可选参数:
--mixed
意思是:不删除工作空间改动代码,撤销commit,并且撤销git add . 操作
这个为默认参数,git reset –mixed HEAD^ 和 git reset HEAD^ 效果是一样的。

--soft
不删除工作空间改动代码,撤销commit,不撤销git add .

--hard
删除工作空间改动代码,撤销commit,撤销git add .
注意完成这个操作后,就恢复到了上一次的commit状态,会删除你写的代码,慎用。

撤销已经push到远端的commit

在使用git时,push到远端后发现commit了多余的文件,或者希望能够回退到以前的版本。
先在本地回退到相应的版本:

1
2
3
git reset --hard <版本号>
// 注意使用 --hard 参数会抛弃当前工作区的修改
// 使用 --soft 参数的话会回退到之前的版本,但是保留当前工作区的修改,可以重新提交

如果此时使用命令:
git push origin <分支名>

会提示本地的版本落后于远端的版本:

1
2
failed to push some refs to '仓库地址'
update were rejected because the tip of your current branch is behind...

为了覆盖掉远端的版本信息,使远端的仓库也回退到相应的版本,需要加上参数–force
git push origin <分支名> --force

忽略仓库中的某个文件

git update-index --assume-unchanged FILE 在FILE处输入要忽略的文件。

还原:
git update-index --no-assume-unchanged FILE

git推送本地分支到远程分支

场景
有时候我们开发需要开一个分支,这样可以有效的并行开发。
开分支有两种方式:

  • 一种是在远程开好分支,本地直接拉下来
  • 一种是本地开好分支,推送到远程

远程先开好分支然后拉到本地

检出远程的feature-branch分支到本地
git checkout -b feature-branch origin/feature-branch

本地先开好分支然后推送到远程

创建并切换到分支feature-branch
git checkout -b feature-branch

推送本地的feature-branch(冒号前面的)分支到远程origin的feature-branch(冒号后面的)分支(没有会自动创建)
git push origin feature-branch:feature-branch

切换到新仓库

找到仓库根目录的.git文件夹。

找到config文件,编辑url

代码提交规范

Git Commit 规范的几个好处:

  1. 提高代码可读性:规范的 git commit 提交信息可以让其他开发人员更快地了解代码变更的内容,从而提高代码的可读性和可维护性。
  2. 方便代码回溯:如果出现了问题,可以通过 git log 命令查看提交历史,快速定位问题所在,方便代码回溯。
  3. 促进团队协作:规范的 git commit 提交信息可以让团队成员更好地理解彼此的工作,从而促进团队协作。
  4. 方便生成 Change log:规范的 git commit 提交信息可以方便地生成 Change log,记录每个版本的变更内容,方便用户了解软件产品的更新情况。
  5. 总之,遵循 git commit 规范可以提高代码质量、促进团队协作、方便代码回溯和生成 Change log 等。

日志基本格式

1
2
3
<type>(<scope>):<@taskid> <subject>
<BLANK LINE>
<body>

type:代表某次提交的类型,比如是修复一个bug或增加一个新的feature。type类型如下:

feat:新增feature
fix:修复bug
docs:仅仅修改了文档,比如README, CHANGELOG, CONTRIBUTE等等
style:仅仅修改了空格、格式缩进、都好等等,不改变代码逻辑
refactor:代码重构,没有加新功能或者修复bug
perf:优化相关,比如提升性能、体验
test:测试用例,包括单元测试、集成测试等
chore:改变构建流程、或者增加依赖库、工具等
revert:回滚到上一个版本

注意:避免的几个常见的commit

  • 攒一堆代码再commit。请尽可能完成一个功能(独立)立马commit一次。
  • 多个功能拆分commit。
  • 无意义的commit。例如:“修改了几个bug”,“commit”,“优化了代码”
  • 非type动词开头。

scope

commit 影响的范围,比如数据层、控制层、视图层、后端、前台等

分支命名

feature_系统名(英文|中文),尽可能的好理解,见名知义

  • master:线上主分支,上线后记得打tag版本,长期存在,不可删除
  • hotfix:紧急修复分支,短期分支
  • release:准生产的版本,根据当前团队和环境决定或调整,短期
  • dev:开发中的版本,最新代码分支,长期存在,不可删除
  • feature:新版权分支,基于此分支开发代码、提交代码

短期分支类型一旦完成开发,它们就会被合并进dev或master,后续可被删除

Git Commit Template

IDEA 可以安装插件 Git Commit Template

git 分支使用心得

一个环境对应一个分支。

修复bug,一定要从master拉一个新的分支,修复后,将功能合并到master。然后可以删除bug分支。

需要开发新功能时,从master拉出新分支,开发人员再从新分支拉出自己的开发分支。

为什么需要再拉自己的开发分支?便于合并自己开发的功能到其他分支。

git 最佳实践

原文:https://mp.weixin.qq.com/s/3P9lkbwJwORW42dZxuPAfgc

什么是Git

不开玩笑。最常见的Git错误使用,正是来自于没意识到git是什么。大部分git的属性,可以从定义用逻辑推导出来。逻辑是最重要的,只要逻辑错了,就一定是错了。哪怕所有人都这么做,也是错的。

Git是一个分布式版本控制系统,跟踪目录里的修改。它的工作流是非线性的(不同电脑上的平行分支形成了一个graph)。和主从式的系统不一样的是,每台电脑上的每个git目录都是一个完整的repo,包含全部历史完整的版本跟踪能力。(LFS是个例外,后面会提到。)

因为git的本质是一个基于目录的分布式VCS,这里面并没有中心服务器的角色。去中心化是未来。同个项目的所有repo都是平等的端点。一个repo可以在服务器、本地目录、其他人的电脑上。只是为了团队协作的目的,会认为指定一个或多个端点作为”服务器“。是的,可以同时有多个上游服务器。很多时候这么做很有必要。比如对内开发的repo和对外开源的repo,就是两个不同的端点。可以有不同的分支和推送频率。本地只要一个repo就都管理了。

非线性的工作流表示提交和分支操控是一个常规的操作。建立分支、rebase、修订commit、强制推送、cherry-pick、分支复位,在git都是很正常的使用方式。

什么不是Git

很多东西经常和git一起出现,但是并不是git的一部分。

Github/Gitlab

这些都不是git,而是提供git服务和社区的网站。Git是个基于目录的VCS,并不需要网站服务或者网络访问才能工作。早期经常有人没法区分github和git。当要说git的时候,会说github,制造的混乱不是一星半点。

Fork

Fork仍然也是git服务网站的功能,用来简化协作流程。在没有fork的时候,如果你想往开源项目里修bug或者加feature,会需要这样的流程:

  1. 克隆repo
  2. 修改代码
  3. 生成补丁
  4. 发到论坛或者支持的邮件列表
  5. 找作者来review,合并补丁

很多项目到现在还是这么做的。如果有了fork,可以简化成:

  1. Fork并克隆repo
  2. 修改代码
  3. 发出merge request或者pull request

虽然fork很有用,但这仍然不是git的一部分。它用到的是git的分布式能力。本质上,在fork的时候,它会克隆一份repo,把原来的repo设置成上游。所以其实如果你的目标不是为了继续把repo放在网络服务上,那就克隆到本地就是了。太多的人把fork当作like来用,根本就是错的。如果没打算改代码,fork是没意义的。机器学习界这个问题尤其严重。经常放一个README就假开源了,还有几百个fork,都不知道能fork到什么。

Merge request/Pull request

Github上叫Pull Request,gitlab上叫merge request,其实是一个东西的不同视角。这些都是code review和合并的流程,不是git的一部分。

需要注意的是,它们的重点在“request”,而不是merge或者pull。如果你要把一个分支merge到你自己的,没必要开一个MR然后自己给自己通过。在本地merge就是了,更简单更快。

Import

很多git服务支持“Import”,用来从别的git、svn、cvs、p4等VCS导入一个库。如果原本的repo已经是git,那直接push到新的地方就是了,比import更简单。而且这样绝对不会丢失历史记录或者搞错文件。如果是其他VCS的repo,那也可以用插件或脚本来先转成一个本地的git repo,然后再push到新的地方。

选对工具

Git本身是个命令行工具。但是,非线性工作流的本质就让它没可能在字符界面显示出分支的graph。选个好的GUI非常关键。不但可以大幅度增加工作效率,更重要的是,减少出错的机会。第二个常见的git使用错误来源,正是因为用错了工具造成了。

Windows上最好的git GUI是TortoiseGit,没有之一。它只是个GUI,git命令行需要事先安装。和其他Tortoise打头的工具(TortoiseCVS、TortoiseSVN)一样,它的风格是没有主UI,而集成到Windows的文件管理器里面。Repo里的文件(也就是目录里的)图标上会覆盖上状态。右键点击这个目录,菜单里可以看到TortoiseGit的子菜单,包含git的一些操作。大部分VCS的GUI工具,比如P4V、SourceTree,UGit,都有个主UI显示映射了的工作空间,而不是目录本身。对于git来说,这其实是个错误,因为git是基于目录的,不存在工作空间这个概念。而且,这种情况下非常常见的错误就是忘记提交新增的文件。在TortoiseGit里,除了盖在图标上的状态之外,提交窗口也可以显示出哪些文件还没添加,不会出现遗漏的情况。

另外,TortoiseGit有一个独特的版本graph查看器,里面可以显示出repo的整个分支结构。通过这个查看器,可以很方便地看出来repo是怎么成长的,有那些不必要的分支,如何从一个分支跳到另一个,等等。这是TortoiseGit比其他git UI好的一个重要原因。不管是Visual Studio里的、SourceTree、还是UGit,在UI设计上都像用传统的VCS思路来套用到git上,而不是git的思路。主它们的共同问题就是,基本只关注于当前分支。而有能力同时看所有分支,对git来说非常重要,因为git的工作流是非线性的。

其他高级功能,比如打补丁、处理submodule(非常重要),都可以在TortoiseGit的GUI里完成。但它没法覆盖所有的功能。有些很少用的,还是得通过命令行。

尽量在本地

所有的git操作都可以在本地repo上完成,因为服务端的并没有更高优先级。虽然大部分提供git服务的网站都在网页界面里有cherry-pick、新建分支、合并这些操作,但是在本地执行更容易,而且比在服务端执行了再拉下来要更快。

分支策略

Git的工作流是基于分支的。不但每个repo是平等的,每个分支也是。Master/main、develop这些只是为了简化管理而人工指定的有特殊含义的分支。这里的分支策略是为了更好地协作而产生的习惯规范,不是git的工作流本身必须定义的。分支可以分为几个层次。

Main分支

这是整个项目的稳定分支,里面的内容可能相对较老,但是这个分支里的内容都是经过测试和验证的。原先都叫master,因为政治正确的要求,最近越来越多新项目开始用main。有些快速开发的项目甚至不采用main分支。

Develop分支

开发主要发生在develop分支。新特性先放到这个分支,再去优化和增强稳定性。

大项目可选的团队develop分支

对于跨团队的大项目,每个团队都有自己的兴趣点和发布周期。很常见的做法是,每个团队有自己的develop分支。每过一段时间合并到总的develop分支。一般来说,中等大小的团队,专注于repo的某一部分,可以采取这样的分支形式。小团队或者个人没有必要有自己的develop分支。那样反而会浪费时间和增加合并过程中的风险。

Feature分支

Feature分支是生命周期很短的分支,专注于单个功能的开发。在git中新建分支的开销很低,所以可以高频的新建分支、合并分支。现在有一个新功能要做,常规的流程如下:

  1. 从develop分支上新建一个feature分支feature_a
  2. 在feature_a上编写代码,并提交(如果是自己开发,可以不用推送到远程)
  3. 将feature_a合并到develop分支
  4. 验证功能没有问题后,可以删除feature_a分支

对于本地repo里的feature分支,你可以做任何事。常见的用法是在开发过程中非常频繁地提交,走一小步就提交一次。在发出MR之前,先合并成一个commit,把这个分支变整洁,方便后续操作。

Merge还是rebase

merge会把一个分支合并到目标分支,在顶上建立一个commit用来合并,两个分支里已有的commit不会有变化。

rebase会从分支分出来的地方切开,“嫁接” 到目标分支的顶端上。

处理冲突

当出现合并冲突的时候,最好的方式是先把你的feature分支rebase到目标分支的顶端,这时候解决冲突,然后force push。如果用WOA的冲突解决(可能有些别的基于web的git服务也有),它会每次都做merge。结果经常把简单的单个commit rebase,变成了复杂的三分支合并。

常见错误:解决合并冲突后建了个新的MR

因为冲突解决的错误行为,有可能在解决之后,修改被提交到了一个新的分支。这时候应该把你的分支reset到新的去,force push,再删掉新的;而不是关掉原先的MR,在新分支上开个新MR。

常见错误:把分支搞乱

如果真的遇到了多分支复杂交错的情况,有两个方法可以尝试清理出来。

  1. 强制rebase。Fetch一下整个repo;把你的分支rebase到目标分支上的时候勾选force;这时候在列表里选要拿去rebase的commit。大部分时候这都能行。但有时候git因为分支太错综复杂而搞不清楚commit,在列表里会有遗漏。
  2. Cherry-pick。在目标分支上新建一个临时分支;把有用的commit都cherry-pick过去;把你的分支reset到那个临时分支上;最后删掉那个临时分支。

两个方法最后都需要force push。

小而完整的commit

每个commit都该小而完整,有些人把这个叫做”原子性“。不要把多个特性压到一个commit里,同时不要有一堆必须合起来才能用的commit。

常见错误:一个commit里做多件事情

这是一个非常常见的错误。一个大的commit包含多个任务的代码。这样的commit必须要拆成多个才行。在git里,这样的拆分比较容易。如果一个分支“Feature”包含了特性A和特性B的代码,那么,

  1. 在“Feature”的顶端建立“Feature A”和“Feature B”两个分支
  2. 切换到“Feature A”分支,删掉其中特性B的代码,开amend提交
  3. 把“Feature B”分支rebase到新的“Feature A”分支

这就行了。现在两个分支都分别只包含一个特性。如果特性B不依赖于特性A,它还可以继续rebase到develop分支去。

常见错误:多个不完整的commit

另一个非常常见的错误是不完整的commit,比如不能编译、不能运行、只包含琐碎的修改、或者仅仅为了未来的使用而做的修改。这样的commit只是中间结果,没法单独存在,需要和其他commit合起来才变成一个完整的commit。那它们就需要合并之后才发MR。

拆分大的commit

是的,有时候是需要把一个大的commit拆分成多个,让MR更容易看。但是这里的拆分并不能让commit变得不完整。如果一个大commit中的一部分,本身就能对现在的代码库有帮助,拿着就能提出来变成一个独立的commit。常见的是独立的bug修复、代码整理、或者重构。

给git配置代理

设置http代理

1
git config --global http.proxy 127.0.0.1:7890
1
git config --global https.proxy 127.0.0.1:7890

设置socks5代理

1
git config --global http.proxy socks5 127.0.0.1:7890
1
git config --global https.proxy socks5 127.0.0.1:7890

取消代理

1
git config --global --unset-all http.proxy
1
git config --global --unset-all https.proxy

查看代理

1
git config --global --get http.proxy
1
git config --global --get https.proxy

git 别名最佳实践

查看已定义的别名

1
git config --get-regexp alias

删除别名

1
git config --global --unset alias.acp

整合git addgit commitgit push

  1. 创建别名:
1
git config --global alias.acp '!git add . && git commit -m "commit message" && git push'

注意:在windows的终端下执行这个指令会有问题,因为是引号嵌套,实际添加的别名是:

1
git add . && git commit -m commit

这显然是不对的,需要对双引号部分进行转义:

1
git config --global alias.acp '!git add . && git commit -m \"commit message\" && git push'

解释:

  • --global:表示这个别名是全局的,对所有仓库生效。如果只想对当前仓库生效,可以去掉 --global
  • acp:这是你定义的别名名称,你可以根据需要改成其他名字(如 quickpush)。
  • !:表示后面的内容是 shell 命令,而不是 Git 子命令。
  • git add .:添加所有更改到暂存区。
  • git commit -m "commit message":提交更改,提交信息为 "commit message"
  • git push:推送更改到远程仓库。
  1. 使用别名
    定义别名后,你可以通过以下命令来使用它:
1
git acp
  1. 动态提交信息
    如果你希望每次运行时动态输入提交信息,可以将别名改为接受参数的形式:
1
git config --global alias.acp '!f() { git add . && git commit -m "$1" && git push; }; f'

使用时,你需要传递提交信息作为参数:

1
git acp "你的提交信息"

# 大厂都在用的Git代码管理规范

分支命名

master 分支

master 为主分支,也是用于部署生产环境的分支,需要确保master分支稳定性。master 分支一般由 release 以及 hotfix 分支合并,任何时间都不能直接修改代码。

develop 分支

develop 为开发环境分支,始终保持最新完成以及bug修复后的代码,用于前后端联调。一般开发的新功能时,feature分支都是基于develop分支创建的。

feature 分支

开发新功能时,以develop为基础创建feature分支。

分支命名时以 feature/ 开头,后面可以加上开发的功能模块, 命名示例:feature/user_modulefeature/cart_module

test分支

test为测试环境分支,外部用户无法访问,专门给测试人员使用,版本相对稳定。

release分支

release 为预上线分支(预发布分支),UAT测试阶段使用。一般由 test 或 hotfix 分支合并,不建议直接在 release 分支上直接修改代码。

hotfix 分支

线上出现紧急问题时,需要及时修复,以master分支为基线,创建hotfix分支。修复完成后,需要合并到 master 分支和 develop 分支。

分支命名以hotfix/ 开头的为修复分支,它的命名规则与 feature 分支类似。

分支与环境对应关系

在系统开发过程中常用的环境:

  • DEV 环境(Development environment):用于开发者调试使用

  • FAT环境(Feature Acceptance Test environment):功能验收测试环境,用于测试环境下的软件测试者测试使用

  • UAT环境 (User Acceptance Test environment):用户验收测试环境,用于生产环境下的软件测试者测试使用

  • PRO 环境(Production environment):生产环境

对应关系:

分支 功能 环境 可访问
master 主分支,稳定版本 PRO
develop 开发分支,最新版本 DEV
feature 开发分支,实现新特性
test 测试分支,功能测试 FAT
release 预上线分支,发布新版本 UAT
hotfix 紧急修复分支,修复线上bug

分支合并流程规范

业界常见的两大主分支(master、develop)、三个辅助分支(feature、release、hotfix)的生命周期:

图片

以上生命周期仅作参考,不同开发团队可能有不同的规范,可自行灵活定义。

例如我们团队在开发时,至少需要保证以下流程:

  • develop 分支和 hotfix 分支,必须从 master 分支检出

  • 由 develop 分支合并到 test 分支

  • 功能测试无误后,由 test 分支合并到 release 分支

  • UAT测试通过后,由 release 分支合并到 master分支

  • 对于工作量小的功能开发(工时小于1天),可以直接在devolop 分支进行开发,否则由 develop 分支检出 feature 分支进行开发,开发完后合并到develop 分支

Git Commit Message规范

Git commit message规范指提交代码时编写的规范注释,编写良好的Commit messages可以达到3个重要的目的:

  • 加快代码review的流程

  • 帮助我们编写良好的版本发布日志

  • 让之后的维护者了解代码里出现特定变化和feature被添加的原因

Angular Git Commit Guidelines

业界应用的比较广泛的是Angular Git Commit Guidelines:

<type>(<scope>): <subject> <BLANK LINE> <body> <BLANK LINE> <footer>

  • type:提交类型

  • scope:可选项,本次 commit 波及的范围

  • subject:简明扼要的阐述下本次 commit 的主旨,在Angular Git Commit Guidelines中强调了三点。使用祈使句,首字母不要大写,结尾无需添加标点

  • body: 同样使用祈使句,在主体内容中我们需要把本次 commit 详细的描述一下,比如此次变更的动机

  • footer: 描述下与之关联的 issue 或 break change

简易版

项目中实际可以采用简易版规范:

<type>(<scope>):<subject>

type规范

Angular Git Commit Guidelines中推荐的type类型如下:

  • feat: 新增功能

  • fix: 修复bug

  • docs: 仅文档更改

  • style: 不影响代码含义的更改(空白、格式设置、缺失 分号等)

  • refactor: 既不修复bug也不添加特性的代码更改

  • perf: 改进性能的代码更改

  • test: 添加缺少的测试或更正现有测试

  • chore: 对构建过程或辅助工具和库(如文档)的更改

除此之外,还有一些常用的类型:

  • delete:删除功能或文件

  • modify:修改功能

  • build:改变构建流程,新增依赖库、工具等(例如webpack、gulp、npm修改)

  • test:测试用例的新增、修改

  • ci:自动化流程配置修改

  • revert:回滚到上一个版本

单次提交注意事项

  • 提交问题必须为同一类别

  • 提交问题不要超过3个

  • 提交的commit发现不符合规范,git commit --amend -m "新的提交信息"或 git reset --hard HEAD 重新提交一次

配置.gitignore文件

.gitignore是一份用于忽略不必提交的文件的列表,项目中可以根据实际需求统一.gitignore文件,减少不必要的文件提交和冲突,净化代码库环境。

通用文件示例:

 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
51
52
HELP.md  
target/  
!.mvn/wrapper/maven-wrapper.jar  
!**/src/main/**/target/  
!**/src/test/**/target/  
  
### STS ###  
.apt_generated  
.classpath  
.factorypath  
.project  
.settings  
.springBeans  
.sts4-cache  
  
### IntelliJ IDEA ###  
.idea  
*.iws  
*.iml  
*.ipr  
  
### NetBeans ###  
/nbproject/private/  
/nbbuild/  
/dist/  
/nbdist/  
/.nb-gradle/  
build/  
!**/src/main/**/build/  
!**/src/test/**/build/  
  
### VS Code ###  
.vscode/  
  
# Log file  
*.log  
/logs*  
  
# BlueJ files  
*.ctxt  
  
# Mobile Tools for Java (J2ME)  
.mtj.tmp/  
  
# Package Files #  
*.jar  
*.war  
*.ear  
*.zip  
*.tar.gz  
*.rar  
*.cmd

其他

此外,还有一些其他建议:

  • master 分支的每一次更新,都建议打 tag 添加标签,通常为对应版本号,便于管理

  • feature分支、hotfix分支在合并后可以删除,避免分支过多管理混乱

  • 每次 pull 代码前,提交本地代码到本地库中,否则可能回出现合并代码出错,导致代码丢失