# Git

> The desktop app's git pickers and git bar. A branch and pull request menu, the where menu for a new session, a folder picker for a remote host with clone on send, the checkout's live status with sync, and the repository import, on a GitHost.

`zidane-ui/git` holds the desktop app's git pickers. `BranchMenu` switches between pull requests and branches, and starts a branch or a worktree. `WherePicker` chooses where a new session runs: this computer, a server or a shared machine, in the folder, a worktree or a sandbox. `RemoteFolderPicker` picks a folder on a remote host. `GitBar` shows where the checkout stands under a composer, and `useImportRepo` clones a repository. A `GitHost` provides the data and runs the actions. The previews below run on a fixture host.

## Mount it

```tsx
import { createServerClient, createServerGitHost } from 'zidane-ui/adapters'
import { GitProvider, RemoteFolderPicker } from 'zidane-ui/git'

const client = createServerClient({ url, token })
const git = createServerGitHost({ client })

export function FolderStep({ folder, setFolder }) {
  return (
    <GitProvider host={git}>
      <RemoteFolderPicker hostName="box" folder={folder} onChange={setFolder} />
    </GitProvider>
  )
}
```

`AppHostProvider` (`zidane-ui/app`) puts `host.git` in a `GitProvider` for you. `WherePicker` reads no host: it shows the hosts, the environment kinds and the draft you pass.

## The where menu

`WherePicker` lists the places first: this computer, the saved servers and the machines shared with the draft's host. Then it lists the isolation kinds of the picked place. A kind that takes a branch (a worktree) adds a branch field. An empty field uses the suggested name. `WhereMenu` is the same menu without the pill, for a surface of your own.

The draft keeps only an explicit pick. `resolveDraftEnvironment(descriptors, draft)` turns it into the environment a session is created with: the pick while it is usable, else the default kind, else Local. `withMachines(descriptors)` orders a host's kinds before its machines' kinds and marks an offline machine's kinds unavailable. `GitHost.environments` lists the kinds when the host can. `useEnvironmentChoices(cwd, hostId)` reads them for a draft, `withMachines` applied, and reads them again on `GitHost.subscribeEnvironments` (the desktop app: a settings write or the window's focus; zidane-server: `settings:changed` and a reconnect).

## The folder picker

`RemoteFolderPicker` is a pill and its menu. `RemoteFolderBrowser` is the menu alone, for a surface of your own. The menu lists the roots, the folder's path (each step opens it), its folders and `..`, then Use. Type in its field to filter the folder it shows. Enter opens the one match. A text that starts with `/` is a path, and Enter opens it. When nothing matches, Enter searches the folders below by name (`GitHost.searchFolders`). New folder creates one in the folder shown (`GitHost.createFolder`) while the listing allows it (`listing.allows.createFolders`). A shared machine (`machineId`) gets the filter and the path only. A new session's composer on a server shows `ServerScopeToggle` (`zidane-ui/chat/composer`) instead, the project menu of this computer over the server's folders, with this browser behind "Select directory…".

## Clone on send

A new session on a server can start from a project on this computer. While no folder on the server is picked, `useDraftTarget` reads the project's `origin` (`GitHost.originUrl`) and lists the server's first root. `resolveRemoteClone(origin, root)` (`zidane-ui/git/headless`) says what a send would clone:

| `state` | When | The pill shows |
| - | - | - |
| `clone` | No folder of the repository's name in the first root | `webheim · will clone` |
| `exists` | A folder of that name is there | `webheim` |
| `off` | The server takes no clones (`listing.allows.clone` is `false`) | `Default folder`; the clone row shows `cloning off`, greyed out |

`ServerScopeToggle` shows it: pass `draft.remoteClone` and `draft.cloning` as `clone` and `cloning`. The clone is the menu's first row, picked while no folder is. A listing without `allows` gets no clone row. `createServerGitHost` and the desktop app fill `allows` from the hello's `clone` and `createFolders` flags, or, on an older server that sends neither, from the methods its hello lists; `allows.searchFolders` says whether the browser offers its search. On send, `useSendPrompt` clones first (`GitHost.cloneOnServer`) into `<first root>/<repository name>`, then creates the session there. A folder that already holds the same repository is used as it is. With cloning off, nothing is cloned and the session starts in the first root. When the clone fails, no session is made: the error shows on the draft and the composer keeps the text.

## The branch menu

`BranchMenu` opens from a control you pass as `anchorRef`. Pull requests waiting on your review come first, then yours, then the others, then the local branches. Each section shows a few rows until you expand it or type. Typing filters the loaded rows at once and asks the host to search every open pull request. A branch with an open pull request shows once, under it. A typed name that matches nothing can start a branch, or a worktree when you pass `worktree`.

A branch that another worktree has checked out is refused. The host names the worktree and the chats there. `resolveChat` picks the chat to offer and `onOpenChat` opens it.

## The git bar

`GitBar` is one quiet line under a composer: the branch (it opens `BranchMenu`, or the switcher you pass as `BranchMenu`), the commits to push and pull (`SyncChip`: a click syncs, or publishes a branch with no remote copy, after a confirmation `confirmSync` can turn off), the branch's open pull request, and the uncommitted changes (`onShowChanges`). In a narrow composer the extras step aside, the pull request first. It slides away for a chat without a folder and outside a git checkout, and holds its place, faded, while a folder it has not seen is read.

`useGitBar(cwd, sessionId)` is its data: `status` refreshed when the refs move (`subscribeRefs`), when the session's work lands (`subscribeActivity`), on focus and on a slow timer while the window has focus; `branchPullRequest` followed per branch. Facts are kept per folder, so another chat in the same folder shows them at once. `syncAction`, `syncSummary` and `syncTooltip` (`zidane-ui/git/headless`) are the chip's wording.

