Git随手记
Git随手记
主要参考(强烈推荐):廖雪峰的Git教程。
1. 启程
以下教程默认在Ubuntu上操作。因为Git最早也是为Linux打造的。
1.1 安装
查看是否已安装:
git
若未安装,安装:
sudo apt-get install git
Git也有Windows版本,可以从官网下载安装。我们会得到一个类似命令行窗口的Git Bash。
1.2 设置机器身份
Git 是分布式版本控制系统,因此每个机器都需要有对应的ID:名字和邮箱地址。
我们来设置本台机器的信息:
git config --global user.name "Name"
git config --global user.email "Email address"
--global
的意思是:这台机器上的所有Git仓库都使用该配置信息。
1.3 创建版本库
版本库(repository)其实就是一个仓库。特别的是,该仓库内所有文件都会被Git管理起来。这些文件的修改和删除都可以被Git跟踪,从而也能够恢复和还原。
假设现在有一个文件夹,路径为/home/xing/MEQE_local
。我们想让它变成一个版本库。很简单:在该目录下执行:
git init
此时会产生一个隐藏的.git
目录,可以用ls -ah
查看到。该目录是 Git 用以跟踪版本库的,千万不要手动修改。
注意,为了避免异常,路径中不要有中文!
我们要明确:版本控制系统只能跟踪文本文件的改动,比如txt,网页和所有的程序代码等。具体来说,版本控制系统能知道每次改动了哪个位置、什么内容。
但对于图片、视频等二进制文件,版本控制系统无法知道其具体改动,只知道图片大小从100KB变成了120KB等。注意,Word也是二进制格式。
此外,在Windows编程时注意:Windows自带的记事本不要用,其会在每个文件开头添加0xefbbbf
(十六进制)的字符,导致程序报错、网页显示异常等。强烈建议使用免费的Notepad++代替记事本,记得将默认编码设置为UTF-8 without BOM
。
2. 基础操作
先讲怎么做,后面再讲原理。
2.1 提交修改或新增文件至版本库
假设我们在该目录下创建了一个txt文件,名为readme.txt
。
该文件只有一行内容:Git is free software.
第一步,告诉Git有哪些文件修改(或新增)需要记录:
git add readme.txt
提示: 1 file changed, 1 insertion(+)
。因为添加了一个文件,写了一行。
第二步,将修改内容正式提交至版本库备份:
git commit -m "create a readme file."
-m
是说明内容,可以任意。当然,越详细越有利于日后回滚。
如果是非空文件夹,也可以提交,如:
git add demo/
如果是多个文件,可以多次记录,一次性提交:
git add file0.txt
git add file1.txt file2.txt file3.txt
git commit -m "add 4 files."
2.2 修改文件并查看修改
假设我们加上了新的一行:Keep moving!
此时运行git status
查看仓库状态,会显示:
$ 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检测到变动:readme.txt
,但是没有准备提交的修改,并且连暂存区内也没有 。这些概念后面会提。
我们可以用git diff
查看变动内容(difference),实际上是查看工作区和版本库最新版本的区别:
$ git diff readme.txt
diff --git a/readme.txt b/readme.txt
index f5b143e..a48528a 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1 +1,2 @@
Git is free software.
+Keep moving!
最后一行告诉我们,增加了一行Keep moving!
假设我们忘记了修改内容且没有正式提交,则上述操作还是很有用的。
现在,我们只add
不commit
,看看状态变成什么。
git add readme.txt # 没有任何输出。对UNIX而言,正常的状态就是没有输出。
此时再查看状态,就不一样了:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: readme.txt
确认将要正式commit
的文件只有readme.txt
,那么就可以正式提交了。
$ git commit -m "add distributed"
[master e475afc] add distributed
1 file changed, 1 insertion(+), 1 deletion(-)
再次查看状态,就显示没有文件需要commit
,并且工作目录是干净的(没有变动):
$ git status
On branch master
nothing to commit, working tree clean
2.3 查看历史变动信息
每一次commit
,实际上就是一次游戏存盘,或者说一次正式快照。
我们可以用git log
命令查看历史变动信息,由近及远:
$ git log
commit b87e2c5fd623591a510a4873aecdc43793de392e
Author: xing <ryanxingql@foxmail.com>
Date: Wed Nov 7 21:41:48 2018 +0800
add a new line.
commit f5c7b7bb1df2455cec90a092ecd231dd8e395bc3
Author: xing <ryanxingql@foxmail.com>
Date: Wed Nov 7 20:57:11 2018 +0800
create a readme file
如果我们想让每一次变动只显示在一行内,可以这么做:
$ git log --pretty=oneline
b87e2c5fd623591a510a4873aecdc43793de392e add a new line.
f5c7b7bb1df2455cec90a092ecd231dd8e395bc3 create a readme file
前面一串是commit id
。如此复杂的原因,是因为分布式系统可能会有多人协同,版本号当然不能用1,2,3简单地表示。我们还可以用一些可视化工具更清晰地看到修改时间线。
2.4 版本回退
我们准备把readme.txt
回退到最初版本。
首先我们要知道:**在Git中,HEAD
指向并代表当前版本,HEAD^
代表上一个版本,HEAD^^
代表上上一个版本……如果是往上100个,那就是HEAD~100**
。
回退很简单:
$ git reset --hard HEAD^
HEAD is now at f5c7b7b create a readme file
--hard
参数后面再讲。
再查看文件时,已经恢复成最初的模样!
此时再查看历史记录:
$ git log
commit f5c7b7bb1df2455cec90a092ecd231dd8e395bc3
Author: xing <ryanxingql@foxmail.com>
Date: Wed Nov 7 20:57:11 2018 +0800
create a readme file
完了,记录没了,回不去了?
且慢,只要命令行没关,你可以先找回原来的版本号b87e2c5fd623591a510a4873aecdc43793de392e
,然后指定版本号回退:
git reset --hard b87e2c5fd623591a510a4873aecdc43793de392e
又恢复成最后的样子了!
其实可以只输入版本号的前几位b87e
(只要不混淆),Git会自动去找。
Git的回退速度非常快,因为本质上是在Git内部完成了HEAD
指针的移动,并且同时把工作区的文件更新为历史版本。HEAD
指针永远指向当前版本。
如果命令行关闭了,版本号也丢失了,那么我们还可以用git reflog
来找回版本号。其原理是:Git记录了每一次命令,而命令中就记录了回退的那个版本号:
$ git reflog
b87e2c5 HEAD@{0}: reset: moving to b87e2c5fd623591a510a4873aecdc43793de392e
f5c7b7b HEAD@{1}: reset: moving to HEAD^
b87e2c5 HEAD@{2}: commit: add a new line.
f5c7b7b HEAD@{3}: commit (initial): create a readme file
2.5 工作区和暂存区
暂存区是Git的特点之一。
我们看到的目录/home/xing/MEQE_local
,实际上是工作区(working directory)而不是版本库。
我们最开始提到的.git
隐藏目录,不属于工作区,而正是版本库(repository)。
版本库中存了很多东西。最重要的有:
- 暂存区(stage或index);
- Git自动为我们创建的第一个分支
master
(后述); - 指向
master
的指针HEAD
(后述)。
如图,这就明白了add
和commit
的工作原理。
add
把文件从工作区添加到暂存区;commit
,把文件从暂存区提交到当前分支。由于master
是创库时Git自动创建的唯一分支,因此会提交至master
分支。
所以我们说:我们可以连续add
很多文件到暂存区,最后一次性commit
到当前分支里。
理解了原理,现在我们开始实验。我们把readme.txt
的第二行删掉(文件修改),再创建一个LICENSE.txt
文件(新增文件)。然后再查看状态:
$ 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.txt
no changes added to commit (use "git add" and/or "git commit -a")
系统检测到:
readme.txt
改动了,但没有add
至暂存区(以准备commit
);LICENSE.txt
还从未add
到暂存区,连初始文件(不是改动)都没有,因此没有被“追踪”。
我们分别对这两个文件都执行add
以后,状态将变成:
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: LICENSE.txt
modified: readme.txt
即,两个文件现在都在暂存区里了:
然后git commit -m "commit 2 files."
,暂存区就空了:
在这次提交后,到下次查看状态git status
前,如果没有对工作区进行任何修改,那么就称为工作区是“干净的”(working tree clean)。
2.6 管理修改而不是管理文件
现在的 readme.txt
只有一行。
假设我们:
-
在
readme.txt
中增加一行; -
仅仅
add
又commit
; -
又增加一行,然后无任何操作;
-
commit
如果Git管理的是文件,那么既然文件被二次修改,则二次修改后的文件就应该被提交到分支中。
然而,尽管文件已经修改了两回,但只有第一次修改通过add
放入到暂存区,第二次修改没有被放入暂存区。因此如果查看状态,会显示:
$ 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 diff HEAD -- readme.txt
diff --git a/readme.txt b/readme.txt
index 80cc1f9..785e827 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,3 @@
Git is free software.
Hello
+Hello again
2.7 撤销修改
分以下三种情况,从轻到重。
没有add
即:工作区变化,但暂存区和版本库都没有变化。此时只需要一步,即可撤销工作区变动:
git checkout -- readme.txt
--
很重要:保持在当前分支。否则就变成了“切换到另一分支”的命令,后述。
注意,这个操作有两个语境:
- 上次操作为
commit
。则该操作使得工作区文件恢复至版本库最新版本。 - 上次操作为
add
。则该操作使得工作区文件恢复至暂存区状态。
总之,就是退一步。
仅add
即:工作区和暂存区发生了变化,但版本库没变。此时需要两步:
第一步:将暂存区记录取消。
git reset HEAD readme.txt
之前我们学习过用reset
完成版本回退。这里我们知道,reset
还可以用来撤销暂存区的变动。
第二步:撤销工作区文件的变动。这和上面的做法一致:
git checkout -- readme.txt
add
+commit
此时,版本库都变化了。撤销修改只能用版本回退的方法。
不仅commit
,还推送至了远程仓库
还记得Git是分布式系统吗?如果已经推送至远程仓库,那么真的就凉了……后述。
2.8 删除文件
假设我们在文件管理器中把LICENSE.txt
删除:$ rm LICENSE.txt
。
由于该文件被追踪,且版本库和工作区不一致,此时查看状态会显示deleted
:
$ 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: LICENSE.txt
no changes added to commit (use "git add" and/or "git commit -a")
如果确认不是误删,我们可以从版本库中删除对应文件,注意也要commit
:
$ git rm test.txt
rm 'LICENSE.txt'
$ git commit -m "remove LICENSE.txt"
[master d46f35e] remove test.txt
1 file changed, 1 deletion(-)
delete mode 100644 LICENSE.txt
当然了,用git add
代替git rm
也可以,因为删除也是一种对文件的修改。
如果我们删错了,很简单,和上一节“没有add
”的操作一样,我们只需要撤回工作区修改:
git checkout -- LICENSE.txt
一定要注意:若该文件在版本库中压根不存在,那么是无法恢复的!即未追踪。
3. Git的杀手锏一:远程仓库
如果只是本地版本控制系统(或集中式),那么Git相较于SVN没有任何优势。
但作为分布式管理系统,Git有众多优势。远程仓库是第一个杀手锏。
Git的同一个仓库,可以部署到不同的机器上。
对个人玩家(比如我)而言,我们不会在一台机器的多个硬盘上进行同步,而是在个人电脑和远程服务器上进行同步。
我们可以自行搭建一台运行Git的服务器,但更多时候,我们会借助 GitHub等Git仓库托管服务平台。
注意,本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,因此需要以下准备:
-
创建SSH Key。参考教程。
在命令行中输入:
ssh-keygen -t rsa -C "youremail@example.com"
,无需设置密码。在用户主目录可以找到
.ssh
目录,里面有公钥id_rsa.pub
和私钥id_rsa
。 -
登录远程仓库,填入
id_rsa.pub
文件的内容。原因是,远程仓库需要知道是本人提交的推送,而 Git 支持 SSH 协议。
如果我们有很多电脑,那就把这些设备的公钥都设置好。
3.1 添加远程仓库
在网站上操作。
假设我们现在有一个文件夹:/home/xing/Desktop/MFQE_code
,并将其设置为Git可管理的本地仓库。
我们将其和网站上的MFQE仓库关联。在当前目录下运行:
git remote add origin https://gitee.com/XINGRYAN/MFQE.git
后面的地址别搞错了,否则可能会关联到别人的远程库,但由于公钥不在其列表中,因此无法推上去。
添加后,远程仓库名就是origin
。这是Git默认的叫法,可改,但一看到origin
就知道是远程库。
3.2 推送上去
我们写一个README.md
,将其推送到远程库上。
推送前,我们应该先add
+commit
。
然后执行:
$ git push -u origin master
Counting objects: 7, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (7/7), 593 bytes | 0 bytes/s, done.
Total 7 (delta 0), reused 0 (delta 0)
remote: Powered by Gitee.com
To https://gitee.com/XINGRYAN/MFQE.git
* [new branch] master -> master
Branch master set up to track remote branch master from origin.
意思是:将当前分支master
推送到远程。
由于远程库是空的,因此第一次推送时,我们加上-u
参数,将本地master
和远程master
分支(默认名称是origin
)关联起来,在以后的推送和拉取时可以简化命令。
3.3 从远程库克隆
如果我们本地什么都没有,希望从远程库上克隆下来一个库,那么我们可以这么操作。
首先,假设有一个库名为gitskills
,属于michaelliao的,里面只有一个README.md
。那么我们执行:
$ 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.
此时在本地的gitskills
目录下,就有了一个完全相同的库,含README.md
。
实际上Git支持多种协议,包括SSH和https。但https较慢,而且必须每次输入口令。
4. 分支管理
如果你要开发系统的一个子功能,你可以先创建自己的分支,然后在上面完善代码,最后再提交。Git比其他版本控制系统SVN等更快,可以在1秒内完成数万文件版本库的分支管理。
4.1 创建与合并分支
我们之前只用到了master
这一主分支,HEAD
指针指向着它。实际上是master
指针指向当前的提交,而HEAD
指针指向master
,以此确定当前提交所在分支。
若我们创建一个分支dev
,那么就会产生一个新的指针dev
指向当前提交。而HEAD
转而指向新分支dev
。例如图中就是创建分支dev
后又新提交一次后的情形:
所以Git快啊!创建新分支时,仅需要将HEAD
指针换个指向!
假如工作已完成,需要合并分支,那么master
指针就指向dev
指针指向的最新提交,然后HEAD
回来即可:
此时dev
分支可以删除,本质就是将指针删除。
下面是代码:
git branch dev # 创建分支dev
$ git checkout dev # 切换至分支dev。以上两行等同于: git checkout -b dev
Switched to branch 'dev'
$ git branch # 查看所有分支及当前分支
* dev
master
$ git checkout master # 完成一系列工作后,切换回master分支
Switched to branch 'master'
$ git merge dev # 合并dev分支到master分支
Updating d46f35e..b17d20e
Fast-forward # 由于仅仅是master指针的移动,因此很快很简单,属于fast-forward模式
readme.txt | 1 +
1 file changed, 1 insertion(+)
$ git branch -d dev # 删除dev分支
Deleted branch dev (was b17d20e).
我们之前说过,git checkout -- <file>
是撤销修改指令。它很容易和git checkout dev
混淆。因此新版Git提供了switch
指令来切换分支:
git switch -c dev # 创建+切换
4.2 解决冲突
分支合并时很容易发生冲突。比如某人在feature1
分支上修改了readme.txt
,没有提交,就切换到了master
主分支。切换时会看到领先提示:
$ git switch master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
接着,还没有合并分支,就在master
分支上修改了readme.txt
。现在,两个分支上都有最新提交:
此时再合并。如果两次提交的内容不同,就会引发冲突:
$ 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 status
也会提示有冲突存在,必须手动解决。我们现在在master
分支上,将readme.txt
手动修改为feature1
分支上的最新版本,合并即可。
最后,我们可以用git log --graph
指令查看历史:
$ 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
等价于该图:
4.3 分支管理策略
我们回忆一下。在fast forward模式下,我们的图是这样的:
这样,dev
分支的提交信息会随着分支删除而消失。我们可以尝试类似上面的做法,只需要关掉fast forward:
$ git switch -c dev # 创建并切换至dev
Switched to a new branch 'dev'
git add readme.txt # 修改readme.txt
$ git commit -m "add merge"
[dev f52c633] add merge
1 file changed, 1 insertion(+)
$ git switch master # 切换回master
Switched to branch 'master'
$ git merge --no-ff -m "merge with no-ff" dev # 合并,注意no-ff参数
Merge made by the 'recursive' strategy.
readme.txt | 1 +
1 file changed, 1 insertion(+)
$ git log --graph --pretty=oneline --abbrev-commit # 查看分支历史
* e1e9c68 (HEAD -> master) merge with no-ff
|\
| * f52c633 (dev) add merge
|/
* cf810e4 conflict fixed
...
可见,Git在禁用ff的合并时,会创建一次commit(提交),也就在历史中可考了。
看,这图和上上图是一样的。
在实际开发时,我们可以这么做:master
分支只有稳定的发布版本,而dev
分支是工作分支。小伙伴们在dev
的基础上,分出自己的分支,独立工作,然后合并到dev
。到了新版本发布日,dev
再合并到master
。如图:
4.4 Bug分支
我们前面提过,当两个分支都修改了readme.txt
时,无论在哪一个分支提交,都会引发冲突,需要手动解决。
一个很直接的例子是:当我们在dev
分支工作时,已经对readme.txt
进行了一定修改且add
但没有commit
。此时,已发布版本(在master
)发现了bug-101需要修复。
如果我们直接创建一个新分支issue-101
进行bug修复,在commit
时要么会引发冲突,要么会使得dev
上的“半成品”被提交。但我们不希望dev
分支上的工作提交。
此时,Git就提供了一个stash
功能,可以将dev
分支上的工作隐藏:
$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge
此时git status
时,会提示工作区是干净的。然后执行:
$ 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'
修复完后,切换到master
,合并并删除issue-101
分支:
$ git switch 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(-)
然后再回到dev
分支:
$ git switch dev
Switched to branch 'dev'
$ git status
On branch dev
nothing to commit, working tree clean
用git stash list
可以查看隐藏历史:
$ git stash list
stash@{0}: WIP on dev: f52c633 add merge
此时,我们可以用git stash apply
恢复但不删除stash
,也可以用git stach pop
恢复且删除:
$ 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)
最后还有一个问题。由于dev
分支是从早期master
分出来的,因此刚刚的bug在dev
分支仍然存在。现在有两种解决方案:
-
在
dev
分支上,重新执行一次刚刚的修改(4c805e2)并提交。 -
Git提供了一个更简单的
commit
复制指令,如下:$ git cherry-pick 4c805e2 [master 1d4b803] fix bug 101 1 file changed, 1 insertion(+), 1 deletion(-)
我的实验
-
创建
file-master.txt
,内容为haah
。在
master
下add
+commit
。 -
创建并切换至
dev
。创建
file-dev.txt
,内容为devvv
。add
+commit
。 -
继续添加一行
day2
,仅add
没有commit
。 -
此时突然要回到
master
分支,将file-master
里的haah
改成haha
。尝试切换但失败:$ git checkout master error: Your local changes to the following files would be overwritten by checkout: file-dev.txt Please commit your changes or stash them before you switch branches. Aborting
-
那我选择
stash
,然后回到master
。此时文件夹里的file-dev.txt
不见了!!! -
修好
haah
并提交(id是907d79c,后面用得上)。切换回dev
(那一瞬间file-dev
就回来了)。 -
git stash pop
,此时file-master.txt
的haah
又回来了!这说明:
dev
和master
里文件的状态是不同的!这就是所谓的:dev
分支bug仍然存在!!! -
所以,要么手动修一遍,要么
git cherry-pick 907d79c
。此时file-master.txt
里又成了haha
。 -
假设我们没有在
dev
修复bug,执行版本回退:git reset --hard HEAD^
。haah
又回来了。但是注意,由于day2
也没来得及提交,因此也不见了! -
现在重新执行第3步,并且
commit
。 -
最后一步,我们尝试merge。现在两个分支的
file-master
是不同的,master
分支里是haha
,但dev
分支里还是haah
。结果是,file-dev
也出现了,同时file-master
里是haha
。理解:git管理的是修改而不是文件。
dev
分支的提交没有对file-master
文件进行修改,只记录了对file-dev
文件的修改。因此合并时仅仅加入了file-dev
文件,file-master
文件还是沿袭master
最新提交。如果
dev
分支和master
分支都修改了file-master
文件,那么就是典型的冲突问题,需要手工解决。
现在进行另一个实验:
-
创建
exp.txt
,在第二行输入hahaha
。add
+commit
。 -
创建并切换至
dev
,在第三行输入dev
。add
+commit
。 -
切换回
master
,第三行马上就消失了。在第一行输入master
。add
+commit
。 -
合并会报错,虽然修改不在同一行:
$ git merge dev Auto-merging exp.txt CONFLICT (content): Merge conflict in exp.txt Automatic merge failed; fix conflicts and then commit the result.
而
exp.txt
里的内容是:<<<<<<< HEAD "master" hahaha ======= hahaha dev >>>>>>> dev
-
手工修改全文,
add
+commit
即可。
4.5 Feature分支
场景:你被要求开发一个新功能vulcan
,因此和上一节类似,创建了一个新的分支feature-vulcan
并切换到上面进行操作。
如果一切顺利,我们提交新功能,然后切回dev
合并即可,删除该分支。
但是,如果计划突然中断,并且要销毁该分支,那么我们应执行:
$ 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 branch -D feature-vulcan
Deleted branch feature-vulcan (was 287773e).
4.6 多人协作
当你从远程仓库克隆时,默认是将本地的master
分支与远程的master
分支对应。远程仓库默认名称为origin
。
当你推送分支时,例如希望将本地的dev
推至origin
,可以这么做:
git push origin dev
要注意:master
和dev
分支需要和远程同步,因为一个是发布库,一个是大家的工作库。但bug
和feature
就不一定了,取决于你是否需要合作开发。
现在重点讲解一下多人协作的流程和冲突解决。
假设你有一个小伙伴。他从远程库clone仓库。默认情况下,他只能看到本地master
分支(git branch
只有master
)。为了显示本地dev
分支,他就必须执行以下操作:
git checkout -b dev origin/dev
此时,他就可以在dev
分支上操作,提交后推送给远方的dev
:
git push origin dev
但是碰巧,你也在dev
修改了相同的文件。当推送时就发生了冲突:
$ 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 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 branch --set-upstream-to=origin/dev dev
Branch 'dev' set up to track remote branch 'dev' from 'origin'.
此时就可以下拉了,但是出现了本地冲突(刚刚是远程库的冲突)。此时的解决方法就跟以前学过的冲突解决一样了。
$ git pull
Auto-merging env.txt
CONFLICT (add/add): Merge conflict in env.txt
Automatic merge failed; fix conflicts and then commit the result.
5. 标签管理
我们让每一次提交的id对应一个标签,比如版本号。这样更好记,更好溯源。
打标签和查看标签都很简单:
git tag v1.0 # 给最新提交打标签
$ git tag # 查看标签
v1.0
git tag v0.9 f52c633 # 给某一提交打标签
$ git tag # 注意,是按字母顺序列出
v0.9
v1.0
git tag -a v0.1 -m "version 0.1 released" 1094adb # 可以加上说明
删除标签也很简单:
git tag -d v0.1 # 不会自动推送到远程
注意,标签默认只在本地存储,不会自动推送到远程。我们需要手动推送:
$ 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
$ git push origin --tags # 推送所有标签
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
* [new tag] v0.9 -> v0.9
此时,如果我们要删除标签,不仅需要删除本地标签,还需要删除远程标签:
$ git push origin :refs/tags/v0.9
To github.com:michaelliao/learngit.git
- [deleted] v0.9
6. 其他
6.1 清空历史版本
有时候我们会把GitHub当网盘用,但问题是本地会留下大量的历史记录。尤其是当文件中不小心混入非文本文件时,版本库会很大。此外,历史版本中可能存在泄密问题,比如密码等。
那么,如何安全地清空历史记录,让仓库看起来像新的一样呢?参见stackover。一定要注意,我们决不能直接删除.git
文件夹。这会导致本地和远程仓库不一致。
6.2 忽略特殊文件
有时候,我们的git目录里会放一些不能提交的文件,比如保存了密码的文件。每一次git status
都会提示未追踪,这让强迫症很抓狂。
简单的是,git提供了.gitignore
文件。将需要忽略的文件的文件名按格式填入即可。
建议以下3类文件可忽略:
-
忽略操作系统自动生成的文件,比如缩略图等;
-
忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
-
忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。
详见教程。
7. 故事
7.1 Git的诞生
Linus在1991年创建了开源的Linux。这个系统受到了全世界志愿者的欢迎。在2002年前,志愿者们将源代码通过diff的方式发送给Linus,然后Linus再手工合并代码。当时市面上有免费的集中式版本控制软件,如CVS和SVN,但由于速度慢、必须联网,因此Linus不予采用。而商用版本控制软件是收费的,与Linux开源精神不符,也不予采用。
此时,由于代码库太大,因此Linus不得不选择了一个商业版本控制系统BitKeeper。其公司出于人道主义精神,授权Linux社区免费使用。
然而,2005年Linux社区有人试图破解BitKeeper的协议被公司发现,因此Linux社区的免费使用权被收回。
最神奇的事情来了:Linus花了两周时间,用C写了一个分布式版本控制系统Git。一个月内,Linux系统的源码便由Git管理了!
2008年GitHub上线,它为开源项目免费提供Git存储,将分布式版本控制系统Git推向了高峰。
7.2 集中式vs.分布式
-
集中式版本控制系统:版本库都存放在中央服务器;每次在本地干活之前,需要从中央服务器获取最新版本;干完后,再推至中央服务器。最大的问题:必须联网。
-
分布式版本控制系统:每一台终端都是一个中央服务器。若终端A修改了一部分,终端B修改了一部分,当联网后,二者相互交换各自修改的内容即可完成同步。优势:多重备份,非常安全。
在分布式版本控制系统的实际应用中,我们常常将一个远程服务器当作永不修改内容的终端B。此时的优势仍然存在:终端A不需要联网也能正常工作。
若还有更多的终端C、D……,则每个终端都可以将修改内容推至终端B,保存一个公共版本。为了方便多终端协同,分布式版本控制系统还引入了一个大杀器:分支管理。