git 是当今世界上最流行的版本控制系统。本实验要求你通过提交 git repo 的方式来提交一个迷你版的命令行工具 gitm(inus)。

注意:本实验将只能在 Linux 操作系统中完成,因为你不得不使用系统调用,而 OJ 是 Linux 的。提交 Windows 上可以编译运行的代码,在评测机上注定不可兼容

首先,你需要学习 git ,否则你将完全不明白 gitm 的功能,并且也无法用 git 来管理本次作业的代码。

是的!我们将会发布一个由 git 管理的框架代码,并且要求你一直使用 git 来管理,最终提交一个 git repo。

获取框架代码:

1
git clone https://git.nju.edu.cn/Tilnel/gitm.git

提交要求

所有代码,包括 .c 和 .h 文件需要放在 git repo 的根目录下。对自己使用的头文件的引用请以双引号的形式,以便编译脚本能够正常工作。

例如:

1
2
3
4
5
6
7
8
9
10
11
gitm
├── gitm.c
├── gitm.h
├── whateveryouwant.c
├── whateveryouwant.h
├── ...
└── Makefile

/* gitm.c */
#include "gitm.h"
...

你可以修改 Makefile,但请不要删除其中的 git 目标依赖。我们在 Makefile 中确保了你的每一次编译运行都能够自动进行 git commit。这些自动的 commit 可以帮助你回滚到自己想要的任意版本,并且在未来查重工作中产生疑问时,良好的 commit 记录将成为重要的证明。

在你的 git repo 里请包含所有编译所需的源文件,但不要出现编译不需要的多余的源文件。

尝试编译:

1
make

你将看到根目录下产生了一个名为 gitm 的可执行文件。

1
./gitm version

你将看到一个小彩蛋(你之后可以自由地删掉它或修改掉,不影响成绩)。

注意:本次实验你编写的是一个 “命令行工具”。也就是说,我们将以和使用 git 相同的方式来使用它:在命令行里输入命令和参数。这意味着,这次你需要真正 “解析参数” (被 parse.c 支配的恐惧)。

而且,这次我们将会在运行中多次调用你的程序。也就是说,你的程序并不是在一直运行着,每一次调用都会做不同的事。你存储在内存里的数据都将随着功能完成,进程结束而消失。所以,关于 gitm repository 的有用的信息,你需要将它们持久化到磁盘上,以便进行后续的操作。因此学习 C 语言的文件操作是必不可少的。

为了实现一个 git,首先你要了解 git 的功能

需要实现的功能

假设我们当前在一个文件夹 dir 下

1
gitm init

初始化当前的 dir 为一个 gitm repository。如果当前 dir 已经是一个 gitm repo,则不做任何操作。

此时的 gitm 中应当不存在任何 commit,gitm 的仓库中应不存在任何文件。

具体来说,你可以在当前目录下创建一个 .gitm 目录,用于存放一些记录仓库状态的文件。

对,就像 git 那样!

1
gitm commit

将当前仓库中文件改动后状态作为一个提交,并记录下来。然后不重复地给出一个长度为 8 的小写十六进制数(例如 3bdc8902),用于唯一地指示这一次 commit。

git 中的提交是一个树形的结构。我们希望你在 gitm 中,同样实现这样的树形结构。

img

gitm 中不要求实现对分支的命名

1
gitm checkout commit

checkout 用于将当前目录的状态切换到 commit 所指示的提交上。

若当前目录的状态较 gitm 当前所处的 commit 有改动,则拒绝本次 checkout,并且你的 main() 函数以返回值 1 退出

checkout 正常完成后,你目录中文件的状态(除了 .gitm 目录以外)必须与指定的 commit 相同。

1
gitm checkout .

特殊地,这一条命令用于将目录文件恢复到当前所处的 commit 时的状态。也就是说,放弃此时对文件的所有改动。

1
gitm merge commit

找到当前所处 commit 与命令指定的 commit 的公共祖先,并将两个 commit 合并起来。

具体来说,是将命令指定的 commit 相对于公共祖先的修改,应用于当前所处的 commit。

如果合并的两个 commit 相对于公共祖先,均对同一个文件产生了修改(创建、删除、编辑),那么命令直接拒绝执行,输出 “conflict\n” 并使 main 函数返回 1

在其他情况下,你需要合并,并产生一个新的 commit。逻辑上,这个 commit 将成为被合并的两个 commit 的共同后继。

我们如何检测这一点?

假设有 commit a-g,b, c 由 a 分支而来,d 由 b, c 合并而来,e 是 b 的后继,f 是 c 的后继,g 是 d 的后继。

你的程序应当有能力找到 e, g 的公共祖先是 b,f, g 公共祖先是 c,在此基础上合并是无冲突的。如果你只能找到 a,则合并有可能产生冲突,因为 e 相对 a 改变了 a.c,而 g 相对 a 也改变了 a.c。

image-20230103113625830

测试脚本

我们会将你的 repo 里所有的 C 源文件和头文件收集起来进行编译,并生成一个名为 gitm 的可执行文件。然后原地创建一个文件夹,作为你的 gitm 需要管理的 repository。例如(其中 > 开头的行表示命令行输出):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mkdir dir
cd dir
../gitm init
../gitm commit
> 3bce5ff0 # 空 commit,我们的 OJ 一定会创建一个空 commit 作为第一个
echo "hello world" > hello.txt # 创建文件并写入
../gitm commit
> b926d817
echo "this is my git" > readme.txt
git checkout 3bce5ff0
> You've made change. Please commit or garbage your change.
echo $? # 给出上一条命令的返回值。正常退出的程序应当为 0
> 1
../gitm commit
> ef938aa6
ls -a
> hello.txt readme.txt .gitm
../gitm checkout b926d817
ls -a
> hello.txt .gitm

随着时间的流逝,我将会发布进一步的实验指南。

实现要求

  • 你创建的所有文件都要放到运行目录的 .gitm 目录下。
    • 注意目录不要膨胀得过大。把每一个 commit 都完整地保存下来是一个方法,但 OJ 会给你扣分的
  • commit 数量不会超过 10000 个。
  • 你的 gitm 只需要管理文本类型的文件。其他类型的文件不会出现。