Skip to content

Clone and commit

The core round trip: acquire a repository, change files, record the change, publish it.

Throughout, ctx is a context.Context you supply. Every network-touching call takes one so a hung remote can be cancelled. See context and cancellation.

Clone into a directory

r, err := repo.NewRepo(repo.Settings{
    Forge: repo.ForgeGitLab,
    Token: repo.StaticToken(os.Getenv("GITLAB_TOKEN")),
    FS:    afero.NewOsFs(),
})
if err != nil {
    return err
}

gitRepo, worktree, err := r.Clone(ctx, url, "/tmp/work", repo.WithShallowClone(1))

Clone targets an explicit path. OpenLocal opens a path that may or may not already be a repository, and OpenInMemory clones into RAM. See work in memory. All three bind the repository and worktree to r, so later calls need no handle.

They also return the raw *git.Repository and *git.Worktree for setup-time convenience. Use them during setup only; do not hand them to other goroutines (why).

Start a fresh repository

OpenLocal init-if-absent, which means it cannot tell "opened an existing repository" from "initialised a new one". When that distinction matters (scaffolding that must never git init inside someone's existing checkout) use the init-only pair:

found, err := repo.DiscoverRepository(target) // read-only upward probe
if err != nil {
    return err
}

if !found {
    _, _, err = r.InitLocal(target, "main") // ErrAlreadyRepository if it is one
}

DiscoverRepository walks upward using git's own discovery semantics, so a subdirectory of a repository reports true. InitLocal defaults to branch main when you pass "".

Change files

Write through the worktree filesystem. Files written there are the worktree, with no sync step:

fs, err := r.WorkFS()
if err != nil {
    return err
}

if err := afero.WriteFile(fs, "README.md", body, 0o644); err != nil {
    return err
}

See read and write the worktree for the live-view-versus-copy distinction.

Stage and commit

if err := r.AddAll(); err != nil {
    return err
}

hash, err := r.Commit(ctx, "docs: add readme", nil)

AddAll honours .gitignore; Add(\".\") does not

AddAll wraps go-git's AddWithOptions{All: true}, which applies the repository's ignore patterns, so build artefacts stay out of the commit. A plain Add(".") would stage them. The .gitignore file itself is staged.

Commit accepts nil options; pass *git.CommitOptions to set the author, amend, or sign. It returns the commit hash.

nil options need a git identity in the environment

With nil, go-git fills the author from git configuration: user.name and user.email across the system, global and repository scopes. Where none is set, as in most containers and CI runners, the commit fails with author field is required. This module supplies no identity of its own. Set one explicitly rather than relying on ambient configuration:

hash, err := r.Commit(ctx, "docs: add readme", &git.CommitOptions{
    Author: &object.Signature{Name: "Release Bot", Email: "bot@example.com", When: time.Now()},
})

Branch and push

if err := r.CreateBranch(ctx, "feature/x"); err != nil {
    return err
}

if err := r.Push(ctx, nil); err != nil {
    return err
}

CreateBranch creates the branch and checks it out. For a branch that already exists it instead checks it out and then pulls, so on a repository with no remote the second call with the same name fails with remote not found where the first succeeded. The pull is skipped for in-memory repositories. To move to an existing branch without any of that, use Checkout.

Push(nil) uses the credentials resolved at construction. Pass *git.PushOptions to override; when opts.Auth or opts.Progress is nil the configured value is filled in for you, so you can set other fields without re-supplying credentials or losing the server's output.

Set Settings.Progress before you push

Push is where the server talks back. Pre-receive hook rejection reasons and the "To create a merge request, visit …" URL arrive on that sideband channel and nowhere else. With Progress nil, a hook-rejected push gives you an error with the explanation stripped out. See diagnostics.

To move between existing refs, use Checkout(plumbing.NewBranchReferenceName("main")) or CheckoutCommit(hash).

Order matters, and mis-ordering is an error not a panic

Every method has a defined unopened state. Call one before Clone/Open*/InitLocal and you get a sentinel back:

_, err := r.Commit(ctx, "too early", nil)
errors.Is(err, repo.ErrNoWorktree) // true, never a panic

Worktree operations return ErrNoWorktree; repository operations return ErrNoRepository. InitLocal on an existing repository returns ErrAlreadyRepository. The full method-by-method table is in the errors reference.