# Git integration

> Git repository detection, detached HEAD behavior, marker exclusion, unsafe source states, and preserved index/working-tree semantics.

- Repository: anomalyco/rift
- GitHub: https://github.com/anomalyco/rift
- Human docs: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662
- Complete Markdown: https://www.grok-wiki.com/public/docs/anomalyco-rift-ea3fd5dbf662/llms-full.txt

## Source Files

- `specs.md`
- `crates/core/src/git.rs`
- `crates/core/src/lib.rs`
- `README.md`

---

---
title: "Git integration"
description: "Git repository detection, detached HEAD behavior, marker exclusion, unsafe source states, and preserved index/working-tree semantics."
---

Rift treats Git as an optional integration layer in `crates/core/src/git.rs`. Both `Manager::init` and `Manager::create_with_options` call `check_source` before filesystem work; when the source is a repository, Rift hides the `.rift` marker from local Git status and detaches `HEAD` in created workspaces while preserving copied index and working-tree state. Rift does not create branches, commit changes, or replace normal Git commands.

## Role in the workspace model

Git integration sits beside the core Rift model defined by `.rift` markers, the SQLite registry, and copy-on-write strategies. A directory without `.git` is classified as `Source::PlainDirectory` and skips all Git preparation. A directory with a `.git` directory is `Source::Repository` and receives marker exclusion and, on `create`, detached-HEAD preparation in the destination.

```mermaid
flowchart TB
  subgraph cli [CLI layer]
    InitCmd["rift init"]
    CreateCmd["rift create"]
    GitRoot["git_root() ancestor walk"]
  end

  subgraph core [crates/core]
    Manager["Manager"]
    GitMod["git.rs"]
    Strategy["Strategy::copy_directory"]
    Registry["Registry"]
    Marker["marker.rs"]
  end

  InitCmd --> GitRoot
  GitRoot --> Manager
  CreateCmd --> Manager
  Manager --> GitMod
  GitMod -->|"check_source"| Manager
  Manager --> Strategy
  Manager --> Marker
  Manager --> Registry
  GitMod -->|"hide_marker / detach_destination"| Manager
```

## Repository detection

`check_source(path)` classifies the workspace root:

| Condition | Classification | Result |
| --- | --- | --- |
| No `.git` entry | `PlainDirectory` | Git preparation skipped |
| `.git` exists and is a directory | `Repository` | Unsafe-state checks run; Git preparation enabled |
| `.git` exists but is not a directory (linked worktree file) | — | `Error::UnsafeGit` |

Detection runs on the exact path passed to core `init` or resolved as the copy source for `create`. It does not walk parent directories.

<Info>
Core `init` initializes exactly `at`. Git-root selection is CLI-only: when `rift init` runs without `--here` and no managed ancestor exists, the CLI walks upward for the nearest `.git` entry and passes that path to core `init`.
</Info>

### CLI Git-root selection

`init_target` in `crates/cli/src/main.rs` resolves the initialization path:

1. If `--here` is set, use the canonicalized requested directory.
2. Else if a managed workspace exists above the request, use that Rift root.
3. Else if a registry record exists but the marker is missing, restore at that path.
4. Else walk ancestors for the first directory where `.git` exists; if none is found, use the requested directory.

`git_root` only tests `.git` existence. Linked worktrees (`.git` as a file) may be selected by the CLI but fail during core `init` with `unsafe_git`.

## Marker exclusion

Rift keeps the `.rift` ULID marker out of `git status` by appending `/.rift` to `.git/info/exclude`:

- Creates `.git/info` if needed.
- Appends `/.rift` on its own line when not already present.
- Preserves existing exclude content and adds a newline separator when required.
- Is idempotent: repeated calls do not duplicate the entry.

| Operation | Where `hide_marker` runs |
| --- | --- |
| `init` on a new or re-initialized Git repository | Source workspace |
| `init` on an already-registered Git repository | Source workspace (after marker restore or verification) |
| `create` from a Git repository | Destination workspace, then source workspace |

The destination receives a new `.rift` marker after the copy. Exclusion ensures `git status --porcelain -- .rift` is empty in the child workspace.

<Note>
Marker exclusion is local to each repository via `info/exclude`. It does not modify `.gitignore` or tracked files.
</Note>

## Detached HEAD in created workspaces

After a successful copy and before registry insertion, `detach_destination` runs on the destination when the source is a repository.

```mermaid
sequenceDiagram
  participant M as Manager::create
  participant S as Strategy
  participant G as git.rs
  participant FS as .git/HEAD

  M->>S: copy_directory(from, destination)
  S-->>M: copied tree incl. .git
  M->>M: marker::write(destination)
  M->>G: hide_marker(destination)
  M->>G: detach_destination(destination)
  alt HEAD resolves to a commit
    G->>FS: write "{commit_oid}\n"
  else unborn branch (no commits)
    G-->>M: no HEAD change
  end
  M->>G: hide_marker(from)
  M->>M: registry.insert_child
```

