标签 svn 下的文章

图解git原理的几个关键概念

img{512x368}

git是那个“爱骂人”的Linux之父Linus Torvalds继Linux内核后奉献给全世界程序员的第二个礼物(不能确定已经逐渐老去的Torvalds能否迸发第三春,第三次给我们一个超大惊喜^_^)。这里再强调一下,git读作/git/,而不是/dʒit/

在诞生十余载后(2005年发布第一版),git毫无争议地成为了程序员版本管理工具的首选,它改变了全世界程序员的代码版本管理和生产协作的模式,极大促进了开源软件运动的发展。进化到今天的git已经成为了一个比较复杂的工具,多数程序员都将目光聚焦在如何记住这些命令并用好这些命令,对这些复杂命令行背后的原理却知之不多,虽然大多数程序员的确不太需要深刻了解git背后的原理^_^。

关于git原理的文章在互联网上也呈现出“汗牛充栋”之势,有些文章“蜻蜓点水”,有些文章“事无巨细”,看后似乎都无法让我满意。结合自己对git原理的学习,我觉得多数人把握住git运作机制的几个关键概念即可,于是就有了这篇文章,我努力尝试给大家讲清楚。

一. 我就是仓库,我拥有全部

我们首先要明确一个git与先前的版本管理工具(主要是subversion)的不同。下面是使用subversion版本管理工具时,程序员进行代码生产以及程序员间围绕代码仓库进行协作的模式:

img{512x368}

图:subversion代码生产和协作模式

众所周知,subversion是基于中心版本仓库进行版本管理协作的版本管理工具。就像上图中那样,所有开发人员开始生产代码的前提是必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录;而进行版本管理操作或者与他人进行协作的前提也是:中心版本仓库必须始终可用。这有点像以太网的“半双工的集线器(hub)模式”:svn中心仓库就像集线器本身,每个程序员节点就像连接到集线器上的主机;当一个程序员提交(commit)代码到中心仓库时,其他程序员不能提交,否则会出现冲突;如果中心仓库挂掉了,那么整个版本管理过程也将停止,程序员节点间无法进行协作,这就像集线器(hub)挂掉后,所有连接到hub上的主机节点间的网络也就断开无法相互通信一样。

如果我们使用git,我们是不需要“集线器”的:

img{512x368}

图:git代码生产和协作模式

如上图所示,git号称分布式版本管理系统,本质上是没有像subversion中那个所谓的“中心仓库”的。每个程序员都拥有一个本地git仓库,而不仅仅是一份代码拷贝,这个仓库就是一个独立的版本管理节点,它拥有程序员进行代码生产、版本管理、与其他程序员协作的全部信息。即便在一台没有网络连接的机器上,程序员也能利用该仓库完成代码生产和版本管理工作。在网络ready的情况下,任意两个git仓库之间可以进行点对点的协作,这种协作无需中间协调者(中心仓库)参与。

二. github实现了基于git网络协作的控制平面

git实现了分布式版本管理系统,每个git仓库节点都是自治的。诸多git仓库节点一起形成了一个分布式git版本管理网络。这样的一个分布式网络存在着与普通分布式系统的类似的问题:如何发现对端节点的git仓库、如何管理和控制仓库间的访问权限等。如果说linus的git本身是这个分布式网络的数据平面工具(实现client/server间的双向数据通信),那么这个分布式网络还缺少一个“控制平面”

github恰恰给出了一份git分布式网络控制平面的实现:托管、发现、控制…。其名称中含有的“hub”字样让我们想起了上面的“hub模式”:

img{512x368}

图:github:git分布式网络控制平面的实现

我们看到在github的git协作模式实践中,引入了“中心仓库”的概念,各个程序员的节点git仓库源于(clone于)中心仓库。但是它和subversion的“中心仓库”有着本质的不同,这个仓库只是一个“upstream”库、是一个权威库。它并不是“集线器”,也没有按照“集线器”的那种工作模式进行协作。所有程序员节点的代码生产和版本管理操作完全可以脱离该所谓“中心库”而独立实施。

三. objects是个筐,什么都往里面装

上面都是从“宏观”谈git的一些与众不同的理念,而git原理,其实是从这一节才真正开始的^_^。

我们知道:每个git仓库的所有数据都存储在仓库顶层路径下的.git目录下:

$tree -L 1 -F
.
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks/
├── index
├── info/
├── logs/
├── objects/
└── refs/

