Fossil 是什么?

Fossil 是一款分布式版本控制软件,由 SQLite 的主要作者 D. Richard Hipp 为了支持 SQLite 的开发而所设计。

Fossil 主要由 C 和一些 SQL 语句写成,在实现内置 Web 服务器时,为了排版,用 C 实现了一个内置的小的 Tcl 方言,必要的地方用到了一点 JavaScript。因此,Fossil 小巧而精致,所有内容最终包含到单独一个可执行文件当中。

Fossil 是跨平台的,除了版本控制之外,还通过内置的 Web 服务器提供了包括问题跟踪 (工单系统) 、wiki、论坛和博客等功能。

Fossil 是自由软件,遵循BSD许可证发布。

为什么要用版本控制软件?

咳,问倒我了,下一题吧。

初始化版本库

首先 cd 到工作文件夹,然后执行:

$ fossil init <版本库名>

譬如说我想初始化一个名为 WTF 的版本库,那么输入 fossil init WTF ,这样,在工作目录就生成了一个名为 WTF 的 SQLite 数据库文件。

但是如果我想从别人那铐一份他们正在开发的项目呢?只需执行克隆命令:

$ fossil clone URL <版本库名>

当然,前提是被克隆的那位仁兄打开 Fossil 的内置服务器让你可以连上来克隆。方法也很简单,只需让他执行:

$ fossil server

通常来说,团队合作会设置一台常开的设备作为远程版本库。否则,甲克隆乙的,乙克隆丙的,甲提交更改还要通过乙才能提交到丙那,就很混乱。

Git 中,初始化操作是很相似的。只是,git init 之后不需要跟上版本库名,Git 会在当前目录下生成一个名为 .git 的隐藏文件夹,而当前所处的目录则成了唯一的工作目录。同样,git clone URL 之后的命名只是可选项,默认以远程版本库的名字作为克隆下来的文件夹名称,而在 Fossil 中则是必须的,设计如此。

打开工作空间

对于 Git 用户来说,可能有些困惑——初始化时所处目录不就是工作空间么?

实际上,Fossil 采用了与 Git 完全不同的设计,Fossil 版本库只是一个 SQLite 数据库文件,它可以在不同目录多次检出。这种设计清晰易解,只需 cd 就能进入不同版本进行编辑和 diff 。我在之后会对比不同设计所导致的不同操作思路。

通常可以在当前目录直接打开工作空间:

$ fossil open <版本库名>

操作完成后,当前目录会产生一个名为 .fslckout 的隐藏文件来标记工作空间。如果是克隆的项目,Fossil 会把主分支上最新的文件放置到当前目录中。

当然啦,cd 到其他目录打开工作空间完全是可行的,只需要在 版本库名 处填写正确的文件路径。

提交

譬如说我在工作空间中新建了一个文本文件,名为 ass.py,现在想要提交更改。

我所要做的是添加对 ass.py 这个文件的追踪:

$ fossil add ass.py

然后提交更改:

$ fossil commit -m "新建 ass.py"

听起来跟 Git 很像嘛。其实并不是。

Git 引入了一个叫暂存区的概念,所有文件在提交前都要先放入暂存区,无论是新建的还是从前提交过的。而在 Fossil 中并不存在,只需要单纯地添加文件的跟踪即可。譬如说我们的例子,现在我在 ass.py 里加了一个函数,然后想提交它:

$ fossil commit -m "添加交易函数"

这样就可以了。而在 Git,你需要先 git add ass.py,再 git commit

为什么?因为设计如此。Git 把这些工作交给了用户,用户可以以此做一些细致的操作,譬如用户一时兴起同时更改了 A 和 B 两个文件,但不想一次全提交,于是先 add A,commit,再 add B,commit,这样提交时间线看起来好像很棒。再譬如说 add A 后家里的猫跳上键盘把 A 文件踩得乱七八糟,这时可以把暂存的 A 文件拿出来覆盖被踩得乱七八糟的在工作目录的 A 文件。

回到 Fossil,如果我不再想追踪 ass.py 的变动了呢?只需:

$ fossil rm ass.py

或者:

$ fossil forget ass.py

通常,rm 或者 delete 或者 forget 只是取消追踪,但是通过设置,rmdelete 可以直接删除磁盘上的文件,而 forget 则是无论怎么设置都不会删除磁盘文件。

分支 (branch)

当想添加一些新 bug 特性或者做一些实验性的更改亦或修复一个祖传 bug 时,就要用到分支 (branch) 功能啦。你只需要在当前工作目录直接改改改,改完后执行:

$ fossil commit --branch <分支名>

这样就提交到了一个新分支上。

你继续在这个新分支上编辑着,突然发现主分支 (trunk) 上有个该死的错误急需修改,但是手头新分支的工作还没完成,又不好就这么提交了,咋整啊?

没关系, Fossil 支持多次签出,你只需 cd 到一个新的工作目录,然后签出需要修改的那一版:

$ fossil open <版本库文件名> <版本编号>

而版本编号可以通过 fossil ui 弹出的页面或者 fossil timeline -R 版本库文件名 来查看。当要打开的是某分支最新提交,可以直接填分支名。

现在你就可以在你想要动刀的那个版本上编辑了,编辑完,fossil commit 提交,然后 fossil close 关闭工作空间。现在可以放心 cd 回先前的工作空间继续改新分支了。

