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!

假设我们忘记了修改内容且没有正式提交,则上述操作还是很有用的。

现在,我们只addcommit,看看状态变成什么。

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)

版本库中存了很多东西。最重要的有:

  1. 暂存区(stage或index)
  2. Git自动为我们创建的第一个分支master(后述);
  3. 指向master的指针HEAD(后述)。

工作原理

如图,这就明白了addcommit的工作原理

  1. add把文件从工作区添加到暂存区;
  2. 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")

系统检测到:

  1. readme.txt改动了,但没有add至暂存区(以准备commit);
  2. 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 只有一行。

假设我们:

  1. readme.txt中增加一行;

  2. 仅仅addcommit

  3. 又增加一行,然后无任何操作;

  4. 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

--很重要:保持在当前分支。否则就变成了“切换到另一分支”的命令,后述。

注意,这个操作有两个语境:

  1. 上次操作为commit。则该操作使得工作区文件恢复至版本库最新版本。
  2. 上次操作为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加密的,因此需要以下准备:

  1. 创建SSH Key。参考教程

    在命令行中输入:ssh-keygen -t rsa -C "youremail@example.com",无需设置密码。

    在用户主目录可以找到.ssh目录,里面有公钥id_rsa.pub和私钥id_rsa

  2. 登录远程仓库,填入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后又新提交一次后的情形:

dev

所以Git快啊!创建新分支时,仅需要将HEAD指针换个指向!

假如工作已完成,需要合并分支,那么master指针就指向dev指针指向的最新提交,然后HEAD回来即可:

merge

此时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。现在,两个分支上都有最新提交:

conflict

此时再合并。如果两次提交的内容不同,就会引发冲突:

$ 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

等价于该图:

merge

4.3 分支管理策略

我们回忆一下。在fast forward模式下,我们的图是这样的:

ff

这样,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(提交),也就在历史中可考了。

no ff

看,这图和上上图是一样的。

在实际开发时,我们可以这么做:master分支只有稳定的发布版本,而dev分支是工作分支。小伙伴们在dev的基础上,分出自己的分支,独立工作,然后合并到dev。到了新版本发布日,dev再合并到master。如图:

team play

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分支仍然存在。现在有两种解决方案:

  1. dev分支上,重新执行一次刚刚的修改(4c805e2)并提交。

  2. Git提供了一个更简单的commit复制指令,如下:

    $ git cherry-pick 4c805e2
    [master 1d4b803] fix bug 101
    1 file changed, 1 insertion(+), 1 deletion(-)
    

我的实验

  1. 创建file-master.txt,内容为haah

    masteradd+commit

  2. 创建并切换至dev

    创建file-dev.txt,内容为devvv

    add+commit

  3. 继续添加一行day2,仅add没有commit

  4. 此时突然要回到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
    
  5. 那我选择stash,然后回到master。此时文件夹里的file-dev.txt不见了!!!

  6. 修好haah并提交(id是907d79c,后面用得上)。切换回dev(那一瞬间file-dev就回来了)。

  7. git stash pop,此时file-master.txthaah又回来了!

    这说明:devmaster里文件的状态是不同的!这就是所谓的:dev分支bug仍然存在!!!

  8. 所以,要么手动修一遍,要么git cherry-pick 907d79c。此时file-master.txt里又成了haha

  9. 假设我们没有在dev修复bug,执行版本回退:git reset --hard HEAD^haah又回来了。但是注意,由于day2也没来得及提交,因此也不见了!

  10. 现在重新执行第3步,并且commit

  11. 最后一步,我们尝试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文件,那么就是典型的冲突问题,需要手工解决。

现在进行另一个实验:

  1. 创建exp.txt,在第二行输入hahahaadd+commit

  2. 创建并切换至dev,在第三行输入devadd+commit

  3. 切换回master,第三行马上就消失了。在第一行输入masteradd+commit

  4. 合并会报错,虽然修改不在同一行:

    $ 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
    
  5. 手工修改全文,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

要注意:masterdev分支需要和远程同步,因为一个是发布库,一个是大家的工作库。但bugfeature就不一定了,取决于你是否需要合作开发。

现在重点讲解一下多人协作的流程和冲突解决。

假设你有一个小伙伴。他从远程库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类文件可忽略:

  1. 忽略操作系统自动生成的文件,比如缩略图等;

  2. 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;

  3. 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

详见教程

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,保存一个公共版本。为了方便多终端协同,分布式版本控制系统还引入了一个大杀器:分支管理

posted @ 2019-03-02 12:06  RyanXing  阅读(396)  评论(0编辑  收藏  举报