Updated Jul 20, 2026

08 - Git Automation

📋 Jump to Takeaways

🎁 What if every config change, branch, and PR in your team's workflow could happen programmatically, with zero manual copy-paste?

Imports in code snippets are trimmed for brevity. See the full example linked at the end for complete, compilable source.

go-git Library

The go-git library (github.com/go-git/go-git/v5) is a pure-Go Git implementation. No CGo, no libgit2, no system git binary needed. It supports clone, fetch, push, commit, branch, tag, diff, and tree traversal. Everything required for DevOps automation.

go get github.com/go-git/go-git/v5

Core types you'll use constantly:

Type Purpose
git.Repository Represents a local repo
git.Worktree Working directory operations
plumbing.Hash SHA-1 commit/object reference
object.Commit Commit metadata and tree
object.Tree Directory listing at a commit

Cloning & Checkout

import (
    "fmt"
    git "github.com/go-git/go-git/v5"
    "github.com/go-git/go-git/v5/plumbing"
)

func cloneRepo(url, dir string) (*git.Repository, error) {
    repo, err := git.PlainClone(dir, false, &git.CloneOptions{
        URL:           url,
        Depth:         1,               // shallow clone for speed
        SingleBranch:  true,
        ReferenceName: plumbing.Main,
    })
    if err != nil {
        return nil, fmt.Errorf("clone failed: %w", err)
    }
    return repo, nil
}

// Clone with progress reporting
func cloneWithProgress(url, dir string) (*git.Repository, error) {
    return git.PlainClone(dir, false, &git.CloneOptions{
        URL:      url,
        Progress: os.Stdout,
    })
}

For existing repos, open them with git.PlainOpen(path).

Commits, Branches & Tags

Creating Branches

func createBranch(repo *git.Repository, branchName string) error {
    headRef, err := repo.Head()
    if err != nil {
        return err
    }

    ref := plumbing.NewHashReference(
        plumbing.NewBranchReferenceName(branchName),
        headRef.Hash(),
    )
    return repo.Storer.SetReference(ref)
}

func checkoutBranch(repo *git.Repository, branchName string) error {
    wt, err := repo.Worktree()
    if err != nil {
        return err
    }
    return wt.Checkout(&git.CheckoutOptions{
        Branch: plumbing.NewBranchReferenceName(branchName),
    })
}

Making Commits Programmatically

import (
    "time"
    "github.com/go-git/go-git/v5/plumbing/object"
)

func commitFile(repo *git.Repository, filepath, message string) (plumbing.Hash, error) {
    wt, err := repo.Worktree()
    if err != nil {
        return plumbing.ZeroHash, err
    }

    // Stage the file
    _, err = wt.Add(filepath)
    if err != nil {
        return plumbing.ZeroHash, fmt.Errorf("staging failed: %w", err)
    }

    // Create commit
    hash, err := wt.Commit(message, &git.CommitOptions{
        Author: &object.Signature{
            Name:  "Deploy Bot",
            Email: "[email protected]",
            When:  time.Now(),
        },
    })
    if err != nil {
        return plumbing.ZeroHash, fmt.Errorf("commit failed: %w", err)
    }

    return hash, nil
}

Reading Diffs

func getChangedFiles(repo *git.Repository, fromHash, toHash plumbing.Hash) ([]string, error) {
    fromCommit, err := repo.CommitObject(fromHash)
    if err != nil {
        return nil, err
    }
    toCommit, err := repo.CommitObject(toHash)
    if err != nil {
        return nil, err
    }

    fromTree, _ := fromCommit.Tree()
    toTree, _ := toCommit.Tree()

    changes, err := fromTree.Diff(toTree)
    if err != nil {
        return nil, err
    }

    var files []string
    for _, change := range changes {
        name := change.To.Name
        if name == "" {
            name = change.From.Name // deleted file
        }
        files = append(files, name)
    }
    return files, nil
}

Authentication Methods

SSH Key Authentication

import (
    gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
    "golang.org/x/crypto/ssh"
)

func sshAuth(privateKeyPath, password string) (*gitssh.PublicKeys, error) {
    key, err := os.ReadFile(privateKeyPath)
    if err != nil {
        return nil, err
    }

    signer, err := ssh.ParsePrivateKeyWithPassphrase(key, []byte(password))
    if err != nil {
        // Try without passphrase
        signer, err = ssh.ParsePrivateKey(key)
        if err != nil {
            return nil, err
        }
    }

    return &gitssh.PublicKeys{
        User:   "git",
        Signer: signer,
    }, nil
}