经过一番鏖战,你终于完成了新分支的编辑工作,现在想合并到主分支 (trunk) 中。你需要做的是 commit 最后一次修改:

$ fossil commit --close

这行命令中, –close 的意思是关闭分支,它等同于:

$ fossil tag add --raw closed <分支编号>

意思是说,给当前所处版本的分支贴上一个 closed 的底层标签 (raw,请允许我这么翻译) ,被标记 closed 以后,原分支名就可以重复利用了。

21年7月的更新之后这个标签操作被进一步简化为:

$ fossil branch close <分支名>

之后将当前工作目录切换到需要合并到的分支,在我们这是指 trunk 的最新版:

$ fossil update trunk

这样工作空间就更新到需要合并到的分支上的最新提交版本上了,执行:

$ fossil merge <要合并的分支名>

Fossil 会尝试合并文件内容。不过合并冲突是难免的。这时就需要你手动解决冲突。Fossil 会在冲突的地方作出标记,并且把冲突的文件在两个分支上的源文件放到当前工作空间中提供参考。

总之,你解决了合并冲突,合并编辑完成了,最后需要做的是提交合并后的版本:

$ fossil commit

总结:Fossil 可以多次检出的特性使得版本编辑清晰明了,当当前工作空间的修改已全部提交,当前工作空间就可以随意 update 到任意分支的任意版本;当当前工作空间的工作尚未完成却急需修改其他任意分支的任意版本时,只需在其他目录检出就行。合并分支时需要关闭不需要的分支,具体方法是贴上 closed 底层标签。同样的,可以猜想出 trunk 其实也是个底层标签 (--raw sym-trunk)。当然啦,fossil commit –branch <分支名> –close 这样的操作也是可以的。

那么,如果用的是 Git,操作上有什么区别吗?

Git 没有多处检出的概念 (update: 现在 Git 可以使用 git worktree 做到类似的操作了) ,所以引入了 搁置 的概念——当急需切换时,先执行 搁置 操作,你的改动就会打包藏起来,然后就能变更当前工作空间的版本。完成后,再切换回先前工作的那个版本,把搁置的内容取出套用并丢弃搁置。

习惯上,Git 在分支操作前就创建新分支并切换,而不是提交时新建分支。

分岔派生 (fork)

Fossil 通常开启了自动同步,远程版本库的改动会及时同步。考虑如下场景:甲和乙两位开发员同时在同一个分支同一个版本上工作,甲先提交,乙后提交,那么当乙提交时就有可能产生分岔,Fossil 会提示乙:“would fork, “update” first, or use “–allow-fork”。这时有两个选择:分支合并,或者创建分岔。如果使用 fossil commit –allow-fork,分岔就产生了。

在 Fossil 中,你可以把任何一个分岔当作主分支用,进而发展出姊妹成品。但出于实际考虑,我们大多数只希望有一个 trunk 就够了。因此,你需要处理分岔情况。方法很简单:添加分支名为底层标签,然后删除原主干标签。

$ fossil tag add --raw branch <版本编号> <取的分支名>
$ fossil  tag cancel --raw sym-trunk <版本编号>

好,现在它是一个分支了。

关于分支的具体细节和注意事项可以看一下官方 wiki

Git 中,fork 其实并不是原生存在的,这只是一个 Github 使用的概念,你可以 fork 别人的项目到自己的仓库,然后改改改,改成自己想要的样子然后提交回去。

处理错误提交

某夜凌晨,你灌下两瓶东欧进口伏特加,写了不少糊话,还给提交了。这时怎么处理?

commit 并不像 update 那样可以 undo ,你要做的只是把它丢了不管——即把它移动到一个分支并关闭。

$ fossil tag add --raw branch <错误提交的版本的编号> Mistake
$ fossil tag add --raw closed <错误提交的版本的编号>
$ fossil tag cancel --raw sym-trunk <错误提交的版本的编号>
$ fossil update <恢复到的版本的编号>

可以看出来只是标签操作。

选择 Git 还是 Fossil

我们大致了解了 Fossil 的一些基本操作以及与 Git 的操作差异。那么,在实际使用时是应该选择 Git 还是 Fossil 呢?

回答这个问题首先要看你需管理的项目的开发方式。Fossil 是为支持 SQLite 开发而设计的,设计为几名互相了解的核心开发员进行大教堂式开发;Git 是为支持 Linux 内核开发而设计的,属于集市型开发。Fossil 很少使用私人分支,所有核心开发人员的编辑动向对所有人实时同步开放。D. Richard Hipp 信任并很少去推翻其他核心开发员的提交。而 Linus 本人肯定不希望世界上成千上万的程序员对内核干了什么都反馈给他,他只希望处理提交给他的更改。

此外, Git 拥有最庞大的生态社区,这意味着 Git 的历史遗留缺陷可以得到弥补。如果记不住那些繁杂命令可以使用设计精良的图形界面。

安全性方面,Git 目前受困于从 SHA-1 转向 SHA-256 ,而 Fossil 已经完成到 SHA-3 的迁移了。

所以我个人的选择是,放到 Github 上的用 Git ,自己的小项目之类用 Fossil

Fossil 的命令用起来很方便,Git 的命令我经常傻傻记不清,就用图形界面了 (捂脸逃

另外,如果想在命令行下体验到简洁舒适的 Git ,可以尝试使用 Gitless

初稿于 2020年3月11日

CC BY-NC-SA 4.0

知识共享许可协议