an Unofficial application around https://git.gay website, with support for what you need.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-26 15:12:27 +02:00
.forgejo/workflows fix: bump the version code instead of rebuilding it 2026-09-26 15:03:42 +02:00
.idea initial commit 2026-08-27 23:05:46 +02:00
app chore: bump to 1.0.2 2026-09-26 15:12:27 +02:00
gradle feat: redesign in-app updates on Material 3 Expressive 2026-09-26 13:27:53 +02:00
screenshots initial commit 2026-08-27 23:05:46 +02:00
.gitignore initial commit 2026-08-27 23:05:46 +02:00
build.gradle.kts initial commit 2026-08-27 23:05:46 +02:00
gradle.properties feat: sign in with a one-time code by exchanging it for a token 2026-09-26 11:29:28 +02:00
gradlew initial commit 2026-08-27 23:05:46 +02:00
gradlew.bat initial commit 2026-08-27 23:05:46 +02:00
LICENSE initial commit 2026-08-27 23:05:46 +02:00
README.md feat: identify releases by versionCode 2026-09-26 13:49:25 +02:00
settings.gradle.kts initial commit 2026-08-27 23:05:46 +02:00

GitGay

A native Android client for git.gay, a queer-run Git hosting platform built on Forgejo. GitGay is a dark Material 3 app with a selection of colorful themes, and it talks directly to the Forgejo REST API.

This is the mobile companion for git.gay. Anything you can do in the web interface, you can do from your phone: browse repositories, read code, open issues and pull requests, watch your notifications, and manage your account.

Screenshots

Home Explore Repository
Home Explore Repository
Search Settings Themes
Search Settings Themes

Features

  • Sign in with your git.gay username, password and two-factor code, the same three fields the web login asks for
  • A home feed with trending and recent repositories, plus your own repos
  • Repository browsing: file tree, source files with light syntax highlighting, commit history and diffs, issues and pull requests, star, fork and watch
  • Issues and pull requests: read, create, comment, and reply
  • Organizations and user profiles, members, followers and following
  • Notifications
  • Search for repositories, users, and organizations
  • Account management: SSH keys, GPG keys, access tokens, webhooks, packages, action runners, organizations, and blocked users
  • In-app updates: checks the project's own releases and installs a downloaded APK through the system installer
  • An About screen with dependency licenses

Themes

GitGay is dark-only by design, but ships with several accent themes. You can switch between them any time from Settings, and the app picks up the change on the spot.

Theme Accent
Pink (git.gay Dark) #FF69B4
Monochrome #D7D7D7
Green #93E0A8
Blue #8CC8FF
Purple #D3BDFF
Teal #86E7DA
Yellow #EECA4F
Orange #FFB77B
Red #FFB4AB
Material You follows the system

The Monochrome theme uses true OLED black for its background, with only slightly brighter elevated surfaces to keep the UI readable on AMOLED panels.

Tech stack

  • Kotlin 2.2 and Jetpack Compose (Material 3, BOM 2026.02)
  • Android Gradle Plugin 9.3 with Gradle 9.5
  • Retrofit, OkHttp and Gson for the API layer
  • Coil for images, Markwon for Markdown, AndroidSVG for vector rendering
  • DataStore, Android Security Crypto, and Navigation Compose
  • minSdk 28, targetSdk 37, dark-only UI

Building

The project uses the standard Android Gradle build. With the Android SDK installed and local.properties (or ANDROID_HOME) pointing at it:

./gradlew :app:assembleDebug     # debug build, signed with the debug key
./gradlew :app:assembleRelease   # release build

The release APK lands in app/build/outputs/apk/release/.

Signing

Release signing keys are kept private to the project. The keystore is never committed; instead it is stored as a Forgejo Actions secret and reconstructed by the build workflow, so every published APK is signed with the same key and no one can slip in a release built with a different one.

The CI signing secrets are:

Secret Value
ANDROID_KEYSTORE_B64 the keystore file as base64
ANDROID_KEYSTORE_PATH /opt/signing.keystore
ANDROID_KEYSTORE_PASSWORD the keystore password
ANDROID_KEY_ALIAS the key alias
ANDROID_KEY_PASSWORD the key password

To set this up, generate a keystore outside the repo and set the secrets:

keytool -genkeypair -keystore signing.keystore -alias gitgay \
  -keyalg RSA -keysize 2048 -validity 10000 \
  -dname "CN=GitGay Release, O=GitGay"
# set the passwords you used as ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_PASSWORD
base64 -w0 signing.keystore   # put the output in ANDROID_KEYSTORE_B64

Keep the keystore file and passwords safe and back them up. Losing them means you can no longer sign updates for an already-published app.

Locally, ./gradlew :app:assembleRelease signs with the debug keystore so it works out of the box. To use a real key locally, create a keystore.properties in the project root (gitignored) with storeFile, storePassword, keyAlias and keyPassword, or set the ANDROID_KEYSTORE_* environment variables.

Sign-in

GitGay signs in with your git.gay username, password, and — if your account uses two-factor authentication — the current six-digit code from your authenticator. One form, the same three fields the web login asks for.

How the code is used once, and then never again

This is the part that decides the whole design, so it is worth being explicit.

git.gay validates the two-factor code on every HTTP Basic request (services/auth/method/basic.go → validateTOTP, reached from reqBasicAuth on each call). A six-digit code is only good for a 30-second window, so an app that kept using Basic auth would be locked out the moment the code rolled over. Storing the code cannot fix this: HMAC-SHA1 is a pseudorandom function, so the code for the next window cannot be derived from the code you already have. The TOTP secret is no help either — git.gay only ever renders it on the enrolment page (twofa_enroll.tmpl is the sole template that does), so for an account that is already enrolled there is nowhere to read it back from.

