Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Whole-upstream tutorial** - Added a second tutorial showing how to vendor an entire upstream repository into a local directory with `remote:.` or `remote:/`.
- **Claude sandbox starter** - Added `sbx-kits/claude` so repo-local sandbox examples now cover both OpenCode and Claude.
- **Private fork migration tutorial** - Added a guarded tutorial for migrating an existing private fork or derivative repo to a repo-root `git-cross` patch with explicit backup and override review steps.
- **Repo split tutorial** - Added `docs/tutorials/split-repo-into-upstream-and-private.md`, a concise walkthrough for splitting one original repo into a clean secret-free upstream plus a private `git-cross`-managed fork. Uses a single shared rule: one `.crossignore` and one `git ls-files --ignored --exclude-from=.crossignore` selection piped to `git rm` (remove from upstream) or `rsync` (back up and keep), so the upstream and the fork can never disagree about what is private. The selection command is documented as a repeatable preview (refine `.crossignore` in a loop until it lists exactly the private files) and clarified as non-destructive — the seed removal only strips the throwaway upstream copy while private files stay in the fork as git-cross overlays. The upstream-seed removal deletes the tracked-only selection (`--cached`, without `--others`) with `rm -rf` followed by `git add -A`, instead of `git rm`: `git rm` aborts the whole batch on untracked/gitignored pathspecs (`node_modules/`, `.envrc.local`) and on submodule/embedded-repo gitlinks (`could not lookup name for submodule ...`), whereas working-tree deletion + restage handles files, directories, and gitlinks uniformly. Documented that `rm` has no `--dry-run` (preview via `xargs … echo` or confirm via `rm -rI`/`-ri` with `xargs -o`), and that `test/` matches a `test` directory at any depth while `*/test/*` does not. The backup keeps `--others` so untracked private files are still copied. Each step is labelled with the repo it runs in.

### Changed
- **`.crossignore` entry syntax** - Override review behavior now uses plain non-comment `.crossignore` lines such as `.env` or `config/private` instead of `!override <path>` markers. Wildcard pattern matching is still not supported.
- **`.crossignore` entry syntax** - Override review behavior now uses plain non-comment `.crossignore` lines. Supported forms include basename entries such as `.env` anywhere under the patch, basename globs such as `*.env`, and directory entries such as `config/`. Full `.gitignore` semantics are still not supported.
- **README workflow guidance** - Documented the current `.crossignore` behavior as a review-oriented workflow for local overlay files layered on top of upstream-managed content.

### Fixed
- **Migration tutorial backup command** - Removed the broken recommendation to call the internal Just `_crossignore_overrides` recipe during private-fork migration. The backup step now expands `.crossignore` (which is a gitignore-style *pattern* prescription, not a literal file list) against the real working tree with `git ls-files --ignored --exclude-from=.crossignore` piped into `rsync`, so globs and not-yet-present paths no longer cause `rsync --files-from` "No such file or directory" failures. Documented the safer "clean upstream seed repo first, then patch into the private repo" workflow.
- **Rust override diff build failure** - Renamed a Rust loop variable that used the reserved keyword `override`, restoring Rust CLI builds and regression execution on current toolchains.
- **Just override warning output** - Removed a shell-breaking semicolon from the override review warning so `just cross diff` prints the manual review commands correctly.
- **P0: Sparse checkout broken on newer Git versions** - `git sparse-checkout set <path>` in `--no-cone` mode no longer reliably checks out directories without trailing `/`. Fixed across all three implementations (Go, Rust, Justfile.cross) by appending `/` to sparse-checkout patterns and using `git read-tree -mu HEAD` instead of bare `git checkout` (which can no-op after `--no-checkout`).
Expand Down
48 changes: 47 additions & 1 deletion Justfile.cross
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ update_crossfile +cmd:
grep -qF "{{cmd}}" "{{CROSSFILE}}" 2>/dev/null || echo "{{cmd}}" >> "{{CROSSFILE}}"; exit 0

[no-cd]
_crossignore_overrides local_dir:
_crossignore_patterns local_dir:
#!/usr/bin/env fish
set -l file "{{local_dir}}/.crossignore"
if not test -f "$file"
Expand All @@ -153,6 +153,52 @@ _crossignore_overrides local_dir:
end
end < "$file"

[no-cd]
_crossignore_match pattern rel_path:
#!/usr/bin/env fish
set -l normalized (string trim -r -c '/' -- "{{pattern}}")
if test -z "$normalized"
exit 1
end
if string match -q '*/*' -- "$normalized"
if test "{{rel_path}}" = "$normalized"; or string match -q -- "$normalized/*" "{{rel_path}}"
printf '%s\n' "$normalized"
exit 0
end
exit 1
end

set -l parts (string split / -- "{{rel_path}}")
for idx in (seq (count $parts))
if string match -q -- "$normalized" "$parts[$idx]"
string join / $parts[1..$idx]
exit 0
end
end
exit 1

