基本概念
工作区(working Directory)就是你在电脑里能看到的目录
版本库(repository) 工作区有一个隐藏目录.git,这个不算工作区,是版本库
暂存区 Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区
master 还有Git为我们自动创建的第一个分支master
HEAD 指向master的一个指针叫HEAD
我们把文件往Git版本库里添加的时候,是分两步执行的:
第一步是用git add
把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit
提交更改,实际上就是把暂存区的所有内容提交到当前分支。因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit
就是往master分支上提交更改。可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改
安装git
mac上
-
安装brew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
安装git
➜ ~ brew install git
#若出现下面的情况表示安装成功
➜ ~ git --version
git version 2.10.1
ubuntu上
root@fan:~# sudo apt-get install git
sudo: 无法解析主机:fan
正在读取软件包列表... 完成
正在分析软件包的依赖关系树
正在读取状态信息... 完成
git 已经是最新版 (1:2.7.4-0ubuntu1)。
升级了 0 个软件包,新安装了 0 个软件包,要卸载 0 个软件包,有 0 个软件包未被升级。
安装成功后还需要配置git
$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"
#配置别名
git config --global alias.st status #查看状态
git config --global alias.co checkout
git config --global alias.ci commit #提交
git config --global alias.br branch #分支
git config --global alias.unstage 'reset HEAD --'
git config --global alias.last 'log -1 HEAD'
git config --global alias.clog "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
git config --global alias.llog "log -graph --pretty=oneline --abbrev-commit"
还可以设置编辑器
git config --global core.editor vim
创建本地版本库
创建裸仓库
git --base init
sh-3.2# pwd
/Applications/XAMPP/htdocs
sh-3.2# mkdir learngit
sh-3.2# cd learngit/
sh-3.2# #初始化目录为git可以管理的仓库
sh-3.2# git init
Initialized empty Git repository in /Applications/XAMPP/xamppfiles/htdocs/learngit/.git/
查看当前目录下会多了一个.git目录
sh-3.2# ls -ah
. .. .git
在这时最好先建好.gitignore 文件然后在commit,或者克隆远程仓库前,确保远程仓库.gitignore文件正确
远程仓库的管理和使用
克隆远程仓库
一次 Git 克隆会建立你自己的本地分支 master 和远程分支 origin/master,它们都指向 origin/master 分支的最后一次提交
克隆操作会默认使用的 master作为分支名 和 origin远程库名
git clone [url]
或者 git clone [url] + 要克隆到哪个目录
$ git clone git://github.com/schacon/grit.git
$ git clone git://github.com/schacon/grit.git mygrit
添加远程仓库
git remote add [shortname] [url]
添加新的远程仓库-
git remote
查看当前配置中每个远程库的简短名称git remote -v
显示克隆地址
git pull
命令自动抓取数据下来,然后将远端分支自动合并到本地仓库中当前分支git fetch [shortname]
要抓取所有远程仓库有的,但本地仓库的远程分支没有的信息。如果是克隆了一个仓库,此命令会抓取从你上次克隆以来别人上传到此远程仓库中的所有更新。
fetch 命令只是将远端的数据拉到本地仓库的远程分支,并不自动合并到当前工作分支,只有当你确实准备好了,才能手工合并
$ git fetch origin
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 15 (delta 5), reused 0 (delta 0)
Unpacking objects: 100% (15/15), done.
From git@github.com:schacon/simplegit
* [new branch] serverfix -> origin/serverfix
在 fetch 操作下载好新的远程分支之后,你仍然无法在本地编辑该远程仓库中的分支。换句话说,在本例中,你不会有一个新的serverfix 分支,有的只是一个你无法移动的 origin/serverfix 指针
如果要把该内容合并到当前分支,可以运行 git merge origin/serverfix
。如果想要一份自己的 serverfix 来开发,可以在远程分支的基础上分化出一个新的分支来:
$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"
这会切换到新建的 serverfix 本地分支,其内容同远程分支 origin/serverfix 一致,这样你就可以在里面继续开发了。
举例
root@fan:/home/wwwroot/git# git init
初始化空的 Git 仓库于 /home/wwwroot/git/.git/
root@fan:/home/wwwroot/git# git status
位于分支 master
初始提交
无文件要提交(创建/拷贝文件并使用 "git add" 建立跟踪)
root@fan:/home/wwwroot/git# git remote add demo git@git.coding.net:fan_code/ceshi2.git
root@fan:/home/wwwroot/git# git remote
demo
root@fan:/home/wwwroot/git# git remote -v
demo git@git.coding.net:fan_code/ceshi2.git (fetch)
demo git@git.coding.net:fan_code/ceshi2.git (push)
root@fan:/home/wwwroot/git# git fetch demo
remote: Counting objects: 7993, done.
remote: Compressing objects: 100% (4985/4985), done.
remote: Total 7993 (delta 2470), reused 7993 (delta 2470)
接收对象中: 100% (7993/7993), 7.59 MiB | 337.00 KiB/s, 完成.
处理 delta 中: 100% (2470/2470), 完成.
来自 git.coding.net:fan_code/ceshi2
* [新分支] master -> demo/master
现在,远程仓库[shortname]
的主干分支(master)已经完全可以在本地访问了,对应的名字是 demo/master,你可以将它合并到自己的某个分支,或者切换到这个分支,看看有些什么有趣的更新。
修改远程仓库地址
Git 仓库地址修改办法 git remote set-url origin [NEW_URL]
推送数据到远程仓库
git push [remote-name] [branch-name]
例如:把本地的 master 分支推送到origin 服务器上
$ git push origin master
把本地的 master 分支推送到 origin 上并改名为 test
$ git push origin master:test
查看远程仓库信息
git remote show [remote-name]
查看某个远程仓库的详细信息
举例
$ git remote show origin
* remote origin
URL: git@github.com:defunkt/github.git
Remote branch merged with 'git pull' while on branch issues
issues
Remote branch merged with 'git pull' while on branch master
master
New remote branches (next fetch will store in remotes/origin)
caching
Stale tracking branches (use 'git remote prune')
libwalker
walker2
Tracked remote branches
acl
apiv2
dashboard2
issues
master
postgres
Local branch pushed with 'git push'
master:master
它告诉我们,运行 git push
时缺省推送的分支是什么(译注:最后两行)。它还显示了有哪些远端分支还没有同步到本地(译注:第六行的caching 分支),哪些已同步到本地的远端分支在远端服务器上已被删除(译注:Stale tracking branches 下面的两个分支),以及运行git pull 时将自动合并哪些分支(译注:前四行中列出的 issues 和 master 分支)。
远程仓库的移除和重命名
git remote rename 原名 新名
git remote rm 远程仓库别名
碰到远端仓库服务器迁移,或者原来的克隆镜像不再使用,又或者某个参与者不再贡献代码,那么需要移除对应的远端仓库,可以运行
文件管理
将已被git跟踪的文件移除跟踪
另外一种情况是,我们想把文件从Git仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,仅是从跟踪清单中删除。比如一些大型日志文件或者一堆.a 编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在 .gitignore 文件中补上,用 --cached 选项即可:
$ git rm --cached readme.txt
删除文件
后面可以列出文件或者目录的名字,也可以使用 glob 模式。比方说:
$ git rm log/\*.log
注意到星号*之前的反斜杠,因为Git有它自己的文件模式扩展匹配方式,所以我们不用shell来帮忙展开(译注:实际上不加反斜杠也可以运行,只不过按照shell扩展的话,仅仅删除指定目录下的文件而不会递归匹配。上面的例子本来就指定了目录,所以效果等同,但下面的例子就会用递归方式匹配,所以必须加反斜 杠。)。此命令删除所有log/ 目录下扩展名为 .log 的文件。类似的比如:
$ git rm \*~
会递归删除当前目录及其子目录中所有 ~ 结尾的文件。
移动文件
不像其他的VCS系统,Git并不跟踪文件移动操作。如果在Git中重命名了某个文件,仓库中存储的元数据并不会体现出这是一次改名操作。不过Git非常聪明,它会推断出究竟发生了什么,至于具体是如何做到的,我们稍后再谈。
既然如此,当你看到 Git 的 mv 命令时一定会困惑不已。要在 Git 中对文件改名,可以这么做:
$ git mv file_from file_to
它会恰如预期般正常工作。实际上,即便此时查看状态信息,也会明白无误地看到关于重命名操作的说明:
$ git mv README.txt README
$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
# Changes to be committed:
# (use "git reset HEAD
..." to unstage)
#
# renamed: README.txt -> README
#
其实,运行 git mv 就相当于运行了下面三条命令:
$ mv README.txt README
$ git rm README.txt
$ git add README
如此分开操作,Git也会意识到这是一次改名,所以不管何种方式都一样。当然,直接用 git mv
轻便得多,不过有时候用其他工具批处理改名的话,要记得在提交前删除老的文件名,再添加新的文件名。
文件对比
git diff
比较工作区和暂存区文件差异git diff --staged/--cached
比较暂存区和版本库文件差异git diff HEAD 可以查看工作区和版本库里面最新版本的区别
-
git diff HEAD -- 文件名 可以查看指定文件工作区和版本库里面最新版本的区别
举例
root@fan:/home/wwwroot/Demo# vim t2 root@fan:/home/wwwroot/Demo# git diff diff --git a/t2 b/t2 index 9bc7ad0..2672a75 100644 --- a/t2 +++ b/t2 @@ -1 +1,2 @@ t2 +t2 root@fan:/home/wwwroot/Demo# git add t2 root@fan:/home/wwwroot/Demo# git diff root@fan:/home/wwwroot/Demo# git diff --staged diff --git a/t2 b/t2 index 9bc7ad0..2672a75 100644 --- a/t2 +++ b/t2 @@ -1 +1,2 @@ t2 +t2 root@fan:/home/wwwroot/Demo# git commit -m '+t2' [master 8a74487] +t2 1 file changed, 1 insertion(+) root@fan:/home/wwwroot/Demo# git status 位于分支 master 您的分支领先 'origin/master' 共 5 个提交。 (使用 "git push" 来发布您的本地提交) 无文件要提交,干净的工作区 root@fan:/home/wwwroot/Demo# git diff root@fan:/home/wwwroot/Demo# git diff --staged
提交历史
查看
git log
有许多选项可以帮助你搜寻感兴趣的提交,接下来我们介绍些最常用的。
我们常用 -p
选项展开显示每次提交的内容差异,
-2
则仅显示最近的两次更新:
$ git log -p -2
--stat,仅显示简要的增改行数统计
$ git log --stat
-
--pretty
选项可以指定使用完全不同于默认格式的方式展示提交历史。比如用oneline将每个提交放在一行显示,这在提交数很大时非常有用**
$ git log --pretty=oneline
edde8f5624c75017e81004b221d2a70e5ed31092 update README.md
75cc8c7ad7cf14f6eaded270c6b472b0b18accbd Initial commit
-
还可以用
oneline
结合--graph
选项显示ASCII图形表示的分支合并历史,形象地展示了每个提交所在的分支及其分化衍合情况, `--abbrev-commit` 仅显示sha-1前几个字符,而非所有字符**
$ git log --graph --pretty=oneline --abbrev-commit
* 6be44b4 merge dev
|\
| * d081be9 I am dev
* | c65bc40 I am master
|/
* aaa7a54 初始化
-
format
可以定制要显示的记录格式,这样的输出便于后期编程提取分析
$ git log --pretty=format:"%h - %an, %ar : %s"
ca82a6d - Scott Chacon, 11 months ago : changed the version number
085bb3b - Scott Chacon, 11 months ago : removed unnecessary test code
a11bef0 - Scott Chacon, 11 months ago : first commit
格式占位符说明
选项 说明
%H 提交对象(commit)的完整哈希字串
%h 提交对象的简短哈希字串
%T 树对象(tree)的完整哈希字串
%t 树对象的简短哈希字串
%P 父对象(parent)的完整哈希字串
%p 父对象的简短哈希字串
%an 作者(author)的名字
%ae 作者的电子邮件地址
%ad 作者修订日期(可以用 -date= 选项定制格式)
%ar 作者修订日期,按多久以前的方式显示
%cn 提交者(committer)的名字
%ce 提交者的电子邮件地址
%cd 提交日期
%cr 提交日期,按多久以前的方式显示
%s 提交说明
git log
支持的其他命令选项
选项 说明
-p 按补丁格式显示每个更新之间的差异。
--stat 显示每次更新的文件修改统计信息。
--shortstat 只显示 --stat 中最后的行数修改添加移除统计。
--name-only 仅在提交信息后显示已修改的文件清单。
--name-status 显示新增、修改、删除的文件清单。
--abbrev-commit 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。
--relative-date 使用较短的相对时间显示(比如,“2 weeks ago”)。
--graph 显示 ASCII 图形表示的分支合并历史。
--pretty 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。
过滤log输出
选项 说明
-(n) 仅显示最近的 n 条提交
--since, --after 仅显示指定时间之后的提交。
--until, --before 仅显示指定时间之前的提交。
--author 仅显示指定作者相关的提交。
--committer 仅显示指定提交者相关的提交。
例如
$ git log --pretty="%h - %s" --author=gitster --since="2008-10-01" --before="2008-11-01" --no-merges -- t/
修改最后一次提交
有时候我们提交完了才发现漏掉了几个文件没有加,或者提交信息写错了。想要撤消刚才的提交操作,可以使用--amend
选项重新提交:启动文本编辑器后,会看到上次提交时的说明,编辑它确认没问题后保存退出,就会使用新的提交说明覆盖刚才失误的提交。
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
上面的三条命令最终只是产生一个提交,第二个提交命令修正了第一个的提交内容。
例如:本机mac举例
root@fan:/home/wwwroot/Demo# vim README.md
root@fan:/home/wwwroot/Demo# vim test.md
root@fan:/home/wwwroot/Demo# git log --pretty=oneline --abbrev-commit -2
097860c one
90a8fec phpstorm idea
root@fan:/home/wwwroot/Demo# git add README.md
root@fan:/home/wwwroot/Demo# git commit -m 'two'
[master 490cf35] two
1 file changed, 1 insertion(+)
root@fan:/home/wwwroot/Demo# git status
位于分支 master
您的分支领先 'origin/master' 共 2 个提交。
(使用 "git push" 来发布您的本地提交)
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git checkout -- <文件>..." 丢弃工作区的改动)
修改: test.md
修改尚未加入提交(使用 "git add" 和/或 "git commit -a")
root@fan:/home/wwwroot/Demo# git add test.md
root@fan:/home/wwwroot/Demo# git log --pretty=oneline --abbrev-commit -2
490cf35 two
097860c one
root@fan:/home/wwwroot/Demo# git commit --amend
[master 372f01e] ootwo
Date: Tue Dec 6 20:13:35 2016 +0800
2 files changed, 2 insertions(+)
root@fan:/home/wwwroot/Demo# git log --pretty=oneline --abbrev-commit -2
372f01e ootwo
097860c one
git 各种撤销操作
版本回退
说明
类似3628164...882e1e0的是
commit_id
(版本号),和SVN不一样,Git的commit_id
不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示,而且你看到的commit_id
和我的肯定不一样,以你自己的为准。为什么commit_id
需要用这么一大串数字表示呢?因为Git是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用1,2,3……作为版本号,那肯定就冲突了。
每提交一个新版本,实际上Git就会把它们自动串成一条时间线。如果使用可视化工具查看Git历史,就可以更清楚地看到提交历史的时间线
root@fan:/home/wwwroot/Demo# git log --pretty=oneline
8a744873ac755647767abd149adef2e86ae53d71 +t2
97a15f36b57f6a0c588a058f654901ebeab6de31 +t1
ec30b4cb269ca10560b9abfe203fdd7a5abc7cfb 初始化
HEAD
指向的版本就是当前版本,HEAD^
指的是上一个版本,HEAD^^
指的是上上个版本。因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id
。commit id版本号没必要写全,前几位就可以了,Git会自动去找-
穿梭前,用
git log
可以查看提交历史,以便确定要回退到哪个版本。要重返未来,用git reflog
查看命令提交历史,以便确定要回到未来的哪个版本举例
root@fan:/home/wwwroot/Demo# git log --pretty=oneline --abbrev-commit 8a74487 +t2 97a15f3 +t1 ec30b4c 初始化 root@fan:/home/wwwroot/Demo# git reset --hard HEAD^ HEAD 现在位于 97a15f3 +t1 root@fan:/home/wwwroot/Demo# git log --pretty=oneline --abbrev-commit 97a15f3 +t1 ec30b4c 初始化 root@fan:/home/wwwroot/Demo# git reflog 97a15f3 HEAD@{0}: reset: moving to HEAD^ 8a74487 HEAD@{1}: commit: +t2 97a15f3 HEAD@{2}: commit: +t1 ec30b4c HEAD@{3}: commit: 初始化 root@fan:/home/wwwroot/Demo# git reset --hard 8a74487 HEAD 现在位于 8a74487 +t2 root@fan:/home/wwwroot/Demo# git log --pretty=oneline --abbrev-commit 8a74487 +t2 97a15f3 +t1 ec30b4c 初始化 root@fan:/home/wwwroot/Demo# git reflog 8a74487 HEAD@{0}: reset: moving to 8a74487 97a15f3 HEAD@{1}: reset: moving to HEAD^ 8a74487 HEAD@{2}: commit: +t2 97a15f3 HEAD@{3}: commit: +t1 ec30b4c HEAD@{4}: commit: 初始化
撤销文件修改
假如现在工作区,暂存区某个文件为版本2,版本库为版本1,修改工作区文件后,工作区变为版本3,运行命令git reset HAED file ,暂存区变为版本1,跟版本库一致,而工作区的版本还为版本3
-
场景1:使用git checkout — readme.txt丢弃工作区修改
当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令
git checkout -- file
。命令git checkout — readme.txt
把readme.txt文件在工作区的修改全部撤销,这里有两种情况:一种是readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次
git commit
或git add
时的状态。 -
场景2:使用git reset HEAD file丢弃暂存区修改
当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,添加完之后又修改了工作区,想丢弃暂存区的修改,分两步,
第一步用命令
git reset HEAD file
,
用命令
git reset HEAD file
可以把暂存区的修改撤销掉(unstage),而工作区的文件没有变化就回到了场景1,
第二步按场景1操作。
-
场景3:版本回退
已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退,不过前提是没有推送到远程库。
标签管理
新建
git tag <name>
用于新建一个标签,默认标签是打在最新提交的commit上,也可以指定一个commit id;git tag -a <tagname> -m "blablabla..."
可以指定标签信息;git tag -s <tagname> -m "blablabla..."
可以用PGP签名标签;
查看
-
git tag
可以查看所有标签,git show <tagname>
查看标签所指向的提交例子
root@fan:/home/wwwroot/git/demo# git status 位于分支 master 无文件要提交,干净的工作区 root@fan:/home/wwwroot/git/demo# git reflog 7aca999 HEAD@{0}: commit: c a8e8e16 HEAD@{1}: commit: b 9cab6a8 HEAD@{2}: commit (initial): a root@fan:/home/wwwroot/git/demo# clear root@fan:/home/wwwroot/git/demo# git tag 'cc' root@fan:/home/wwwroot/git/demo# git tag cc root@fan:/home/wwwroot/git/demo# git reflog 7aca999 HEAD@{0}: commit: c a8e8e16 HEAD@{1}: commit: b 9cab6a8 HEAD@{2}: commit (initial): a root@fan:/home/wwwroot/git/demo# git show cc commit 7aca999821cd3384d634a7985782e8816d851d9b Author: fan <fan@qq.com> Date: Wed Dec 7 21:08:35 2016 +0800 c diff --git a/c b/c new file mode 100644 index 0000000..f2ad6c7 --- /dev/null +++ b/c @@ -0,0 +1 @@ +c root@fan:/home/wwwroot/git/demo# git tag -a bb -m 'bbbbb' a8e8e16 root@fan:/home/wwwroot/git/demo# git tag bb cc root@fan:/home/wwwroot/git/demo# git show bb tag bb Tagger: fan <fan@qq.com> Date: Wed Dec 7 21:09:52 2016 +0800 bbbbb commit a8e8e16567a8c9adf06fe2778ee647250425451d Author: fan <fan@qq.com> Date: Wed Dec 7 21:07:53 2016 +0800 b diff --git a/b b/b new file mode 100644 index 0000000..6178079 --- /dev/null +++ b/b @@ -0,0 +1 @@ +b
推送标签到服务器
推送单个标签
git push origin <tagname>
推送所有标签
git push origin --tags
删除服务器标签
首先删除本地标签
git tag -d [tagname]
-
删除服务器上标签
git push origin :refs/tags/[tagname]
例子
root@fan:/home/wwwroot/git/Demo# git pull Already up-to-date. root@fan:/home/wwwroot/git/Demo# git push origin --tags Total 0 (delta 0), reused 0 (delta 0) To git@git.coding.net:fan_code/Demo.git * [new tag] 1 -> 1 * [new tag] 2 -> 2 root@fan:/home/wwwroot/git/Demo# git tag -d 1 已删除标签 '1'(曾为 90a8fec) root@fan:/home/wwwroot/git/Demo# git tag 2 root@fan:/home/wwwroot/git/Demo# git push origin :refs/tags/1 To git@git.coding.net:fan_code/Demo.git - [deleted] 1
分支管理
工作场景
假设此时,你突然接到一个电话说有个很严重的问题需要紧急修补,那么可以按照下面的方式处理:
返回到原先已经发布到生产服务器上的分支。
为这次紧急修补建立一个新分支,并在其中修复问题。
通过测试后,回到生产服务器所在的分支,将修补分支合并进来,然后再推送到生产服务器上。
切换到之前实现新需求的分支,继续工作。在切换回来之后,应该也把当前分支的bug修复
查看分支 git branch [name]
[name] 是添加分支
-d [name] 删除干净的分支(假如分支中包含尚未合并进来的工作成果,则为不干净的分支)
-D [name] 强制删除
-v 查看各个分支最后一个提交对象的信息
--merged 查看哪些分支已被并入当前分支(也就是说哪些分支是当前分支的直接上游,如果有分支内容与当前分支内容一致,也会显示)
--no-merged 查看哪些分支未被并入当前分支
切换分支 git checkout [name]
git checkout master
它把 HEAD 指针移回到 master 分支,并把工作目录中的文件换成了 master 分支所指向的快照内容
创建并切换分支 git checkout -b [name]
合并分支到当前工作分支 git merge [被合并的分支]
分支合并分为:直接祖先合并、非直接祖先合并(会进行三方合并)
-
直接祖先合并
合并时出现了“Fast forward”的提示。由于当前 master 分支所在的提交对象是要并入的 hotfix 分支的直接上游,Git 只需把master 分支指针直接右移。换句话说,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。
-
非直接祖先合并(合并mster-c4和iss53->c5)
这次你的开发历史是从更早的地方开始分叉的。由于当前 master 分支所指向的提交对象(C4)并不是 iss53 分支的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次简单的三方合并计算。这次,Git 没有简单地把分支指针右移,而是对三方合并后的结果重新做一个新的快照,并自动创建一个指向它的提交对象(C6)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。
删除分支 git branch -d [name]
推送所有分支到远程仓库origin git push origin --all
root@fan:/home/wwwroot/git/Demo# git push origin --all
对象计数中: 3, 完成.
压缩对象中: 100% (3/3), 完成.
写入对象中: 100% (3/3), 280 bytes | 0 bytes/s, 完成.
Total 3 (delta 2), reused 0 (delta 0)
To git@git.coding.net:fan_code/Demo.git
* [new branch] dev -> dev
* [new branch] dev2 -> dev2
* [new branch] test -> test
长期分支
由于 Git 使用简单的三方合并,所以就算在较长一段时间内,反复多次把某个分支合并到另一分支,也不是什么难事。也就是说,你可以同时拥有多个开放的分支,每个分支用于完成特定的任务,随着开发的推进,你可以随时把某个特性分支的成果并到其他分支中。
特性分支
在任何规模的项目中都可以使用特性(Topic)分支。一个特性分支是指一个短期的,用来实现单一特性或与其相关工作的分支。可能你在以前的版本控 制系统里从未做过类似这样的事情,因为通常创建与合并分支消耗太大。然而在 Git 中,一天之内建立、使用、合并再删除多个分支是常见的事。
远程分支
远程分支(remote branch)是对远程仓库中的分支的索引。它们是一些无法移动的本地分支;只有在 Git 进行网络交互时才会更新。
我们用 (远程仓库名)/(分支名) 这样的形式表示远程分支
一次 Git 克隆会建立你自己的本地分支 master 和远程分支 origin/master,它们都指向 origin/master 分支的最后一次提交
跟踪远程分支
从远程分支 checkout 出来的本地分支,称为_跟踪分支(tracking branch)_。跟踪分支是一种和远程分支有直接联系的本地分支。在跟踪分支里输入git push,Git 会自行推断应该向哪个服务器的哪个分支推送数据。反过来,在这些分支里运行 git pull 会获取所有远程索引,并把它们的数据都合并到本地分支中来。
在克隆仓库时,Git 通常会自动创建一个名为 master 的分支来跟踪 origin/master。这正是git push 和 git pull 一开始就能正常工作的原因。当然,你可以随心所欲地设定为其它跟踪分支,比如origin 上除了 master 之外的其它分支。刚才我们已经看到了这样的一个例子:git checkout -b [分支名] [远程名]/[分支名]。如果你有 1.6.2 以上版本的 Git,还可以用--track 选项简化:
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"
要为本地分支设定不同于远程分支的名字,只需在前个版本的命令里换个名字:
$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "sf"
现在你的本地分支 sf 会自动向 origin/serverfix 推送和抓取数据了。
删除远程分支
如果不再需要某个远程分支了,比如搞定了某个特性并把它合并进了远程的 master 分支(或任何其他存放稳定代码的地方),可以用这个非常无厘头的语法来删除它:git push [远程名] :[分支名]
。如果想在服务器上删除serverfix 分支,运行下面的命令:
$ git push origin :serverfix
To git@github.com:schacon/simplegit.git
- [deleted] serverfix
有种方便记忆这条命令的方法:记住我们不久前见过的 git push [远程名] [本地分支]:[远程分支]
语法,如果省略 [本地分支]
,那就等于是在说“在这里提取空白然后把它变成[远程分支]
”。
在服务器上部署 Git
开始架设 Git 服务器前,需要先把现有仓库导出为裸仓库 — 即一个不包含当前工作目录的仓库。克隆时用 --bare
选项即可。裸仓库的目录名一般以.git
结尾,像这样:
$ git clone --bare my_project my_project.git
Initialized empty Git repository in /opt/projects/my_project.git/
有了裸仓库的副本后,剩下的就是把它放到服务器上并设定相关协议。假设一个域名为 git.example.com
的服务器已经架设好,并可以通过 SSH 访问,我们打算把所有 Git 仓库储存在/opt/git 目录下。只要把裸仓库复制过去:
$ scp -r my_project.git user@git.example.com:/opt/git
现在,所有对该服务器有 SSH 访问权限,并可读取 /opt/git 目录的用户都可以用下面的命令克隆该项目:
$ git clone user@git.example.com:/opt/git/my_project.git
如果某个 SSH 用户对 /opt/git/my_project.git
目录有写权限,那他就有推送权限。
由此可见,根据现有的 Git 仓库创建一个裸仓库,然后把它放上你和同事都有 SSH 访问权的服务器是多么容易。现在已经可以开始在同一项目上密切合作了。
这是架设一个少数人具有连接权的 Git 服务的全部 — 只要在服务器上加入可以用 SSH 登录的帐号,然后把裸仓库放在大家都有读写权限的地方。一切都准备停当,无需更多。
小型安装
如果设备较少或者你只想在小型开发团队里尝试 Git ,那么一切都很简单。架设 Git 服务最复杂的地方在于账户管理。如果需要仓库对特定的用户可读,而给另一部分用户读写权限,那么访问和许可的安排就比较困难。
SSH 连接
如果已经有了一个所有开发成员都可以用 SSH 访问的服务器,架设第一个服务器将变得异常简单,几乎什么都不用做(正如上节中介绍的那样)。如果需要对仓库进行更复杂的访问控制,只要使用服务器操作系统的本地文件访问许可机制就行了。
如果需要团队里的每个人都对仓库有写权限,又不能给每个人在服务器上建立账户,那么提供 SSH 连接就是唯一的选择了。我们假设用来共享仓库的服务器已经安装了 SSH 服务,而且你通过它访问服务器。
有好几个办法可以让团队的每个人都有访问权。
第一个办法是给每个人建立一个账户,直截了当但略过繁琐。反复运行 adduser 并给所有人设定临时密码可不是好玩的。
第二个办法是在主机上建立一个 git 账户,让每个需要写权限的人发送一个 SSH 公钥,然后将其加入 git 账户的~/.ssh/authorized_keys 文件。这样一来,所有人都将通过 git 账户访问主机。这丝毫不会影响提交的数据 — 访问主机用的身份不会影响提交对象的提交者信息。
另一个办法是让 SSH 服务器通过某个 LDAP 服务,或者其他已经设定好的集中授权机制,来进行授权。只要每个人都能获得主机的 shell 访问权,任何可用的 SSH 授权机制都能达到相同效果。
1. 生成 SSH 公钥
大多数 Git 服务器都会选择使用 SSH 公钥来进行授权。系统中的每个用户都必须提供一个公钥用于授权,没有的话就要生成一个。生成公钥的过程在所有操作系统上都差不多。首先先确认一下是否已经有一个公钥了。SSH 公钥默认储存在账户的主目录下的~/.ssh 目录。进去看看:
$ cd ~/.ssh
$ ls
authorized_keys2 id_dsa known_hosts
config id_dsa.pub
关键是看有没有用 something 和 something.pub 来命名的一对文件,这个 something 通常就是 id_dsa 或 id_rsa。有 .pub 后缀的文件就是公钥,另一个文件则是密钥。假如没有这些文件,或者干脆连.ssh 目录都没有,可以用 ssh-keygen 来创建。该程序在 Linux/Mac 系统上由 SSH 包提供,而在 Windows 上则包含在 MSysGit 包里:
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/schacon/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/schacon/.ssh/id_rsa.
Your public key has been saved in /Users/schacon/.ssh/id_rsa.pub.
The key fingerprint is:
43:c5:5b:5f:b1:f1:50:43:ad:20:a6:92:6a:1f:9a:3a schacon@agadorlaptop.local
它先要求你确认保存公钥的位置(.ssh/id_rsa)
,然后它会让你重复一个密码两次,如果不想在使用公钥的时候输入密码,可以留空。
现在,所有做过这一步的用户都得把它们的公钥给你或者 Git 服务器的管理员(假设 SSH 服务被设定为使用公钥机制)。他们只需要复制 .pub 文件的内容然后发邮件给管理员。公钥的样子大致如下:
$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU
GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3
Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA
t3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/En
mZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx
NrRFi9wrf+M7Q== schacon@agadorlaptop.local
关于在多个操作系统上设立相同 SSH 公钥的教程,可以查阅 GitHub 上有关 SSH 公钥的向导:http://github.com/guides/prov...。
2. 架设服务器
现在我们过一边服务器端架设 SSH 访问的流程。本例将使用 authorized_keys
方法来给用户授权。我们还将假定使用类似 Ubuntu 这样的标准 Linux 发行版。首先,创建一个名为 ‘git’ 的用户,并为其创建一个.ssh 目录。
$ sudo adduser git
$ su git
$ cd
$ mkdir .ssh
接下来,把开发者的 SSH 公钥添加到这个用户的 authorized_keys 文件中。假设你通过电邮收到了几个公钥并存到了临时文件里。重复一下,公钥大致看起来是这个样子:
$ cat /tmp/id_rsa.john.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCB007n/ww+ouN4gSLKssMxXnBOvf9LGt4L
ojG6rs6hPB09j9R/T17/x4lhJA0F3FR1rP6kYBRsWj2aThGw6HXLm9/5zytK6Ztg3RPKK+4k
Yjh6541NYsnEAZuXz0jTTyAUfrtU3Z5E003C4oxOj6H0rfIF1kKI9MAQLMdpGW1GYEIgS9Ez
Sdfd8AcCIicTDWbqLAcU4UpkaX8KyGlLwsNuuGztobF8m72ALC/nLF6JLtPofwFBlgc+myiv
O7TCUSBdLQlgMVOFq1I2uPWQOkOWQAHukEOmfjy2jctxSDBQ220ymjaNsHT4kgtZg2AYYgPq
dAv8JggJICUvax2T9va5 gsg-keypair
只要把它们逐个追加到 authorized_keys 文件尾部即可:
$ cat /tmp/id_rsa.john.pub >> ~/.ssh/authorized_keys
$ cat /tmp/id_rsa.josie.pub >> ~/.ssh/authorized_keys
$ cat /tmp/id_rsa.jessica.pub >> ~/.ssh/authorized_keys
现在可以用 --bare 选项运行 git init 来建立一个裸仓库,这会初始化一个不包含工作目录的仓库。
$ cd /opt/git
$ mkdir project.git
$ cd project.git
$ git --bare init
这时,Join,Josie 或者 Jessica 就可以把它加为远程仓库,推送一个分支,从而把第一个版本的项目文件上传到仓库里了。值得注意的是,每次添加一个新项目都需要通过 shell 登入主机并创建一个裸仓库目录。我们不妨以gitserver 作为 git 用户及项目仓库所在的主机名。如果在网络内部运行该主机,并在 DNS 中设定 gitserver 指向该主机,那么以下这些命令都是可用的:
# 在 John 的电脑上
$ cd myproject
$ git init
$ git add .
$ git commit -m 'initial commit'
$ git remote add origin git@gitserver:/opt/git/project.git
$ git push origin master
这样,其他人的克隆和推送也一样变得很简单:
$ git clone git@gitserver:/opt/git/project.git
$ vim README
$ git commit -am 'fix for the README file'
$ git push origin master
用这个方法可以很快捷地为少数几个开发者架设一个可读写的 Git 服务。
作为一个额外的防范措施,你可以用 Git 自带的 git-shell 工具限制 git 用户的活动范围。只要把它设为git 用户登入的 shell,那么该用户就无法使用普通的 bash 或者 csh 什么的 shell 程序。编辑 /etc/passwd 文件:
$ sudo vim /etc/passwd
在文件末尾,你应该能找到类似这样的行:
git:x:1000:1000::/home/git:/bin/sh
把 bin/sh
改为 /usr/bin/git-shell
(或者用 which git-shell 查看它的实际安装路径)。该行修改后的样子如下:
git:x:1000:1000::/home/git:/usr/bin/git-shell
现在 git 用户只能用 SSH 连接来推送和获取 Git 仓库,而不能直接使用主机 shell。尝试普通 SSH 登录的话,会看到下面这样的拒绝信息:
$ ssh git@gitserver
fatal: What do you think I am? A shell?
Connection to gitserver closed.
Gitosis 可以设置谁能读或者写哪个项目
把所有用户的公钥保存在 authorized_keys 文件的做法,只能凑和一阵子,当用户数量达到几百人的规模时,管理起来就会十分痛苦。每次改删用户都必须登录服务器不去说,这种做法还缺少必要的权限管理 — 每个人都对所有项目拥有完整的读写权限。
幸好我们还可以选择应用广泛的 Gitosis 项目。简单地说,Gitosis 就是一套用来管理 authorized_keys 文件和实现简单连接限制的脚本。有趣的是,用来添加用户和设定权限的并非通过网页程序,而只是管理一个特殊的 Git 仓库。你只需要在这个特殊仓库内做好相应的设定,然后推送到服务器上,Gitosis 就会随之改变运行策略,听起来就很酷,对吧?
Gitosis 的安装算不上傻瓜化,但也不算太难。用 Linux 服务器架设起来最简单 — 以下例子中,我们使用装有 Ubuntu 8.10 系统的服务器。
Gitosis 的工作依赖于某些 Python 工具,所以首先要安装 Python 的 setuptools 包,在 Ubuntu 上称为 python-setuptools:
$ apt-get install python-setuptools
接下来,从 Gitosis 项目主页克隆并安装:
$ git clone git://eagain.net/gitosis.git
$ cd gitosis
$ sudo python setup.py install
这会安装几个供 Gitosis 使用的工具。默认 Gitosis 会把 /home/git 作为存储所有 Git 仓库的根目录,这没什么不好,不过我们之前已经把项目仓库都放在/opt/git 里面了,所以为方便起见,我们可以做一个符号连接,直接划转过去,而不必重新配置:
$ ln -s /opt/git /home/git/repositories
Gitosis 将会帮我们管理用户公钥,所以先把当前控制文件改名备份,以便稍后重新添加,准备好让 Gitosis 自动管理 authorized_keys 文件:
$ mv /home/git/.ssh/authorized_keys /home/git/.ssh/ak.bak
接下来,如果之前把 git 用户的登录 shell 改为 git-shell 命令的话,先恢复 ‘git’ 用户的登录 shell。改过之后,大家仍然无法通过该帐号登录(译注:因为authorized_keys 文件已经没有了。),不过不用担心,这会交给 Gitosis 来实现。所以现在先打开 /etc/passwd 文件,把这行:
git:x:1000:1000::/home/git:/usr/bin/git-shell
改回:
git:x:1000:1000::/home/git:/bin/sh
好了,现在可以初始化 Gitosis 了。你可以用自己的公钥执行 gitosis-init 命令,要是公钥不在服务器上,先临时复制一份:
$ sudo -H -u git gitosis-init < /tmp/id_dsa.pub
Initialized empty Git repository in /opt/git/gitosis-admin.git/
Reinitialized existing Git repository in /opt/git/gitosis-admin.git/
这样该公钥的拥有者就能修改用于配置 Gitosis 的那个特殊 Git 仓库了。接下来,需要手工对该仓库中的 post-update 脚本加上可执行权限:
$ sudo chmod 755 /opt/git/gitosis-admin.git/hooks/post-update
基本上就算是好了。如果设定过程没出什么差错,现在可以试一下用初始化 Gitosis 的公钥的拥有者身份 SSH 登录服务器,应该会看到类似下面这样:
$ ssh git@gitserver
PTY allocation request failed on channel 0
fatal: unrecognized command 'gitosis-serve schacon@quaternion'
Connection to gitserver closed.
说明 Gitosis 认出了该用户的身份,但由于没有运行任何 Git 命令,所以它切断了连接。那么,现在运行一个实际的 Git 命令 — 克隆 Gitosis 的控制仓库:
# 在你本地计算机上
$ git clone git@gitserver:gitosis-admin.git
这会得到一个名为 gitosis-admin 的工作目录,主要由两部分组成:
$ cd gitosis-admin
$ find .
./gitosis.conf
./keydir
./keydir/scott.pub
gitosis.conf 文件是用来设置用户、仓库和权限的控制文件。keydir 目录则是保存所有具有访问权限用户公钥的地方— 每人一个。在keydir 里的文件名(比如上面的 scott.pub)应该跟你的不一样 — Gitosis 会自动从使用 gitosis-init 脚本导入的公钥尾部的描述中获取该名字。
看一下 gitosis.conf 文件的内容,它应该只包含与刚刚克隆的 gitosis-admin 相关的信息:
$ cat gitosis.conf
[gitosis]
[group gitosis-admin]
writable = gitosis-admin
members = scott
它显示用户 scott — 初始化 Gitosis 公钥的拥有者 — 是唯一能管理 gitosis-admin 项目的人。
现在我们来添加一个新项目。为此我们要建立一个名为 mobile 的新段落,在其中罗列手机开发团队的开发者,以及他们拥有写权限的项目。由于 ‘scott’ 是系统中的唯一用户,我们把他设为唯一用户,并允许他读写名为iphone_project 的新项目:
[group mobile]
writable = iphone_project
members = scott
修改完之后,提交 gitosis-admin 里的改动,并推送到服务器使其生效:
$ git commit -am 'add iphone_project and mobile group'
[master]: created 8962da8: "changed name"
1 files changed, 4 insertions(+), 0 deletions(-)
$ git push
Counting objects: 5, done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 272 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To git@gitserver:/opt/git/gitosis-admin.git
fb27aec..8962da8 master -> master
在新工程 iphone_project 里首次推送数据到服务器前,得先设定该服务器地址为远程仓库。但你不用事先到服务器上手工创建该项目的裸仓库— Gitosis 会在第一次遇到推送时自动创建:
$ git remote add origin git@gitserver:iphone_project.git
$ git push origin master
Initialized empty Git repository in /opt/git/iphone_project.git/
Counting objects: 3, done.
Writing objects: 100% (3/3), 230 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@gitserver:iphone_project.git
* [new branch] master -> master
请注意,这里不用指明完整路径(实际上,如果加上反而没用),只需要一个冒号加项目名字即可 — Gitosis 会自动帮你映射到实际位置。
要和朋友们在一个项目上协同工作,就得重新添加他们的公钥。不过这次不用在服务器上一个一个手工添加到 ~/.ssh/authorized_keys 文件末端,而只需管理keydir 目录中的公钥文件。文件的命名将决定在 gitosis.conf 中对用户的标识。现在我们为 John,Josie 和 Jessica 添加公钥:
$ cp /tmp/id_rsa.john.pub keydir/john.pub
$ cp /tmp/id_rsa.josie.pub keydir/josie.pub
$ cp /tmp/id_rsa.jessica.pub keydir/jessica.pub
然后把他们都加进 ‘mobile’ 团队,让他们对 iphone_project 具有读写权限:
[group mobile]
writable = iphone_project
members = scott john josie jessica
如果你提交并推送这个修改,四个用户将同时具有该项目的读写权限。
Gitosis 也具有简单的访问控制功能。如果想让 John 只有读权限,可以这样做:
[group mobile]
writable = iphone_project
members = scott josie jessica
[group mobile_ro]
readonly = iphone_project
members = john
现在 John 可以克隆和获取更新,但 Gitosis 不会允许他向项目推送任何内容。像这样的组可以随意创建,多少不限,每个都可以包含若干不同的用户和项目。甚至还可以指定某个组为成员之一(在组名前加上@ 前缀),自动继承该组的成员:
[group mobile_committers]
members = scott josie jessica
[group mobile]
writable = iphone_project
members = @mobile_committers
[group mobile_2]
writable = another_iphone_project
members = @mobile_committers john
如果遇到意外问题,试试看把 loglevel=DEBUG 加到 [gitosis] 的段落(译注:把日志设置为调试级别,记录更详细的运行信息。)。如果一不小心搞错了配置,失去了推送权限,也可以手工修改服务器上的/home/git/.gitosis.conf 文件 — Gitosis 实际是从该文件读取信息的。它在得到推送数据时,会把新的 gitosis.conf 存到该路径上。所以如果你手工编辑该文件的话,它会一直保持到下次向 gitosis-admin 推送新版本的配置内容为止。
工作流程
提交规范
请将每次提交限定于完成一次逻辑功能。并且可能的话,适当地分解为多次小更新,以便每次小型提交都更易于理解。请不要在周末穷追猛打一次性 解决五个问题,而最后拖到周一再提交。就算是这样也请尽可能利用暂存区域,将之前的改动分解为每次修复一个问题,再分别提交和加注说明。如果针对两个问题 改动的是同一个文件,可以试试看git add --patch 的方式将部分内容置入暂存区域(我们会在第六章再详细介绍)。无论是五次小提交还是混杂在一起的大提交,最终分支末端的项目快照应该还是一样的,但分解开 来之后,更便于其他开发者复阅。这么做也方便自己将来取消某个特定问题的修复。我们将在第六章介绍一些重写提交历史,同暂存区域交互的技巧和工具,以便最 终得到一个干净有意义,且易于理解的提交历史
最后需要谨记的是提交说明的撰写。写得好可以让大家协作起来更轻松。一般来说,提交说明最好限制在一行以内,50 个字符以下,简明扼要地描述更新内容,空开一行后,再展开详细注解。Git 项目本身需要开发者撰写详尽注解,包括本次修订的因由,以及前后不同实现之间的比较,我们也该借鉴这种做法
例如
本次更新的简要描述(50 个字符以内)
如果必要,此处展开详尽阐述。段落宽度限定在 72 个字符以内。
某些情况下,第一行的简要描述将用作邮件标题,其余部分作为邮件正文。
其间的空行是必要的,以区分两者(当然没有正文另当别论)。
如果并在一起,rebase 这样的工具就可能会迷惑。
另起空行后,再进一步补充其他说明。
- 可以使用这样的条目列举式。
- 一般以单个空格紧跟短划线或者星号作为每项条目的起始符。每个条目间用一空行隔开。
不过这里按自己项目的约定,可以略作变化。
-
工作流程
最简单的协作方式之一:先在自己的特性分支中工作一段时间,完成后合并到自己的 master 分支;然后下载合并 origin/master 上的更新(如果有的话),再推回远程服务器。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。