画一个 Sublime Merge 同款 Commit Graph

最后更新于

上面的 Git 记录来自这个真实的仓库:https://github.com/opensumi/core

我从起码 5 年前 就想画这玩意儿了,但一直缺少某些前置条件:a. 解析 Git Log 的东西和 b. 想明白这个图是怎么画的。前者我在 VS Code 的 内置 Git 插件 里抄到了,于是问题变成:

已知每个 Commit(看成列表里的元素)包含以下信息:

interface Commit {
  // 当前 commit ID
  hash: string
  // 上一个 commit 是谁,如果是 merge commit 会有两个 parent
  parents: string[]
}

想办法用它(和它前后的 commit,如果需要的话)计算出绘制 Graph(也就是上面例子中左侧的图形)的必要信息。

考虑到现实场景中存在 几十万 条 commits 的情况,显然 Sublime 不可能是离线(就是需要先获取到所有 commits 再计算结果)算的。因为 Sublime Merge 直接有最终效果供参考,可以通过瞪眼法先盲猜一下每一行需要的信息是什么:

再仔细看看,会发现:

那么可以先这么定义一下中间状态:

// 一开始会有一根预置的线表示正在等待 HEAD commit 出现
let currentTracks = [HEAD.hash]
let update = (commit) => {
  // 更新 currentTracks 并计算图
}

每次要处理某个 commit 时,对「上一次出来的线」计算「这一次他们变道到了哪一路」。什么情况下会变道呢?只有「出现了正在等待的 commit」时,这些线才会合并到第一根上,否则保持直线向下。

type Track = { depth: number; in: number[]; out: number[] }
let nextTracks = []
let merged: Track | undefined
let k = 0 // 取 commit.parents 用
for (let j = 0; j < currentTracks.length; j++) {
  let parent = currentTracks[j]
  if (parent) {
    if (parent === commit.hash) {
      if (!merged) {
        // 出现了,直接合并
        nextTracks[j] = commit.parents[k++] // 这根线接下来要找另一个 parent 了
        merged = { depth: j, in: [j], out: nextTracks[j] ? [j] : [] }
      } else {
        // 合并到第一根上,所以下一次没有这根线了
        nextTracks[j] = null
        merged.in.push(j)
      }
    } else {
      // 无关的线继续向下
      nextTracks[j] = parent
    }
  } else {
    // 空的还是空的
    nextTracks[j] = null
  }
}
// 此时没被 merge 的 parents 要么往左合并,要么往右分叉
while (commit.parents[k]) {
  let exist = currentTracks.indexOf(commit.parents[k])
  if (exist) {
    // 已经往左合并了
    merged?.out.push(exist)
    nextTracks[exist] = commit.parents[k++]
  } else {
    // 分叉,往右找到第一个空位
    let index = nextTracks.indexOf(null)
    if (index < 0) index = nextTracks.length
    nextTracks[index] = commit.parents[k++]
    if (merged) {
      // 添加到当前 merge commit 的输出里
      merged.out.push(index)
    } else {
      // 如果没有 merge commit,那么新的 parent 自动形成一路
      merged = { depth: index, in: [], out: [index] }
    }
  }
}

currentTracks = nextTracks

上述实现已经在 https://github.com/hyrious/git-tm 里使用了,感兴趣可以用用。