[no-cd]
_crossignore_overrides local_dir:
#!/usr/bin/env fish
set -l patterns (just cross _crossignore_patterns "{{local_dir}}")
if test (count $patterns) -eq 0
exit 0
end

set -l matches
set -l entries (find "{{local_dir}}" -mindepth 1 -not -path '*/.git/*' -print0 | string split0)
for line in $entries
set -l rel (realpath --relative-to="{{local_dir}}" "$line")
for pattern in $patterns
set -l match_path (just cross _crossignore_match "$pattern" "$rel" 2>/dev/null)
if test -n "$match_path"
contains -- "$match_path" $matches; or set matches $matches "$match_path"
break
end
end
end
printf '%s\n' $matches

# Internal: Log message with color
_log level +message:
#!/usr/bin/env fish
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ compose.override.yaml
EOF
```

Rule of thumb:

- a plain basename entry such as `.env` matches that name in any subdirectory under the patch
- a directory entry such as `config` or `config/` matches that directory tree
- a basename glob such as `*.env` also matches anywhere under the patch
- this is intentionally simpler than full `.gitignore` semantics

What this does today:

- `git cross status` shows `Override` for that patch
Expand Down Expand Up @@ -166,6 +173,7 @@ This lets your main repo keep its local opinionated result while the upstream co
- [Tutorial: Local Overlays And Upstream Contribution](docs/tutorials/local-overlays-and-upstream.md)
- [Tutorial: Whole Upstream Into A Local Directory](docs/tutorials/whole-upstream-into-local-dir.md)
- [Tutorial: Migrate A Private Fork To git-cross](docs/tutorials/migrating-private-fork-to-git-cross.md)
- [Tutorial: Split One Repo Into A Clean Upstream And A Private Overlay](docs/tutorials/split-repo-into-upstream-and-private.md)
- [Sandbox Kits](sbx-kits/README.md)

## Commands
Expand Down
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

## Completed Tasks

- [x] Fix the private-fork migration tutorial so it no longer points users at the internal `_crossignore_overrides` Just recipe, and document the cleaner "seed a public upstream, then track it from the private repo" migration path.
- [x] Add a standalone tutorial (`docs/tutorials/split-repo-into-upstream-and-private.md`) for splitting one mixed private repo into a clean upstream plus a private git-cross-managed overlay, cross-linked from README and the migration tutorial.
- [x] Refactor remaining Rust commands to use `duct` for better error visibility.
- [x] Complete `push` command verification in native implementations.
- [x] Integrate integration tests (001-009) into a unified test runner.
Expand Down
137 changes: 117 additions & 20 deletions docs/tutorials/migrating-private-fork-to-git-cross.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,53 @@ Assume:
- your private repo also contains local-only files such as `.env`, `.env.local`, `docker-compose.override.yml`, or private config directories
- you want the repo to stay at repo root, not move upstream content into `vendor/...`

## Before Step 1: Decide Where The Upstream Comes From

There are two valid starting points.

### Case A: A Clean Upstream Repo Already Exists

If you already have a real upstream repository that contains only the shareable code, continue with Step 1.

### Case B: Your Current Private Repo Is The Only Copy

If the current private repo is the only place where the code exists and it mixes:

- upstream-worthy project files
- private files such as `.env`
- machine-local or company-local overrides

create a clean upstream seed repository first, then come back and migrate the private repo with `git-cross`.

This split has its own focused walkthrough:
`docs/tutorials/split-repo-into-upstream-and-private.md`.

The core idea there is a single shared rule: one `.crossignore` lists the private
patterns, and `git ls-files` selects the matching files for both jobs —

```bash
# back up / keep (rsync): include untracked too
git ls-files -z --cached --others --ignored --exclude-from=.crossignore

