Migrated your repo from subversion to git? Here’s how I migrated my working copies.

The long-used FreshPorts subversion repositories have been converted from subversion to git. The process I used is documented in https://github.com/FreshPorts/git-conversion. Today, I’m moving the working copy subversion checkouts to git working copies. The original code is retained and any local changes will be available for check-in / diff after the conversion. Here’s an outline of that process.

FreshPorts left Subversion on 5 October 2026, but the hosts didn’t. The ingress code on each server still runs from Subversion working copies, which now point at a server that no longer exists. This post shows how I switched one of them, on my development host dvl-ingress01, to the new git repositories, in place, without losing my uncommitted work.

The same steps work for any Subversion working copy whose repository has been converted to git.

What’s there now

Ingress is installed as two packages, and on a development host each one is a working copy:

DirectorySubversion URLgit repository
/usr/local/libexec/freshports^/ingress/scripts/branches/gitFreshPorts/ingress-scripts
/usr/local/lib/perl5/site_perl/FreshPorts^/ingress/modules/branches/gitFreshPorts/ingress-modules

Two facts make an in-place switch possible:

  • The working copies are at the final revision. Both are at r6278, the last commit before the Subversion server was shut down.
  • branches/git became main. The conversion turned that branch into main in each repository, and checked it file for file against Subversion. So main holds exactly what these working copies hold, plus one README added during the conversion.

Here is the scripts working copy before I started:


[17:25 dvl-ingress01 dvl /usr/local/libexec/freshports] % cd /usr/local/libexec/freshports
[17:25 dvl-ingress01 dvl /usr/local/libexec/freshports] % svn info | egrep 'URL|Revision'
URL: svn+ssh://svnusers@svn.int.unixathome.org/freshports-1/ingress/scripts/branches/git
Relative URL: ^/ingress/scripts/branches/git
Revision: 6278
[17:25 dvl-ingress01 dvl /usr/local/libexec/freshports] % svn status
?       ._.idea
?       ._process_vuxml_new.pl
?       .git
?       README.md
?       default_version_change.py
?       detect_default_version_changes.py
?       fetch-parse-meta.conf
?       populate_port_default_version_deps.py
?       port_default_version_deps.py
M       process_default_versions.pl
M       set-pkgversion-build-run-depends.pl
?       sync_default_version_variable.py
?       sync_port_default_version_variable.py
[17:25 dvl-ingress01 dvl /usr/local/libexec/freshports] % 

The M lines are files I’ve changed but not committed. The ? lines are files Subversion isn’t tracking, some of them new work. Both need to survive the switch.

Why not just clone?

The obvious approach is to move the old directory aside, git clone into its place, and copy the changed files back. It works, but it’s easy to miss a file. The directory is also briefly empty, and this host runs from it.

Instead I’ll make the existing directory into a git working copy. git keeps its records in a .git directory, the way Subversion uses .svn. If I create .git and tell git which commit the files on disk came from, git compares the two and reports my changes, exactly as svn status did. No file on disk is touched.

Step 1: make a backup

None of the following should change a file, but a backup costs nothing:


[17:11 dvl-ingress01 dvl ~] % tar -C /usr/local/libexec -czf ~/ingress-scripts-svn-r6278.tgz freshports
[17:28 dvl-ingress01 dvl ~] % ls -l ~/ingress-scripts-svn-r6278.tgz
-rw-r--r--  1 dvl dvl 368941 2026.10.08 17:11 /usr/home/dvl/ingress-scripts-svn-r6278.tgz

Step 2: create the git repository

cd /usr/local/libexec/freshports
git init -b main
git remote add origin git@github.com:FreshPorts/ingress-scripts.git
git fetch origin

[17:11 dvl-ingress01 dvl ~/scripts] % cd /usr/local/libexec/freshports && git init -b main
Initialized empty Git repository in /usr/local/libexec/freshports/.git/
[17:12 dvl-ingress01 dvl /usr/local/libexec/freshports] % git remote add origin git@github.com:FreshPorts/ingress-scripts.git && git fetch origin && git reset origin/main && git branch -u origin/main && git restore README.md

