前言

git的命令行非常的简洁,博主基本上只使用git命令行工具,比如windows上的git bash。同时,为了加深理解,强烈建议初学某个功能或命令的时候,动手尝试一下。动手才是检验真理的唯一标准。
本文的立意并非是全面讲解git,而是从浅处着手,逐渐深入,直到接触到暂存区、底层存储等令人“眼花缭乱”的概念。


导读

  1. 概览
  2. 搭建git测试服务器
  3. 常规命令
  4. git暂存区
  5. git的底层存储
  6. 进阶命令
  7. 参考文档

概览


搭建git测试服务器

关于git的远程仓库,大家可以使用github,也可以自己搭建gitlab,在这里我们只是在centos上搭建一个非常简单的、用于测试的git服务器。

sudo yum install -y git curl curl-devel expat expat-devel gettext gettext-devel openssl openssl-devel zlib zlib-devel perl perl-devel
sudo groupadd git
sudo useradd git -g git -s /bin/bash
sudo mkdir -p ~git/.ssh
sudo touch ~git/.ssh/authorized_keys
sudo chown -R git:git ~git/.ssh
sudo chmod 600 ~git/.ssh/authorized_keys

sudo vim ~git/.ssh/authorized_keys
#将用户的公钥贴进该文件(每行一个),然后保存、退出vim
#在这里我们创建一个名称是timchow的仓库
[vagrant@hadoop-100 ~]$ sudo mkdir -p ~git/git-repository
[vagrant@hadoop-100 ~]$ sudo git init --bare ~git/git-repository/timchow.git
Initialized empty Git repository in /home/git/git-repository/timchow.git/
[vagrant@hadoop-100 ~]$ sudo chown -R git:git ~git/git-repository/
# 测试git服务器是否成功搭建
[vagrant@hadoop-100 ~]$ git clone git@192.168.100.100:/home/git/git-repository/timchow.git
Cloning into 'timchow'...
warning: You appear to have cloned an empty repository.

常规命令

1,git init [/path/to/your/respository]
创建一个空的版本库或重新初始化已经存在的版本库。一个git版本库就是带有objectsrefs/headsrefs/tags子目录以及模版文件的.git目录。同时也会创建指向当前分支的HEAD文件
重新初始化一个已经存在的版本库是安全的,因为git init不会重写已经存在的任何东西。重新运行git init的主要原因是:

git init命令还有一个比较常用的选项:--bare。当指定这个选项的时候,会创建一个裸仓库裸仓库没有工作目录(working directory),只保存修订版本,可以直接作为服务器仓库。比如:

[git@hadoop-100 git-repository]$ git init --bare timchow.git
Initialized empty Git repository in /home/git/git-repository/timchow.git/
[git@hadoop-100 git-repository]$ ls timchow.git/
branches  config  description  HEAD  hooks  info  objects  refs

当没有给git init命令指定路径的时候,缺省的路径是当前目录(.)。

2,git remote
git remote命令是用来管理远程仓库的:

git remote add origin git@192.168.100.100:/home/git/git-repository/timchow.git
[vagrant@hadoop-100 timchow]$ git remote set-url origin git@192.168.100.100:/home/git/git-repository/timchow.git
[vagrant@hadoop-100 timchow]$ git remote -v
origin  git@192.168.100.100:/home/git/git-repository/timchow.git (fetch)
origin  git@192.168.100.100:/home/git/git-repository/timchow.git (push)

3,git push
使用本地的引用更新远程仓库的引用,并且会发送相关的对象在git push的时候,也会在本地给 远程分支 建立 远程追踪分支)。git push的语法是:

git push [ [...]]

其中repository既可以是远程仓库的URL,也可以是远程仓库的名称。
在git中,分支(branches)标签(tags)对commit对象的引用,更多关于git对象的细节,将在git的底层存储小节中进行讲述。因此,我们可以使用git push将分支或标签推送到远程仓库上。
refspec参数的完整格式是:[+][[<src>]:]<dst>(中括号内的部分是可省的),它表示使用本地的src引用 更新 远程仓库上的dst引用。当省略src,而不省略冒号的时候,表示使用本地的空引用去更新远程仓库的某个引用,也就意味着删除远程仓库上的引用,比如:

