Revision Control Tools

If you have any questions abount this article, or if you have found any errors in this article, email to me, please. This is my email address: zrg1390556486@gmail.com

1. Git vs SVN

  • Git是分布式的,SVN不是;
  • Git把内容按元数据方式存储,而SVN是按文件;
  • Git分支与SVN的分支不同;
  • Git没有一个全局的版本号,而SVN有;
  • Git的内容完整性要优于SVN:Git内容存储使用的是SHA-1算法。。

2. Get Started

2.1. Installing

  • Ubuntu/Debian

    $ sudo apt-get install git
    $ sudo apt-get install git-doc git-svn git-email gitk
    
  • RHEL/Fedora/CentOS

    $ yum install git
    $ yum install git-svn git-email gitk
    
  • MacOS

    $ brew install git
    

2.2. Repository

2.2.1. Repository Config

  • Set username and email

    $ git config --global user.name "your name"
    $ git config --global user.email "your_email@youremail.com"
    
    // unset
    $ git config --unset --global user.name
    $ git config --unset --global user.email
    
  • Support chinese

    //UTF-8:
    $ git config --global core.quotepath false
    //GBK:
    $ git config --global i18n.logOutputEncoding gbk
    //录入提交说明的字符集:
    $ git config --global i18n.commitEncoding gbk
    
  • Show Config List

    $ git config -L
    

2.2.2. Repository Init

  1. 初始化一个新仓库

    $ echo "# This is a new repository" > README.md
    $ git init
    $ git add README.md
    $ git commit -m "first commit"
    
  2. 推送到远端仓库

    // 将创建的仓库推送到远端仓库
    $ git remote add origin git@github.com:xxx/repo-name.git
    $ git push -u origin master
    
    // 将创建的仓库推送到已存在的远端仓库
    $ git remote add origin git@github.com:xxx/repo-name.git
    $ git push -u origin master
    
  3. 检出仓库

    // 仓库在本地
    $ git clone /path/to/repository
    // 仓库在远端
    $ git clone username@host:/path/to/repository
    $ git clone git@github.com:xxx/hello_world.git //GitHub
    // 拉取最新内容
    $ git pull
    $ git pull orgion master
    
  4. 删除仓库中的文件

    // 删除本地仓库文件
    $ rm test.php
    // 从Git仓库中删除文件
    $ git rm test.php
    
  5. 放弃本地所有的修改

    $ git checkout . && git clean -xdf
    
  6. 彩色的 git 输出

    $ git config color.ui true
    
  7. 显示历史记录时,每个提交的信息只显示一行:

    $ git config format.pretty oneline
    

2.2.3. Flow

本地仓库由 git 维护的三棵“树”组成。

  • 第一个是你的工作目录,它持有实际文件;
  • 第二个是 缓存区(Index),它像个缓存区域,临时保存你的改动;
  • 最后是 HEAD,指向你最近一次提交后的结果。

commit1.png

Figure 1: 提交流程

演示

  1. 假如我新增了某个功能,现在把它提交到暂存区。

    //单独提交某个文件
    $ git add <filename>
    //提交所有
    $ git add -A
    $ git add .
    
  2. 额……突然想起还有某个地方要改动一下,撤销提交到暂存区的代码。

    // 撤销提交到暂存区的所有文件
    $ git reset
    $ git reset HEAD .
    // 撤销提交到暂存区的某个文件
    $ git reset HEAD --filename
    
  3. 修改完遗漏功能后,再次提交了代码到暂存区,再提交到本地仓库分支。

    $ git commit -m "Description"
    //直接跳过暂存区提交
    $ git commit -a -m "Description"
    
  4. 经过上一步操作后,又想起刚才提交到本地仓库分支的代码需要进一步优化,所以只能撤消 commit。

    //查看提交日志
    $ git log
    //执行撤销到上一个版本
    $ git reset --soft HEAD^
    $ git reset --soft HEAD~1
    //参数解释:
    --mixed: 不删除工作空间改动代码,撤销commit,并且撤销git add . 操作
    --soft: 不删除工作空间改动代码,撤销commit,不撤销git add . 
    --hard: 删除工作空间改动代码,撤销commit,撤销git add . 
    注意:如果 commit 注释写错了,只是想改一下注释,只需要执行以下命令,注释写完后保存就 OK 了。
    $ git commit --amend
    
  5. 完成测试后,现在推送到远端仓库

    $ git push
    $ git push origin master //master为分支名称
    
  6. 发现好几次提交改动都不大,压缩提交历史

    $ git rebase -i
    $ git rebase -i HEAD~2 //在历史记录中合并为一次完美的提交
    
  7. 从远端仓库 pull 来替换本地改动

    // 有的时候,本地文件不小心被删除或者内容被修改
    $ git checkout file 
    
    // 丢弃本地的所有改动与提交,可以到服务器上获取最新的版本历史,并将你本地主分支指向它:
    $ git fetch origin
    $ git reset --hard origin/master
    

