Skip to content

9. Staging area and index, part 2: staging and committing 暂存区和索引,第二部分:暂存和提交

OK. Let’s create commits.

好的。让我们来创建提交。

We have almost everything we need for that, except for three last things:

我们几乎具备了所有需要的条件,除了最后三个要点:

  1. We need commands to modify the index, so our commits aren’t just a copy of their parent. Those commands are add and rm.
  2. 我们需要命令来修改索引,以便我们的提交不仅仅是父提交的副本。这些命令是 addrm
  3. These commands need to write the modified index back, since we commit from the index.
  4. 这些命令需要将修改后的索引写回,因为我们是从索引中提交的。
  5. And obviously, we’ll need the commit function and its associated wyag commit command.
  6. 显然,我们还需要 commit 函数及其相关的 wyag commit 命令。

9.1. Writing the index 写入索引

We’ll start by writing the index. Roughly, we’re just serializing everything back to binary. This is a bit tedious, but the code should be straightforward. I’m leaving the gory details for the comments, but it’s really just index_read in reverse — refer to it if needed, and the GitIndexEntry class.

我们将首先写入索引。大致上,我们只是将所有内容序列化回二进制。这有点繁琐,但代码应该是直接明了的。我会将一些细节留给注释,但实际上这只是 index_read 的反向操作——如有需要,请参考它和 GitIndexEntry 类。

python
def index_write(repo, index):
    with open(repo_file(repo, "index"), "wb") as f:

        # HEADER
        # 头部

        # Write the magic bytes.
        # 写入魔术字节。
        f.write(b"DIRC")
        # Write the number of entries.
        # 写入版本号。
        f.write(index.version.to_bytes(4, "big"))
        # Write the number of entries.
        # 写入条目数量。
        f.write(len(index.entries).to_bytes(4, "big"))

        # ENTRIES
        # 条目

        idx = 0
        for e in index.entries:
            f.write(e.ctime[0].to_bytes(4, "big"))
            f.write(e.ctime[1].to_bytes(4, "big"))
            f.write(e.mtime[0].to_bytes(4, "big"))
            f.write(e.mtime[1].to_bytes(4, "big"))
            f.write(e.dev.to_bytes(4, "big"))
            f.write(e.ino.to_bytes(4, "big"))

            # Mode
            # 模式
            mode = (e.mode_type << 12) | e.mode_perms
            f.write(mode.to_bytes(4, "big"))

            f.write(e.uid.to_bytes(4, "big"))
            f.write(e.gid.to_bytes(4, "big"))

            f.write(e.fsize.to_bytes(4, "big"))
            # @FIXME Convert back to int.
            # @FIXME 转换回整数。
            f.write(int(e.sha, 16).to_bytes(20, "big"))

            flag_assume_valid = 0x1 << 15 if e.flag_assume_valid else 0

            name_bytes = e.name.encode("utf8")
            bytes_len = len(name_bytes)
            if bytes_len >= 0xFFF:
                name_length = 0xFFF
            else:
                name_length = bytes_len

            # We merge back three pieces of data (two flags and the
            # 我们将三个数据片段(两个标志和名称长度)合并到同两个字节中。
            f.write((flag_assume_valid | e.flag_stage | name_length).to_bytes(2, "big"))

            # Write back the name, and a final 0x00.
            # 写入名称和最后的 0x00。
            f.write(name_bytes)
            f.write((0).to_bytes(1, "big"))

            idx += 62 + len(name_bytes) + 1

            # Add padding if necessary.
            # 如有必要,添加填充。
            if idx % 8 != 0:
                pad = 8 - (idx % 8)
                f.write((0).to_bytes(pad, "big"))
                idx += pad

9.2. The rm command | rm 命令

The easiest change we can do to an index is to remove an entry from it, which mean that the next commit won’t include this file. This is what the git rm command does.

对索引进行的最简单修改是从中移除一个条目,这意味着下一个提交将不包括该文件。这就是 git rm 命令的作用。

危险

The easiest change we can do to an index is to remove an entry from it, which mean that the next commit won’t include this file. This is what the git rm command does.