# 将本地仓库的master分支推送到远程仓库origin的development分支
[vagrant@hadoop-100 timchow]$ git push origin master:development
Total 0 (delta 0), reused 0 (delta 0)
To git@192.168.100.100:/home/git/git-repository/timchow.git
 * [new branch]      master -> development

# 删除远程仓库origin上的development分支
[vagrant@hadoop-100 timchow]$ git push origin :development
To git@192.168.100.100:/home/git/git-repository/timchow.git
 - [deleted]         development

# 将本地仓库的master分支推送到远程仓库origin的test-tag标签
[vagrant@hadoop-100 timchow]$ git push origin master:refs/tags/test-tag
Total 0 (delta 0), reused 0 (delta 0)
To git@192.168.100.100:/home/git/git-repository/timchow.git
 * [new tag]         master -> test-tag

# 删除远程仓库origin的test-tag标签
[vagrant@hadoop-100 timchow]$ git push origin :refs/tags/test-tag
To git@192.168.100.100:/home/git/git-repository/timchow.git
 - [deleted]         test-tag

当同时省略src和冒号的时候,会将名称为dst的本地引用推送到远程仓库的同名引用,比如:

# 创建本地分支test-branch
[vagrant@hadoop-100 timchow]$ git branch test-branch

# 将本地分支test-branch推送到远程仓库origin的test-branch分支
[vagrant@hadoop-100 timchow]$ git push origin test-branch
Total 0 (delta 0), reused 0 (delta 0)
To git@192.168.100.100:/home/git/git-repository/timchow.git
 * [new branch]      test-branch -> test-branch

向远程仓库推送分支的时候,可以使用git push (-u|--set-upstream) <remote-repository> [<local-branch>:]<remote-branch>将本地分支和远程分支关联起来。以后,在<local-branch>分支上,执行不带参数的git push的时候,就会自动将<local-branch>推送到远程分支<remote-repository>/<remote-branch>。比如:

# 将本地分支test-branch推送到远程仓库origin的test-branch分支,并将本地分支和远程分支关联起来
[vagrant@hadoop-100 timchow]$ git push -u origin test-branch
Branch test-branch set up to track remote branch test-branch from origin.
Everything up-to-date

# 之后,可以直接执行git push,而不必指定远程版本库名 和 分支名
[vagrant@hadoop-100 timchow]$ git push
Everything up-to-date

在建立关联后,打开.git/config会发现类似的内容:

 12 [branch "test-branch"]
 13     remote = origin
 14     merge = refs/heads/test-branch

4,git clone <remote-repository> [<directory>]
将远程版本库克隆到新创建的目录中。如果没有指定<directory>参数,则使用远程版本库名去掉.git后缀作为目录名。git clone除了将远程版本库完整的镜像下来之外,还会做如下的事情:


git暂存区

在git中,博主认为最难理解的概念就是暂存区了。理解了暂存区以及后面的底层存储之后,git将不再神秘。至于为什么引入暂存区的概念,请大家自己探究。

暂存区也称为index或stage,它是介于工作目录(working direcotry) 和 git目录(git directory,也就是.git目录)之间的中间状态,很多git命令都涉及到暂存区的概念,比如git commitgit addgit diffgit status等。

git-staging-area.png

(图片说明:使用git checkout将版本库中的内容检出到工作目录)
(图片说明:使用git add将工作目录中的修改更新到暂存区)
(图片说明:使用git commit将暂存区中的内容提交到版本库)

暂存区保存的其实就是包含文件索引的目录树。该目录树中记录了文件名、文件的状态信息(时间戳、文件长度等),以及文件对应的blob对象的id。暂存区保存在.git/index文件中。git版本库的结构如下图所示:

git-index.jpg

其中,HEAD指向 当前所在的分支,而分支(和标签)则是指向commit对象的引用,接下来介绍git中的对象。


git的底层存储

值得说明的是:git底层存储的是文件快照,也就是整个文件的内容。而不是一个版本相对于另外一个版本的差分。
在git中有四类对象:blob,tree,commit,tag。git的对象存储在对象库中(位于.git/objects)。

