# Git

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

Every method is a thin wrapper over
[`commands.run`](/docs/sandbox/commands). 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](https://dashboard.impello.ai/keys).

<Callout kind="warning">
The Python git module is deprecated and will be removed in the next major version of the `impello` package. It works today; for new Python code run git through `sandbox.commands.run("git ...")` instead. The TypeScript module is not deprecated.
</Callout>

## 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.

<CodeTabs>
<Tab label="TypeScript">

```ts
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()
```

</Tab>
<Tab label="Python">

```python
from impello import Sandbox

sandbox = Sandbox.create("base")
repo = "/home/user/hello"

sandbox.git.clone(
    "https://github.com/octocat/Hello-World",
    path=repo,
    branch="master",
    depth=1,
)

result = sandbox.commands.run(f"ls {repo}")
print(result.stdout)

sandbox.kill()
```

</Tab>
</CodeTabs>

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

### Clone options

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>TypeScript</th><th>Python</th><th>Default</th><th>What it does</th></tr>
  </thead>
  <tbody>
    <tr><td>`path`</td><td>`path`</td><td>the repository name</td><td>Destination directory</td></tr>
    <tr><td>`branch`</td><td>`branch`</td><td>the remote default</td><td>Branch to check out, as a single-branch clone</td></tr>
    <tr><td>`depth`</td><td>`depth`</td><td>full history</td><td>Shallow clone depth</td></tr>
    <tr><td>`username`</td><td>`username`</td><td>none</td><td>Username for HTTP(S) authentication</td></tr>
    <tr><td>`password`</td><td>`password`</td><td>none</td><td>Password or token for HTTP(S) authentication</td></tr>
    <tr><td>`dangerouslyStoreCredentials`</td><td>`dangerously_store_credentials`</td><td>`false`</td><td>Leave the credentials in the clone's remote URL</td></tr>
  </tbody>
</table>
</div>

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](/docs/sandbox/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.

<CodeTabs>
<Tab label="TypeScript">

```ts
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!,
})
```

</Tab>
<Tab label="Python">

```python
import os
from impello import Sandbox

sandbox = Sandbox.create("base")
repo = "/home/user/private-repo"

sandbox.git.clone(
    "https://github.com/your-org/private-repo",
    path=repo,
    username="x-access-token",
    password=os.environ["GITHUB_TOKEN"],
)
```

</Tab>
</CodeTabs>

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`.

<CodeTabs>
<Tab label="TypeScript">

```ts
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,
})
```

</Tab>
<Tab label="Python">

```python
sandbox.git.dangerously_authenticate(
    username="x-access-token",
    password=os.environ["GITHUB_TOKEN"],
)

sandbox.git.clone("https://github.com/your-org/private-repo", path=repo)
```

</Tab>
</CodeTabs>

<Callout kind="warning">
The credential is written to the sandbox's git credential store and is readable by anything running in the sandbox, including an agent. Prefer a short-lived token, and prefer the per-call form when a single clone or push is all you need.
</Callout>

## 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.

<CodeTabs>
<Tab label="TypeScript">

```ts
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!,
})
```

</Tab>
<Tab label="Python">

```python
sandbox.git.configure_user("Agent", "agent@example.com")

sandbox.files.write(f"{repo}/notes.md", "# Notes\n")
sandbox.git.add(repo, files=["notes.md"])
sandbox.git.commit(repo, "Add notes")

sandbox.git.push(
    repo,
    remote="origin",
    branch="main",
    username="x-access-token",
    password=os.environ["GITHUB_TOKEN"],
)
```

</Tab>
</CodeTabs>

`files.write()` comes from the [filesystem](/docs/sandbox/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

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>TypeScript</th><th>Python</th><th>Default</th><th>What it does</th></tr>
  </thead>
  <tbody>
    <tr><td>`remote`</td><td>`remote`</td><td>the current upstream</td><td>Remote to push to</td></tr>
    <tr><td>`branch`</td><td>`branch`</td><td>the current branch</td><td>Branch to push</td></tr>
    <tr><td>`setUpstream`</td><td>`set_upstream`</td><td>`true`</td><td>Adds `--set-upstream`, but only when a remote is resolved</td></tr>
    <tr><td>`username`</td><td>`username`</td><td>none</td><td>Username for HTTP(S) authentication</td></tr>
    <tr><td>`password`</td><td>`password`</td><td>none</td><td>Password or token for HTTP(S) authentication</td></tr>
  </tbody>
</table>
</div>

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`.

