DocsGit

Git

sandbox.git runs the git binary inside a sandbox and parses what it returns.

Every method is a thin wrapper over commands.run. It builds a git command line and runs it with GIT_TERMINAL_PROMPT=0, so a credential prompt fails instead of hanging. status(), branches(), getConfig() and remoteGet() return parsed values; the rest return the command result. Git is installed in the default base template, alongside gh. Every example reads IMPELLO_API_KEY from the environment; get a key at dashboard.impello.ai/keys.

Clone a repository

clone() takes a URL and an optional destination. Without path, git creates a directory named after the repository inside the command's working directory. Pass an absolute path and reuse it in every later repository call — add(), commit(), push(), pull(), status(), branches(), reset(), restore() and the branch methods all take the repository path first. setConfig(), getConfig(), configureUser() and dangerouslyAuthenticate() are not repository calls: they take a key or an identity first, and the config pair takes path only as an option, for the local scope.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')
const repo = '/home/user/hello'

await sandbox.git.clone('https://github.com/octocat/Hello-World', {
  path: repo,
  branch: 'master',
  depth: 1,
})

const result = await sandbox.commands.run(`ls ${repo}`)
console.log(result.stdout)

await sandbox.kill()

branch runs git clone --branch <name> --single-branch; depth adds --depth <n> for a shallow clone.

Clone options

TypeScriptPythonDefaultWhat it does
pathpaththe repository nameDestination directory
branchbranchthe remote defaultBranch to check out, as a single-branch clone
depthdepthfull historyShallow clone depth
usernameusernamenoneUsername for HTTP(S) authentication
passwordpasswordnonePassword or token for HTTP(S) authentication
dangerouslyStoreCredentialsdangerously_store_credentialsfalseLeave the credentials in the clone's remote URL

Every git method also accepts the envs, user, cwd, timeoutMs and requestTimeoutMs options of commands.run (envs, user, cwd, timeout and request_timeout in Python, with the timeouts in seconds). In TypeScript a git command inherits the 60-second default of commands.run, so raise timeoutMs for a large clone. In Python a git method sends no timeout at all — unlike commands.run, which defaults to 60 seconds — so a long clone has no SDK-side deadline; it is bounded only by the sandbox timeout (5 minutes by default — see Timeouts). Pass timeout if you want a shorter one. Python accepts request_timeout for symmetry, but the command runner does not apply it; only timeout bounds a git command.

Authenticate

Two ways to reach a private repository over HTTPS. Both take a username and a password or token, and only http(s) URLs accept them.

Per call

Pass username and password to clone(), push() or pull(). The credentials go into the remote URL for that one command and are removed afterwards. clone() rewrites origin to the clean URL once the clone finishes. push() and pull() restore the original remote URL when they return, whether or not the command succeeded. Nothing is stored unless you set dangerouslyStoreCredentials: true on clone().

