如何用go-git实现git log -G并获取提交增删行
实现go-git版本的
git log -G功能 问题背景
我希望借助go-git库,编写一个功能与git log -G类似的脚本。现有代码可以打印仓库内的所有提交记录,但不清楚如何获取每个提交中新增、删除的代码行,现有实现代码如下:
package main import ( "fmt" "os" "path/filepath" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" ) func main() { if len(os.Args) != 2 { fmt.Println("too few arguments. Please specify a directory containing git repos.") os.Exit(1) } err := SearchLog(os.Args[1]) if err != nil { fmt.Println(err) os.Exit(1) } } func SearchLog(dir string) error { files, err := os.ReadDir(dir) if err != nil { return err } for _, f := range files { if f.IsDir() { path := filepath.Join(dir, f.Name()) r, err := git.PlainOpen(path) if err != nil { fmt.Printf("%s %v", f.Name(), err) continue } err = searchLogInRepo(r) if err != nil { return err } } } return nil } func searchLogInRepo(r *git.Repository) error { options := git.LogOptions{} cIter, err := r.Log(&options) if err != nil { return err } err = cIter.ForEach(func(c *object.Commit) error { fmt.Println(c) return nil }) return err }
核心实现逻辑
要实现和git log -G一致的效果,核心是拿到每个提交相对其父提交的diff内容,逐行检查增删的代码是否匹配目标规则:
- 对每个遍历到的提交,先获取它的父提交:普通提交有1个父提交,合并提交有多个,根提交无父提交时用空树作为对比基准
- 调用提交树的Diff接口,拿到两个版本之间的所有文件变更补丁
- 遍历补丁的每个代码块,逐行区分新增行(前缀
+)、删除行(前缀-),和目标正则做匹配 - 只要任意一行变更内容匹配规则,就输出该提交的信息,和原生
git log -G行为对齐
修改后的完整代码
补充匹配逻辑,支持传入要搜索的正则字符串,行为和原生git log -G对齐:
package main import ( "fmt" "os" "path/filepath" "regexp" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) // 全局搜索正则,对应git log -G 后面的匹配参数 var searchPattern *regexp.Regexp func main() { if len(os.Args) != 3 { fmt.Println("usage: binary <parent-dir-of-git-repos> <search-regex-pattern>") os.Exit(1) } var err error searchPattern, err = regexp.Compile(os.Args[2]) if err != nil { fmt.Printf("invalid regex pattern: %v\n", err) os.Exit(1) } err = SearchLog(os.Args[1]) if err != nil { fmt.Println(err) os.Exit(1) } } func SearchLog(dir string) error { files, err := os.ReadDir(dir) if err != nil { return err } for _, f := range files { if f.IsDir() { repoPath := filepath.Join(dir, f.Name()) r, err := git.PlainOpen(repoPath) if err != nil { fmt.Printf("skip %s: open repo failed: %v\n", f.Name(), err) continue } if err = searchLogInRepo(r); err != nil { fmt.Printf("search %s failed: %v\n", f.Name(), err) continue } } } return nil } func searchLogInRepo(r *git.Repository) error { commitIter, err := r.Log(&git.LogOptions{}) if err != nil { return err } defer commitIter.Close() return commitIter.ForEach(func(curCommit *object.Commit) error { // 获取当前提交的文件树 curTree, err := curCommit.Tree() if err != nil { return err } // 获取父提交作为对比基准 parentIter := curCommit.Parents() defer parentIter.Close() parentCommit, err := parentIter.Next() var prevTree *object.Tree if err == object.ErrParentNotFound { // 根提交无父节点,用空树对比首次提交的全量内容 prevTree = &object.Tree{} } else if err != nil { return err } else { // 默认取第一个父提交对比,和原生git log -G默认行为一致 prevTree, err = parentCommit.Tree() if err != nil { return err } } // 生成两个版本之间的全量diff changes, err := object.DiffTree(prevTree, curTree) if err != nil { return err } // 遍历diff检查是否有匹配行 isMatched := false for _, change := range changes { patch, err := change.Patch() if err != nil { return err } for _, filePatch := range patch.FilePatches() { for _, line := range filePatch.Lines() { // 只检查新增、删除的代码行,跳过上下文行 if line.Type == object.Add || line.Type == object.Delete { if searchPattern.MatchString(line.Content) { isMatched = true break } } } if isMatched { break } } if isMatched { break } } // 匹配到规则则打印提交信息 if isMatched { fmt.Printf("=== Repo matched commit ===\ncommit %s\nAuthor: %s <%s>\nDate: %s\n\n %s\n\n", curCommit.Hash.String(), curCommit.Author.Name, curCommit.Author.Email, curCommit.Author.When.Format("Mon Jan 2 15:04:05 2006 -0700"), curCommit.Message, ) } return nil }) }
补充说明
- 合并提交默认只和第一个父提交对比,如果需要检查所有父分支的变更,可以循环遍历parentIter的所有父提交做diff,对应原生git的
-m参数行为 - 代码里做了提前跳出逻辑,匹配到第一行符合规则的内容就终止当前提交的diff遍历,大仓库下性能更好
- go-git会自动跳过二进制文件的diff解析,不需要额外做文件类型判断,和原生git行为一致
内容的提问来源于stack exchange,提问作者guettli
相关产品推荐
相关产品推荐