1,blob对象
blob对象用来存储文件内容,需要特别强调的是:blob只保存文件的内容,而忽略到其他元数据,比如文件名、路径等。blob对象的id就是文件内容的sha1值。可以使用git hash-object <filename>命令计算文件内容的sha1值,假设文件内容的sha1值是95f8cd0ca82c7d33423d8fb9b52bf6995eec3757,那么对应的blob对象存储在.git/objects/95/f8cd0ca82c7d33423d8fb9b52bf6995eec3757文件中。当执行git add <filename>命令时,就会生成相应的blob对象(也会将blob对象的id更新到暂存区的文件索引中)。
可以使用git cat-file -t <SHA1值>,来查看对象的类型。
使用git cat-file -p <SHA1值>,来查看对象的内容。比如:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git cat-file -t 95f8cd0ca82c7d33423d8fb9b52bf6995eec3757
blob

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git cat-file -p 95f8cd0ca82c7d33423d8fb9b52bf6995eec3757
this is a
I am a new line

2,tree对象
每次提交的时候(也就是执行git commit)的时候,都会生成一个tree对象(当然也会生成一个commit对象),一个tree对象代表当次提交时的目录信息,其内容包含:

也就是说,从项目根目录开始,每个目录都对应一个tree对象,因为目录具有层级关系,所以tree对象也具有包含关系。比如:

# 获取当前分支所指向的commit对象
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git rev-parse HEAD
9836fb0b78ecd9006b6575c361fd4e3d7642d31d

# 查看commit对象的内容,commit对象中包含项目根目录所对应的tree对象
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git cat-file -p 9836fb0b78ecd9006b6575c361fd4e3d7642d31d
tree 7bcdfcafe040d3c35240445aea8cafeb8db7067c
parent c20fc0e94630718b35b3ef50964b6c8269876c21
author zhoujingjiang(周井江.海外事业部.海外技术中心)  1499880364 +0800
committer zhoujingjiang(周井江.海外事业部.海外技术中心)  1499880364 +0800

this is a message

# 查看项目根目录所对应的tree对象,可以看到其中包含 文件名以及文件对应的blob对象;子目录名以及子目录所对应的tree对象  
# 例子中的a,d是项目根目录下的文件;b是项目根目录下的子目录
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git cat-file -p 7bcdfcafe040d3c35240445aea8cafeb8db7067c
100644 blob 95f8cd0ca82c7d33423d8fb9b52bf6995eec3757    a
040000 tree 2aaf3de65932e828fbcdd5a09085f1277a4c0e81    b
100644 blob 41ea8d852675618eb71cc40abbff3b5a2cd53d4e    d

# 继续看子目录b所对应的tree对象包含什么
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git cat-file -p 2aaf3de65932e828fbcdd5a09085f1277a4c0e81
100644 blob 5e5e2c3d099404a4e6badbbf030b261faf4a9fa3    c
# 从命令的运行结果可以看到子目录b下,不再包含子目录了,并且有一个叫c的文件  

下面看一个tree对象包含tree对象的例子:

tree-object.png

3,commit对象
每次提交(也就是执行git commit)的时候,都会生成一个commit对象(同时也会生成一个tree对象)。commit对象中包含:

下面看一个例子:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git cat-file -p HEAD
tree 8e975d674b20c62f13ef9e3d469b038dd4f67ab7
parent 9836fb0b78ecd9006b6575c361fd4e3d7642d31d
author zhoujingjiang(周井江.海外事业部.海外技术中心)  1499882682 +0800
committer zhoujingjiang(周井江.海外事业部.海外技术中心)  1499882682 +0800

mmm

4,tag对象
tag对象比较好理解,其实就是给commit对象起一个更易读的名字。


分支

[branch "test"]
        remote = .
        merge = refs/heads/master
[branch "test2"]
        remote = .
        merge = refs/heads/master

分支合并

1,git merge

