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 只需要管理文本类型的文件。其他类型的文件不会出现。