```tsx
<GitBar cwd={folder} sessionId={id} running={running} onShowChanges={() => showDiffs()} />
```

## Import a repository

`useImportRepo()` opens `ImportRepoDialog` on the modal layer and resolves with the cloned folder, or `null`. The preview under the field is the host's own parse (`clone.parse`), so it is what the host will clone. Once the field names a GitHub repository and holds still, a card shows what it is (`clone.info`): its preview image, the owner's avatar, stars, language, the branch a clone checks out and the description, as the skill browser shows a source. `useImportRepo({ hostId })` clones on that saved server instead (`cloneOnServer`, into its first root), with the same card. The progress comes from `clone.onProgress`; Stop or Esc calls `clone.cancel`. `pickFolder` adds Change… for the parent folder. Without `GitHost.clone` it opens nothing: hide the control that calls it.

## Pick a host

| Host | From | Offers |
| - | - | - |
| `createServerGitHost({ client })` | `zidane-ui/adapters` | Remote folders (`fs.listDirs`, `machines.listDirs`), environment kinds (`environments.list`), worktree moves (`sessions.moveEnvironment`), and the checkout in a folder of the server's workspace roots: branches, pull requests, status and the branch's pull request (`git.branches`, `git.pullRequests`, `git.status`, `git.branchPullRequest`), switching, new branches, sync and the repository import (`git.checkout`, `git.createBranch`, `git.sync`, `git.clone` with its progress and `git.cloneCancel`). It also creates and searches the server's folders (`fs.mkdir`, `fs.searchDirs`), and fills `listing.allows` from the hello's `createFolders` and `clone`. Cloning, new folders and search need a `chat` token, the other writes an `admin` one. It leaves out `originUrl` and `cloneOnServer`: a browser has no project of its own to clone. `subscribeRefs` follows the server's watch of the checkout (`git.watch`, `git:refs`, taken again on each new connection). Each member is offered when the connection may call its method (build the host after the handshake). |
| `createFixtureGitHost(options)` | `zidane-ui/git/testing` | Everything, in memory, for demos and tests |
| Your own | `zidane-ui/git/headless` | Any subset of the members below |

| Member | Gives |
| - | - |
| `branches(cwd)`, `checkout(cwd, target, sessionId)` | The branch menu. Without both, it renders nothing. |
| `pullRequests({ cwd, search, fresh })` | The pull request sections. `{ kind: 'unavailable' }` shows a note; `{ kind: 'rate-limited', retryAt }` says when it resumes. |
| `createBranch(cwd, name, sessionId)` | "Start branch" for a typed name |
| `startWorktree(sessionId, branch)` | "Start a worktree" and "Start as worktree" |
| `subscribeRefs(cwd, onChange)` | An open menu refreshes when the refs move |
| `listFolders({ hostId, machineId, path })` | The remote folder picker. `listing.allows` says whether New folder and clone on send apply there |
| `createFolder({ hostId, parent, name })` | New folder in the remote folder picker |
| `searchFolders({ hostId, query, path })` | The remote folder picker's search of the folders below |
| `originUrl(cwd)` | Clone on send: the `origin` of the local project |
| `cloneOnServer(hostId, input, parent)` | Clone on send: the clone on the server |
| `environments({ cwd, hostId })` | The kinds the where menu shows |
| `subscribeEnvironments({ cwd, hostId }, onChange)` | The kinds are read again when they may have changed |
| `status(cwd)` | Where the checkout stands (branch, ahead and behind, changes) |
| `pickFolder()` | A native folder dialog (the desktop app); Change… in the import dialog |
| `subscribeActivity(sessionId, onChange)` | The git bar refreshes when the session's tool calls and terminal commands finish |
| `branchPullRequest(cwd, branch, fresh)` | The git bar's pull request |
| `sync(cwd, sessionId)` | The git bar's Sync and Publish. A refusal comes back as `{ ok: false, error, conflicts? }` |
| `clone` (`parse`, `defaultParent`, `info`, `clone`, `onProgress`, `cancel`) | The repository import, and its card (`info`) |

Errors come back as values: `{ code: 'failed', message }` shows the host's words, `{ code: 'unavailable' }` the `gitUnavailable` label.

## Test your host

```ts
import { describeGitHostContract } from 'zidane-ui/git/testing/contract'

describeGitHostContract('my host', backend => makeHarness(backend), ['listFolders', 'environments'])
```

| Entry point | Holds |
| - | - |
| `zidane-ui/git` | `GitProvider`, `useGitHost`, `useGitLabels`, `BranchMenu`, `WherePicker`, `WhereMenu`, `RemoteFolderPicker`, `RemoteFolderBrowser`, `GitBar`, `SyncChip`, `useGitBar`, `ImportRepoDialog`, `useImportRepo`, `useEnvironmentChoices` |
| `zidane-ui/git/headless` | `GitHost`, the branch menu's model (`switcherSections`, `switcherCreates`, `prMatches`, `switcherCacheOf`), the draft model (`resolveDraftEnvironment`, `resolveDraftDescriptor`, `withMachines`, `environmentKindIcon`), clone on send (`resolveRemoteClone`), the sync chip's wording (`syncAction`, `syncSummary`, `syncTooltip`), the labels |
| `zidane-ui/git/testing` | `createFixtureGitHost`, demo branches, pull requests, kinds, folders, origins, the branches' pull requests and a repository to import |
| `zidane-ui/git/testing/contract` | `describeGitHostContract` |