git merge用于分支合并,它用来把其他分支commit的内容合并到当前分支。下面用一个实例,来讲解:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop
$ git init git_merge_test.git
Initialized empty Git repository in C:/Users/zhoujingjiang.DS/Desktop/git_merge_test.git/.git/

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop
$ cd git_merge_test.git/

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ echo "line 1" >> git_merge_test.txt

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git add git_merge_test.txt

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git commit -m "this is first commit on master" git_merge_test.txt
[master (root-commit) 9377280] this is first commit on master
 1 file changed, 1 insertion(+)
 create mode 100644 git_merge_test.txt

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ echo "line 2" >> git_merge_test.txt

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git commit -m "this is second commit on master" git_merge_test.txt  
[master 458de11] this is second commit on master
 1 file changed, 1 insertion(+)  
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git branch test-git-merge-branch master

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git branch -a
* master
  test-git-merge-branch

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git checkout test-git-merge-branch
Switched to branch 'test-git-merge-branch'

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (test-git-merge-branch)
$ echo "line 3" >> git_merge_test.txt

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (test-git-merge-branch)
$ git commit -m "this is first commit on test-git-merge-branch" git_merge_test.txt  
[test-git-merge-branch 385845c] this is first commit on test-git-merge-branch
 1 file changed, 1 insertion(+)
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (test-git-merge-branch)
$ git checkout master
Switched to branch 'master'  

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ echo "line 4" >>git_merge_test.txt

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git commit -m "this is third commit on master" git_merge_test.txt  
[master 103b9bc] this is third commit on master
 1 file changed, 1 insertion(+)

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master)
$ git merge test-git-merge-branch
Auto-merging git_merge_test.txt
CONFLICT (content): Merge conflict in git_merge_test.txt
Automatic merge failed; fix conflicts and then commit the result.

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master|MERGING)
$ git status -s
UU git_merge_test.txt
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master|MERGING)
$ cat git_merge_test.txt
line 1
line 2
<<<<<<< HEAD
line 4
=======
line 3
>>>>>>> test-git-merge-branch

其中,“=======”分隔符上面的部分是HEAD分支(也就是当前所在的分支,master)的内容;下面的部分是test-git-merge-branch分支中的内容。之后人工解决冲突,比如:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master|MERGING)
$ cat git_merge_test.txt
line 1
line 2
line 4
line 3

使用git add将冲突标记为已解决

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master|MERGING)
$ git add git_merge_test.txt

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master|MERGING)
$ git status -s
M  git_merge_test.txt

然后,使用git commit进行提交:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/timchow (master|MERGING)
$ git commit -m "merge from test-git-merge-branch"
[master dc23c21] merge from test-git-merge-branch
git-log.jpg

git merge的过程中,git会进行一些特殊处理:找到两个分支的末端和它们的共同祖先,然后进行三方合并,之后对三方合并的结果做快照,并创建一个指向它的commit对象。

2,git rebase

大家可以先阅读一下这篇文档
首先,大家应该了解的是:在git push的时候,git会比较commit history,如果commit history不一致,git就会拒绝提交(一种比较危险的做法是:使用git push -f将远程版本库上的分支强制 更新为 本地版本库的!!!但是,这样会影响其他人的工作!!!)。
对于这种情况,可以使用git rebase来解决。仍然使用一个例子进行讲解:

[git@hadoop-100 git-repository]$ cd /home/git/git-repository/
[git@hadoop-100 git-repository]$ git init --bare test-rebase.git
Initialized empty Git repository in /home/git/git-repository/test-rebase.git/
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop
$ git clone git@192.168.100.100:/home/git/git-repository/test-rebase.git test-rebase-1
Cloning into 'test-rebase-1'...
warning: You appear to have cloned an empty repository.

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop
$ git clone git@192.168.100.100:/home/git/git-repository/test-rebase.git test-rebase-2
Cloning into 'test-rebase-2'...
warning: You appear to have cloned an empty repository.

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop
$ cd test-rebase-1/

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ echo "this is line 1" >> text

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ git add text text

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ git commit -m "first commit" text
[master (root-commit) 083c189] first commit
 1 file changed, 1 insertion(+)
 create mode 100644 text

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ git push -u origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 213 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To 192.168.100.100:/home/git/git-repository/test-rebase.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ echo "this is line 2" >> text

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ git commit -m "second commit on test-rebase-1" text
[master e6752d5] second commit on test-rebase-1
 1 file changed, 1 insertion(+)

# !!!commit之后没有git push哦!!!