remote: Enumerating objects: 6537, done.
remote: Counting objects: 100% (6537/6537), done.
remote: Compressing objects: 100% (2264/2264), done.
remote: Total 6537 (delta 4325), reused 6464 (delta 4253), pack-reused 0 (from 0)
Receiving objects: 100% (6537/6537), 2.54 MiB | 24.97 MiB/s, done.
Resolving deltas: 100% (4325/4325), done.
From github.com:FreshPorts/ingress-scripts
 * [new branch]      main                  -> origin/main
 * [new branch]      pre-split/FreshPorts1 -> origin/pre-split/FreshPorts1
 * [new branch]      pre-split/FreshPorts2 -> origin/pre-split/FreshPorts2
 * [new branch]      trunk                 -> origin/trunk
 * [new tag]         1.1.32                -> 1.1.32
 * [new tag]         2.0.13                -> 2.0.13
 * [new tag]         1.1.10                -> 1.1.10
 * [new tag]         1.1.11                -> 1.1.11
 * [new tag]         1.1.12                -> 1.1.12
 * [new tag]         1.1.13                -> 1.1.13
 * [new tag]         1.1.14                -> 1.1.14
 * [new tag]         1.1.15                -> 1.1.15
 * [new tag]         1.1.16                -> 1.1.16
 * [new tag]         1.1.17                -> 1.1.17
 * [new tag]         1.1.18                -> 1.1.18
 * [new tag]         1.1.19                -> 1.1.19
 * [new tag]         1.1.20                -> 1.1.20
 * [new tag]         1.1.21                -> 1.1.21
 * [new tag]         1.1.22                -> 1.1.22
 * [new tag]         1.1.23                -> 1.1.23
 * [new tag]         1.1.24                -> 1.1.24
 * [new tag]         1.1.25                -> 1.1.25
 * [new tag]         1.1.26                -> 1.1.26
 * [new tag]         1.1.27                -> 1.1.27
 * [new tag]         1.1.28                -> 1.1.28
 * [new tag]         1.1.29                -> 1.1.29
 * [new tag]         1.1.30                -> 1.1.30
 * [new tag]         1.1.31                -> 1.1.31
 * [new tag]         1.1.4                 -> 1.1.4
 * [new tag]         1.1.5                 -> 1.1.5
 * [new tag]         1.1.6                 -> 1.1.6
 * [new tag]         1.1.7                 -> 1.1.7
 * [new tag]         1.1.8                 -> 1.1.8
 * [new tag]         1.1.9                 -> 1.1.9
 * [new tag]         2.0.0                 -> 2.0.0
 * [new tag]         2.0.1                 -> 2.0.1
 * [new tag]         2.0.10                -> 2.0.10
 * [new tag]         2.0.11                -> 2.0.11
 * [new tag]         2.0.12                -> 2.0.12
 * [new tag]         2.0.14                -> 2.0.14
 * [new tag]         2.0.15                -> 2.0.15
 * [new tag]         2.0.16                -> 2.0.16
 * [new tag]         2.0.17                -> 2.0.17
 * [new tag]         2.0.18                -> 2.0.18
 * [new tag]         2.0.19                -> 2.0.19
 * [new tag]         2.0.2                 -> 2.0.2
 * [new tag]         2.0.20                -> 2.0.20
 * [new tag]         2.0.21                -> 2.0.21
 * [new tag]         2.0.22                -> 2.0.22
 * [new tag]         2.0.23                -> 2.0.23
 * [new tag]         2.0.24                -> 2.0.24
 * [new tag]         2.0.25                -> 2.0.25
 * [new tag]         2.0.26                -> 2.0.26
 * [new tag]         2.0.27                -> 2.0.27
 * [new tag]         2.0.28                -> 2.0.28
 * [new tag]         2.0.29                -> 2.0.29
 * [new tag]         2.0.3                 -> 2.0.3
 * [new tag]         2.0.30                -> 2.0.30
 * [new tag]         2.0.31                -> 2.0.31
 * [new tag]         2.0.34                -> 2.0.34
 * [new tag]         2.0.35                -> 2.0.35
 * [new tag]         2.0.36                -> 2.0.36
 * [new tag]         2.0.37                -> 2.0.37
 * [new tag]         2.0.38                -> 2.0.38
 * [new tag]         2.0.39                -> 2.0.39
 * [new tag]         2.0.4                 -> 2.0.4
 * [new tag]         2.0.40                -> 2.0.40
 * [new tag]         2.0.41                -> 2.0.41
 * [new tag]         2.0.42                -> 2.0.42
 * [new tag]         2.0.43                -> 2.0.43
 * [new tag]         2.0.45                -> 2.0.45
 * [new tag]         2.0.46                -> 2.0.46
 * [new tag]         2.0.47                -> 2.0.47
 * [new tag]         2.0.48                -> 2.0.48
 * [new tag]         2.0.49                -> 2.0.49
 * [new tag]         2.0.5                 -> 2.0.5
 * [new tag]         2.0.50                -> 2.0.50
 * [new tag]         2.0.51                -> 2.0.51
 * [new tag]         2.0.52                -> 2.0.52
 * [new tag]         2.0.53                -> 2.0.53
 * [new tag]         2.0.54                -> 2.0.54
 * [new tag]         2.0.55                -> 2.0.55
 * [new tag]         2.0.56                -> 2.0.56
 * [new tag]         2.0.57                -> 2.0.57
 * [new tag]         2.0.6                 -> 2.0.6
 * [new tag]         2.0.7                 -> 2.0.7
 * [new tag]         2.0.8                 -> 2.0.8
 * [new tag]         2.1.0                 -> 2.1.0
 * [new tag]         2.1.1                 -> 2.1.1
 * [new tag]         2.1.2                 -> 2.1.2
 * [new tag]         2.1.3                 -> 2.1.3
 * [new tag]         2.2.0                 -> 2.2.0
 * [new tag]         2.2.1                 -> 2.2.1
 * [new tag]         2.2.2                 -> 2.2.2
 * [new tag]         2.2.3                 -> 2.2.3
 * [new tag]         2.2.4                 -> 2.2.4
 * [new tag]         2.2.5                 -> 2.2.5
 * [new tag]         2.2.6                 -> 2.2.6
 * [new tag]         2.2.7                 -> 2.2.7
 * [new tag]         2.3.0                 -> 2.3.0
 * [new tag]         2.3.0.1               -> 2.3.0.1
 * [new tag]         pre-split/1.0.19      -> pre-split/1.0.19
 * [new tag]         pre-split/1.0.25      -> pre-split/1.0.25
 * [new tag]         pre-split/1.0.27      -> pre-split/1.0.27
 * [new tag]         pre-split/1.0.28      -> pre-split/1.0.28
 * [new tag]         pre-split/1.0.33      -> pre-split/1.0.33
 * [new tag]         pre-split/1.0.34      -> pre-split/1.0.34
 * [new tag]         pre-split/1.0.8       -> pre-split/1.0.8
 * [new tag]         pre-split/1.1.4       -> pre-split/1.1.4