2.3. Branch

2.3.1. 理解分支

branches.png

Figure 2: 理解分支

  1. 在 Git 中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。
  2. 假设在工作目录中有三个文件,准备将它们暂存后提交。暂存操作会对每一个文件计算校验和(即第一章中提到的 SHA-1 哈希字串),然后把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 类型的对象存储这些快照),并将校验和加入暂存区域:

    $ git add README test.rb LICENSE
    $ git commit -m 'initial commit of my project'
    
  3. 现在,Git 仓库中有五个对象:三个表示文件快照内容的 blob 对象;一个记录着目录树内容及其中各个文件对应 blob 对象索引的 tree 对象;一个包含指向 tree 对象(根目录)的索引和其他提交信息元数据的 commit 对象。

    git-branch01.png

    Figure 3: 单个提交对象在仓库中的数据结构

  4. 作些修改后再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针(译注:即下图中的 parent 对象)。两次提交后,仓库历史会变成下图的样子:

    git-branch02.png

    Figure 4: 多个提交对象之间的链接关系

  5. Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。

    git-branch03.png

    Figure 5: 分支其实就是从某个提交对象往回看的历史

  6. 那么,Git 又是如何创建一个新的分支的呢?比如新建一个 test 分支,可以使用 git branch 命令:

    $ git branch test
    

    这会在当前 commit 对象上新建一个分支指针,如图:

    git-branch04.png

    Figure 6: 多个分支指向提交数据的历史

  7. 那么,Git 是如何知道你当前在哪个分支上工作的呢?它保存着一个名为 HEAD 的特别指针。在 Git 中,它是一个指向你正在工作中的本地分支的指针(译注:将 HEAD 想象为当前分支的别名)。 运行git branch 命令,仅仅是建立了一个新的分支,但不会自动切换到这个分支中去,所以,我们依然还在 master 分支里,如图:

    git-branch05.png

    Figure 7: HEAD 指向当前所在的分支

  8. 要切换到其他分支,可以执行 git checkout 命令。切换到新建的 testing 分支:

    $ git checkout testing
    

    这样 HEAD 就指向了 testing 分支:

    git-branch06.png

    Figure 8: HEAD 在你转换分支时指向新的分支

  9. 不妨再提交一次:

    $ git commit -a -m 'made a change'
    

    提交后的结果:

    git-branch07.png

    Figure 9: 每次提交后 HEAD 随着分支一起向前移动

  10. 回到 master 分支看看:

    $ git checkout master
    

    git-branch08.png 这条命令做了两件事。它把 HEAD 指针移回到 master 分支,并把工作目录中的文件换成了 master 分支所指向的快照内容。
    也就是说,现在开始所做的改动,将始于本项目中一个较老的版本。它的主要作用是将 testing 分支里作出的修改暂时取消,这样你就可以向另一个方向进行开发。

  11. 作些修改后再次提交:

    $ git commit -a -m 'made other changes'
    

    git-branch09.png

    Figure 10: 不同流向的分支历史

  12. 由于 Git 中的分支实际上仅是一个包含所指对象校验和(40 个字符长度 SHA-1 字串)的文件,所以创建和销毁一个分支就变得非常廉价。
    这和大多数版本控制系统形成了鲜明对比,它们管理分支大多采取备份所有项目文件到特定目录的方式,所以根据项目文件数量和大小不同,可能花费的时间也会有相当大的差别,快则几秒,慢则数分钟。而 Git 的实现与项目复杂度无关,它永远可以在几毫秒的时间内完成分支的创建和切换。同时,因为每次提交时都记录了祖先信息(译注:即parent 对象),将来要合并分支时,寻找恰当的合并基础(译注:即共同祖先)的工作其实已经自然而然地摆在那里了,所以实现起来非常容易。Git 鼓励开发者频繁使用分支,正是因为有着这些特性作保障。