5 directories, 5 files

而在这些目录和文件中,又以objects路径下的数据内容最多,也最为重要。在git的设计中,objects目录就是一个“筐”,git的核心对象(object)都往里面“装”
img{512x368}

图:git核心数据对象类型与objects目录

从上图中,我们看到objects中存储的最主要的有三类对象:blob、commit和tree。这时你可能还不知道它们究竟是啥。不过没关系,我们通过一个例子来做一下“对号入座”。

我们在一个目录下建立git-internal-repo-demo目录,进入该目录,执行下面命令创建一个git仓库:

➜  /Users/tonybai/test/git/git-internal-repo-demo git:(master) ✗ $git init .
Initialized empty Git repository in /Users/tonybai/Test/git/git-internal-repo-demo/.git/

这是一个处于初始状态的git仓库,我们看看存储git仓库数据的.git目录下的结构:

➜  /Users/tonybai/test/git/git-internal-repo-demo git:(master) $tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

8 directories, 15 files

这个时候,objects这个筐还是空的!我们这就为仓库添点内容:

$mkdir -p cmd/demo

在cmd/demo目录下添加main.go文件,内容如下:

// cmd/demo/main.go
package main

import "fmt"

func main() {
    fmt.Println("hello, git")
}

接下来我们使用git add将cmd/demo目录加入到stage区:

$git add .

$git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   cmd/demo/main.go

这时我们来看一下objects这个筐是否有变化:

├── objects
│   ├── 3e
│   │   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
│   ├── info
│   └── pack

我们有一个object已经被装入到“筐”中了。我们看到objects目录下是一些以哈希值命名的文件和目录,其中目录由两个字符组成,是每个object hash值的前两个字符。hash值后续的字符串用于命名对应的object文件。在这里我们的object的hash值(实质是sha-1算法)为3e759ef88951df9b9b07077a7ec01f96b8e659b3,于是这个对象就被放入名为3e的目录下,对应的object文件为759ef88951df9b9b07077a7ec01f96b8e659b3。

我们使用git提供的低级命令查看一下这个object究竟是什么,其中git cat-file -t查看object的类型,git cat-file -p查看object的内容:

$git cat-file -t 3e759ef889
blob

$git cat-file -p 3e759ef889
package main

import "fmt"

func main() {
    fmt.Println("hello, git")
}

我们看到objects这个筐中多了一个blob类型的对象,对象内容就是前面main.go文件中内容。

接下来,我们提交一下这次变更:

$git commit -m"first commit" .
[master (root-commit) 3062e0e] first commit
 1 file changed, 7 insertions(+)
 create mode 100644 cmd/demo/main.go

再来看看.git/objects中的变化:

├── objects
│   ├── 1f
│   │   └── 51fe448aacc69c0f799def9506e61ed3eb60fa
│   ├── 30
│   │   └── 62e0ebad9415b704e96e5cee1542187b7ed571
│   ├── 3d
│   │   └── 2045367ea40c098ec5c7688119d72d97fb09a5
│   ├── 3e
│   │   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
│   ├── 40
│   │   └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b
│   ├── info
│   └── pack

我们看到筐里被一下子新塞入4个object。我们分别看看新增的4个object类型和内容都是什么:

$git cat-file -t 1f51fe448a
tree
$git cat-file -p 1f51fe448a
100644 blob 3e759ef88951df9b9b07077a7ec01f96b8e659b3    main.go

$git cat-file -t 3062e0ebad
commit
$git cat-file -p 3062e0ebad
tree 406d08e1159e03ae82bcdbe1ad9f076a04a41e2b
author Tony Bai <bigwhite.cn@aliyun.com> 1586243612 +0800
committer Tony Bai <bigwhite.cn@aliyun.com> 1586243612 +0800

first commit

$git cat-file -t 3d2045367e
tree
$git cat-file -p 3d2045367e
040000 tree 1f51fe448aacc69c0f799def9506e61ed3eb60fa    demo

$git cat-file -t 406d08e115
tree
$git cat-file -p 406d08e115
040000 tree 3d2045367ea40c098ec5c7688119d72d97fb09a5    cmd

这里我们看到了另外两种类型的object被加入“筐”中:commit和tree类型。objects这个筐里目前有了5个object,我们不考虑git是以何种格式存储这些object的,我们想知道的是这几个object的关系是什么样的。请看下一小节^_^。