Unstaged changes after reset:
D	README.md
M	index_pkgversions.py
M	process_default_versions.pl
M	set-pkgversion-build-run-depends.pl
M	show-config.pl
M	test-categories.pl
M	test-master-port.sh
branch 'main' set up to track 'origin/main'.

What each command does:

  • git init -b main creates an empty .git, with a branch called main that has no commits yet. The files in the directory are untouched; at this point git sees them all as untracked.
  • git remote add origin ... records where the repository lives. I use the SSH URL so I can push from this host later; the repositories are public, so an https://github.com/... URL works for fetching too.
  • git fetch origin downloads the full history into .git, but doesn’t touch the files in the directory.

Step 3: point main at the fetched history

git reset origin/main
git branch -u origin/main

I did that above.

This is the key step. git reset has three modes, and which one you use matters:

CommandMoves the branchUpdates the indexOverwrites files on disk
git reset --softyesnono
git reset (--mixed, the default)yesyesno
git reset --hardyesyesyes, and your changes are lost

The default, --mixed, is what’s needed here. main now points at the last commit from Subversion. The index, git’s record of what the next commit will contain, matches that commit. The files on disk are left as they were. git then reports any difference between disk and commit as a change of mine. Do not add --hard: it would replace my modified files with the committed versions.

git branch -u origin/main sets main to track origin/main, so a plain git pull, git push or git status knows which branch to compare with.

Step 4: check that git agrees with Subversion

git status --short

