この記事はラクスアドベントカレンダー2025 17日目の記事です。
はじめに
こんにちは! エンジニア3年目のTKDSです! 今回はgo-gitについて紹介します。
go-gitとは
https://github.com/go-git/go-git
goで書かれたgit操作用ライブラリです。
git操作とGoコードを組み合わせかけるので、非常に汎用性が高く便利です。
基本的な操作
まず基本的な操作を紹介します。
今回はPublicリポジトリなので、認証部分は省いてます。
一部抜粋して載せてきます。
全文はリポジトリに載せてあるので、main.goにコピペして実行してください。
- clone
リモートリポジトリをcloneする操作は以下のとおりです。
r, err := git.PlainClone("poc/", &git.CloneOptions{ URL: "https://github.com/tkeshun/go-git-poc", Progress: os.Stdout, // 進捗を表示したくなければ nil でOK })
これを使って、cloneしてからlogを表示するコードを実行すると以下のようになります。

- add
addしてステータス確認
if _, err := wt.Add(addpath); err != nil { slog.Error("Add Error: " + err.Error()) return } st, _ := wt.Status() print(st.String())
addして取り消すコードを実行すると以下のようになります。

なぞの文字列とファイル名が表示されてますが、Aがadd, ??がステージング前を表しているようです。
- commit
commitする操作は以下のとおりです。
if _, err = wt.Commit("test", &git.CommitOptions{ Author: &object.Signature{ Name: "go-git-invalid", Email: "", When: time.Now(), }, }); err != nil { slog.Error("commit Error: " + err.Error()) }
commitするコードを実行するしてgit logでみるとcommitが追加されてるのがわかります。
このcommitを消したいときはheadを親commitに戻して、indexをリセットすれば良いです。 ※ Errorハンドリング書くのめんどくさくなって省略してますが、実際はだめです!!!
headRef, _ := r.Head()
headCommit, _ := r.CommitObject(headRef.Hash())
parent, _ := headCommit.Parent(0)
wt.Reset(&git.ResetOptions{
Commit: parent.Hash,
Mode: git.MixedReset,
})

さっきのcommitが消えて親commitに戻ってます。 ここまででよく使う操作は紹介できました。
- push(※番外)
一応公式のexampleにPushもあります。
現状私があまり必要としてないため飛ばします。
興味がある人は公式exampleみて試してみてください。
このように、goコード上からclone, add, commit, pushなどの基本の操作が可能です。
応用的な操作
次に応用的な使い方です。
- 指定したcommitハッシュ間の比較
Hash値を指定して比較することで差分の比較が可能です。
ファイル差分を単純に比較することや差分内容を取得することができます。
commitが隣合わせでなくても差分は出すことができ、1commitで複数のファイルをcommitしていたとしても1ファイルずつ差分を取り出せます。
t1, _ := r.CommitObject(commit1Hash)
t2, _ := r.CommitObject(commit2Hash)
p, _ := t1.Patch(t2)
この3つの処理で差分を取り出せ、p.FilePatches()に差分が入ってるので、それをループで回すとファイルごとに2点のcommit間の差分を取り出せます。
for _, fp := range p.FilePatches() {
}
色々試したサンプルを載せおきます。
package main import ( "fmt" "log/slog" git "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/plumbing" ) func main() { r, err := git.PlainOpen("./poc") if err != nil { slog.Error(err.Error()) return } commit1Hash := plumbing.NewHash("cacf82fd8a097b8266fb7a7c84f02a9a1a599a4c") commit2Hash := plumbing.NewHash("b20da7389753009b3cd6be1ae0cc66e5551e8916") t1, _ := r.CommitObject(commit1Hash) t2, _ := r.CommitObject(commit2Hash) p, _ := t1.Patch(t2) print(p.String()) for _, fp := range p.FilePatches() { from, to := fp.Files() if from != nil { f := from.Path() fmt.Println(f) } if to != nil { t := to.Path() fmt.Println("file: " + t) } for _, c := range fp.Chunks() { fmt.Println(c.Content()) } } }
- ブランチ間の比較
Reference関数でブランチとハッシュが取れるので、Reference関数が要求するplumbing.ReferenceNameをplumbing.NewBranchReferenceNameを作ります。
Reference関数を使うと返り値で*plumbing.Referenceが取得できます。
*plumbing.Reference.Hashを使うとハッシュ値が取得できるので、あとは同じようにcommitハッシュの差分をとれば、ファイル差分の取得ができます。
r, err := git.PlainOpen("./poc") if err != nil { slog.Error(err.Error()) return } ref1, err := r.Reference(plumbing.NewBranchReferenceName("main"), true) if err != nil { slog.Error(err.Error()) return } fmt.Println(ref1) ref2, err := r.Reference(plumbing.NewBranchReferenceName("test"), true) if err != nil { slog.Error(err.Error()) return } // あとは同じ
- 最新版と旧版の比較
fetchと組み合わせて使うことで、現在あるremoteの状態と最新のremoteの状態の比較が可能です。
コードは以下のようになってます。
package main import ( "fmt" "log/slog" git "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/config" "github.com/go-git/go-git/v6/plumbing" ) func main() { r, err := git.PlainOpen("./poc") if err != nil { slog.Error(err.Error()) return } remoteRefName := plumbing.NewRemoteReferenceName("origin", "test") beforeRef, err := r.Reference(remoteRefName, true) if err != nil { slog.Error(err.Error()) return } beforeHash := beforeRef.Hash() err = r.Fetch(&git.FetchOptions{ RemoteName: "origin", RefSpecs: []config.RefSpec{ "+refs/heads/test:refs/remotes/origin/test", }, }) if err != nil && err != git.NoErrAlreadyUpToDate { slog.Error(err.Error()) return } afterRef, err := r.Reference(remoteRefName, true) if err != nil { slog.Error(err.Error()) return } afterHash := afterRef.Hash() if beforeHash == afterHash { fmt.Println("diff: no changes") return } beforeCommit, err := r.CommitObject(beforeHash) if err != nil { slog.Error(err.Error()) return } afterCommit, err := r.CommitObject(afterHash) if err != nil { slog.Error(err.Error()) return } patch, err := beforeCommit.Patch(afterCommit) if err != nil { slog.Error(err.Error()) return } for _, fp := range patch.FilePatches() { _, to := fp.Files() if to != nil { t := to.Path() fmt.Println("file: " + t) } for _, c := range fp.Chunks() { fmt.Print(c.Content()) } } }
実行してみると下記画像のようになります。
2回目はもう更新済みなのでdiffがでません。

このように、単純なcommit間の差分、ブランチ間の差分、最新化した差分など様々な差分が取得できます。
実例
使用例を出してみました。
以前、登壇したことのある発表で出したツールの簡易版です。
差分があったファイルのみSQL実行
応用例で紹介したように、現状のHeadのcommit Hashを保持しておいてfetchをしたあとまたHeadのcommitを取得することで、更新差分が取得できます。
これを使うと、ブランチに新しく追加されたファイルを検知し、実行することができます。
コードは長いのでリポジトリをみてください。
差分検出にsql実行部分をくっつけます。
commitとpushして実行します。

init.sqlは以下のようになってます。
CREATE TABLE IF NOT EXISTS demo_items ( id bigserial PRIMARY KEY, name text NOT NULL UNIQUE, note text NOT NULL DEFAULT '' );
go run main.goで実行したあと、DBにつないで実行するとテーブルが作成されていることがわかります。

今回は単純な実行コードですが、もう少し作り込むならSQLファイルの内容チェックなどがあったほうがよいですね。
insertとselectも順番にやってみましょう

投入と検索もうまくいきました。
もし差分検知して自動実行したい場合は、これを定期起動すれば差分の自動実行もうまくいきそうです。
まとめ
ここまでみていただきありがとうございました。
今回はgo-gitの紹介と簡単な実用例を紹介しました!
実際に社内用のプロダクトでも使われており非常に便利なライブラリです。
この記事でgo-gitに興味持った人は、ぜひgitを活かして色々やっててみてください!