以下の内容はhttps://devlights.hatenablog.com/entry/2025/05/29/073000より取得しました。


Goメモ-583 (gitコマンドを使って特定コミット間で変更されたファイルを取得してZIPで出力する)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。存在を忘れてしまいそうなので、忘れないうちにメモメモ。。。

たまーにですが、特定のコミットで変更されたファイル一式を手元に置いておきたい場合があったりします。

以下のコマンドを実行したら出来るのですが

$ REV1=$(git rev-parse HEAD~1)
$ REV2=$(git rev-parse HEAD)
$ git archive --prefix=archive/ main $(git diff --name-only ${REV1} ${REV2}) -o archive.zip

毎回、これを思い出して打ち込んで実行するのを覚えてる自信がまず無いです。

てことで、ツールにしてしまって、コマンド引数のデフォルトで直近コミットのファイル一覧をアーカイブ出力するようにしてしまえば良いかなと。

んじゃ、シェルスクリプト作ればよいやんってなりますが、WindowsとLinuxでシェルスクリプトを分けて作らないといけないのも面倒なので、Goでコマンド呼び出しちゃえという感じ。

サンプル

main.go

package main

import (
    "archive/zip"
    "bytes"
    "flag"
    "fmt"
    "io"
    "log"
    "os"
    "os/exec"
    "strings"
)

const (
    DefaultCommitFrom      = "HEAD~1"
    DefaultCommitTo        = "HEAD"
    DefaultRepoPath        = "."
    DefaultArchivePrefix   = "archive"
    DefaultArchiveFilePath = "./archive.zip"
)

type (
    Gitcmd string
)

func NewGitcmd() Gitcmd {
    return Gitcmd("git")
}

func (me Gitcmd) Exec(args ...string) ([]byte, error) {
    var (
        cmd = exec.Command(string(me), args...)
        out []byte
        err error
    )
    out, err = cmd.CombinedOutput()
    if err != nil {
        return nil, err
    }

    return bytes.TrimSuffix(out, []byte("\n")), nil
}

func (me Gitcmd) ExecString(args ...string) (string, error) {
    var (
        out []byte
        err error
    )
    out, err = me.Exec(args...)
    if err != nil {
        return "", err
    }

    return string(out), nil
}

type (
    Args struct {
        CommitFrom      string
        CommitTo        string
        RepoPath        string
        ArchivePrefix   string
        ArchiveFilePath string
        Verbose         bool
    }
)

func (me *Args) restore() {
    if me.CommitFrom == "" {
        me.CommitFrom = DefaultCommitFrom
    }
    if me.CommitTo == "" {
        me.CommitTo = DefaultCommitTo
    }
    if me.RepoPath == "" {
        me.RepoPath = DefaultRepoPath
    }
    if me.ArchivePrefix == "" {
        me.ArchivePrefix = DefaultArchivePrefix
    }
    if me.ArchiveFilePath == "" {
        me.ArchiveFilePath = DefaultArchiveFilePath
    }
}

var (
    args Args
)

func init() {
    flag.StringVar(&args.CommitFrom, "from", DefaultCommitFrom, "起点となるコミットハッシュ")
    flag.StringVar(&args.CommitTo, "to", DefaultCommitTo, "終点となるコミットハッシュ")
    flag.StringVar(&args.RepoPath, "repo", DefaultRepoPath, "リポジトリパス")
    flag.StringVar(&args.ArchivePrefix, "prefix", DefaultArchivePrefix, "アーカイブ内のプレフィックスディレクトリ")
    flag.StringVar(&args.ArchiveFilePath, "archive", DefaultArchiveFilePath, "アーカイブファイル")
    flag.BoolVar(&args.Verbose, "v", false, "詳細表示")
}

func main() {
    log.SetFlags(0)
    flag.Parse()

    (&args).restore()

    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    var (
        git = NewGitcmd()
        err error
    )

    // リポジトリの場所に移動
    err = os.Chdir(args.RepoPath)
    if err != nil {
        return err
    }

    // --------------------------------------------------------------------------------------------
    // 以下と同じことをする
    //
    // $ REV1=$(git rev-parse HEAD~1)
    // $ REV2=$(git rev-parse HEAD)
    // $ git archive --prefix=archive/ main $(git diff --name-only ${REV1} ${REV2}) -o archive.zip
    // --------------------------------------------------------------------------------------------

    // コミットハッシュを取得
    var (
        from string
        to   string
    )
    from, err = git.ExecString("rev-parse", args.CommitFrom)
    if err != nil {
        return err
    }
    to, err = git.ExecString("rev-parse", args.CommitTo)
    if err != nil {
        return err
    }

    if args.Verbose {
        log.Printf("起点コミットハッシュ: %s(%s)", args.CommitFrom, from)
        log.Printf("終点コミットハッシュ: %s(%s)", args.CommitTo, to)
    }

    // 変更されたファイルの一覧を取得
    var (
        diff  string
        files []string
    )
    diff, err = git.ExecString("diff", "--name-only", from, to)
    if err != nil {
        return err
    }
    files = strings.Split(diff, "\n")
    if len(files) == 0 {
        return nil
    }

    // ZIPファイルを作成
    var (
        zipFile *os.File
        writer  *zip.Writer
    )
    zipFile, err = os.Create(args.ArchiveFilePath)
    if err != nil {
        return err
    }
    defer zipFile.Close()

    writer = zip.NewWriter(zipFile)
    defer writer.Close()

    for _, file := range files {
        if file == "" {
            continue
        }

        // ファイルの内容を取得し、ZIPファイルにエントリ書き込み
        var (
            contents []byte
            entry    io.Writer
        )
        contents, err = git.Exec("show", fmt.Sprintf("%s:%s", to, file))
        if err != nil {
            return err
        }

        entry, err = writer.Create(fmt.Sprintf("%s/%s", args.ArchivePrefix, file))
        if err != nil {
            return err
        }

        if _, err = entry.Write(contents); err != nil {
            return err
        }

        if args.Verbose {
            log.Printf("ファイル追加: %s", file)
        }
    }
    if err = writer.Close(); err != nil {
        return err
    }

    return nil
}

Taskfile.yml

# https://taskfile.dev

version: '3'

vars:
  APP_NAME: gitar

tasks:
  default:
    cmds:
      - task: clean
      - task: build
      - task: run
  clean:
    cmds:
      - rm -f ./{{.APP_NAME}}
  build:
    cmds:
      - go build -o {{.APP_NAME}} .
  run:
    cmds:
      - ./{{.APP_NAME}}
      - unzip -l archive.zip

自分で使う場合のコマンド名は git + ar(chive) で gitar って名前で go install しておいて利用したりしています。たまーに便利です。

参考情報

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。




以上の内容はhttps://devlights.hatenablog.com/entry/2025/05/29/073000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14