「Git内核」实现HEAD

「这是我参与11月更文挑战的第 12 天,活动详情查看:2021最后一次更文挑战


实现 HEAD 查询

现在我们知道了 .git/HEAD.git/refs/heads 下的文件如何协同工作。开始着手实现它,首先我们定义一些相关类型:

const HASH_BYTES: usize = 20;

// A (commit) hash is a 20-byte identifier.
// We will see that git also gives hashes to other things.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Hash([u8; HASH_BYTES]);

// The head is either at a specific commit or a named branch
enum Head {
  Commit(Hash),
  Branch(String),
}
复制代码

接下来,我们将希望能够在40个字符的十六进制表示和紧凑的20字节表示之间来回转换哈希值。

use std::fmt::{self, Display, Formatter};
use std::io::Error;
use std::str::FromStr;

impl FromStr for Hash {
  type Err = Error;

  fn from_str(hex_hash: &str) -> io::Result<Self> {
    // Parse a hexadecimal string like "af64eba00e3cfccc058403c4a110bb49b938af2f"
    // into  [0xaf, 0x64, ..., 0x2f]. Returns an error if the string is invalid.
    // ...
  }
}

impl Display for Hash {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    // Turn the hash back into a hexadecimal string
    for byte in self.0 {
      write!(f, "{:02x}", byte)?;
    }
    Ok(())
  }
}
复制代码

现在我们可以编写核心逻辑:读取.git/HEAD文件并确定其对应的提交哈希值。

fn get_head() -> io::Result<Head> {
  use Head::*;

  let hash_contents = fs::read_to_string(HEAD_FILE)?;
  // Remove trailing newline
  let hash_contents = hash_contents.trim_end();
  // If .git/HEAD starts with `ref: refs/heads/`, it's a branch name.
  // Otherwise, it should be a commit hash.
  Ok(match hash_contents.strip_prefix(REF_PREFIX) {
    Some(branch) => Branch(branch.to_string()),
    _ => {
      let hash = Hash::from_str(hash_contents)?;
      Commit(hash)
    }
  })
}

impl Head {
  fn get_hash(&self) -> io::Result<Hash> {
    use Head::*;

    match self {
      Commit(hash) => Ok(*hash),
      Branch(branch) => {
        // Copied from get_branch_head()
        let ref_file = Path::new(BRANCH_REFS_DIRECTORY).join(branch);
        let hash_contents = fs::read_to_string(ref_file)?;
        Hash::from_str(hash_contents.trim_end())
      }
    }
  }
}

fn main() -> io::Result<()> {
  let head = get_head()?;
  let head_hash = head.get_hash()?;
  println!("Head hash: {}", head_hash);
  Ok(())
}
复制代码

现在,无论我们查看主分支还是直接查看提交哈希值,都会打印出来。

Head hash: af64eba00e3cfccc058403c4a110bb49b938af2f
复制代码

我们已经成功确定了当前提交的哈希值。现在,我们该如何找出该提交所存储的信息呢?

提交中有什么?

当你在GitHub这样的网页界面或通过 git show 这样的命令查看一个提交时,你会看到该提交带来的变化("diff")。

所以你可能会认为,git 会把每个提交作为一个 diff 来存储。也有可能像备份一样存储每个提交,里面包含该提交的每个文件的内容。

这两种方法其实都可以:你可以从两个文件的副本中计算出一个差异,你也可以通过按顺序应用每个差异(从空的版本库或从最近的提交开始)来计算文件的内容。使用哪种方法取决于你要优化的内容。

基于diff的方法会占用较少的存储空间;它最大限度地减少了重复的信息量,因为它只存储变化的内容。然而,存储内容使得在某个特定的提交中检查代码的速度更快,因为我们不需要应用潜在的成千上万的差异。(这也使得 git clone --depth 1 的实现变得容易,它通过只下载最近的提交来加快克隆的速度)。

而且,如果改动不大,从两个提交的内容中计算差异也不会太费时:差异算法相当快,而且git可以自动跳过没有改动的目录/文件,这个我们后面会看到。

由于这些原因,git采用了 "存储每个文件的内容" 的方法。 git的实现设法只存储一个相同文件的副本,这比天真的解决方案节省了大量的存储空间。