golangのgit ライブラリ「go-git」を使ってインメモリでgit操作をする

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。齋藤です。 今日はgolangを使ってgitの操作をやります。

はじめに

golangには pure go implementation な git のライブラリがあります。

今回の記事では、このライブラリを使って git の操作を行います。

今回動かしたいシナリオは以下のようなものです。

  • あるリポジトリAを リリースする
  • リポジトリAのイベントをhook起点に別のリポジトリBに コミットして PRを送る

今回はリポジトリBに commit して PRを送るに着目して この記事では リポジトリBを clone した後にファイルを変更・commit して push までをやってみます。

なお後々、Webhook などで動かしたいので AWS Lambda の上で動かしてみます。 gitの操作は全てインメモリで行います。

準備

今回はAWS Lambdaで実行することを考えているので アクセストークンを利用してリポジトリを clone します。

そのため、GitHubからなんらかの方法でrepo scopeのトークンを取得しておいてください。 なお、go-gitの場合、実行環境でGitHub に設定しているssh鍵がある場合は不要です。

lambdaを起動するための cloudformation

本題ではないので、ここは以下のようにさっくり設定しておきます。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: golang-lambda-test
Parameters:
  GitHubToken:
    Type : String
    Description : Enter github repo scope token.
Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      Handler: handler
      Runtime: go1.x
      CodeUri: handler.zip
      Handler: handler
      Role: !GetAtt GoOnLambdaIamRole.Arn
      Timeout: 60
      Environment: 
        Variables:
          GITHUB_ACCESS_TOKEN: !Ref GitHubToken
      Events:
        ProxyApiRoot:
          Type: Api
          Properties:
            Path: /
            Method: ANY
  GoOnLambdaIamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      Path: /
      Policies:
      - PolicyName: root
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup # ログ出力のためのポリシー
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: '*'

サンプルコードとビルド

サンプルコードです。 cloneします。

インメモリでcloneしています。

    f := memfs.New()
    repo, err := git.Clone(memory.NewStorage(), f, &git.CloneOptions{
        URL:           "https://" + userID + ":" + token + "@github.com/<your-id>/<your-repository>.git",
        ReferenceName: plumbing.ReferenceName("refs/heads/develop"),
    })

新しいブランチをcheckoutします。

    w, err := repo.Worktree()
    err = w.Checkout(&git.CheckoutOptions{
        Create: true,
        Branch: "feature/test-update",
    })

リポジトリにあるファイルを書き換えます。

    // ファイルを書き換える
    // f := memfs.New()
    err = rewriteVersion(f, ur.Version)

func rewriteVersion(fs billy.Filesystem, version string) (err error) {
    file, err := fs.Open(".version")
    if err != nil {
        return
    }
    b, err := ioutil.ReadAll(file)
    if err != nil {
        return
    }
    err = file.Close()
    if err != nil {
        return
    }

    var obj interface{}
    json.Unmarshal(b, &obj)
    jsonpointer.Set(obj, "/version", version)
    rb, err := json.MarshalIndent(obj, "", "    ")

    if err != nil {
        return
    }
    err = fs.Remove(".version")
    if err != nil {
        return
    }

    file, err = fs.OpenFile(".version", os.O_RDWR|os.O_CREATE, 0666)
    if err != nil {
        return
    }

    _, err = file.Write(rb)
    if err != nil {
        return
    }

    _, err = file.Write([]byte("\n"))
    return
}

今度は書き換えたファイルをcommitします。

    // Commitする
    _, err = w.Add(".version")
    ref := plumbing.ReferenceName("feature/test-update")
    if err == nil {
        hash, _ := w.Commit("update version", &git.CommitOptions{
            Author: &object.Signature{
                Name:  "<your-id>",
                Email: "<your-mail-address>",
                When:  time.Now(),
            },
    })
    // ここが何故か必要だったのだけれど調べてない
        repo.Storer.SetReference(plumbing.NewReferenceFromStrings("feature/test-update", hash.String()))
    }

コミットしたブランチをpushします。

    // Pushする
    remote, err := repo.Remote("origin")
    if err == nil {
        err = remote.Push(&git.PushOptions{
            Progress: os.Stdout,
            RefSpecs: []config.RefSpec{
                config.RefSpec(ref + ":" + plumbing.ReferenceName("refs/heads/feature/test-update")),
            },
        })
    }

サンプルコード全体はこちらから見れます。

ビルドは以下のコマンドで AWS Lambda向けにビルドしておきます。 今回は簡略化のためにビルドのオプションは渡していません。 必要に応じて適切にビルドのオプションを設定してください。

# zip作っておく。
GOOS=linux GOARCH=amd64 go build -o handler handler.go && zip handler.zip handler

アプリケーションのデプロイ

先ほどビルドしたバイナリがあることを前提にしています。

# s3にcodeをアップロードしつつ templateを出力
aws cloudformation package --template-file template.yml --output-template-file packed.yml --s3-bucket <your-bucket>
# 出力されたテンプレートを使って、GitHubのTokenを渡しつつデプロイ
aws cloudformation deploy --template-file packed.yml --stack-name lambda-go-test-stack --parameter-overrides "GitHubToken=<token>" --capabilities CAPABILITY_IAM

デプロイしてlambdaを叩いてみます

今回はデプロイはマネージメントコンソールから行いました。 今回のサンプルコードでは、以下のような形で呼び出すことができます。

curl -XPOST https://$REST_API_ID.execute-api.$REGION.amazonaws.com/$STAGE -d '{"version": "from curl"}'
# updated version

まとめ

今回は golangとgitのライブラリである、go-gitを使って、基本的なgit操作を行いました。 今回のサンプルコードでは、GitHubにあるprivateリポジトリにあるコードを インメモリで編集してPushする所まで行いました。

ここからPRを作る所まで自動化できると楽しそうですね。

また、最近isomorphic-git という node/browserで動くgitのライブラリもあるので 気になっています。git操作に関するエコシステムが育っていくと楽しそうですね。

今度はここから発展した記事を書く予定でいます。 ではまた次の記事でお会いしましょう。

おまけ: IP制限

# StackにRestApiが一つしかないのでこれで大丈夫
REST_API_ID=$(aws cloudformation describe-stack-resources --stack-name lambda-go-test-stack | jq '.StackResources[] | select(.ResourceType == "AWS::ApiGateway::RestApi") | .PhysicalResourceId' -r)

# ポリシードキュメントを `jq ". | tostring" -c` でエスケープ。
JSON=$(cat << EOF | jq ". | tostring" -c
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": [
               "arn:aws:execute-api:<>:<your-account-id>:$REST_API_ID/*"
            ],
            "Condition" : {
                "IpAddress": {
                    "aws:SourceIp": [ "<your-ip-address>" ]
                }
            }
        }
    ]
}
EOF
)

# 設定を更新
aws apigateway update-rest-api --rest-api-id $REST_API_ID --patch-operations op=replace,path=/policy,value=$JSON
# 設定を更新後、再デプロイする必要がある。
aws apigateway update-stage --rest-api-id $REST_API_ID --stage-name <your-stage>

curl -XPOST https://$REST_API_ID.execute-api.$REGION.amazonaws.com/prod -d '{"version": "from curl"}'
# updated version