四. 每个commit都是一个git仓库的快照

要理清objects“筐”中各object间的关系,就必须要把握住一个关键概念:“每个commit都是git仓库的一个快照” – 以一个commit为入口,我们能将当时objects下面的所有object联系在一起。因此,上面5个object中的那个commit对象就是我们分析各object关系的入口。我们根据上述5个object的内容将这5个object的关系组织为下面这幅示意图:

img{512x368}

图:commit、tree、blob对象之间的关系

通过上图我们看到:

  • commit是对象关系图的入口;

  • tree对象用于描述目录结构,每个目录节点都会用一个tree对象表示。目录间、目录文件间的层次关系会在tree对象的内容中体现;

  • 每个commit都会有一个root tree对象;

  • blob对象为tree的叶子节点,它的内容即为文件的内容。

上面仅是一次commit后的关系图,为了更清晰的看到多个commit对象之间关系,我们再来对git repo进行一次变更提交:

我们创建pkg/foo目录:

$mkdir -p pkg/foo

然后创建文件pkg/foo/foo.go,其内容如下:

// pkg/foo/foo.go
package foo

import "fmt"

func Foo() {
    fmt.Println("this is foo package")
}

提交这次变更:

$git add pkg
$git commit -m"add package foo" .
[master 6f7f08b] add package foo
 1 file changed, 7 insertions(+)
 create mode 100644 pkg/foo/foo.go

下面是提交变更后的“筐”内的对象:

$tree objects
objects
├── 1f
│   └── 51fe448aacc69c0f799def9506e61ed3eb60fa
├── 29
│   └── 3ae375dcef1952c88f35dd4d2a1d4576dea8ba
├── 30
│   └── 62e0ebad9415b704e96e5cee1542187b7ed571
├── 3d
│   └── 2045367ea40c098ec5c7688119d72d97fb09a5
├── 3e
│   └── 759ef88951df9b9b07077a7ec01f96b8e659b3
├── 40
│   └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b
├── 65
│   └── 5dd3aae645813dc53834ebfa8d19608c4b3905
├── 6e
│   └── e873d9c7ca19c7fe609c9e1a963df8d000282b
├── 6f
│   └── 7f08b14168beb114c3cc099b8dc1c09ccd4739
├── cc
│   └── 9903a33cb99ae02a9cb648bcf4a71815be3474
├── info
└── pack

12 directories, 10 files

object已经多到不便逐一分析了。但我们把握住一点:commit是分析关系的入口。我们通过commit的输出或commit log(git log)可知,新增的commit对象的hash值为6f7f08b141。我们还是以它为入口分析新增object的关系以及它们与之前已存在的object的关系:

img{512x368}

图:commit、tree、blob对象之间的关系1

从上图我们看到:

  • git新创建tree对象对应我们新建的pkg目录以及其子目录;

  • cmd目录下的子目录和文件内容并未改变,因此这次commit所对应的root tree对象(293ae375dc)直接使用了已存在的cmd目录对应的对象(3d2045367e);

  • 新commit对象会将第一个commit对象作为parent,这样多个commit对象之间构成一个单向链表。

上面的两个提交都是新增内容,我们再来提交一个commit,这次我们对已有文件内容做变更:

将cmd/demo/main.go文件内容变更为如下内容:

// cmd/demo/main.go
package main

import (
    "fmt"

    "github.com/bigwhite/foo"
)

func main() {
    fmt.Println("hello, git")
    foo.Foo()
}

提交变更:

$git commit -m"call foo.Foo in main" .
[master 2f14635] call foo.Foo in main
 1 file changed, 6 insertions(+), 1 deletion(-)

和上面的分析方法一样,我们通过最新commit对应的hash值2f146359b4对新对象和现存对象的关系进行分析:

img{512x368}

图:commit、tree、blob对象之间的关系2

如上图,第三次变更提交后,我们看到:

  • 由于main.go文件变更,git重建了main.go blob对象、demo、cmd tree对象

  • 由于pkg目录、其子目录布局、子目录下文件内容没有改变,于是新commit对象对应的root tree对象直接“复用”了上一次commit的pkg tree对象。

  • 新commit对象加入commit对象单向链表,并将上一次的commit对象作为parent。

我们看到沿着最新的commit对象(2f146359b4),我们能获取当前仓库的最新结构布局以及各个blob对象的最新内容,即最新的一个快照!