2.3.2. 分支的管理

  1. 创建分支与切换分支

    $ git branch branchName
    
    // 创建并切换分支
    $ git checkout -b branchName
    

    举例说明
    实际工作中大体也会用到这样的工作流程:正在开发某个新的需求,创建了一个分支;正在这个分支上开展工作。
    突然,接到一个电话说有个很严重的问题需要紧急修补,那么可以按照下面的方式处理:

    • 第一步,返回到原先已经发布到生产服务器上的分支。
    • 第二步,为这次紧急修补建立一个新分支,并在其中修复问题。
    • 第三步, 通过测试后,回到生产服务器所在的分支,将修补分支合并进来,然后再推送到生产服务器上。
    • 第四步,切换到之前实现新需求的分支,继续工作。


    A.首先,我们假设你正在项目中愉快地工作,并且已经提交了几次更新:

    git-branch10.png

    Figure 11: 一个简短的提交历史


    B.现在,你决定要修补问题追踪系统上的 #53 问题。(这里为了说明要解决的问题,才把新建的分支取名为 iss53。)

    $ git checkout -b iss53
    //这相当于执行下面这两条命令:
    $ git branch iss53
    $ git checkout iss53
    该命令执行结果:
    

    git-branch11.png

    Figure 12: 创建了一个新分支(专门解决53问题)的指针

    在提交了若干次更新后,iss53 分支的指针也会随着向前推进。
    

    git-branch12.png

    Figure 13: iss53 分支随工作进展向前推进


    C.现在你就接到了那个网站问题的紧急电话,需要马上修补。

    此时,确定你已经提交了所有的修改,接下来切换到 master 分支:
    $ git checkout master
    切换回主分支后,工作目录中的内容和你在解决问题 #53 之前一模一样,你可以集中精力进行紧急修补。
    
    特别注意:Git 会把工作目录的内容恢复为检出某分支时它所指向的那个提交对象的快照。它会自动添加、删除和修改文件以确保目录的内容和你当时提交时完全一样。
    
    创建一个紧急修补分支 hotfix 来开展工作,直到搞定:
    $ git checkout -b 'hotfix'
    

    git-branch13.png

    Figure 14: hotfix 分支是从 master 分支所在点分化出来的


    D.测试,确保修补是成功的。然后回到 master 分支并把它合并进来,然后发布到生产服务器。用 git merge 命令来进行合并:

    $ git checkout master
    $ git merge hotfix
    Updating f42c576..3a0874c
    Fast forward
    README |    1 -
    1 files changed, 0 insertions(+), 1 deletions(-)
    
    请注意,合并时出现了“Fast forward”的提示。由于当前 master 分支所在的提交对象是要并入的 hotfix 分支的直接上游,Git 只需把master 分支指针直接右移。
    换句话说,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。
    

    git-branch14.png

    Figure 15: 合并之后,master 分支和 hotfix 分支指向同一位置


    E.在那个超级重要的修补发布以后,你想要回到被打扰之前的工作。

    由于当前 hotfix 分支和 master 都指向相同的commit,所以 hotfix 已经完成了使命,可以删掉了:
    $ git branch -d <BranchName>
    删除远程分支(原理是把一个空分支push到server上,相当于删除该分支。)
    $ git push origin :<BranchName>
    


    F.现在回到之前未完成的 #53 问题修复分支上继续工作

    $ git checkout iss53
    

    git-branch15.png

    Figure 16: iss53 分支可以不受影响继续推进

  2. 查看分支

    // 如果不加任何参数,它会给出当前所有分支的清单:
    $ git branch
    
    //查看各个分支最后一个提交对象的信息
    $ git branch -v
    
  3. 删除分支

    $ git branch -d branchName
    
  4. 修改分支名称

    // 本地分支重命名
    $ git branch -m oldName newName
    
    // 远程分支重命名
    $ git branch -m oldName newName
    $ git push --delete origin oldName
    $ git push origin newName
    $ git branch --set-upstream-to origin/newName
    
  5. 筛选已合并的分支

    要从该清单中筛选出你已经(或尚未)与当前分支合并的分支,可以用 --merged 和 --no-merged 选项
    $ git branch --merged
    iss53
    *master
    // 列表中没有 * 的分支通常都可以用 git branch -d 来删掉。原因很简单,既然已经把它们所包含的工作整合到了其他分支,删掉也不会损失什么。
    
    // 查看尚未合并到当前分支的分支
    $ git branch --no-merged
    // 这样就显示还未合并进来的分支列表,如果此时用git branch -d 删除该分支会提示错误,因为那样做会丢失数据:
    
    $ git branch -d testing
    error: The branch 'testing' is not an ancestor of your current HEAD.
    If you are sure you want to delete it, run 'git branch -D testing'.
    // 当然,你也可以用大写 -D 强制执行。
    