git rm破坏性的wyag rm 也是如此。该命令不仅修改索引,还会从工作区中删除文件。与 git 不同,wyag rm 不关心它移除的文件是否已保存。请谨慎操作。

rm takes a single argument, a list of paths to remove:

rm 接受一个参数,即要移除的路径列表:

python
argsp = argsubparsers.add_parser("rm", help="Remove files from the working tree and the index.")
argsp.add_argument("path", nargs="+", help="Files to remove")

def cmd_rm(args):
  repo = repo_find()
  rm(repo, args.path)
python
argsp = argsubparsers.add_parser("rm", help="从工作树和索引中移除文件。")
argsp.add_argument("path", nargs="+", help="要移除的文件")

def cmd_rm(args):
  repo = repo_find()
  rm(repo, args.path)

The rm function is a bit long, but it’s very simple. It takes a repository and a list of paths, reads that repository index, and removes entries in the index that match this list. The optional arguments control whether the function should actually delete the files, and whether it should abort if some paths aren’t present on the index (both those arguments are for the use of add, they’re not exposed in the wyag rm command).

rm 函数稍微长一些,但它非常简单。它接受一个仓库和一个路径列表,读取该仓库的索引,并移除与该列表匹配的索引条目。可选参数控制函数是否实际删除文件,以及如果某些路径在索引中不存在,是否应中止操作(这两个参数用于 add,在 wyag rm 命令中不暴露)。

python
def rm(repo, paths, delete=True, skip_missing=False):
  # Find and read the index
  index = index_read(repo)

  worktree = repo.worktree + os.sep

  # Make paths absolute
  abspaths = list()
  for path in paths:
    abspath = os.path.abspath(path)
    if abspath.startswith(worktree):
      abspaths.append(abspath)
    else:
      raise Exception("Cannot remove paths outside of worktree: {}".format(paths))

  kept_entries = list()
  remove = list()

  for e in index.entries:
    full_path = os.path.join(repo.worktree, e.name)

    if full_path in abspaths:
      remove.append(full_path)
      abspaths.remove(full_path)
    else:
      kept_entries.append(e) # Preserve entry

  if len(abspaths) > 0 and not skip_missing:
    raise Exception("Cannot remove paths not in the index: {}".format(abspaths))

  if delete:
    for path in remove:
      os.unlink(path)

  index.entries = kept_entries
  index_write(repo, index)
python
def rm(repo, paths, delete=True, skip_missing=False):
  # 查找并读取索引
  index = index_read(repo)

  worktree = repo.worktree + os.sep

  # 将路径转换为绝对路径
  abspaths = list()
  for path in paths:
    abspath = os.path.abspath(path)
    if abspath.startswith(worktree):
      abspaths.append(abspath)
    else:
      raise Exception("无法移除工作树外的路径:{}".format(paths))

  kept_entries = list()
  remove = list()

  for e in index.entries:
    full_path = os.path.join(repo.worktree, e.name)

    if full_path in abspaths:
      remove.append(full_path)
      abspaths.remove(full_path)
    else:
      kept_entries.append(e) # 保留条目

  if len(abspaths) > 0 and not skip_missing:
    raise Exception("无法移除索引中不存在的路径:{}".format(abspaths))

  if delete:
    for path in remove:
      os.unlink(path)

  index.entries = kept_entries
  index_write(repo, index)

And we can now delete files with wyag rm.

现在我们可以使用 wyag rm 删除文件。

9.3. The add command | add 命令

Adding is just a bit more complex than removing, but nothing we don’t already know. Staging a file to a three-steps operation:

添加操作比移除操作稍微复杂一些,但没有什么是我们不熟悉的。将文件添加到暂存区是一个三步操作:

  1. We begin by removing the existing index entry, if there’s one, without removing the file itself (this is why the rm function we just wrote has those optional arguments).
  2. 首先,如果已有索引条目,则移除该条目,但不删除文件本身(这就是我们刚刚编写的 rm 函数包含可选参数的原因)。
  3. We then hash the file into a glob object,
  4. 然后对文件进行哈希处理,生成一个 blob 对象。
  5. create its entry,
  6. 创建该条目。
  7. And of course, finally write the modified index back.
  8. 最后,当然要将修改后的索引写回。

First, the interface. Nothing surprising, wyag add PATH ... where PATH is one or more file(s) to stage. The bridge is as boring as can be.

首先是接口。没有什么惊喜,wyag add PATH ...,其中 PATH 是一个或多个要暂存的文件。桥接函数非常简单。

python
argsp = argsubparsers.add_parser("add", help = "Add files contents to the index.")
argsp.add_argument("path", nargs="+", help="Files to add")

def cmd_add(args):
  repo = repo_find()
  add(repo, args.path)
python
argsp = argsubparsers.add_parser("add", help="将文件内容添加到索引。")
argsp.add_argument("path", nargs="+", help="要添加的文件")

def cmd_add(args):
  repo = repo_find()
  add(repo, args.path)

The main difference with rm is that add needs to create an index entry. This isn’t hard: we just stat() the file and copy the metadata in the index’s field (stat() returns those metadata the index stores: creation/modification time, and so on)

rm 的主要区别在于 add 需要创建一个索引条目。这并不难:我们只需对文件进行 stat() 操作,并将元数据复制到索引的字段中(stat() 返回索引存储的元数据:创建/修改时间等)。

python
def add(repo, paths, delete=True, skip_missing=False):

  # First remove all paths from the index, if they exist.
  # 首先从索引中移除所有路径(如果存在)。
  rm(repo, paths, delete=False, skip_missing=True)

  worktree = repo.worktree + os.sep

  # Convert the paths to pairs: (absolute, relative_to_worktree).
  # Also delete them from the index if they're present.
  # 将路径转换为对: (绝对路径,相对工作树路径)。
  # 如果它们在索引中,则也将其删除。
  clean_paths = list()
  for path in paths:
    abspath = os.path.abspath(path)
    if not (abspath.startswith(worktree) and os.path.isfile(abspath)):
      raise Exception("不是文件,或不在工作树内:{}".format(paths))
    relpath = os.path.relpath(abspath, repo.worktree)
    clean_paths.append((abspath, relpath))

    # Find and read the index.  It was modified by rm.  (This isn't
    # optimal, good enough for wyag!)
    #
    # @FIXME, though: we could just
    # move the index through commands instead of reading and writing
    # it over again.
    # 查找并读取索引。它已被 rm 修改。(这不是最优的,但对 wyag 足够了!)
    #
    # @FIXME: 我们本可以通过命令移动索引,而不是读取和重新写入它。
    index = index_read(repo)

    for (abspath, relpath) in clean_paths:
      with open(abspath, "rb") as fd:
        sha = object_hash(fd, b"blob", repo)

      stat = os.stat(abspath)

      ctime_s = int(stat.st_ctime)
      ctime_ns = stat.st_ctime_ns % 10**9
      mtime_s = int(stat.st_mtime)
      mtime_ns = stat.st_mtime_ns % 10**9

      entry = GitIndexEntry(ctime=(ctime_s, ctime_ns), mtime=(mtime_s, mtime_ns), dev=stat.st_dev, ino=stat.st_ino,
                            mode_type=0b1000, mode_perms=0o644, uid=stat.st_uid, gid=stat.st_gid,
                            fsize=stat.st_size, sha=sha, flag_assume_valid=False,
                            flag_stage=False, name=relpath)
      index.entries.append(entry)

    # Write the index back
    # 将索引写回
    index_write(repo, index)

9.4. The commit command | commit 命令

Now that we have modified the index, so actually staged changes, we only need to turn those changes into a commit. That’s what commit does.

现在我们已经修改了索引,也就是实际的 暂存更改,我们只需要将这些更改转换为一个提交。这就是 commit 的作用。

python
argsp = argsubparsers.add_parser("commit", help="Record changes to the repository.")

argsp.add_argument("-m",
                   metavar="message",
                   dest="message",
                   help="Message to associate with this commit.")