<CodeTabs>
<Tab label="TypeScript">

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

</Tab>
<Tab label="Python">

```python
sandbox.git.restore(repo, paths=["notes.md"], staged=True)
sandbox.git.reset(repo, mode="hard", target="HEAD")
```

</Tab>
</CodeTabs>

## 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`.

<CodeTabs>
<Tab label="TypeScript">

```ts
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 })
```

</Tab>
<Tab label="Python">

```python
sandbox.git.create_branch(repo, "feature/notes")

branches = sandbox.git.branches(repo)
print(branches.branches)  # ['feature/notes', 'main']
print(branches.current_branch)  # 'feature/notes'

sandbox.git.checkout_branch(repo, "main")
sandbox.git.delete_branch(repo, "feature/notes", force=True)
```

</Tab>
</CodeTabs>

## 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`).

<CodeTabs>
<Tab label="TypeScript">

```ts
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)
  }
}
```

</Tab>
<Tab label="Python">

```python
status = sandbox.git.status(repo)

print(status.current_branch, status.ahead, status.behind)

if not status.is_clean:
    for file in status.file_status:
        print(file.status, file.name, file.staged)
```

</Tab>
</CodeTabs>

### Status fields

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>TypeScript</th><th>Python</th><th>Meaning</th></tr>
  </thead>
  <tbody>
    <tr><td>`currentBranch`</td><td>`current_branch`</td><td>Branch name, if on one</td></tr>
    <tr><td>`upstream`</td><td>`upstream`</td><td>Tracking branch, if set</td></tr>
    <tr><td>`ahead`, `behind`</td><td>`ahead`, `behind`</td><td>Commits relative to upstream</td></tr>
    <tr><td>`detached`</td><td>`detached`</td><td>Whether HEAD is detached</td></tr>
    <tr><td>`fileStatus`</td><td>`file_status`</td><td>One entry per changed file</td></tr>
    <tr><td>`isClean`</td><td>`is_clean`</td><td>No tracked or untracked changes</td></tr>
    <tr><td>`hasChanges`</td><td>`has_changes`</td><td>Any tracked or untracked change</td></tr>
    <tr><td>`hasStaged`</td><td>`has_staged`</td><td>At least one staged change</td></tr>
    <tr><td>`hasUntracked`</td><td>`has_untracked`</td><td>At least one untracked file</td></tr>
    <tr><td>`hasConflicts`</td><td>`has_conflicts`</td><td>At least one merge conflict</td></tr>
    <tr><td>`totalCount`, `stagedCount`, `unstagedCount`, `untrackedCount`, `conflictCount`</td><td>`total_count`, `staged_count`, `unstaged_count`, `untracked_count`, `conflict_count`</td><td>Counts of the above</td></tr>
  </tbody>
</table>
</div>

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.

<CodeTabs>
<Tab label="TypeScript">

```ts
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'
```

</Tab>
<Tab label="Python">

```python
sandbox.git.set_config("pull.rebase", "true")
sandbox.git.set_config("core.autocrlf", "input", scope="local", path=repo)

rebase = sandbox.git.get_config("pull.rebase")
print(rebase)  # 'true'
```

</Tab>
</CodeTabs>

## 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.

<CodeTabs>
<Tab label="TypeScript">

```ts
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'))
```

</Tab>
<Tab label="Python">

```python
fresh = "/home/user/fresh"

sandbox.git.init(fresh, initial_branch="main")
sandbox.git.remote_add(fresh, "origin", "https://github.com/your-org/fresh")

print(sandbox.git.remote_get(fresh, "origin"))
```

</Tab>
</CodeTabs>

## 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](/docs/errors) for the full hierarchy.

## Async Python

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

```python
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](/docs/sandbox/network).
