Skip to content

3. 创建仓库:init

显然,按照时间顺序和逻辑顺序,第一个 Git 命令是 git init,所以我们将首先创建 wyag init。为此,我们需要一些非常基础的仓库抽象。

3.1. 仓库对象

显然,我们需要对仓库的抽象:几乎每次运行 Git 命令时,我们都是在尝试对某个仓库进行操作,创建、读取或修改。

Git 仓库由两部分组成:一个是“工作区(work tree)”,其中存放要进行版本控制的文件,另一个是“Git 目录(git directory)”,Git 在这里存储自己的数据。在大多数情况下,工作区是一个常规目录,而 Git 目录是工作区的一个子目录,名为 .git

Git 支持更多的情况(裸仓库、分离的 Git 目录 等),但我们不需要这些:我们将坚持使用基本的 worktree/.git 方法。我们的仓库对象将仅包含两个路径:工作区和 Git 目录。

要创建一个新的 Repository 对象,我们只需进行一些检查:

  • 我们必须验证该目录是否存在,并且包含一个名为 .git 的子目录。
  • 我们读取 .git/config 中的配置(这只是一个 INI 文件),并确保 core.repositoryformatversion 为 0。稍后我们会详细讨论这个字段。

构造函数接受一个可选的 force 参数,用于禁用所有检查。这是因为稍后我们将创建的 repo_create() 函数使用 Repository 对象来创建仓库。因此,我们需要一种方法,即使在(仍然)无效的文件系统位置,也能创建仓库。

python
class GitRepository (object):
    """一个 Git 仓库"""

    worktree = None
    gitdir = None
    conf = None

    def __init__(self, path, force=False):
        self.worktree = path
        self.gitdir = os.path.join(path, ".git")

        if not (force or os.path.isdir(self.gitdir)):
            raise Exception("Not a Git repository %s" % path)

        # 读取 .git/config 中的配置文件
        self.conf = configparser.ConfigParser()
        cf = repo_file(self, "config")

        if cf and os.path.exists(cf):
            self.conf.read([cf])
        elif not force:
            raise Exception("Configuration file missing")

        if not force:
            vers = int(self.conf.get("core", "repositoryformatversion"))
            if vers != 0:
                raise Exception("Unsupported repositoryformatversion %s" % vers)

我们将会在仓库中处理大量的路径。不妨创建一些工具函数来计算这些路径,并在需要时创建缺失的目录结构。首先,我们先写一个通用的路径构建函数:

python
def repo_path(repo, *path):
    """Compute path under repo's gitdir."""
    return os.path.join(repo.gitdir, *path)

(关于 Python 语法的一点说明:*path 前的星号使得函数具有可变参数特性,因此可以将多个路径组件作为单独的参数调用。例如,repo_path(repo, "objects", "df", "4ec9fc2ad990cb9da906a95a6eda6627d7b7b0") 是一个有效的调用。函数接收到的 path 是一个列表。)

接下来的两个函数,repo_file()repo_dir(),分别返回并可选地创建指向文件或目录的路径。它们之间的区别在于,文件版本只会创建到最后一个组件的目录。

python
def repo_file(repo, *path, mkdir=False):
    """Same as repo_path, but create dirname(*path) if absent.  For
example, repo_file(r, \"refs\", \"remotes\", \"origin\", \"HEAD\") will create
.git/refs/remotes/origin."""

    if repo_dir(repo, *path[:-1], mkdir=mkdir):
        return repo_path(repo, *path)

def repo_dir(repo, *path, mkdir=False):
    """Same as repo_path, but mkdir *path if absent if mkdir."""

    path = repo_path(repo, *path)

    if os.path.exists(path):
        if (os.path.isdir(path)):
            return path
        else:
            raise Exception("Not a directory %s" % path)

    if mkdir:
        os.makedirs(path)
        return path
    else:
        return None

(关于语法的第二个也是最后一个说明:由于 *path 中的星号使得函数具有可变参数特性,因此 mkdir 参数必须通过名称显式传递。例如,repo_file(repo, "objects", mkdir=True)。)

创建 一个新的仓库,我们从一个目录开始(如果该目录尚不存在则创建),然后在其中创建 git 目录(该目录必须尚不存在,或者为空)。这个目录名为 .git(前面的点使其在 Unix 系统上被视为“隐藏”),并包含:

  • .git/objects/ : 对象存储,我们将在 下一节 中介绍。
  • .git/refs/ : 引用存储,我们稍后会讨论 更多内容。它包含两个子目录,headstags
  • .git/HEAD : 当前 HEAD 的引用(稍后会详细介绍!)
  • .git/config : 仓库的配置文件。
  • .git/description : 包含该仓库内容的自由格式描述,供人类阅读,且很少使用。