python
argsp = argsubparsers.add_parser("commit", help="记录对仓库的更改。")

argsp.add_argument("-m",
                   metavar="message",
                   dest="message",
                   help="与此提交关联的消息。")

To do so, we first need to convert the index into a tree object, generate and store the corresponding commit object, and update the HEAD branch to the new commit (remember: a branch is just a ref to a commit).

为此,我们首先需要将索引转换为树对象,生成并存储相应的提交对象,并将 HEAD 分支更新为新的提交(请记住:分支只是指向提交的引用)。

Before we get to the interesting details, we will need to read git’s config to get the name of the user, which we’ll use as the author and committer. We’ll use the same configparser library we’ve used to read repo’s config.

在进入有趣的细节之前,我们需要读取 Git 的配置,以获取用户的名字,作为作者和提交者。我们将使用之前用来读取仓库配置的 configparser 库。

python
def gitconfig_read():
    xdg_config_home = os.environ["XDG_CONFIG_HOME"] if "XDG_CONFIG_HOME" in os.environ else "~/.config"
    configfiles = [
        os.path.expanduser(os.path.join(xdg_config_home, "git/config")),
        os.path.expanduser("~/.gitconfig")
    ]

    config = configparser.ConfigParser()
    config.read(configfiles)
    return config

And just a simple function to grab, and format, the user identity:

接下来是一个简单的函数,用于获取并格式化用户身份:

python
def gitconfig_user_get(config):
    if "user" in config:
        if "name" in config["user"] and "email" in config["user"]:
            return "{} <{}>".format(config["user"]["name"], config["user"]["email"])
    return None

Now for the interesting part. We first need to build a tree from the index. This isn’t hard, but notice that while the index is flat (it stores full paths for the whole worktree), a tree is a recursive structure: it lists files, or other trees. To “unflatten” the index into a tree, we’re going to:

现在进入有趣的部分。我们首先需要从索引构建一棵树。这并不困难,但请注意,虽然索引是平面的(它为整个工作树存储完整路径),而树是一个递归结构:它列出文件或其他树。为了将索引“反扁平化”为一棵树,我们将:

  1. Build a dictionary (hashmap) of directories. Keys are full paths from worktree root (like assets/sprites/monsters/), values are list of GitIndexEntry — files in the directory. At this point, our
  2. 建立一个目录的字典(哈希映射)。键是来自工作树根的完整路径(如 assets/sprites/monsters/),值是 GitIndexEntry 的列表——该目录中的文件。此时,我们的字典仅包含 文件:目录仅作为其键。
  3. Traverse this list, going bottom-up, that is, from the deepest directories up to root (depth doesn’t really matter: we just want to see each directory before its parent. To do that, we just sort them by full path length, from longest to shortest — parents are obviously always shorter). As an example, imagine we start at assets/sprites/monsters/
  4. 遍历此列表,从最深的目录向上到根(深度实际上并不重要:我们只希望在看到每个目录的 父目录 之前看到它。为此,我们只需按 完整 路径长度从长到短对它们进行排序——父目录显然总是较短的)。例如,想象我们从 assets/sprites/monsters/ 开始。
  5. At each directory, we build a tree with its contents, say cacodemon.png, imp.png and baron-of-hell.png.
  6. 在每个目录下,我们使用其内容构建一棵树,比如 cacodemon.pngimp.pngbaron-of-hell.png
  7. We write the new tree to the repository.
  8. 将新树写入仓库。
  9. We then add this tree to this directory’s parent. Meaning that at this point, assets/sprites/ now contains our new tree object’s SHA-1 id under the name monsters.
  10. 然后将此树添加到该目录的父目录中。这意味着此时,assets/sprites/ 现在包含我们新树对象的 SHA-1 ID,名称为 monsters
  11. And we iterate over the next directory, let’s say assets/sprites/keys where we find red.png, blue.png and yellow.png, create a tree, store the tree, add the tree’s SHA-1 under the name keys under assets/sprites/, and so on.
  12. 接着我们迭代下一个目录,比如 assets/sprites/keys,在这里我们发现 red.pngblue.pngyellow.png,创建一棵树,存储该树,并在 assets/sprites/ 下以名称 keys 添加该树的 SHA-1,依此类推。

