Skip to content

Clone and commit

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

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(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("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.

Branch and push

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

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

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("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.