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/v5Core 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-gitis 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-tokenas username for GitHub; SSH auth works withPublicKeysand 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?