And since trees are recursive? So the last tree we’ll build, which is necessarily the one for root (since its key’s length is 0), will ultimately refer to all others, and thus will be only one we’ll need. We’ll simply return its SHA-1, and be done.

由于树是递归的?因此我们构建的最后一棵树必然是根树(因为它的键长度为 0),最终将引用所有其他树,因此它将是我们唯一需要的树。我们只需返回其 SHA-1,就完成了。

Since this may seem a bit complex, let’s work this example in full details — feel free to skip. At the beginning, the dictionary we built from the index looks like this:

由于这可能看起来有些复杂,让我们详细演示这个例子——随意跳过。在开始时,我们从索引构建的字典如下所示:

bash
contents["assets/sprites/monsters"] =
  [ cacodemon.png : GitIndexEntry
  , imp.png : GitIndexEntry
  , baron-of-hell.png : GitIndexEntry ]
contents["assets/sprites/keys"] =
  [ red.png : GitIndexEntry
  , blue.png : GitIndexEntry
  , yellow.png : GitIndexEntry ]
contents["assets/sprites/"] =
  [ hero.png : GitIndexEntry ]
contents["assets/"] = [] # No files in here # 这里没有文件
contents[""] = # Root! # 根!
  [ README: GitIndexEntry ]

We iterate over it, by order of descending key length. The first key we meet is the longest, so assets/sprites/monsters. We build a new tree object from its contents, which associates the three file names (cacodemon.png, imp.png, baron-of-hell.png) with their corresponding blobs (A tree leaf stores less data than the index — just path, mode and blob. So converting entries that way is easy)

我们按键长度从长到短的顺序进行迭代。我们遇到的第一个键是最长的,即 assets/sprites/monsters。我们根据其内容构建一个新的树对象,将三个文件名(cacodemon.pngimp.pngbaron-of-hell.png)与它们对应的 blob 关联起来(树的叶子存储的数据 索引少——仅存储路径、模式和 blob。因此,以这种方式转换条目是容易的)。

Notice we don’t need to concern ourselves with storing the contents of those files: wyag add did create the corresponding blobs as needed. We need to store the trees we create to the object store, but we can assume the blobs are there already.

注意,我们不需要关心存储这些文件的 内容wyag add 确实根据需要创建了相应的 blob。我们需要将我们创建的 存储到对象库中,但我们可以假设 blob 已经在那里。

Let’s say that our new tree hashes, made from the index entries that lived directly in assets/sprites/monsters, hashes down to 426f894781bc3c38f1d26f8fd2c7f38ab8d21763. We modify our dictionary to add that new tree object to the directory’s parent, like this, so what remains to traverse now looks like this:

假设我们新生成的树哈希值,由直接来自 assets/sprites/monsters 的索引条目生成,哈希值为 426f894781bc3c38f1d26f8fd2c7f38ab8d21763。我们 修改我们的字典,将这个新的树对象添加到目录的父级,像这样,所以现在剩下的遍历内容看起来是这样的:

txt
contents["assets/sprites/keys"] = # <- unmodified. # <- 未修改。
  [ red.png : GitIndexEntry
  , blue.png : GitIndexEntry
  , yellow.png : GitIndexEntry ]
contents["assets/sprites/"] =
  [ hero.png : GitIndexEntry
  , monsters : Tree 426f894781bc3c38f1d26f8fd2c7f38ab8d21763 ] <- 看这里
contents["assets/"] = [] # 空
contents[""] = # 根!
  [ README: GitIndexEntry ]

We do the same for the next longest key, assets/sprites/keys, producing a tree of hash b42788e087b1e94a0e69dcb7a4a243eaab802bb2, so:

我们对下一个最长的键 assets/sprites/keys 做同样的操作,生成一个哈希为 b42788e087b1e94a0e69dcb7a4a243eaab802bb2 的树,因此:

