Git学习笔记(on macOS)

  • 2017.9.6 更新至 分支管理-Bug分支

一年前用过git完成一些课程作业,一些基本知识早就忘记了。正好趁现在复习一遍,以后用处还很多。

主要参考:廖雪峰的Git教程,这确实是一份十分友好的入门教程。在整理笔记前,该教程已经有600多万的阅读量了。

下面进入整理后的正文部分。篇幅较长,可使用目录快速跳转。

目录

安装Git

创建版本库

时光机穿梭

版本回退/前进
工作区和暂存区
管理修改
撤销修改
删除文件

远程仓库

添加到远程仓库
从远程仓库克隆

分支管理

创建与合并分支
解决冲突
分支管理策略
Bug分支(未完待续)

一、安装Git

输入git,以查看系统是否安装Git。

安装git:

sudo apt-get install git

安装完成后,设置名字和Email:

git config --global user.name "Your Name"
git config --global user.email "[email protected]"

--global参数表示该计算机上所有Git仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和Email地址。

二、创建版本库

版本库也叫仓库,英文名repository,可以理解成一个目录。该目录里的所有文件都会被Git所管理。Git可以跟踪每个文件的修改、删除,以便追踪历史或者复原修改。

首先创建一个空目录:

mkdir learngit
cd learngit

第二步,通过git init命令将此目录变成git可以管理的仓库,即初始化(Initialize)。

git init

注意:不一定只有空目录才能创建仓库,选择一个已有文件的目录也可以。

下面在该目录中(或者在其子目录中)添加一个readme.md文件(.md为markdown文件后缀,有关markdown的学习笔记在这篇文章里有记载,使用纯文本文件.txt不影响效果),内容如下:

Git is a version control system.
Git is free software.

目前该文件处在目录中而不是Git仓库中,我们还需要两步来把该文件放入仓库:

  1. 用命令git add将文件添加到暂存区(暂存区的概念在后文描述):
git add readme.md
  1. 用命令git commit告诉Git,把文件提交到仓库:
git commit -m "wrote a readme file"

-m后面输入的是提交说明。

commit命令一次可以提交多个文件。

例如:再创建三个文件,内容分别为file1/file2/file3,文件名为file1.md…,并将它们添加、提交。

三、时光机穿梭

首先学习git statusgit diff这两个命令。

我们修改readme.md中的内容,在第一行加入单词distributed

Git is a distributed version control system.
Git is free software.
  • 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.md

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

结果表明readme.md被修改,但没有提交。

  • git diff命令可以查看修改前后的差别。运行后得到:
diff --git a/readme.md b/readme.md
index 8d7a2fb..a20531f 100644
--- a/readme.md
+++ b/readme.md
@@ -1,3 +1,3 @@
-Git is a version control system.
+Git is a distributed version control system.
 Git is free software.

添加并提交(commit)该文件,再次运行git status,便得知:

On branch master
nothing to commit, working tree clean

版本回退/前进

为了产生多个版本,我们再次修改readme.md文件,将内容改为:

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

然后使用git addgit commit命令提交。

这样我们就有了三个版本的readme.md文件,查看版本的命令为:git log,结果如下:

commit dacffeaf5d817a376e7ba0d97d4d7644e3ebe817
Author: Hope <[email protected]>
Date:   Tue Sep 5 17:29:04 2017 +0800

    add GPL

commit c94ee6cb523d8ea55dfa6c571fc8a366a16e6607
Author: HusterHope <[email protected]>
Date:   Tue Sep 5 15:38:42 2017 +0800

    add distributed

commit 51b6aa32a9a84142ae9fa4df8b90e6b41e247c3d
Author: HusterHope <[email protected]>
Date:   Tue Sep 5 15:36:47 2017 +0800

    add 3 files.

commit c2e7c59e9a2454ec64d1c2394350a552a77504b5
Author: HusterHope <[email protected]>
Date:   Tue Sep 5 15:34:09 2017 +0800

    wrote a readme file

当然,这些信息看上去比较杂,可以用git log --pretty=oneline命令简化输出,得到:

dacffeaf5d817a376e7ba0d97d4d7644e3ebe817 add GPL
c94ee6cb523d8ea55dfa6c571fc8a366a16e6607 add distributed
51b6aa32a9a84142ae9fa4df8b90e6b41e247c3d add 3 files.
c2e7c59e9a2454ec64d1c2394350a552a77504b5 wrote a readme file

历史记录中,每一行的开头就是版本号。(这些版本号在不同的电脑上是不一样的)

Git中,以HEAD表示当前版本,HEAD^代表上一个版本,HEAD^^代表上上一个版本,HEAD~n代表上n个版本(n为整数)。

现在我们希望退回上一版本(即“Add distributed”),输入回退命令:

git reset --hard HEAD^

如果需要撤销刚才的回退操作,我们就需要用到版本号(commit_id)了,首先使用命令:

git reflog

得到历史操作的版本号:

c94ee6c HEAD@{0}: reset: moving to HEAD^
dacffea HEAD@{1}: commit: add GPL
c94ee6c HEAD@{2}: commit: add distributed
51b6aa3 HEAD@{3}: commit: add 3 files.
c2e7c59 HEAD@{4}: commit (initial): wrote a readme file

可以看到“add GPL”的版本依然存在,于是使用命令:

git reset --hard dacffea

这样就又回到了最新版本。

工作区和暂存区

工作区(Working Directory)

电脑里能够看到的目录,比如LearnGit文件夹。

暂存区(Stage)