2.3.3. 分支的合并

  1. 在问题 #53 相关的工作完成之后,可以合并回 master 分支。

    $ git checkout master
    $ git merge iss53
    

    请注意,这次合并操作的底层实现,并不同于之前 hotfix 的并入方式。如下图所示。
    由于当前 master 分支所指向的提交对象(C4)并不是 iss53 分支的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次简单的三方合并计算。

    git-branch16.png

    Figure 17: Git 为分支合并自动识别出最佳的同源合并点

    Git 没有简单地把分支指针右移,而是对三方合并后的结果重新做一个新的快照,并自动创建一个指向它的提交对象(C6),见下图所示。

    git-branch17.png

    Figure 18: Git 自动创建了一个包含了合并结果的提交对象

  2. 既然之前的工作成果已经合并到 master 了,那么 iss53 也就没用了。你可以就此删除它,并在问题追踪系统里关闭该问题。

    $ git branch -d iss53
    
  3. 遇到冲突时的分支合并
    有时候合并操作并不会如此顺利。如果在不同的分支中都修改了同一个文件的同一部分,Git 就无法干净地把两者合到一起(译注:逻辑上说,这种问题只能由人来裁决)。
    如果你在解决问题 #53 的过程中修改了hotfix 中修改的部分,将得到类似下面的结果:

    $ git merge iss53
    Auto-merging index.html
    CONFLICT (content): Merge conflict in index.html
    Automatic merge failed; fix conflicts and then commit the result.
    

    Git 作了合并,但没有提交,它会停下来等你解决冲突。要看看哪些文件在合并时发生冲突,可以用 git status 查阅:

    $ git status
    index.html: needs merge
    # On branch master
    # Changed but not updated:
    #   (use "git add 
    ..." to update what will be committed)
    #  (use "git checkout -- 
    ..." to discard changes in working directory)
    #
    #unmerged:   index.html
    

    任何包含未解决冲突的文件都会以未合并(unmerged)的状态列出。Git 会在有冲突的文件里加入标准的冲突解决标记,可以通过它们来手工定位并解决这些冲突。可以看到此文件包含类似下面这样的部分:

    <<<<<<< HEAD:index.html
    contact : email.support@github.com
    =======
    please contact us at support@github.com
    >>>>>>> iss53:index.html
    

    可以看到 ===== 隔开的上半部分,是 HEAD(即 master 分支,在运行merge 命令时所切换到的分支)中的内容,下半部分是在 iss53 分支中的内容。解决冲突的办法:手动合并;利用合并工具自动合并。自动合并,可以利用有图形界面的工具来解决,运行:

    $ git mergetool
    merge tool candidates: kdiff3 tkdiff xxdiff meld gvimdiff opendiff emerge vimdiff
    Merging the files: index.html
    Normal merge conflict for 'index.html':
    {local}: modified
    {remote}: modified
    Hit return to start merge resolution tool (opendiff):
    

    不想用默认的合并工具,可以在上方”merge tool candidates”里找到可用的合并工具列表,输入你想用的工具名。再运行一次 git status 来确认所有冲突都已解决:

    $ git status
    

    如果确认所有冲突都已解决,也就是进入了暂存区,就可以用 git commit 来完成这次合并提交。提交的记录注释差不多是这样:

    Merge branch 'iss53'
    Conflicts:
    index.html
    #
    # It looks like you may be committing a MERGE.
    # If this is not correct, please remove the file
    # .git/MERGE_HEAD
    # and try again.
    #
    

    如果想给将来看这次合并的人一些方便,可以修改该信息,提供更多合并细节。

2.3.4. 分支的衍合

