To migrate from SVN to Git without losing history, convert with git svn and a complete authors file, turn the imported tag branches into real Git tags, verify every tag tree against an SVN export, and remove any old secrets before the first push, when rewriting costs nothing. Then rotate those secrets anyway, because the SVN repository and every old checkout still hold them.
Below: which conversion route fits which repository, exact commands from the Git and Subversion documentation, what happens to each SVN property, a measured history rewrite, and how to keep security findings continuous after the move.
Key takeaways
git svnships with Git and suits most single project repositories, but it aborts on any SVN username missing from your authors file.- Per the git-svn manual, every SVN property except
svn:executableis ignored. Externals, keywords, line endings and ignore lists need a plan. - The cheapest moment to strip a secret from history is between conversion and the first push, when new commit IDs break nothing.
- Rewriting never un-leaks a secret. SVN, its backups and old checkouts keep it. Rotate first.
- SVN path-based read restrictions do not carry over; restricted paths need their own Git repository.
- In our 2,000 commit test, removing one file changed 1,501 commit IDs.
filter-branchtook 172 seconds; afast-exportandfast-importpipeline gave identical IDs in 0.21 seconds. - Subversion 1.15.0 shipped on 8 October 2026, so migrating is a workflow choice, not an emergency.
Why SVN to Git migrations still matter in 2026
Subversion has not gone away. The Apache Subversion news page lists 1.15.0 as released on 8 October 2026, after three release candidates between July and September 2026. The 1.15 release notes describe optional "pristines on demand" working copies that can cut client storage by up to half, faster streamed checkouts and a CMake build, and they say 1.15 servers read and write repositories created by earlier versions without a dump and reload.
The Stack Overflow Developer Survey 2022 put Subversion at roughly 5 percent of respondents. Small next to Git, but in a large enterprise that usually means a few long lived repositories holding firmware, build tooling or a decade of product history, plus things nobody would commit today: a 2014 database password, a signing key checked in for a week. Conversion is the one moment when cleaning that history is nearly free.
Which tool for which repository
There are three broad routes. Pick based on layout complexity, size and whether you need SVN and Git to coexist for a while.
| Route | What it is | Best fit | Main limits |
|---|---|---|---|
git svn (ships with Git) | Replays each SVN revision as a Git commit over the SVN protocol | One project per repository, standard or near standard layout, up to tens of thousands of revisions | Slow on very large histories; aborts on unmapped authors; ignores most SVN properties |
| Rules based fast-export converters (open source) | Read the SVN repository directly (usually a local copy or dump) and emit a git fast-import stream, with rules mapping paths to repositories and branches | Monorepos holding many projects, irregular layouts, repositories that moved trunk around over the years | Needs local filesystem access to the repository; you write and maintain a rules file |
| Commercial migration services (described generically) | Vendor tooling and staff that run conversion, verification and cutover | Many repositories at once, regulated environments that want a signed off process | Cost; you still own the decisions on secrets, access control and LFS |
git svn is the route most teams start with, and it is the one the Pro Git book documents in its "Migrating to Git" chapter. The git-svn manual itself points one way traffic toward git-filter-repo for history rewriting: under the noMetadata setting it says that if you plan to migrate and are "certain about dropping SVN history", you should consider git-filter-repo instead, noting that it can also reformat metadata and rewrite authorship.
Rule of thumb: one trunk, one branches directory and one tags directory means git svn --stdlayout will do. Moved projects, a renamed trunk or forty products in one repository mean a rules based converter saves days of cleanup.
Step 1: build a complete authors file
SVN records a username. Git wants a name and an email. The authors file maps one to the other, one line per SVN user:
schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>
The Pro Git book generates the list of SVN usernames with this pipeline, run inside a working copy or against the repository URL:
svn log --xml --quiet | grep author | sort -u | \
perl -pe 's/.*>(.*?)<.*/$1 = /' > users.txt
It needs grep, sort and perl. Fill in the Git identity after each =.
What happens when an author is missing
Per the git-svn manual, if git svn meets a committer missing from the authors file, it aborts; you add the entry and re-run, and it continues where it stopped. Build the file from the whole history up front.
Two practical points:
- Run the
svn logagainst the repository root, not trunk alone. Branch and tag commits can come from people who never touched trunk. - Look for revisions with no author at all. SVN shows these as
(no author)in log output; they often come from repository creation, imports from older systems, or administrative fixes. Map that label to a neutral identity such as a "migration" account so the history stays readable.
As a fallback, the manual documents --authors-prog: for each missing committer, git svn runs your program with the SVN username and expects one line back in the form Name <email>.
Fixing identities after conversion
If identities need cleaning later, git-filter-repo accepts --mailmap <file> to rewrite author, committer and tagger names and emails across history, and --use-mailmap as shorthand for a .mailmap file in the repository. Doing that before the first push is free. Doing it after changes every commit ID from the first rewritten commit onward.
Step 2: choose the layout flags
Per the git-svn manual, --trunk, --branches and --tags take relative paths or full URLs, --branches and --tags may repeat, and --stdlayout (-s) is shorthand for trunk, branches and tags. Explicit options win if you pass both.
Standard layout:
git svn clone https://svn.example.com/repos/product \
--authors-file=users.txt --stdlayout --prefix=svn/ product-git
Non standard layout, for example a repository where trunk lives under main, release branches under releases and some older branches under branches/legacy:
git svn clone https://svn.example.com/repos/product \
--authors-file=users.txt \
--trunk=main \
--branches=releases --branches=branches/legacy \
--tags=tags \
--prefix=svn/ product-git
The default --prefix has been origin/ since Git 2.0, per the manual. Pro Git uses --prefix "", which is why its cleanup loops below read refs/remotes/tags; adjust the loops to whatever prefix you pick.
Should you pass --no-metadata?
By default git svn appends a git-svn-id: line with the SVN URL and revision to every commit message. The sources disagree here. Pro Git uses --no-metadata and says the lines can bloat the log. The git-svn manual calls the option "NOT recommended", because old revision numbers in bug reports become hard to trace and git svn cannot fetch again without metadata.
Keep the metadata through dry runs and the pre-cutover sync, because git svn fetch needs it. If tickets say "fixed in r48213", the git-svn-id lines are your only map to Git commits; if you want a clean log, save a revision map with the archive first, then strip the lines with git-filter-repo's message rewriting.
Large histories
--log-window-size sets how many log entries git svn fetches per request (default 100); the manual says very large repositories may need more, at the cost of memory and timeouts. Running close to the data over file:// helps most, with rewriteRoot set before import so metadata records the public URL.
Step 3: turn imported refs into real branches and tags
The git-svn manual explains that SVN allows commits to a tag, so git svn "acts conservatively and imports all SVN tags as branches", prefixed with tags/. You convert them into Git tags yourself. Pro Git gives these loops (written for an empty prefix):
# SVN tags become Git tags
for t in $(git for-each-ref --format='%(refname:short)' refs/remotes/tags); do
git tag ${t/tags\//} $t && git branch -D -r $t
done
# Remaining remote refs become local branches
for b in $(git for-each-ref --format='%(refname:short)' refs/remotes); do
git branch $b refs/remotes/$b && git branch -D -r $b
done
# Optional: drop peg revision branches such as release@1234
for p in $(git for-each-ref --format='%(refname:short)' | grep @); do
git branch -D $p
done
# trunk duplicates the main branch
git branch -d trunk
Three things to check after the loops:
- Tags that received commits. A tag branch with commits past its copy point was modified in SVN after creation. Tag the tip, and record which tags this happened to.
- Annotated or lightweight. The loop creates lightweight tags. If your release process expects annotated ones, use
git tag -a -m "Imported from SVN tags/1.4.0"instead. - Peg revision branches. Names ending in
@1234appear when a branch was deleted and recreated. Keep them in the archive copy even if you drop them from the published repository.
Finally, add the remote and push, as Pro Git shows. Do not run these until the verification and secret steps below are done:
git remote add origin git@git.example.com:team/product.git
git push origin --all
git push origin --tags
What happens to svn:externals, svn:ignore, svn:keywords, svn:eol-style, empty directories and mergeinfo
The git-svn manual's BUGS section contains the sentence that drives this whole section: it ignores all SVN properties except svn:executable, and logs the unhandled ones to $GIT_DIR/svn/<refname>/unhandled.log. Read that log. It is the to-do list for anything SVN was doing on your behalf.
| SVN feature | What git svn does | Git equivalent | What to do during migration |
|---|---|---|---|
svn:ignore, svn:global-ignores | Not applied automatically | .gitignore | Run git svn create-ignore to write .gitignore files (staged, not committed), or git svn show-ignore for a single list |
svn:externals | Not converted; git svn show-externals lists them | Submodules, subtree merges, or package manager dependencies | Inventory every external, pin each to a revision or version, choose one replacement mechanism per external |
svn:keywords ($Id$, $Revision$, $Author$, $Date$, $HeadURL$) | Not expanded | ident attribute expands only $Id$, to the blob object name | Decide whether any build reads these values; replace with build time version stamping |
svn:eol-style | Not applied | text and eol in .gitattributes | Translate per path: native to text, LF to text eol=lf, CRLF to text eol=crlf |
svn:executable | Preserved | File mode 100755 | Nothing; spot check a few scripts |
svn:mime-type (binary marking) | Not applied | -text or LFS rules in .gitattributes | Mark binaries so Git never converts line endings in them |
| Empty directories | Recreated in the working tree from unhandled.log; not tracked by Git | Placeholder files | Use --preserve-empty-dirs if builds rely on empty directories existing |
svn:mergeinfo | Ignored as a property; merge commits pushed back to SVN cannot carry Git merge history | Git merge commits | Treat converted branch history as mostly linear; verify a few known merges by hand |
| Path-based authz rules | Not part of the repository; not converted | None at directory level for reads | See the access control section below |
Externals
The SVN book describes svn:externals as a versioned property mapping local subdirectories (and since 1.6, single files) to other repository URLs. The format changed in 1.5 from path first to URL first, with peg revisions such as URL@1234, and an external with no revision follows HEAD. The book says you "should seriously consider using explicit revision numbers". Unpinned externals mean old builds were never exactly reproducible, so pin every replacement to a specific commit or release.
Keywords and line endings
svn:keywords expands placeholders such as $Revision: 144 $ and $Id: calc.c 148 2006-07-28 21:30:43Z sally $ in working copies, and only when asked to: the SVN book notes Subversion "will never attempt to perform textual substitutions" unless the property is set. Git's ident attribute is narrower. Per the gitattributes documentation, it replaces $Id$ with $Id: followed by the 40 character blob object name, nothing else. Any build that reads a revision number from a file will break; stamp versions at build time from git describe or the CI build number instead.
For line endings, write a .gitattributes at the root before your first commit on the Git side. The gitattributes documentation gives this pattern:
* text=auto
*.txt text
*.vcproj text eol=crlf
*.sh text eol=lf
*.jpg -text
Commit the attributes file, then run git add --renormalize . once and commit the result as its own commit, so line ending churn is isolated and reviewable.
How long does conversion take?
We found no source publishing reliable git svn throughput figures, and speed depends on server latency, revision size, branch count and whether you read over file://. We did not benchmark an SVN conversion, so we quote no number. What we did measure is the step teams underestimate: rewriting converted history to remove a file, using the test described in "How we measured".
| Measurement (2,000 commits, 8 tags, one secret file added at commit 500 and deleted at commit 900) | Result |
|---|---|
| Commits whose ID changed after removing the file | 1,501 of 2,000 |
| Commits whose ID did not change (before the file was added) | 499 |
| Tags whose tree changed | 2 of 8 (the tags created while the file existed) |
Time with git filter-branch --index-filter | 172.19 seconds wall clock |
Time with a git fast-export to git fast-import stream filter | 0.21 seconds |
| Did both methods produce identical commit IDs? | Yes, all 2,000 matched |
References to the file left in the rewritten repository after filter-branch | Still reachable via 8 backup refs under refs/original |
| Copies of the file in a mirror cloned before the rewrite | Still present, untouched |
Every commit after the first touched one gets a new ID, because each commit ID covers its parent's ID. The stream based approach, the same fast-export and fast-import model git-filter-repo uses, ran in a fraction of a second where the old built in command took almost three minutes. And no rewrite touches copies that already exist elsewhere.
How to verify nothing was lost
Verification is where migrations earn trust. Do it on every dry run and again on the final conversion.
1. Revision and commit counts
For a standard layout, count SVN revisions that touched trunk and compare them with commits on the Git main branch:
svn log -q https://svn.example.com/repos/product/trunk | grep -c '^r'
git rev-list --count main
Expect an explained difference rather than a perfect match: svn log follows copies back through history unless you pass --stop-on-copy. Write the explanation down.
2. Tree comparison for every tag
This is the check that catches real problems. For each tag, export the SVN tag and the Git tag and compare them file by file:
tag=1.4.0
svn export --ignore-keywords --native-eol LF \
https://svn.example.com/repos/product/tags/$tag /tmp/svn-$tag
mkdir -p /tmp/git-$tag
git archive "$tag" | tar -x -C /tmp/git-$tag
diff -r /tmp/svn-$tag /tmp/git-$tag && echo "tag $tag matches"
--ignore-keywords stops the SVN side expanding $Id$ and friends, and --native-eol LF normalizes line endings, so the diff shows real content differences rather than the two expected sources of noise. Loop this over every tag. Missing directories on the Git side are usually externals, which makes this a useful inventory too.
3. File counts and sizes on the main line
svn list -R https://svn.example.com/repos/product/trunk | grep -v '/$' | wc -l
git ls-tree -r --name-only main | wc -l
Differences here usually trace back to empty directories (SVN lists them, Git does not), externals, or files that were moved to LFS.
4. Spot check history, not only trees
Compare svn log with git log --follow for five long lived files, and check that three known merges landed on the right branch.
Large binaries: when to move them to Git LFS
An SVN checkout fetches only the current version of each file; a Git clone fetches every version by default. Years of large design files or installers become painful the moment they land in Git. The right time to move binaries to LFS is during conversion, before the first push, for the same reason as secrets: the rewrite changes commit IDs, and before anyone clones, that costs nothing. The git lfs migrate documentation describes three modes. info reports sizes by file type and changes nothing. import converts matching files into LFS pointers across history. export does the reverse.
# See what is heavy, across every ref
git lfs migrate info --everything
# Move specific types into LFS across all history
git lfs migrate import --everything --include="*.psd,*.mp3,*.zip"
# Or move everything above a size threshold
git lfs migrate import --everything --above=100Kb
The documentation states that import and export "rewrite your Git history, changing commits and generating new commit SHAs", that you must commit or stash local work first, that rewritten branches need a force push to every remote, and that --above cannot be combined with --include or --exclude. Run LFS migration after the secret rewrite, so you do not rewrite twice, or fold both into a single planned rewrite pass.
One security side effect deserves a mention. Once files are LFS pointers, any tool that reads the Git tree sees small text pointers rather than the binary content. If you scan vendored archives or binary dependencies, check what your scanners see; our post on what dependency scanners see in Git LFS pointer files covers that in detail.
Secrets in old revisions: rewrite during conversion, or rotate and keep history
This decision turns a routine migration into a security project. Rotation and rewriting are layers, not alternatives.
What each choice preserves and breaks
| Choice | What it preserves | What it breaks or leaves behind | When it fits |
|---|---|---|---|
| Rotate only, keep history exactly | Every commit, byte for byte; any signatures; all SVN revision references | The old value stays readable to anyone with clone access to the new Git repository, forever | Secrets that are already rotated and worthless; repositories with tightly controlled read access |
| Rotate, then rewrite during conversion (before first push) | All commits as commits; authorship; messages; git-svn-id lines; tag names | Commit IDs change from the first touched commit, but nobody depends on them yet. SVN still holds the secret | Most migrations. This is the default we recommend |
| Rotate, then rewrite after the Git repository is in use | Commits and messages | Every later commit ID; signatures; open pull request comments; forks and clones still hold the data; collaborators must rebase | Only when a secret was discovered late and the risk justifies the disruption |
| Rewrite without rotating | Nothing that matters | The secret is still valid and still exists in SVN, backups, old working copies and earlier clones | Never |
GitHub's documentation on removing sensitive data puts rotation first: you "need to revoke and/or rotate that secret", after which a rewrite may not be needed. It lists the costs of rewriting a hosted repository: new commit hashes, stripped signatures, closed pull request diffs that no longer display, forced rebases, and old clones that can push the data straight back.
During conversion, almost all of those costs disappear: no pull requests, no forks, no clones, and the Git IDs are new whatever you do.
How to rewrite during conversion
Work on a fresh clone of the converted repository. The git-filter-repo README says the tool is designed to work in a fresh clone and will stop if it detects otherwise, unless you pass --force. It requires Git 2.36.0 or later and Python 3.6 or later, and the README notes it is "now recommended by the git project" in place of git filter-branch.
First, look before you cut. --analyze writes a directory of reports about the repository's history and does not modify anything:
git clone --mirror /path/to/product-git product-clean.git
cd product-clean.git
git filter-repo --analyze
To remove whole files from every commit:
git filter-repo --sensitive-data-removal --invert-paths \
--path config/secrets.env --path deploy/old-signing.key
To replace specific strings inside files, write an expressions file. The filter-repo manual says each line is literal text by default, supports regex: and glob: prefixes, and replaces matches with ***REMOVED*** unless you give your own replacement after ==>:
EXAMPLE_LITERAL_PASSWORD_VALUE
regex:db_password\s*=\s*\S+==>db_password=REDACTED
git filter-repo --sensitive-data-removal --replace-text ../expressions.txt
GitHub's page says the --sensitive-data-removal option needs git-filter-repo 2.47 or later. Per the filter-repo manual, after a run the .git/filter-repo directory holds a commit map showing how every commit was or was not changed, and a ref map for references. Save both with the migration records. The manual states it directly: when you rewrite history, all commit IDs from the first changed commit onward are modified.
Then check the result before any push:
git log --all --oneline -- config/secrets.env # expect no output
git grep -I "EXAMPLE_LITERAL_PASSWORD_VALUE" $(git rev-list --all) | head
For general practice on finding secrets in history before you decide what to remove, use whichever dedicated secret detection tooling your team already runs; our posts on what survives a git history rewrite and on rotating instead of rewriting go deeper on the trade offs.
The SVN side still holds the secret
Rewriting the Git copy does nothing to the SVN repository. After cutover, the SVN server, its backups, every developer's old working copy and any svnsync mirror still contain the old revisions. Your options there:
- Make the SVN repository read only and restrict read access to a small archive group.
- If the secret must be removed from SVN itself, the long standing route is to dump the repository with
svnadmin dump, filter out the path withsvndumpfilter exclude, and load into a new repository. This changes nothing about copies that already exist. - Keep a record of which secrets were found, when they were rotated, and which revisions contained them.
That record is the evidence that exposure ended when the credential stopped working.
SVN path-based authorization does not carry over
SVN lets administrators grant read or write access per directory. The SVN book describes an authz file read by both Apache's mod_authz_svn (via AuthzSVNAccessFile) and svnserve (via authz-db), with sections such as [repo:/path], permissions of r or rw, groups prefixed with @, and inheritance where "the most specific path always matches first". Users not listed get no access by default.
Git has no equivalent for reads. Anyone who can clone a Git repository can read every file in every commit of it. So before conversion, export your authz file and sort every rule into one of three buckets:
| SVN rule type | Example | Git replacement |
|---|---|---|
| Read restriction on a subtree | [product:/firmware/keys] readable by @release only | Split that path into its own repository with its own access list, and remove it from the main repository's history with --invert-paths |
| Write restriction on a subtree | [product:/core] writable by @core, readable by all | Branch protection plus a CODEOWNERS file requiring review from the owning team; see our CODEOWNERS guide |
| Repository wide access | [product:/] @engineering = rw | Team permissions on the hosting platform |
The read restriction bucket is the one that bites. If a directory was hidden from most of the company in SVN and you convert the whole repository into one Git repository, you have just published that directory's entire history to everyone with clone access. The SVN book argues such rules should exist only where there is a genuine need, so assume each one had a reason.
Ranked: what is hardest to undo after cutover
We ranked common mistakes by the cost of correcting them after the first push, using the side effects the sources above document.
| Rank | Mistake | Why it is hard to undo later | Source of the cost |
|---|---|---|---|
| 1 | Publishing a path that SVN restricted for reads | Once cloned, the content cannot be recalled; only rotation and a split repository help | SVN book authz model; GitHub sensitive data page on clones and forks |
| 2 | Pushing history that still contains secrets | Rewrite after the fact changes IDs, strips signatures and breaks pull request history; clones keep the data | GitHub sensitive data page; filter-repo manual |
| 3 | Pushing large binaries without LFS | Moving them later rewrites every branch and needs force pushes to every remote | git lfs migrate documentation |
| 4 | Wrong or placeholder author identities | Fixable with a mailmap rewrite, which again changes every commit ID from the first fix | git-svn manual; filter-repo --mailmap |
| 5 | Dropping git-svn-id lines without saving a revision map | Old ticket references to SVN revisions can no longer be traced | git-svn manual on --no-metadata |
| 6 | Unreplaced externals | Builds break or silently pull a different version; fixable with a forward commit | SVN book on externals |
| 7 | Missing .gitattributes for line endings | Fixable with one renormalize commit, at the cost of noisy diffs | gitattributes documentation |
| 8 | Missing .gitignore files | Fixable with a forward commit at any time | git-svn create-ignore |
Anything that needs a history rewrite to fix belongs before the first push.
Cutover: freeze, final sync, read only SVN, archive
- Announce a freeze window with a date and time.
- Make SVN read only. Give every user
rin the authz file, or add a pre-commit hook that rejects commits and names the new Git URL. - Final sync. Run
git svn fetch(metadata kept), then rerun the branch and tag loops. - Final rewrite pass. Apply the filter-repo expressions and LFS migration from your dry runs, and diff the commit map against the dry run.
- Verify every tag tree and the counts on main.
- Push all branches and tags, and turn on branch protection immediately.
- Repoint CI jobs, deploy scripts, documentation links and anything that polled SVN.
- Archive. Take a final
svnadmin dumpof the read only repository and store it with restricted access, together with the authors file, the revision map, the filter-repo commit map and the list of rotated secrets. Our guide to decommissioning old repositories covers retention and shutdown.
Apply new repository defaults on day one; our secure defaults checklist for a new Git repository is a good starting list.
Keeping vulnerability triage history and SBOMs continuous after the move
Security teams often discover a migration when two years of triage decisions reset to zero on a "new" repository. The fix is bookkeeping:
- Baseline before cutover. Generate an SBOM and dependency scan from the last SVN trunk export.
- Scan the first Git main commit the same way. Any difference points to unreplaced externals, LFS pointers or build changes.
- Carry decisions, not just data. Re-apply risk acceptances and suppressions to the new project, noting the SVN revision each was made against.
- Archive the old project record so an auditor can still see past decisions.
Our post on keeping SBOM and finding history continuous after a repository move walks through this in more detail. If you are folding several converted repositories into one, see importing repositories into a monorepo with git filter-repo.
Where Safeguard fits
Safeguard does not convert repositories and does not scan history for secrets, so neither the conversion nor the secret cleanup above depends on it. Where it helps is the continuity step. Once the new Git repositories exist, you connect them through Safeguard's GitHub, GitLab, Bitbucket or Azure DevOps integrations for dependency scanning, SBOMs and reachability analysis. For the baseline, you can run safeguard scan --dir . against the final SVN export and again against the first Git checkout, and compare the two JSON results written under ./output. Policies and gates evaluate in the platform the same way for the new project as for the old one, and the platform shows which projects contain a given component and version, which is useful when the same library appears in both the archived and the live project during the transition.
Common mistakes
- Starting conversion before the authors file is complete. The conversion aborts at the first unknown username, often hours in.
- Using
--no-metadataon the dry run. You lose the ability togit svn fetchincrementally, and the final sync turns into a full reconversion. - Comparing only the main branch. Most real loss shows up in tags and old release branches. Compare every tag tree.
- Forgetting externals. The converted repository builds on the migration engineer's machine because the old external checkout is still lying around, then fails in CI.
- Rewriting without rotating. The secret is still valid, still in SVN, still in backups.
- Leaving
refs/originalor old pack files around after afilter-branchexperiment and then pushing with--mirror. In our test,filter-branchleft all eight rewritten refs reachable underrefs/original. - Letting security findings reset silently. Baseline and carry decisions across.
How we measured
Environment. A MacBook with an Apple M5 processor, Git 2.54.0 (Apple Git-157), Python 3, on 23 October 2026. Subversion and git-filter-repo were not installed on the test machine, so we did not benchmark git svn or run git-filter-repo itself.
Repository. We generated a synthetic linear history of 2,000 commits with git fast-import. Each commit changed one of 50 text files. Commit 500 added config/secrets.env containing a dummy placeholder value; commit 900 deleted it. A tag was created every 250 commits, giving 8 tags. Before rewriting, we made a mirror clone to stand in for "a copy someone already has".
Rewrite A. git filter-branch -f --index-filter 'git rm -q --cached --ignore-unmatch config/secrets.env' --tag-name-filter cat -- --all, timed with /usr/bin/time -p.
Rewrite B. git fast-export --all --signed-tags=strip from the untouched mirror, piped through a 15 line Python filter that drops file modify and delete lines for that one path, into git fast-import in a new empty repository. Timed with wall clock timestamps around the pipeline.
Checks. We compared the ordered list of commit IDs before and after with git rev-list --reverse main, compared each tag's tree ID, ran git log --all -- config/secrets.env in the rewritten repositories and in the pre-rewrite mirror, and compared the commit ID lists produced by the two methods.
Limits. One run each, small text files, fast local disk. Real repositories will be slower in absolute terms and the timing ratio will vary; the commit ID and reachability findings do not depend on size. All other figures (release dates, survey share, tool behavior) come from the sources below, fetched on 23 October 2026.
Fix it without waiting for upstream
A migration is a natural point to reset how fast your team fixes what it ships. The repositories you just converted carry the same open source dependencies they did in SVN, and the new Git workflow is the place to stop waiting on upstream for those fixes.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
FAQ
How do I migrate an SVN repository to Git and keep all history?
Use git svn clone with --authors-file and --stdlayout or explicit layout paths, convert imported tag branches into Git tags, compare every tag tree against an svn export, then push all branches and tags. Each revision becomes a commit with its original author, date and message.
What happens if an SVN author is missing from the authors file?
According to the git-svn manual, git svn aborts. Add the missing entry and rerun the same command; it continues where it stopped. Alternatively, pass --authors-prog with a script that returns a Name <email> line for any unknown username.
Does git svn convert svn:externals?
No. The git-svn manual says it ignores every SVN property except svn:executable. git svn show-externals lists the externals so you can replace each one with a submodule, a subtree or a package manager dependency, pinned to a specific version.
Should I use no-metadata mode when converting SVN to Git?
Only for the final, one shot conversion, and only after saving a map of SVN revisions to Git commits. Without metadata, git svn cannot fetch again, and the git-svn manual warns it makes old SVN revision references in tickets and documents hard to trace.
Can I remove passwords from SVN history while converting to Git?
Yes. After git svn clone and before the first push, run git-filter-repo with --invert-paths --path to drop whole files or --replace-text to replace strings. Commit IDs change, but nobody depends on them yet. Rotate the credentials as well, because the SVN repository and old checkouts still contain them.
Is rotating a leaked secret enough, or do I also have to rewrite history?
Rotation is what ends the exposure. Rewriting reduces how many places the dead value is visible. During an SVN to Git conversion, rewriting is nearly free, so do both. After a Git repository is in active use, a rewrite has real costs, and rotation alone is often the right call.
How do I keep SVN path-based permissions in Git?
You cannot at directory level for reads. Anyone who can clone a Git repository can read all of it. Move read restricted paths into their own repositories with their own access lists, and use branch protection with CODEOWNERS for write review on shared directories.
How long does an SVN to Git conversion take?
It depends on revision count, branches, file sizes and server latency, and we found no reliable published numbers. Run on or near the SVN server over file://, and time a full dry run.
See how Safeguard would help
Safeguard does not do the conversion. These demos show what comes after it.
The first demo is a real Safeguard CLI session: asked to fix a vulnerable lodash dependency, it reads the finding and advisory, asks before every command, edits package.json, runs install and tests with your approval, and pushes nothing. The same flow applies to a freshly converted repository whose dependencies have not moved since the SVN days.
Click through it at your own pace, or open it full screen.
The second demo sets up the Safeguard runner on your own machine, approves it with a device code and scans from the terminal. That is how you would baseline the final SVN export and the first Git checkout on one host.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. Once converted repositories are connected, you can ask about their vulnerabilities in plain language.
Click through it at your own pace, or open it full screen.
To try it on your own repositories, download Safeguard or read the CLI documentation.
Quick check
Why is the period between git svn clone and the first push the best time to remove secrets from history?
Because nobody has cloned, forked or opened pull requests against the Git repository yet. A rewrite changes commit IDs from the first touched commit onward, and before the first push those IDs are new anyway, so nothing depends on them. You still need to rotate the secrets, because SVN and its copies keep them.
Does an SVN path-based read restriction survive conversion to Git?
No. Git has no per-directory read permission, so anyone who can clone the repository can read every file in every commit. Restricted paths need their own repository with its own access list.
Sources
- Git documentation: git-svn
- Pro Git, Migrating to Git (Subversion section)
- git-filter-repo README
- git-filter-repo manual
- Git LFS: git-lfs-migrate manual
- Git documentation: gitattributes
- GitHub Docs: Removing sensitive data from a repository
- Version Control with Subversion: Path-Based Authorization
- Version Control with Subversion: Keyword Substitution
- Version Control with Subversion: Externals Definitions
- Apache Subversion news
- Apache Subversion security advisories
- Apache Subversion 1.15 release notes
- Stack Overflow Developer Survey 2022
Last reviewed: 23 October 2026. Tool options change between releases; check the linked documentation for the version you run.