7.3 KiB · text History ea1f5be

GitGud Android client

Native Android client for GitGud. Talks to the mobile JSON API at /api/mobile/v1/..., authenticated with a Personal Access Token scanned from a QR on the forge’s own settings page.

Sister app to ../../waxx/android — same Compose / OKLCh palette / B612-mono shell, so they feel like one product family.

What it does

Surface
Repos Every repo the token can reach — owned, org, team-granted — with the latest CI status per row, filterable by namespace
CI Runs, jobs, steps and logs; re-run a whole run, re-run one job in place, cancel a run in flight. Polls while anything is queued or running
Issues List by state, read with server-rendered markdown, comment, quote-reply, close and reopen
Pull requests List by state (merged is its own state), read description + reviews + discussion + line comments, comment, approve / request changes, merge — with the repo’s merge blockers spelled out
Wiki Page index and rendered pages (read-only)

Stack

Layer Choice
Language / UI Kotlin 2.0 · Jetpack Compose · Material 3
HTTP Retrofit · OkHttp · kotlinx.serialization
Storage EncryptedSharedPreferences (AES via the Android Keystore)
Scanner com.journeyapps:zxing-android-embedded (ScanContract)
Build Gradle 9.0 · AGP 8.5.2 · JDK 17+
minSdk 26 (Android 8) · targetSdk 34

Single-activity, single-module, no DI framework — dependencies are constructed inline. Adding Hilt when the screen count justifies it is a fine follow-up.

Layout

android/
  app/src/main/
    AndroidManifest.xml
    java/org/neiam/gitgud/app/
      MainActivity.kt            # nav host, gitgud://pair dispatch, theme provider
      data/
        TokenStore.kt            # bearer + base URL (https-coerced)
        ThemeStore.kt            # selected theme key
        PairPayload.kt           # gitgud://pair URI parser
        GitGudClient.kt          # Retrofit interface + factory + error unwrapping
        Models.kt                # all the JSON DTOs
      auth/
        PairScreen.kt            # QR scan + paste fallback + scope check
      ui/
        RepoListScreen.kt        # home: repos + namespace filter + theme picker
        RepoScreen.kt            # one repo, four tabs
        CiTab.kt                 # run list (polls while active)
        RunScreen.kt             # jobs + steps + re-run / cancel
        LogScreen.kt             # step-grouped logs, no wrapping
        IssuesTab.kt             # issue list + shared state filter
        IssueScreen.kt           # issue + comments + composer; CommentCard
        PullsTab.kt              # PR list
        PullScreen.kt            # PR + reviews + comments + review/merge dialogs
        WikiTab.kt               # page index + WikiPageScreen
        Composer.kt              # comment box + quote-reply helper
        RichText.kt              # renders the API's body_html
        StatusChip.kt            # CI status + issue/PR state vocabulary
        Common.kt                # AppBarTitle, empty states, relative times
        theme/                   # 9 palettes ported from assets/css/app.css
    res/font/                    # B612 regular + bold

Building

cd android
echo "sdk.dir=$HOME/Android/Sdk" > local.properties   # or set ANDROID_HOME
./gradlew :app:assembleDebug

Produces app/build/outputs/apk/debug/app-debug.apk. Install it:

./gradlew :app:installDebug
# or:
adb install -r app/build/outputs/apk/debug/app-debug.apk

Pairing

  1. Sign in on the web and go to Settings → API tokens.
  2. Tick Mobile app, name the token, and create it.
  3. A QR appears next to the plaintext token. It encodes gitgud://pair?base=<server>&token=<ggp_…>.
  4. In the app, tap Scan QR and point at it. The app verifies the token against GET /api/mobile/v1/me before storing anything, and warns if the token is missing part of the mobile scope bundle.

There’s no separate “connected devices” list: a paired device is its token, so revoking it on the tokens page signs that phone out.

The QR carries a bearer token in plaintext. Treat a screenshot of it the way you’d treat a screenshot of a password.

Running against a local dev server

The emulator reaches the host as http://10.0.2.2:4000; a physical device on the same LAN needs the host’s LAN IP. GitGudWeb.Endpoint.url() is what gets encoded into the QR, so set the endpoint’s :url config to match before generating one — otherwise the QR points at localhost, which on a phone means the phone.

TokenStore coerces http://<fqdn> to https://<fqdn> on save and load. IPs, localhost and unqualified hostnames are left alone, so the LAN and emulator paths still work.

Scopes

The Mobile app checkbox is a bundle, not a scope. It ticks:

repo:read  ci:read  ci:write  issues:read  issues:write
pulls:read pulls:write  wiki:read

A paired phone carries those individually, so you can revoke one surface later — mint a token without pulls:write, say, and the PR screen loses its merge and review buttons while everything else keeps working. Endpoints check the scope the action needs and the user’s permission on the repository: a scope says what the token may ask for, never what its owner is allowed to do.

Themes

⋮ on the repo list → Theme offers the same nine palettes as the web (Dark, Light, Her, After Dark, Forest, Sky, Clays, Stones, Blueprint). The OKLCh values are ported from the @plugin "daisyui-theme" blocks in assets/css/app.css and converted at runtime, so adding a theme on the web is a copy of three numbers rather than a trip through a color picker.

CI status colors (green / red / amber) are deliberately not per-theme — “this build failed” has to read the same on every palette.

Selection persists in gitgud_prefs, a separate EncryptedSharedPreferences file from credentials, so signing out doesn’t clobber it.

Rendering

Issue bodies, comments, PR descriptions and wiki pages arrive as both body (markdown source) and body_html (rendered server-side). The app displays the HTML through HtmlCompat in a TextView, because the forge already renders GFM with cross-references, alerts, footnotes and syntax highlighting — shipping a second markdown dialect on the phone would mean two things that disagree about the same text.

The cost: tables and images flatten to text. Every repo screen has an “open on the web” action for those.

What’s not shipped yet

  • Wiki editing — the wiki is a git repo of markdown files, and editing one on a phone means resolving conflicts on a phone. wiki:write exists as a scope for whenever that’s worth doing.
  • Opening issues and PRs — the app comments on and closes existing ones. Opening a PR needs a branch picker and a diff preview to be anything but a guess.
  • Diff view — line comments are listed with their file and line rather than anchored to a diff. The code review screen is the one place the web’s extra width genuinely earns its keep.
  • Push notifications — no FCM wiring. “Is anything red” is a pull today; the repo list answers it in one screen.
  • Launcher icon — using the framework default. Drop an IconKitchen mipmap-* set into app/src/main/res/ and point the manifest at it.