把一个分支整合到另一个分支的办法有两种:merge 和 rebase(译注:rebase 的翻译暂定为“衍合”)。

  1. 基本的衍合操作
    回顾之前有关合并的章节,开发进程分叉到两个不同分支,又各自提交了更新。

    git-branch27.png

    Figure 19: 最初分叉的提交历史

    通过合并一个分支来整合分叉了的历史

    git-branch28.png

    Figure 20: 通过合并一个分支来整合分叉了的历史

    其实,还有另外一个选择:你可以把在 C3 里产生的变化补丁在 C4 的基础上重新打一遍。在 Git 里,这种操作叫做_衍合(rebase)。

    $ git checkout experiment
    $ git rebase master
    

    原理:回到两个分支最近的共同祖先,根据当前分支(也就是要进行衍合的分支 experiment)后续的历次提交对象(这里只有一个 C3),生成一系列文件补丁,然后以基底分支(也就是主干分支master)最后一个提交对象(C4)为新的出发点,逐个应用之前准备好的补丁文件,最后会生成一个新的合并提交对象(C3’),从而改写 experiment 的提交历史,使它成为 master 分支的直接下游,如图所示:

    git-branch29.png

    Figure 21: 把 C3 里产生的改变到 C4 上重演一遍

    现在回到 master 分支,进行一次快进合并

    git-branch30.png

    Figure 22: master 分支的快进

    一般我们使用衍合的目的,是想要得到一个能在远程分支上干净应用的补丁 — 比如某些项目你不是维护者,但想帮点忙的话,最好用衍合:先在自己的一个分支里进行开发,当准备向主项目提交补丁的时候,根据最新的origin/master 进行一次衍合操作然后再提交,这样维护者就不需要做任何整合工作(译注:实际上是把解决分支补丁同最新主干代码之间冲突的责任,化转为由提交补丁的人来解决。),只需根据你提供的仓库地址作一次快进合并,或者直接采纳你提交的补丁。


    请注意,合并结果中最后一次提交所指向的快照,无论是通过衍合,还是三方合并,都会得到相同的快照内容,只不过提交历史不同罢了。
    衍合是按照每行的修改次序重演一遍修改,而合并是把最终结果合在一起。

  2. 衍合举例
    衍合也可以放到其他分支进行,并不一定非得根据分化之前的分支。以下图为例,给服务器端代码添加一些功能而创建了特性分支 server,然后提交 C3 和 C4。然后又从 C3 的地方再增加一个client 分支来对客户端代码进行一些相应修改,所以提交了 C8 和 C9。最后,又回到 server 分支提交了 C10。

    git-branch31.png

    Figure 23: 从一个特性分支里再分出一个特性分支的历史

    假设在接下来的一次软件发布中,我们决定先把客户端的修改并到主线中,而暂缓并入服务端软件的修改(因为还需要进一步测试)。把基于 server 分支而非 master 分支的改变(即 C8 和 C9),跳过 server 直接放到master 分支中重演一遍,但这需要用git rebase 的 –onto 选项指定新的基底分支master:

    $ git rebase --onto master server client
    // 这好比在说:“取出 client 分支,找出 client 分支和 server 分支的共同祖先之后的变化,然后把它们在master 上重演一遍”。
    

    git-branch32.png

    Figure 24: 将特性分支上的另一个特性分支衍合到其他分支

    // 现在可以快进 master 分支了
    $ git checkout master
    $ git merge client
    

    git-branch33.png

    Figure 25: 快进 master 分支,使之包含 client 分支的变化

    // 现在我们决定把 server 分支的变化也包含进来。
    $ git rebase master server
    于是,server 的进度应用到 master 的基础上:
    

    git-branch34.png

    Figure 26: 在 master 分支上衍合 server 分支

    // 然后就可以快进主干分支 master 了:
    $ git checkout master
    $ git merge server
    
    // 现在 client 和 server 分支的变化都已经集成到主干分支来了,可以删掉它们了。
    $ git branch -d client
    $ git branch -d server
    

    git-branch35.png

    Figure 27: 最终的提交历史

  3. 衍合的风险
    奇妙的衍合也并非完美无缺,要用它得遵守一条准则:一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行衍合操作。用一个实际例子来说明为什么公开的衍合会带来问题。假设你从一个中央服务器克隆然后在它的基础上搞了一些开发,提交历史类似下图所示:

    git-branch36.png

    Figure 28: 克隆一个仓库,在其基础上工作一番

    现在,某人在 C1 的基础上做了些改变,并合并他自己的分支得到结果 C6,推送到中央服务器。当你抓取并合并这些数据到你本地的开发分支中后,会得到合并结果 C7,历史提交会变成:

    git-branch37.png

    Figure 29: 抓取他人提交,并入自己主干

    接下来,那个推送 C6 上来的人决定用衍合取代之前的合并操作;继而又用 git push –force 覆盖了服务器上的历史,得到 C4’。而之后当你再从服务器上下载最新提交后,会得到:

    git-branch38.png

    Figure 30: 有人推送了衍合后得到的 C4’,丢弃了你作为开发基础的 C4 和 C6

    下载更新后需要合并,但此时衍合产生的提交对象 C4’ 的 SHA-1 校验值和之前 C4 完全不同,所以 Git 会把它们当作新的提交对象处理,而实际上此刻你的提交历史 C7 中早已经包含了 C4 的修改内容,于是合并操作会把 C7 和 C4’ 合并为 C8

    git-branch39.png

    Figure 31: 你把相同的内容又合并了一遍,生成一个新的提交 C8

    C8 这一步的合并是迟早会发生的,因为只有这样你才能和其他协作者提交的内容保持同步。而在 C8 之后,你的提交历史里就会同时包含 C4 和 C4’,两者有着不同的 SHA-1 校验值,如果用git log 查看历史,会看到两个提交拥有相同的作者日期与说明,令人费解。而更糟的是,当你把这样的历史推送到服务器后,会再次把这些衍合后的提交引入到中央服务 器,进一步困扰其他人(译注:这个例子中,出问题的责任方是那个发布了 C6 后又用衍合发布 C4’ 的人,其他人会因此反馈双重历史到共享主干,从而混淆大家的视听。)。

2.3.5. 实际开发工作流程

  • 长期分支
    由于 Git 使用简单的三方合并,所以就算在较长一段时间内,反复多次把某个分支合并到另一分支,也不是什么难事。也就是说,你可以同时拥有多个开放的分支,每个分支用于完成特定的任务,随着开发的推进,你可以随时把某个特性分支的成果并到其他分支中。
    许多使用 Git 的开发者都喜欢用这种方式来开展工作,比如仅在 master 分支中保留完全稳定的代码,即已经发布或即将发布的代码。与此同时,他们还有一个名为develop 或 next 的平行分支,专门用于后续的开发,或仅用于稳定性测试 — 当然并不是说一定要绝对稳定,不过一旦进入某种稳定状态,便可以把它合并到master 里。这样,在确保这些已完成的特性分支(短期分支,比如之前的 iss53 分支)能够通过所有测试,并且不会引入更多错误之后,就可以并到主干分支中,等待下一次的发布。
    本质上我们刚才谈论的,是随着提交对象不断右移的指针。稳定分支的指针总是在提交历史中落后一大截,而前沿分支总是比较靠前。

    git-branch18.png

    Figure 32: 稳定分支总是比较老旧

    git-branch19.png

    Figure 33: 想象成流水线可能会容易点

  • 特性分支
    特性分支是指一个短期的,用来实现单一特性或与其相关工作的分支。
    在 Git 中,一天之内建立、使用、合并再删除多个分支是常见的事。一个实际的例子:

    git-branch20.png 由下往上,起先我们在 master 工作到 C1,然后开始一个新分支 iss91 尝试修复 91 号缺陷,提交到 C6 的时候,又冒出一个解决该问题的新办法,于是从之前 C4 的地方又分出一个分支iss91v2,干到 C8 的时候,又回到主干 master 中提交了 C9 和 C10,再回到 iss91v2 继续工作,提交 C11,接着,又冒出个不太确定的想法,从 master 的最新提交 C10 处开了个新的分支dumbidea 做些试验。
    现在,假定两件事情:我们最终决定使用第二个解决方案,即 iss91v2 中的办法;另外,我们把 dumbidea 分支拿给同事们看了以后,发现它竟然是个天才之作。所以接下来,我们准备抛弃原来的iss91 分支(实际上会丢弃 C5 和 C6),直接在主干中并入另外两个分支。最终的提交历史将变成这样:

    git-branch21.png 注意:这些分支全部都是本地分支,这一点很重要。当你在使用分支及合并的时候,一切都是在你自己的 Git 仓库中进行的 — 完全不涉及与服务器的交互。

2.3.6. remote branch


一次 Git 克隆会建立你自己的本地分支 master 和远程分支 origin/master,它们都指向 origin/master 分支的最后一次提交。

git-branch22.png

Figure 34: Git克隆

