SVN → Git Migration
Agent instructions — read this before every action:
You ORCHESTRATE the migration. Every phase runs as a background subagent via the
tasktool — you never runpowershellphase snippets yourself. Your context is for orchestration only.Windows execution model (critical — applies inside every subagent):
powershell -File script.ps1→ ALWAYS BLOCKED by Group Policy. Never use this.powershell -Command "..."→ Never blocked. Always use this.- Inline technique: read
lib/core.pslibas text, wrap in[scriptblock]::Create(), dot-source it — no-File, no ExecutionPolicy issue.For every Windows phase: Tell a
tasksubagent the runbook path (e.g.phases/phase-0.md) and the placeholder values. The subagent reads the file, fills in values, and executes the snippet. The orchestrator never opens a runbook file.macOS / Linux: Use
bash scripts/migrate.sh run <url> ...— no policy constraint. Run it via the bash tool (or delegate to a subagent).State between phases: Each phase saves its output to
$target\.svn2git\*.json. The next phase reads it. This is how cross-phase state (sha-index, refs, config) survives across separatepowershell -Commandinvocations.
git-svn is no longer bundled with Git for Windows (and macOS Homebrew dropped it too). This skill recreates the full
git svn clone behaviour using only the svn CLI and standard git plumbing commands (hash-object, write-tree,
commit-tree, update-ref, update-index). No Perl, no native extensions required.
Platform support:
- Windows — agent runs each phase inline via
powershell -Command; state persisted as JSON between phases - macOS / Linux — agent runs
migrate.sh rundirectly via bash
⚠️ macOS users: The system bash is version 3.2 and will not work. Install bash ≥ 4 first:
brew install bash. The script checks at startup and aborts with instructions if the version is too old.
Prerequisites
Verify these before starting:
svn --version # any modern version (TortoiseSVN, SlikSVN, Homebrew, distro pkg)
git --version # 2.x or later
python3 --version # 3.x — used for XML parsing in the bash script
macOS only:
bash --version # must be 4.0 or later
# If not: brew install bash
Files (relative to the skill directory):
- macOS / Linux:
scripts/migrate.sh - Windows:
lib/core.pslib— function library loaded inline by the phase runbooks (never run directly)
Step 1 — Detect
Agent: Delegate detect to a
tasksubagent — pass the skill directory path and the current working directory. The subagent readsphases/detect.md, runs the detection commands, and returns the structured result: SVN URL, layout, authors, head revision.Once the subagent returns, immediately present the interview questions from Step 2 to the user in a single message — do not wait for a follow-up prompt. Include in that message:
- A summary of what detect found (URL, layout, author count, head revision)
- All interview questions that are not already answered by the detect output
- A pre-filled suggestion for each question where detect gave enough information
Only ask the user for the URL if the subagent reports it could not be auto-detected.
Step 2 — Interview: migration options
Work through these decisions in order based on what the svn info / svn log output showed.
a. Repository layout
What layout does your SVN repository use?
| Layout | Description | Flag to use |
|---|---|---|
| Standard | Has trunk/, branches/, tags/ at the top level |
--stdlayout |
| Custom paths | Different names (e.g. main/, rb/, rel/) |
-T <trunk> -b <branches> -t <tags> |
| Single path | No branches or tags — just one linear history | (no flag) |
The detect output will suggest the likely layout. You can always override it.
b. Author mapping
Do you want real names and email addresses in git commits?
SVN commits store only a username (e.g. jsmith). Without a mapping, each commit author defaults to
jsmith@<repo-uuid> — fine for private repositories, but not ideal for public ones.
To provide a mapping, create an authors.txt file (see assets/authors.template.txt):
jsmith = Jane Smith <jane.smith@example.com>
bwilliams = Bob Williams <bob@example.com>
(no author) = Unknown Committer <unknown@example.com>
The detect output lists every SVN author that appears in the log — use that list to build your file. Ask me to
generate a stub authors.txt from that list if you'd like.
⚠️ If the detect output shows
AUTHOR_SCAN_COMPLETE: false, the full log scan timed out. Some older committers may be missing from the list. You can get the complete set by running locally:svn log --xml -q <svn-url> | Select-String '<author>' | ForEach-Object { ($_ -replace '.*<author>(.*)</author>.*','$1').Trim() } | Sort-Object -UniqueAny SVN username not in
authors.txtwill default tousername@<repo-uuid>and will be listed as "Unmapped SVN authors" in the Phase 8 summary.
Pass the file with: --authors-file authors.txt
c. Metadata trailer
Keep git-svn-id in each commit message?
Each commit will end with a line like:
git-svn-id: https://svn.example.com/repos/myproject/trunk@42 a1b2c3d4-e5f6-...
- Default (recommended): keep it — preserves SVN revision traceability. You can always strip it later with
git filter-repo. - To omit: add
--no-metadata
d. Path filtering
Are there directories you want to exclude — generated code, large binaries, IDE files?
Use --ignore-paths <regex> with a regex matched against ref-root-relative paths.
Example — exclude a generated directory and all .class files:
--ignore-paths "(^generated/|\.class$)"
⚠️ Migrating from git-svn?
git-svnmatched--ignore-pathsagainst repo-root-relative paths (e.g./trunk/generated/). This skill matches against ref-root-relative paths (e.g.generated/). Adjust your patterns accordingly. Seereferences/git-svn-mapping.mdfor the full mapping.
Use --include-paths <regex> to do the inverse (whitelist).
e. Tag handling
Clean tags (directories that were never modified after the SVN copy) automatically become annotated git tags. Dirty tags
(modified after creation) automatically become branches named tags/<tagname>, with a warning printed in the summary.
Override with --tags-as-branches to turn all SVN tags into branches regardless of cleanliness.
f. Default branch name
What should the trunk become in git?
| Choice | Flag |
|---|---|
main (default) |
(omit flag) |
master |
--default-branch master |
trunk |
--default-branch trunk |
g. svn:ignore → .gitignore conversion
The skill reads svn:ignore properties from the SVN repository and writes .gitignore files into the git repository,
then creates a final commit on each ref. Recommended: keep the default (enabled).
Variable to set in Phase 0: $noCreateIgnore
| User wants | Set $noCreateIgnore to |
|---|---|
Create .gitignore files (default) |
$false |
Skip .gitignore creation |
$true |
⚠️ The variable is negated —
$false= enabled,$true= disabled. Do not confuse with a$createIgnoreflag.
h. Target directory
Where should the git repository be created?
Default: ./repo-name (derived from the SVN URL). Override with --target <path>.
i. Revision range
Migrate all revisions (default), or a specific range?
- All revisions: (omit flag) — equivalent to
--revision 1:HEAD - Specific range:
--revision N:M - Single revision:
--revision N
For very large repositories (> 5 000 revisions), consider migrating in chunks and re-running with a later range. See
references/troubleshooting.md for guidance.
Step 3 — Run the phases
Agent: For each phase, launch a
tasksubagent with the runbook path and placeholder values — do not open the runbook file yourself. The subagent reads it, fills in placeholders, and runs the snippet.
Windows runs the migration as individual phases — this works regardless of ExecutionPolicy, gives visibility into
progress, and allows resuming if interrupted. macOS/Linux uses a single run command (no ExecutionPolicy constraint
there).
macOS / Linux
One command does everything:
bash scripts/migrate.sh run https://svn.example.com/repos/myproject \
--stdlayout \
--authors-file authors.txt \
--default-branch main \
--target ./myproject-git
Tell me your answers to the interview questions and I'll produce the exact command.
Windows — Phase-by-phase (always use this)
For each phase, launch a task subagent with the runbook path and the three placeholder values. Phases 1–8 all use the
same runbook (phases/run-phase.md) — only $phase changes.
Phase: —
- Runbook file:
phases/detect.md - Name: Detect
- What it does: Gather SVN repo info before the interview
- Runbook file:
Phase: 0
- Runbook file:
phases/phase-0.md - Name: Preflight + config
- What it does: SVN reachability, writes
config.json, copieslib/core.pslibto state dir
- Runbook file:
Phase: 1 ⟳
- Runbook file:
phases/run-phase.md—$phase='1' - Name: Git init
- What it does: Creates the git repository — parallel with 2 & 3
- Runbook file:
Phase: 2 ⟳
- Runbook file:
phases/run-phase.md—$phase='2' - Name: Author map
- What it does: Validates the authors file — parallel with 1 & 3
- Runbook file:
Phase: 3 ⟳
- Runbook file:
phases/run-phase.md—$phase='3' - Name: Resolve refs
- What it does: Enumerates SVN branches/tags, writes
refs.json— parallel with 1 & 2
- Runbook file:
Phase: 4
- Runbook file:
phases/run-phase.md—$phase='4' - Name: Convert trunk
- What it does: Replays trunk revisions as git commits (long-running)
- Runbook file:
Phase: 5
- Runbook file:
phases/run-phase.md—$phase='5' - Name: Convert branches
- What it does: Replays branch revisions (may be long)
- Runbook file:
Phase: 6
- Runbook file:
phases/run-phase.md—$phase='6' - Name: Convert tags
- What it does: Creates annotated git tags or dirty-tag branches
- Runbook file:
Phase: 7
- Runbook file:
phases/run-phase.md—$phase='7' - Name: Post-processing
- What it does:
svn:ignore→.gitignore, set HEAD, rungit gc
- Runbook file:
Phase: 8
- Runbook file:
phases/run-phase.md—$phase='8' - Name: Summary
- What it does: Prints the migration report
- Runbook file:
⟳ = phases 1, 2, 3 can run in parallel — launch all three simultaneously, wait for all three before Phase 4.
If a phase fails, fix the problem and re-run that phase — earlier phases don't need to be repeated. The SHA index and
state are preserved in $target\.svn2git\.
Step 4 — Execute
Agent — orchestration model (critical):
Every phase runs as a background subagent via the
tasktool (agent_type: "task",mode: "background"). You are the orchestrator — you read runbooks, fill in placeholders, launch subagents, read results, and report progress to the user. You never runpowershelltool calls yourself for phase execution.Why: Keeps your context small. Phase output (potentially hundreds of lines of SVN progress) goes into subagent context, not yours. You only see the summary returned by the subagent.
Phases 4, 5, 6 — detached mode + polling (mandatory): These phases can run for hours. A subagent call that stays open that long dies when its session token expires (
403/ "token expired or invalid") — a long-lived request can't be refreshed from the inside. Detached processes don't have this problem: they survive independently of the tool call, the subagent, and even the orchestrator's own session. So for phases 4–6, launch via the detached mode launcher inphases/run-phase.md(a short subagent call that starts a background process and returns immediately), then pollprogress.json/phase-<N>.status.jsonevery 2–5 minutes with short subagent calls — never hold one call open waiting for the phase to finish. If corporate policy blocks spawning detached processes, use the chunked fallback instead: run the foreground snippet with$maxMinutesset (e.g.20) and relaunch until the phase reportssuccess. Any phase can be safely re-run — already-converted revisions/refs are skipped via the SHA index, so a relaunch after a checkpoint or an error always resumes rather than restarting.How to launch a phase as a subagent:
Phase 0:
task(prompt="Read phases/phase-0.md in skill dir <skillDir>. Fill in all interview values. Run the PowerShell snippet.")Phases 1, 2, 3, 7, 8:
task(prompt="Read phases/run-phase.md in skill dir <skillDir>. Set $skillDir='<skillDir>', $stateDir='<stateDir>', $phase='<N>'. Run the foreground PowerShell snippet.")Phases 4, 5, 6:
task(prompt="Read phases/run-phase.md in skill dir <skillDir>. Set $skillDir='<skillDir>', $stateDir='<stateDir>', $phase='<N>'. Run the detached-mode launcher snippet and report the LAUNCHED line.")Then poll with separate short subagent calls per the polling protocol in that runbook.
Only 3 substitutions for phases 1–8:
$skillDir(constant),$stateDir(from Phase 0),$phase(the number). No assembling 30-line templates.Model selection for phase subagents: Phases 1–8 are mechanical — the subagent reads a runbook and runs a fixed PowerShell snippet. Heavy reasoning is not needed. Always use the lightest model available in the user's setup:
- Claude Haiku if available (
claude-haiku-4.5)- GPT-4.1 mini / GPT-4o mini as an alternative
- Fall back to the user's default only if no lightweight model is configured
Pass
model: "claude-haiku-4.5"(or equivalent) on everytask()call for phases 1–8. Phase 0 and the Detect step may use the default model as they require light reasoning to fill placeholders.The subagent reads the runbook, fills placeholders, and executes — the orchestrator never reads runbook content itself.
Runbook access: The subagent reads its own runbook — the orchestrator only passes the skill directory path and the placeholder values gathered from the interview. The orchestrator never reads runbook content.
Always pass absolute paths: Subagents run in an unknown working directory. Always resolve
$targetand$authorsFileto absolute paths before passing them — use the CWD reported by the detect subagent as the base for any relative path the user provided.
Execution order:
Step: Detect
- What: Auto-detect SVN repo
- How: Subagent reads
phases/detect.md— returns URL, layout, authors, head revision
Step: Phase 0
- What: Preflight + config
- How: Subagent reads
phases/phase-0.md— 15+ placeholder values from interview. CaptureCANONICAL_STATEDIR=andCANONICAL_TARGET=from its output — these are$stateDirand$targetfor all subsequent phases.
Step: Phases 1 + 2 + 3
- What: Git init, Author map, Resolve refs
- How: 3 subagents in parallel — each reads
phases/run-phase.mdwith$phase='1','2','3'. Only 3 substitutions:$skillDir,$stateDir,$phase. Use lightest available model.
Step: Phase 4
- What: Convert trunk
- How: Subagent reads
phases/run-phase.mdwith$phase='4'. Before launching, tell the user: "Phase 4 is starting — this replays every trunk revision and can take minutes to hours." Launch detached, then poll (see orchestration note above) — never a single blocking call. Use lightest available model. (long-running)
Step: Phase 5
- What: Convert branches
- How: Subagent reads
phases/run-phase.mdwith$phase='5'. Warn if repo has many branches. Launch detached, then poll. Use lightest available model.
Step: Phase 6
- What: Convert tags
- How: Subagent reads
phases/run-phase.mdwith$phase='6'. Launch detached, then poll. Use lightest available model.
Step: Phase 7
- What: Post-processing
- How: Subagent reads
phases/run-phase.mdwith$phase='7'. Use lightest available model.
Step: Phase 8
- What: Summary
- How: Subagent reads
phases/run-phase.mdwith$phase='8'. Use lightest available model.
After each subagent completes (or, for phases 4–6, after polling reports
success), report a one-line status to the user:✅ Phase N complete — <key result>If a phase fails, consult the consolidated
## On errortables inphases/run-phase.md, fix the problem, and re-launch that phase only — earlier phases don't need to repeat (state is preserved in$target\.svn2git\); any phase can be safely re-run since already-converted revisions/refs are skipped automaticallyA state guard at the start of phases 4–6 will tell you exactly which prior phase to re-run if state files are missing
The most common issues are covered in references/troubleshooting.md.
Step 5 — Verify
After the migration completes, check the result:
# Check recent commit messages and SVN revision traceability
git log --oneline | head -20
# Check all branches were created
git branch -a
# Check tags
git tag
# Inspect the latest commit in full
git show HEAD
If anything looks wrong (missing branches, garbled commit messages, missing history), share the output here.
Step 6 — Next steps
Push to a remote:
git remote add origin <remote-url>
git push --all origin
git push --tags origin
Optional — strip git-svn-id trailers (irreversible; requires git filter-repo):
git filter-repo --message-callback 'return re.sub(rb"\ngit-svn-id:.*", b"", message)'
Reference files
references/algorithm.md— technical algorithm spec (for debugging/understanding the scripts)references/git-svn-mapping.md— mapping from original git-svn CLI switches to this skill's optionsreferences/troubleshooting.md— auth issues, encoding, large repos, edge cases