一个本地git版本管理示例的实现

举报
码乐 发表于 2025/08/31 08:31:35 2025/08/31
【摘要】 1 简介在git中版本管理使用了递归树状结构。每个目录对应一个独立的tree对象,包含子tree(目录)和blob(文件)的条目。构建tree时,Git递归遍历目录层次,从叶子节点向上构建tree对象,确保每个子目录的tree哈希被包含在上层tree中。 2 示例:这个教学实现只做“扁平树”(直接存“路径→blob 哈希”),且只在 checkout 里对已跟踪文件做最小清理;未实现复杂的...

1 简介

在git中版本管理使用了递归树状结构。

每个目录对应一个独立的tree对象,包含子tree(目录)和blob(文件)的条目。

构建tree时,Git递归遍历目录层次,从叶子节点向上构建tree对象,确保每个子目录的tree哈希被包含在上层tree中。

2 示例:

这个教学实现只做“扁平树”(直接存“路径→blob 哈希”),且只在 checkout 里对已跟踪文件做最小清理;

未实现复杂的工作区脏检查与冲突检测,实际应用环境要更严格。

package main

import (
    "bufio"
    "bytes"
    "crypto/sha1"
    "encoding/hex"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "os"
    "path/filepath"
    "sort"
    "strings"
    "time"
)

const (
    repoDir      = ".tinyvcs"
    objectsDir   = ".tinyvcs/objects"
    refsHeadsDir = ".tinyvcs/refs/heads"
    headFile     = ".tinyvcs/HEAD"
    indexFile    = ".tinyvcs/index"
)

type Commit struct {
    Tree      string `json:"tree"`
    Parent    string `json:"parent,omitempty"`
    Message   string `json:"message"`
    Author    string `json:"author"`
    Timestamp int64  `json:"timestamp"`
}

type Index map[string]string // path -> blobHash

func main() {
    if len(os.Args) < 2 {
        usage()
        return
    }
    cmd := os.Args[1]
    switch cmd {
    case "init":
        must(initRepo())
    case "add":
        must(ensureRepo())
        if len(os.Args) < 3 {
            fatal("tinyvcs add <file...>")
        }
        must(addFiles(os.Args[2:]))
    case "commit":
        must(ensureRepo())
        msg := parseCommitMessage(os.Args[2:])
        if msg == "" {
            fatal(`tinyvcs commit -m "message"`)
        }
        must(commit(msg))
    case "branch":
        must(ensureRepo())
        if len(os.Args) == 2 {
            must(listBranches())
        } else {
            must(createBranch(os.Args[2]))
        }
    case "checkout":
        must(ensureRepo())
        if len(os.Args) != 3 {
            fatal("tinyvcs checkout <branch>")
        }
        must(checkoutBranch(os.Args[2]))
    case "log":
        must(ensureRepo())
        must(showLog())
    case "status":
        must(ensureRepo())
        must(showStatus())
    default:
        usage()
    }
}

func usage() {
    fmt.Println(`tinyvcs - a tiny VCS in Go

Usage:
  tinyvcs init
  tinyvcs add <file...>
  tinyvcs commit -m "message"
  tinyvcs branch              # list
  tinyvcs branch <name>       # create
  tinyvcs checkout <name>
  tinyvcs log
  tinyvcs status
`)
}

func fatal(msg string) {
    fmt.Fprintln(os.Stderr, "Error:", msg)
    os.Exit(1)
}

func must(err error) {
    if err != nil {
        fatal(err.Error())
    }
}

func ensureRepo() error {
    if _, err := os.Stat(repoDir); err != nil {
        return errors.New("not a tinyvcs repo (run `tinyvcs init` first)")
    }
    return nil
}

func initRepo() error {
    paths := []string{repoDir, objectsDir, refsHeadsDir}
    for _, p := range paths {
        if err := os.MkdirAll(p, 0o755); err != nil {
            return err
        }
    }
    // HEAD -> master
    if _, err := os.Stat(headFile); errors.Is(err, os.ErrNotExist) {
        if err := os.WriteFile(headFile, []byte("ref: refs/heads/master\n"), 0o644); err != nil {
            return err
        }
    }
    // Empty index
    if _, err := os.Stat(indexFile); errors.Is(err, os.ErrNotExist) {
        if err := saveIndex(Index{}); err != nil {
            return err
        }
    }
    // Create empty master ref if not exists
    masterRef := filepath.Join(refsHeadsDir, "master")
    if _, err := os.Stat(masterRef); errors.Is(err, os.ErrNotExist) {
        if err := os.WriteFile(masterRef, []byte(""), 0o644); err != nil {
            return err
        }
    }
    fmt.Println("Initialized empty tinyvcs repository in", repoDir)
    return nil
}