进入到test-rebase-2,先更新master分支到最新状态,然后修改,提交,推送:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ cd ../test-rebase-2/

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-2 (master)
$ git pull origin master
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From 192.168.100.100:/home/git/git-repository/test-rebase
 * branch            master     -> FETCH_HEAD
 * [new branch]      master     -> origin/master

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-2 (master)
$ echo "this is line 3" >> text

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-2 (master)
$ git commit -m "first commit in test-rebase-2" text
[master 781fba4] first commit in test-rebase-2
 1 file changed, 1 insertion(+)

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-2 (master)
$ git push
Counting objects: 3, done.
Writing objects: 100% (3/3), 260 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To 192.168.100.100:/home/git/git-repository/test-rebase.git
   083c189..781fba4  master -> master

回到test-rebase-1,推送之前的提交到远程版本库:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-2 (master)
$ cd ../test-rebase-1/

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ git push
To 192.168.100.100:/home/git/git-repository/test-rebase.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'git@192.168.100.100:/home/git/git-repository/test-rebase.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.

可以发现,git push被拒绝了~

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ git fetch origin master
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From 192.168.100.100:/home/git/git-repository/test-rebase
 * branch            master     -> FETCH_HEAD
   083c189..781fba4  master     -> origin/master
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ git rebase origin/master
First, rewinding head to replay your work on top of it...
Applying: second commit on test-rebase-1
Using index info to reconstruct a base tree...
M       text
Falling back to patching base and 3-way merge...
Auto-merging text
CONFLICT (content): Merge conflict in text
error: Failed to merge in the changes.
Patch failed at 0001 second commit on test-rebase-1
The copy of the patch that failed is found in: .git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".


zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master|REBASE 1/1)
$ git status -s
UU text

可以发现,rebase的时候,发生冲突了,如果想要停止git rebase,可以使用git rebase --abort。此时,我就不abort了~

# 假设已经解决了冲突
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master|REBASE 1/1)
$ git add text
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master|REBASE 1/1)
$ git rebase  --continue
Applying: second commit on test-rebase-1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-rebase-1 (master)
$ git status -s
$ git log --graph text
* commit 41c87d8830491b22a25dd3587e9c347a88d91f82
| Author: timchow <744475502@qq.com>
| Date:   Mon Jul 17 18:18:12 2017 +0800
|
|     second commit on test-rebase-1
|
* commit 781fba44a2c7ef5524b0294fe31813003d514d6b
| Author: timchow <744475502@qq.com>
| Date:   Mon Jul 17 18:21:30 2017 +0800
|
|     first commit in test-rebase-2
|
* commit 083c1895481ba2eb2dd81dda6bf10f3b84f8206d
  Author: timchow <744475502@qq.com>
  Date:   Mon Jul 17 18:16:36 2017 +0800

      first commit

我们会发现,commit history是线性的!非常清晰。
git rebase <another>的原理是:

3,git cherry-pick

git cherry-pick可以选择一个分支的一个或多个commit进行合并;相比之下,git merge可以称为完全合并。比如,线上的分支是v1.0,正在开发的分支是v2.0,如果只想把v2.0的某几个特性合并到线上分支v1.0,那么就应该使用git cherry-pick,而不是git merge,因为git merge会将v2.0上所有的改动都合并到v1.0,进而导致版本混乱。
下面举一个例子:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop
$ git init test-cherry-pick
Initialized empty Git repository in C:/Users/zhoujingjiang.DS/Desktop/test-cherry-pick/.git/

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop
$ cd test-cherry-pick/

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (master)
$ git checkout -b v1.0
Switched to a new branch 'v1.0'

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ echo "this is feature1" >>feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ git add feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ git commit -m "this is feature1" feature1
[v1.0 (root-commit) 8aaa499] this is feature1
 1 file changed, 1 insertion(+)
 create mode 100644 feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ echo "this is feature2" >>feature2

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ git add feature2

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ git commit -m "this is feature2" feature2
[v1.0 465cfc7] this is feature2
 1 file changed, 1 insertion(+)
 create mode 100644 feature2

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ git branch v2.0 v1.0

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ git checkout v2.0
Switched to branch 'v2.0'

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ echo "new functions of feature1" >>feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git commit -m "new functions of feature1" feature1
[v2.0 1af5ca7] new functions of feature1
 1 file changed, 1 insertion(+)

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ echo "this is feature3" >>feature3

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git add feature3

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git commit -m "this is feature3" feature3
[v2.0 52e94e9] this is feature3
 1 file changed, 1 insertion(+)
 create mode 100644 feature3