五. object是不可变的,默克尔树(Merkle Tree)判断变化

从上面的三次变更,我们看到无论哪种对象object,一旦放入到objects这个“筐”就是不可变的(immutable)。即便是第三次commit对main.go进行了修改,git也只是根据main.go的最新内容创建一个新的blob对象,而不是修改或替换掉第一版main.go对应的blob对象。

对应目录的tree object亦是如此。如果某目录下的二级目录发生变化或目录下的文件内容发生改变,git会新生成一个对应该目录的tree对象,而不是去修改原先已存在的tree对象。

实际上,git tree对象的组织本身就是一棵默克尔树(Merkle Tree)

默克尔树是一类基于哈希值的二叉树或多叉树,其叶子节点上的值通常为数据块的哈希值,而非叶子节点上的值,是将该节点的所有孩子节点的组合结果的哈希值。默克尔树的特点是,底层数据的任何变动,都会传递到其父亲节点,一直到树根。

img{512x368}

图:默克尔树(图片来自网络)

以上图为例:我们自下向上看,D0、D1、D2和D3是叶子节点包含的数据。N0、N1、N2和N3是叶子节点,它们是将数据(也就是D0、D1、D2和D3)进行hash运算后得到的hash值;继续往上看,N4和N5是中间节点,N4是N0和N1经过hash运算得到的哈希值,N5是N2和N3经过hash运算得到的哈希值。(注意,hash值计算方法:把相邻的两个叶子结点合并成一个字符串,然后运算这个字符串的哈希)。最后,Root节点是N4和N5经过hash运算后得到的哈希值,这就是这颗默克尔树的根哈希。当N0包含的数据发生变化时,根据默克尔树的节点hash值形成机制,我们可以快速判断出:N0、N4和root节点会发生变化

对应git来说,叶子节点对应的就是每个文件的hash值,tree对象对应的是中间节点。因此,通过默克尔树(Merkle Tree)的特性,我们可以快速判断哪些对象对应的目录或文件发生了变化,应该重新创建对应的object。我们还以上面的第三次commit为例:

img{512x368}

图:通过默克尔树(Merkle Tree)的特性判断哪些对象发生变化需要重新创建

如上图所示,第三次commit是因为cmd/demo/main.go内容发生了变化,根据merkle tree特性,我们可以快速判断红色的object会随之发生变化。于是git会自底向上逐一创建这些新对象:main.go文件对应的blob对象以及demo、cmd以及根节点对应的tree对象。

六. branch和tag之所以轻量,因为它们都是“指针”

使用subversion时,创建branch或打tag使用的是svn copy命令。svn copy执行的就是真实的文件拷贝,相当于将trunk下的目录和文件copy一份放到branch或tag下面,建立一个trunk的副本,这样的操作绝对是“超重量级”的。如果svn仓库中的文件数量庞大且size很大,那么svn copy执行起来不仅速度慢,而且还会在svn server上占用较大的磁盘存储空间,因此使用svn时,打tag和创建branch是要“谨慎”的。

而git的branch和tag则极为轻量,我们来给上面例子中的仓库创建一个dev分支:

$git branch dev

我们看看.git下有啥变化:

.

└── refs
    ├── heads
    │   ├── dev
    │   └── master
    └── tags

我们看到.git/refs/heads下面多出了一个dev文件,我们查看一下该文件的内容:

$cat refs/heads/dev
2f146359b475909f2fdcdef046af3431c8077282

$git log --oneline

2f14635 (HEAD -> master, dev) call foo.Foo in main
6f7f08b add package foo
3062e0e first commit

对比发现,dev文件中的内容恰是最新的commit对象:2f146359b475909f2fdcdef046af3431c8077282。

我们再来给repo打一个tag:

$git tag v0.0.1

同样,我们来查看一下.git目录下的变化:

└── refs
    ├── heads
    │   ├── dev
    │   └── master
    └── tags
        └── v0.0.1

我们看到在refs/tags下面增加一个名为v0.0.1的文件,查看其内容:

$cat refs/tags/v0.0.1
2f146359b475909f2fdcdef046af3431c8077282

和dev分支文件一样,它的内容也是最新的commit对象:2f146359b475909f2fdcdef046af3431c8077282。

可见,使用git创建分支或tag仅仅是创建了一个指向某个commit对象的“指针”,这与subversion的副本操作相比,简直不能再轻量了。

