いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-601 (git showを使ってN世代前のファイルを出力する)

関連記事

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

概要

以下、自分用のメモです。

たまにgit管理されているファイルのN世代前を別名で出力したいってときがあったります。

普通にコマンド実行すれば

$ git show コミットのSHA:リポジトリルートからのパス

で出力できます。リポジトリルートからのパスとなるので

カレントディレクトリを対象ファイルの場所まで移動している場合は

git rev-parse --show-prefix などでリポジトリルートからのパスを取得してから実行する必要があります。

で、手習いがてらにGoでそのあたりの処理をするサンプルつくってみました。

Goで作らないといけない理由なんて特にないですが(シェルスクリプトとかPythonで書いた方が楽w)

サンプル

main.go

package main

import (
    "flag"
    "fmt"
    "io"
    "log"
    "os"
    "path/filepath"
    "strings"
)

type (
    AppArgs struct {
        NumLogCount int
        File        string
    }
)

var (
    appArgs AppArgs
    out     io.Writer
)

func init() {
    flag.IntVar(&appArgs.NumLogCount, "n", 1, "何世代前の版を取得するかの値")
    flag.StringVar(&appArgs.File, "f", "", "ファイル")
}

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

    if appArgs.File == "" {
        log.Fatal("invalid argument: file")
    }

    out = os.Stdout

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

func run() error {
    // 1.パス文字が含まれていない場合、Prefixを取得してパス作る (git rev-parse --show-prefix)
    //   含んでいる場合、ユーザが明示的に指定しているとみなしPrefixは付けない
    // 2.SHA取得      (git log --pretty=format:"%h" -世代数 ファイル名)
    // 3.ファイル生成 (git show SHA:パス)
    //
    // MEMO:
    //   git log と git show では渡すパスの形が異なる
    //     - git log  は カレントディレクトリからの相対パスを受け付ける仕様
    //     - git show は リポジトリルートからのパスを受け付ける仕様

    var (
        git   = new(GitCmd)
        fpath = appArgs.File
        err   error
    )
    if !strings.Contains(fpath, "/") {
        var (
            prefix string
        )
        if prefix, err = git.Prefix(); err != nil {
            return err
        }

        fpath = filepath.Join(prefix, fpath)
    }

    var (
        sha string
    )
    if sha, err = git.Sha(appArgs.File, appArgs.NumLogCount); err != nil {
        return err
    }
    if sha == "" {
        return fmt.Errorf("SHA取得失敗: %s", appArgs.File)
    }

    var (
        r io.ReadCloser
    )
    if r, err = git.Show(sha, fpath); err != nil {
        return err
    }
    defer r.Close()

    if _, err = io.Copy(out, r); err != nil {
        return err
    }

    return nil
}

reader.go

package main

import (
    "io"
    "os/exec"
)

type (
    CmdReader struct {
        pipe io.ReadCloser
        cmd  *exec.Cmd
    }
)

var _ io.ReadCloser = (*CmdReader)(nil)

func (me *CmdReader) Read(p []byte) (int, error) {
    return me.pipe.Read(p)
}

func (me *CmdReader) Close() error {
    var (
        errPipe = me.pipe.Close()
        errWait = me.cmd.Wait()
    )
    if errPipe != nil {
        return errPipe
    }
    if errWait != nil {
        return errWait
    }

    return nil
}

git.go

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "os/exec"
    "strings"
)

type (
    GitCmd struct{}
)

func (me *GitCmd) exec(args []string) (*CmdReader, error) {
    var (
        cmd    = exec.Command("git", args...)
        stdout io.ReadCloser
        err    error
    )
    if stdout, err = cmd.StdoutPipe(); err != nil {
        return nil, err
    }
    if err = cmd.Start(); err != nil {
        return nil, err
    }

    return &CmdReader{stdout, cmd}, nil
}

// Prefix は、git rev-parse --show-prefix を実行します。
func (me *GitCmd) Prefix() (string, error) {
    var (
        args   = []string{"rev-parse", "--show-prefix"}
        reader *CmdReader
        err    error
    )
    if reader, err = me.exec(args); err != nil {
        return "", err
    }
    defer reader.Close()

    var (
        buf = new(bytes.Buffer)
    )
    if _, err = io.Copy(buf, reader); err != nil {
        return "", err
    }

    return strings.ReplaceAll(buf.String(), "\n", ""), nil
}

// Sha は、 git log --pretty=format:"%h" を実行しcount番目のSHAを返します。
func (me *GitCmd) Sha(fpath string, count int) (string, error) {
    var (
        args   = []string{"log", "--pretty=format:'%h'", fpath}
        reader *CmdReader
        err    error
    )
    if reader, err = me.exec(args); err != nil {
        return "", err
    }
    defer reader.Close()

    var (
        buf = new(bytes.Buffer)
    )
    if _, err = io.Copy(buf, reader); err != nil {
        return "", err
    }

    var (
        scanner = bufio.NewScanner(buf)
        sha     string
    )
    for i := 0; scanner.Scan(); i++ {
        if i == count {
            sha = scanner.Text()
            break
        }
    }

    if err = scanner.Err(); err != nil {
        return "", err
    }

    return strings.ReplaceAll(sha, "'", ""), nil
}

// Show は、 git show sha:fpath を実行します。
func (me *GitCmd) Show(sha, fpath string) (io.ReadCloser, error) {
    var (
        args   = []string{"show", fmt.Sprintf("%s:%s", sha, fpath)}
        reader *CmdReader
        err    error
    )
    if reader, err = me.exec(args); err != nil {
        return nil, err
    }

    return reader, nil
}

Taskfile.yml

# https://taskfile.dev

version: '3'

vars:
  APP_NAME: gitbkup

tasks:
  default:
    cmds:
      - task: build
  build:
    cmds:
      - go build -o {{.APP_NAME}}{{exeExt}} .
  clean:
    cmds:
      - go clean

実行イメージ

$ cd /path/to/ファイルがある場所
$ gitbkup -f ファイル > /path/to/1世代前のファイル

ってすると1世代前のファイルが出力されます。

$ cd /path/to/ファイルがある場所
$ gitbkup -n 2 -f ファイル > /path/to/2世代前のファイル

ってすると2世代前のファイルが出力されます。

参考情報

tree-sitter.github.io

github.com

Goのおすすめ書籍


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

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