现在,只想把v2.0中对feature1的修改,合并到v1.0。并不想将feature3合并到v1.0。

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git log --pretty=oneline
52e94e9ad708f3ef65858e804b3ba3a7afc78852 this is feature3
1af5ca75fb61c46fb6997c9e7a54771edc322c41 new functions of feature1
465cfc705bef3d0741ef1142f26784f28d5bc92b this is feature2
8aaa499bcb8ab8f49a6784023c9afb20a6eb4303 this is feature1

要合并到v1.0的commit id是:1af5ca75fb61c46fb6997c9e7a54771edc322c41

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ git cherry-pick 1af5ca75fb61c46fb6997c9e7a54771edc322c41
[v1.0 9286ca6] new functions of feature1
 Date: Tue Jul 18 14:09:34 2017 +0800
 1 file changed, 1 insertion(+)

当然在进行git cherry-pick的时候,仍然可能出现冲突。解决方法和git merge一样。


回滚

1,git revert

通过生成新的提交来撤销某些已经存在的提交。git revert不会改变commit history(这很重要,因为git push的时候,git会比较commit history,如果commit history不一致,就会导致push被拒绝)。
下面继续使用 学习git cherry-pick时 的版本库,进行举例:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v1.0)
$ git checkout v2.0
Switched to branch 'v2.0'

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git log --pretty=oneline
52e94e9ad708f3ef65858e804b3ba3a7afc78852 this is feature3
1af5ca75fb61c46fb6997c9e7a54771edc322c41 new functions of feature1
465cfc705bef3d0741ef1142f26784f28d5bc92b this is feature2
8aaa499bcb8ab8f49a6784023c9afb20a6eb4303 this is feature1

假设想要撤销的commit是1af5ca75fb61c46fb6997c9e7a54771edc322c41

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ sed -i 's/\(^new.*$\)/\1 with a suffix/g' feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git commit -m "add a suffix" feature1
[v2.0 8e42832] add a suffix
 1 file changed, 1 insertion(+), 1 deletion(-)
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git revert 1af5ca75fb61c46fb6997c9e7a54771edc322c41
error: could not revert 1af5ca7... new functions of feature1
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add ' or 'git rm '
hint: and commit the result with 'git commit'

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0|REVERTING)
$ git status -s
UU feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0|REVERTING)
$ vim feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0|REVERTING)
$ cat feature1
this is feature1
with a suffix

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0|REVERTING)
$ git add feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0|REVERTING)
$ git revert --continue
[v2.0 69e509e] Revert "new functions of feature1"
 1 file changed, 1 insertion(+), 1 deletion(-)

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git log --graph feature1
* commit 69e509ed1e4cb4e89707d71afaef34359a3dca17
| Author: timchow <744475502@qq.com>
| Date:   Tue Jul 18 15:52:16 2017 +0800
|
|     Revert "new functions of feature1"
|
|     This reverts commit 1af5ca75fb61c46fb6997c9e7a54771edc322c41.
|
* commit 8e42832569f63bde1a522c755a3d3c238bfa669d
| Author: timchow <744475502@qq.com>
| Date:   Tue Jul 18 15:44:43 2017 +0800
|
|     add a suffix
|
* commit 1af5ca75fb61c46fb6997c9e7a54771edc322c41
| Author: timchow <744475502@qq.com>
| Date:   Tue Jul 18 14:09:34 2017 +0800
|
|     new functions of feature1
|
* commit 8aaa499bcb8ab8f49a6784023c9afb20a6eb4303
  Author: timchow <744475502@qq.com>
  Date:   Tue Jul 18 14:05:14 2017 +0800

      this is feature1

通过git log看到,git revert并没有修改commit history,而是产生了一个新的commit。

2,git reset [<options>...] <commit>

我们知道,分支和标签其实就是指向commit对象的引用,可以通过git reset重置(reset)当前分支所指向的commit。
git reset有两个重要的选项:--soft(默认)、--hard