前面说过,一个commit对象都是一个git仓库的快照,切换到(git checkout xxx)某个branch或tag,就是将本地工作拷贝切换到commit对象所代表的仓库快照的状态。当然也会将commit对象组成的单向链表的head指向该commit对象,这个head即.git/HEAD文件的内容。

七. 小结

到这里,git原理的几个关键概念就交代完了,再回顾一下:

  • 和subversion这样的集中式版本管理工具最大的不同就是每个程序员节点都是git仓库,拥有全部开发、协作所需的全部信息,完全可以脱离“中心节点”;

  • 如果说git聚焦于数据平面的功能,那么github则是一个基于git网络协作的控制平面的实现;

  • objects是个筐,什么都往里面装。git仓库的核心数据都存在.git/objects下面,主要类型包括:blob、tree和commit;

  • 每个commit都是一个git仓库的快照,记住commit对象是分析对象关系的入口;

  • git是基于数据内容的hash值做等值判定的,object是不可变的,默克尔树(Merkle Tree)用来快速判断变化。

  • branch和tag因为是“指针”,因此创建、销毁和切换都非常轻量。

八. 参考资料


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

使用git操作svn仓库

如今,虽然Git已经大行其道,但是仍有很多IT公司和组织依旧在使用集中式的版本控制系统subversion,尤其是一些传统软件公司,他们倾向于集中式的联网开发。如果你是一个Git fans,并且你要是遇到代码仓库依旧是使用subversion进行版本控制的情况,你又该如何施展呢?

其实git很早就支持与subversion repo的互操作了,2011年我就曾写过一篇《小试git-svn》的博文,也正是那一年,我第一次使用git操作subversion仓库。

《小试git-svn》一文仅是通过文字性描述简要说明了git操作svn repo的基本命令和功能,并未结合实际例子,也缺少直观的图片展示,并且未涉及branch和tag的操作。这里打算再写一篇关于使用git操作svn仓库的文章,相比于前者,我期望该文能更为系统并结合demo图文并茂的把使用git操作svn仓库这个事情讲的更形象和透彻一些。

一. 使用git操作svn repo的基本工作流

使用git操作svn repo的多数场景是已经存在一个svn repo,git fans想用git命令与之交互。下图展示了使用git操作这样的svn repo的基本工作流:

img{512x368}

下面我们就用一个demo来详细地说明一下这个基本工作流。

1. 建立一个svn demo库

自己搭建一个svn server还是比较费力的,我们选择一个在线的svn代码托管SaaS服务:svnbucket.com。我们在svnbucket.com上注册账号并创建一个svn repo:test-git-svn,该repo采用标准的项目布局(trunk/branches/tags):

img{512x368}

接下来我们就开始git操作svn repo的过程!

2. 通过git首次获取svn仓库

git是分布式版本管理工具,无论是git repo还是svn repo,如果要用git操作,那么首先需要获取到repo的所有数据。git提供了svn子命令来操作远程的svn repo库,我们看一下首次获取svn repo信息的过程:

$git svn clone svn://svnbucket.com/bigwhite/test-git-svn/
Initialized empty Git repository in /Users/tony/Test/git-svn-test/test-git-svn/.git/
W: +empty_dir: branches
W: +empty_dir: tags
W: +empty_dir: trunk
r1 = 8cfdc2f6059ff06f53c83d64518dcba146722c04 (refs/remotes/git-svn)
Checked out HEAD:
  svn://svnbucket.com/bigwhite/test-git-svn r1
creating empty directory: branches
creating empty directory: tags
creating empty directory: trunk

$tree ./test-git-svn
./test-git-svn
├── branches
├── tags
└── trunk

3 directories, 0 files
$cd test-git-svn
$git branch -a
* master
  remotes/git-svn

可以看到:我们通过git svn clone(注意:不是git clone)将远程server上的svn repo下载到了本地,后续我们就可以在本地host上快乐地使用git管理本地的代码了。

3. 从svn repo中同步最新的代码变更

接下来,远程的svn仓库经常会发生了变更,某开发人员向svn仓库提交了一些initial code,比如在trunk下建立git-svn-demo目录,并创建go.mod和main.go:

//在svn repo中的trunk/git-svn-demo目录下:

$cat main.go
package main

import "fmt"

func main() {
    fmt.Println("git-svn-demo initial version")
}

$cat go.mod
module github.com/bigwhite/git-svn-demo