txt
contents["assets/sprites/"] =
  [ hero.png : GitIndexEntry
  , monsters : Tree 426f894781bc3c38f1d26f8fd2c7f38ab8d21763
  , keys : Tree b42788e087b1e94a0e69dcb7a4a243eaab802bb2 ]
contents["assets/"] = [] # empty # 空
contents[""] = #Root! # 根!
  [ README: GitIndexEntry ]

We then generate tree 6364113557ed681d775ccbd3c90895ed276956a2 from assets/sprites, which now contains our two trees and hero.png.

接着,我们从 assets/sprites 生成哈希为 6364113557ed681d775ccbd3c90895ed276956a2 的树,它现在包含我们的两个树和 hero.png

txt
contents["assets/"] = [
  sprites: Tree 6364113557ed681d775ccbd3c90895ed276956a2 ]
contents[""] = # Root! # 根!
  [ README: GitIndexEntry ]

Assets in turn becomes tree 4d35513cb6d2a816bc00505be926624440ebbddd, so:

assets 反过来变成哈希为 4d35513cb6d2a816bc00505be926624440ebbddd 的树,因此:

txt
contents[""] = # Root! # 根!
  [ README: GitIndexEntry,
    assets: 4d35513cb6d2a816bc00505be926624440ebbddd]

We make a tree from that last key (with the README blob and the assets subtree), it hashes to 9352e52ff58fa9bf5a750f090af64c09fa6a3d93. That’s our return value: the tree whose contents are the same as the index’s.

我们从最后一个键(带有 README blob 和 assets 子树)生成一棵树,它的哈希值为 9352e52ff58fa9bf5a750f090af64c09fa6a3d93。这就是我们的返回值:这棵树的内容与索引的内容相同。

Here’s the actual function:

这里是实际的函数:

python
def tree_from_index(repo, index):
    contents = dict()
    contents[""] = list()

    # Enumerate entries, and turn them into a dictionary where keys
    # are directories, and values are lists of directory contents.
    # 枚举条目,并将它们转换为一个字典,其中键是目录,值是目录内容的列表。
    for entry in index.entries:
        dirname = os.path.dirname(entry.name)

        # We create all dictonary entries up to root ("").  We need
        # them *all*, because even if a directory holds no files it
        # will contain at least a tree.
        # 我们创建所有到根目录 ("") 的字典条目。我们需要它们 *全部*,因为即使一个目录没有文件,它至少会包含一个树。
        key = dirname
        while key != "":
            if key not in contents:
                contents[key] = list()
            key = os.path.dirname(key)

        # For now, simply store the entry in the list.
        # 暂时将条目存储在列表中。
        contents[dirname].append(entry)

    # Get keys (= directories) and sort them by length, descending.
    # This means that we'll always encounter a given path before its
    # parent, which is all we need, since for each directory D we'll
    # need to modify its parent P to add D's tree.
    # 获取键(即目录)并按长度降序排序。
    # 这意味着我们总是会在其父目录之前遇到给定路径,这正是我们需要的,因为对于每个目录 D,我们需要修改其父目录 P 以添加 D 的树。
    sorted_paths = sorted(contents.keys(), key=len, reverse=True)

    # This variable will store the current tree's SHA-1.  After we're
    # done iterating over our dict, it will contain the hash for the
    # root tree.
    # 这个变量将存储当前树的 SHA-1。完成遍历后,它将包含根树的哈希。
    sha = None

    # We ge through the sorted list of paths (dict keys)
    # 我们遍历排序后的路径列表(字典键)
    for path in sorted_paths:
        # Prepare a new, empty tree object
        # 准备一个新的空树对象
        tree = GitTree()

        # Add each entry to our new tree, in turn
        # 将每个条目依次添加到我们的新树中
        for entry in contents[path]:
            # An entry can be a normal GitIndexEntry read from the
            # index, or a tree we've created.
            # 条目可以是从索引读取的普通 GitIndexEntry,或者是我们创建的树。
            if isinstance(entry, GitIndexEntry):  # 普通条目(一个文件)

                # We transcode the mode: the entry stores it as integers,
                # we need an octal ASCII representation for the tree.
                # 我们转换模式:条目将其存储为整数,我们需要树的八进制 ASCII 表示。
                leaf_mode = "{:02o}{:04o}".format(entry.mode_type, entry.mode_perms).encode("ascii")
                leaf = GitTreeLeaf(mode=leaf_mode, path=os.path.basename(entry.name), sha=entry.sha)
            else: # Tree.  We've stored it as a pair: (basename, SHA) # 树。我们将其存储为一对: (basename, SHA)
                leaf = GitTreeLeaf(mode=b"040000", path=entry[0], sha=entry[1])

            tree.items.append(leaf)

        # Write the new tree object to the store.
        # 将新的树对象写入存储。
        sha = object_write(tree, repo)

        # Add the new tree hash to the current dictionary's parent, as
        # a pair (basename, SHA)
        # 将新的树哈希添加到当前字典的父目录,作为一对 (basename, SHA)
        parent = os.path.dirname(path)
        base = os.path.basename(path) # The name without the path, eg main.go for src/main.go # 不带路径的名称,例如 src/main.go 的 main.go
        contents[parent].append((base, sha))

    return sha