# remove from upstream seed (delete from copy): tracked only
git ls-files -z --cached --ignored --exclude-from=.crossignore
```

- the tracked list, deleted with `rm -rf` then `git add -A`, strips those files from the new upstream seed
- the full list piped to `rsync` in your private repo *backs up and keeps* them

Use `rm` + `git add -A` rather than `git rm` for the seed: `git rm` aborts the
whole batch on untracked pathspecs or on a submodule it cannot name. Deleting from
the working tree and restaging handles files, directories, and submodule gitlinks
uniformly.

Do the split there, then return here at Step 1 with a clean upstream in hand.

Important note:

- if secrets or private files were ever committed into Git history and you plan to publish that history, do a real history rewrite first
- the split tutorial only covers the working-tree split, not secret-history cleanup

## Step 1: Make A Safety Snapshot

Before changing anything, create a backup branch or tag.
Expand All @@ -63,7 +110,7 @@ The safest migration is done in a fresh clone of your private repo.

That way, if the first root patch is not what you expected, you can discard the whole working copy.

## Step 3: Inventory Local-Only Files
## Step 3: Inventory Local-Only Files And Draft `.crossignore`

Make a list of files that must stay private or local-only.

Expand All @@ -75,52 +122,105 @@ Typical examples:
- `config/private/`
- machine-local certificate files

For current shipped behavior, define them as explicit `.crossignore` entries.
For current shipped behavior, write them into `.crossignore` first.

Current parsing rules are simple:

- each non-empty, non-comment line is treated as one literal override path
- each non-empty, non-comment line is one override pattern
- a plain basename entry such as `.env` matches that name in any subdirectory under the patch
- plain entries such as `.env` or `config/private` are supported
- wildcard forms such as `*.env` or `config/*` are **not** supported today
- basename globs such as `*.env` are supported anywhere under the patch
- directory entries such as `config` or `config/` are supported
- full gitignore semantics are **not** supported today

Example list:

```text
.env
.env.local
*.env
docker-compose.override.yml
config/private
config/
```

Examples that are **not** currently supported as patterns:
Examples that are still **not** promised as full gitignore-style patterns:

```text
*.env
config/*
**/*.env
!negation
```

## Step 4: Copy Local-Only Files Out Of The Repo

Before the first `git-cross` root patch, copy those files outside the repository.

Example:
`.crossignore` is a pattern prescription, not a literal file list. Some lines
are globs (`*.env`) or directories (`config/`), and some patterns may not match
any file that exists yet. So you cannot feed it straight into
`rsync --files-from`; that treats every line as an exact path and fails on the
first pattern or missing file.

Instead, expand the patterns against the real working tree with `git`, and back
up only the files that actually exist. This needs only `git` and `rsync`, both
already required by `git-cross`:

```bash
mkdir -p ../private-overrides-backup
cp .env ../private-overrides-backup/ 2>/dev/null || true
cp .env.local ../private-overrides-backup/ 2>/dev/null || true
cp docker-compose.override.yml ../private-overrides-backup/ 2>/dev/null || true
cp -R config/private ../private-overrides-backup/ 2>/dev/null || true
git ls-files -z --cached --others --ignored --exclude-from=.crossignore \
| rsync -av --from0 --files-from=- ./ ../private-overrides-backup/
```

Why this works:

- `git ls-files --ignored --exclude-from=.crossignore` matches the
gitignore-style patterns against files that exist, so missing patterns are
silently skipped instead of erroring
- `--cached --others` covers both tracked secret files (a committed `.env`) and
untracked ones (an uncommitted `.env.secrets`)
- `-z` / `--from0` handle spaces and unusual filenames safely
- `--files-from` preserves the relative paths, so restore in Step 7 is a plain
`rsync -av ../private-overrides-backup/ ./`

This step is repeatable. The command is a preview until you pipe it anywhere, so
refine `.crossignore` in a loop: run the preview, read the list, edit
`.crossignore`, run again — repeat until it lists exactly the private files you
want removed from upstream and kept locally.

```bash
# edit .crossignore, then re-run until the list is exactly right
git ls-files --cached --others --ignored --exclude-from=.crossignore
```

Nothing is deleted from your repo here. The backup is only a safety copy; your
private files stay in place and become git-cross overlays after the root patch.
Only the separate upstream seed (a throwaway copy) has them stripped.

Do not rely on `Justfile.cross` internal helper recipes such as `_crossignore_overrides` for this migration step. Those recipes are implementation internals, not stable user-facing commands, and they are not meant to be invoked directly from another repository.

If you prefer to be fully explicit, list exact paths instead:

```bash
mkdir -p ../private-overrides-backup
rsync -avR ./.env ./config/private ../private-overrides-backup/ 2>/dev/null || true
```

If a file is sensitive, verify that your backup location is safe.

Advanced alternatives if you prefer them:

- create a tar archive of the private files before migration
- temporarily move local-only files out through Git history or branch surgery tools before the root patch

Those approaches are more invasive. The external backup copy is still the simplest migration checkpoint.

## Step 5: Register The Upstream Remote

If you want upstream contribution later, the cleanest pattern is to register a writable fork from the start.

If you only want to mirror the original upstream first, register the original upstream now and switch to a fork later.

If you created a clean upstream seed repository in the earlier split step, register that repository here.

Example:

```bash
Expand Down Expand Up @@ -150,24 +250,21 @@ Restore the local-only files you copied out earlier.
Example:

```bash
cp ../private-overrides-backup/.env . 2>/dev/null || true
cp ../private-overrides-backup/.env.local . 2>/dev/null || true
cp ../private-overrides-backup/docker-compose.override.yml . 2>/dev/null || true
cp -R ../private-overrides-backup/private ./config/ 2>/dev/null || true
rsync -av ../private-overrides-backup/ ./ 2>/dev/null || true
```

Then write `.crossignore`:

```bash
cat > .crossignore <<'EOF'
.env
.env.local
*.env
docker-compose.override.yml
config/private
config/
EOF
```

Use explicit literal entries even when it feels repetitive. The current code treats `.crossignore` here as a small override registry, not as full gitignore-style pattern matching.
Use simple explicit patterns. The current code treats `.crossignore` here as a small override matcher, not as full gitignore-style pattern matching.

## Step 8: Review The Migrated State

Expand Down
Loading