仍然以上面的版本库作为例子:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git log --pretty=oneline
69e509ed1e4cb4e89707d71afaef34359a3dca17 Revert "new functions of feature1"
8e42832569f63bde1a522c755a3d3c238bfa669d add a suffix
52e94e9ad708f3ef65858e804b3ba3a7afc78852 this is feature3
1af5ca75fb61c46fb6997c9e7a54771edc322c41 new functions of feature1
465cfc705bef3d0741ef1142f26784f28d5bc92b this is feature2
8aaa499bcb8ab8f49a6784023c9afb20a6eb4303 this is feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git reset --hard Head~1
HEAD is now at 8e42832 add a suffix

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git log --pretty=oneline
8e42832569f63bde1a522c755a3d3c238bfa669d add a suffix
52e94e9ad708f3ef65858e804b3ba3a7afc78852 this is feature3
1af5ca75fb61c46fb6997c9e7a54771edc322c41 new functions of feature1
465cfc705bef3d0741ef1142f26784f28d5bc92b this is feature2
8aaa499bcb8ab8f49a6784023c9afb20a6eb4303 this is feature1

可以发现git reset也引起了commit history的改变,所以在git push的时候,可能被拒绝。

3,git stash

许多git命令都要求工作目录是“干净”的,比如git revertgit merge等。如果在执行这类命令的时候,工作目录上已经有改动,但是既不想丢弃,也不想提交这些改动,那么就可以使用git stash将这些未提交的改动“暂存”起来(存在.git/refs/stash),并且可以使用git stash多次保存工作进度;还可以通过git stash list显示进度列表;使用git stash pop [<stash>]恢复工作进度;使用git stash drop [<stash>]删除工作进度。
仍然使用上面的版本库作为例子:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ ls
feature1  feature2  feature3

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ echo "another line of feature1" >>feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git stash
Saved working directory and index state WIP on v2.0: 8e42832 add a suffix
HEAD is now at 8e42832 add a suffix

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ echo "another line of feature2" >>feature2

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git stash
Saved working directory and index state WIP on v2.0: 8e42832 add a suffix
HEAD is now at 8e42832 add a suffix

# 最新的工作进度在上面
zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git stash list
stash@{0}: WIP on v2.0: 8e42832 add a suffix
stash@{1}: WIP on v2.0: 8e42832 add a suffix

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ echo "another line of feature3" >>feature3

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git stash
Saved working directory and index state WIP on v2.0: 8e42832 add a suffix
HEAD is now at 8e42832 add a suffix

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git stash list
stash@{0}: WIP on v2.0: 8e42832 add a suffix
stash@{1}: WIP on v2.0: 8e42832 add a suffix
stash@{2}: WIP on v2.0: 8e42832 add a suffix

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git stash drop stash@{0}
Dropped stash@{0} (d9f7dcdeb9c328f5c326fa74f5621d18ee019942)

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git stash pop stash@{1}
On branch v2.0
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

        modified:   feature1

no changes added to commit (use "git add" and/or "git commit -a")
Dropped stash@{1} (c24e5aab913dfeebdb7cf70bc7774b9b05eb88f1)

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git stash pop
On branch v2.0
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

        modified:   feature1
        modified:   feature2

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (ea754d575e063a9fa1db3bfe66ee986bd14a8cac)
4,git checkout

git checkout的作用是将版本库中特定的修订版本检出到工作目录(切换分支),也可以利用它撤销未提交的修改

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git status -s
 M feature1
 M feature2

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git checkout feature1

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git status -s
 M feature2

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git checkout .

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git status -s

归档

对于java项目,部署到线上的是打好的jar包或war包。但是对于python、php等项目,部署到线上的可能直接就是源代码。此时,我们是不希望 代码目录中包含跟版本库相关的文件 的(比如svn中的.svn目录、git中的.git目录)。对于svn,可以使用svn export命令。对与git则可以使用git archive命令。

git archive [--format=(tar|zip)] [-o <outputfile> | --output <outputfile>] <tree-ish> [<path>...]

比如:

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ ls
feature1  feature2  feature3  myproject.zip

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ git archive --format=zip -o myproject.zip HEAD

zhoujingjiang@ZHOUJINGJIANG MINGW64 ~/Desktop/test-cherry-pick (v2.0)
$ ls
feature1  feature2  feature3  myproject.zip

一点说明

个人建议:


参考文档