I’ve had a Git server at https://git.msfjarvis.dev for a while now, running Gitea but my faith in the project has steadily been going down which is exacerbated every time I am reminded that they do not have the confidence to develop Gitea on Gitea. When Codeberg announced they were forking Gitea as Forgejo I quietly put down a line item to switch over in my overflowing TODO list and promptly forgot about it.
With GitHub officially announcing its demise as an independent entity and its joining Microsoft’s “CoreAI” division, the time has never been more ripe to invest in alternatives. Given this, I decided to pull up the Forgejo migration and start investigating this in earnest. This ended up being a little more involved than I had expected, with several tangents involved.
This is gonna be long and winding, but the TL;DR is that I ended up migrating individual accounts instead of the whole instance data by using a simple CLI tool that I ironically generated using GitHub Copilot (the code is available on my Git server).
The naive way
Forgejo initially used to support migrating data from Gitea pretty easily, but the increased friction of having to maintain a full fork against a moving target moved them to drop support for these migration paths. I of course am stupid and was actually not aware that this had occurred and that swapping the latest Gitea for latest Forgejo will simply not work. The NixOS manual’s migration section suggested that this was possible, but it’s actually missing critical information about version requirements that was added later.
I attempted to follow these steps, but ended up beefing it and nearly corrupted my Gitea data in the process. I learned my lesson from this and all future attempts used a separate copy of the Gitea state directory.
The gitea dump
command
This looked like it had some potential until I realized the lack of a companion gitea restore
functionality. The generated dump is a big ZIP file of your entire state directory, along with the SQL commands to recreate your database with whatever database engine you were using. The recommended way to use this dump is to unzip it and then replay the SQL commands into sqlite
(my current database) to build up the state directory. This was particularly unhelpful but I kept at it anyway.
I had also chosen to run Forgejo using PostgreSQL instead of SQLite since I had proper backup strategies in place for it and was already using it with a bunch of other software running on this machine. This meant I had to reach out for pgloader to handle this translation which worked pretty well. Running pgloader sqlite:///var/lib/gitea/data/gitea.db 'postgresql://forgejo:@/forgejo?host=/run/postgresql'
as root rebuilt the Gitea database onto Postgres.
Unfortunately this didn’t work due to the previously discussed version incompatibilities, and Forgejo rejected the database I had created for it. At this point I had lost a couple hours on this and electricity problems at my place had additionally caused my patience to run thin. I decided to lean into the fabled NixOS reproducibility and just build a fresh instance of Forgejo with the same settings and migrate the data for my account manually.
Vibe coding deployed somewhat effectively
Aside, I’m not generally a big believer in the AI hype. I have yet to pay for any of these tools, between GitHub and my employer I get plenty of access to top of the line models that are supposedly reinventing my field of work every 3 months. LLMs have yet to meaningfully help me at work, and the only times I’ve gotten real value out of them is by getting them to write one-off things that I am glad to have but would likely never invest the time to upskill into and build myself.
I hooked up the free license to GitHub Copilot I get for satisfying some criteria of “not worthless” into Zed and wrote out a simple README file describing what I wanted out of the tool and had it go to town. The end result of this was an unnecessarily abstracted Go project (I doubt anybody would use that directory structure for a project this size) that looked like it would do the job.
Now onto actually running this tool. For this to work, it would require both my new Forgejo server and my old Gitea server to be up at the same time. First hurdle: conflicting ports. This was solved pretty easily.
I screwed up here yet again, by running Forgejo on the primary domain and deploying the old Gitea server into a new Tailscale-based service. This caused multiple failures:
Inability to log into the Gitea instance.
I could no longer sign into Gitea since I had 2FA enabled using Passkeys which require the domains to match up.
To make up for the inability to log into my Gitea server to create an access token, I just used the gitea CLI instead.
sudo -i su - gitea
# To get the actual path to the Gitea binary, which weirdly isn't installed for the gitea user?
systemctl status gitea.service
/nix/store/foo-bar-baz-gitea-1.23.1/bin/gitea admin -w $(pwd) user generate-access-token --token-name forgejo-migration --scopes "read:repository,read:user" --raw
On the forgejo side, a similar dance ensued but this time to actually create my account since this was a fresh start.
sudo -i su - forgejo
systemctl status forgejo
# yes the actual CLI seems to still be available as gitea
/nix/store/foo-bar-baz-forgejo-10.0.0/bin/gitea admin user create -w $(pwd) --username msfjarvis --email me@msfjarvis.dev --admin --access-token --access-token-name forgejo-migration --access-token-scopes "read:user,write:repository"
With access tokens in hand, I ran the tool and hit my second problem.
Tailscale ACLs bite me in the ass
The way Tailscale’s networking shebang works meant that the server running this isolated Gitea service couldn’t connect to it via the network without some tweaks to the ACL policies. I was not feeling like I had this extra debugging in me, so I opted to just temporarily hijack a different service’s domain and put Gitea on it then resumed from there.
Mirrors of GitHub private repos did not work
Back when I first created mirrors of all my stuff I had also done so for my private repos and just forgot about it. When the tool tried to migrate them they obviously failed to mirror since I wasn’t providing any access tokens for GitHub. I instructed Claude to add a retry for this and pull an access token from the $GITHUB_TOKEN
environment variable.
All said and done
Overall this was generally worthwhile, both for documenting it for others as well as for me to finally be on Forgejo. I trust the people in charge significantly more than I do for Gitea, and they continue to deliver great work driven in part by them actually using their own software at scale.