Skip to content

3. Creating repositories: init 创建仓库:init

Obviously, the first Git command in chronological and logical order is git init, so we’ll begin by creating wyag init. To achieve this, we’re going to first need some very basic repository abstraction.

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

3.1. The Repository object 仓库对象

Obviously, the first Git command in chronological and logical order is git init, so we’ll begin by creating wyag init. To achieve this, we’re going to first need some very basic repository abstraction.

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

A git repository is made of two things: a “work tree”, where the files meant to be in version control live, and a “git directory”, where Git stores its own data. In most cases, the worktree is a regular directory and the git directory is a child directory of the worktree, called .git.

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

Git supports much more cases (bare repo, separated gitdir, etc) but we won’t need them: we’ll stick to the basic approach of worktree/.git. Our repository object will then just hold two paths: the worktree and the gitdir.

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

To create a new Repository object, we only need to make a few checks:

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

  • We must verify that the directory exists, and contains a subdirectory called .git.
  • 我们必须验证该目录是否存在,并且包含一个名为 .git 的子目录。
  • We read its configuration in .git/config (it’s just an INI file) and control that core.repositoryformatversion is 0. More on that field in a moment.
  • 我们读取 .git/config 中的配置(这只是一个 INI 文件),并确保 core.repositoryformatversion 为 0。稍后我们会详细讨论这个字段。

The constructor takes an optional force which disables all checks. That’s because the repo_create() function which we’ll create later uses a Repository object to create the repo. So we need a way to create repository even from (still) invalid filesystem locations.

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

python
class GitRepository (object):
    """A git repository"""
    """一个 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)

We’re going to be manipulating lots of paths in repositories. We may as well create a few utility functions to compute those paths and create missing directory structures if needed. First, just a general path building function:

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

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

(A note on Python syntax: the star on the *path makes the function variadic, so it can be called with multiple path components as separate arguments. For example, repo_path(repo, "objects", "df", "4ec9fc2ad990cb9da906a95a6eda6627d7b7b0") is a valid call. The function receives path as a list)

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

The next two functions, repo_file() and repo_dir(), return and optionally create a path to a file or a directory, respectively. The difference between them is that the file version only creates directories up to the last component.

接下来的两个函数,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

(Second and last note on syntax: because the star in *path makes the functions variadic, the mkdir argument must be passed explicitly by name. For example, repo_file(repo, "objects", mkdir=True).)

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

To create a new repository, we start with a directory (which we create if doesn’t already exist) and create the git directory inside (which must not exist already, or be empty). That directory is called .git (the leading period makes it “hidden” on Unix systems), and contains:

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

  • .git/objects/ : the object store, which we’ll introduce in the next section.
  • .git/objects/ : 对象存储,我们将在 下一节 中介绍。
  • .git/refs/ the reference store, which we’ll discuss a bit later. It contains two subdirectories, heads and tags.
  • .git/refs/ : 引用存储,我们稍后会讨论 更多内容。它包含两个子目录,headstags
  • .git/HEAD, a reference to the current HEAD (more on that later!)
  • .git/HEAD : 当前 HEAD 的引用(稍后会详细介绍!)
  • .git/config, the repository’s configuration file.
  • .git/config : 仓库的配置文件。
  • .git/description, holds a free-form description of this repository’s contents, for humans, and is rarely used.
  • .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

The configuration file is very simple, it’s a INI-like file with a single section ([core]) and three fields:

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

  • repositoryformatversion = 0: the version of the gitdir format. 0 means the initial format, 1 the same with extensions. If > 1, git will panic; wyag will only accept 0.
  • repositoryformatversion = 0:gitdir 格式的版本。0 表示初始格式,1 表示相同格式但带有扩展。如果大于 1,git 将会崩溃;wyag 只接受 0。
  • filemode = false: disable tracking of file modes (permissions) changes in the work tree.
  • filemode = false:禁用对工作区中文件模式(权限)更改的跟踪。
  • bare = false: indicates that this repository has a worktree. Git supports an optional worktree key which indicates the location of the worktree, if not ..; wyag doesn’t.
  • bare = false:表示该仓库有一个工作区。Git 支持一个可选的 worktree 键,用于指示工作区的位置,如果不是 ..;而 wyag 不支持这个。

We create this file using Python’s configparser lib:

我们使用 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. The init command | init 命令

Now that we have code to read and create repositories, let’s make this code usable from the command line by creating the wyag init command. wyag init behaves just like git init — with much less customizability, of course. The syntax of wyag init is going to be:

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

txt
wyag init [path]

We already have the complete repository creation logic. To create the command, we’re only going to need two more things:

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

  1. We need to create an argparse subparser to handle our command’s argument.

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

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

    In the case of init, there’s a single, optional, positional argument: the path where to init the repo. It defaults to ., the current directory:

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

    python
    argsp.add_argument("path",
                       metavar="directory",
                       nargs="?",
                       default=".",
                       help="仓库创建的路径。")
  3. We also need a “bridge” function that will read argument values from the object returned by argparse and call the actual function with correct values.

  4. 我们还需要一个“桥接”函数,该函数将从 argparse 返回的对象中读取参数值,并使用正确的值调用实际函数。

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

And we’re done! If you’ve followed these steps, you should now be able to wyag init a git repository anywhere:

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

txt
$ wyag init test

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

3.3. The repo_find() function | repo_find() 函数

While we’re implementing repositories, we’re going to need a function to find the root of the current repository. We’ll use it a lot, since almost all Git functions work on an existing repository (except init, of course!). Sometimes that root is the current directory, but it may also be a parent: your repository’s root may be in ~/Documents/MyProject, but you may currently be working in ~/Documents/MyProject/src/tui/frames/mainview/. The repo_find() function we’ll now create will look for that root, starting at the current directory and recursing back to /. To identify a path as a repository, it will check for the presence of a .git directory.

在我们实现仓库的过程中,我们需要一个函数来找到当前仓库的根目录。我们会频繁使用这个函数,因为几乎所有的 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)

    # If we haven't returned, recurse in parent, if w
    parent = os.path.realpath(os.path.join(path, ".."))

    if parent == path:
        # Bottom case
        # os.path.join("/", "..") == "/":
        # If parent==path, then path is root.
        if required:
            raise Exception("No git directory.")
        else:
            return None

    # Recursive case
    return repo_find(parent, required)
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)

And we’re done with repositories!

仓库的部分就完成了!