func parseCommitMessage(args []string) string {
    for i := 0; i < len(args); i++ {
        if args[i] == "-m" && i+1 < len(args) {
            return args[i+1]
        }
    }
    return ""
}

// ---------- object store ----------

func hashAndPath(typ string, data []byte) (string, string) {
    content := []byte(typ + "\n")
    content = append(content, data...)
    sum := sha1.Sum(content)
    hash := hex.EncodeToString(sum[:])
    return hash, filepath.Join(objectsDir, hash)
}

func writeObject(typ string, data []byte) (string, error) {
    hash, path := hashAndPath(typ, data)
    if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
        payload := append([]byte(typ+"\n"), data...)
        if err := os.WriteFile(path, payload, 0o644); err != nil {
            return "", err
        }
    }
    return hash, nil
}

func readObject(hash string) (string, []byte, error) {
    path := filepath.Join(objectsDir, hash)
    f, err := os.Open(path)
    if err != nil {
        return "", nil, err
    }
    defer f.Close()
    r := bufio.NewReader(f)
    typ, err := r.ReadString('\n')
    if err != nil {
        return "", nil, err
    }
    typ = strings.TrimSpace(typ)
    data, err := io.ReadAll(r)
    return typ, data, err
}

func writeBlob(path string) (string, error) {
    b, err := os.ReadFile(path)
    if err != nil {
        return "", err
    }
    return writeObject("blob", b)
}

func writeTree(entries map[string]string) (string, error) {
    // entries: path -> blobHash
    buf, err := json.Marshal(entries)
    if err != nil {
        return "", err
    }
    return writeObject("tree", buf)
}

func readTree(hash string) (map[string]string, error) {
    typ, data, err := readObject(hash)
    if err != nil {
        return nil, err
    }
    if typ != "tree" {
        return nil, errors.New("object is not a tree: " + hash)
    }
    var m map[string]string
    if err := json.Unmarshal(data, &m); err != nil {
        return nil, err
    }
    return m, nil
}

func writeCommit(c Commit) (string, error) {
    buf, err := json.Marshal(c)
    if err != nil {
        return "", err
    }
    return writeObject("commit", buf)
}

func readCommit(hash string) (*Commit, error) {
    typ, data, err := readObject(hash)
    if err != nil {
        return nil, err
    }
    if typ != "commit" {
        return nil, errors.New("object is not a commit: " + hash)
    }
    var c Commit
    if err := json.Unmarshal(data, &c); err != nil {
        return nil, err
    }
    return &c, nil
}

func readBlob(hash string) ([]byte, error) {
    typ, data, err := readObject(hash)
    if err != nil {
        return nil, err
    }
    if typ != "blob" {
        return nil, errors.New("object is not a blob: " + hash)
    }
    return data, nil
}

// ---------- index ----------

func loadIndex() (Index, error) {
    b, err := os.ReadFile(indexFile)
    if err != nil {
        return nil, err
    }
    var idx Index
    if len(b) == 0 {
        return Index{}, nil
    }
    if err := json.Unmarshal(b, &idx); err != nil {
        return nil, err
    }
    return idx, nil
}

func saveIndex(idx Index) error {
    b, err := json.MarshalIndent(idx, "", "  ")
    if err != nil {
        return err
    }
    return os.WriteFile(indexFile, b, 0o644)
}

// ---------- refs & HEAD ----------

func readHEADRef() (string, error) {
    b, err := os.ReadFile(headFile)
    if err != nil {
        return "", err
    }
    line := strings.TrimSpace(string(b))
    if !strings.HasPrefix(line, "ref: ") {
        return "", errors.New("HEAD is detached or invalid")
    }
    return strings.TrimPrefix(line, "ref: "), nil
}