The web UI does not have this problem, because it does not re-check anything after login — typing the code once yields a session cookie. GitGay does the same thing one step later. Sign-in makes a single request:

POST /api/v1/users/{username}/tokens
Authorization: Basic base64(user:password)
X-Gitea-OTP: 123456

That route accepts Basic auth (reqBasicOrRevProxyAuth → reqBasicAuth), so the code is validated on that one request and the response is a long-lived access token. The app stores the token and every later request sends Authorization: token …. AccessToken.Verify goes straight from the token lookup to AuthenticationSuccess and never calls validateTOTP, so two-factor is not re-checked and nothing expires.

Net effect: you type the code once, exactly like on the web, and stay signed in. The code is never stored — it is meaningless seconds later — and neither is the password. Only the token is kept.

Accounts without two-factor leave the code field empty; git.gay skips the check entirely for those rather than rejecting the request.

Why not browser sign-in or a hand-made token

Both were tried and both are worse:

  • OAuth needs a redirect URI. Forgejo's registration form validates redirect URIs with ValidUrlList → IsValidURL (modules/validation/helpers.go), which permits only http/https, so the usual gitgay://oauth/callback custom scheme cannot be registered. A loopback http://127.0.0.1/ redirect does work, but it needs the app to stay alive across a browser round trip, and it gains nothing over the form above since the password is needed either way.
  • Asking users to paste a token defeats two-factor and is a poor thing to ship.

There is no cookie option either: Forgejo's API auth group (routers/api/shared/middleware.go) registers no session method and passes a nil session store, so a browser session cannot authenticate /api/v1/* at all. Only the web router registers one, and the web frontend never calls the JSON API.

The token

Sign-in creates one access token named GitGay Mobile with the all scope, so the app can do everything the web UI can. It appears in Settings → Applications on git.gay, where it can be revoked — revoking it signs the app out, and signing in again creates a fresh one. The stored token is cleared on sign-out.

Sign-in is idempotent. git.gay enforces unique token names and answers a duplicate with 400 access token name has been used already, so a second sign-in would fail outright. The old token also cannot be reused, because the list endpoint returns only token_last_eight and never the secret. So sign-in lists the account's tokens, deletes any that are named exactly GitGay Mobile, and creates a replacement — all within the same sign-in, so the one-time code is still valid. Tokens the user created in git.gay's own settings are matched by exact name and never touched.

This fails safe: if the delete succeeds but the create does not, the next attempt finds no token of ours and creates one, so there is no state that needs manual recovery beyond re-entering the code.

The token is a bearer credential, so it is worth the usual care: it is a full account login that does not re-check two-factor. That is the same trust the web places in its session cookie, and it is stored in EncryptedSharedPreferences.

Continuous integration

Two Forgejo Actions workflows keep the project building and versioned automatically.

Build and release

.forgejo/workflows/release.yml runs on every push to main. It provisions the Android SDK, uses a cached Gradle home, signs the release APK with the keystore restored from secrets, and then:

  1. reads the current versionName from app/build.gradle.kts,
  2. creates the tag v<versionName> and pushes it (skipping it if it already exists),
  3. publishes a release on the releases page with a single asset, app.apk.

So merging dev into main is all you need: the workflow tags the merge with the version that dev already bumped to, and uploads the APK under that tag.

Automatic version bumps

.forgejo/workflows/version-bump.yml runs on every push to the dev branch. It counts the commits that changed anything other than the version file, then sets versionCode to 100000 + n, where n is that count, and commits the bump back to dev. versionName is left untouched: it names the release a build belongs to, so it is set when a release is cut rather than on every push. Because the bump itself only touches the version file, it never counts itself, so the workflow cannot loop.

In-app updates

GitGay checks its own repository for a newer release and offers to install it, either from a pill over the bottom navigation or manually under Settings → Check for updates.

Releases are identified by versionCode, not versionName. release.yml tags each build v<versionCode>, and the app parses that tag back into a build number and offers the update when it is greater than its own. Comparing one integer means no name-normalisation rules, and a tag that is not a build number is skipped rather than guessed at. versionName is not part of the update path and is not maintained by the bump workflow.

It compares versionName against the latest published release, ignoring drafts and prereleases. Downloading and release checks deliberately bypass the authenticated HTTP client: the app should reach its public repository even when the stored token is stale or was issued for a different instance, and the BODY-level logging used for API debugging would otherwise try to buffer a whole APK in memory.

A downloaded APK is verified before it is offered to the system installer — the file must be a readable ZIP/APK and its manifest must carry this app's own package name, so a wrong or tampered asset is rejected rather than handed to the installer. Installation itself goes through the system installer, which shows its own confirmation; GitGay cannot install silently, and REQUEST_INSTALL_PACKAGES means the user has to grant permission the first time.

Releases

Releases are cut automatically. The version is bumped on dev and, when you merge dev into main, the build workflow tags the merge as v<version> and attaches app.apk to a new release. There is no need to tag by hand:

git checkout main
git merge dev
git push origin main   # build job tags v<version> and publishes the release

Each release contains exactly one attached APK named app.apk, signed with the project's private key.

Tests

./gradlew :app:testDebugUnitTest            # JVM unit tests
./gradlew :app:connectedDebugAndroidTest    # instrumentation tests, needs a device

License

GitGay is free software released under the GNU General Public License, version 3. See LICENSE for the full text.