如果我们本地使用svn工具,我们只需在联网的情况下通过svn update命令即可将远程svn repo的最新改动同步到本地working copy中。但在git下,我们不能像git repo同步那样使用git pull来同步,而是需要使用git svn rebase来获取svn repo中的最新更新,并rebase我们的工作目录(working copy):

 $git svn rebase
    A    trunk/git-svn-demo/go.mod
    A    trunk/git-svn-demo/main.go
r2 = f826b74bfff2799deaafbca81354c38e0862509c (refs/remotes/git-svn)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/git-svn.

$tree .
.
├── branches
├── tags
└── trunk
    └── git-svn-demo
        ├── go.mod
        └── main.go

4 directories, 2 files

git svn rebase子命令会根据svn上的revision创建对应的commit,这一命令几乎等效于”svn update”,同样也可能会存在远程svn repo中的代码与git repo冲突的可能性,解决冲突的方法在《小试git-svn》中已经做了描述,这里就不赘述了。

4. 将代码更新推送到远程svn repo

在这种模式下,本地开发已经完全变成了基于git的开发模式,开发者可以自由地发挥git的各种优势了,再也不用担心本地代码没有版本控制而出现各种“误删除”、“意外覆盖”的情况了。开发测试并提交(只需普通git commit)到local git repo后,最终还是要将这些commit推送到远程的svn repo中。这里我们不能用push,而要用git svn dcommit:

// 本地git repo中更新后的main.go

$cat main.go
package main

import "fmt"

func main() {
    fmt.Println("git-svn-demo: git-svn dcommit v0")
}

先提交到git本地的仓库:

$git commit -m"[git svn]: first commit" .
[master be36a7f] [git svn]: first commit
 1 file changed, 1 insertion(+), 1 deletion(-)

然后再“推送”到远程的svn 仓库:

$git svn dcommit
Committing to svn://svnbucket.com/bigwhite/test-git-svn ...
    M    trunk/git-svn-demo/main.go
Committed r3
    M    trunk/git-svn-demo/main.go
r3 = e35efbe999cd035b2d5d67886c9a786ef86c681e (refs/remotes/git-svn)
No changes between be36a7f1164b73a994f28ee3b0e0bb711b5ba2ff and refs/remotes/git-svn
Resetting to the latest refs/remotes/git-svn

dcommit会将git repo当前branch与远程svn repo中的差异的git commit都提交到svn repo,并为每个git commit生成一个对应的svn revision。这和”git push”很类似。

我们再来本地做两次git commit:

$git commit -m"[git svn]: commit #2" .

$git commit -m"[git svn]: commit #3" .

dcommit到svn repo:

$git svn dcommit
Committing to svn://svnbucket.com/bigwhite/test-git-svn ...
    M    trunk/git-svn-demo/main.go
Committed r4
    M    trunk/git-svn-demo/main.go
r4 = c997db60e3d82c97ce8da23b308d611005740844 (refs/remotes/git-svn)
    M    trunk/git-svn-demo/main.go
Committed r5
    M    trunk/git-svn-demo/main.go
r5 = 3b6215a3e5ae0659743e1e8063f842448c19147c (refs/remotes/git-svn)
No changes between ee0df22b9f41882518a7c7b975c38924a9422395 and refs/remotes/git-svn
Resetting to the latest refs/remotes/git-svn

我们看到git svn为每个commit生成一个对应的svn revision(svn版本号),这里是r4、r5。

二. 利用git branch的优势

和svn建立branch的“重量级”操作(文件copy)相比,git的branch创建和切换可谓“超轻量级”。因此在日常使用git中,多数开发者都会充分发挥git branch的优势,通过在不同branch上的操作、分支的merge等来减少对master的并发修改带来冲突的影响。

我们经常使用feature branch或bugfix branch。以feature branch为例,在feature branch上一般会有多个commit。但在merge到master分支时,我们可以选择多种merge策略,或是fast forward,或是多个commit自动合并为一个commit,又或git merge支持–squash策略(即只merge代码到本地Working copy,不commit到git repo,后续可作为一个commit手工提交到git repo)。

我个人在用git操作svn repo库时,在git本地开发中,更倾向于使用git merge –squash的方法,因为在feature branch上,我更喜欢频繁的小变更的提交,导致commit很多。如果这些commit都dcommit到svn库,可能让svn commit history项目过多,有些commit甚至没有比较完善的意义。

我们在上面的demo上演示一下这个过程。