func readRef(refPath string) (string, error) {
    b, err := os.ReadFile(filepath.Join(repoDir, refPath))
    if err != nil {
        return "", err
    }
    return strings.TrimSpace(string(b)), nil
}

func writeRef(refPath, hash string) error {
    return os.WriteFile(filepath.Join(repoDir, refPath), []byte(hash+"\n"), 0o644)
}

3 数据结构和对象模型的区别

Tree对象的表示和构建算法:

Git: 使用递归树状结构。每个目录对应一个独立的tree对象,包含子tree(目录)和blob(文件)的条目。

构建tree时,Git递归遍历目录层次,从叶子节点向上构建tree对象,确保每个子目录的tree哈希被包含在上层tree中。

这允许高效的子树复用和增量更新。哈希计算基于排序后的条目列表(mode + name + hash),确保确定性。

本地git工具: Tree简化成一个扁平的map<string, string>(路径 -> blob哈希),序列化为单个JSON对象。构建时,直接从index收集所有文件路径,排序后序列化成一个tree对象。没有递归子tree,所有路径(如"dir/sub/file")都扁平存储。

这在算法上简化了遍历(无需递归构建),但在大型仓库中可能导致tree对象过大,且无法高效复用子目录(每次commit都需完整重写tree,即使只改动子目录)。区别在于Git的算法支持O(1)子树引用,而tinyvcs是O(n)线性存储(n为文件数)。

Commit对象的结构和历史遍历:

Git: Commit包含tree哈希、多个parent(支持合并commit,有多个父节点)、author、committer、message等。

历史遍历使用DAG(有向无环图)算法,支持从任意commit回溯多条路径(e.g., via BFS/DFS遍历parents)。这允许复杂操作如merge-base计算(找到共同祖先)。

tinyvcs: Commit只支持单个parent(线性历史),无多个parents。因此,历史是严格的单链表(linked list),遍历算法简化成while循环跟随parent直到空。

缺少合并支持,无法处理分支合并的算法(如三路合并或递归合并策略)。此外,timestamp是Unix时间戳,但无committer区分。

Blob和对象哈希计算:

Git: Blob哈希是SHA-1("blob " + size + \0 + content)。对象存储使用deflate压缩,支持loose objects和packfiles(打包优化,使用delta压缩算法减少存储)。

示例本地git: 哈希是SHA-1(typ + “\n” + data),无size字段,无压缩。算法更简单,但缺少delta压缩(每个对象独立存储),在重复文件多时存储效率低。

无packfiles算法,无法优化历史仓库大小。

4 小结

进一步扩展的方向

三方合并(merge)

读取共同祖先(lowest common ancestor),对文本执行 diff3。

Go 可自己实现 LCS/三方合并,或引入库(例如 sergi/go-diff)做行级合并与冲突标记。

目录树分层

将 tree 做成真正的分层结构,entry 包含 mode/name/oid/type,更贴近 Git。

状态检测(status)

比对工作区 vs index、index vs HEAD tree,给出“未跟踪/已修改/已暂存”的三分视图。

远端与协作

简单 HTTP 远端:

push:把缺失对象与 refs 上传到服务端(set-if-unmodified 以防并发覆盖)。

pull/fetch:下载对象并更新本地 refs(不自动合并)。

做到这里,你就具备最简版“多人协作”的雏形。

安全与一致性

对象写入时使用临时文件 + 原子 rename;refs 更新使用锁文件(.lock)。

校验对象哈希一致性,防止损坏。

性能

打包(packfile)、delta 压缩、并行 I/O、内存映射。

可用性

ignore 规则、.attributes、hooks、GPG 签名、Conventional Commits 校验等。

将这些理念落地到团队协作的实施建议

先选一个分支模型(建议:GitHub Flow 为默认;发布驱动型再考虑 Git Flow)。

配置:受保护分支、强制 PR、强制 CI、简称统一命名与 Conventional Commits。

文化:小批量提交、小 PR、自动化测试优先、Feature Flag 控灰度、禁 force-push 到共享分支。

可视化:配套看板(Issue/PR 状态)、发布看板(release 与 hotfix)。

复盘:每个版本做一次“变更审计”(Changelog 自动化 + 失败案例复盘)。

可以继续添加功能:merge/rebase、真正的分层 tree、远端同步。

【版权声明】本文为华为云社区用户翻译文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容, 举报邮箱:cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。