python
def repo_create(path):
    """Create a new repository at path."""

    repo = GitRepository(path, True)

    # First, we make sure the path either doesn't exist or is an
    # empty dir.

    if os.path.exists(repo.worktree):
        if not os.path.isdir(repo.worktree):
            raise Exception ("%s is not a directory!" % path)
        if os.path.exists(repo.gitdir) and os.listdir(repo.gitdir):
            raise Exception("%s is not empty!" % path)
    else:
        os.makedirs(repo.worktree)

    assert repo_dir(repo, "branches", mkdir=True)
    assert repo_dir(repo, "objects", mkdir=True)
    assert repo_dir(repo, "refs", "tags", mkdir=True)
    assert repo_dir(repo, "refs", "heads", mkdir=True)

    # .git/description
    with open(repo_file(repo, "description"), "w") as f:
        f.write("Unnamed repository; edit this file 'description' to name the repository.\n")

    # .git/HEAD
    with open(repo_file(repo, "HEAD"), "w") as f:
        f.write("ref: refs/heads/master\n")

    with open(repo_file(repo, "config"), "w") as f:
        config = repo_default_config()
        config.write(f)

    return repo

配置文件非常简单,它是一个类似于 INI 的文件,包含一个部分([core])和三个字段:

  • repositoryformatversion = 0:gitdir 格式的版本。0 表示初始格式,1 表示相同格式但带有扩展。如果大于 1,git 将会崩溃;wyag 只接受 0。
  • filemode = false:禁用对工作区中文件模式(权限)更改的跟踪。
  • bare = false:表示该仓库有一个工作区。Git 支持一个可选的 worktree 键,用于指示工作区的位置,如果不是 ..;而 wyag 不支持这个。

我们使用 Python 的 configparser 库来创建这个文件:

python
def repo_default_config():
    ret = configparser.ConfigParser()

    ret.add_section("core")
    ret.set("core", "repositoryformatversion", "0")
    ret.set("core", "filemode", "false")
    ret.set("core", "bare", "false")

    return ret

3.2. init 命令

现在我们有了读取和创建仓库的代码,让我们通过创建 wyag init 命令来使这些代码可以从命令行使用。wyag init 的行为与 git init 一样——当然,定制化程度要低得多。wyag init 的语法如下:

txt
wyag init [path]

我们已经有了完整的仓库创建逻辑。要创建这个命令,我们只需要再添加两件事:

  1. 我们需要创建一个 argparse 子解析器来处理我们命令的参数。

    python
    argsp = argsubparsers.add_parser("init", help="初始化一个新的空仓库。")

    init 的情况下,有一个单独的可选位置参数:初始化仓库的路径。默认值为当前目录 .

    python
    argsp.add_argument("path",
                       metavar="directory",
                       nargs="?",
                       default=".",
                       help="仓库创建的路径。")
  2. 我们还需要一个“桥接”函数,该函数将从 argparse 返回的对象中读取参数值,并使用正确的值调用实际函数。

    python
    def cmd_init(args):
        repo_create(args.path)

就这样完成了!如果你按照这些步骤操作,现在应该能够在任何地方执行 wyag init 来创建一个 Git 仓库:

txt
$ wyag init test

wyag 可执行文件通常不在你的 $PATH 中:你需要使用完整名称调用它,例如 ~/projects/wyag/wyag init .

3.3. repo_find() 函数

在我们实现仓库的过程中,我们需要一个函数来找到当前仓库的根目录。我们会频繁使用这个函数,因为几乎所有的 Git 功能都在现有的仓库上工作(当然,init 除外!)。有时这个根目录是当前目录,但也可能是父目录:你的仓库根目录可能在 ~/Documents/MyProject,而你当前可能在 ~/Documents/MyProject/src/tui/frames/mainview/ 工作。我们现在要创建的 repo_find() 函数将从当前目录开始查找根目录,并递归向上直到 /。为了识别一个路径是否为仓库,它将检查 .git 目录是否存在。

python
def repo_find(path=".", required=True):
    path = os.path.realpath(path)

    if os.path.isdir(os.path.join(path, ".git")):
        return GitRepository(path)

    # 如果没有返回,递归查找父目录
    parent = os.path.realpath(os.path.join(path, ".."))

    if parent == path:
        # 底部情况
        # os.path.join("/", "..") == "/":
        # 如果 parent==path,那么 path 就是根目录。
        if required:
            raise Exception("没有 git 目录。")
        else:
            return None

    # 递归情况
    return repo_find(parent, required)

仓库的部分就完成了!