版本控制是一种记录若干文件内容变化,以便将来查阅特定版本修订情况的系统.
关于版本控制分为三种:本地版本控制系统,如rcs;集中化的版本控制系统,如CVS、SVN;分布式版本控制系统,如Git。
Git基础要点
Git和其它版本控制系统的主要差别在于:Git只关心文件数据的整体是否发生变化,而大多数其它系统则只关心文件内容的具体差异。
对于任何一个文件,在Git内都只有三种状态:已提交(committed)、已修改(modified)和已暂存(staged)。已提交表示该文件已经被安全地保存在本地数据库中了;已修改表示修改了某个文件,但还没有提交保存;已暂存表示把已修改的文件放在下次提交时要保存的清单中。
每个项目都有一个git目录,它是Git用来保存元数据和对象数据库的地方。该目录非常重要,每次克隆镜像仓库的时候,实际拷贝的就是这个目录里面的数据。
所谓的暂存区域只不过是个简单的文件,一般都放在git目录中。有时候人们会把这个文件叫做索引文件。
基本的Git工作流程:(1)、在工作目录中修改某些文件;(2)、对这些修改了的文件作快照,并保存到暂存区域;(3)、提交更新,将保存在暂存区域的文件快照转储到git目录中。
对于已安装的Git,第一个要配置的是你个人的用户名称和电子邮件地址。这两条配置很重要,每次Git提交时都会引用这两条信息,说明是谁提交了更新,所以会随更新内容一起被永久纳入历史记录:
$ git config --global user.name "Spring"
$ git config --global user.email Spring@163.com
如果用了--global选项,那么更改的配置文件就是位于你用户主目录的那个,以后你所有的项目都会默认使用这里配置的用户信息。如果要在某个特定的项目中使用其他名字或者电邮,只要去掉--global选项重新配置即可,新的设定保存在当前项目的./git/config文件里。
Git常用命令
有两种取得Git项目仓库的方法。
第一种是在现存的目录下,通过导入所有文件来创建新的Git仓库:(1)、从当前目录初始化:$ git init ,初始化后,在当前目录下会出现一个名为.git的目录,所有Git需要的数据和资源都存放在这个目录中;(2)、如果当前目录下有几个文件想要纳入版本控制,需要先用git add命令告诉Git开始对这些文件进行跟踪,然后提交:
$ git add README
$ git commit -m 'initial project version'
其中README文件是已经存在在当前目录中的。git add后可以接要跟踪的文件或目录的路径。如果是目录的话,就说明要递归跟踪所有该目录下的文件。
第二种是从已有的Git仓库克隆出一个新的镜像仓库来,如:
$ git clone git://github.com/schacon/grit.git
如果希望在克隆的时候,自己定义要新建的项目目录名称,可以在上面的命令最后指定:
$ git clone git://github.com/schacon/grit.git mygrit
唯一的差别就是,现在新建的目录成了mygrit,其它的都和上边的一样。
工作目录下面的所有文件都不外乎两种状态:已跟踪或未跟踪。已跟踪的文件是指本来就被纳入版本控制管理的文件,在上次快照中有它们的记录,工作一段时间后,它们的状态可能是未更新,已修改或者已放入暂存区。而所有其他文件都属于未跟踪文件。
检查当前文件状态:
$ git status
忽略某些文件:可以创建一个名为.gitignore的文件,列出要忽略的文件模式。
要查看尚未暂存的文件更新了哪些部分 ,不加参数直接输入:
$ git diff
单单git diff不过是显示还没有暂存起来的改动,而不是这次工作和上次提交之间的差异。
如要看已经暂存起来的文件和上次提交时的快照之间的差异,可以用:
$ git diff --cached
每次准备提交前,先用git status看下,是不是都已经暂存起来了,然后在运行提交命令:
$ git commit
这种方式会启动文本编辑器以便输入本次提交的说明,也可以使用-m参数后跟提交说明的方式:
$ git commit -m "commit message"
提交时记录的是放在暂存区域的快照,任何还未暂存的仍然保持已修改状态,可以在下次提交时纳入版本管理。每一次运行提交操作,都是对你项目作一次快照,以后可以回到这个状态,或者进行比较。
提交更新:现在的暂存区域已经准备妥当可以提交了。在此之前,请一定要确认还有什么修改过的或新建的文件还没有git add 过,否则提交的时候不会记录这些还没暂存起来的变化。所以,每次准备提交前,先用git status 看下,是不是都已暂存起来了,然后再运行提交命令git commit。
跳过使用暂存区域:只要在提交的时候,给git commit加上-a选项,Git就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过git add步骤。
移除文件:要从Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。可以用git rm 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中了:
$ git rm README
如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项”-f”,以防误删除文件后丢失修改的内容。
移动文件:要在Git中对文件改名:
$ git mv file_from file_to
其实,运行git mv就相当于运行了下面三条命令:
$ mv file_from file_to
$ git rm file_from
$ git add file_to
查看提交历史:可以使用
$ git log
默认不用任何参数的话,git log会按提交时间列出所有的更新,最近的更新排在最上面。
常用”-p”选项展开显示每次提交的内容差异,用-2则仅显示最近的两次更新。”--stat”仅显示简要的增改行数统计。”--pretty”选项,可以指定使用完全不同于默认格式的方式展开提交历史,比如用online将每个提交放在一行显示:
$ git log --pretty=oneline
另外还有short、full、fuller和format可以用
git log --pretty=format:"%h - %an, %ar : %s"
下面列出一些其它常用的选项及其释义:
“-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(后跟指定格式)。
“-(n)”: 仅显示最近的n 条提交
--since,--after: 仅显示指定时间之后的提交,$ git log --since=2.weeks
--until,--before: 仅显示指定时间之前的提交。
--author: 仅显示指定作者相关的提交。
--committer”仅显示指定提交者相关的提交。
使用图形化工具查阅提交历史:gitk,它是用Tcl/Tk写成的,基本上相当于git log命令的可视化版本,凡是git log可以用的选项也都能用在gitk上。在项目工作目录中输入
$ gitk
gitk命令后,就会启动gitk图形化工具,如果提示没有安装,则执行以下命令进行安装gitk:
$ sudo apt-get install gitk
撤销操作:有些操作并不总是可以撤销的,所以请务必谨慎小心,一旦失误,就有可能丢失部分工作成果。
修改最后一次提交:想要撤销刚才的提交操作,可以使用--amend选项重新提交:
$ git commit --amend
此命令将使用当前的暂存区域快照提交。如果刚才提交完没有作任何改动,直接运行此命令的话,相当于有机会重新编辑提交说明,而所提交的文件快照和之前的一样。启动文本编辑器后,会看到上次提交时的说明,编辑它确认没问题后保存退出,就会使用新的提交说明覆盖刚才失误的提交。如果刚才提交时忘了暂存某些修改,可以先补上暂存操作,然后再运行--amend 提交:
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
三条命令最终得到一个提交,第二个提交命令修正了第一个的提交内容。
取消已经暂存的文件:
$ git reset HEAD test.txt
取消对文件的修改:
$ git checkout -- test.txt
这条命令有些危险,所有对文件的修改都没有了。所以在用这条命令前,请务必确定真的不再需要保留刚才的修改。
远程仓库的使用:远程仓库是指托管在网络上的项目仓库。管理远程仓库的工作,包括添加远程库,移除废弃的远程库,管理各式远程库分支,定义是否跟踪这些分支等。
查看当前的远程库:要查看当前配置有哪些远程仓库,可以用
$ git remote
它会列出每个远程库的简短名字。在克隆完某个项目后,至少可以看到一个名为origin 的远程库,Git 默认使用这个名字来标识你所克隆的原始仓库。也可以加上”-v”选项,显示对应的克隆地址。
添加远程仓库:要添加一个新的远程仓库,可以指定一个简单的名字,以便将来引用,运行
$ git remote add [shortname] [url]
从远程仓库抓取数据:
$ git fetch [remote-name]
此命令会到远程仓库中拉取所有你本地仓库中还没有的数据。运行完成后,你就可以在本地访问该远程仓库中的所有分支,将其中某个分支合并到本地,或者只是取出某个分支。fetch 命令只是将远端的数据拉到本地仓库,并不自动合并到当前工作分支,只有当你确实准备好了,才能手工合并。如果设置了某个分支用于跟踪某个远端仓库的分支,可以使用
$ git pull
git pull 命令自动抓取数据下来,然后将远端分支自动合并到本地仓库中当前分支。
推送数据到远程仓库:
$ git push [remote-name] [branch-name]
只有在所克隆的服务器上有写权限,或者同一时刻没有其他人在推数据,这条命令才会如期完成任务。如果在你推数据前,已经有其他人推送了若干更新,那你的推送操作就会被驳回。你必须先把他们的更新抓取到本地,并到自己的项目中,然后才可以再次推送。
查看远程仓库信息:
$ git remote show [remote-name]
查看某个远程仓库的详细信息。
远程仓库的删除和重命名:可以用git remote rename命令修改某个远程仓库的简短名称,比如想把pb改成paul,可以这样运行:
$ git remote rename pb paul
对远程仓库的重命名,也会使对应的分支名称发生变化,原来的pb/master 分支现在成了paul/master。碰到远端仓库服务器迁移,或者原来的克隆镜像不再使用,又或者某个参与者不再贡献代码,那么需要移除对应的远端仓库,可以运行git remote rm 命令:
$ git remote rm paul
打标签:同大多数VCS 一样,Git 也可以对某一时间点上的版本打上标签。人们在发布某个软件版本(比如v1.0 等等)的时候,经常这么做。
列出已有的标签:
$ git tag
显示的标签按字母顺序排列,所以标签的先后并不表示重要程度的轻重。如果只对某系列的版本感兴趣,可以使用:
$ git tag -l 'v1.4.2.*'
新建标签:Git使用的标签有两种类型,轻量级的(lightweight)和含附注的(annotated)。轻量级标签就像是个不会变化的分支,实际上它就是个指向特定提交对象的引用。而含附注标签,实际上是存储在仓库中的一个独立对象,它有自身的校验和信息,包含着标签的名字,电子邮件地址和日期,以及标签说明,标签本身也允许使用GNU Privacy Guard (GPG) 来签署或验证。一般都建议使用含附注型的标签,以便保留相关信息。
含附注的标签:
$ git tag -a v1.0 -m 'my version 1.0'
而-m选项则指定了对应的标签说明,Git会将此说明一同保存在标签对象中。如果在此选项后没有给出具体的说明内容,Git会启动文本编辑软件供你输入。
可以使用git show命令查看相应标签的版本信息,并连同显示打标签时的提交对象:
$ git show v1.0
签署标签:如果你有自己的私钥,还可以用GPG来签署标签,只需要把之前的-a改为-s即可。
轻量级标签:轻量级标签实际上就是一个保存着对应提交对象的校验和信息的文件。要创建这样的标签,一个-a,-s 或-m 选项都不用,直接给出标签名字即可
$ git tag v2.0
验证标签:可以使用
$ git tag -v [tag-name]
的方式验证已经签署的标签。此命令会调用GPG来验证签名,所以你需要有签署者的公钥,存放在keyring中才能验证。
后期加注标签:可以在后期对早先的某次提交加注标签。只要在打标签的时候跟上对应提交对象的校验和(或前几位字符)即可:
$ git tag -a v1.3 d06e3de00
分享标签:默认情况下,git push并不会把标签传送到远端服务器上,只有通过显式命令才能分享标签到远端仓库,运行
$ git push origin [tagname]
即可。如果要一次推送所有(本地新增的)标签上去,可以使用--tags选项:
$ git push origin --tags
Git分支
Git中的分支,其实本质上仅仅是个指向commit 对象的可变指针。Git会使用master 作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的master 分支,它在每次提交的时候都会自动向前移动。
创建一个新的分支,即创建一个新的分支指针。比如创建一个testing分支,可以使用git branch命令:
$ git branch testing
HEAD特别指针,在Git中,它是一个指向你正在工作中的本地分支的指针。运行git branch 命令,仅仅是建立了一个新的分支,但不会自动切换到这个分支中去。要切换到其它分支,可以执行git checkout命令:
$ git checkout testing
这样HEAD就指向了testing分支。每次提交后HEAD随着分支一起向前移动。
$ git checkout -b testing
相当于:
$ git branch testing
$ git checkout testing
两条命令。
由于Git中的分支实际上仅是一个包含所指对象校验和(40个字符长度SHA-1字串)的文件,所以创建和销毁一个分支就变得非常廉价。说白了,新建一个分支就是向一个文件写入41 个字节(外加一个换行符)那么简单,当然也就很快了。转换分支的时候最好保持一个清洁的工作区域。Git会把工作目录的内容恢复为检出某分支时它所指向的那个commit 的快照。它会自动添加、删除和修改文件以确保目录的内容和你上次提交时完全一样。
分支合并:用git merge命令来进行合并,
$ git merge testing
删除一个分支:
$ git branch -d testing
冲突的合并:要看看哪些文件在合并时发生冲突,可以用
$ git status
查阅。任何包含未解决冲突的文件都会以未合并(unmerged)状态列出。Git会在有冲突的文件里加入标准的冲突解决标记,可以通过它们来手工定位并解决这些冲突。在解决了所有文件里的所有冲突后,运行
$ git add
将把它们标记为已解决(resolved)。因为一旦暂存,就表示冲突已经解决。最后再用
$ git commit
来完成这次合并提交。
如果想用一个有图形界面的工具来解决冲突,可以运行
$ git mergetool
它会调用一个可视化的合并工具并引导你解决所有冲突。
分支管理:git branch命令不仅仅能创建和删除分支,如果不加任何参数,它会给出当前所有分支的清单。若要查看各个分支最后一次commit的信息,运行,
$ git branch -v
要从该清单中筛选出你已经(或尚未)与当前分支合并的分支,可以用--merge 和--no-merged选项。比如,
$ git branch --merge
查看哪些分支已被并入当前分支。清单中带有”*”字符的表示当前所在的分支。一般来说,列表中没有”*”的分支通常都可以用git branch -d来删掉。对于未合并的分支,用git branch -d删除该分支会导致失败。不过,如果你坚信你要删除它,可以用大写的删除选项-D 强制执行,例如,
$ git branch -D testing
分支式工作流程:长期分支、特性分支
特性分支:在任何规模的项目中都可以使用特性(Topic)分支。一个特性分支是指一个短期的,用来实现单一特性或与其相关工作的分支。
请务必牢记这些分支全部都是本地分支,这一点很重要。当你在使用分支及合并的时候,一切都是在你自己的Git仓库中进行----完全不涉及与服务器的交互。
远程分支(remote branch):是对远程仓库状态的索引。它们是一些无法移动的本地分支;只有在进行Git的网络活动时才会更新。远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置。
推送:要想和其他人分享某个分支,你需要把它推送到一个你拥有写权限的远程仓库。你的本地分支不会被自动同步到你引入的远程分支中,除非你明确执行推送操作。换句话说,对于无意分享的,你尽可以保留为私人分支,而只推送那些协同工作的特性分支。可以运行
$ git push (远程仓库名) (分支名)
跟踪分支:从远程分支检出的本地分支,称为跟踪分支(tracking branch)。跟踪分支是一种和远程分支有直接联系的本地分支。在跟踪分支里输入
$ git push
Git会自行推断应该向哪个服务器的哪个分支推送数据。反过来,在这些分支里运行
$ git pull
会获取所有远程索引,并把它们的数据都合并到本地分支中来。
在克隆仓库时,Git通常会自动创建一个master分支来跟踪origin/master。这正是git push和git pull一开始就能正常工作的原因。当然,你可以随心所欲地设定为其它跟踪分支,比如origin 上除了master 之外的其它分支,
$ git checkout -b [分支名] [远程名]/[分支名]
删除远程分支:如果不再需要某个远程分支了,比如搞定了某个特性并把它合并进了远程的master 分支(或任何其他存放稳定代码的地方),可以用这个非常无厘头的语法来删除它:
$ git push [远程名]:[分支名]
衍合:把一个分支整合到另一个分支的办法有两种:merge(合并)和rebase(衍合)。
有了rebase命令,就可以把在一个分支里提交的改变在另一个分支里重放一遍。如,
$ git checkout testing
$ git rebase master
它的原理是回到两个分支(你所在的分支和你想要衍合进去的分支)的共同祖先,提取你所在分支每次提交时产生的差异(diff),把这些差异分别保存到临时文件里,然后从当前分支转换到你需要衍合入的分支,依序使用每一个差异补丁文件。衍合能产生一个更为整洁的提交历史。如果视察一个衍合过的分支的历史记录,看起来更清楚,仿佛所有修改都是先后进行的,尽管实际上它们原来是同时发生的。现在可以回到master分支然后进行一次快进合并:
$ git checkout master
$ git merge testing
$ git rebase master testing
命令会先检出特性分支testing,然后在主分支master上重演,然后快进主分支master,
$ git checkout master
$ git merge testing
永远不要衍合那些已经推送到公共仓库的更新。在衍合的时候,实际上抛弃了一些现存的commit 而创造了一些类似但不同的新commit。如果你把commit 推送到某处然后其他人下载并在其基础上工作,然后你用git rebase 重写了这些commit 再推送一次,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容的时候事情就会变得一团糟。如果把衍合当成一种在推送之前清理提交历史的手段,而且仅仅衍合那些永远不会公开的commit,那就不会有任何问题。如果衍合那些已经公开的commit,而与此同时其他人已经用这些commit 进行了后续的开发工作,那就麻烦了。
服务器上的Git
建立一个大家都可以访问的共享仓库,从那里推送和拉取数据。我们将把这个仓库称为“Git 服务器”。远程仓库通常只是一个纯仓库(bare repository)----一个没有当前工作目录的仓库。因为该仓库只是一个合作媒介,所以不需要从一个处于已从硬盘上检出状态的快照;仓库里仅仅是Git 的数据。简单的说,纯仓库是你项目里.git 目录的内容,别无他物。
协议:Git可以使用四种主要的协议来传输数据:本地传输,SSH协议,Git协议和HTTP 协议。
在服务器部署Git:开始架设Git 服务器的时候,需要把一个现存的仓库导出为新的纯仓库----不包含当前工作目录的仓库。方法非常直截了当。把一个仓库克隆为纯仓库,可以使用clone 命令的--bare 选项。纯仓库的目录名以.git 结尾,如
$ git clone -bare my_project my_project.git
现在在my_project.git中已经有了一份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 init 命令的时候加上--shared 选项,Git 会自动对该仓库加入可写的组:
$ ssh user@git.example.com
$ cd /opt/git/my_project.git
$ git init --bare --shared
现在已经可以开始在同一个项目上密切合作了。
Git托管服务:可供选择的托管服务数量繁多,如GitHub。
分布式Git
分布式工作流程:同传统的集中式版本控制系统(CVCS)不同,开发者之间的协作方式因Git 的分布式特性而变得更为灵活多样。在集中式系统上,每个开发者就像是连接在集线器上的节点,彼此的工作方式大体相像。而在Git 网络中,每个开发者同时扮演着节点和集线器的角色,这就是说,每一个开发者都可以将自己的代码贡献到另外一个开发者的仓库中,或者建立自己的公共仓库,让其他开发者基于自己的工作开始,为自己的仓库贡献代码。于是,Git 的分布式协作便可以衍生出种种不同的工作流程。
以上内容摘自于《Pro Git》
实际工作中常用命令汇总:
1. 查看远程分支:$ git branch -a
2. 查看本地分支:$ git branch
3. 切换分支:$ git checkout 分支名
4. 更新到指定版本:$ git reset --hard 39c9064fe34a1331e3624fbbef33a3f66df8f6ba
5. 更新到上个版本:$ git reset --hard HEAD^
6. 将远程分支下载到本地:$ git checkout -b branch1 origin/branch1
7. 创建新分支,并立即切换到此新分支:$ git checkout -b branchname
8. 从远程分支下载指定的分支:$ git pull origin branchname
9. 查看某个文件作了哪些修改:$ git diff src/a.cpp
10. 在合并进仓库之前,修改上次的提交:$ git commit --amend
11. 撤销为提交的修改:$ git checkout -- a.cpp
12. 撤销当前目录下所有文件的修改(慎用):$ git checkout .
13. 查看某个文件详细的修改信息:$ git blame src/a.cpp
14. 删除本地所有未提交的新增的文件(如果文件已经被staged不会被回退):$ git clean -df
15. 撤销本地所有未提交的更改(包括已经被staged的文件,但不包括新增的文件):$ git reset --hard
16. 修改指定文件的名字,如将a.cpp重命名为b.cpp:$ git mv a.cpp b.cpp
17. 删除本地仓库中指定的文件src/a.cpp:$ git rm src/a.cpp
18. 删除本地仓库中指定的目录src/aa:$ git rm -rf src/aa
19. 假如之前基于master新建了一个分支dev,但是后来master有了多于一次的提交,但是dev一直没有改动,假如现在想把master的提交更新到dev分支上,因为dev分支一直没有改动过,假如master提交了两次,除了手动调整改动的文件外还可以这样操作,当前在dev分支:$ git merge master , $ git reset HEAD^^,因为master多于一次的提交,直接git merge master,在git push是不行的
20. 如果一个项目的提交记录太多,直接使用git log并不太方便查看提交历史,此时可通过tree的方式输出,执行如下命令后,每次查看时终端执行 $ git lg 即可
git config --global alias.lg "log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all"
21. 本地执行git branch -a查看远程分支时出现了一些实际远程仓库中已不存在的分支,此时可在本地执行:git pull --prune命令清理远程仓库中已不存在的分支
22. 从另一个分支dev拷贝文件funset.hpp或目录到现在的分支master,则执行:$ git checkout dev funset.hpp ,执行后funset.hpp在master分支中默认是暂存状态;若取消暂存,可执行: $ get reset HEAD
参考文献:
1. http://git-scm.com/book/zh/v1
2. http://gitref.org/zh/index.html