### Resolution path

On Linux and macOS, Rift first tries `git2` via `resolve_head_commit`, which opens the repository and peels `HEAD` to a commit OID. If libgit2 cannot resolve the layout, Rift falls back to the Git CLI:

```bash
git -C <destination> rev-parse --verify HEAD^{commit}
```

When a commit OID is obtained, Rift writes it directly to `.git/HEAD` as a detached HEAD (a raw OID line, not a symbolic ref). Symbolic tag heads such as `refs/tags/release` are peeled to their underlying commit.

When `rev-parse` fails (typical for repositories with no commits yet), `detach_destination` returns without modifying `HEAD`, leaving the unborn branch state unchanged.

### Verification signals

After `rift create` from a Git repository with commits:

- `git symbolic-ref -q HEAD` fails in the destination (HEAD is detached).
- `.git/HEAD` contains the same commit OID as `git rev-parse --verify HEAD^{commit}` on the source at copy time.
- Staged paths, unstaged modifications, and untracked files present in the copied tree remain in the destination.

## Preserved index and working-tree semantics

Git preparation runs after the filesystem copy. Copy-on-write cloning duplicates the `.git` directory and tracked working-tree content; Rift then adjusts only `HEAD` and `info/exclude`.

Default filtered copy (`CopyMode::Filtered`) omits heavyweight regenerable artifacts (`node_modules`, `target`, virtualenvs, framework caches, `dist`, `build`, `coverage`, and related paths defined in `crates/core/src/filter.rs`). It still preserves:

- Manifests and lockfiles
- Staged changes in the index
- Unstaged modifications
- Untracked files outside the excluded artifact set
- Ignored files outside the excluded artifact set

`--copy-all` / `copyAll: true` copies the full tree, including dependency and build artifacts, before the same Git preparation steps.

Postcreate hooks from `.rift.toml` run after workspace creation, Git preparation, and registry insertion.

## Unsafe source states

Rift refuses `init` and `create` when Git state would make a safe exact copy unclear. `check_source` scans `.git` for these markers:

| Marker | Type |
| --- | --- |
| `MERGE_HEAD` | file |
| `CHERRY_PICK_HEAD` | file |
| `REVERT_HEAD` | file |
| `BISECT_LOG` | file |
| `rebase-merge` | directory |
| `rebase-apply` | directory |
| `index.lock` | file |
| `HEAD.lock` | file |

Additionally, linked Git worktrees (`.git` is a `gitdir:` pointer file, not a directory) are always rejected.

On failure, no destination directory is created and no registry row is inserted. Tests confirm the expected child path does not exist and `list` returns empty.

### Error surfaces

| Surface | Code / variant | Typical message |
| --- | --- | --- |
| Rust core | `Error::UnsafeGit` | `unsafe Git source: …` |
| FFI / JavaScript | `unsafe_git` | Same message via `RiftError`; no `path` field |
| CLI | exit code `1` | Prints the core error string to stderr |

Example messages:

- `unsafe Git source: linked Git worktree sources are not supported`
- `unsafe Git source: Git state in progress: MERGE_HEAD`

<Warning>
Finish or abort in-progress merge, rebase, cherry-pick, revert, and bisect operations, and clear Git lock files before running `rift init` or `rift create`.
</Warning>

## What Rift does not do

Per `specs.md` and the implementation:

- Does not create branches in the destination
- Does not commit staged or unstaged changes
- Does not run `git checkout`, `git switch`, or other branch-management commands
- Does not modify `.gitignore` (only `info/exclude` for `/.rift`)
- Does not validate Git cleanliness beyond the unsafe-state markers above

Normal Git workflows remain the developer's responsibility after workspace creation.

## Operation summary

| Operation | Git checks | Git side effects |
| --- | --- | --- |
| `init` | `check_source(at)` | `hide_marker(at)` when repository |
| `create` | `check_source(from)` before copy | `hide_marker(destination)`, `detach_destination(destination)`, `hide_marker(from)` when repository |
| `remove`, `list`, `ancestors`, `gc` | None | None |

The JavaScript `init` function initializes exactly `at`; Git-root selection and `--here` are CLI-only behaviors documented on the initialize-workspace page.

## Related pages

<CardGroup>
  <Card title="Initialize a workspace" href="/initialize-workspace">
    Git-root vs `--here` selection, marker restoration, and init success signals.
  </Card>
  <Card title="Create workspaces" href="/create-workspaces">
    Filtered vs `--copy-all` copy, Git preparation order, and postcreate hooks.
  </Card>
  <Card title="Workspaces and registry" href="/workspaces-and-registry">
    `.rift` marker identity, upward resolution, and parent-child provenance.
  </Card>
  <Card title="Error codes" href="/error-codes">
    Full `RiftError` catalog including `unsafe_git`.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Unsafe Git state failures and common recovery steps.
  </Card>
</CardGroup>