暂存区处于git的版本库(Repository)中,后者是工作区内的隐藏目录.git,内部除了暂存区外,还有Git自动创建的第一个分支master,和指向master的指针HEAD

在把文件添加到Git版本库中的时候,第一步就是把文件添加到暂存区;

第二步则是将暂存区的内容提交到当前分支。

管理修改

Git跟踪并管理的是修改,而非文件。

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

为了和参考教程保持一致,我们将readme.md修改成:
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.
并commit.

撤销修改

这一节原教程中有部分内容存在歧义,等以后有使用需求后再整理。

删除文件

比如我们使用指令rm file1.md或者在文件管理器删除了file1.md,则工作区和版本库就出现了差异:

$ 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:    file1.md

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

Git系统提示我们:

use “git add/rm ..." to update what will be committed.

use “git checkout – ..." to discard changes in working directory.

即:可以使用git add/rm <file>... 指令将准备进行commit的文件提交到版本库,这里用rm而不用add

  • 注意:Git在删除时也需要commit。

还可以用git checkout -- <file>...命令撤销修改。

四、远程仓库

创建远程仓库的方法详见原文链接,此处不再赘述。

添加到远程库

在Github中添加Repository,选择默认设置。

然后,Github会给出一些操作提示,我们将本地仓库放置到刚创建的Github仓库中:

git remote add origin https://github.com/HusterHope/learngit.git
git push -u origin master

第一步:关联远程库。这里的origin为远程库的名字,也可以修改成其他名称。HusterHope为我的用户名,learngit为Github仓库名。

第二步:把本地内容推送到远程库。这里终端会提示输入GitHub的用户名和密码。-u参数的作用是让Git把本地的master分支内容推送到远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。

这样,只要本地作了提交,就可以通过命令:

git push origin master

把本地分支的修改推送至GitHub.

从远程库克隆

假设我们从零开发,即从远程库克隆到本地。

为了不和本地现有的learnGit仓库冲突,我们在与它平行的目录下操作,终端输入:

cd ..

返回learnGit仓库所在目录。

然后克隆远程库:

git clone https://github.com/HusterHope/GitSkills.git

进入GitSkills目录下,使用git status命令查看状态:

On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

这样就获取了远程库,并完成同步。

  • 注意:远程库在克隆到本地后会自动生成一个同仓库名相同的文件夹,这个文件夹就是Git仓库,我们就不用在本地手动init仓库了。

五、分支管理

分支就像《命运石之门》里的平行世界线。

如果整个工程只有一条主线,且开发一个新功能需要两周才能完成。第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了;如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。

这样就有了使用分支的需求,特别是用于版本迭代。

创建与合并分支

到现在为止,learnGit仓库中都只有一条分支mastermaster分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点。

创建分支

首先,创建分支dev

git branch dev

可用命令git branch查看分支。

切换分支

现在切换到刚才创建的分支上,输入指令:

git checkout dev

创建和切换到分支dev的过程可以用以下一行指令代替:

git checkout -b dev
测试分支

然后就可以在dev分支上正常提交了。比如加上一句话后提交:

Creating a new branch is quick.

再切换回master分支:

git checkout master

神奇的事情发生了,刚才我们修改过的readme.md,新的内容不见了!

原因是刚才的改动处于dev分支上。

合并分支

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

git merge dev

其中,git merge命令用于合并指定分支到当前分支。

删除分支

合并完成后,可用以下命令删除dev分支:

git branch -d dev

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

解决冲突

刚才创建和合并分支的过程没有出现两条分支一同前进的进度差异,因此合并的过程也十分顺畅。

当两个分支都对同一文件作出改动时,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突。

例如,创建并切换到分支feature1,将readme.md的最后一行改为:

Creating a new branch is quick AND simple.

提交修改后,切换到master分支,并在此分支上将最后一行改为:(只有AND一词和另一分支有差别)

Creating a new branch is quick & simple.

再次提交修改。

这时,如果我们合并分支,就会产生冲突:

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

打开readme.md,发现Git已经非常贴心地告诉了我们冲突的地方:

<<<<<<< HEAD

Creating a new branch is quick & simple.

Creating a new branch is quick AND simple.

>>>>>>> feature1

于是将master分支上的readme.md文件最后一行改为:

Creating a new branch is quick AND simple.

并提交。

  • 注意,此时Git已经自动帮我们把分支合并了,如果使用指令git merge feature1会发现提示:
Already up-to-date.

当然,如果没有两份readme.md本身内容上没有冲突,合并依然可以进行,我们切换到分支feature1,加入内容:

Merging branches may cause conflicts.

提交,合并分支:

$ git add readme.md 

$ git commit -m "no conflict"
[feature1 98e19b0] no conflict
 1 file changed, 3 insertions(+), 1 deletion(-)

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

$ git merge feature1
Merge made by the 'recursive' strategy.
 readme.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

这样,我们就可以删除分支feature1,并push到Github了。

PS.用指令git log --graph可以查看分支合并图,使用指令git log --graph --pretty=oneline --abbrev-commit可以查看更简洁的分支图。

分支管理策略

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,合并分支后,会丢掉分支信息。也就是在log里看不出曾经分支的痕迹。

$ git log --graph --pretty=oneline --abbrev-commit
*   192c4f3 merge with no-ff
|\  
| * 8e27cb7 add merge
|/  
* e1116e0 backup to ff
* cb4a200 git use ff

如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。

可以使用--no-ff变量禁用Fast forward模式。

分支策略:在实际开发中,我们应该按照几个基本原则进行分支管理:

首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;

干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;

你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。

效果如下图:

Bug分支

(未完待续)