The token is on the command line while git runs, so anything else in the sandbox can read it from the process list (sandbox.commands.list() returns each process's arguments); for push and pull it also sits in .git/config until the call returns, so a sandbox killed or paused mid-push keeps it. Use a short-lived token either way.

import { Sandbox } from '@impello/sdk'

const sandbox = await Sandbox.create('base')
const repo = '/home/user/private-repo'

await sandbox.git.clone('https://github.com/your-org/private-repo', {
  path: repo,
  username: 'x-access-token',
  password: process.env.GITHUB_TOKEN!,
})

A password without a username raises InvalidArgumentError (InvalidArgumentException in Python) before anything runs. When the credentials are stripped after the clone, a destination is required: either pass path, or use a URL the SDK can derive a directory name from.

For the whole sandbox

dangerouslyAuthenticate() sets credential.helper to store in the global git config and approves one credential for a host, so every later git command in the sandbox can use it. The default host is github.com and the default protocol is https.

await sandbox.git.dangerouslyAuthenticate({
  username: 'x-access-token',
  password: process.env.GITHUB_TOKEN!,
})

await sandbox.git.clone('https://github.com/your-org/private-repo', {
  path: repo,
})

Commit and push

The snippets below continue from the private repository cloned above, whose default branch is main.

add() stages with git add -A when you pass no files; all: false makes that default git add . instead. Passing files runs git add -- <files> and ignores all. Every command runs as git -C <repository path>, so the . is the repository root. commit() needs an identity: set one for the sandbox with configureUser(), or pass authorName and authorEmail (author_name, author_email) on the commit itself, which applies them to that commit only. allowEmpty (allow_empty) permits an empty commit. push() adds --set-upstream when a remote is resolved — when you pass remote, or pass credentials so the SDK resolves the repository's single remote.

await sandbox.git.configureUser('Agent', 'agent@example.com')

await sandbox.files.write(`${repo}/notes.md`, '# Notes\n')
await sandbox.git.add(repo, { files: ['notes.md'] })
await sandbox.git.commit(repo, 'Add notes')

await sandbox.git.push(repo, {
  remote: 'origin',
  branch: 'main',
  username: 'x-access-token',
  password: process.env.GITHUB_TOKEN!,
})

files.write() comes from the filesystem module. pull() takes the same remote, branch, username and password as push(); with neither remote nor branch it needs an upstream to already be set.

Push options

TypeScriptPythonDefaultWhat it does
remoteremotethe current upstreamRemote to push to
branchbranchthe current branchBranch to push
setUpstreamset_upstreamtrueAdds --set-upstream, but only when a remote is resolved
usernameusernamenoneUsername for HTTP(S) authentication
passwordpasswordnonePassword or token for HTTP(S) authentication

When username and password are given, remote is required unless the repository has exactly one remote.

Undo

reset() takes mode (soft, mixed, hard, merge or keep), target (a commit, branch or ref) and paths. restore() takes paths and restores the working tree by default; set staged: true to unstage instead, and source to restore from a given ref. Pass worktree: true alongside staged: true (worktree=True, staged=True) to do both; setting both to false raises InvalidArgumentError.

await sandbox.git.restore(repo, { paths: ['notes.md'], staged: true })
await sandbox.git.reset(repo, { mode: 'hard', target: 'HEAD' })

Branches

branches() lists local branches and names the current one. createBranch() runs git checkout -b, checkoutBranch() runs git checkout, and deleteBranch() runs git branch -d, or -D with force: true.

await sandbox.git.createBranch(repo, 'feature/notes')

const list = await sandbox.git.branches(repo)
console.log(list.branches) // ['feature/notes', 'main']
console.log(list.currentBranch) // 'feature/notes'

await sandbox.git.checkoutBranch(repo, 'main')
await sandbox.git.deleteBranch(repo, 'feature/notes', { force: true })

Status

status() runs git status --porcelain=1 -b and parses it. The fields are camelCase in TypeScript (status.currentBranch, status.isClean) and snake_case in Python (status.current_branch, status.is_clean).

const status = await sandbox.git.status(repo)

console.log(status.currentBranch, status.ahead, status.behind)

if (!status.isClean) {
  for (const file of status.fileStatus) {
    console.log(file.status, file.name, file.staged)
  }
}

Status fields

TypeScriptPythonMeaning
currentBranchcurrent_branchBranch name, if on one
upstreamupstreamTracking branch, if set
ahead, behindahead, behindCommits relative to upstream
detacheddetachedWhether HEAD is detached
fileStatusfile_statusOne entry per changed file
isCleanis_cleanNo tracked or untracked changes
hasChangeshas_changesAny tracked or untracked change
hasStagedhas_stagedAt least one staged change
hasUntrackedhas_untrackedAt least one untracked file
hasConflictshas_conflictsAt least one merge conflict
totalCount, stagedCount, unstagedCount, untrackedCount, conflictCounttotal_count, staged_count, unstaged_count, untracked_count, conflict_countCounts of the above

Each file entry has name, status, staged, the raw porcelain characters indexStatus and workingTreeStatus (index_status, working_tree_status), and renamedFrom (renamed_from) when the file was renamed. status is one of conflict, renamed, copied, deleted, added, modified, typechange, untracked or unknown.

Configure git

setConfig() and getConfig() read and write git config. The scope defaults to global; use local with a path to configure one repository, or system. getConfig() returns undefined (None in Python) when the key is not set in that scope. configureUser() is setConfig() for user.name and user.email together.

await sandbox.git.setConfig('pull.rebase', 'true')
await sandbox.git.setConfig('core.autocrlf', 'input', {
  scope: 'local',
  path: repo,
})

const rebase = await sandbox.git.getConfig('pull.rebase')
console.log(rebase) // 'true'

Start a repository without cloning

init() creates a repository at a path, with initialBranch (initial_branch) and bare as options. remoteAdd() adds a remote; set overwrite: true to replace the URL of one that already exists, and fetch: true to fetch it straight away. remoteGet() returns a remote's URL, or undefined (None) when there is no such remote.

const fresh = '/home/user/fresh'

await sandbox.git.init(fresh, { initialBranch: 'main' })
await sandbox.git.remoteAdd(fresh, 'origin', 'https://github.com/your-org/fresh')

console.log(await sandbox.git.remoteGet(fresh, 'origin'))

Errors

  • GitAuthError (GitAuthException): clone() failed, or push()/pull() failed while relying on an ambient credential — a credential helper, or one already stored in the remote URL. With GIT_TERMINAL_PROMPT=0 a private repository reached with no credentials fails this way at once instead of waiting on a prompt.
  • GitUpstreamError (GitUpstreamException): push() or pull() has no upstream to work from. For push(), pass remote (and branch) so the default setUpstream records the tracking branch. For pull(), pass remote and branch explicitly — pull() has no setUpstream option and only checks for an upstream when you pass neither. push() reports a missing upstream only on the credential-free path.
  • InvalidArgumentError (InvalidArgumentException): an argument the SDK rejects itself. Five cases raise it:
    • A password with no username.
    • A non-HTTP(S) URL with credentials.
    • An unknown reset mode.
    • restore() with no paths, or with staged and worktree both false.
    • username and password on a repository that does not have exactly one remote, with no remote to pick one.
  • CommandExitError (CommandExitException): any other non-zero exit from git. The error carries git's stderr.

GitAuthError wraps clone() and the credential-free path of push() and pull(). When you pass username and password to push() or pull(), git's non-zero exit reaches you as CommandExitError (CommandExitException) carrying git's stderr; a rejected token is not converted to GitAuthError.

See Errors for the full hierarchy.

Async Python

AsyncSandbox has the same git module, and every method is awaited.

import asyncio
from impello import AsyncSandbox


async def main():
    sandbox = await AsyncSandbox.create("base")
    repo = "/home/user/hello"

    await sandbox.git.clone(
        "https://github.com/octocat/Hello-World", path=repo, depth=1
    )
    status = await sandbox.git.status(repo)
    print(status.current_branch, status.is_clean)

    await sandbox.kill()


asyncio.run(main())

Next: Network and public URLs.