传送
一、基本概念
git是一种分布式版本控制系统。而与之相对就是集中式版本控制系统。集中式版本控制系统的代表是CVS和Subversion,其版本库集中存放在中央服务器上,干活的时候,要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。每个开发者之间无法进行版本的传递。所以集中式版本控制系统最大的毛病就是必须联网才能工作。
而分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上,所以可以在本地进行提交。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?对于分布式版本库系统,每个开发者之间是可以直接进行版本的传递的,比方说你在自己电脑上改了文件A,你的同事也在他的电脑上改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。但是在实际开发中,为了方便版本的传递(交换修改),通常也会有一个中央服务,这个中央服务器是一个裸版本库,仅仅是为了方便版本传递,没有这个中央服务器也能正常工作,只是没有这么方便而已。
二、git入门
① 初始化,创建版本库
要想使用git,那么我们需要创建一个版本库,用于存储该项目本身及其历史。为此,我们需要创建一个项目根目录,如first-steps,然后进入到项目根目录下,然后在该项目根目录下执行git init
,init命令会在当前目录下创建一个名为.git的隐藏目录,并在其中创建一个版本库,这个带版本库的项目目录,我们通常称为工作区。
> cd /path/to/first-steps
> git init
Initialized empty Git repository in /path/to/first-steps/.git/
② 首次提交
版本库创建好之后,就可以进行提交了,提交分为两步,第一,先使用add命令将需要提交的文件添加到暂存区;第二,然后使用commit命令将暂存区中的内容提交到版本库中。提交完成后,git会给该提交赋予一个散列值,如下面的c12ac1c,用于标识这次新提交。
> git add foo.txt bar.txt
> git commit --message "Sample project imported."
[master (root-commit) c12ac1c] Sample project imported.
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 bar.txt
create mode 100644 foo.txt
③ 检查状态
现在我们可以修改一下foo.txt内容并且删除bar.txt,然后添加一个index.html文件,然后通过git status
命令,可以查看到该项目从上次提交以来所有发生的修改。
> 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: bar.txt
modified: foo.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
index.html
no changes added to commit (use "git add" and/or "git commit -a")
④ 提交修改
工作区有了修改后,同样也是需要使用add和commit进行提交。
> git add foo.txt index.html
> git rm bar.txt
rm 'bar.txt'
> git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
deleted: bar.txt
modified: foo.txt
new file: index.html
> git commit --message "Some changes"
[master f1c648a] Some changes
3 files changed, 13 insertions(+)
delete mode 100644 bar.txt
create mode 100644 index.html
可以看到,新的提交也有一个对应的散列值(f1c648a),这里使用到了git rm
命令,其与rm命令的区别就是,rm仅仅是删除工作区的文件,并没有从暂存区中删除;而git rm 是删除工作区的文件同时将修改添加到暂存区中。 所以,git rm bar.txt
等价于 rm bar.txt
和 git add bar.txt
,就是多了一个将工作区修改添加到暂存区的操作。
⑤ 显示提交历史
我们可以通过git log
来查看项目的提交历史,所有的提交都会按时间顺序被降序排列出来。
> git log
commit f1c648a033ad509f5ffe08f96152432be1c12c7b (**HEAD ->** **master**)
Author:
Date: Tue Dec 31 11:01:40 2019 +0800
Some changes
commit c12ac1ced34a57104f29f4bceea13f88e2c3cbcf
Author:
Date: Tue Dec 31 10:32:32 2019 +0800
Sample project imported.
log命令还可以查看某个文件的所有提交记录,即git log <file>
,如:
// 查看所有对bar.txt修改过的提交版本
> git log bar.txt
log命令除了可以查看提交记录外,还可以查看每次提交的diff,如:
// 查看bar.txt文件每次修改的diff
> git log -p bar.txt
⑥ 显示操作历史
我们可以通过git reflog
命令来查看git的操作历史,操作历史中包含了提交历史,所以即使以后我们通过操作分支改变了分支的head指针位置,那么我们也可以通过操作历史找到当前head之后的版本,从而恢复重置head指针前的版本。
// 查看操作历史,可以找到曾经提交的任何一个版本
> git reflog
⑦ 查看某个版本的修改
要查看某个版本改了什么内容,那么我们可以通过git show <commit-id> [<file>]
,查看指定的commit-id修改的内容,如果传入了指定的文件名,那么只显示当前commit-id对指定文件的修改信息。
// 查看7a2d6f这次提交的所有修改内容
> git show 7a2d6f
// 查看7a2d6f这次提交中对foo.txt的修改内容
> git show 7a2d6f foo.txt
git show file,可以查看指定文件的最后一次修改,如:
// 查看foo.txt文件的最后一次修改
> git show foo.txt
三、git的协作功能
① 克隆版本库
git提供了一个clone命令,可以将某个版本库克隆到指定的目录下面。其用法为git clone 源版本库地址 目标版本库所在目录
,可以在任何目录下执行clone命令,如果没有指定目标版本库所在的目录,那么是直接将版本库克隆到当前目录下面,并且是将.git目录和工作区内容及其所在的目录都克隆过去,即整个项目克隆过去,但是如果指定了目标版本库所在的目录,那么克隆的仅仅是.git目录和工作区内容,即以目标版本库所在目录作为项目根目录。
> mkdir /path/to/first-steps-clone
# 指定了目标版本库位置,只会克隆源版本库中的.git目录和已被提交到版本库中的文件(如果有)
> git clone /path/to/first-steps /path/to/first-steps-clone
Cloning into first-steps-clone...
done.
> mkdir /path/to/first-steps-clone
# #未指定了目标版本库位置,会将整个项目克隆过去
> git clone /path/to/first-steps
Cloning into first-steps...
done.
需要注意的是,clone的时候,只有已经提交到版本库中的文件才会跟着版本库一起复制过来。
② 从源版本库中取回修改
现在我们分别在first-steps 和 first-steps-clone中进行一次提交,如:
// first-steps修改foo.txt的内容
> cd /path/to/first-steps
> git add foo.txt
> git commit --message 'A change in the original'
// first-steps-clone修改index.html的内容
> cd /path/to/first-steps-clone
> git add index.html
> git commit --message 'A change in the clone'
现在两个版本库中都作了相应的修改,就像两个开发者分别在开发,这个时候就需要同步对方的修改,对于clone版本库(first-steps-clone)而言,因为克隆的时候,源版本库的路径已经存储在了克隆版本库中,所以可以直接用git pull
命令取回源版本库的修改。
这里需要注意的是,而执行git pull
命令之后,获取到了修改foo.txt的提交,与当前修改index.html的提交相当于出现了分叉(虽然是同一个分支上的修改,但是来自于两个不同的开发者),所以需要把源版本库中对foo.txt的修改和克隆版本库中对index.html的修改进行一次合并merge。因为同一个分支中不能出现分叉,必须将其合并成一个,git pull
命令获取到源版本库的修改后会自动发起merge合并的操作。
> git log --graph
* commit 3f78c7f4b0c6c3bd9e8ee5b15d061cb9018c9f44 (**HEAD ->** **master**)
|\ Merge: f324964 b40f4a0
| | Author:
| | Date: Tue Dec 31 12:43:05 2019 +0800
| |
| | Merge branch 'master' of /Users/**/Downloads/first-steps
| |
| * commit b40f4a0d0e4344f49afaad609659046014031fbc (**origin/master**, **origin/HEAD**)
| | Author:
| | Date: Tue Dec 31 12:25:34 2019 +0800
| |
| | A change in the original
| |
* | commit f324964540fbc031fbbea42d3fc853c5722a4236
|/ Author:
| Date: Tue Dec 31 12:42:55 2019 +0800
|
| A change in the clone
|
* commit f1c648a033ad509f5ffe08f96152432be1c12c7b
| Author:
| Date: Tue Dec 31 11:01:40 2019 +0800
|
| Some changes
|
* commit c12ac1ced34a57104f29f4bceea13f88e2c3cbcf
Author:
Date: Tue Dec 31 10:32:32 2019 +0800
Sample project imported.
可以看到,分支出现了分叉,然后又收到merge合并为了一个分支。总共有5次提交,最初的2次提交+克隆版本库的提交+源版本库的提交+手动merge的提交。
③ 从任意版本库中取回修改git pull
在没有参数的情况,只能在克隆版本库中执行,去取回源版本库的修改。而源版本库中执行git pull是无法从克隆版本库获取到克隆版本库的修改的。
> cd /path/to/first-steps
> git pull
fatal: No remote repository specified. Please, specify either a URL or a
remote name from which new revisions should be fetched.
但是可以通过指定版本库路径和分支名的方式来从任何一个版本库中取回修改,即git pull 版本库路径 分支名
,如:
> cd /path/to/first-steps
> git pull /path/to/first-steps-clone master
四、创建共享版本库
① 克隆裸版本库
所谓裸版本库,就是一个不带工作区的版本库,仅仅用于开发者之间传递提交的一个汇聚点,可以通过git clone --bare来克隆一个裸的版本库。
> git clone --bare /path/to/first-steps /path/to/first-steps-bare.git
> Cloning into bare repository first-steps-bare.git...
done.
还可以直接在init的时候创建一个裸的版本库,如:
> git init --bare
Initialized empty Git repository in /Users/banma-798/Downloads/bare-repo/
加上--bare后,将不会出现.git目录了,而是直接将.git目录中的内容放到了当前目录下。
如果我们克隆的源版本库是由git init --bare
初始化而来的,那么源版本库中还没有任何分支,所以当我们克隆下来准备提交的时候会提示,上游分支不存在(the upstream is gone),如:
> git status
Your branch is based on 'origin/master', but the upstream is gone.
(use "git branch --unset-upstream" to fixup)
// 我们可以先取消上游分支的关联
> git branch --unset-upstream
// 设置上游分支并且将本地提交推送到远程分支,即将本地的当前分支提交推送到远程的origin版本库中的master分支上
> git push --set-upstream origin master
也可以直接执行git push --set-upstream origin master,将本地master分支内容推送到远程的origin的master分支上
② push推送修改到裸版本库
用法为git push 裸版本库路径 分支
,为什么必须要创建一个裸版本库呢?因为push的时候,如果克隆分支和源版本库处于同一分支的时候,那么默认会被拒绝。
// 初始化一个普通的版本库(带work tree)
> cd /path/to/normal-repo
> git init
// 从普通的版本库中克隆
> cd /path/to/normal-clone
> git clone /path/to/normal-repo .
> git push
// 提示源仓库也处于master分支上,并且源仓库的receive.denyCurrentBranch变量的值为refuse所以会拒绝
remote: 'receive.denyCurrentBranch' configuration variable to 'refuse'.
To /path/to/normal-repo
! [remote rejected] master -> master (branch is currently checked out)
解决方法:
① 修改源仓库的配置文件,即.git/config中的内容,加上如下代码:
[receive]
denyCurrentBranch = ignore
表示忽略同分支push操作。
② 将源仓库的分支切换到其他分支上,这样clone分支和源分支就不在同一个分支上了,就可以进行push操作了
// 进入到源仓库目录下
> cd /path/to/normal-repo
// 创建并切换到bug分支上
> git checkout -b bug
// 回到clone仓库中
> cd /path/to/normal-clone
// 再次push
> git push
所以我们最好创建一个裸的版本库,这样源版本库中就不会在任何一个分支上了,因为源版本库中只有版本库没有work tree,所以无所谓工作区的操作,所以不可能处在哪个分支下。但是某个版本库去克隆了该裸版本库后,那么默认是处于master分支上的。
这里需要注意的是,如果另一个开发者在我们之前已经做过一次push操作,那么我们再次执行push命令就会被拒绝推送,这个时候,我们必须先把其他人的提交pull过来然后才能接着push。
对于克隆版本库而言,执行push和pull命令的时候,可以直接用origin代表源版本库,如:
> git push origin master
> git pull origin master
所以可以看到,如果自己的版本库比目标版本库的要落后,那么是无法push推送自己的修改到目标版本库的,所以在实际开发中,如果我们提交了一个版本,但是突然发现该次提交存在重大bug,我们无法通过在本地直接回退到上一个版本然后在push推送到目标版本库的方式去取消上次提交,因为一旦回退版本,那么你的版本库就比目标版本库落后了,所以push推送失败。当然,我们可以通过带上-f参数来强制提交上去,但是不建议这么做。
// 强制推送修改到目标版本库
> git push -f origin master
③ fetch操作
当我们克隆远程仓库到本地后,本地仓库中会有一个和远程仓库同名的分支,比如本地仓库的master分支和远程仓库remotes/origin/master,这个remotes/origin/master分支就是执行克隆操作的时候对远程仓库的一个副本,可以与远程仓库进行版本的交换。当我们在本地的master分支上执行git merge操作的时候,其实是和remotes/origin/master分支进行merge,即等价于git merge origin/master。
// 此时还未与远程仓库分支关联,本地仓库只有master分支
> git branch -a
* master
// 从远程仓库分支拉取最新的代码到本地
> git fetch
warning: no common commits
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /path/to/bare-repo
* [new branch] master -> origin/master
// 远程仓库最新的版本会被下载到远程仓库的副本remotes/origin/master分支上
> git branch -a
* master
remotes/origin/master
// 执行merge操作会将remotes/origin/master分支上的更新合并到本地master分支上
> git merge --allow-unrelated-histories
// 等价于 git merge origin/master
当我们从远程仓库fetch拉取最新提交后,如果进行合并操作的时候,我们的本地版本中仍然有正在修改的文件,那么如果fetch过来的最新提交中如果有对同一个文件的修改,即合并会导致文件冲突,那么将会导致merge失败;如:
// 本地版本库修改1.txt文件
> vi 1.txt
// 拉取远程版本库中的最新修改,其中有对1.txt文件的修改
> git fetch
// 执行merge操作,提示1.txt文件将会被重写,merge失败
> git merge
Updating 4b4b9ee..94ab502
error: Your local changes to the following files would be overwritten by merge:
1.txt
Please commit your changes or stash them before you merge.
Aborting
// 需要先将本地的修改保存的储藏区中
> git stash
// 再次进行merge操作
> git merge
// 接下来解决冲突即可
> git stash pop
④ 用远程分支覆盖本地分支
当本地分支修改的面目全非了,那么我们可以直接丢弃本地的提交,直接同步远程分支,这里的同步不是用git pull
去将远程分支的内容抓取过来并与本地分支合并,而是直接用远程分支的内容覆盖掉本地分支,让本地分支和远程分支保持一致,主要是通过git fetch --all
将远程分支的内容抓取下拉,但不合并,然后将本地分支指针重置到 origin/分支名处,如:
// 将远程分支内容全部抓取下来,但是不进行合并操作
> git fetch --all
Fetching origin
// 将本地分支指针重置到origin/master处
> git reset --hard origin/master
HEAD is now at ab1ce21 commit-3
// 查看是否与远程分支一样
> git log
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。