在本地建立新分支:feature-branch-1:

$git checkout -b feature-branch-1
Switched to a new branch 'feature-branch-1'

在feature-branch-1做两次修改并commit:

$git commit -m"add foo" .
[feature-branch-1 d12ca00] add foo
 1 file changed, 4 insertions(+)

$git commit -m"add bar" .
[feature-branch-1 160e5ed] add bar
 1 file changed, 4 insertions(+)

回到master分支,merge feature分支的修改,并合并为本地的一次commit:

 $git checkout master
Switched to branch 'master'

$git merge feature-branch-1 --squash
Updating 3b6215a..160e5ed
Fast-forward
Squash commit -- not updating HEAD
 trunk/git-svn-demo/main.go | 8 ++++++++
 1 file changed, 8 insertions(+)

$git commit -m"[git svn]: add foo and bar function" .
[master fe8f153] add foo and bar function
 1 file changed, 8 insertions(+)

接下来,将这次合并的commit同步到svn repo上:

$git svn dcommit
Committing to svn://svnbucket.com/bigwhite/test-git-svn ...
    M    trunk/git-svn-demo/main.go
Committed r6
    M    trunk/git-svn-demo/main.go
r6 = 37bbfbdb99cb7331057a05b72dc55b3faf55b645 (refs/remotes/git-svn)
No changes between fe8f153cac62e027ca068fdd55c2bdaa8751aaf8 and refs/remotes/git-svn
Resetting to the latest refs/remotes/git-svn

三. 通过git为svn库建立branch和打tag

通过git为svn repo建立branch和tag这类操作其实并没有体现出git的优势,因此日常开发人员一般会用svn命令直接操作svn repo,而不是用git svn子命令。但这里我们仍然要介绍一下通过git为svn repo建立branch和tag的方法。

我们先来看看创建branch:

$git svn branch feature-branch-1-from-git
Multiple branch paths defined for Subversion repository.
You must specify where you want to create the branch with the --destination argument.

我们看到git svn branch命令出错:让我们指定–destination参数,那我们就再来一遍:

 $git svn  branch feature-branch-1-from-git --destination=branches
Unknown branch destination branches

依旧报错!似乎git不认识“branches”这个存放branch的目录!要想解决这个问题,我们需要对.git/config中的配置做些变更,添加最后两行:

$cat .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
        ignorecase = true
        precomposeunicode = true
[svn-remote "svn"]
        url = svn://svnbucket.com/bigwhite/test-git-svn
        fetch = :refs/remotes/git-svn
        branches = branches/*:refs/remotes/*
        tags = tags/*:refs/remotes/*

原先的.git/config中并没有设置branhes和tags的入口。我们再来试一下建立branch:

git svn --username=bigwhite  branch feature-branch-1-from-git
Copying svn://svnbucket.com/bigwhite/test-git-svn at r8 to svn://svnbucket.com/bigwhite/test-git-svn/branches/feature-branch-1-from-git...
Authorization failed: Unable to connect to a repository at URL 'svn://svnbucket.com/bigwhite/test-git-svn': Can't get password at /usr/local/Cellar/git/2.12.2/libexec/git-core/git-svn line 1200.

仍然报错!不过这个错误应该是git(我使用的是2.12.2版本)的一个bug,我们用try-run方式运行的结果却是一切ok的:

$git svn --username=bigwhite -n branch feature-branch-1-from-git
Copying svn://svnbucket.com/bigwhite/test-git-svn at r8 to svn://svnbucket.com/bigwhite/test-git-svn/branches/feature-branch-1-from-git...

打tag的方式与建立 branch的方式类似:

 $git svn tag v1.0.0 -n  -m "[git svn]: tag v1.0.0" --destination=tags
Copying svn://svnbucket.com/bigwhite/test-git-svn at r5 to svn://svnbucket.com/bigwhite/test-git-svn/tags/v1.0.0...

四. 小结

git svn子命令是git fans操作svn repo的利器。由于git svn clone svn_repo后的repo就是一个标准的本地git repo,因此我们还可以为该git repo建立remote upstream repo,这样就可以在local git repo、remote git repo以及remote svn repo三者之间进行代码变更的同步了,当然这种场景操作还是蛮复杂的,也相对少见。

个人建议,无论个人还是组织,即便使用svn中心repo,在本地也尽量用git来进行源码版本管理,并通过git svn与中心svn repo互操作。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats