zoukankan      html  css  js  c++  java
  • 版本控制系统及Git使用小记

    版本控制系统(Version Control System,VCS)有CVS、SVN、ClearBase、Vss、Git等,其中Git最流行。Git为分布式的版本控制系统(DVCS),而SVN等为集中式的版本控制系统(CVCS)。

    DVCS与CVCS的区别

    DVCS:         CVCS: 

    对于DVCS,本地磁盘上有项目的所有完整提交历史(本地仓库);而CVCS则没有,执行提交、查看提交等命令时都需要从服务端获取。这也是DVCS的一个重要优势,可不联网使用、本地相当于有服务端的完整备份故故障容错性更好(笔者忍受过一离开实验室就没法使用svn的痛苦..)。

     

    Git速度快的原因:

    Git中保存的是文件各个版本的快照(每个快照都有一个唯一标识,即commit id),而不是像很多其他版本管理系统那样保存文件差异;

    Git中所有操作都是增加数据(创建、修改、删除文件底层都是产生新快照);

    快照间通过指针相连形成快照引用链,每个快照可有多个前驱、多个后继快照。

    基于上述原因:

    Git中创建分支、切换等操作非常快(只要创建或移动指针即可,而有些VCS创建分支就是所有文件复制一份,可想后者多低效);

    分支、标签等本质上是某个快照的别名,通常通过这些别名在不同分支或标签间切换。然而,实际上你可以回退到任何地方(只要你知道它的commit id),即使这些地方没有分支或标签名或原先有但后来被删了。

    缺点:空间占用比较大,因只增不删故只要提交过的文件都会在仓库中。很明显,这是空间换时间的做法(故不需要的文件尽量别加入到仓库,否则即使你之后删了它,虽然你在当前快照看不到但它实际上仍在Git仓库中,这会导致整个项目空间占用越来越大)。不过,Git也有垃圾回收机制gc,在涉及网络传输或者Git仓库真的体积很大的时候,不仅会清除无用的object,还会把已有的相似object打包压缩。

    实际使用中发现,Git对于文件重命名或文件在不同目录中的移动都可以自动追踪到从源文件到目的文件的变化(而不会认为是删旧文件创建全新新文件),即在目的文件上仍保留完整的历史提交记录(实际上,可在目的文件上通过blame命令查看内容的原始来源)。这点在项目重构、项目结构优化的场景下很好用,因为这些场景下场需要进行文件移动或重命名。不知其他的版本管理工具是否也能做到这样。

    以下记录一些Git实践的经验或总结。

    (关于相关命令的作用,首推荐参阅文档:https://git-scm.com/docs/,也通过 在命令后加--helpgit help 命令 查看用法。推荐在有点使用经验后有问题直接官方文档,比看网上以讹传讹的一堆烂文章靠谱..) 

    1. 数据模型

    数据模型

    Git数据模型如下图所示:

    在Git中有四个部分(各种Git操作实际上就是让文件或文件快照在四个部分间流动):

    工作区(本地项目的根目录):称为workspace或working directory,对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。

    暂存区(项目根目录的.git文件夹下):称为stage area,是一个文件,保存了下次将提交的文件列表信息。 有时候也被称作`‘索引’',不过一般说法还是叫暂存区域。

    本地仓库(项目根目录的.git文件夹下):称为local repository,是 Git 用来保存项目元数据和对象数据库的地方。这是 Git 中最重要的部分,从其它计算机克隆仓库时就是复制这里的数据。

    远程仓库(远程服务器上):称为remote repository,同上

     

    文件状态及生命周期

    Git中保存的是各个版本的文件快照而不是文件差异

    文件有四种状态:

    未追踪(位于工作区):新建的文件为此状态,表示Git尚未追踪该文件、在Git快照中没有这些文件。

    未修改/已提交(位于工作区):已追踪但没做任何编辑的文件,暂存区的文件提交到本地仓库后变为此状态。

    已修改(位于工作区):工作区中的文件修改后就是此状态。

    已暂存(位于暂存区):表示对一个新建文件或已修改文件的当前版本做了标记,使之包含在下次提交的快照中。

    文件状态变化周期:

    图中箭头大多是Git操作,如git rm、git add、git commit

    Git的大多数操作就是针对已追踪的文件的(特别是已修改、已暂存状态的文件);对于未追踪文件,除了add操作将之变为已修改外,其他大多数命令都无法奈之何。

    本质:各种复杂的操作实际上都可归为上述的文件状态的转换,故可通过该图来掌握各种操作

    基本使用流程

    基本的 Git 工作流程如下:

    在工作目录中修改文件。

    暂存文件,将文件的快照放入暂存区域。

    提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。

    2. 基本操作

    全局配置(config)

     git config --global user.name xx 

     git config --global user.email xxx 

     git config --list 

    初始化仓库(init)

     git init 

    删除文件(rm)

    用git命令删除文件只能删除已被追踪的文件。

     git rm "file.txt" :将文件从工作区及暂存区删除。执行该命令后该操作所造成的修改就自动被 add到暂存区了,故该命令效果类似  rm file.txt && git add file.txt (但并不完全相同,见下面)

    特殊情况:

    -f 强制删除:若文件被修改过但未提交(即处于已修改或已暂存状态),则默认删不掉,以防误删未提交的数据。确实要删的话可加 -f  参数来强制删除。

    --cached:暂存区中删除该文件、工作区中该文件变为未追踪状态。示例如下:

    移动文件(mv)

     git mv a b ,该命令与 git rm 命令类似,执行该命令后该操作所做的修改就自动被add到暂存区了,故该命令等价于: mv a b  &&  git add a b 

    暂存/提交修改(add/commit)

     git add "file1.txt" :表示将已修改的文件产生快照到暂存区,以备下一次提交。该命令用于:跟踪未跟踪状态的新文件、把已跟踪的文件放入暂存区、把合并时有冲突的文件标记为已解决冲突状态等。该命令的内部原理是创建文件快照放在暂存区。

    有时候并不想将所有文件add,此时:法1可通过指定文件列表或文件名模式,法2可用交互式提交提交命令 git add -i 

     git commit -m "添加文件1" :表示将暂存区的快照内容提交到本地仓库。提交后会针对该次提交产生一个commit id,用来表示该提交。commit id是个SHA哈希值,如7a7d2d6490ba219db0a959ee19e1a70985a8a87d,很多其他命令可以通过该值来引用该提交(也可只写前若干位,只要没有歧义即可)。

    add 和 commit的两步操作可以一步到位: git commit -a -m "xxx" ,通过commit的 -a 参数,不用执行add操作就可以将所有工作区和暂存区的修改提交到本地仓库。

    commit时的msg应该是有意义的,有个规范,见:https://www.conventionalcommits.org/en/v1.0.0/

    主要语义:

    fix 修复

    feat 功能

    chore 杂项

    docs 文档

    style 样式

    查看状态(status)

     git status :会列出当前未提交到仓库的各种修改,包括新增的、编辑过的、已标记暂存的

     git status -s :状态简览,推荐用此。与不带参数的相比,此命令列出的信息更简洁,示例如下:

    $ git status -s
     M README
    MM Rakefile
    A  lib/git.rb
    M  lib/simplegit.rb
    ?? LICENSE.txt

    前面的字符表示文件状态:??标记新加的未被跟踪的文件、M标记修改过但未暂存的文件、A标记暂存的文件、MM标记暂存后又再修改的文件(左右M分别表示暂存、修改)。

     

    比较修改(diff)

    分别列出每个文件的当前内容与其最近一次提交的差异

     git diff file1 file2 file3 ,工作区和暂存区的比较

     git diff --cached file1 file2 file3 ,暂存区和本地仓库比较。Git 1.6.1及之后的版本还允许用 --staged 参数,与--cached等效。

     git diff commitId1 commitId2 :列出两个提交间的差异

     git diff test..dev :两点语法,列出由test分支最新提交变为dev分支最新提交所需做的改变

     git diff test...dev :三点语法,列出由test、dev分支最近公共提交变为dev分支最新提交所需做的改变。能干净地列出某个分支与其创建时的起点间的区别。

    储存当前的修改(stash)

    git stash list            //查看储存的修改
    git stash push/pop   //将修改压入栈顶 或 将储存的修改从栈顶移除并应用到工作区
    git stash push -m '注释' 文件名  //将指定文件的修改压入栈顶
    git stash save  '注释'        //储存修改,放到储存栈中。不要用-a,其会将.gitignore中列举忽略的文件也储存
    git stash apply stash@{id}  //将储存的修改应用到工作区,但不从栈中移除
    git stash drop stash@{id}   //将储存的指定修改从栈中移除,不会应用到工作区
    git stash clean                   //删除栈中所有储存的修改
    git stash branch <branchname> [<stash>] //将指定的储存内容应用到指定的新分支,会基于该stash储存时所基于的分支创建新分支、会移除该stash。慎用此命令!

    注:

    储存时默认会将 暂存区中未提交的修改工作区中已追踪的文件修改 储存起来。储存时的选项:

    -u 或 --include-untracked:未追踪的文件也储存,默认不会储存未追踪的文件

    -a:不仅未追踪文件,连.gitignore中所列忽略的文件修改也会被储存。很少用此选项。

    --keep-index:暂存区中未提交的修改不要储存。

    储存的内容可以应用到任意分支,而不要求储存内容的来源分支与应用的目标分支是同一分支。

    将储存的内容应用到工作区时,并不要求工作区是干净的。

    查看修改或历史(log/reflog/shortlog/show/blame)

    1. 查看提交历史(log)

    查看commit历史: git log ,会列出提交历史。有很多参数用于 筛选提交、展示形式 等的设置。如:

     git log -p -2 :参数-p指示列出展开每次提交所做的修改(相当于git show commitId),-2指定显示最近的两次更新。

     git log --stat :列出每次提交所修改的文件

     git log --pretty=oneline :列出提交历史的简要信息,每条仅包含commitId和注释,示例: 8ebc1882fa63e8048a8ad983e9de7fa413f54580 add file test.txt 

    --pretty指定提交历史的展示格式,值有 oneline、short、full、fuller、format等。format用于指定自定义格式,其选项可参阅常用选项

     git log --graph --all --decorate --oneline :查看提交历史graph,会显示 ASCII 图形表示的分支合并历史。结果示例:

    筛选历史提交记录

      git log --since = 2.weeks # or 2018-01-03  :根据提交信息搜索提交,此为最近一周内的提交。根据提交信息搜索的还有 --until、--author、--grep等。--all-match用于指定多个搜索条件为与的关系,否则默认为或的关系。

     git log -Sauth :根据修改的文件内容搜索提交,可列出添加或移除了某些字符串的提交,如找出添加或移除了对某一特定函数的引用的提交。

     git log expserver2c/ :搜索指定文件的历史提交

    排除筛选

    --not语法: git log dev testing staging --not master ,查看在dev或testing或staging但不在master上的提交。--not用于排除,

    ^ 语法:作用同--not,不过--not要放在最后而这里不必,故更灵活。如上述命令等价于 git log dev ^master testing staging 

    区间筛选

    二点语法: git log dev..test ,列出在test分支但不在dev分支的commit,即后者commit集减前者commit集。等价于 git log test --not dev 。

    这个命令很有用,可以查看即将合并的内容,如查看即将推送到远端的内容: git log origin/master..HEAD 。

    两点的两步可以少分支参数,此时默认为HEAD。下面的三点语法同。

    三点语法: git log dev...test ,结果为 (dev ∪ test) - (dev ∩ dev),即两分支各自独有的commit的集合。可加 --left-right 参数,结果会标识提交属于哪个分支。如: git log --oneline --left-right HEAD...MERGE_HEAD 可以查看与合并冲突有关的提交

     综合示例: git log --pretty="%h - %s" --author=gitster --since="2008-10-01" --before="2008-11-01" --no-merges -- t 

    2. 查看提交简报(shorlog)

     git shortlog [<options>]  :查看提交简报(Summarize 'git log' output),会列出提交者及每个人的提commig msg,可同过参数 --no-merges、--not 等筛选要列出的内容。

    3. 查看引用日志(reflog)

    查看所有操作记录(包含commit、reset等所有记录): git reflog 。reflog——引用日志,顾名思义,引用日志记录了你的 HEAD 和分支引用所指向的历史。

    值得注意的是,引用日志只存在于本地仓库,一个记录你在你自己的仓库里做过什么的日志。 其他人拷贝的仓库里的引用日志不会和你的相同;而你新克隆一个仓库的时候,引用日志是空的,因为你在仓库里还没有操作。

    可查看所有分支的所有操作记录,包括提交、合并、重置、还原操作及已经被删除的commit记录等,git log则不能察看已经删除了的commit记录。示例:

    a53d87ab HEAD@{1}: commit (amend): [add] 课程列表增加是否上架到商店过的字段
    2940c47f HEAD@{2}: commit (amend): [add] 课程列表增加是否上架到商店过的字段
    edf7bb6f HEAD@{3}: commit: [add] 课程列表增加是否上架到商店过的字段
    bc7bde23 HEAD@{4}: commit (amend): [update]审核通过时不默认将课程推到商店
    25b56994 (test) HEAD@{5}: checkout: moving from test to dev
    25b56994 (test) HEAD@{6}: reset: moving to HEAD
    25b56994 (test) HEAD@{7}: reset: moving to HEAD
    25b56994 (test) HEAD@{8}: reset: moving to HEAD
    25b56994 (test) HEAD@{9}: reset: moving to HEAD
    25b56994 (test) HEAD@{10}: checkout: moving from dev to test
    View Code

    结合reflog和reset命令,可以达到撤销合并操作的目的,见后文。

    4. 二分查找commit(bisect)

    场景:当前的代码运行有问题,自从上次可正常运行到现在已经引入了数十或上百提交,此时为了定位最早引入哪个commit导致出错的,就可用bisect命令来二分查找。

    为用户提供交互式的二分查找命令来查找某个commit,命令列表:

    git bisect [help|start|bad|good|new|old|terms|skip|next|reset|visualize|view|replay|log|run]

    (base) G104E1901067:CourseDesignServer zhangshaoming1$ git bisect help
    usage: git bisect [help|start|bad|good|new|old|terms|skip|next|reset|visualize|view|replay|log|run]
    
    git bisect help
        print this long help message.
    git bisect start [--term-{old,good}=<term> --term-{new,bad}=<term>]
             [--no-checkout] [<bad> [<good>...]] [--] [<pathspec>...]
        reset bisect state and start bisection.
    git bisect (bad|new) [<rev>]
        mark <rev> a known-bad revision/
            a revision after change in a given property.
    git bisect (good|old) [<rev>...]
        mark <rev>... known-good revisions/
            revisions before change in a given property.
    git bisect terms [--term-good | --term-bad]
        show the terms used for old and new commits (default: bad, good)
    git bisect skip [(<rev>|<range>)...]
        mark <rev>... untestable revisions.
    git bisect next
        find next bisection to test and check it out.
    git bisect reset [<commit>]
        finish bisection search and go back to commit.
    git bisect (visualize|view)
        show bisect status in gitk.
    git bisect replay <logfile>
        replay bisection log.
    git bisect log
        show bisect log.
    git bisect run <cmd>...
        use <cmd>... to automatically bisect.
    View Code

    基于上述场景,命令使用:

    1 初始化搜索范围:

    git bisect start           #启用二分查找,会创建一个临时分支并切换到该分支
    git bisect bad $comitId1   #告诉Git指定的commit有问题,不指定id则默认为HEAD
    git bisect good $comitId2  #告诉Git指定的commit是正常的

    经上述命令后会 创建临时分支并切过去、列出这些范围内的提交个数、切到指定的提交范围的中点的提交:

    Bisecting: 6 revisions left to test after this
    [ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo

    2 交互式减少搜索范围:

    用户在该中点提交上测试代码是否仍有问题,根据测试结果执行 git bisect good 或 git bisect bad 以让Git二分减小搜索范围(这里的"good"、"bad"可定义其他的,只要在start时指定即可,可看上面命令列表中start用法):

    $ git bisect good
    Bisecting: 3 revisions left to test after this
    [b047b02ea83310a70fd603dc8cd7a6cd13d15c04] secure this thing

    范围足够少后,Git会告知我们第一个导致代码有问题的提交:

    $ git bisect good
    b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit
    commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04
    Author: PJ Hyett <pjhyett@example.com>
    Date:   Tue Jan 27 14:48:32 2009 -0800
    
        secure this thing
    
    :040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730
    f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M  config

    搜索过程中可通过 git bisect log 查看二分点列表(即被test的提交列表)及每个二分点的"good"或"bad"标记。

    3 结束搜索: git bisect reset #结束二分查找,会删除临时分支并切回启用前的分支 

    4 自动运行:也可不用让用户参与到每次二分点的"good"、"bad"决策,而是给出脚本,让Git自动在每个被checkout的提交里执行脚本直到找到首个致错提交:

    $ git bisect start HEAD v1.0
    $ git bisect run test-error.sh

    也可以执行 make 或者 make tests 或者其他东西来进行自动化测试。

    5. 查看某次提交的修改(show)

    查看某次commit做的修改:  git show ${commit_id}  ,commit id可以只列出前几个字母,只要没有歧义即可。

      

    6. 查看文件每行的提交信息/来源(blame)

     git blame simplegit.rb :查看文件每行最近一次的修改者及修改时间等信息。结果示例:

    ^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 12)  def show(tree = 'master')
    ^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 13)   command("git show #{tree}")
    ^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 14)  end
    ^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 15)
    9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 16)  def log(tree = 'master')
    79eaf55d (Scott Chacon  2008-04-06 10:15:08 -0700 17)   command("git log #{tree}")
    9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 18)  end
    9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 19)
    42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 20)  def blame(path)
    42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 21)   command("git blame #{path}")
    42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 22)  end
    View Code

    每行含义:最近一次修改的 所属commitId、修改者、修改时间、行号、修改后的内容。commitId左边带 ^ 符号的(如^4832fe2)表示该文件 第一次被提交时就有的且之后未被修改 的行

    常用选项:

    -L:指定要查看的行范围,如 -L 12, 22    -L 12, +22 两者表示的范围分别为 [12, 22]、[12, 12+22]

    -C:列出每行的文件来源。如果当前文件中某些内容是从其他文件复制过来的(重构项目或复制代码时常有此情况),Git会列出这些行的原始文件出处。该功能非常有用。示例:

    $ git blame -C -L 141,153 GITPackUpload.m
    f344f58d GITServerHandler.m (Scott 2009-01-04 141)
    f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC
    f344f58d GITServerHandler.m (Scott 2009-01-04 143) {
    70befddd GITServerHandler.m (Scott 2009-03-22 144)         //NSLog(@"GATHER COMMI
    ad11ac80 GITPackUpload.m    (Scott 2009-03-24 145)
    ad11ac80 GITPackUpload.m    (Scott 2009-03-24 146)         NSString *parentSha;
    ad11ac80 GITPackUpload.m    (Scott 2009-03-24 147)         GITCommit *commit = [g
    View Code

    实际使用中发现,Git对于文件重命名或文件在不同目录中的移动都可以自动追踪到从源文件到目的文件的变化(而不会认为是删旧文件创建全新新文件),即在目的文件上仍保留完整的历史提交记录。这点在项目重构、项目结构优化的场景下很好用,因为这些场景下场需要进行文件移动或重命名。

    实际上,Git 不会显式地记录文件的重命名操作。 它会记录快照,然后在事后尝试计算出重命名的动作。示例:你在工作区删掉一个文件然后通过git status会提示你删掉了一个文件并新增了一个untracked文件,当你通过git add把变更提交到暂存区后再执行git status会发现Git提示你将文件重命名了而非删旧增新。

    不论是用户的复制操作、还是Git推断出的重命名操作,都可通过 git blame 命令找出文件中从别的地方复制过来的代码片段的原始出处,即使该代码是在其他目录其他名字的文件里的。通常来说,你会认为复制代码过来的那个提交是最原始的提交,因为那是你第一次在这个文件中修改了这几行。 但 Git 会告诉你,你第一次写这几行代码的那个提交才是原始提交,即使这是在另外一个文件里写的。

    标签(tag)

    有两种标签:附注标签(annotated tag)、轻量标签(lightweight tag)

    创建标签:git tag [-a] <tagname>  [<commit> | <object>] ,通过指定commitId可以对旧的提交创建标签

    附注标签: git tag -a tag_name -m 'your tag msg' ,如 git tag -a v1.4 -m 'my version 1.4' ,-a表明这是个annotated tag。

    轻量标签: git tag tag_name 

    查看标签列表: git tag [-l 'search pattern'] ,如 git tag 或 git tag -l "611*" 

    查看标签信息: git tag show tag_name 

    删除标签: git tag -d tag_name 

    标签同步到远程仓库(提交代码修改并不会将创建的标签也提交): git push origin tag_name 、 git push origin --tags 分别为同步一个、同步所有。

    切换到某标签的版本上: git checkout -b new_branch_name tag_name ,会基于指定的标签创建一个新分支。

    搜索文件内容(grep)

    与shell的grep命令类似,支持强大的搜索功能。不同的是可以指定分支、速度更快等。

     git grep -n authUtil* dev :在dev分支所有文件中搜索包含能部分匹配"authUtil*"正则的行。也可以指定从某个tag的代码中搜索(分支名换成标签名),甚至可以是指定的文件如 *.java。

    会输出 分支、文件名、行号、内容,结果示例:

    (base) G104E190:ss1Server test1$ git grep -n authUtil dev
    dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:75:   private ExpserverAuthUtil<?> authUtil;
    dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:89:           String userId = authUtil.getUserIdFromRequest(request);
    dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:437:          String userId = authUtil.getUserIdFromRequest(request);
    dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:579:          String userId = authUtil.getUserIdFromRequest(request);
    dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:590:          String userId = authUtil.getUserIdFromRequest(request);
    dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaultCommonCourseImpl.java:23:  ExpserverAuthUtil<?> authUtil;
    dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaultCommonCourseImpl.java:34://                        String userId = authUtil.getUserIdFromRequest(request);
    View Code

    主要选项:

    -n 输出行号

    --count 不输出匹配的内容而是统计每个文件中匹配到多少行。

    更多选项可参阅命令文档: git help grep 

    生成构建号(describe)

     git describe dev :生成一个字符串,它由最近的附注标签名、自该标签之后的提交数目和你所描述的提交的部分 SHA-1 值构成,如 1.6.2-rc1-20-g8c5b85c 。

    只有存在附注标签时才能生成该字符串

    生成压缩包(archive)

     git archive dev --prefix='marchon/' --format=zip > `git describe dev`.zip :会将指定分支的文件生成压缩包。

    --prefix:指定前缀,分支跟目录下(仅限第一层)的所有文件或文件夹名都会加该前缀,若前缀带斜杠,则相当于指定了个父文件夹

    --format:指定压缩格式,不指定则默认为.tar.gz格式。

    3. 分支管理

    默认的远程主机名为origin、默认的分支名为master,只是默认名,与自己创建的相比并没有特殊的地方。

    分支实际上只是个对快照的引用。

    本地仓库中的分支类型

    本地分支:码农维护的分支。

    远程跟踪分支:git维护的分支。是远程分支状态的引用,名字格式为  远程主机名/分支名 ,如origin/dev;远程跟踪分支不是远程分支, 它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。 远程跟踪分支像是你上次连接到远程仓库时,那些分支所处状态的书签。

    分支操作

    创建分支: git branch 新分支名 [源分支名] [ ] 

    分支重命名: git branch -m oldBranchName newBranchName 

    删除本地分支: git branch -d 分支名 

    删除远程分支: git branch --delete origin branch_name 或者 git push --delete origin branch_name ,会将远程分支删除,origin为远程主机名。不过Git 服务器通常会保留数据一段时间直到垃圾回收运行,故还是可恢复的。

    切换分支: git checkout 分支名 ,或   git checkout -b 新分支名 [源分支名] [原分支commit终点] ,后者为从指定源分支(默认为master)创建并切换分支。

    查看分支(假设远程和本地库中都只有master分支,远程主机名在本地被取为origin):

      • 查看本地: git branch ,得到master
      • 查看远程分支: git branch -r ,得到origin/master
      • 查看本地和远程的所有分支: git branch -a ,得到master和remotes/origin/master两条记录。远程具有哪些分支可能在动态变化,在本地有涉及到网络的操作时会自动更新远程分支信息到本地。
      • 查看各分支最后一次提交的信息: git branch -v 
      • 查看所有本地分支及其跟踪的远程分支: git branch -vv 
      • 查看哪些分支 已经/尚未 合并到当前分支: git branch --merged / git branch --no-merged 

    取回远程分支

     git fetch 远程主机名 [分支名] ,如git fetch origin master,取回远程分支信息并更新本地的远程跟踪分支。

    若没有指定分支名则取回所有分支的更新;取回分支只是将远程主机版本库的更新取回,对本地分支没影响;所取回的更新,在本地主机上要用"远程主机名/分支名"的形式读取

    取回远程分支前后的状态示例:

    取回前:,   取回后:

      

    合并分支

    示意图:_  _

    为便于表述,这里定义几个概念:

    br:branch current,当前所处的分支

    bm:branch merged,要被合入到当前所处分支的分支

    lcbr:last commit of branch current,当前分支的最后一次提交,如图中的C3(br为experiment时)

    lcbm:last commit of branch merged,被合并分支的最后一次提交,如图中的C4(bm为master时)

    lcc:last common commit of two branchs,两分支的最近的一次公共父提交,即分支的最近公共父节点,如图中的C2

    三种合并方式

    1.  git merge [--no-ff] 被合并分支名 :将bm合并到bc。(关于创建与合并分支的原理,可见创建与合并分支-廖雪峰Git分支管理策略-阮一峰分支合并-腾讯技术工程

    (1)若两分支的commit是分叉的,则会基于lcbr、lcbm、lcc三个提交进行合并、并以两分支最后一个commit作为祖先来创建新的commit节点,新commit的message通常自动为"Merge branch 'xx1' into xx2"。若有冲突则此时还需要解决冲突并提交。为何要三方合并?因为对一个文件的同时修改仅凭lcbr、lcbm无法确定以谁的为准,有了lcc后就能确定。

    示例:如将B分支(a1、b1、b2)合入A分支(a1、a2)A将变成(a1、[a2、b1、b2]、c1),这里中间的三个commit并不一定是这个顺序而是会按它们的先后时间排序

    节首图中在master分支执行 git merge experiment 的结果示例:

    有时两分支的lcc是不唯一的,这时会以先这两个lcc作为待合并节点去寻找这两lcc的lcc执行合并,依次递归,这也是"recursive"合并策略命名的来源。示例如下,要对节点5、6合并,由于它们lcc为节点2、3不唯一,故先合并 2、3及其lcc 1 得到内容为B的临时节点、再将临时节点及5、6合并得到内容为C的节点7,如图:

    (2)若两分支的commit不存在分叉(即bc的提交链是bm的提交链的“前缀”),则合并时会直接把指针前移(称为Fast forward,即快进式合并)。显然这是上述情况的特例,但可通过 --no-ff  参数指定不要进行Fast forward,即合并时强制生成新commit节点。

    示例:如将B分支(a1、b1、b2)合入A分支(a1)时A将也变成(a1、b1、b2)

    其他用法:

     git merge --squash feature-1 ,带--squash参数的用于合并多个commit为一个新commit。

    会将从最近公共祖先之后被合并分支上有而当前分支没有的所有修改应用到当前分支,并暂存到暂存区(但不会提交),之后需要手动提交,提交时默认的commit msg为"Squashed commit of the following:"+各被合并的commig的msg。可见,只是将修改应用到当前分支成为一个新的commit。

    2. git rebase 被合并的分支名  :也用于合并分支,其将bc从lcc之后新增的提交所对应的修改在bm上replay一遍(产生新的提交并丢去原有的提交),因此不同于有分叉的merge操作,rebase操作后提交历史不会有分叉而是一条直线,效果是就像一直在bc上操作一样。注意这里是”重放“,重放后的修改虽然一样,但他们是不同的commit,可参阅下面示例,C4和C4'虽然效果一样但是是不同提交。变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 

    节首图中在experiment执行 git rebase master 的结果示意图:

    更多用法: git rebase -i [start-commit] [end-commit] # 区间左开右闭、默认end-commit为HEAD ,可详细筛选、修改要合并的commit。

     git rebase与git merge的区别在于

      后者合并时会产生一个merge commit(commit message形如“Merge branch 'dev' of xxx into local_dev”)而前者不会。

    合并后commit的组织顺序不同:假设基于分支A的分支A1、A2上分别有3、4个新commit,现在A1要将A2的修改合并进来,则通过后者进行合并后7个新commit会被按照时间排序,而通过前者执行合并后7个commit不会整体按时间排序,而是要么A1的3个commit在前要么A2的4个commit在前,取决于其最早的一个commit谁早,也因此rebase后当前分支的commit历史会被打乱。

    rebase的适用场景:通常用于为保持干净的、“线性”(没分叉)提交记录的合并,这要求合并的源、目的两者共同修改少,这个前提在项目中较少能满足,故实际使用少。

    如:在同一分支上,本地分支将远程分支同步到本地,可以使用rebase。基于dev分支拉dev-feature分支并在新分支上开发新feature,这期间定期将dev rebase到dev-feature。然而这些场景下使用rebase的前提是被合并者与当前者共同修改的地方交集很少甚至没有,否则合并时会有很多冲突,且由于历史提交会被打乱,导致冲突很难解决。因此,在较大项目中通常不要进行rebase

    rebase的使用原则:一旦当前分支中的commit发布到公共仓库,就千万不要对该分支进行rebase操作,因为别人可能拉取了你rebase前的分支,其某些commit和你rebase后的分支对应commit的效果虽然一样但commitId不同了。只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作。

    更多可参阅:https://www.progit.cn/#_rebasing

    3.  git cherry-pick commitId1 commitId2 :会将指定commit(不管是哪个分支的)的修改应用到当前分支。注意并不会把commit合并进来,而是只将修改应用到当前分支,并产生新的commit。可以指定多个commit,甚至可以指定commit区间。

    分支冲突处理,几个有用的选项或命令:

    -Xignore-all-space 或 -Xignore-space-change :合并时忽略空白修改。如两个对同一行的修改一个是增加一空格一个是增加一行代码则合并时会忽略前者的修改。

    -Xours、-Xtheirs:对于合并冲突的文件采取指定的一边的分支的代码,而不是在文件中做冲突标记

    --conflict=diff3,指定冲突时的标记,默认是两个分支的最新commit id,指定为此值后会变为"outs"、"theirs"。可通过 git config --global merge.conflictstyle diff3 使配置全局生效。

    列出与冲突有关的提交: git log --oneline --left-right HEAD...MERGE_HEAD 或 git log --oneline --left-right --merge 

    列出发生冲突的文件的两个版本内容:git diff

    分支合并策略

    recursive,默认的合并策略,上述操作都是此策略下的。

    ours recursive, git merge -s ours dev ,通过-s ours指定策略,在合并时只保留当前分支的代码而忽略被合并分支的代码。注意它与上面“ours” recursive 合并选项不是一回事。

    octpus,一次合并多个分支。上述策略默认只能将一个分支合并到当前分支,即一次只能对两个分支进行合并,若有很多分支的修改需要合并在一起则依次合并会导致大量合并节点产生,此时可用octpus,如 git merte b1 b2 b3 。

    resolve

    subtree

     

     内部原理:Git中存的是文件快照,各个提交形成快照引用链,故创建分支、切换分支等只需创建一个指针或修改指针指向即可,速度非常快。

      

    4. 撤销/回退/替换

    从应用的角度介绍几个“反悔”操作。相关可参阅:http://www.ruanyifeng.com/blog/2019/12/git-undo.html

    4.1 丢弃commit(reset)

    reset命令原理:

    (可参阅:https://www.progit.cn/#_git_reset )

    Git中分支名(如dev、master)实际是个指针,指向某个提交。Git中还有个名为HEAD的特殊指针,指向某个分支。如HEAD -> dev -> 38eb946,为便表述,明确下概念:

    “HEADE的指向”意为HEAD指向的分支,此即dev。git checkout branchName 命令移动的即此。

    “HEAD分支的指向”意为HEAD指向的分支所指向的提交,此即38eb946。git reset 命令移动的即此。

    HEAD的作用是记录"下一次commit的父节点":Git总是以HEAD分支的指向作为当前分支的最新提交(可见HEAD效果相当于链表中的头结点者);当有新提交时总会以HEAD分支的指向作为前驱节点来添加新节点、然后移动HEAD分支指向新提交(可见HEAD效果相当于链表中的current指针者)。因此可以通过更改HEAD 分支的指向 来达到重置提交的目的,这就是reset命令

    从执行过程看,运行reset命令时,会尝试执行如下三个步骤(有三种选项:--soft、--mixed、--hard):

    1 移动 HEAD 分支的指向(若指定了 --soft,则到此停止)

    2 使暂存区内容看起来像 HEAD 分支指向的内容(若指定 --mixed 或三种都未指定,则到此停止)

    3 使工作目录内容看起来像暂存区内容(若指定 --hard,则到此停止)

    从执行效果上看,移动HEAD分支的指向会导致增加(往新移)或减少(往旧移)一些commit,根据移动后 这些commit的修改 是否 相应地应用到暂存区和工作区,有三种重置效果:

    1 暂存区不改、工作区不改: git reset --soft $cmtId  软重置。此时执行git status会提示"Changes to be committed"。

    2 暂存区改、工作区不改: git reset --mixed [$cmtId] [file]  ,混合重置,reset不带参数时默认为此。此时执行git status会提示"Changes not staged for commit"。

    3 暂存区改、工作区改: git reset --hard $cmtId  ,硬重置。此时执行git status会发现是干净的,没有要暂存和提交的东西。

    这三种重置操作都可以用来撤销提交,区别在于提交撤销后暂存区或工作区是否也同步撤销而已。另外,与add命令一样,可以加--patch选项来选择性地重置哪些内容。 

    下面给个示例,假设目前工作区、暂存区、HEAD的状态如图1所示,针对目前状态分别执行三种reset后的结果如图所示:

    图1:, 图2:

     图3:,  图4:

    可见,“撤销提交到暂存区的修改”可通过第二种重置来完成,命令为: git reset HEAD xxx  ,执行git status时提示用的(use "git reset HEAD <file>..." to unstage)正是此命令。

    reset命令说明:

     git reset --hard commit_id :硬重置,回退到指定的commit,该commit之后的所有修改和commit均会丢失。

    • 这里版本号可以不全写会根据已写的自动查找;
    • 除了用commit_id外,也可用特殊标记:Git用 HEAD 表示当前分支的最新版本、 HEAD^ 表示上版本、 HEAD^^ 表示上上版本、 HEAD~100 表示往上100个版本以此类推
    • 回退后,HEAD的指向也变成当前的最新版,可能造成往历史版本回滚后滚不回真正的最新版,如对于版本号为1到10的十个版本,回滚到版本5后HEAD就是版本5了此时滚不回10。解决:可以通过git reflog查看版本10提交时的commit_id从而滚到最新版。
    • 通过reflot和reset操作可达到撤销合并操作的目的

     git reset --mixed commit_id [file]  :混合重置,回退到指定commit,暂存区也回退,但工作区不回退。--mixed可省略,如 git reset HEAD 、 git reset HEAD pom.xml 

      git reset --soft commit_id  :软重置,回退到指定的commit,该commit之后的所有提交丢失、但修改会保留在工作区和暂存区。此功能很有用,意味着回退后可以修改提交过的修改并重新提交。 

    4.2 撤销commit(revert)

    最好别用此命令!

    1 rever只有单亲的commit: git revert [倒数第一个提交] [倒数第二个提交] ,撤销最新、次新、... 的提交。

    这里指定的提交列表必须是由最新到旧的且中间不能跳过某些提交。执行后会产生对应个数的新提交,每个新提交都是将对应的原提交所做编辑撤销。

    如假设当前提交历史为Ca <- Cb <- Cc <- Cd,则执行 git revert Cd Cc 得到Ca <- Cb <- Cc <- Cd <- Cd' <- Cc' 

    2 rever具有双亲的commit:对于分支no fast forward合并产生的节点,由于有两个父节点,上述命令不work,因为不知要撤销哪个分支的提交。须通过-m 参数指定rever后要保留哪个父节点,1为合并分支者、2位被合并分支者。示例:  git revert -m 1 HEAD 。示例(假设是master merge topic):

    rever操作实际上是通过将 要撤销的提交  被提交前时的HEAD文件快照(Cc、Cb、C6)替换被提交后的文件(Cd、Cd'、M)来产生新提交的(Cd'、Cc'、^M)。撤销后^M与C6(或Cd'与Cc、Cc'与Cb)文件内容完全一样,但被撤销的提交历史仍然被保留。示例:

    ^M处与M处的区别:提交历史上前者包含后者的所有提交历史;但前者没有C3、C4所对应的文件修改,而后者有。因此,在有双亲节点的rever场景下,会有问题:revert后,topic分支再做修改并合入master时C3、C4的修改并不会被合入(因为前面revert M时相当于基于它们做修改产生^M),这很容易让人迷惑且容易造成后续的工作出错,因此,应该慎用甚至别用revert。当然,也有解决方法,即对 ^M 执行revert得到^^M,^^M与^M相抵消,因此此时^^M与M的相比,前者不仅包含后者的提交历史,而且两者文件内容是完全一样的,基于^^M合并topic时会把C3、C4的修改合入。示例:

    4.3 重写历史commit

    这里的替换commit是指 取消commit、重新编辑后重新提交。替换前后commit id会变化,因为commit id是基于文件内容计算的hash,你所做的修改都变了,显然就不可能让commit id仍不变。

    替换最近一次commit(amend)

     git commit --amend 

    用来修改最近一次的提交,实际上不仅可以修改提交信息,还可以整个把上一次提交替换掉。

    对于最近一次已经commit但未push到远程仓库的提交,可以直接修改其代码并在执行git add命令后通过命令  git commit --amend   来覆盖该commit;显然,如果代码没变化则此时相当于修改最近一次的message。详情参阅:https://stackoverflow.com/questions/179123/how-to-modify-existing-unpushed-commits 

    修改指定某次的历史提交:https://xiewenbo.iteye.com/blog/1285693

    替换前后的commit的id是不一样的。

    用一个commit替换最近连续的若干个commit(soft reset)

    通过上面说的软重置来实现。如下所示,先回退到某个commit,然后修改代码,最后提交。

    git reset --soft $commitId  # 回退到指定commit,该commit之后的commit将移除且相应的修改放入工作区

    edit file ...
    git add . git com
    -m "xxx"

    历史commit删除/修改/替换/合并/拆分/改序(rebase -i)

    rebase -i 命令很强大,可以对历史commit进行删除、修改、替换、多个合为一个、一个拆分为多个、更改先后顺序 等操作。前文的两个替换操作实际上都可由 rebase -i 命令完成。

    关于该命令的使用,推荐参阅:https://www.progit.cn/#_rewriting_history

     git rebase -i HEAD~3 :回退到指定commit并弹出交互式环境让用户选择对其之后的每个commit作何操作,可以 保持不变、修改commit msg、修改内容、合并commit、丢弃commit等,很灵活。

    内部原理:执行上述命令后将首先弹出交互式环境让用户选择对每个commit做什么操作,由之产生to do清单;保存该清单后会将分支回滚到指定commit(上面是HEAD~3),然后将操作清单中的依个应用到当前分支。可见,相当于redo。

    操作清单示例:

    pick f7f3f6d changed my name a bit
    pick 310154e updated README formatting and added blame
    pick a5f4a0d added cat-file

    如果删掉最后一个步骤,则相当于最后一个commit不会redo,从而相当于将该commit从提交历史中移除;如果调换步骤的顺序则相当于修改commit顺序。

    修改大量/全部历史提交(filter-branch)

    场景:从某个提交开始不小心把真实密码写在配置文件里,则要修改该次提交之后的所有提交;全局修改你的邮箱地址;从每一个提交中移除一个文件 等。filter-branch可以用来修改整个提交历史

    note:由于修改commit会使得原commit的id发生改变,因此如果修改已经发布到远程仓库则应慎重考虑使用此命令,否则会影响已经拉取了这些commit的其他人。

    命令示例: git filter-branch --tree-filter 'rm -f passwords.txt' HEAD ,遍历当前分支的每个提交,将指定文件从该提交中删除后重新提交替换原提交。

    --all 选项:使得在所有分支上都执行该命令

    更多可参阅:https://www.progit.cn/#_rewriting_history

    4.4 撤销添加到暂存区的修改(reset HEAD)

     git reset HEAD [filename] 

    工作区文件的变化在执行git add命令后就被提交到了暂存区,通过该命令可以将指定文件的修改从暂存区撤销,回到工作区(可见相当于add命令的相反命令)。

    由于暂存区的文件可能是 创建文件 或 修改文件 后执行git add产生的,故执行上述命令后文件可能为 未追踪或已修改状态。

    实际上,在执行git add命令后,执行git status的结果中就有提示可通过此命令撤回add操作。示例: Changes to be committed: (use "git reset HEAD <file>..." to unstage) 

    此命令与git rm --cached的区别:

    前者表示撤销暂存区中待提交的修改,若暂存区中无待提交修改,则该命令result in nothing;

    而后者是无论暂存区中是否有待提交修改,都会从暂存区中删除文件、将工作区中文件改为未追踪状态。

    4.5 撤销工作区的文件修改(checkout)

     git checkout -- [filename] 

    撤销工作区中还未被add到暂存区(从而当然也就尚未被commit)的指定文件的修改,通过 git checkout .  可以撤销所有文件的修改。

    这是个危险的命令,因为工作区修改了但未暂定的文件一旦被checkout,就无法找回

    4.6 撤销添加的未追踪的文件(clean)

     git clean :移除工作区中所有未追踪的文件

    4.7 撤销历史动作(如合并操作)

    假设当前在test分支且执行完了merge dev操作。此时想撤销合并操作。两步:

    通过 git reflog 命令 找到合并之前的状态。

    18db1460 (HEAD -> test, dev) HEAD@{0}: merge dev: Fast-forward
    629b67c7 (origin/dev-2b-vo-dto-refine, origin/dev, dev-2b-vo-dto-refine) HEAD@{1}: checkout: moving from dev-2b-vo-dto-refine to test
    629b67c7 (origin/dev-2b-vo-dto-refine, origin/dev, dev-2b-vo-dto-refine) HEAD@{2}: checkout: moving from test to dev-2b-vo-dto-refine
    18db1460 (HEAD -> test, dev) HEAD@{3}: reset: moving to HEAD@{2}
    d44ca9db HEAD@{4}: reset: moving to HEAD@{17}
    7098c1d9 (origin/dev_before_VO_DAO_refine, dev_before_VO_DAO_refine) HEAD@{5}: reset: moving to HEAD@{1}
    18db1460 (HEAD -> test, dev) HEAD@{6}: merge dev: Fast-forward
    7098c1d9 (origin/dev_before_VO_DAO_refine, dev_before_VO_DAO_refine) HEAD@{7}: checkout: moving from dev_before_VO_DAO_refine to test
    7098c1d9 (origin/dev_before_VO_DAO_refine, dev_before_VO_DAO_refine) HEAD@{8}: checkout: moving from test to dev_before_VO_DAO_refine
    83ddd758 HEAD@{9}: reset: moving to HEAD
    83ddd758 HEAD@{10}: commit: test
    View Code

    这里合并前为 HEAD@{1} 

    通过 git reset HEAD@{1}  返回到合并之前的状态。

    可见,此法也可以回滚其他操作,如提交操作。

    5. 远程仓库管理

    (参考自Git远程操作详解-阮一峰

    git中有四个部分:工作区(本地项目的根目录)、暂存区(项目根目录的.git文件夹下)、本地仓库(项目根目录的.git文件夹下)、远程仓库。

    Git和其他版本控制系统如SVN的一个不同之处就是有暂存区(即stage或index)的概念。

    git clone

    git fetch

    git pull

    git push

    git remote

     

    git clone 

     git clone <版本库的网址> [<本地目录名>] ,从远程主机克隆一个版本库。

    • 该命令会在本地主机生成一个目录,不指定名称的话与远程主机的版本库同名;
    • Git要求每个远程主机都必须指定一个主机名,默认为origin,可以通过-o参数指定,如 git clone -o jQuery https://github.com/jquery/jquery.git ,此外可以通过 -b 指定克隆版本库的指定分支。
    • git clone还支持HTTP(s)、FTP、SSH、file等协议,如 git clone file:///opt/git/project.git 

    git fetch

     git fetch 用法见上面的分支管理部分。

    git pull

     git pull <远程主机名> <远程分支名>[:<本地分支名>] :从指定远程主机的某个分支拉取更新并merge到指定的本地分支,如 git pull origin next:master 将origin/next分支更新拉取并合并到本地master分支。

    • 若未指定本地分支时默认为当前分支,如 git pull origin next 取回origin/next分支并与当前分支合并,相当于 git fetch origin/next、git merge origin/next两步操作。
    • 若当前分支与远程分支存在追踪关系,git pull就可以省略远程分支名,如git pull origin
      • 在git clone时所有本地分支默认与远程主机的同名分支,建立追踪关系,也即本地的master分支自动"追踪"origin/master分支。
      • Git也允许手动建立追踪关系,如git branch --set-upstream master origin/next指定master分支追踪origin/next分支。
    • 若当前分支只有一个追踪分支,连远程主机名都可以省略,如 git pull

     git pull --rebase <远程主机名> <远程分支名>[:<本地分支名>] :用法与上面类似,只不过加--rebase参数,表示通过rebase命令而非通过merge命令合并。如 git pull --rebase origin next:master 等价于将origin/next分支更新拉取并通过rebase合并到本地master分支。

    merge:git pull (等价于 git fetch & git merge)

    rebase:git pull --rebase(等价于 git fetch & git rebase)

    git push

     git push <远程主机名> <本地分支名>:<远程分支名> :将本地分支的更新,推送到远程主机,如 git push origin master:next 将本地master分支推送到远程origin主机的next分支上。

    • 省略远程分支名表示将本地分支推送到与之存在"追踪关系"的远程分支(通常两者同名),如果该远程分支不存在,则会被新建。如 git push origin master 将本地的master分支推送到origin主机的master分支。
    • 省略本地分支名表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。如 git push origin :master 等同于 git push origin --delete master ,表示删除origin主机的master分支。
    • 若当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。如 git push origin 将当前分支推送到origin主机的对应分支。
    • 若当前分支只有一个追踪分支,那么主机名都可以省略。如 git push 
    • 若当前分支与多个主机存在追踪关系,则可使用-u选项指定一个默认主机,如 git push -u origin master 将本地的master分支推送到origin主机的master分支,同时指定origin为默认主机并将两分支关联起来,在以后的推送或者拉取时就可以不加任何参数使用git push了。 

    git remote

    默认的远程主机名为origin。

      git remote  :管理远程主机名

    • 不带参数时,列出所有远程主机
    • -v参数,列出所有远程主机及网址
    •  git remote add <主机名> <网址>  ,添加远程主机
    •  git remote rm <主机名>  ,删除远程主机
    •  git remote rename <主机名>  ,重命名远程主机
    •  git remote show <主机名> :查看远程仓库信息,会列出远程仓库的分支、与本地分支的对应关系等,示例:
      (base) G104E1:DesignServer zhangsan$ git remote show origin 
      * remote origin
        Fetch URL: git@gitlab.sz.xx.com:SenseStudyCreators/CourseDesignServer.git
        Push  URL: git@gitlab.sz.xx.com:SenseStudyCreators/CourseDesignServer.git
        HEAD branch: master
        Remote branches:
          dev                                                           tracked
          dev_2c                                                        tracked
          dev_2c_raw                                                    tracked
          dev_from_sensestudyserver-dev                                 tracked
          master                                                        tracked
          refs/remotes/origin/dev_2c_multi_login_with_same_account      stale (use 'git remote prune' to remove)
          refs/remotes/origin/dev_2c_raw__multi_login_with_same_account stale (use 'git remote prune' to remove)
          refs/remotes/origin/dev_2c_raw_multi_login_with_same_account  stale (use 'git remote prune' to remove)
          refs/remotes/origin/dev_multi_login_with_same_account         stale (use 'git remote prune' to remove)
          refs/remotes/origin/tmp                                       stale (use 'git remote prune' to remove)
        Local branches configured for 'git pull':
          dev                           merges with remote dev
          dev_2c                        merges with remote dev_2c
          dev_2c_raw                    merges with remote dev_2c_raw
          dev_from_sensestudyserver-dev merges with remote dev_from_sensestudyserver-dev
          master                        merges with remote master
        Local refs configured for 'git push':
          dev                           pushes to dev                           (up to date)
          dev_2c                        pushes to dev_2c                        (up to date)
          dev_2c_raw                    pushes to dev_2c_raw                    (up to date)
          dev_from_sensestudyserver-dev pushes to dev_from_sensestudyserver-dev (up to date)
          master                        pushes to master                        (up to date)
      View Code

      

    仓库关联

    创建新仓库 / 将已有未被管理的项目加入到某个仓库 / 将已有已被管理的项目加入到另一个仓库:

    6. .gitignore

    有些文件我们不希望被git管理,如项目导入到IDE时IDE参数的配置相关的文件、日志文件、编译时的临时文件等。可以在项目根目录创建名为.igtignore文件,在其中列出要忽略的内容即可。

    规则:

    示例:

    # 此为注释 – 将被 Git 忽略
    # 忽略所有 .a 结尾的文件
    *.a
    # 但 lib.a 除外
    !lib.a
    # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
    /TODO
    # 忽略 build/ 目录下的所有文件
    build/
    # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
    doc/*.txt
    # 忽略 doc/ 目录下所有扩展名为 txt 的文件
    doc/**/*.txt
    .gitignore语法示例
    /**/target/
    /target/
    !.mvn/wrapper/maven-wrapper.jar
    
    ### STS ###
    .apt_generated
    .classpath
    .factorypath
    .project
    .settings
    .springBeans
    .sts4-cache
    
    ### IntelliJ IDEA ###
    .idea
    *.iws
    *.iml
    *.ipr
    ### NetBeans ###
    /nbproject/private/
    /build/
    /nbbuild/
    /dist/
    /nbdist/
    /.nb-gradle/
    
    ### DS_Store ###
    .DS_Store
    
    
    .vscode
    /bin/
    .gitignore示例

    GitHub上有各种语言项目的.gitignore模板,见:https://github.com/github/gitignore

    7. 其他 

    其他分支的commit的修改应用到当前分支(cherry-pick/rebase)

    法1:基于cherry-pick

    在当前分支上执行: git cherry-pick $commitid1 $commitid2  ,即可将指定的commit的修改应用到到当前分支,并在当前分支上产生一个新commit,若出现冲突则解决之,然后 git cherry-pick --coutinue ,若没有冲突则会自动合并进来并自动commit。

    注意,这里合入到当前分支后产生的commit id与原分支的commit id不同,因此题中说“修改应用到当前分支”而非“commit合并到当前分支”。

    选项:

     -n :不自动commit

     -e :不自动用被pick的commit的message

    更多可参阅:https://git-scm.com/docs/git-cherry-pick

     

    法2:基于rebase

     git rebase dev :效果相当于基于dev分支创建一个临时分支,然后将当前分支与临时分支最近共同祖先提交以来的当前分支的改动应用到临时分支,然后临时分支的commit完全替换调当前分支的。与cherry-pick一样,是“应用”,故rebase后“应用”的commit的id变了,相当于是“虽效果一样但是不同的提交”。

    由于会修改提交的commit,故对于已经推送到远程仓库的commit,最好不要使用这两个命令去修改该commit,否则修改并提交历史中就会存在一个变更的两次commit从而令人困惑。当然,如果你真得修改了该commit而又不想在历史中出现一个修改产生两个commit的现象,那么可以通过git push -f来覆盖远程服务器上的版本,不过这很危险,谨慎操作!!!

    大小写

    git默认对文件夹或文件名的大小不区分,这是个小坑,需要注意。

    合并其他仓库分支到当前仓库分支并保存两分支各自的提交记录

    场景:初期产品p1、p2后端由于很多东西共用,故将他们放在一个java maven项目A下,分别属于不同的module。后来随着产品变大变多,需要将他们分别拆成单独项目。这里假设p1仍在A上开发(如删除与p2有关的东西并自己飞奔前进了),p2将脱离A单独在新项目B中开发。

    解决:

    可能的方法(错误):一种方法是初始化B项目后将p2在A中的代码都复制到B并合并提交,此法的问题是这样导致p2在A中的所有历史提交记录都丢失了,故不可取

    正确方法:在B中将A的remote host加入到B这样B就可拉取A的某个分支,然后执行带参数的合并操作。在B执行如下命令:

    1 git remote add originA xxx.git # originA为自己取的A的origin名字、末尾为A的仓库地址 
    2 git fetch originA devA:devA #在B上将A的某个分支拉取到B
    3 git checkout devB #切换到B自身的某个分支
    4 git merge devA --allow-unrelated-histories #将A的分支合并到B的该分支

     执行完这些命令后,B中在merge A之前的commit(如 init commit等)仍会在提交历史中。若不希望保留这些commit(如B是刚init的项目,只有init commit等与p2无关的commit),则可这样做:在merge后将B hard reset到p2的最后一次提交。

    合并其他仓库分支到当前分支的子目录

    参阅:https://www.progit.cn/#_git_reset

    类似于上面,将其他仓库的分支拉取到本仓库后,通过命令 git read-tree --prefix=thirdParty/ -u rack_branch 将该分支内容加入到当前分支的某个子目录。

    查看子目录与被引入目标分支的区别,不是diff命令,而是diff-tree: git diff-tree -p rack_branch 

    此合并方式相当于把目标分支作为当前分支的一个子模块。使用场景,如:将所有项目的代码迁移提交到一个地方。

    这个操作的原理实际上是在操作Git内部三种数据类型中的tree object,具体可参阅后文“内部原理”一节。

    连接GitHub/Gitlab

    GtiHub不适合作为个人不愿公开的项目的托管,可以使用Gitlab。

    为Gitlab账号添加SSH key并使用Git连接Gitlab(为GitLab帐号添加SSH keys并连接GitLab):有两种方式从Gitlab上clone项目,http和ssh。

    • 前者每次clone、push等操作都需要用户输入账号的用户名和密码,比较麻烦;
    • 可以使用后者并配置SSH Key来避免这种麻烦。其实本质上使用SSH也需要输入账号和相应密码,但我们通过生成并添加SSH Key使得在clone等操作时计算机帮我们做了身份验证的事。(且由于生成了公钥和私钥并把公钥放到了Gitlab上,它们相当于一对锁和钥匙,在连接时进行公钥和私钥的匹配,所以用SSH方式不需要知道账号的密码了,在生成SSH Key时提示设置的密码也不是账号的密码,而是push操作时的密码,可以不设置,这样以后clone等操作都不需要输入密码了)

    8. 推荐开发流程

    (下图来自同事的分享,觉得很有道理,刚好与自己两年来的实践很吻合)

    说明

    • 三种分支:dev、feature、release,分别为持续开发分支、新大功能分支、稳定分支
    • 不同分支间合并使用merge;同一分支的local和remote同步使用rebase(?这点不同意,要如此的前提是本地和远程各自的修改很少甚至没有冲突,这前提现实中不总能满足)
    • 小需求直接在dev进行。
    • 大需求作为feature拉新feature分支进行。新feature分支在本分支commit后,定期将dev merge进来,以减少未来并入dev时的冲突。新feature分支并不是完全开发完才能合入dev:开发过程中完成部分功能时也可合入dev,然后继续在原feature分支继续开发。
    • 需求做拉release分支提测,提测发现的bug在release改并提交,确认bug修改好后将release并回dev。

    更多工作流可参阅:https://www.progit.cn/#_%E5%88%86%E6%94%AF%E5%BC%80%E5%8F%91%E5%B7%A5%E4%BD%9C%E6%B5%81 

    上面那种开发流程适合定期迭代发版的场景。还有一种场景,要求有个环境较实时地更新最新的feature、且要求该环境的代码几乎不能出现bug。对于这种场景,可以在上述开发流程的基础上,增加个staging分支,使用:

    1 staging分支基于dev分支,且定期将已经开发好完整新feature的dev分支合入staging分支;

    2 基于staging分支部署两套环境:一套是会根据staging分支自动更新的供测试人员测试用的testing环境、另一套是不会根据staging分支自动更新的用于demo的staging的环境。

    开发人员在dev上开发了个完整feature(小或大均可)且自测通过后将dev合入staging分支,此时testing环境自动更新,测试人员可在该环境上测试新功能,反馈bug给开发人员修改;

    测试人员持续在testing环境测试(自动化测试等),测试通过后可按需(如demo展示)手动更新到staging环境;

    当要发新版本时,直接基于staging分支的代码拉release分支,测试人员在该分支上测试。

    该流程的优点有:

    发版时测试省时、bug少:测试人员平时即可在test环境持续地进行测试,故:当要发版时大多数feature已经事先测过了,故回归下即可;事先测过故release分支bug少

    满足新feature的demo需求:基于staging分支的staging环境按需更新——testing环境测试过的feature才手动更新到staging环境,故该环境能够较稳定地展示最新feature。

    上面所述的release分支相当于是master分支,由于release有多个,所以相当于项目有多个master分支,这样就能够满足不同版本的持续迭代优化。 如release2.7相当于2.7版本的master分支。

    9、实践经验

    多同步远程仓库上的提交到本地,并合并到当前分支,以减少最后合并的冲突。

    尽量保持线性(没有分叉)的提交历史:使用rebase来合并远程别人的提交(前提是确认本地和他们的同时改动少,如不是改的同一个大功能),如git pull --rebase 、git rebase等

    10、进阶之内部原理 

    Git内部的数据存储

    前面介绍的绝大多数命令都是应用层命令,它们最终都是转化为底层命令去操纵git存储的数据来起作用的。大千世界形形色色、纷繁复杂,但如果透过现象看本质——看Git内部是如何存储和操作数据的,则就能很容易地理解这些命令的作用并更好地使用它们。

    (参阅:Git对象内部原理-Git SCM

    git 中的数据包括三种类型:文件内容、目录结构、commit信息,它们都在 .git/objects 文件夹下。

    可以通过如下两个命令查看object类型和内容:

     git cat-file -t $SHA_VAL :列出object类型,可能是 blob、tree、commit

     git cat-file -p $SHA_VAL :列出object内容,数据是被压缩过的,直接cat是乱码,用此命令可正确查看到原先内容。

    三种object:

    1 数据对象(blob object):文件内容快照。存储压缩后的文件内容,不存才文件名等其他信息。

    每个被git track过(被执行了git add命令)的文件都会被git 压缩保存在 .git/objects 文件夹下——取文件内容与头信息的 SHA-1 校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下 38 个字符为文件命名 (保存至子目录下)。

    如: .git/objects/58/c9bdf9d017fcd178dc8c073cbfcbb7ff240d6c 。该文件内容是压缩过的,直接看是乱码,可用前述命令查看:

    $ git cat-file -t 58c9
    blob
    $ git cat-file -p 58c9
    hello world!

    2 目录对象(tree object):目录结构快照。储存一个目录结构(类似于文件夹)以及每一个文件(或者子文件夹)的权限、类型、对应的唯一标识(SHA1值)、文件名。示例:

    $ git cat-file -t 4caaa1
    tree
    $ git cat-file -p 4caaa1
    100644 blob 58c9bdf9d017fcd178dc8c0...    a.txt
    100644 blob c200906efd24ec5e783bee7...    b.txt

    3 提交对象(commit object):储存的是一个提交的信息,包括对应目录结构的快照tree的哈希值,上一个提交的哈希值(从HEAD指针获得。若第一个提交则无父节点。若是merge产生的提交则可能出现多个父节点),提交的作者、提交的具体时间、该提交的msg和commitId等。示例:

    $ git cat-file -t 0c96bf
    commit
    $ git cat-file -p 0c96bf
    tree 4caaa1a9ae0b274fba9e3675f9ef071616e5b209
    author xxx  1573302343 +0800
    committer xxx  1573302343 +0800
    [add] init

    三种object的对应关系示意如下:

    由于三种数据间以 Merkle Tree(默克尔树,也称Hash Tree)的形式组织,因此只要其中一个被串改,就很容易发现。

    题外话,Merkle Tree是一种存储hash值的树,父节点的值是子节点值和的hash。通过它可以快速判断数据是否损坏或修改,P2P网络等很多地方用到,示例如下:

    Git的diff与合并

      

    Git中的引用(分支、标签、HEAD、stash)

    Git中的引用包括 本地分支、远程追踪分支、标签、HEAD,它们本质上是指向commit的指针(一级或二级指针)。

    HEAD保存在 .git/HEAD 文件里, $ cat .git/HEAD :结果为  ref: refs/heads/dev 

    本地分支保存在 .git/refs/heads 文件夹下:

    $ ll .git/refs/heads/
    total 16
    -rw-r--r--  1 zhangsan  453037844  41  5 25 22:09 dev
    -rw-r--r--  1 zhangsan  453037844  41  5 18 18:55 dev_oauth
    
    $ cat .git/refs/heads/dev
    ef4065fbadadf59595971ff5f95c6a22a995be3b

    远程追踪分支保存在 .git/refs/remotes 文件夹下:

    $ ll .git/refs/remotes/
    total 0
    drwxr-xr-x  8 zhangshaoming1  453037844  256  5 25 18:21 origin
    
    $ ll .git/refs/remotes/origin/
    total 48
    -rw-r--r--  1 zhangshaoming1  453037844  41  5 25 18:21 dev
    -rw-r--r--  1 zhangshaoming1  453037844  41  4 30 10:39 staging
    -rw-r--r--  1 zhangshaoming1  453037844  41  3 19 17:40 master
    
    $ cat .git/refs/remotes/origin/dev 
    0d41e07621a7f1ddac9e84355a9f5921c8926e84
    View Code

    标签保存在 .git/refs/tags 文件夹下:

    ll .git/refs/tags/
    total 24
    -rw-r--r--  1 zhangsan  453037844  41  3 19 17:40 i18n-1.0.1
    -rw-r--r--  1 zhangsan  453037844  41  3 17 15:23 auth-1.0.0
    -rw-r--r--  1 zhangsan  453037844  41  3 17 15:07 i18n-1.0.0

    stash标签

     git push的过程就是将本地分支相较于远程追踪分支的新commit对象及其所包含的数据对象都上传的过程。


    直接记录快照,而非差异比较

    Git每次提交时有变化的文件产生一个新的文件快照,而SVN则是保留文件的变化。两者的示例图分别如下:

                  

    存储快照而非差异的优点之一是内容比较、创建分支、切换分支、回滚等非常简单迅速,缺点是占用空间较大,但总的来说利远大于弊。

    近乎所有操作都是本地执行

    这是DVCS的优势,由于每个本地仓库都是整个远程仓库的副本,因此绝大多数操作都可以在本地完成而不用与远程服务器交互。

    完整性保证

    Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 这个功能建构在 Git 底层,是构成 Git 哲学不可或缺的部分。 若你在传送过程中丢失信息或损坏文件,Git 就能发现。实际上,Git 数据库中保存的信息(如commit id)都是以文件内容的哈希值来索引,而不是文件名。

    校验和基于 Git 中文件的内容或目录结构计算出来,采用的是SHA-1散列算法。算出的值为由 40 个十六进制字符(0-9 和 a-f)组成字符串。如commit id: 094717bd5aaaa8797dedb721209871a93bf58618  

    11. 参考资料

    文档(推荐):https://git-scm.com/docs

    Pro Git书籍:https://www.progit.cn/ ,实际上与上面的一样,不知是谁抄谁。

  • 相关阅读:
    Git常用命令清单笔记
    MySQL sql语句获取当前日期|时间|时间戳
    mysql-5.7.17.msi安装
    mysql sql语句大全
    解决Springboot集成ActivitiModel提示输入用户名密码的问题
    JAVA Spring MVC中各个filter的用法
    Spring Boot gradle 集成servlet/jsp 教程及示例
    RequireJS 参考文章
    Javascript模块化工具require.js教程
    Javascript模块化编程之路——(require.js)
  • 原文地址:https://www.cnblogs.com/z-sm/p/4203845.html
Copyright © 2011-2022 走看看