[17:34 dvl-ingress01 dvl ~/scripts] % git status --short                                                                    
 M index_pkgversions.py
 M process_default_versions.pl
 M set-pkgversion-build-run-depends.pl
 M show-config.pl
 M test-categories.pl
 M test-master-port.sh
?? ._.idea
?? ._process_vuxml_new.pl
?? .idea/
?? .svn/
?? Jail/.#upgrade-jail-via-replace.sh
?? UpdatePackagesFromRawPackages.py
?? check_git.sh
?? check_repos_for_new_stuff.py
?? config.sh
?? config.sh~
?? default_version_change.py
?? detect_default_version_changes.py
?? fetch-extract-parse-import-one-abi.sh
?? fetch-parse-meta.conf
?? get_packagesite.txz_date
?? git-delta.sh
?? git-last-processed-tags.sh
?? git-range-of-commits.sh
?? git-single-commit.sh
?? git-to-freshports-xml.py
?? helper_scripts/
?? import-via-copy-packagesite-all-raw-fields.py
?? import_packagesite.py
?? populate_port_default_version_deps.py
?? port_default_version_deps.py
?? set-pkgversion-build-run-depends.pl~
?? sync_default_version_variable.py
?? sync_port_default_version_variable.py

Compare this with the svn status output above. git uses ?? where Subversion uses ?, but the lists should match, with one exception:

  • D README.md. The conversion added a README to each repository, and the working copy has never had it. Restore it:
git restore README.md

Any other M or D line that Subversion didn’t show means the working copy and main really differ, so stop and investigate. .svn is still there, so svn diff on that file will show what the working copy holds. My modified files should show the same changes in both:

git diff --stat
svn diff --summarize

[17:35 dvl-ingress01 dvl ~/scripts] % git diff --stat
 index_pkgversions.py                |   0
 process_default_versions.pl         | 215 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
 set-pkgversion-build-run-depends.pl |  10 +++++++
 show-config.pl                      |   0
 test-categories.pl                  |   0
 test-master-port.sh                 |   0
 6 files changed, 184 insertions(+), 41 deletions(-)
[17:36 dvl-ingress01 dvl ~/scripts] % svn diff --summarize
M       process_default_versions.pl
M       set-pkgversion-build-run-depends.pl

Step 5: remove the Subversion metadata

Once git and Subversion agree, .svn is no longer needed. Its server is gone anyway.


rm -rf .svn
git status
git log --oneline -3

[17:42 dvl-ingress01 dvl /usr/local/libexec/freshports] % rm -rf .svn
[17:42 dvl-ingress01 dvl /usr/local/libexec/freshports] % git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
	modified:   index_pkgversions.py
	modified:   process_default_versions.pl
	modified:   set-pkgversion-build-run-depends.pl
	modified:   show-config.pl
	modified:   test-categories.pl
	modified:   test-master-port.sh

Untracked files:
  (use "git add ..." to include in what will be committed)
	._.idea
	._process_vuxml_new.pl
	.idea/
	Jail/.#upgrade-jail-via-replace.sh
	UpdatePackagesFromRawPackages.py
	check_git.sh
	check_repos_for_new_stuff.py
	config.sh
	config.sh~
	default_version_change.py
	detect_default_version_changes.py
	fetch-extract-parse-import-one-abi.sh
	fetch-parse-meta.conf
	get_packagesite.txz_date
	git-delta.sh
	git-last-processed-tags.sh
	git-range-of-commits.sh
	git-single-commit.sh
	git-to-freshports-xml.py
	helper_scripts/
	import-via-copy-packagesite-all-raw-fields.py
	import_packagesite.py
	populate_port_default_version_deps.py
	port_default_version_deps.py
	set-pkgversion-build-run-depends.pl~
	sync_default_version_variable.py
	sync_port_default_version_variable.py

no changes added to commit (use "git add" and/or "git commit -a")
[17:42 dvl-ingress01 dvl /usr/local/libexec/freshports] % git log --oneline -3
f33b564 (HEAD -> main, origin/main, origin/HEAD) Add README describing the conversion from Subversion
f286e1c Give the make child a PATH which includes /usr/local/bin
2ed2da6 Update copyright lines to Dan Langille, through 2026
[17:42 dvl-ingress01 dvl /usr/local/libexec/freshports] % 