This was the hard part; I hope it’s clear enough. From this, creating the commit object and updating HEAD will be way easier. Just remember that what this function does is built and store as many tree objects as needed to represent the index, and return the root tree’s SHA-1.

这部分比较复杂;我希望它足够清晰。从这里开始,创建提交对象和更新 HEAD 将会简单得多。只需记住,这个函数 的事情是构建和存储尽可能多的树对象,以表示索引,并返回根树的 SHA-1。

The function to create a commit object is simple enough, it just takes some arguments: the hash of the tree, the hash of the parent commit, the author’s identity (a string), the timestamp and timezone delta, and the message:

创建提交对象的函数足够简单,它只接受一些参数:树的哈希、父提交的哈希、作者的身份(一个字符串)、时间戳和时区差值,以及消息:

python
def commit_create(repo, tree, parent, author, timestamp, message):
    commit = GitCommit() # Create the new commit object. # 创建新的提交对象
    commit.kvlm[b"tree"] = tree.encode("ascii")
    if parent:
        commit.kvlm[b"parent"] = parent.encode("ascii")

    # Format timezone
    # 格式化时区
    offset = int(timestamp.astimezone().utcoffset().total_seconds())
    hours = offset // 3600
    minutes = (offset % 3600) // 60
    tz = "{}{:02}{:02}".format("+" if offset > 0 else "-", hours, minutes)

    author = author + timestamp.strftime(" %s ") + tz

    commit.kvlm[b"author"] = author.encode("utf8")
    commit.kvlm[b"committer"] = author.encode("utf8")
    commit.kvlm[None] = message.encode("utf8")

    return object_write(commit, repo)

All what remains to write is cmd_commit, the bridge function to the wyag commit command:

剩下的就是 cmd_commit,它是 wyag commit 命令的桥接函数:

python
def cmd_commit(args):
    repo = repo_find()
    index = index_read(repo)
    # Create trees, grab back SHA for the root tree.
    # 创建树,获取根树的 SHA
    tree = tree_from_index(repo, index)

    # Create the commit object itself
    # 创建提交对象
    commit = commit_create(repo,
                           tree,
                           object_find(repo, "HEAD"),
                           gitconfig_user_get(gitconfig_read()),
                           datetime.now(),
                           args.message)

    # Update HEAD so our commit is now the tip of the active branch.
    # 更新 HEAD,使我们的提交成为当前分支的顶端
    active_branch = branch_get_active(repo)
    if active_branch: # If we're on a branch, we update refs/heads/BRANCH # 如果我们在一个分支上,更新 refs/heads/BRANCH
        with open(repo_file(repo, os.path.join("refs/heads", active_branch)), "w") as fd:
            fd.write(commit + "\n")
    else: # Otherwise, we update HEAD itself. # 否则,更新 HEAD 本身
        with open(repo_file(repo, "HEAD"), "w") as fd:
            fd.write("\n")

And we’re done!

我们完成了!