Two Machines, One Setup – with Forgejo, chezmoi and ghorg
Table of Contents
I work on two machines – a notebook and a desktop, both running CachyOS. All my code and configuration lives on my own Forgejo instance at git.minifisch.at. Here’s what the whole thing looks like in the end:
(dotfiles)"] repos["Project repos"] end subgraph nb["Notebook"] nbk[("Keyring
token")] end subgraph dt["Desktop"] dtk[("Keyring
token")] end dot <-- "chezmoi" --> nb dot <-- "chezmoi" --> dt repos -- "ghorg clone" --> nb repos -- "ghorg clone" --> dt
VS Code without telemetry and AI extras #
It started with a fresh VS Code install. The first thing to go was everything that phones home: telemetry, experiments, the online settings search and automatic updates (those come through pacman anyway). The built-in chat and Copilot features are switched off too – there’s an official master switch for that by now:
{
"telemetry.telemetryLevel": "off",
"workbench.enableExperiments": false,
"update.mode": "manual",
"chat.disableAIFeatures": true
}
The obvious way to sync all this would be the built-in Settings Sync. But it requires a Microsoft or GitHub account and runs through their servers – exactly what I had just been getting rid of.
Dotfiles with chezmoi #
Instead I use chezmoi, which I already had in place for my dotfiles. The source repo lives on Forgejo, and with autoCommit and autoPush every change ends up there right away. Newly added:
~/.config/Code/User/settings.json– the VS Code settings, including all extension settings~/.gitconfig– my Git identity~/.zshrc– already tracked, but it got a few new functions (more on those below)
On the other machine, a chezmoi update is all it takes to be on the same state.
chezmoi add …, or edit the source directly with chezmoi edit --apply …. Just pick one and stick with it.Every repo at once: ghorg #
From GitLab I knew glab, which can clone every repo in a group in one go. For Forgejo there’s forgejo-cli (fj), a very good command-line tool for issues, pull requests and releases. What fj can’t do is list repos – every command works on exactly one repo you name.
For bulk cloning I therefore use ghorg. Forgejo is compatible with the Gitea API, so it runs on the gitea backend. The config in ~/.config/ghorg/conf.yaml looks roughly like this:
GHORG_SCM_TYPE: gitea
GHORG_SCM_BASE_URL: https://git.minifisch.at
GHORG_CLONE_TYPE: user
GHORG_CLONE_PROTOCOL: ssh
GHORG_ABSOLUTE_PATH_TO_CLONE_TO: /home/patrick/dev
GHORG_SKIP_ARCHIVED: true
GHORG_CONCURRENCY: 4
With that, ghorg clone minifisch puts all my repos under ~/dev/minifisch/<repo>. The nice part: running it again updates every existing clone and fetches any new repos. So it’s also my “bring this machine up to date” command.
minifisch is a user, not an organisation – hence clone-type: user. A --dry-run is also worth it the first time, and --no-clean makes sure ghorg doesn’t reset existing repos.The token in the keyring, not in a file #
ghorg needs a Forgejo token with read access. I didn’t want it in the config file.
autoPush, it ends up in the Git repo on Forgejo right away.So the token lives in the system keyring:
secret-tool store --label='ghorg forgejo' service ghorg host git.minifisch.at
A small function in .zshrc fetches it on every call:
ghorg() {
local token
token=$(secret-tool lookup service ghorg host git.minifisch.at) || {
echo "No token in keyring. Store it with:" >&2
echo " secret-tool store --label='ghorg forgejo' service ghorg host git.minifisch.at" >&2
return 1
}
GHORG_GITEA_TOKEN="$token" command ghorg "$@"
}
The function contains no secret, so it’s fine to sync it with chezmoi. Each machine gets its own token, so I can revoke either one on its own at any time.
Order on disk and in the editor #
The repos are now laid out the same way as on the server: ~/dev/<owner>/<repo>. I open them from the command line, one repo per window – simply code ~/dev/minifisch/webpage. Settings that only matter for one project live in that repo’s .vscode/ folder, so they come along through Forgejo automatically.
I only need a workspace in the sense of a .code-workspace file when a task really spans several repos at once. What I avoid: opening all of ~/dev as one folder. With lots of nested Git repos, VS Code’s source control view quickly becomes a mess.
So I don’t have to type paths, there’s also a small fzf picker:
dev() {
local repo
repo=$(fd -H -t d --prune -g .git "${DEV_ROOT:-$HOME/dev}" -x echo {//} | sort |
fzf --query="$*" --select-1 --preview 'git -C {} log --oneline -10') && code "$repo"
}
dev lists every repo under ~/dev with its latest commits in the preview, and dev web opens the matching repo straight away if there’s only one hit.
What deliberately isn’t synced #
Not everything belongs in a Git repo:
- Trusted folders in VS Code: these live in VS Code’s internal state (an SQLite database) that each machine rewrites constantly. Besides, “I trust this folder” is a security decision I want to make per machine.
- The extensions themselves: their settings come along via
settings.json, but I install them on each machine myself. - The Forgejo token: in each machine’s own keyring.
Conclusion #
- chezmoi syncs the configuration.
- ghorg brings the repos to every machine.
- fj handles working with Forgejo itself.
- The keyring keeps the token out of every file.
Everything runs on my own infrastructure, and a new machine is set up in a few minutes with chezmoi init --apply, a token in the keyring and one ghorg clone minifisch.