git status should report Your branch is up to date with 'origin/main', followed by my changes. The latest commit should be the one that adds the README, preceded by the last commits from Subversion.

Step 6 (optional): hide editor clutter

I edit these files from a Mac, so the directory picks up .idea/ from the editor and ._* files from macOS. I don’t want them in the repository, and I don’t want to commit a .gitignore just for my own setup. git has a per-repository ignore file that is never committed:

printf '.idea/\n._*\n' >> .git/info/exclude
git status --short
[PASTE OUTPUT HERE: git status --short after the exclude]

The modules

The Perl modules get the same treatment, only the path and repository differ:

tar -C /usr/local/lib/perl5/site_perl -czf ~/ingress-modules-svn-r6278.tgz FreshPorts
cd /usr/local/lib/perl5/site_perl/FreshPorts
svn status
git init -b main
git remote add origin git@github.com:FreshPorts/ingress-modules.git
git fetch origin
git reset origin/main
git branch -u origin/main
git restore README.md
git status --short

[17:37 dvl-ingress01 dvl ~] % cd /usr/local/lib/perl5/site_perl/FreshPorts
[17:37 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % svn status                                  
?       ._.idea
?       ._category.pm
?       ._element.pm
?       ._port.pm
?       ._verifyport.pm
?       ._vuxml.pm
?       ._xml_munge_git.pm
?       .git
?       .idea
?       README.md
?       Verify
M       special_processing_files.pm
[17:37 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % git init -b main
warning: re-init: ignored --initial-branch=main
Reinitialized existing Git repository in /usr/local/lib/perl5/site_perl/FreshPorts/.git/
[17:37 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % git remote add origin git@github.com:FreshPorts/ingress-modules.git
error: remote origin already exists.
[17:37 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % git fetch origin
[17:37 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % git reset origin/main
Unstaged changes after reset:
M	special_processing_files.pm
[17:37 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % git branch -u origin/main
branch 'main' set up to track 'origin/main'.
[17:38 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % git restore README.md
[17:38 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % git status --short
 M special_processing_files.pm
?? ._.idea
?? ._category.pm
?? ._element.pm
?? ._port.pm
?? ._verifyport.pm
?? ._vuxml.pm
?? ._xml_munge_git.pm
?? .idea/
?? .svn/
?? Verify/
?? config.pm
?? config.pm~
?? special_processing_files.pm~
?? status.pm
?? svn-commit.tmp~
?? vuxml_parsing.pm~

When the two status lists agree:


rm -rf .svn
printf '.idea/\n._*\n' >> .git/info/exclude
git status
git log --oneline -3

[17:39 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % rm -rf .svn
[17:39 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % printf '.idea/\n._*\n' >> .git/info/exclude
[17:41 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git restore ..." to discard changes in working directory)
	modified:   special_processing_files.pm

Untracked files:
  (use "git add ..." to include in what will be committed)
	Verify/
	config.pm
	config.pm~
	special_processing_files.pm~
	status.pm
	svn-commit.tmp~
	vuxml_parsing.pm~

no changes added to commit (use "git add" and/or "git commit -a")
[17:41 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % git log --oneline -3
36c6a07 (HEAD -> main, origin/main, origin/HEAD) Add README describing the conversion from Subversion
0394cc6 Update copyright lines to Dan Langille, through 2026
ae8b937 (tag: 2.3.0) Schedule a refresh when the comparison finds work
[17:41 dvl-ingress01 dvl /usr/local/lib/perl5/site_perl/FreshPorts] % 

What changes day to day

BeforeNow
svn statusgit status
svn diffgit diff
svn upgit pull
svn commitgit commit, then git push
svn revert filegit restore file
svn loggit log

The main difference is that git commit only records the change locally; nothing reaches GitHub until git push. In return, I can commit on the development host as often as I like and push only when the work is ready.

For older history, the commits converted from Subversion carry a git-svn-id: line with their original revision. In ingress-scripts, git log --grep='@6278 ' finds r6278. Subversion revision numbers are shared across the whole repository, so a given number appears only in the project that the commit changed.

Website Pin Facebook Twitter Myspace Friendfeed Technorati del.icio.us Digg Google StumbleUpon Premium Responsive

Leave a Comment

Scroll to Top