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
| TypeScript | Python | Default | What it does |
|---|---|---|---|
path | path | the repository name | Destination directory |
branch | branch | the remote default | Branch to check out, as a single-branch clone |
depth | depth | full history | Shallow clone depth |
username | username | none | Username for HTTP(S) authentication |
password | password | none | Password or token for HTTP(S) authentication |
dangerouslyStoreCredentials | dangerously_store_credentials | false | Leave 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
| TypeScript | Python | Default | What it does |
|---|---|---|---|
remote | remote | the current upstream | Remote to push to |
branch | branch | the current branch | Branch to push |
setUpstream | set_upstream | true | Adds --set-upstream, but only when a remote is resolved |
username | username | none | Username for HTTP(S) authentication |
password | password | none | Password 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
| TypeScript | Python | Meaning |
|---|---|---|
currentBranch | current_branch | Branch name, if on one |
upstream | upstream | Tracking branch, if set |
ahead, behind | ahead, behind | Commits relative to upstream |
detached | detached | Whether HEAD is detached |
fileStatus | file_status | One entry per changed file |
isClean | is_clean | No tracked or untracked changes |
hasChanges | has_changes | Any tracked or untracked change |
hasStaged | has_staged | At least one staged change |
hasUntracked | has_untracked | At least one untracked file |
hasConflicts | has_conflicts | At least one merge conflict |
totalCount, stagedCount, unstagedCount, untrackedCount, conflictCount | total_count, staged_count, unstaged_count, untracked_count, conflict_count | Counts 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, orpush()/pull()failed while relying on an ambient credential — a credential helper, or one already stored in the remote URL. WithGIT_TERMINAL_PROMPT=0a private repository reached with no credentials fails this way at once instead of waiting on a prompt.GitUpstreamError(GitUpstreamException):push()orpull()has no upstream to work from. Forpush(), passremote(andbranch) so the defaultsetUpstreamrecords the tracking branch. Forpull(), passremoteandbranchexplicitly —pull()has nosetUpstreamoption 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
passwordwith nousername. - A non-HTTP(S) URL with credentials.
- An unknown reset mode.
restore()with no paths, or withstagedandworktreebothfalse.usernameandpasswordon a repository that does not have exactly one remote, with noremoteto pick one.
- A
CommandExitError(CommandExitException): any other non-zero exit from git. The error carries git'sstderr.
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.