版本控制工具主要实现2个功能:
版本管理
在开发中,这是刚需,必须允许可以很容易对产品的版本进行任意回滚,版本控制工具实现这个功能的原理简单来讲,就是你每修改一次代码,它就帮你做一次快照
协作开发
一个复杂点的软件,往往不是一个开发人员可以搞定的,公司为加快产品开发速度,会招聘一堆跟你一样的开发人员开发这个产品,拿微信来举例,现在假设3个人一起开发微信,A开发联系人功能,B开发发文字、图片、语音通讯功能,C开发视频通话功能, B和C的功能都是要基于通讯录的,你说简单,直接把A开发的代码copy过来,在它的基础上开发就好了,可以,但是你在他的代码基础上开发了2周后,这期间A没闲着,对通讯录代码作了更新,此时怎么办?你和他的代码不一致了,此时我们知道,你肯定要再把A的新代码拿过来替换掉你手上的旧通讯录功能代码, 现在人少,3个人之间沟通很简单,但想想,如果团队变成30个人呢?来回这样copy代码,很快就乱了, 所以此时亟需一个工具,能确保一直存储最新的代码库,所有人的代码应该和最新的代码库保持一致
常见版本管理工具介绍
1、VSS-- Visual Source Safe
此工具是Microsoft提供的,是使用的相当普遍的工具之一,他可以与VS.net进行无缝集成,成为了独立开发人员和小型开发团队所适合的工具,基本上Window平台上开发的中小型企业,当规模较大后,其性能通常是无法忍受的,对分支与并行开发支持的比较有限。
2、CVS--Concurrent Versions System
此工具是一个开源工具,与后面提到的SVN是同一个厂家:Collab.Net提供的。
CVS是源于unix的版本控制工具,对于CVS的安装和使用最好对unix的系统有所了解能更容易学习,CVS的服务器管理需要进行各种命令行操作。目前,CVS的客户端有winCVS的图形化界面,服务器端也有CVSNT的版本,易用性正在提高。使用成熟的“Copy-Modify-Merge"开发模型,可以大大的提高开发效率,适合于项目比较大,产品发布频繁,分支活动频繁的中大型项目。
3、SVN --CollabNet Subversion
此工具是在CVS 的基础上,由CollabNet提供开发的,也是开源工具,应用比较广泛。
他修正cvs的一些局限性,适用范围同cvs,目前有一些基于SVN的第三方工具,如TortoiseSVN,是其客户端程序,使用的也相当广泛。在权限管理,分支合并等方面做的很出色,他可以与Apache集成在一起进行用户认证。
不过在权限管理方面目前还没有个很好用的界面化工具,SVNManger对于已经使用SVN进行配置的项目来说,基本上是无法应用的,但对于从头开始的项目是可以的,功能比较强大,但是搭建svnManger比较麻烦。
是一个跨平台的软件,支持大多数常见的操作系统。作为一个开源的版本控制系统,Subversion 管理着随时间改变的数据。 这些数据放置在一个中央资料档案库中。 这个档案库很像一个普通的文件服务器, 不过它会记住每一次文件的变动。 这样你就可以把档案恢复到旧的版本, 或是浏览文件的变动历史。Subversion 是一个通用的系统, 可用来管理任何类型的文件, 其中包括了程序源码。
4. GIT
因为最初是从Linux起家的,非常依赖文件系统的一些特性,这些在 Linux 下表现的很好,而 Windows 下特别糟糕Git 中文教程
Git是一个开源的分布式版本控制系统,用以有效、高速的处理从很小到非常大的项目版本管理.
Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。
Torvalds 开始着手开发 Git 是为了作为一种过渡方案来替代 BitKeeper,后者之前一直是 Linux 内核开发人员在全球使用的主要源代码工具。开放源码社区中的有些人觉得 BitKeeper 的许可证并不适合开放源码社区的工作,因此 Torvalds 决定着手研究许可证更为灵活的版本控制系统。尽管最初 Git 的开发是为了辅助 Linux 内核开发的过程,但是我们已经发现在很多其他自由软件项目中也使用了 Git。例如 最近就迁移到 Git 上来了,很多 Freedesktop 的项目也迁移到了 Git 上。
5、BitKeeper
是由BitMover公司提供的,BitKeeper自称是“分布式”可扩缩SCM系统。
不是采用C/S结构,而是采用P2P结构来实现的,同样支持变更任务,所有变更集的操作都是原子的,与svn,cvs一致。
版本库创建
版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。
选择一个合适的地方,创建一个空目录:
$ mkdir git_trainning $ cd git_trainning/ $ git init Initialized empty Git repository in /Users/alex/git_trainning/.git/
当前目录下多了一个 .git 的目录,这个目录是Git来跟踪管理版本库的
把文件添加到版本库
所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等,Git也不例外。
版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。
而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。
不幸的是,Microsoft的Word格式是二进制格式,因此,版本控制系统是没法跟踪Word文件的改动的,前面我们举的例子只是为了演示,如果要真正使用版本控制系统,就要以纯文本方式编写文件。
因为文本是有编码的,比如中文有常用的GBK编码,日文有Shift_JIS编码,如果没有历史遗留问题,强烈建议使用标准的UTF-8编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。
编写一个first_git_file.txt文件,内容如下:
$ vim first_git_file.txt first time using git, excited! 第一次用git哈哈
一定要放到git_trainning目录下(子目录也行),因为这是一个Git仓库,放到其他地方Git再厉害也找不到这个文件。
第一步,用命令 git add 告诉Git,把文件添加到仓库:
$ git add first_git_file.txt
执行上面的命令,没有任何显示,说明添加成功。
第二步,用命令 git commit 告诉Git,把文件提交到仓库:
$ git commit -m "commit my first git file" [master (root-commit) 621e6e4] commit my first git file Your name and email address were configured automatically based on your username and hostname. Please check that they are accurate. You can suppress this message by setting them explicitly. Run the following command and follow the instructions in your editor to edit your configuration file: git config --global --edit After doing this, you may fix the identity used for this commit with: git commit --amend --reset-author 1 file changed, 2 insertions(+) create mode 100644 first_git_file.txt
在往git库里提交代码时,你需要告诉git你是谁,这样git就会纪录下来是谁改的代码,其实就是为了日后查询方便,你只需要提供一个名字和邮件地址就可以,你可以通过git config --global --edit修改
git commit 命令,-m,后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
为什么Git添加文件需要 add,commit 一共两步呢?因为 commit 可以一次提交很多文件,所以你可以多次 add 不同的文件,比如:
$ git add file1.txt $ git add file2.txt file3.txt $ git commit -m "add 3 files."
代码回滚
1、代码修改并提交
我们已经成功地添加并提交了一个first_git_file.txt文件,现在,是时候继续工作了,于是,我们继续修改first_git_file.txt文件,改成如下内容:
First time using git, excited! update ... insert line here... 第一次用git哈哈
现在,运行 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: first_git_file.txt no changes added to commit (use "git add" and/or "git commit -a")
git diff 这个命令可以查看 没有被add到暂存区的文件 与 仓库文件的区别 :
$ git diff first_git_file.txt diff --git a/first_git_file.txt b/first_git_file.txt index 2d13c2c..248d853 100644 --- a/first_git_file.txt +++ b/first_git_file.txt @@ -1,3 +1,4 @@ -first time using git, excited! +First time using git, excited! update ... insert line here... 第一次用git哈哈 +insert line again haha...
输出中+号绿色显示的就是修改或新增的内容,-号红色显示的就是去掉或被修改的内容
知道了对first_git_file.txt 作了什么修改后,再把它提交到仓库就放心多了,提交修改和提交新文件是一样的两步,
第一步是 git add .
$ git add . # . 代表把当前目录下所有改动的文件都提交到代码库 Alexs-MacBook-Pro:git_trainning alex$ git commit -m "commit changes" [master 50ad6b5] commit changes Committer: Alex Li <alex@Alexs-MacBook-Pro.local> 1 file changed, 1 insertion(+)
提交后,我们再用 git status
命令看看仓库的当前状态:
$ git status # On branch master nothing to commit (working directory clean)
Git告诉我们当前没有需要提交的修改,而且,工作目录是干净(working directory clean)的。
4.2 代码回滚
每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为 commit
一旦你把文件改乱了,或者误删了文件,还可以从最近的一个 commit 恢复,然后继续工作,而不是把几个月的工作成果全部丢失。
版本控制系统肯定有某个命令可以告诉我们历史记录,在Git中,我们用 git log 命令查看:
$ git log commit 445965781d1fd0d91e76d120450dd18fd06c7489 Author: firefly <442994909@qq.com> Date: Tue Oct 4 18:44:29 2016 +0800 add new content commit be02137bb2f54bbef0c2e99202281b3966251952 Author: firefly <442994909@qq.com> Date: Tue Oct 4 17:55:16 2016 +0800 update again commit 50ad6b526810bb7ccfea430663757ba2337b9816 Author: firefly <442994909@qq.com> Date: Tue Oct 4 17:46:51 2016 +0800 commit changes commit 621e6e44d04fa6a1cdc37826f01efa61b451abd1 Author: firefly <442994909@qq.com> Date: Tue Oct 4 17:42:50 2016 +0800 commit my first git file
git log 命令显示从最近到最远的提交日志,如果嫌输出信息太多,看得眼花缭乱的,可以试试加上 --pretty=oneline 参数
$ git log --pretty=oneline 445965781d1fd0d91e76d120450dd18fd06c7489 add new content be02137bb2f54bbef0c2e99202281b3966251952 update again 50ad6b526810bb7ccfea430663757ba2337b9816 commit changes 621e6e44d04fa6a1cdc37826f01efa61b451abd1 commit my first git file
一大串类似字符的是commit id(版本号),和SVN不一样,Git的 commit id 不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示
回滚
我们要把当前版本“add new content”回退到上一个版本“update again”,就可以使用 git reset 命令:
$ git reset --hard HEAD^ HEAD is now at be02137 update again
此时还可以继续再往前回退一个版本,我们用 git log 再看看现在版本库的状态:
$ git log --pretty=oneline be02137bb2f54bbef0c2e99202281b3966251952 update again 50ad6b526810bb7ccfea430663757ba2337b9816 commit changes 621e6e44d04fa6a1cdc37826f01efa61b451abd1 commit my first git file
最新的那个版本add new content已经看不到了!好比你从21世纪坐时光穿梭机来到了19世纪,想再回去已经回不去了,肿么办?
办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个add new content的 commit id 是445965781d1fd0d91e76d120450dd18fd06c7489
于是就可以指定回到未来的某个版本:
git reset --hard 4459657 HEAD is now at 4459657 add new content
版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。
Git的版本回退速度非常快,因为Git在内部有个指向当前版本的 HEAD 指针,当你回退版本的时候,Git仅仅是把HEAD从指向 add new content
现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的commit id怎么办?
当你用 $ git reset --hard HEAD^ 回退到update again版本时,再想恢复到最新add new content的版本,就必须找到add new contentL的commit id。
Git提供了一个命令 git reflog 用来记录你的每一次命令:
$ git reflog 4459657 HEAD@{0}: reset: moving to 4459657 be02137 HEAD@{1}: reset: moving to HEAD^ 4459657 HEAD@{2}: commit: add new content be02137 HEAD@{3}: reset: moving to be02137bb 50ad6b5 HEAD@{4}: reset: moving to 50ad6b5 621e6e4 HEAD@{5}: reset: moving to 621e6e44 50ad6b5 HEAD@{6}: reset: moving to HEAD^ be02137 HEAD@{7}: commit: update again 50ad6b5 HEAD@{8}: commit: commit changes 621e6e4 HEAD@{9}: commit (initial): commit my first git file
工作区和暂存区
Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念。
工作区(Working Directory)
就是你在电脑里能看到的目录,比如我的git_trainning文件夹就是一个工作区:
$ ls git_trainning/ first_git_file.txt
版本库(Repository)
工作区有一个隐藏目录 .git,这个不算工作区,而是Git的版本库。
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,
还有Git为我们自动创建的第一个分支 master,以及指向 master 的一个指针叫 HEAD。
前面讲了我们把文件往Git版本库里添加的时候,是分两步执行的:
第一步是用 git add 把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用 git commit 提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为我们创建Git版本库时,Git自动为我们创建了唯一一个 master 分支,所以,现在,git commit 就是往 master 分支上提交更改。
你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。
我们再练习一遍,先对 first_git_file.txt 做个修改,比如加上一行内容:
First time using git, excited! update ... insert line here..改之前的. 第一次用git哈哈 insert line again haha... 加点新内容 update v5
然后,在工作区新增一个 readme.md 文本文件(内容随便写)。
先用 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: first_git_file.txt Untracked files: (use "git add <file>..." to include in what will be committed) readme.md no changes added to commit (use "git add" and/or "git commit -a")
Git非常清楚地告诉我们,first_git_file.txt 被修改了,而 readme.md 还从来没有被添加过,所以它的状态是Untracked。
现在,使用命令git add .,再用 git status 再查看一下:
$ git add . $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: first_git_file.txt new file: readme.md
现在,暂存区的状态就变成这样了:
(盗图关系, 这里readme.txt = first_git_file.txt , LICENSE = readme.md)
所以,git add 命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行 git commit 就可以一次性把暂存区的所有修改提交到分支。
$ git commit -m "知道暂存区stage的意思了" [master 9d65cb2] 知道暂存区stage的意思了 2 files changed, 2 insertions(+) create mode 100644 readme.md
一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的:
$ git status On branch master nothing to commit, working directory clean
现在版本库变成了这样,暂存区就没有任何内容了:
(盗图关系, 这里readme.txt = first_git_file.txt , LICENSE = readme.md)
暂存区是Git非常重要的概念,弄明白了暂存区,就弄明白了Git的很多操作到底干了什么。
撤销修改
自然,你是不会犯错的。不过现在是凌晨两点,你正在赶一份工作报告,你在 readme.md 中添加了一行:
#git study repo git is great but my stupid boss still prefers SVN.
在你准备提交前,一杯咖啡起了作用,你猛然发现了“stupid boss”可能会让你丢掉这个月的奖金!
既然错误发现得很及时,就可以很容易地纠正它。你可以删掉最后一行,手动把文件恢复到上一个版本的状态。如果用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.md no changes added to commit (use "git add" and/or "git commit -a")
你可以发现,Git会告诉你,git checkout -- file可以丢弃工作区的修改:
$ git checkout -- readme.md $ more readme.md #git study repo
你刚才添加的2行骂老板的话就被撤销了,
命令 git checkout -- readme.md 意思就是,把readme.md文件在工作区的修改全部撤销,这里有两种情况:
一种是 readme.md 自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是 readme.md 已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次 git commit 或 git add时的状态。
git checkout -- file命令中的 -- 很重要,没有--,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到 git checkout 命令。
情景二:
现在假定是凌晨3点,你不但写了一些胡话,还 git add 到暂存区了:
$ cat readme.md Git is a distributed version control system. Git is free software distributed under the GPL. Git has a mutable index called stage. Git tracks changes of files. My stupid boss still prefers SVN. $ git add readme.md
庆幸的是,在commit之前,你发现了这个问题。用git status查看一下,修改只是添加到了暂存区,还没有提交:
$ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: readme.md
Git同样告诉我们,用命令 git reset HEAD file 可以把暂存区的修改撤销掉(unstage),重新放回工作区:
$ git reset HEAD readme.md Unstaged changes after reset: M readme.md
git reset命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本。
再用 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.md # no changes added to commit (use "git add" and/or "git commit -a")
还记得如何丢弃工作区的修改吗?
$ git checkout -- readme.md $ more readme.md #git study repo
整个世界终于清静了!
删除操作
在Git中,删除也是一个修改操作,我们实战一下,先添加一个新文件test.txt到Git并且提交:
$ git add . $ git commit -m "add test.txt" [master a8fa95a] add test.txt 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test.txt
一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用 rm 命令删了
$ rm test.txt
这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,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: test.txt no changes added to commit (use "git add" and/or "git commit -a")
现在你有两个选择,(1)是确实要从版本库中删除该文件,那就用命令 git rm 删掉,并且 git commit:
$ git rm test.txt rm 'test.txt' $ git commit -m "remove test" [master 03df00a] remove test 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test.txt
现在,文件就从版本库中被删除了。
(2)另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:
$ git checkout -- test.txt
git checkout 其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
远程仓库
注册GitHub账号。由于你的本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以,需要一点设置:
第1步:创建SSH Key。
在用户主目录下,看看有没有.ssh目录如(C:\Users\admin\.ssh),如果有,再看看这个目录下有没有 id_rsa 和 id_rsa.pub 这两个文件,如果已经有了,可直接跳到下一步。
如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key:
$ ssh-keygen -t rsa -C "youremail@example.com"
你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。
如果一切顺利的话,可以在用户主目录里找到 .ssh 目录,里面有 id_rsa 和 id_rsa.pub 两个文件,
这两个就是SSH Key的秘钥对,id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,可以放心地告诉任何人。
第2步:登陆GitHub,
打开“Account settings”,“SSH Keys”页面:
然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴 id_rsa.pub 文件的内容:
点“Add Key”,你就应该看到已经添加的Key
为什么GitHub需要SSH Key呢?
因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。
当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。
创建远程仓库
现在的情景是,你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。
首先,登陆GitHub,然后,在右上角找到“New repository”按钮,创建一个新的仓库:
目前,在GitHub上的这个oldboy_website仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。
现在,我们根据GitHub的提示,在本地已有的git_trainning仓库下运行命令:
$ git remote add origin git@github.com:xxxxxx.git # 添加远程仓库 $ git push -u origin master # 推到远程 The authenticity of host 'github.com (192.30.253.113)' can't be established. RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8. Are you sure you want to continue connecting (yes/no)? yes # 第一次推会出现,写yes Warning: Permanently added 'github.com,192.30.253.113' (RSA) to the list of known hosts. Counting objects: 20, done. Delta compression using up to 8 threads. Compressing objects: 100% (14/14), done. Writing objects: 100% (20/20), 1.76 KiB | 0 bytes/s, done. Total 20 (delta 4), reused 0 (delta 0) remote: Resolving deltas: 100% (4/4), done. To git@github.com:xxxxxxx.git * [new branch] master -> master Branch master set up to track remote branch master from origin.
请千万注意,把上面的triaquae替换成你自己的GitHub账户名,否则,你在本地关联的就是我的远程库,关联没有问题,但是你以后推送是推不上去的,因为你的SSH Key公钥不在我的账户列表中。
添加后,远程库的名字就是 origin,这是Git默认的叫法,也可以改成别的,但是 origin 这个名字一看就知道是远程库。
把本地库的内容推送到远程,用 git push 命令,实际上是把当前分支 master 推送到远程。
刷新远程仓库页面, 就看到了你刚从本地推上来的代码了
从现在起,只要本地作了提交,就可以通过命令:
$ git push origin master
创建一个index.html文件,同时上传到远程
$ vim index.html $ git add . $ git commit -m "add home page" [master 8675486] add home page 1 file changed, 6 insertions(+) create mode 100644 index.html $ git push origin master #推到远程 Counting objects: 3, done. Delta compression using up to 8 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 362 bytes | 0 bytes/s, done. Total 3 (delta 0), reused 0 (delta 0) To git@github.com:xxxxxxxx.git 03df00a..8675486 master -> master
然后刷新下远程仓库页面,就看到你的新创建的文件了
在Github 上面上建了一个repositories,怎么才能让他人也有push到这个repositories的权限?
所在仓库的settings -> Collaborators -> 输入他人的用户名/全名/邮箱 -> add Collaborator,自动生成一个邀请链接发送给他,点确认。
从远程库克隆
我们讲了先有本地库,后有远程库的时候,如何关联远程库。
现在,假设我们从零开发,那么最好的方式是先创建远程库,然后,从远程库克隆。
首先,登陆GitHub,创建一个新的仓库,名字叫gitskills:
我们勾选 Initialize this repository with a README,这样GitHub会自动为我们创建一个 README.md 文件。创建完毕后,可以看到 README.md文件:
现在,远程库已经准备好了,下一步是用命令 git clone 克隆一个本地库:
在本地找一个你想存放这个远程仓库的目录,然后在本地命令行用 git clone 命令来克隆这个远程库
$ git clone git@github.com:triaquae/gitskills.git Cloning into 'gitskills'... Warning: Permanently added the RSA host key for IP address '192.30.253.112' to the list of known hosts. remote: Counting objects: 3, done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Receiving objects: 100% (3/3), done. Checking connectivity... done. $ cd gitskills/ #进入刚clone下来的目录 $ ls README.md
如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。
你也许还注意到,GitHub给出的地址不止一个,还可以用https://github.com/triaquae/gitskills.git 这样的地址。
实际上,Git支持多种协议,默认的 git:// 使用ssh,但也可以使用 https 等其他协议。
使用https除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http端口的公司内部就无法使用ssh协议而只能用https
分支管理
分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
其他版本控制系统如SVN等都有分支管理,但是用过之后你会发现,这些版本控制系统创建和切换分支比蜗牛还慢,简直让人无法忍受,结果分支功能成了摆设,大家都不去用。
但Git的分支是与众不同的,无论创建、切换和删除分支,Git在1秒钟之内就能完成!无论你的版本库是1个文件还是1万个文件。
创建与合并分支
在学习版本回退部分时,你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。
截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master分支。
HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。
一开始的时候,master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点:
每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长,
当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:
假如我们在dev上的工作完成了,就可以把dev合并到master上。Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:
所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支:
我们创建dev分支,然后切换到dev分支:
$ git checkout -b dev Switched to a new branch 'dev'
git checkout 命令加上 -b 参数表示创建并切换,相当于以下两条命令:
$ git branch dev $ git checkout dev Switched to branch 'dev'
然后,用 git branch 命令查看当前分支:
$ git branch * dev master
git branch 命令会列出所有分支,当前分支前面会标一个*
号。
然后,我们就可以在 dev 分支上正常提交,比如对readme.txt做个修改,加上一行:
Creating a new branch is quick.
然后提交:
$ git add readme.txt $ git commit -m "branch test" [dev fec145a] branch test 1 file changed, 1 insertion(+)
现在,dev分支的工作完成,我们就可以切换回master分支:
$ git checkout master Switched to branch 'master'
切换回master分支后,再查看一个readme.txt文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变:
现在,我们把dev分支的工作成果合并到master分支上:
$ git merge dev Updating d17efd8..fec145a Fast-forward readme.txt | 1 + 1 file changed, 1 insertion(+)
git merge 命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。
注意到上面的 Fast-forward 信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward,我们后面会讲其他方式的合并。
合并完成后,就可以放心地删除dev
分支了:
$ git branch -d dev Deleted branch dev (was fec145a).
删除后,查看branch,就只剩下master分支了:
$ git branch * master
因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。
解决冲突
人生不如意之事十之八九,合并分支往往也不是一帆风顺的。
准备新的feature1分支,继续我们的新分支开发:
$ git checkout -b feature1 Switched to a new branch 'feature1'
修改readme.txt最后一行,改为:
added this line from branch feature 1
在feature1分支上提交:
$ git add readme.txt $ git commit -m "add feature" [feature1 75a857c] AND simple 1 file changed, 1 insertion(+), 1 deletion(-)
切换到master分支:
$ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 1 commit.
Git还会自动提示我们当前master分支比远程的master分支要超前1个提交。
在master分支上把readme.txt文件的最后一行改为:
added this line from master
提交:
$ git add readme.txt $ git commit -m "master update" [master 400b400] & simple 1 file changed, 1 insertion(+), 1 deletion(-)
现在,master分支和feature1分支各自都分别有新的提交,变成了这样:
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:
$ 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告诉我们,readme.txt文件存在冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件:
$ git status # On branch master # Your branch is ahead of 'origin/master' by 2 commits. # # Unmerged paths: # (use "git add/rm <file>..." as appropriate to mark resolution) # # both modified: readme.txt # no changes added to commit (use "git add" and/or "git commit -a")
我们可以直接查看readme.txt的内容:
#git study repo Creating a new branch is quick. <<<<<<< HEAD added this line from master ======= added this line from branch feature 1 >>>>>>> feature1
Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容,我们修改如下后保存:
#git study repo Creating a new branch is quick. added this line from master added this line from branch feature 1
再提交
$ git add readme.txt $ git commit -m "conflict fixed" [master 59bc1cb] conflict fixed
现在,master分支和feature1分支变成了下图所示:
用带参数的git log也可以看到分支的合并情况:
$ git log --graph --pretty=oneline * feedd786cad3e18323a41846fcc1b0d52fc0c98e fix conflict |\ | * 01f8f8d168e113fac9fbe24c4cfa6d4c351a9821 update from branch * | 743ccee30f3d74f1993f17e7312032b7399b1306 from master |/ * edfbc29982927236596539e0f1971b0575f803c0 branch test * 8675486bfeeb340914369e80d2cfcf3e854e88a3 add home page
分支策略
实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;
你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。
所以,团队合作的分支看起来就像这样:
bug分支
软件开发中,bug就像家常便饭一样。有了bug就需要修复,
在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101来修复它,但是,等等,当前正在dev上进行的工作还没有提交:
并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?
幸好,Git还提供了一个 stash 功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:
现在,用git status查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。
首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支:
$ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 6 commits. $ git checkout -b issue-101 Switched to a new branch 'issue-101'
现在修复bug,需要把“Git is free software ...”改为“Git is a free software ...”,然后提交:
$ git add readme.txt $ git commit -m "fix bug 101" [issue-101 cc17032] fix bug 101 1 file changed, 1 insertion(+), 1 deletion(-)
修复完成后,切换到master分支,并完成合并,最后删除issue-101分支:
$ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 2 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(-) $ git branch -d issue-101 Deleted branch issue-101 (was cc17032).
太棒了,原计划两个小时的bug修复只花了5分钟!现在,是时候接着回到dev分支干活了!
工作区是干净的,刚才的工作现场存到哪去了?用 git stash list 命令看看:
工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:
方式一:是用 git stash apply 恢复,但是恢复后,stash内容并不删除,你需要用 git stash drop 来删除;
方式二:是用 git stash pop,恢复的同时把stash内容也删了:
再用git stash list查看,就看不到任何stash内容了:
你可以多次stash,恢复的时候,先用git stash list查看,然后恢复指定的stash,用命令:
$ git stash apply stash@{0}
多人协作
当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin。
要查看远程库的信息,用git remote:
$ git remote origin
或者,用git remote -v显示更详细的信息:
$ git remote -v origin git@github.com:triaquae/gitskills.git (fetch) origin git@github.com:triaquae/gitskills.git (push)
上面显示了可以抓取和推送的origin的地址。如果没有推送权限,就看不到push的地址。
推送分支
把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
$ git push origin master
如果要推送其他分支,比如dev,就改成:
$ git push origin dev
但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?
-
master分支是主分支,因此要时刻与远程同步;
-
dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
-
bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
-
feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!
抓取分支
多人协作时,大家都会往master和dev分支上推送各自的修改。
现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆:
$ git clone git@github.com:triaquae/gitskills.git Cloning into 'gitskills'... remote: Counting objects: 16, done. remote: Compressing objects: 100% (7/7), done. remote: Total 16 (delta 0), reused 10 (delta 0), pack-reused 0 Receiving objects: 100% (16/16), done. Checking connectivity... done.
当你的小伙伴从远程库clone时,默认情况下,你的小伙伴只能看到本地的master分支。不信可以用git branch命令看看:
$ git branch * master
现在,你的小伙伴要在dev分支上开发,就必须创建远程origin的dev分支到本地,于是他用这个命令创建本地dev分支:
$ git checkout -b dev origin/dev
现在,他就可以在dev
上继续修改,然后,时不时地把dev
分支push
到远程:
$ git add . $ git commit -m "small updates" [dev f1b762e] small updates 2 files changed, 5 insertions(+), 1 deletion(-) Alexs-MacBook-Pro:gitskills alex$ git push origin dev Counting objects: 4, done. Delta compression using up to 8 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (4/4), 438 bytes | 0 bytes/s, done. Total 4 (delta 0), reused 0 (delta 0) To git@github.com:triaquae/gitskills.git 33ec6b4..f1b762e dev -> dev
你的小伙伴已经向origin/dev分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:
$ git add . $ git commit -m "add Dog class" [dev 7e7b1bf] add Dog class 2 files changed, 7 insertions(+) $ git push origin dev To git@github.com:triaquae/gitskills.git ! [rejected] dev -> dev (fetch first) error: failed to push some refs to 'git@github.com:triaquae/gitskills.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已经提示我们,先用 git pull 把最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突,再推
$ git pull remote: Counting objects: 4, done. remote: Compressing objects: 100% (3/3), done. remote: Total 4 (delta 0), reused 4 (delta 0), pack-reused 0 Unpacking objects: 100% (4/4), done. From github.com:triaquae/gitskills 33ec6b4..f1b762e dev -> origin/dev 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 pull也失败了,原因是没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置dev和origin/dev的链接:
$ git branch --set-upstream-to=origin/dev dev Branch dev set up to track remote branch dev from origin.
再pull:
$ git pull Auto-merging hello.py CONFLICT (content): Merge conflict in hello.py Auto-merging branch_test.md CONFLICT (content): Merge conflict in branch_test.md Automatic merge failed; fix conflicts and then commit the result.
这回git pull成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push:
$ git add . $ git commit -m "merge & fix hello.py" [dev 93e28e3] merge & fix hello.py $ git push origin dev Counting objects: 8, done. Delta compression using up to 8 threads. Compressing objects: 100% (7/7), done. Writing objects: 100% (8/8), 819 bytes | 0 bytes/s, done. Total 8 (delta 1), reused 0 (delta 0) remote: Resolving deltas: 100% (1/1), done. To git@github.com:triaquae/gitskills.git f1b762e..93e28e3 dev -> dev
因此,多人协作的工作模式通常是这样:
-
首先,可以试图用git push origin branch-name推送自己的修改;
-
如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
-
如果合并有冲突,则解决冲突,并在本地提交;
-
没有冲突或者解决掉冲突后,再用git push origin branch-name推送就能成功!
如果git pull提示“no tracking information”,则说明本地分支和远程分支的链接关系没有创建,
用命令git branch --set-upstream branch-name origin/branch-name
这就是多人协作的工作模式,一旦熟悉了,就非常简单。
github使用
如何参与一个开源项目呢?
比如人气极高的bootstrap项目,这是一个非常强大的CSS框架,
你可以访问它的项目主页https://github.com/twbs/bootstrap,
点“Fork”就在自己的账号下克隆了一个bootstrap仓库,然后,从自己的账号下clone:
git clone git@github.com:michaelliao/bootstrap.git
一定要从自己的账号下clone仓库,这样你才能推送修改。
如果从bootstrap的作者的仓库地址git@github.com:twbs/bootstrap.git克隆,因为没有权限,你将不能推送修改。
Bootstrap的官方仓库twbs/bootstrap、你在GitHub上克隆的仓库my/bootstrap,以及你自己克隆到本地电脑的仓库,他们的关系就像下图显示的那样:
如果你想修复bootstrap的一个bug,或者新增一个功能,立刻就可以开始干活,干完后,往自己的仓库推送。
如果你希望bootstrap的官方库能接受你的修改,你就可以在GitHub上发起一个pull request。当然,对方是否接受你的pull request就不一定了。
小结
-
在GitHub上,可以任意Fork开源仓库;
-
自己拥有Fork后的仓库的读写权限;
-
可以推送pull request给官方仓库来贡献代码。
忽略特殊文件.gitignore
有些时候,你必须把某些文件放到Git工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次git status都会显示Untracked files ...,有强迫症的童鞋心里肯定不爽。
好在Git考虑到了大家的感受,这个问题解决起来也很简单,在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。
不需要从头写.gitignore文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore
忽略文件的原则是:
-
忽略操作系统自动生成的文件,比如缩略图等;
-
忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
-
忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。
举个例子:
假设你在Windows下进行Python开发,Windows会自动在有图片的目录下生成隐藏的缩略图文件,如果有自定义目录,目录下就会有Desktop.ini文件,因此你需要忽略Windows自动生成的垃圾文件:
# Windows: Thumbs.db ehthumbs.db Desktop.ini
然后,继续忽略Python编译产生的.pyc、.pyo、dist等文件或目录:
# Python: *.py[cod] *.so *.egg *.egg-info dist build
加上你自己定义的文件,最终得到一个完整的.gitignore文件,内容如下:
# Windows: Thumbs.db ehthumbs.db Desktop.ini # Python: *.py[cod] *.so *.egg *.egg-info dist build # My configurations: db.ini deploy_key_rsa
最后一步就是把.gitignore也提交到Git,就完成了!当然检验.gitignore的标准是git status命令是不是说working directory clean。
使用Windows的童鞋注意了,如果你在资源管理器里新建一个.gitignore文件,它会非常弱智地提示你必须输入文件名,但是在文本编辑器里“保存”或者“另存为”就可以把文件保存为.gitignore了。
有些时候,你想添加一个文件到Git,但发现添加不了,原因是这个文件被.gitignore忽略了:
$ git add App.class The following paths are ignored by one of your .gitignore files: App.class Use -f if you really want to add them.
如果你确实想添加该文件,可以用-f
强制添加到Git:
$ git add -f App.class
或者你发现,可能是.gitignore写得有问题,需要找出来到底哪个规则写错了,可以用git check-ignore命令检查:
$ git check-ignore -v App.class .gitignore:3:*.class App.class
Git会告诉我们,.gitignore的第3行规则忽略了该文件,于是我们就可以知道应该修订哪个规则。
小结
-
忽略某些文件时,需要编写.gitignore;
-
.gitignore文件本身要放到版本库里,并且可以对.gitignore做版本管理!