如果你在本地 master 分支做了些改动,与此同时,其他人向 git.ourcompany.com 推送了他们的更新,那么服务器上的master 分支就会向前推进。不过只要你不和服务器通讯,你的 origin/master 指针仍然保持原位不会移动。

git-branch23.png

Figure 35: 在本地工作的同时有人向远程仓库推送内容会让提交历史开始分流

可以运行 git fetch origin 来同步远程服务器上的数据到本地。

git-branch24.png

Figure 36: git fetch 命令会更新 remote 索引

把另一个服务器加为远程仓库

git-branch25.png

Figure 37: 把另一个服务器加为远程仓库

在本地有了一个指向 teamone 服务器上 master 分支的索引

git-branch26.png

Figure 38: 在本地有了一个指向 teamone 服务器上 master 分支的索引

如果你有个叫 serverfix 的分支需要和他人一起开发,可以运行:

// 推送本地分支
$ git push origin serverfix
// 跟踪远程分支
从远程分支 checkout 出来的本地分支,称为_跟踪分支(tracking branch)。
$ git checkout --track origin/serverfix
// 删除远程分支
在服务器上删除serverfix 分支,运行下面的命令:
$ git push origin :serverfix
//拉取远程仓库最新改动到本地仓库,执行:
$ git pull

2.3.7. tag

  • 查看标签

    //显示所有标签
    $ git tag
    //查看 v4.0 系列的标签
    $ git tag -l v4.0.*
    //查看相应标签的版本信息
    $ git show V0.1
    
  • 创建标签

    //推荐为软件发布创建标签。这个概念在SVN中也有。
    //创建一个叫做1.0.0的标签:
    $ git tag 1.0.0 1b2e1d63ff //1b2e1d63ff是你想要标记的提交 ID 的前 10 位字符。
    //创建带有注释的标签
    $ git tag -a V0.1 -m "版本0.1" 
    
  • 修改标签

    $ git tag newTag oldTag
    $ git tag -d oldTag
    $ git push origin :refs/tags/old
    $ git push --tags
    
  • 删除标签

    $ git tag -d tagName
    
  • 推送tag到远程仓库

    $ git push origin --tags
    

2.3.8. log

  • 基本使用
    • log

      //查看提交日志,不带参数,按提交时间列出所有的更新,最近的更新排在最上面
      $ git log
      
      //查看所有操作日志
      $ git reflog
      
      //只显示指定文件的日志信息
      $ git log README.md
      
    • show

      $ git show
      //显示最后 5 次的文件改变的具体内容
      
      $ git show commitid
      //显示某个 commitid 改变的具体内容
      
    • whatchanged

      $ git whatchanged --stat
      每次修改的文件列表, 及文件修改的统计
      
  • 选项(Option)
    • -p

      $ git log -p -2 README.md
      //-p 选项展开显示每次提交的内容差异,-2 则仅显示最近的两次更新:
      
    • –stat

      $ git log --stat -1
      $ git log --name-status -1
      //--stat 选项仅显示简要的增改行数统计,--shortstat 选项只显示,--name-status 每次修改的文件列表, 显示状态
      
    • –graph

      //以图表形式输出分支提交日志
      $ git log --graph
      
    • –word-diff

      //--word-diff 选项,进行单词层面上的对比。你需要在书籍、论文这种很大的文本文件上进行对比的时候,这个功能就显出用武之地了。
      $ git log -U1 --word-diff
      //-U1,表示希望上下文( context )行数从默认的 3 行,减为 1 行 
      
    • –pretty

      //--pretty 选项指定使用完全不同于默认格式的方式展示提交历史
      // 将每个提交放在一行显示
      $ git log --pretty=oneline
      // 另外还有short,full,fuller 和 format 可以用。format可以定制要显示的记录格式,这样的输出便于后期编程提取分析
      
    • –name-only

      仅在提交信息后显示已修改的文件清单。
      
    • –name-status

      显示新增、修改、删除的文件清单。
      
    • –abbrev-commit

      仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。
      
    • –relative-date

      使用较短的相对时间显示(比如,“2 weeks ago”)。
      

2.3.9. diff

//查看更改前后的差别
$git diff 
//查看工作树和最新提交的差别
$git diff HEAD

2.4. .gitignore

以斜杠“/”开头表示目录;
以星号“*”通配多个字符;
以问号“?”通配单个字符
以方括号“[]”包含单个字符的匹配列表;
以叹号“!”表示不忽略(跟踪)匹配到的文件或目录;

3. GitHub

