# GitOne - full documentation > GitOne keeps public code and private files in one working directory and maps every file to exactly one Git repository. When it cannot tell where a file belongs, it stops. This file is the complete GitOne documentation in one place: the instruction block GitOne writes into a project's `AGENTS.md`, the usage guide, the hands-on tutorial and the error reference. It is generated from the same sources https://gitone.io renders, so it says what the repository says. The short index is at https://gitone.io/llms.txt. --- # Instructions for AI agents The canonical block, published by `gitone agents update` into the project `AGENTS.md`. An agent working in a GitOne project needs this and little else. This project uses GitOne: one working directory, several Git repositories, ownership decided by path in `.gitone.yml`. - Use `gitone`, never `git`, for status, staging, committing and pushing. There is no `.git/` in the project root. - `gitone status --json` is the machine-readable project state (schema `version: 1`). Exit code 0 means ordinary changes, 1 means the `issues` array explains why the project is unsafe. - Available: `init`, `repo validate`, `repo list`, `status [--porcelain|--json]`, `add [-A|-u|...|.]`, `unstage [-A|...|.]`, `restore [--staged] ...`, `commit [...] -m `, `commit [...] -F -`, `diff [all|] [--staged]`, `log [all|] [-n ]`, `branch []`, `switch `, `fetch [all|]`, `pull [all|]`, `push [all|] [--yes]`, `show --repository --source -- `, `vscode info --json`, `recover`, `abort`, `doctor`, `agents`, `agents update`, `vscode install [--vsix ] [--force]`. `gitone --help` prints the accepted syntax. Everything else fails with `CLI001`; do not fall back to plain `git` to work around it. - Never create, edit or commit `.gitone/` or `.gitone.local.yml`. Never move a path across repository boundaries to make an error go away. - A new file must be added to the `paths` of exactly one repository in `.gitone.yml` (public) or `.gitone.local.yml` (private) before it can be staged. `PATH001` means unassigned, `PATH002` means assigned twice. - Repository boundaries are build boundaries. Before one file references another - an import, an include, a build input - check whether both paths have the same owner, and check the references you added again before you hand the work back. A reference across repositories works in your working directory and breaks in any checkout that has only one of them, such as CI. GitOne never reads file contents, so it cannot catch this: report it and ask a human rather than moving the path. - `commit` with explicit paths commits the current working-tree version of exactly those managed files, like native `git commit `, and leaves every other staged change staged. A file Git does not know yet has to be staged once before it can be selected. - A path covered by `rules.protected_paths` must never be staged or committed; ownership does not override that hard deny. - Valid patterns are exact paths such as `README.md`, `*` inside one path segment and at most one `**` segment, as in `src/**`, `docs/**/*.md` or `**/*.md`. Duplicate patterns are an error; overlapping ones are not, but a concrete path matching two repositories is `PATH002`. - Non-interactive pushes require `gitone push --yes`; without a terminal a push fails with `PUSH001` instead of asking. Ask a human before pushing. `push: disabled` repositories are never published, and a configured `require_clean_worktree` rule must also pass. - On `LOCK001` wait and retry. On `REC001`, `recover` finishes an interrupted add, unstage, restore or commit and `abort` undoes it. For an interrupted push both commands only report what reached the remotes. Never delete `.gitone/recovery/` by hand. - `gitone doctor` is read-only and safe to run at any time, including while another operation holds the lock. --- # GitOne usage guide Everything documented here behaves as shown: what each command does, what it refuses and why. To run all of it against disposable projects first, follow the [hands-on tutorial](https://gitone.io/docs/tutorial). - [Configuration](#configuration) - [Interactive setup](#interactive-setup) - [Terminal presentation](#terminal-presentation) - [Commands](#commands) - [The normal workflow](#the-normal-workflow) - [Undoing changes](#undoing-changes) - [Machine-readable status](#machine-readable-status) - [Editor integration contract](#editor-integration-contract) - [Cloning a project](#cloning-a-project) - [Migrating an existing repository](#migrating-an-existing-repository) - [Locking, recovery and abort](#locking-recovery-and-abort) - [Reading history](#reading-history) - [Branches](#branches) - [Switching branches](#switching-branches) - [Fetching](#fetching) - [Pulling](#pulling) - [Push safety and partial failures](#push-safety-and-partial-failures) - [Error codes](#error-codes) - [Security boundary](#security-boundary) - [Instructions for AI agents](#instructions-for-ai-agents) ## Configuration A project is the nearest directory above your working directory that contains a `.gitone.yml`. Nested GitOne projects are not supported. | File | Committed | Contains | | --- | --- | --- | | `.gitone.yml` | yes | the public ownership map: everything that is safe for anyone who clones a public repository to read | | `.gitone.local.yml` | never | private repository names, private paths and private remotes | | `.gitone/` | never | the managed Git repositories, the project lock and recovery state | `gitone init` adds `.gitone/` and `.gitone.local.yml` to the project `.gitignore` without touching your existing entries or comments. Both are **reserved paths**: no repository can own, stage or commit them, and `doctor` fails if one of them ever reaches an index or a commit. ### Example `.gitone.yml` - committed, and it explicitly assigns itself and `.gitignore`: ```yaml version: 1 default_branch: main rules: protected_paths: - .env - secrets/** push: require_clean_worktree: true repositories: website: visibility: public remote: git@github.com:example/website.git paths: - .gitignore - .gitone.yml - README.md - src/** ``` `.gitone.local.yml` - never committed, so private names, paths and remotes stay off every public remote: ```yaml version: 1 repositories: notes: visibility: private push: disabled remote: git@internal.example.com:ops/notes.git paths: - notes/** ``` Check it before doing anything else: ```bash gitone repo validate ``` ### Project fields | Field | Required | Meaning | | --- | --- | --- | | `version` | yes | `1`, the initial and only supported schema. | | `default_branch` | yes | The branch `init` creates for every repository. | | `rules.protected_paths` | no | Hard-deny paths that must never be owned, tracked or published. | | `rules.push.require_clean_worktree` | no | When `true`, refuse a push with staged, unstaged or untracked changes. Defaults to `false`. | ### Repository fields | Field | Required | Meaning | | --- | --- | --- | | `visibility` | yes | `public` or `private`. Reported by `status`; it does not by itself grant or deny anything. | | `push` | no | `allowed` by default. `disabled` skips the repository during `gitone push` and refuses it as an explicit push target. | | `paths` | yes | At least one ownership pattern. | | `remote` | no | Shorthand for `remotes.origin`. Defining both with different URLs is an error. | | `remotes` | no | Named remotes. Only `origin` is used by `fetch`, `pull` and `push`. | Repository names match `[a-z][a-z0-9_-]*`; `all` is reserved for command targets. A repository without a remote works for every local command; only pushing it fails. `default_branch` and every key below `remotes` are checked with native Git's own `git check-ref-format`, so a configuration `gitone repo validate` accepts is one the Git commands GitOne runs accept too. Such a name contains no space and none of `~ ^ : ? * [ \`, no `..` and no `@{`, no leading, trailing or doubled `/`, no leading `.` or `-` and no trailing `.` or `.lock`. `default_branch` is additionally never `HEAD`. Names like `bad branch`, `-dash` or `feature.lock` are rejected with `CONFIG003` before any repository is created. ### Path grammar | Pattern | Matches | | --- | --- | | `README.md`, `src/site.css` | that one exact path | | `src/**` | every path below `src/`, at any depth | | `*.md` | every Markdown file in the project root, `.hidden.md` included | | `src/*.min.*.css` | every matching name directly in `src/` | | `docs/**/*.md` | `docs/README.md` and every deeper Markdown file | | `**/*.md` | every Markdown file, including `README.md` in the root | `*` matches zero or more characters except `/`, including a leading `.`, and may appear several times in one segment. `**` matches zero or more complete path segments, has to be a complete segment of its own and may appear at most once in a pattern. Everything else - `?`, `[abc]`, `{a,b}`, negation, regular expressions and Git pathspec syntax - is rejected as invalid configuration. **Duplicate patterns are an error**, including patterns that differ only by case, inside one repository as well as across repositories. Overlapping patterns are not: `docs/**` and `**/*.md` may live in the same repository and still produce one owner, and two repositories may hold patterns that could theoretically meet. There is no priority and no most-specific-match rule, so the moment a **concrete path** matches two repositories the operation stops with `PATH002`. A protected pattern may overlap ownership because its hard deny wins explicitly. Paths are relative to the project root, use `/`, must stay below the root and are matched case-sensitively. Case-insensitive collisions are rejected as well, because macOS and Linux disagree about them. Every relevant file must match exactly **one** repository. Zero matches is `PATH001`, more than one is `PATH002`. This includes ordinary project files: `.gitone.yml`, `.gitignore`, `LICENSE` and `README.md` need explicit ownership like everything else. Only project `.gitignore` files make a path irrelevant; a global Git exclude does not. An ignored directory tree is not walked at all. Already tracked files always need ownership, even if they are ignored later. `protected_paths` uses the same pattern grammar, but is a hard deny rather than ownership. A protected path that is visible, tracked or present in outgoing history produces `PATH003` or `PUSH001`, even when an ownership pattern also matches it. Keep local secret files ignored as usual; the rule is the extra guard that stops them if they become relevant or enter history. `.gitone.yml` and `.gitignore` cannot be protected because every project must own them. ### Merge rules `.gitone.local.yml` merges into `.gitone.yml` by repository name: - a repository only the local file names is added - project `default_branch` and repository `visibility` are replaced - local `protected_paths` extend the committed list; they never remove entries - the push rules can only be tightened: a local `push: disabled` and a local `rules.push.require_clean_worktree: true` take effect, a local `allowed` or `false` never weakens the committed value - `remotes` merge per remote name - `paths` **replaces** the whole public list, it does not extend it - `null` deletes nothing Unknown fields, duplicate YAML keys, several YAML documents in one file and any `version` other than `1` are errors. `default_branch` belongs only at project level. New setup runs write this version 1 format. ## Interactive setup `gitone setup` is the guided entry point. It asks for the configuration a project still needs and then runs the initialization or migration the current directory requires: ```bash gitone setup ``` It is interactive only: it takes no flags, and without a terminal on standard input it fails with `SETUP001` and changes nothing. In a project that has no configuration yet, the first question is the [setup mode](#setup-modes). On a normal terminal, choices and confirmations use the arrow keys and Enter, and the owned paths are chosen in a [navigable browser](#choosing-the-owned-paths). Text fields keep their current value when Enter is pressed, and setup shows a live spinner while it initializes, migrates or installs the optional VS Code extension. Without a suitable terminal, or with `NO_COLOR`, setup asks the same questions as stable lines that name their choices and defaults, including the numbered owned-path list. ### Setup modes A project without configuration is asked first how it is set up: | Mode | What it does | | --- | --- | | `Wizard` | The guided questions below, followed by the initialization or migration the directory needs. It is preselected, so Enter chooses it on both terminal styles. | | `Template` | Writes `.gitone.yml` and `.gitone.local.yml` as commented English templates you edit yourself, and stops. | A directory that already has a `.gitone.yml` is never asked: setup validates the existing configuration and continues with it unchanged. ### Template mode Template mode is the two-step alternative for anyone who wants to read and edit every supported setting instead of answering questions. It asks about the [agent instructions](#instructions-for-ai-agents) first, then names what it would write and asks one question with default `no`: - `.gitone.yml` is created - `.gitone.local.yml` is created - `.gitignore` is created or updated with the GitOne entries - `AGENTS.md` is created or updated with the instructions, if you asked for it - no repository is initialized or migrated Cancelling writes nothing. A confirmed run writes both files under the same rules as the wizard: the ignore entries exist before `.gitone.local.yml` does, `.gitone.local.yml` gets owner-only permissions, `.gitone.yml` gets the ordinary `0644`, and an existing configuration file is never replaced. `.gitone.yml` holds an active `version`, `default_branch` and exactly one repository named `` with its `visibility` and the two project files it has to own. That name is invalid on purpose, so an untouched template can never initialize a plausible example repository - `gitone repo validate` refuses it until you replace it. Every optional project and repository field is there as a commented example: protected paths, the clean-worktree push rule, `push`, `remote`, named `remotes`, exact paths and wildcard patterns. `.gitone.local.yml` holds an active `version: 1`, one fully commented private repository and the merge rules in comments. If you asked for the agent instructions, the placeholder repository also owns `AGENTS.md`, with a comment saying that instructions which must stay private belong to a repository in `.gitone.local.yml` instead. Template mode inventories none of your project files. It only checks the root `AGENTS.md` for the optional instruction step, creates no `.gitone/`, and runs neither `init` nor `migrate` - not even when the directory already contains a root `.git/`. It ends with the two commands to run once the files are edited: ```bash gitone repo validate gitone setup ``` That second run takes the ordinary existing-configuration path and initializes or migrates the project. ### The safety preflight Before the wizard asks for the default branch or a repository, it inventories the project once and refuses the problems that no answer could fix, so they never cost you a full run: - a `PATH003` path the inventory decides on its own: an unsupported symbolic link, a link chain, a link target outside the project, a path name GitOne cannot manage, or a nested Git repository - state below `.gitone/` that no operation explains: an unknown entry, or repository state left over from a configuration that no longer exists Nothing is written and no default-branch or repository question is asked. Ownership-dependent safety is not guessed here - it needs the finished configuration and is checked [before the write](#correcting-the-owned-paths). Template mode has no preflight: it inventories nothing, exactly as described above. ### What setup asks The wizard asks once for the shared default branch and then for one or more repositories: | Question | Meaning | | --- | --- | | Default branch | The branch every repository uses. Enter accepts `main`. A name native Git rejects, such as `bad branch` or `-dash`, is asked again. | | Repository name | Matches `[a-z][a-z0-9_-]*`, is not `all` and is used only once. | | Visibility | Arrow-key choice between `public` and `private`; `public` starts selected. Enter accepts `public`. | | Owned paths | The paths this project has, browsed per directory on a normal terminal and listed flat on a plain one, plus `Custom pattern...`; see [below](#choosing-the-owned-paths). At least one is required. | | Origin remote URL | Optional. Empty keeps the repository local. The remote is always named `origin`. | | Store in | `shared` writes the repository to `.gitone.yml`, `local` writes it to `.gitone.local.yml`. | | Owner | Which repository owns `.gitone.yml` and `.gitignore`. Only asked when the answer is not already clear. | | Agent instructions | Whether the [managed block](#instructions-for-ai-agents) is added to `AGENTS.md`. Default `no`; a stale marked block is offered as an update with default `yes`. Asked last, before the summary. | | VS Code extension | After project setup succeeds, offered with default `no` only when `code` is available and `devidevio.gitone` is not installed. | Setup reads your files for one purpose only: to offer the owned paths below. Repositories, names, visibility, remotes and the file a repository is stored in are never derived from the working tree, and no ownership is ever assigned for you. Project setup performs no network operation - a remote URL is only stored. If you separately answer yes to the final extension question, VS Code's `code` CLI may contact the Marketplace. Each repository is written **completely** to the file you selected, so the two files never override each other. `.gitone.local.yml` is created only when you chose it, with owner-only permissions, and the `.gitignore` entries exist before it does, so private configuration is never visible to Git. ### Choosing the owned paths The owned-path question is the one place setup looks at your files. It inventories the project exactly as the ownership validation does, so ignored paths, GitOne's own state and the `.gitone.yml` and `.gitignore` files setup assigns itself never appear. What is left is offered as: - every relevant file as its exact path; - every relevant directory as `directory/**`, which covers everything inside it, accepted links included; - every accepted [symbolic link](#security-boundary) as its exact path, a link to a directory included: setup offers the link itself and never looks through it. Hidden entries such as `.github/` are offered like any other. Both terminal styles produce the same patterns for the same answer, but they ask differently. A normal terminal browses the inventory one directory at a time. It shows the immediate entries of the current directory, directories before files and both alphabetically, with the breadcrumb of where you are and the number of patterns you have selected: ``` ┃ Owned paths ┃ /docs/ 2 selected ┃ > [~] Select this directory: docs/** 2 selected, 1 unavailable ┃ [~] docs/nested/** 1 selected, 1 unavailable ┃ [x] docs/guide.md ┃ [ ] docs/shared ┃ Custom patterns ┃ [x] docs/**/*.md ┃ Custom pattern... ↑ up • ↓ down • →/l open • ←/h parent • x toggle • / filter • ctrl+a select all • enter submit ``` | Key | What it does | | --- | --- | | `↑`/`↓`, `k`/`j` | move the cursor | | `→`/`l` | open the directory under the cursor | | `←`/`h` | return to the parent directory; nothing at the project root | | `Space`/`x` | select or release the path under the cursor | | `Enter` | submit the complete answer | | `Shift+Tab` | go back to the previous question | | `/` | filter the current directory only; `Esc` sets it, `Esc` again clears it | | `Ctrl+A` | select the whole current directory, or every selectable entry of it | Every non-root directory offers `Select this directory: docs/**` as its first row, and the same pattern can be selected from the parent view. The project root has no such row: setup never offers or infers a `**` catch-all. The checkboxes read `[x]` for a path this repository owns, `[~]` for a directory with owned descendants, `[!]` for a directory that contains a path another repository already owns, and `[ ]` for a free path. A directory row names how many of its concrete paths are selected and how many are unavailable. The answer stays canonical. Selecting a whole directory removes the exact-file and descendant-directory patterns setup generated below it; inside a selected directory every entry is shown dimmed as `covered by docs/**` and cannot be changed until that first row releases the pattern. Patterns you typed yourself are never removed by a directory selection, however redundant they become; a path one of them owns is shown as `covered by a custom pattern`. A filter narrows the current directory only. While it is applied the `Select this directory` row and the custom-pattern section are hidden, so `Ctrl+A` can never reach a pattern wider than the visible matches, and changing directory clears it. A plain terminal keeps the stable numbered list of the top-level patterns, answered with comma-separated numbers. It has no navigation, no filter and no partial state; anything deeper is entered as a custom pattern. The same answer still produces the same ownership patterns in both styles. Both styles end with `Custom pattern...`, which continues into text input for one or more [patterns](#path-grammar) of your own, ended by an empty field. In the browser the input opens immediately and returns you to the same directory with the cursor where it was. Patterns you typed stay listed in a `Custom patterns` section, where you can release them again. Every repository needs at least one offered or one custom pattern. A project with no relevant file is offered only `Custom pattern...`; setup never falls back to `**` or another catch-all. Paths an earlier repository already owns stay visible instead of hiding the directory that contains them. They are shown in the error color with the owner named, such as `owned by backend`, their directories stay open to navigation and count them, and a whole-directory pattern that would overlap that ownership cannot be selected. Each answer is checked while you give it: | Situation | What happens | | --- | --- | | an offered path an earlier repository already owns | shown as unavailable in the browser, not offered in the numbered list | | a custom pattern another repository already configured | rejected, enter another | | a custom pattern overlapping another repository on a real file | rejected, enter another | | a custom pattern that matches nothing today | warned; keep it for later files or edit it | | two patterns of the same repository overlapping | allowed, exactly as in the configuration file | ### Correcting the owned paths Before anything is written, setup validates the complete generated configuration against the working tree. When the only problems are unassigned (`PATH001`) or ambiguous (`PATH002`) paths, setup prints them and corrects them without asking anything else again. Unassigned paths are assigned directly. Setup asks, once per path and in alphabetical path order, which existing repository owns it and lists the repositories in the order you entered them; with a single repository it assigns every path to that one without asking. Each answer adds exactly the reported path - never a parent directory, glob or catch-all derived from it. Setup then validates the complete configuration again. Ambiguous paths reopen the owned-path browser of the repository you name, with its current patterns selected. A selected pattern that now overlaps another repository stays selected so you can release it, is marked as conflicting, and blocks the answer until it is gone. When one round reports both kinds, the unassigned paths are assigned first and only the remaining ambiguity reaches the browser. The default branch and every name, visibility, remote, storage location and other repository's ownership stay exactly as entered, and the project files stay assigned exactly once, however often you correct. A project file claimed by a repository stored only in `.gitone.local.yml` reopens exactly two questions of that repository: its owned paths, with the current patterns selected, and its `Store in` answer, with the current one as the default. Its name, visibility and remote, every other repository and the default branch stay unchanged. You end the correction either by releasing the claim, so a repository in `.gitone.yml` owns the project files, or by storing this repository in `.gitone.yml`. Setup never moves a repository or drops a pattern for you, so the question repeats until one of the two is true or you cancel. Every other problem ends setup with its concrete error and changes nothing: a `PATH003` issue that ownership decides, such as a protected path or a link whose target belongs to another repository. None of those can be fixed by choosing different owned paths. ### The project files `.gitone.yml` and `.gitignore` need explicit ownership like every other project file, but setup creates them itself, so you do not enter them in the owned paths. After the repository questions, setup assigns both: | Situation | What happens | | --- | --- | | you entered neither file, one repository is in `.gitone.yml` | that repository owns both | | you entered neither file, several are in `.gitone.yml` | setup asks which one owns both; there is no default | | you entered neither file, none is in `.gitone.yml` | setup asks which repository is moved from `.gitone.local.yml` to `.gitone.yml` and owns both; every other answer you gave is kept | | you entered one of the files for a repository in `.gitone.yml` | that assignment is kept and the other file is added to it | | you entered both files, for one or two repositories in `.gitone.yml` | both assignments are kept unchanged | Only a repository in `.gitone.yml` can own them, because a clone that has only the committed file must still find them. A repository in `.gitone.local.yml` is therefore never used silently, and claiming a project file for one - for example with a broad pattern such as `**` - reopens that repository's paths and storage before anything is written; see [correcting the owned paths](#correcting-the-owned-paths). The final summary shows the resulting ownership as part of the printed configuration, together with the note that `.gitone.yml` is created and `.gitignore` is created or updated. ### What setup does Before anything is written, setup prints the complete generated configuration and the planned action and asks one question. That single confirmation also authorizes a migration, so no second question follows. The action comes from the state of the current directory: | State | Action | | --- | --- | | no configuration | setup asks for the mode: `Wizard` asks the questions and the project state below decides, `Template` writes the files and stops | | configuration and a root `.git/` | `migrate`, including its warning about a modified working tree | | configuration, no root `.git/`, missing repositories | `init` | | configuration and healthy repositories | reported as done; nothing changes | An existing `.gitone.yml` or `.gitone.local.yml` is never edited or overwritten. Change configuration by editing the file yourself. Setup refuses, with nothing changed: - a directory inside another GitOne project or inside another Git repository, because nested projects are not supported - an invalid existing configuration - an interrupted migration, which `gitone migrate` resolves - repository state below `.gitone/` that neither `init` nor `migrate` explains - a working tree the [preflight](#the-safety-preflight) already rejects, before the wizard asks anything ### Cancellation and retry Choosing `no`, pressing Ctrl+C, or closing the input cancels setup and writes nothing, including a cancelled correction. An invalid single answer is asked again. A problem the [preflight](#the-safety-preflight) can already see refuses the run before the first question. A path problem that only the complete configuration reveals returns you to the [owned-path correction](#correcting-the-owned-paths) before anything is written; anything else refuses the run unchanged. If the configuration is valid but the following `init` or `migrate` fails, the written configuration is kept and the failure is reported. Fix the reported problem and run `gitone setup` again: it then uses the existing configuration unchanged and only retries the action. The extension question comes only after project setup has completed. Setup never passes `--force`. If the optional installation fails, the completed project stays in place and the error starts with `VSCODE001`. ## Terminal presentation Human output uses the terminal's basic color palette close to native Git: staged changes and successful checks are green, unstaged and untracked changes and errors are red, warnings are yellow, branches are cyan, and secondary metadata is subdued. Slow actions show an updating spinner: `init`, `migrate`, `clone`, `fetch`, `pull`, `push`, `branch `, `switch` and the VS Code installation. Color and live rendering are automatic. Redirected output, `TERM=dumb` and `NO_COLOR` stay plain; `NO_COLOR` also makes setup use its stable line-oriented questions. `status --porcelain`, `status --json`, `vscode info --json` and file content written by `show` never contain presentation codes. `diff` and `log` color their output like Git does on a terminal and print it plain otherwise; the pager and any external diff program stay disabled either way. Everything GitOne prints for a person passes one shared output guard first. Only presentation generated by GitOne gets through; every control sequence in repository data is dropped. A commit subject, patch, path name or Git error detail can therefore neither change presentation nor move the cursor, retitle the window, write the clipboard or overwrite a line GitOne already printed. Printable text and normal Unicode names stay exactly as they are. File content written by `show` is the one exception: editors diff against the stored bytes, so those bytes are passed through unchanged. ## Commands GitOne uses an explicit allowlist. An unknown command or flag is rejected with `CLI001`; nothing is forwarded to Git. The table matches `gitone --help`. | Command | Purpose | | --- | --- | | `gitone setup` | Ask for the missing configuration, then initialize or migrate the project. | | `gitone clone [directory]` | Create a project from a repository that commits `.gitone.yml`. | | `gitone init` | Create the managed repositories and the ignore entries. | | `gitone migrate [--yes]` | Convert a root `.git/` into managed repositories. | | `gitone backup list` | List the retained migration backups. | | `gitone backup git [--backup ] -- [arguments...]` | Read a migration backup with native Git. | | `gitone backup restore [--backup ] [--yes]` | Copy a migration backup back to the root `.git/`. | | `gitone repo validate` | Check the merged configuration. | | `gitone repo list` | List the repositories, visibility, branch, push policy and remotes. | | `gitone status [--porcelain \| --json]` | Report the whole project, also when called in a subdirectory. | | `gitone add [-A \| -u \| ...]` | Stage changes in every affected repository. | | `gitone unstage [-A \| ...]` | Unstage changes without changing working-tree files. | | `gitone restore [--staged] ...` | Discard working-tree changes, or unstage with the Git spelling. | | `gitone commit <-m \| -F -> [...]` | Commit staged changes, or only the selected files. | | `gitone diff [all \| ] [--staged]` | Show the changes of every repository as one native Git patch view. | | `gitone log [all \| ] [-n ]` | Show the recent commits of every repository. | | `gitone branch []` | List the local branches, or create one in every repository. | | `gitone switch ` | Move every repository to the same existing local branch. | | `gitone fetch [all \| ]` | Update the `origin` remote-tracking refs of every repository. | | `gitone pull [all \| ]` | Fast-forward every repository to its `origin` branch. | | `gitone push [all \| ] [--yes]` | Push every repository that has outgoing commits. | | `gitone show --repository --source -- ` | Write exact file content from a repository source. | | `gitone vscode info --json` | Report the VS Code integration protocol and GitOne version. | | `gitone vscode install [--vsix ] [--force]` | Install the Marketplace extension or an explicit local VSIX through `code`. | | `gitone recover` | Finish an interrupted add, unstage, restore, commit, branch, switch or pull; report an interrupted push. | | `gitone abort` | Undo an interrupted add, unstage, restore, commit, branch, switch or pull; report an interrupted push. | | `gitone doctor` | Report project health without changing anything. | | `gitone agents` | Check the GitOne instructions in the project `AGENTS.md`. | | `gitone agents update` | Write the current GitOne instructions into the project `AGENTS.md`. | | Option | Purpose | | --- | --- | | `gitone` | Print the global help, exactly like `gitone --help`. | | `gitone --help` | Print the usage, every command and the global options. | | `gitone --help` | Print the arguments and options that command accepts. | | `gitone backup`, `gitone repo`, `gitone vscode` | Print the help of that command group. | | `gitone --version` | Print the version of the binary. | Commands exit `0` on success and `1` on failure. A valid `show` request whose path does not exist in the selected source exits `2` with `SHOW001`, so editor integrations can distinguish an absent version from a broken command. Failures print a stable numeric code as the first token. ### Commands that need an initialized project `status`, `add`, `unstage`, `restore`, `commit`, `show`, `diff`, `log`, `branch`, `switch`, `fetch`, `pull`, `push`, `recover` and `abort` work on managed repository state. They check that the project is usable before doing anything, and a project that is not usable is reported with the command that makes it usable: | Project state | Failure | Next command | | --- | --- | --- | | No `.gitone.yml` in this directory or any parent | `CONFIG001 configuration missing` | `gitone setup` | | Valid configuration, no repository initialized | `REPO001 no managed repository is initialized` | `gitone init` | | The same, but a root `.git/` exists | `REPO001 no managed repository is initialized` | `gitone migrate` | | The same, but root `.git` is a file or symlink | `REPO001 no managed repository is initialized` | `gitone doctor` | | Some repositories missing, or metadata that is not a GitOne repository | one `REPO001` line per repository | `gitone doctor` | Nothing is created, staged, committed or pushed by a refused command, and existing repository metadata is never reinitialized over: partial or invalid metadata and unsupported root `.git` forms are cases for `gitone doctor`, not for `gitone init` or `gitone migrate`. `gitone --help`, `gitone --help`, `gitone --version`, `gitone setup`, `gitone init`, `gitone migrate`, `gitone backup list`, `gitone backup git`, `gitone backup restore`, `gitone repo validate`, `gitone repo list`, `gitone doctor`, `gitone vscode info` and `gitone vscode install` deliberately work before initialization and are not affected by this check. The `backup` commands stay usable in exactly the states that need them, including after a restored root `.git/` makes the project unusable for everything else. Notes on the arguments: - `add`, `unstage` and `restore` path arguments are resolved relative to your current directory, and `.` covers the current subtree. Absolute paths are rejected. Apart from `.`, each argument must name one managed file; there are no Git pathspecs, globs or `--` separators. - `restore` needs at least one path and has no `-A` form. Without `--staged` it overwrites the selected working-tree files with the version their owning repository has in the index, which discards the changes in them. `--staged` does exactly what `unstage` does. `--source`, revisions, patch mode and `--ours`/`--theirs` are not accepted. - `commit -F -` reads the complete commit message from standard input. No other file name is accepted. - `commit` accepts explicit paths before or behind the message, as in `gitone commit src/site.css -m "Adjust the layout"` or `gitone commit -m "Adjust the layout" src/site.css`. Each one is resolved relative to your current directory and must name one managed file Git already knows; `.`, directories, globs, Git pathspecs, `--` and absolute paths are rejected with `CLI001` or `PATH003` before anything changes. A second `-m` or `-F` is rejected the same way. - `diff` takes no target, `all`, or one repository name, each optionally with `--staged`. It prints one heading per repository followed by the native Git patch, in configuration order; a repository without matching changes is left out, and no change at all prints nothing and exits `0`. `--staged` compares the index against `HEAD`, which also reports additions in a repository without a commit yet. Untracked files never appear in a diff; `gitone status` reports them. Paths, revisions, `--stat`, `--patch` and every other Git diff option are rejected with `CLI001`, as is an unknown repository name. - `log` takes no target, `all`, or one repository name, each optionally with `-n `. `count` must be a whole number from `1` to `1000` and defaults to `20`. Revisions, paths, `--graph`, `--pretty`, `--patch` and every other Git log option are rejected with `CLI001`, as are an unknown repository name and an out-of-range or non-numeric `count`. - `branch` takes no argument to list, or exactly one branch name to create that branch in every repository. `-d`, `-D`, `-m`, `-c`, `-f`, `--list`, `--all`, `-r`, a start point and a second name are rejected with `CLI001`; an invalid branch name is rejected with `BRANCH001`. - `fetch` takes no target, `all`, or one repository name. Remotes, refspecs, `--prune`, `--force`, tags and shallow options are rejected with `CLI001`, as is an unknown repository name. - `pull` takes the same target as `fetch`. `--rebase`, `--ff`, `--no-ff`, `--ff-only`, remotes, refspecs and every other Git pull option are rejected with `CLI001`; a pull is always fast-forward-only. - `push` takes no target, `all`, or one repository name, each optionally with `--yes`. Remotes, refspecs, `--force` and tags are not accepted. - `backup git` needs the separating `--` and accepts only `log`, `show`, `diff` and `status` behind it. Everything after that command is passed to Git unchanged; every other Git command and every Git option before the command are rejected with `CLI001`. The exit code is the one Git returned. - `backup git` and `backup restore` select the only backup a project has by themselves. As soon as a second directory exists below `.gitone/migration-backup/`, including an invalid one, the exact `--backup ` is required; a prefix, a friendly name and "the latest one" are not accepted, and an ID containing a path separator or naming a symbolic link is rejected with `MIG001`. - `--help` and `--version` need no project and change nothing. `--help` is accepted for `gitone`, for `repo` and for every command in the table above, and only as the last argument. `-h`, `gitone help` and any extra argument after `--help` are rejected with `CLI001`. - `gitone` without any argument prints the global help and exits `0`, and a command group called without a subcommand prints its group help and exits `0`. Nothing is read or changed, so both work in any directory. - Input rejected by the allowlist keeps exit code `1` and the `CLI001 unsupported command` prefix, followed by one hint line: input that names a known command points to `gitone --help`, everything else to `gitone --help`. The hint never relaxes the allowlist and is printed on standard error only. ## The normal workflow The following commands create a disposable project and two local bare remotes, so the complete workflow needs no network access: ```bash fixture="$(mktemp -d)" mkdir -p "$fixture/remotes" "$fixture/project/src" "$fixture/project/notes" git init --bare "$fixture/remotes/website.git" git init --bare "$fixture/remotes/notes.git" cd "$fixture/project" cat > .gitone.yml <<'YAML' version: 1 default_branch: main repositories: website: visibility: public remote: ../remotes/website.git paths: - .gitignore - .gitone.yml - README.md - src/** YAML cat > .gitone.local.yml <<'YAML' version: 1 repositories: notes: visibility: private remote: ../remotes/notes.git paths: - notes/** YAML printf '# Example\n' > README.md printf 'body {}\n' > src/site.css printf '# Plan\n' > notes/plan.md export GIT_AUTHOR_NAME=GitOne export GIT_AUTHOR_EMAIL=gitone@example.invalid export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" ``` `git init --bare` is used only to create the two test remotes. Inside the project, use `gitone` for every operation: ```bash gitone repo validate gitone init ``` ``` GitOne initialized ✓ notes (main) ✓ website (main) Nothing was staged or committed. ``` `init` requires a directory that already has a valid configuration and no root `.git/`. An existing root repository is rejected with the hint to run [`gitone migrate`](#migrating-an-existing-repository) instead. ```bash gitone status ``` ``` NOTES visibility: private untracked: notes/plan.md WEBSITE visibility: public untracked: .gitignore untracked: .gitone.yml untracked: README.md untracked: src/site.css BRANCHES notes: main (no remote-tracking branch) website: main (no remote-tracking branch) ``` `status` always covers the **whole project**, also when you run it in a subdirectory. Ordinary changes exit `0`. Anything unsafe is listed under `ISSUES` and exits `1`: ``` ISSUES PATH001 secrets.txt: path is not assigned to any repository ``` ```bash gitone add -A ``` ``` Staged 5 paths notes: added notes/plan.md website: added .gitignore added .gitone.yml added README.md added src/site.css ``` Every mutating command validates the complete managed working tree first, even when it only touches one path. As long as `secrets.txt` above is unassigned, `add`, `commit` and `push` all refuse to run. ```bash gitone commit -m "Add site and notes" ``` ``` notes: commit ca535f0bcfc7570850c20d5a5ca5b1634a0bcf3f website: commit 0938d2a7ec12ba398dd1e61691d9b982e25388ea GitOne-Group: 6bfc8400 ``` A commit that spans two or more repositories gives every created commit one shared `GitOne-Group` trailer, so the parts can be found again later. A commit in a single repository is an ordinary native commit with no trailer. `commit` never pushes. ### Committing selected files ```bash gitone commit src/site.css notes/plan.md -m "Adjust the layout" ``` With explicit paths this is native `git commit ...`: the **current working-tree version** of exactly those files is committed, even when a different version was staged. Every unselected staged change stays staged, and a repository without a selected change gets no commit at all - so the same selection may produce one commit, several grouped commits, or `Nothing to commit.` A file Git does not know yet cannot be selected; stage it once with `gitone add` first. A rename or a deletion follows the same rule as in native Git: only the paths you name are committed, and GitOne never adds the other side of a rename to your selection. The whole result is checked before the first commit exists. The tree every participating repository would create has to satisfy the ownership, protected path and safe-link rules, so a selection that would commit a link without its target is refused with `PATH003` and nothing is changed. ```bash gitone push ``` ``` Checking repositories... NOTES ✓ reachable WEBSITE ✓ reachable Validating paths... ✓ no unassigned files ✓ no ambiguous paths ✓ every published path belongs to its repository Ready to push: NOTES remote: origin branch: main commits: 1 WEBSITE remote: origin branch: main commits: 1 Continue? [y/N] ``` The default answer is no. Answering anything but `y`/`yes` prints: ``` Push aborted. No repositories were modified. ``` When standard input is not a terminal - a script, a CI job, an agent - there is nobody to ask, so the push fails immediately unless you pass `--yes`: ``` PUSH001 a push without an interactive terminal requires --yes No repositories were modified. ``` After a confirmed push: ``` notes: pushed ca535f0 to origin/main website: pushed 0938d2a to origin/main ``` ### Selective push ```bash gitone push website --yes ``` Only `website` is published, but the whole working tree is still validated first: local path safety does not depend on which repository you publish. Repositories without outgoing commits never participate, and their remotes are not contacted at all - `gitone push` on an up-to-date project prints `Nothing to push.` A repository with `push: disabled` is skipped by an all-repository push and is listed before the result. Naming it explicitly fails with `PUSH001` before any remote is contacted. When `rules.push.require_clean_worktree` is `true`, a push with any staged, unstaged or untracked change also fails with `PUSH001`. By default a dirty but otherwise safe working tree does not block a push. ## Undoing changes `unstage` takes paths out of the index and never touches a file: ```bash gitone unstage src/site.css ``` ``` Unstaged 1 path website: modified src/site.css ``` `restore` is the Git spelling for both directions. `gitone restore --staged ...` runs exactly the `unstage` above. Without `--staged` it replaces the selected working-tree files with the version their owning repository has in the index: ```bash gitone restore src/site.css ``` ``` Restored 1 path website: modified src/site.css ``` **This throws the changes in those files away.** There is no undo once the command finished; stage or commit anything you want to keep first. A file deleted from the working tree is written back, an untracked file is never restored, and no directory is ever removed. Paths are resolved like `add` paths: relative to your current directory, `.` for the current subtree, no absolute paths and no globs. Files belonging to several repositories are restored in one operation. Every index version is checked out below `.gitone/recovery/` first, with the file mode the index records, and the real files are replaced only afterwards, so a failure never leaves half a file behind. If a replacement fails midway, `gitone recover` finishes the remaining files and `gitone abort` puts back the versions the restore started from. ## Machine-readable status Both formats carry a schema `version` that changes when a field changes meaning or disappears. Both are deterministic: repositories are sorted by name, changes by repository, then state (`staged`, `unstaged`, `untracked`), then path. `gitone status --porcelain` prints one tab-separated record per line. Managed paths cannot contain tabs, so spaces in filenames stay unambiguous. ``` version 1 repository notes private main ? ? repository website public main ? ? change notes staged added notes/plan.md change website staged added .gitignore ``` | Record | Fields | | --- | --- | | `version` | schema version | | `repository` | name, visibility, branch, ahead, behind | | `change` | repository, state, type, path, previous path (renames and copies) | | `issue` | code, path, detail | `gitone status --json`: ```json { "version": 1, "repositories": [ { "name": "notes", "visibility": "private", "branch": "main", "ahead": null, "behind": null } ], "changes": [], "issues": [] } ``` Ahead and behind counts come from local remote-tracking refs only, so they exist after the first successful `push`, `fetch` or `pull`. Before that the counts are reported as **unknown** - `?` in porcelain, `null` in JSON, `(no remote-tracking branch)` in the human output - never as `0`. A detached HEAD is reported as branch `(detached)` and blocks commit and push. ## Editor integration contract The VS Code extension uses only public GitOne commands. It does not inspect `.gitone/` and does not invoke Git directly. Check protocol compatibility without requiring a project: ```bash gitone vscode info --json ``` ```json {"protocol":1,"gitone_version":"devel"} ``` Release binaries report their stamped version instead of `devel`. Install a VSIX downloaded from the matching GitOne release without requiring a project: ```bash gitone vscode install --vsix ./gitone__vscode.vsix ``` During local extension development, replacement is always explicit: ```bash gitone vscode install --vsix ./editors/vscode/gitone-.vsix --force ``` Once `devidevio.gitone` is published in the Marketplace, install or update it with: ```bash gitone vscode install ``` All forms delegate to the official `code` command on `PATH`. Reload the VS Code window after installation. `--force` is accepted only on this explicit command and is never added by `gitone setup`. Read the exact bytes needed for a native editor diff: ```bash gitone show --repository website --source head -- src/site.css gitone show --repository website --source index -- src/site.css ``` `head` reads the current branch commit; `index` reads the staged version. The path must belong to the named repository. Missing content exits `2` with `SHOW001`; unsafe input and Git failures exit `1`. ## Cloning a project ```bash gitone clone git@github.com:example/website.git gitone clone git@github.com:example/website.git ~/projects/website ``` `clone` is the one command that creates a project from a remote. It works outside any GitOne project, and the destination must not exist yet. Without a directory argument the repository name of the URL is used, exactly as native `git clone` would. A relative local URL is resolved from the final project directory, because the configured `origin` keeps using it from there. Everything is built in a temporary directory next to the destination and only published once it validates. Authentication, configuration, checkout and validation failures therefore leave no destination behind and never change an existing path. What a clone does, in order: 1. clones the bootstrap URL through native Git, without a checkout 2. reads the committed `.gitone.yml` from the default branch 3. requires exactly one repository to own `.gitone.yml`, and that repository's configured `origin` to be exactly the cloned URL 4. adopts the cloned repository, with its complete history, as that managed repository 5. initializes every other repository of the committed configuration and fetches the ones that have an `origin` 6. refuses every incoming tree that contains a path its repository does not own, before the first file is written 7. checks out each default branch, sets its `origin` upstream, writes the GitOne ignore entries and runs the same repository, path and configuration checks as `gitone doctor` A repository without a configured `origin` is initialized but stays unborn and is reported with its next steps instead of being guessed or omitted. A configured `origin` without the default branch is refused. ### What a clone cannot discover `.gitone.local.yml` is never committed, so a fresh clone cannot know about repositories that exist only there. They are not cloned automatically. Add them to your own `.gitone.local.yml` afterwards and run `gitone pull `. `--branch`, `--depth`, partial clone, sparse checkout and submodules are not accepted. An existing or non-empty destination is refused rather than filled. ## Migrating an existing repository A directory that is already one ordinary Git repository becomes a GitOne project with: ```bash gitone migrate ``` Migration converts the **project layout**, not the history: - Your files are not touched. Only the idempotent `.gitignore` entries are added, exactly as `init` adds them. - The managed repositories are created empty. They have **no commits and no staged files** and use only the branch and remotes of your configuration. - The complete old `.git/` is copied to a timestamped directory below `.gitone/migration-backup/` before anything changes, and that copy is verified as a usable Git repository first. - Only then is the root `.git/` removed. The backup stays forever; GitOne never moves or deletes it. The command asks before it does anything, and warns more explicitly when the working tree has staged, unstaged or untracked changes. `--yes` is the non-interactive confirmation; without a terminal and without `--yes` the command fails without changing anything. Migration is refused, with nothing changed, when the project is not a plain root repository: a `.git` **file** (linked worktree or submodule checkout), linked worktrees, submodules, a bare repository, a nested repository, an already migrated project, an invalid configuration or an unsafe path. Migration and the restoration below never contact a remote. ### What only lives in the backup afterwards Everything the managed repositories do not get: - every commit, branch and tag of the old repository - stashes, reflogs, the index and the old staging state - hooks, remotes and the Git configuration of the old repository ### Listing, inspecting and restoring a backup `gitone backup` is the command group for the retained backups. It needs a configuration, but no usable managed repositories, so it also works in exactly the states that make everything else fail. ```bash gitone backup list ``` ``` Migration backups below .gitone/migration-backup ID CREATED (UTC) STATE 20260826T133234Z-223441957 2026-08-26 13:32:34 valid ``` Every direct directory below `.gitone/migration-backup/` is listed newest first with its **full ID** - the directory basename, the only durable identifier a backup has - its UTC time and whether it still is a valid Git repository. Symbolic links are not followed and not listed, invalid directories stay visible, and a project without any backup prints `No migration backups found.` and exits `0`. Read a backup with native Git: ```bash gitone backup git -- log --all gitone backup git --backup 20260826T133234Z-223441957 -- show HEAD gitone backup git --backup 20260826T133234Z-223441957 -- status ``` Only `log`, `show`, `diff` and `status` are accepted, and only behind the separating `--`; the arguments after them go to Git unchanged. The backup is the Git directory and the **project root** is the working tree, so `status` and `diff` compare the retained repository with your current files. Optional locks are disabled, so no inspection writes anything into the backup. Git output is streamed as Git produced it, without a GitOne heading around it, and the exit code of Git is the exit code of the command. Restore the old repository at any time - the managed repositories and the backup are independent, so nothing is lost by copying it back: ```bash gitone backup restore --backup 20260826T133234Z-223441957 ``` The command validates the backup, refuses any existing root `.git` path - directory, file or symbolic link - and only then asks, defaulting to `No`. `--yes` is the non-interactive confirmation; without a terminal and without `--yes` it fails without changing anything. Only the complete `.git/` is restored: your files, the ignore entries, the configuration and the managed repositories are never touched, and the backup itself is kept. The copy is written into a temporary directory in the project root and validated there, and only a complete, valid copy is published as `.git/`, so a cancelled or failed restore leaves neither a partial root repository nor a temporary one. While a root `.git/` exists the ordinary GitOne commands are unavailable; `gitone backup list` and `gitone backup git` still work, and another `gitone backup restore` refuses the root repository it published. ### Failure and interruption A migration that fails after the backup was written is rolled back: the repositories it created are removed, the root `.git/` is copied back from the backup, and the backup is kept. While a migration runs it records its state in `.gitone/migration/`. If it is interrupted, the next `gitone migrate` refuses to migrate again and offers the restoration instead: ``` An interrupted migration was found. Backup: /project/.gitone/migration-backup/20260825T101500Z-3f9c21 Restoring copies the backup back to /project/.git and removes the repositories the migration created. The backup itself is kept. Continue? [y/N] ``` The restoration copies; it never moves or consumes the backup. If a root `.git/` exists again, it is never overwritten - remove or move it yourself first if you really want the backup back in place. ## Locking, recovery and abort Only one mutating operation runs at a time. The lock records the process and the command: ``` LOCK001 another GitOne operation is currently running: PID 4711, gitone commit ``` `repo validate` and `doctor` may still inspect a locked project. `recover` and `abort` need the exclusive lock and can run only after the active process ends. A stale lock is then reclaimed by the next operation - unless recovery state must be resolved first: ``` REC001 an interrupted GitOne operation must be recovered first: run gitone recover or gitone abort ``` Staging and unstaging prepare every affected index as a temporary copy and swap the real indexes in only after all Git work succeeded. Commit records the starting branch position and index of every repository, and for a selective commit the selected paths and the version each of those files has, before the first commit is created, `branch ` records the commits its new refs are written at before the first one exists, and `switch ` records the starting branches, commits and indexes before the first checkout. `restore` checks every index version out beside the working tree and records the current version of every selected file before the first one is replaced. They all write their state to `.gitone/recovery/`. ```bash gitone recover # finish an interrupted add, unstage, restore, commit, branch, switch or pull gitone abort # undo an interrupted add, unstage, restore, commit, branch, switch or pull ``` With nothing to do, both print `No interrupted GitOne operation found.` For an interrupted push, both commands only read the remote refs and report what was published. Neither command rolls a push back or silently repeats it. `abort` refuses when a repository, a restored file or a selected file of an interrupted selective commit changed outside GitOne since the interruption. It then prints the concrete manual steps - which commit to inspect, which branch position to restore, which saved index or file version to copy back - instead of overwriting work it did not make. `gitone doctor` reports the lock and the recovery state without touching either. It never reclaims a stale lock and never resolves recovery state. ## Reading history ```bash gitone log gitone log website -n 5 ``` `gitone log` prints one section per configured repository, in configuration order, with the most recent commits of each. Every line comes from native Git and contains the abbreviated commit ID, the author date, the ref decorations and the subject: ``` PRIVATE 9c1f2ab 2026-08-24 (HEAD -> main) Add the authentication notes PUBLIC 4e77d90 2026-08-25 (HEAD -> main, origin/main) Implement authentication 1b02c5e 2026-08-21 Initial commit ``` Each repository shows at most 20 commits; `-n ` lowers or raises that up to 1000. A repository without a commit yet is labeled `no commits yet` instead of failing the command, so one unborn repository never hides the history of the others. Every repository is logged on its own. A commit created by a multi-repository `gitone commit` carries its `GitOne-Group` trailer, but `log` does not reconstruct one shared logical history from it; the trailer is visible in the full commit message through native Git. `log` reads only: it never changes refs, indexes or working-tree files, and it never starts a pager. Like every other command that works on repository state, it checks that the project is initialized and refuses while another GitOne operation or unresolved recovery state exists. ## Branches ```bash gitone branch gitone branch feature/auth ``` `gitone branch` without an argument lists every local branch of the project with the repositories holding it, followed by the current branch of every repository: ``` Branches BRANCH REPOSITORIES feature/auth private, public main private, public release public Current branch private: main public: main release is missing from private. ``` Branch rows are sorted by name and the repositories are listed in configuration order, so the output is deterministic. A branch that only part of the project has, a repository whose `HEAD` is not on a branch, and repositories that disagree about the current branch are stated below the list instead of failing it: listing exists to show exactly that state. A project whose repositories have no commit yet shows its current branch with no repositories and the note `main does not exist in any repository yet.` `gitone branch ` creates that branch in **every** configured repository, at the commit each one currently has checked out: ``` Branch feature/auth created: private ✓ feature/auth at 0c80747 public ✓ feature/auth at 00f3151 No repository switched branches. Every HEAD, index and working-tree file is unchanged. ``` Creation writes nothing but the new refs. Move the project onto the new branch with `gitone switch`. Every repository has to pass preflight before the first ref is written: | Refusal | Failure | | --- | --- | | The name breaks native Git's branch-name rules | `BRANCH001 "" is not a valid branch name` | | A repository has no commit on its current branch | `BRANCH001 repository "" has no commit on yet` | | A repository is not on a branch | `BRANCH001 repository "" is not on a branch` | | The repositories are on different branches | `BRANCH001 the repositories are not on the same branch` | | A repository already has the branch | `BRANCH001 repository "" already has branch ` | Every refusal reports all problems it found at once, leaves every repository unchanged and closes with `No branch, HEAD, index or working-tree file was changed.` Because branch creation is a mutating command, the complete managed working tree is validated too, so unassigned or ambiguous paths stop it as well. The starting commits are recorded under the project lock before the first ref exists, and every created ref gets an operation marker in its reflog. A failure after that deletes only branches with that marker at the recorded commit and reports the same unchanged-project message. Otherwise the recorded state stays for `gitone recover`, which creates the remaining branches, and `gitone abort`, which deletes the created ones. Deleting, renaming, copying, force-resetting, upstreams, start points, remote branches and creating a branch in only one repository are not supported. ## Switching branches ```bash gitone switch feature/auth ``` `gitone switch ` moves **every** configured repository to that branch and updates the shared working tree with it: ``` Switch results: private ✓ main → feature/auth public ✓ main → feature/auth ``` The branch is never created. It has to exist in every repository already, so the normal order is `gitone branch feature/auth` and then `gitone switch feature/auth`. A repository that is already on the branch is reported as `already on feature/auth` and stays untouched. Every repository has to pass preflight before the first one moves: | Refusal | Failure | | --- | --- | | A repository does not have the branch | `SWITCH001 repository "" has no branch ` | | A repository is not on a branch | `SWITCH001 repository "" is not on a branch` | | A repository has staged or unstaged changes | `SWITCH001 repository "" has staged changes on ` | | The target tree contains a path the repository does not own | `SWITCH001 repository "" in : path is owned by ` | | The checkout would overwrite a file already in the working tree | `SWITCH001 repository "" cannot check out ` | Every refusal reports all problems it found at once, leaves every repository unchanged and closes with `No branch, HEAD, index or working-tree file was changed.` Because switching is a mutating command, the complete managed working tree is validated too, so unassigned or ambiguous paths stop it as well. **Local work is never carried across branches.** GitOne does not stash, discard or merge: commit or undo your changes first. Untracked files stay where they are, and a checkout that would overwrite one is refused instead of overwriting it. Repositories that disagree about their current branch are the one project issue `switch` tolerates - putting them back on one branch is exactly what it does. Every other command still refuses that state: ``` REPO001 the repositories are not on the same branch: private on feature, public on main: run gitone switch ``` The configured `default_branch` is only where a repository starts. After a switch the project is on another branch, and `gitone status` and `gitone doctor` accept that as long as all repositories agree. The starting branches, commits and exact indexes are recorded under the project lock before the first checkout, and every checkout is verified against the target commit - native Git reports a checkout as done even when it could not write every file. A failure after that returns the repositories already switched to their starting branch and reports the same unchanged-project message. Otherwise the recorded state stays for `gitone recover`, which switches the remaining repositories, and `gitone abort`, which returns the switched ones to their starting branch. `-c`, `-C`, automatic branch creation, `--detach`, `--force`, `--merge`, start points, remote branch guessing, paths and switching only one repository are not supported. ## Fetching ```bash gitone fetch gitone fetch website ``` `gitone fetch` updates the `origin` remote-tracking refs of every configured repository, in configuration order, through native Git. Credentials, URL rewrites, transport configuration and hooks behave exactly as they do for a plain `git fetch`. The only refs changed are below `refs/remotes/origin/`: branches, `HEAD`s, indexes and working-tree files stay byte-for-byte as they were. Native Git still stores fetched objects and updates `FETCH_HEAD`. Ordinary staged and unstaged changes are therefore allowed, but the complete managed working tree is still validated before the first remote is contacted, exactly as for a push - an unassigned or otherwise unsafe path stops the whole operation with `PATH001`, `PATH002` or `PATH003` and no remote is contacted. A repository without a configured `origin` is skipped by an all-repository fetch and reported as such. Naming it as the target fails with `REPO001` before any remote is contacted, as does an unknown repository name. Each repository reports what really happened, decided by comparing its remote-tracking refs before and after the fetch: ``` Fetch results: notes ✓ updated origin refs website already up to date scratch skipped, no configured origin remote ``` After a fetch, `gitone status` reports the new ahead/behind counts for every repository whose branch tracks `origin`. **A fetch across several repositories is not atomic.** Repositories are fetched one after another, and a failure - authentication, transport, an unreachable remote - names the affected repository and stops with `FETCH001`. The remote-tracking refs that earlier repositories already received stay updated and are never rolled back; the report above the error states exactly which ones those are. Rerun `gitone fetch` to retry the rest. `fetch` never merges, rebases, prunes or changes the working tree. Use `gitone pull` to move the local branches. ## Pulling ```bash gitone pull gitone pull website ``` `gitone pull` fetches the selected repositories and then **fast-forwards only**. It never creates a merge commit and never rebases, stashes or discards local work - there is no flag that changes this, because a repository that cannot be fast-forwarded is a decision only a person can make. Before the first local branch moves, every selected repository must pass the complete preflight: - **working tree** - the whole managed working tree is valid, exactly as for a fetch, and a repository that would move has no staged or unstaged changes. Untracked files are allowed; native Git still refuses to overwrite one. - **upstream** - the local branch tracks exactly `origin/`, and that remote-tracking branch exists after the fetch. A missing or different upstream is refused with `PULL001`. - **fast-forward** - the local branch is an ancestor of `origin/`. A diverged branch is refused; resolve it with Git yourself. - **ownership** - every path of the incoming tree belongs to that repository, so a remote commit can never smuggle another repository's paths into the working tree. Each repository is then reported with what really happened: ``` Pull results: notes ✓ updated main to a1b2c3d website already up to date scratch skipped, no configured origin remote ``` A failed fetch or preflight prints the same complete report before returning the error; one failed fetch does not prevent the remaining fetches. A branch that is already current or that is ahead of `origin` stays unchanged and is reported as such; a local-only repository is skipped by `gitone pull` and rejected as an explicit target with `REPO001`. **A pull is transactional for the local branches, not for the fetch.** All preflight decisions are made before the first branch, index or working-tree file changes, so a refusal leaves the project exactly as it was. If a fast-forward still fails after an earlier repository was advanced - an untracked file in the way, a failing hook, a full disk - the repositories that already moved are restored to the commits they started from, and the pull fails with `PULL001` and no branch changed. If even that restoration is not safe, the recorded state stays behind: `gitone recover` completes the remaining fast-forwards and `gitone abort` restores the branches, indexes and files of every repository the pull advanced. Both refuse a repository that changed outside GitOne since the interruption and print the manual steps instead. The remote-tracking refs a pull fetched always remain, also when the local branch updates are rolled back. A later `gitone pull` fetches again before it retries the local updates. ## Push safety and partial failures Before the first remote is modified, every participating repository must pass the complete preflight: - **policy** - explicitly selected repositories allow push, and a configured clean-worktree rule has no staged, unstaged or untracked change to report - **destination** - `origin` is configured, matches the configuration, and has exactly one fetch and one push URL that are the same URL - **connectivity** - the remote answers a read-only ref listing - **fast-forward** - the remote branch is an ancestor of what would be pushed, and the commit it points at is known locally - **ownership** - every path added, changed or deleted by every outgoing commit, and every path of the resulting published tree, belongs to that repository and is not protected. A branch the remote does not have yet is checked over its complete reachable history. If any check fails, nothing is pushed and the message ends with `No repositories were modified.` The push itself sends the current branch to the branch of the same name on `origin`, without force and without tags, and sets the upstream after the first success. Other remotes, refspecs, deletions and force pushes are not supported. **A push across several repositories is best effort, not atomic.** Several remotes cannot be updated as one transaction. GitOne records what it intends to publish before the first remote changes and the result after each repository, so a failure in the middle is reported precisely instead of guessed: ``` Push results: notes origin/main published ca535f0 website origin/main unchanged at 0938d2a ``` After a crash, `gitone recover` or `gitone abort` reads the recorded refs back **from the remotes** and reports what is really there. Neither command rolls a push back or silently repeats one; rerun `gitone push` to publish the rest. ## Error codes Every failure prints its code as the first token of the message. | Code | Meaning | | --- | --- | | `CONFIG001` | configuration missing | | `CONFIG002` | invalid YAML | | `CONFIG003` | invalid configuration | | `PATH001` | path assigned to no repository | | `PATH002` | path assigned to more than one repository | | `PATH003` | otherwise unsafe path | | `REPO001` | repository invalid or unavailable | | `GIT001` | native Git failure | | `BRANCH001` | branch listing or creation refused or failed | | `SWITCH001` | switch refused or failed for one or more repositories | | `PUSH001` | push preflight failed | | `FETCH001` | fetch failed for one or more repositories | | `PULL001` | pull refused or failed for one or more repositories | | `LOCK001` | another operation is running | | `REC001` | recovery required | | `CLI001` | unsupported command or flag | | `CLONE001` | clone refused or failed | | `MIG001` | migration refused, failed or interrupted | | `SETUP001` | interactive setup refused | | `AGENT001` | AGENTS.md missing, not current or not safely editable | | `SHOW001` | requested path absent from HEAD or index | | `VSCODE001` | VS Code CLI or extension installation failed | `CLI001 unsupported command` is followed by one hint line naming the help page that lists the accepted form: `gitone --help` for a known command, `gitone --help` otherwise. `CONFIG001` and `REPO001` name the next command when a project is not ready: see [Commands that need an initialized project](#commands-that-need-an-initialized-project). ## Security boundary What GitOne enforces: - a path is published only by the repository that owns it, checked against the outgoing commits **and** the resulting tree of every push - a configured protected path is refused even when an ownership pattern also matches it - an unassigned or ambiguous path stops every mutating operation - `.gitone/` and `.gitone.local.yml` can never be owned, staged or committed - an incoming tree carrying an **unsafe** symbolic-link entry is refused before `clone`, `pull` or `switch` changes a branch, an index or a working-tree file; a safe alias is materialized as the link it is - repository content cannot drive your terminal, see [Terminal presentation](#terminal-presentation) What GitOne does **not** do: - it never inspects file contents. There is no secret scanning, no data classification and no entropy check. A private key inside `src/` is published like any other owned file unless its path is explicitly covered by `rules.protected_paths`. - it is not an access-control boundary. Everything sits in one directory that your shell, editor, backup tool and every other local program can read. - it trusts the local environment: the Git binary, Git configuration, credential helpers, hooks and Git LFS filters all run as they normally do. Unsupported inside managed paths, and reported as `PATH003` rather than silently handled: nested or bare Git repositories, non-regular files, ASCII control characters in names, and an ignored `.gitignore` that would hide other paths. Submodules are not supported at all. Normal Unicode filenames are fine. Symbolic links are supported only as a deliberate alias inside one repository, for example: ```text .claude/skills -> ../.agents/skills ``` Such a link is accepted when its stored target is relative and non-empty, resolves inside the project root, exists as a regular file or a real directory, is reached without passing through another link, and belongs to exactly the same repository as the link. A directory target must have at least one relevant path below it and every relevant descendant must have the link's owner; an empty or fully ignored directory is refused. GitOne owns the link path itself and never traverses it to invent managed paths below the alias. Everything else is `PATH003`, naming the link, the stored target and the reason: an absolute target, a target outside the project, a target in another repository, a dangling target, a link chain, a cycle, and any link that is or reaches `.gitone.yml`, `.gitone.local.yml`, `.gitignore`, `.gitone/`, `.git/` or a configured protected path. GitOne never opens such a target for reading or writing. Restore, recover and abort preserve a link as a link: the stored target is recorded and put back without ever following it. Add, unstage, restore, recovery, migration, pull and switch validate the state they would leave behind, so a selection that covers a link without its target is refused and nothing changes. ## Instructions for AI agents GitOne publishes one canonical instruction block. `gitone agents update` writes it into the project `AGENTS.md`; you can also copy it into `CLAUDE.md` or any equivalent yourself. ### The managed block Only `/AGENTS.md` is managed. Nested instruction files, `CLAUDE.md` and tool-specific equivalents are never searched for or edited. The block lives between two fixed markers: ```text ...the block below... ``` Complete markers define the text GitOne owns, so anything between them is replaced. Everything outside them is preserved byte for byte, an existing file keeps its permissions, the result is published atomically, and a symbolic link is refused rather than followed. | State of `AGENTS.md` | `gitone agents` | `gitone agents update` | | --- | --- | --- | | missing | `AGENT001` | creates the file with the marked block | | ordinary file, no GitOne text | `AGENT001` | appends the marked block after a blank line | | the exact current block, unmarked | exit `0` | wraps that exact text in the markers | | one marked block, current | exit `0` | nothing changes | | one marked block, stale or edited by hand | `AGENT001` | replaces only that block | | other recognizable GitOne text | `AGENT001` | `AGENT001` | | duplicate blocks, marked and unmarked text together, missing or repeated markers | `AGENT001` | `AGENT001` | | a symbolic link or another non-regular file | `AGENT001` | `AGENT001` | Before either command decides anything, `AGENTS.md` has to pass the ordinary [ownership rules](#path-grammar): exactly one repository owns it, and it is not protected, case-conflicting or otherwise unsafe. Those failures stay `PATH001`, `PATH002` and `PATH003`. ```bash gitone agents # read-only check, exit 0 or 1 gitone agents update # write the block ``` `gitone agents` needs a valid configuration but no initialized repository, takes no arguments or flags, changes nothing and has no `--json` mode. `gitone agents update` is the explicit mutation: the command itself is the authorization, so it has no confirmation, no `--yes` and needs no terminal. Neither command edits `.gitone.yml` or `.gitone.local.yml`, stages, commits, initializes, migrates or contacts a remote. Assign `AGENTS.md` yourself, then run the update. Both are documented at [gitone.io/#agents](https://gitone.io/#agents). ### What setup offers Both setup modes ask about the block as the last optional question, before the summary and the confirmation: - default `no`, and a `no` never reserves or assigns `AGENTS.md` - skipped when the current block is already there - a stale marked block is offered as `Update GitOne instructions?` with default `yes` - text GitOne must not edit is reported and left alone, without a question - the wizard gives a file it creates the only repository, or asks which repository owns it; a private repository in `.gitone.local.yml` is a valid answer, so instructions never have to be published to be owned - template mode adds `AGENTS.md` to the placeholder repository, with a comment naming the private alternative - an existing configuration is never edited: the block is only offered when exactly one repository already owns `AGENTS.md` The file is written only after the core setup result succeeded. If only that optional write fails, the finished project stays and the failure says so. Whenever the block is not added, setup prints the steps to add it later, including the assignment when no pattern owns the file yet. ### The block Copy this into your `AGENTS.md`, `CLAUDE.md` or equivalent, or let `gitone agents update` keep it current: ```markdown This project uses GitOne: one working directory, several Git repositories, ownership decided by path in `.gitone.yml`. - Use `gitone`, never `git`, for status, staging, committing and pushing. There is no `.git/` in the project root. - `gitone status --json` is the machine-readable project state (schema `version: 1`). Exit code 0 means ordinary changes, 1 means the `issues` array explains why the project is unsafe. - Available: `init`, `repo validate`, `repo list`, `status [--porcelain|--json]`, `add [-A|-u|...|.]`, `unstage [-A|...|.]`, `restore [--staged] ...`, `commit [...] -m `, `commit [...] -F -`, `diff [all|] [--staged]`, `log [all|] [-n ]`, `branch []`, `switch `, `fetch [all|]`, `pull [all|]`, `push [all|] [--yes]`, `show --repository --source -- `, `vscode info --json`, `recover`, `abort`, `doctor`, `agents`, `agents update`, `vscode install [--vsix ] [--force]`. `gitone --help` prints the accepted syntax. Everything else fails with `CLI001`; do not fall back to plain `git` to work around it. - Never create, edit or commit `.gitone/` or `.gitone.local.yml`. Never move a path across repository boundaries to make an error go away. - A new file must be added to the `paths` of exactly one repository in `.gitone.yml` (public) or `.gitone.local.yml` (private) before it can be staged. `PATH001` means unassigned, `PATH002` means assigned twice. - Repository boundaries are build boundaries. Before one file references another - an import, an include, a build input - check whether both paths have the same owner, and check the references you added again before you hand the work back. A reference across repositories works in your working directory and breaks in any checkout that has only one of them, such as CI. GitOne never reads file contents, so it cannot catch this: report it and ask a human rather than moving the path. - `commit` with explicit paths commits the current working-tree version of exactly those managed files, like native `git commit `, and leaves every other staged change staged. A file Git does not know yet has to be staged once before it can be selected. - A path covered by `rules.protected_paths` must never be staged or committed; ownership does not override that hard deny. - Valid patterns are exact paths such as `README.md`, `*` inside one path segment and at most one `**` segment, as in `src/**`, `docs/**/*.md` or `**/*.md`. Duplicate patterns are an error; overlapping ones are not, but a concrete path matching two repositories is `PATH002`. - Non-interactive pushes require `gitone push --yes`; without a terminal a push fails with `PUSH001` instead of asking. Ask a human before pushing. `push: disabled` repositories are never published, and a configured `require_clean_worktree` rule must also pass. - On `LOCK001` wait and retry. On `REC001`, `recover` finishes an interrupted add, unstage, restore or commit and `abort` undoes it. For an interrupted push both commands only report what reached the remotes. Never delete `.gitone/recovery/` by hand. - `gitone doctor` is read-only and safe to run at any time, including while another operation holds the lock. ``` --- # GitOne hands-on tutorial Install GitOne and run every supported command against disposable projects. Use one terminal session on macOS or Linux. For details beyond this walkthrough, see the [complete usage guide](https://gitone.io/docs/usage). ## 1. Check the tools ```bash git --version code --version ``` GitOne requires Git 2.28 or newer, and VS Code 1.127 or newer for the extension. Building from source instead of using a release additionally needs Go 1.25 or newer and Node.js 24 or newer. Create one disposable workspace for everything below: ```bash export GITONE_TUTORIAL_ROOT="$(mktemp -d)" export GIT_AUTHOR_NAME=GitOne export GIT_AUTHOR_EMAIL=gitone@example.invalid export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" echo "$GITONE_TUTORIAL_ROOT" ``` ## 2. Install GitOne Install a release as described in the [README](https://gitone.io/#install), or build the source checkout you are standing in: ```bash export GITONE_SOURCE="$(pwd)" go test ./... go build -o gitone ./cmd/gitone sudo install -m 0755 ./gitone /usr/local/bin/gitone hash -r ``` Either way, confirm what you installed: ```bash command -v gitone gitone --version gitone --help ``` Optional short command: ```bash sudo ln -s "$(command -v gitone)" "$(dirname "$(command -v gitone)")/git1" git1 --version ``` ## 3. Install the VS Code extension Install the `gitone__vscode.vsix` published with the release you installed: ```bash gitone vscode install --vsix ./gitone__vscode.vsix ``` Or build it from the same source checkout, where `--force` replaces an already installed development build of the same version: ```bash cd "$GITONE_SOURCE/editors/vscode" npm install npm test npm run package -- --out gitone-.vsix gitone vscode install --vsix ./gitone-.vsix --force ``` Either way, confirm the result: ```bash code --list-extensions --show-versions | grep '^devidevio\.gitone@' ``` It should print `devidevio.gitone@`. ## 4. Create a local-only project ```bash mkdir -p "$GITONE_TUTORIAL_ROOT/local-project/src" mkdir -p "$GITONE_TUTORIAL_ROOT/local-project/notes" cd "$GITONE_TUTORIAL_ROOT/local-project" printf '# Local GitOne test\n' > README.md printf 'application\n' > src/app.txt printf 'private notes\n' > notes/plan.txt gitone setup ``` Use these answers in the setup wizard: 1. Setup mode: `Wizard`, which Enter selects 2. Default branch: `main` 3. Repository `app`, visibility `public`, paths `README.md` and `src/**` 4. Leave its remote empty and store it in `shared` 5. Add repository `notes`, visibility `private`, path `notes/**` 6. Leave its remote empty and store it in `local` 7. Stop adding repositories, choose `app` as owner if asked, then confirm Inspect the result: ```bash gitone repo validate gitone repo list gitone doctor gitone status gitone status --porcelain gitone status --json ``` ## 5. Stage, inspect and commit ```bash gitone add README.md src/app.txt notes/plan.txt gitone diff --staged gitone show --repository app --source index -- README.md gitone unstage README.md gitone add -A gitone commit -m "Initial local project" gitone log all -n 3 gitone show --repository app --source head -- README.md gitone status ``` The commit spans both repositories and therefore receives one shared `GitOne-Group` trailer. Exercise updates and undo operations: ```bash printf '\ntemporary README change\n' >> README.md printf '\napplication update\n' >> src/app.txt printf 'new file\n' > src/new.txt printf '\nprivate update\n' >> notes/plan.txt gitone diff gitone diff app gitone add -u gitone diff all --staged gitone restore --staged src/app.txt gitone unstage -A gitone restore README.md gitone add -A printf 'Update local files\n' | gitone commit -F - gitone status ``` `add -u` does not stage the new `src/new.txt`. Neither `unstage` nor `restore --staged` changes working files. `restore README.md` deliberately discards only that unstaged test change. A symbolic link inside one repository is an owned path of its own. GitOne stores the link and never looks through it: ```bash ln -s app.txt src/alias.txt gitone status gitone add src/alias.txt gitone commit -m "Alias the application file" gitone show --repository app --source head -- src/alias.txt ``` `show` prints the stored target `app.txt`, not the file behind it. A link that leaves its repository or the project is refused instead: ```bash ln -s ../notes/plan.txt src/plan.txt gitone add src/plan.txt rm src/plan.txt ln -s /etc/hosts src/hosts gitone add -A rm src/hosts ``` Both must report `PATH003`, naming the link, its stored target and the reason: the first target belongs to `notes`, the second is absolute. ## 6. Create and switch branches ```bash gitone branch gitone branch feature/tutorial gitone switch feature/tutorial printf '\nfeature change\n' >> src/app.txt gitone add src/app.txt gitone commit -m "Change feature branch" gitone log app -n 3 gitone switch main gitone branch gitone status ``` Both managed repositories move together. The feature commit exists only in `app` and is no longer visible after switching back to `main`. Local-only repositories are skipped by fetch and pull; push is rejected because commits exist but no remotes are configured: ```bash gitone fetch gitone pull gitone push --yes gitone recover gitone abort ``` The push error is expected. `recover` and `abort` report that no interrupted operation exists. ## 7. Create a project with disposable remotes ```bash mkdir -p "$GITONE_TUTORIAL_ROOT/remotes" mkdir -p "$GITONE_TUTORIAL_ROOT/remote-project/public" mkdir -p "$GITONE_TUTORIAL_ROOT/remote-project/internal" git init --bare -b main "$GITONE_TUTORIAL_ROOT/remotes/public.git" git init --bare -b main "$GITONE_TUTORIAL_ROOT/remotes/internal.git" cd "$GITONE_TUTORIAL_ROOT/remote-project" ``` Create the committed ownership map. The remotes are written as absolute paths: a real project uses URLs, and a relative path would be resolved from each managed repository below `.gitone/` rather than from the project, which the clone in section 8 could not follow. ```bash cat > .gitone.yml < public/README.md printf '# Internal notes\n' > internal/notes.md gitone repo validate gitone init gitone repo list gitone doctor gitone status ``` Create the first remote history: ```bash gitone add -A gitone commit -m "Initialize remote project" gitone push ``` Press Enter at `Continue? [y/N]` to verify that the default aborts without changing a remote. Then publish explicitly: ```bash gitone push all --yes gitone status ``` ## 8. Clone and synchronize ```bash cd "$GITONE_TUTORIAL_ROOT" gitone clone "$GITONE_TUTORIAL_ROOT/remotes/public.git" cloned-project cd "$GITONE_TUTORIAL_ROOT/cloned-project" gitone repo list gitone log all -n 2 gitone status gitone doctor ``` The public repository bootstraps the project. Its committed `.gitone.yml` then tells GitOne to clone `internal` into the same working directory. Create a grouped update in the original project and publish one repository at a time: ```bash cd "$GITONE_TUTORIAL_ROOT/remote-project" printf '\npublic update\n' >> public/README.md printf '\ninternal update\n' >> internal/notes.md gitone add -u gitone commit -m "Update public and internal data" gitone push public --yes ``` Fetch and fast-forward only `public` in the clone: ```bash cd "$GITONE_TUTORIAL_ROOT/cloned-project" gitone fetch public gitone status gitone pull public gitone show --repository public --source head -- public/README.md ``` Publish and receive the remaining update: ```bash cd "$GITONE_TUTORIAL_ROOT/remote-project" gitone push internal --yes cd "$GITONE_TUTORIAL_ROOT/cloned-project" gitone fetch all gitone pull all gitone status gitone doctor ``` ## 9. Migrate an ordinary Git repository ```bash mkdir -p "$GITONE_TUTORIAL_ROOT/migration/src" mkdir -p "$GITONE_TUTORIAL_ROOT/migration/notes" cd "$GITONE_TUTORIAL_ROOT/migration" git init -b main printf 'legacy application\n' > src/app.txt printf 'legacy notes\n' > notes/plan.txt cat > .gitone.yml <<'YAML' version: 1 default_branch: main repositories: app: visibility: public paths: - .gitignore - .gitone.yml - src/** notes: visibility: private paths: - notes/** YAML git add -A git commit -m "Create legacy project" gitone migrate ``` Press Enter to test the safe default. The root `.git/` must remain. Then migrate and inspect the retained history: ```bash gitone migrate --yes test ! -e .git && echo "Root .git removed" gitone backup list gitone backup git -- log --oneline --all gitone status gitone doctor ``` The managed repositories intentionally start empty. The old history remains only in the verified migration backup, which `gitone backup` lists, reads and - with `gitone backup restore` - copies back to the root `.git/` whenever you want the old repository again. ## 10. Restore the migration backup The backup is not a one-way exit. Restoring it is an ordinary command, it changes nothing but the root `.git/`, and it can be undone again. ```bash cd "$GITONE_TUTORIAL_ROOT/migration" gitone backup restore ``` Press Enter to test the safe default: the answer is `No` and nothing changes. Then restore explicitly: ```bash gitone backup restore --yes git log --oneline --all ``` The old history is back at the project root, with every commit the legacy repository had. Pass `--backup ` from `gitone backup list` as soon as a project has more than one backup. While that root `.git/` exists the ordinary commands step aside, and the backup commands do not: ```bash gitone status gitone add -A gitone backup list gitone backup git -- log --oneline --all ``` The first two report `PATH003 .git: the project root is a Git repository` and exit `1`. The backup is still listed: restoring copies it, it never consumes it, so you can restore the same backup again later. Go back to the GitOne project by removing the root repository you just restored. Nothing else has to be undone - the managed repositories, the configuration and your files were never touched: ```bash cd "$GITONE_TUTORIAL_ROOT/migration" rm -rf .git gitone status gitone doctor ``` ## 11. See it in VS Code ```bash cd "$GITONE_TUTORIAL_ROOT/cloned-project" gitone vscode info --json code . ``` In the opened trusted workspace: - Open Source Control and run **GitOne: Refresh**. - Append a line to `public/README.md` and confirm that it appears automatically. - Open its diff, stage it, unstage it and stage it again. - Enter a message and run **GitOne: Commit & Push**. - Run **GitOne: Show Output**, **Recover Interrupted Operation** and **Abort Interrupted Operation**. Finish in the terminal: ```bash gitone status gitone doctor ``` ## 12. Verify the safety boundary An unassigned file must block status and every mutation: ```bash cd "$GITONE_TUTORIAL_ROOT/cloned-project" printf 'unassigned\n' > unassigned.test gitone status gitone add -A gitone push --yes rm unassigned.test gitone status ``` The first three GitOne commands must report `PATH001`. Unsupported commands must report `CLI001` without changing the project: ```bash gitone reset --hard gitone tag tutorial gitone status ``` ## Completed - [ ] Binary built and installed - [ ] VS Code extension built and installed - [ ] Local setup and every local file operation exercised - [ ] Safe symbolic link committed, unsafe ones refused - [ ] Branches created and switched synchronously - [ ] Push, clone, fetch and pull exercised against two remotes - [ ] Ordinary Git repository migrated with a verified backup - [ ] Migration backup restored and the restored root repository removed again - [ ] VS Code Source Control workflow exercised - [ ] Path ownership and unsupported-command safety verified The fixtures remain below `$GITONE_TUTORIAL_ROOT` for inspection. Delete that single disposable directory when finished. --- # GitOne error codes Every GitOne failure prints its code as the first token of the message. Each one below states what it means, what usually causes it and what to do next. Nothing here is a warning - a code means the operation stopped and your repositories were not changed. ### Configuration #### CONFIG001 - configuration missing Usually: Neither this directory nor any parent contains a `.gitone.yml`, so there is no GitOne project here. A project is always the nearest directory above you that has one. Do this: Run `gitone setup` to create a project, or change into a directory inside an existing one. #### CONFIG002 - invalid YAML Usually: The file parses as broken YAML, contains duplicate keys, holds several YAML documents, or has a field GitOne does not know. Unknown fields are errors on purpose - a typo must not be silently ignored. Do this: The message names the line. Fix it and run `gitone repo validate` until it passes. #### CONFIG003 - invalid configuration Usually: The YAML is well-formed but the configuration is not usable: a duplicate ownership pattern, an unsupported pattern such as `src/[ab].md` or `docs/**/**/*.md`, a repository name outside `[a-z][a-z0-9_-]*`, the reserved name `all`, a `version` other than `1`, a repository with no `paths`, `remote` and `remotes.origin` disagreeing, or a `default_branch` or `remotes` key that native Git itself refuses. Do this: The message names the exact conflict. Patterns are exact paths like `README.md`, `*` inside one path segment and at most one `**` segment, as in `src/**`, `docs/**/*.md` or `**/*.md`. `default_branch` and every remote name are checked with `git check-ref-format` before anything is created, so `bad branch`, `-dash` and `feature.lock` are rejected here rather than during `gitone init`. ### Paths and ownership #### PATH001 - path is not assigned to any repository Usually: A relevant file matches no ownership pattern in any repository. This is the most common GitOne error and it is the tool doing its job: it does not know where the file belongs, so it stops. Do this: Add the path to the `paths` of exactly one repository - `.gitone.yml` for public, `.gitone.local.yml` for private - or add it to `.gitignore` if it should not be tracked at all. #### PATH002 - path is assigned to more than one repository Usually: A file matches ownership patterns in two repositories at once. There is no priority rule and no most-specific-match rule, so GitOne refuses instead of picking one. Non-identical patterns may overlap in the configuration; the conflict appears when a concrete path matches both. Do this: Adjust or remove one of the reported ownership patterns so the path matches exactly one repository, then rerun the command. #### PATH003 - path is otherwise unsafe Usually: A path is protected by `rules.protected_paths`, or it is something GitOne will not manage: a nested or bare Git repository, a non-regular file, a name containing ASCII control characters, a case-insensitive collision with another path, or one of the reserved paths `.gitone/` and `.gitone.local.yml` reaching an index or a commit. A symbolic link also lands here unless it is a safe internal alias like `.claude/skills -> ../.agents/skills`: a relative target that resolves inside the project, exists as a regular file or a real directory, is reached without passing through another link, and belongs to the same repository as the link. Absolute, escaping, dangling, empty, cross-repository, chained and cyclic links are refused, as is any link that is or reaches `.gitone.yml`, `.gitone.local.yml`, `.gitignore`, `.gitone/`, `.git/` or a protected path. Do this: The message states which of these it is, and for a link it names the link, its stored target and the concrete reason. A protected path is a hard deny that ownership never overrides - move the file or remove the protection deliberately. A link and its target must be selected and changed together, so a partial `add`, `unstage` or `restore` is refused and nothing changes. ### Repositories #### REPO001 - repository invalid or unavailable Usually: The configuration is valid but the managed repositories are not usable: none is initialized yet, some are missing, the metadata below `.gitone/repositories/` is not a GitOne repository, the repositories are on different branches, or a named target has no configured `origin`. Do this: The message names the command that makes the project usable - `gitone init` on a fresh project, `gitone migrate` when a root `.git/` exists, `gitone switch ` when the branches disagree, and `gitone doctor` for anything else. #### GIT001 - native Git failure Usually: The underlying `git` command failed and GitOne is reporting it rather than interpreting it. Authentication, a failing hook, a full disk, a missing object, an unreadable configuration. Do this: The message contains the exact `git` invocation and its output. Run that command yourself to see the full picture. #### BRANCH001 - branch listing or creation refused or failed Usually: The name breaks native Git branch-name rules, a repository has no commit on its current branch, a repository is not on a branch, the repositories are on different branches, or one already has the branch. Do this: Every problem is reported at once and nothing was changed. Commit first if a repository is unborn; use `gitone switch` to put all repositories on the same branch. #### SWITCH001 - switch refused or failed Usually: A repository does not have the branch, is not on a branch, has staged or unstaged changes, the target tree contains a path the repository does not own, or the checkout would overwrite an existing working-tree file. Do this: Commit or undo your changes first - GitOne never stashes, discards or merges to make a switch possible. Create a missing branch with `gitone branch `. ### Remote operations #### PUSH001 - push preflight failed Usually: One of the checks before the first remote is touched failed: a `push: disabled` repository was named explicitly, `require_clean_worktree` is set and the tree is dirty, `origin` is missing or has different fetch and push URLs, the remote is unreachable, the push would not be a fast-forward, an outgoing commit or the resulting tree contains a path the repository does not own - or there is no terminal to confirm on and `--yes` was not passed. Do this: Nothing was pushed. For the non-interactive case add `--yes`. For a non-fast-forward, fetch and reconcile the branch yourself before retrying. #### FETCH001 - fetch failed for one or more repositories Usually: A remote could not be contacted or refused the ref listing - authentication, transport, an unreachable host. Do this: A fetch across repositories is not atomic. The report above the error names which repositories already updated their remote-tracking refs; rerun `gitone fetch` to retry the rest. #### PULL001 - pull refused or failed Usually: A repository that would move has staged or unstaged changes, its branch does not track exactly `origin/`, the remote-tracking branch is missing after the fetch, the branch has diverged so no fast-forward is possible, or the incoming tree contains a path the repository does not own. Do this: A pull is fast-forward only and no flag changes that. Resolve a diverged branch with plain Git yourself, then return to `gitone pull`. #### CLONE001 - clone refused or failed Usually: The destination already exists, the bootstrap repository has no committed `.gitone.yml` on its default branch, more than one or no repository owns that file, the owning repository’s configured `origin` is not exactly the URL you cloned, a configured `origin` lacks the default branch, or an incoming tree contains a path its repository does not own. Do this: Nothing was created and no existing path was changed. Clone the URL the committed configuration names, and clone into a path that does not exist yet. ### Locking, recovery and setup #### LOCK001 - another GitOne operation is currently running Usually: Only one mutating operation runs at a time. The message names the process ID and the command holding the lock. Do this: Wait for it to finish and retry. `gitone doctor` and `gitone repo validate` are read-only and work while the lock is held. #### REC001 - an interrupted operation must be recovered first Usually: A previous add, unstage, restore, commit, branch, switch or pull was interrupted and left recorded state under `.gitone/recovery/`. GitOne refuses to start anything new on top of it. Do this: Run `gitone recover` to finish it, or `gitone abort` to undo it. Never delete `.gitone/recovery/` by hand. For an interrupted push, both commands only report what actually reached the remotes. #### MIG001 - migration refused, failed or interrupted Usually: The project is not a plain root repository - a `.git` file rather than a directory, linked worktrees, submodules, a bare repository, a nested repository, an already migrated project - or a previous migration was interrupted. `gitone backup` also reports a missing, ambiguous, unknown, unsafe or invalid backup selection and every refused or failed restoration with this code. Do this: For an interruption, `gitone migrate` offers to restore the verified backup instead of migrating again. For a backup command, run `gitone backup list` and pass the full ID as `--backup `; a restore additionally needs the root `.git` path to be absent. The backup is always kept; it is never moved or consumed. #### SETUP001 - interactive setup refused Usually: `gitone setup` asks questions, so it needs an interactive terminal on standard input. It also refuses inside another GitOne project or another Git repository, on an invalid existing configuration, on an interrupted migration, or on repository state under `.gitone/` that neither init nor migrate explains. Do this: Run it in a real terminal. To set a project up non-interactively, write `.gitone.yml` yourself and run `gitone init` or `gitone migrate`. #### AGENT001 - AGENTS.md missing, not current or not safely editable Usually: `gitone agents` found no `AGENTS.md`, one without the GitOne instructions, or a marked block that is no longer the current text. `gitone agents update` reports it too when the file is one GitOne must not edit by itself: a symbolic link or another non-regular file, recognizable GitOne text that is neither marked nor exactly the current block, duplicate blocks, marked and unmarked instructions together, or missing and repeated markers. Do this: Run `gitone agents update` for a missing, incomplete or outdated block. For an unclear file, edit it by hand until it holds exactly one block between `` and ``, then run `gitone agents` again. ### Command line #### CLI001 - unsupported command or flag Usually: GitOne runs an allowlist and forwards nothing to `git`. `reset`, `tag`, `merge`, `rebase`, `stash`, Git pathspecs, globs, `--` separators and most Git flags are rejected here - as are `-h`, `gitone help`, and any argument after `--help`. Bare `gitone` and a command group called without a subcommand are not refusals: they print their help and exit `0`. Do this: Every refusal is followed by one hint line naming the page that lists the accepted form - `gitone --help` when the input names a known command, `gitone --help` otherwise. Do not work around it by falling back to plain `git`: that bypasses every ownership check. #### SHOW001 - requested path absent from HEAD or index Usually: The request was valid but the named path does not exist in the selected source. This exits `2`, not `1`, so an editor integration can tell an absent version from a broken command. Do this: Usually nothing is wrong: a file that is untracked has no HEAD version, and a file that is not staged has no index version. #### VSCODE001 - VS Code CLI or extension installation failed Usually: The `code` command is not on `PATH`, the named VSIX does not exist, the extension is already installed and `--force` was not passed, or VS Code’s own CLI reported a failure. Do this: Install a downloaded VSIX with `gitone vscode install --vsix `. A completed project setup stays in place - only the optional extension step failed.