You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 00:24:37