3.1. Introduction

3.1.1. About GitHub


官网地址:https://github.com/
GitHub是一个利用Git进行版本控制、专门用于存放软件代码与内容的共享虚拟主机服务。它由GitHub公司(曾称Logical Awesome)的开发者Chris Wanstrath、PJ Hyett和Tom Preston-Werner使用Ruby on Rails编写而成。
GitHub同时提供付费账户和免费账户。

3.1.2. Keyword

Repository(仓库)
用于存放项目源代码。
Star(收藏)
收藏项目
Fork(复制克隆项目)
Pull Request(发送请求)
基于Fork,修改或删除代码提交请求。
Watch(关注)
假设项目有任何更新,第一时间收到通知消息。
Issue(事务卡片)
发现了代码存在BUG,但是目前没有成型代码,需要讨论时使用。

3.2. Upload Code to GitHub

3.2.1. SSH


使用SSH方式,在提交时,不需要输入用户名和密码。
首先在本地创建ssh key,一直回车。

$ ssh-keygen -t rsa -C "your_email@youremail.com"
//成功的话会在~/目录下生成.ssh文件夹,进入.ssh目录,打开id_rsa.pub文件,复制里面的key。


进入github官网上,点击个人中心的 Account Settings(账户配置),左边选择"SSH Keys" > "Add SSH Key",title自定义填写,然后将复制的key粘贴到GitHub的文本框中。下面是添加好的SSH Keys示例: sshkeys.png
验证是否成功:

$ ssh -T git@github.com 

3.2.2. HTTPS


使用条件:1)本地配置了global;2)需要输入用户名和密码推送代码

3.2.3. 配置免登录提交代码

$ vim .git/config
[remote "origin"]
url=https://github.com/用户名/仓库名.git
[remote "origin"]
url=https://用户名:密码@github.com/用户名/仓库名.git

3.3. 开源项目贡献流程

  1. 新建Issue:提交问题或建议或想法。
  2. Pull Request:Fork项目,修改代码,发起修改请求。 方式一

    通过 Github 网站图形化操作,New pull request.
    

    方式二

    使用 git 命令操作。
    $ git remote -v
    $ git remote add upstream https://github.com/xxx/xxx.git
    // $ git remote remove upstream
    $ git fetch upstream // 从源仓库同步代码
    $ git merge upstream/master //合并到本地分支
    $ git push
    

3.4. Github Pages

3.4.1. 新建仓库搭建

  1. 创建个人站点,新建仓库。(注:仓库名必须为【用户名.github.io】)
  2. 在新建的仓库下,新建index.html文件即可。

    注:(1)Github Pages仅支持静态网页;(2)仓库里面只能是.html文件
    

3.4.2. 项目仓库下搭建

  1. 进入项目仓库主页,点击settings。
  2. 找到【Github Pages】,点击【Change theme】,选择主题来自动生成主题页面。
  3. 访问:https://用户名.github.io/%E4%BB%93%E5%BA%93%E5%90%8D

4. Problems

4.1. git: fatal: I don't handle protocol 'https'

4.2. fatal: Not possible to fast-forward, aborting.

$ git pull

Note:- you will get an error. Fatal: Not possible to fast-forward, aborting
Try this one:

$ git pull orign master --rebase

Note:- Now after this command, you will get merge conflict issues. So now you can go to your file and manually resolve the conflict.

4.3. error: RPC failed; curl 56 GnuTLS recv error (-9): A TLS packet with unexpected length was received

解决:
$ sudo apt purge git
$ sudo apt install git

4.4. git pull:fatal: refusing to merge unrelated histories


合并pull两个不同的项目,问题解决:
假若我在 Github 新建了一个仓库,在本地又初始化了一个新仓库,并且添加了很多与 Github 内容不同的文件和代码。此时,想把本地代码与远程 Github 仓库的代码合并,于是,通过 git pull origin master 命令拉取代码(origin 就是仓库,而 master 就是需要上传的分支);然后,就输出 refusing to merge unrelated histories。
因为是不同的两个仓库,要把两个不同的项目合并,需要添加 –allow-unrelated-histories 告诉 git 允许不相关历史合并,这句代码是在git 2.9.2版本发生的。
假如我们的源是origin,分支是master,那么需要这样写:

$ git pull origin master --allow-unrelated-histories

如果有设置了默认上传分支就可以用下面代码:

$ git pull --allow-unrelated-histories