Token Authentication (HTTPS)

import "github.com/go-git/go-git/v5/plumbing/transport/http"

func tokenAuth(token string) *http.BasicAuth {
    return &http.BasicAuth{
        Username: "x-access-token", // GitHub convention
        Password: token,
    }
}

// Usage in clone
func clonePrivateRepo(url, dir, token string) (*git.Repository, error) {
    return git.PlainClone(dir, false, &git.CloneOptions{
        URL:  url,
        Auth: tokenAuth(token),
    })
}

Webhook Handlers

Git hosting platforms send webhook payloads on push events. Here's a handler that validates signatures and parses the payload:

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io"
    "net/http"
)

type PushEvent struct {
    Ref        string   `json:"ref"`
    Before     string   `json:"before"`
    After      string   `json:"after"`
    Repository struct {
        FullName string `json:"full_name"`
        CloneURL string `json:"clone_url"`
    } `json:"repository"`
    Commits []struct {
        ID       string   `json:"id"`
        Added    []string `json:"added"`
        Modified []string `json:"modified"`
        Removed  []string `json:"removed"`
    } `json:"commits"`
}

func webhookHandler(secret string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        body, err := io.ReadAll(r.Body)
        if err != nil {
            http.Error(w, "read error", http.StatusBadRequest)
            return
        }

        // Verify HMAC signature (GitHub format)
        sig := r.Header.Get("X-Hub-Signature-256")
        if !verifySignature(body, sig, secret) {
            http.Error(w, "invalid signature", http.StatusUnauthorized)
            return
        }

        var event PushEvent
        if err := json.Unmarshal(body, &event); err != nil {
            http.Error(w, "parse error", http.StatusBadRequest)
            return
        }

        // Process the push event
        changedPaths := extractChangedPaths(event)
        go handleChanges(event.Repository.FullName, changedPaths)

        w.WriteHeader(http.StatusOK)
    }
}

func verifySignature(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Monorepo Change Detection

In monorepos, you need to determine which services changed to trigger selective builds.

type Service struct {
    Name string
    Path string
}

var services = []Service{
    {Name: "api", Path: "services/api/"},
    {Name: "worker", Path: "services/worker/"},
    {Name: "web", Path: "services/web/"},
    {Name: "shared", Path: "pkg/"},
}

func extractChangedPaths(event PushEvent) []string {
    seen := make(map[string]bool)
    var paths []string
    for _, commit := range event.Commits {
        for _, files := range [][]string{commit.Added, commit.Modified, commit.Removed} {
            for _, f := range files {
                if !seen[f] {
                    seen[f] = true
                    paths = append(paths, f)
                }
            }
        }
    }
    return paths
}

func affectedServices(changedPaths []string) []Service {
    var affected []Service
    for _, svc := range services {
        for _, path := range changedPaths {
            if strings.HasPrefix(path, svc.Path) {
                affected = append(affected, svc)
                break
            }
        }
    }
    // If shared code changed, rebuild everything that imports it
    for _, svc := range affected {
        if svc.Name == "shared" {
            return services // all services depend on shared
        }
    }
    return affected
}

func handleChanges(repo string, changedPaths []string) {
    affected := affectedServices(changedPaths)
    for _, svc := range affected {
        fmt.Printf("Triggering build for %s (repo: %s)\n", svc.Name, repo)
        // Output: Triggering build for api (repo: acme/platform)
        // Output: Triggering build for worker (repo: acme/platform)
    }
}

GitOps Commit Bot

Clones a repo, creates a feature branch, writes a generated config file, commits, and pushes. This is the foundational pattern for GitOps config automation where infrastructure changes are driven by Git commits.

Input: A GitHub token and repository URL. Output: A new branch pushed with an automated config commit.

Full source: examples/gitops-commit-bot

Key Takeaways

  • go-git is a pure-Go Git implementation. No external binaries, works everywhere Go compiles.
  • Use shallow clones (Depth: 1) in CI/automation for speed; full clones only when you need history.
  • HMAC-verify webhook signatures before processing payloads. Never trust unsigned requests.
  • Monorepo change detection maps file paths to services to enable selective builds.
  • Token auth uses x-access-token as username for GitHub; SSH auth works with PublicKeys and a signer.
  • The clone→branch→commit→push pattern is the foundation of GitOps config automation.

🎁 What if your services could fetch fresh, unique database credentials on every startup, and automatically rotate them before they expire?

💻 Examples

Complete examples for this lesson. Copy and run locally.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy