# Building, testing and releasing Everything a contributor or maintainer needs and a user doesn't. The README covers what Omnigit is and how to install it. ## Running it Needs the [.NET 10 SDK](https://dotnet.microsoft.com/download). ```bash dotnet run --project src/Omnigit/Omnigit.csproj ``` In VS Code, F5 works via the checked-in `.vscode/launch.json`. (If you press F5 with a `.axaml` file focused *before* that config exists, VS Code offers to find an "Avalonia XAML debugger" — decline it; no such extension is needed.) ## Project layout ``` src/Omnigit/ Models/ Domain types — hosts, accounts, repos, commits, diffs Services/ GitService.cs libgit2 implementation of IGitService GitService.CommitOperations.cs tags, reverts, cherry-picks, resets and conflicts UnifiedDiffParser.cs libgit2 patch text -> renderable diff rows HostResolver.cs origin URL -> which hosting site a clone belongs to WebLinks.cs clone + commit -> the page to open in a browser AccountStore.cs accounts split between JSON and the keyring CredentialStore.cs keyring / Keychain / DPAPI, with a 0600 fallback ActivityLog.cs what the console shows RepositoryStore.cs known-clone list, JSON in the app-data dir RepositoryWatcher.cs notices work done outside the app, debounced DesktopIntegration.cs the AppImage's own .desktop entry and icons FolderPicker.cs native folder dialog via StorageProvider SystemShell.cs open a browser, reach the clipboard MockData.cs design-time sample content only HostProviders/ GitHub in C#, everything else from a JSON manifest ViewModels/ MainWindowViewModel + per-item view models and prompts Views/ MainWindow, RepositoryView, DiffView, CloneView, SettingsView Styles/ Tokens.axaml (theme colours + icons), Controls.axaml (control styles) Assets/ App icon tests/Omnigit.Tests/ xunit — pure functions, plus branch switching, commit operations and conflicts against real throwaway repositories build/ version.sh the one place the version is read from release.sh bump, write release notes, commit, tag linux/ package.sh, .desktop entry, hicolor icons, AppImage, Flatpak windows/ package.ps1, Inno Setup script macos/ package.sh, sign.sh, entitlements, Info.plist template .github/workflows/ ci.yml Linux build on push/PR release.yml Full platform matrix, tag-triggered ``` All colours are defined once in `Styles/Tokens.axaml`, per theme variant, and referenced with `DynamicResource`. To restyle the app, that's the only file you need. ## Tests ```bash dotnet test ``` Some cover the pure functions, which are the parts that break silently: the unified-diff parser, the remote-URL resolution behind repository grouping, the commit-link builder, the `Link`-header paging that repository listing depends on, and the manifest field mapping — including the round trip through the settings form, since a lost field there would quietly mislabel every repository on a site. The rest run against **real repositories** created in a temp directory, because what makes them worth writing is what libgit2 actually leaves behind: - **Branch switching.** Carrying only some uncommitted files across a checkout takes two stashes and six steps, and a mistake there loses work that was never committed — the one place in this codebase where a bug is unrecoverable, so it is verified rather than reasoned about. - **Commit operations.** Tagging, branching from an older commit, detaching onto a commit, reverting, cherry-picking and all three reset modes — each asserted on the state left in the repository, including the half-applied ones. - **Conflicts.** Keeping either side, marking resolved by hand, finishing with a commit and abandoning outright, each checked against the index rather than the working tree. - **Sync state.** What the sync button is told about a branch, including one pushed without an upstream, against a bare "origin" beside the working copy. - **Publish state.** A branch pushed under one name and then renamed locally — git carries the upstream across, so it claims to be published and in sync while its new name is on no server. It has to read as a new branch, and publishing has to send it to a branch of the same name. - **Staging.** That a deleted file, and both halves of a moved one, actually reach the commit — a change that never reaches the list can never be ticked, so it survives every commit without anyone being told. - **Pull requests.** Fetching a pull request's head from an origin that really has a `refs/pull/1/head`, checking it out again after it has been added to, and refusing to move a branch that has diverged from it. The manifest side is covered against a stub `HttpMessageHandler`, reading the manifests that actually ship rather than a copy — so a typo in `gitea.json` fails a test instead of a sign-in. LibGit2Sharp bundles its own native library, so none of this needs git installed. Nothing here talks to the network or the UI. Those are still checked by hand against a real Gitea server — see the docker one-liner in [architecture.md](architecture.md). ## Building installers Each script publishes a **self-contained** build, so end users don't need .NET installed. ```bash # Linux — .deb, .rpm, .tar.gz and .AppImage (needs fpm: gem install fpm) ./build/linux/package.sh linux-x64 ./build/linux/package.sh linux-arm64 # Linux — .flatpak (needs flatpak: flatpak install flathub org.flatpak.Builder) ./build/linux/flatpak/package.sh # macOS — .app bundle inside a .dmg (run on macOS) ./build/macos/package.sh osx-arm64 # Windows — portable .zip and an Inno Setup installer pwsh build/windows/package.ps1 -Rid win-x64 ``` Artifacts land in `dist/`. None of those commands names a version: every one of them defaults to `` in `Omnigit.csproj`, which is the single place it is written down. Pass one as the last argument to override it, as release CI does. The macOS build signs and notarises itself when the `APPLE_*` variables are set — see the list at the top of `build/macos/sign.sh` — and produces the same unsigned bundle when they aren't, so a build on a machine without the certificate still works. It is also the one target published with `PublishSingleFile`, because `Contents/MacOS` is a code location and codesign will not seal a bundle with loose managed assemblies in it. The AppImage is built by `build/linux/appimage.sh`, which `package.sh` calls once the publish is staged. `appimagetool` and the AppImage runtime are downloaded on first use and cached in `build/.tools/`; without network access that step is skipped and the other three artifacts are still produced. Run the script on its own to rebuild only the AppImage — it reuses whatever `package.sh` last staged instead of publishing again: ```bash ./build/linux/appimage.sh linux-x64 ``` The architecture of the output comes from the runtime handed to `appimagetool`, not from the machine running it, so the arm64 AppImage cross-builds from an x64 runner like the rest of the Linux artifacts. **The Flatpak is the exception to that.** Its build runs inside a sandbox on the runtime for the host architecture, so it cannot be cross-built and each architecture needs a runner of its own. It also builds with no network at all, which means every NuGet package has to be listed and hashed in advance — so **bumping a `PackageReference` means regenerating `build/linux/flatpak/nuget-sources-*.json`**, and CI checks that you did. [flatpak.md](flatpak.md) covers that, the permissions the sandbox asks for and why, and how a release reaches Flathub. ## Releasing ```bash ./build/release.sh 1.2.3 "What changed, in a sentence or two." git push origin HEAD && git push origin v1.2.3 ``` That bumps `` in `Omnigit.csproj`, adds a `` entry to the metainfo, commits and tags. Those two edits are all a release needs, and both have to be *inside* the tagged commit rather than derived from the tag: the csproj is what the app reports and what the Flatpak build reads (it never passes `-p:Version`, because Flathub would not either), and the metainfo entry is the release notes a software centre shows — Flathub rejects a build whose newest `` isn't the version being built. A tag names a commit; it can't put anything in one. Everything else is derived from the tag by CI: artifact names, the GitHub Release, the Flathub manifest. Nothing is pushed until you say so, since pushing the tag is what starts a forty-minute build. Two guards, because both edits are the kind you can forget: `release.sh` refuses a dirty tree, a version that already exists, or one the csproj already claims, and `release.yml` refuses to build at all if the tag and the csproj disagree — rather than shipping fourteen artifacts labelled one version by an app that calls itself another. `release.yml` runs the tests first — `ci.yml` only triggers on pushes to a branch, so without that gate a tag would package untested code — then builds every target and attaches the results to a GitHub Release: | Platform | Artifacts | | --- | --- | | Linux x64 / arm64 | `.deb`, `.rpm`, `.tar.gz`, `.AppImage` | | Linux x86_64 / aarch64 | `.flatpak`, one runner each — it is the one thing here that can't cross-build | | Windows x64 | `-setup.exe`, `.zip` | | macOS arm64 / x64 | `.dmg`, signed and notarised | You can also run it manually from the Actions tab and pass a version. Once Omnigit is on Flathub, `flathub.yml` picks up from there: a successful release opens the pull request that publishes it, leaving the test build for a human to install and merge. It needs one repository secret and does nothing at all until that exists — [flatpak.md](flatpak.md) has both the one-time submission and the token. **Every release is checked against a list of what it should contain.** `build/expected-artifacts.sh` names the fourteen files a complete release has, and `build/verify-artifacts.sh` enforces it — once on each runner right after packaging, once when every set is gathered together, and once against the published release itself. That last one is the one that matters: every packaging script treats a missing tool as a skip and still exits 0, and `gh release upload` has exited 0 having quietly not uploaded two of them. Every one of those failures has happened, and all of them looked like a green build. **On CI shape:** the release workflow runs **only** on tags or manual dispatch, and cross-publishes both Linux RIDs from one Linux runner and both macOS RIDs from one macOS runner. Only the Flatpak needs a runner per architecture, because it is the one artifact that cannot be cross-built. Everyday CI is Linux-only and build-only, because this is one codebase with no per-platform branches, so a three-platform matrix on every push would mostly buy you a longer wait for the same answer — plus a few seconds validating the desktop entry, the AppStream file and the Flatpak's NuGet lists, none of which the build would ever notice were wrong.