Android client, and the mobile API it speaks

ea1f5be · Gabriel Morell · 2026-09-12 14:58

94 files +6875 -19
Message
{commit_body(@commit)}

Files changed

added android/.gitignore
+12 −0
@@ -0,0 +1,12 @@
1 +*.iml
2 +.gradle/
3 +local.properties
4 +.idea/
5 +.DS_Store
6 +build/
7 +captures/
8 +.externalNativeBuild
9 +.cxx/
10 +*.apk
11 +*.aab
12 +keys/release.jks
added android/README.md
+172 −0
@@ -0,0 +1,172 @@
1 +# GitGud Android client
2 +
3 +Native Android client for [GitGud](../). Talks to the mobile JSON API
4 +at `/api/mobile/v1/...`, authenticated with a Personal Access Token
5 +scanned from a QR on the forge's own settings page.
6 +
7 +Sister app to `../../waxx/android` — same Compose / OKLCh palette /
8 +B612-mono shell, so they feel like one product family.
9 +
10 +## What it does
11 +
12 +| Surface | |
13 +|---|---|
14 +| **Repos** | Every repo the token can reach — owned, org, team-granted — with the latest CI status per row, filterable by namespace |
15 +| **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 |
16 +| **Issues** | List by state, read with server-rendered markdown, comment, quote-reply, close and reopen |
17 +| **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 |
18 +| **Wiki** | Page index and rendered pages (read-only) |
19 +
20 +## Stack
21 +
22 +| Layer | Choice |
23 +|---|---|
24 +| Language / UI | Kotlin 2.0 · Jetpack Compose · Material 3 |
25 +| HTTP | Retrofit · OkHttp · kotlinx.serialization |
26 +| Storage | EncryptedSharedPreferences (AES via the Android Keystore) |
27 +| Scanner | `com.journeyapps:zxing-android-embedded` (`ScanContract`) |
28 +| Build | Gradle 9.0 · AGP 8.5.2 · JDK 17+ |
29 +| minSdk | 26 (Android 8) · targetSdk 34 |
30 +
31 +Single-activity, single-module, no DI framework — dependencies are
32 +constructed inline. Adding Hilt when the screen count justifies it is a
33 +fine follow-up.
34 +
35 +## Layout
36 +
37 +```
38 +android/
39 + app/src/main/
40 + AndroidManifest.xml
41 + java/org/neiam/gitgud/app/
42 + MainActivity.kt # nav host, gitgud://pair dispatch, theme provider
43 + data/
44 + TokenStore.kt # bearer + base URL (https-coerced)
45 + ThemeStore.kt # selected theme key
46 + PairPayload.kt # gitgud://pair URI parser
47 + GitGudClient.kt # Retrofit interface + factory + error unwrapping
48 + Models.kt # all the JSON DTOs
49 + auth/
50 + PairScreen.kt # QR scan + paste fallback + scope check
51 + ui/
52 + RepoListScreen.kt # home: repos + namespace filter + theme picker
53 + RepoScreen.kt # one repo, four tabs
54 + CiTab.kt # run list (polls while active)
55 + RunScreen.kt # jobs + steps + re-run / cancel
56 + LogScreen.kt # step-grouped logs, no wrapping
57 + IssuesTab.kt # issue list + shared state filter
58 + IssueScreen.kt # issue + comments + composer; CommentCard
59 + PullsTab.kt # PR list
60 + PullScreen.kt # PR + reviews + comments + review/merge dialogs
61 + WikiTab.kt # page index + WikiPageScreen
62 + Composer.kt # comment box + quote-reply helper
63 + RichText.kt # renders the API's body_html
64 + StatusChip.kt # CI status + issue/PR state vocabulary
65 + Common.kt # AppBarTitle, empty states, relative times
66 + theme/ # 9 palettes ported from assets/css/app.css
67 + res/font/ # B612 regular + bold
68 +```
69 +
70 +## Building
71 +
72 +```sh
73 +cd android
74 +echo "sdk.dir=$HOME/Android/Sdk" > local.properties # or set ANDROID_HOME
75 +./gradlew :app:assembleDebug
76 +```
77 +
78 +Produces `app/build/outputs/apk/debug/app-debug.apk`. Install it:
79 +
80 +```sh
81 +./gradlew :app:installDebug
82 +# or:
83 +adb install -r app/build/outputs/apk/debug/app-debug.apk
84 +```
85 +
86 +## Pairing
87 +
88 +1. Sign in on the web and go to **Settings → API tokens**.
89 +2. Tick **Mobile app**, name the token, and create it.
90 +3. A QR appears next to the plaintext token. It encodes
91 + `gitgud://pair?base=<server>&token=<ggp_…>`.
92 +4. In the app, tap **Scan QR** and point at it. The app verifies the
93 + token against `GET /api/mobile/v1/me` before storing anything, and
94 + warns if the token is missing part of the mobile scope bundle.
95 +
96 +There's no separate "connected devices" list: a paired device *is* its
97 +token, so revoking it on the tokens page signs that phone out.
98 +
99 +The QR carries a bearer token in plaintext. Treat a screenshot of it
100 +the way you'd treat a screenshot of a password.
101 +
102 +### Running against a local dev server
103 +
104 +The emulator reaches the host as `http://10.0.2.2:4000`; a physical
105 +device on the same LAN needs the host's LAN IP. `GitGudWeb.Endpoint.url()`
106 +is what gets encoded into the QR, so set the endpoint's `:url` config to
107 +match before generating one — otherwise the QR points at `localhost`,
108 +which on a phone means the phone.
109 +
110 +`TokenStore` coerces `http://<fqdn>` to `https://<fqdn>` on save and
111 +load. IPs, `localhost` and unqualified hostnames are left alone, so the
112 +LAN and emulator paths still work.
113 +
114 +## Scopes
115 +
116 +The **Mobile app** checkbox is a bundle, not a scope. It ticks:
117 +
118 +```
119 +repo:read ci:read ci:write issues:read issues:write
120 +pulls:read pulls:write wiki:read
121 +```
122 +
123 +A paired phone carries those individually, so you can revoke one
124 +surface later — mint a token without `pulls:write`, say, and the PR
125 +screen loses its merge and review buttons while everything else keeps
126 +working. Endpoints check the scope the action needs *and* the user's
127 +permission on the repository: a scope says what the token may ask for,
128 +never what its owner is allowed to do.
129 +
130 +## Themes
131 +
132 +⋮ on the repo list → **Theme** offers the same nine palettes as the web
133 +(Dark, Light, Her, After Dark, Forest, Sky, Clays, Stones, Blueprint).
134 +The OKLCh values are ported from the `@plugin "daisyui-theme"` blocks in
135 +`assets/css/app.css` and converted at runtime, so adding a theme on the
136 +web is a copy of three numbers rather than a trip through a color
137 +picker.
138 +
139 +CI status colors (green / red / amber) are deliberately *not*
140 +per-theme — "this build failed" has to read the same on every palette.
141 +
142 +Selection persists in `gitgud_prefs`, a separate
143 +EncryptedSharedPreferences file from credentials, so signing out doesn't
144 +clobber it.
145 +
146 +## Rendering
147 +
148 +Issue bodies, comments, PR descriptions and wiki pages arrive as both
149 +`body` (markdown source) and `body_html` (rendered server-side). The app
150 +displays the HTML through `HtmlCompat` in a `TextView`, because the
151 +forge already renders GFM with cross-references, alerts, footnotes and
152 +syntax highlighting — shipping a second markdown dialect on the phone
153 +would mean two things that disagree about the same text.
154 +
155 +The cost: tables and images flatten to text. Every repo screen has an
156 +"open on the web" action for those.
157 +
158 +## What's not shipped yet
159 +
160 +- **Wiki editing** — the wiki is a git repo of markdown files, and
161 + editing one on a phone means resolving conflicts on a phone.
162 + `wiki:write` exists as a scope for whenever that's worth doing.
163 +- **Opening issues and PRs** — the app comments on and closes existing
164 + ones. Opening a PR needs a branch picker and a diff preview to be
165 + anything but a guess.
166 +- **Diff view** — line comments are listed with their file and line
167 + rather than anchored to a diff. The code review screen is the one
168 + place the web's extra width genuinely earns its keep.
169 +- **Push notifications** — no FCM wiring. "Is anything red" is a pull
170 + today; the repo list answers it in one screen.
171 +- **Launcher icon** — using the framework default. Drop an IconKitchen
172 + `mipmap-*` set into `app/src/main/res/` and point the manifest at it.
added android/app/build.gradle.kts
+67 −0

Click to load diff…

added android/app/proguard-rules.pro
+10 −0

Click to load diff…

added android/app/src/main/AndroidManifest.xml
+40 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/MainActivity.kt
+233 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/auth/PairScreen.kt
+177 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/data/GitGudClient.kt
+224 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/data/Models.kt
+332 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/data/PairPayload.kt
+28 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/data/ThemeStore.kt
+35 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/data/TokenStore.kt
+90 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/CiTab.kt
+126 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/Common.kt
+109 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/Composer.kt
+103 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/IssueScreen.kt
+261 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/IssuesTab.kt
+131 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/LogScreen.kt
+193 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/PullScreen.kt
+461 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/PullsTab.kt
+115 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/RepoListScreen.kt
+324 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/RepoScreen.kt
+138 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/RichText.kt
+78 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/RunScreen.kt
+332 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/StatusChip.kt
+128 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/WikiTab.kt
+144 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/theme/AppTheme.kt
+162 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/theme/Theme.kt
+22 −0

Click to load diff…

added android/app/src/main/java/org/neiam/gitgud/app/ui/theme/Typography.kt
+39 −0

Click to load diff…

modified ?
+0 −0

Click to load diff…

modified ?
+0 −0

Click to load diff…

added android/app/src/main/res/values/strings.xml
+4 −0

Click to load diff…

added android/app/src/main/res/values/themes.xml
+4 −0

Click to load diff…

added android/app/src/main/res/xml/data_extraction_rules.xml
+11 −0

Click to load diff…

added android/build.gradle.kts
+7 −0

Click to load diff…

added android/gradle.properties
+4 −0

Click to load diff…

added android/gradle/libs.versions.toml
+46 −0

Click to load diff…

modified ?
+0 −0

Click to load diff…

added android/gradle/wrapper/gradle-wrapper.properties
+7 −0

Click to load diff…

added android/gradlew
+251 −0

Click to load diff…

added android/gradlew.bat
+94 −0

Click to load diff…

added android/settings.gradle.kts
+25 −0

Click to load diff…

added docs/mobile-api.md
+171 −0

Click to load diff…

modified lib/git_gud/accounts/personal_access_token.ex
+60 −4

Click to load diff…

modified lib/git_gud/issues.ex
+7 −0

Click to load diff…

added lib/git_gud_web/controllers/api/mobile/auth.ex
+69 −0

Click to load diff…

added lib/git_gud_web/controllers/api/mobile/ci_controller.ex
+192 −0

Click to load diff…

added lib/git_gud_web/controllers/api/mobile/helpers.ex
+80 −0

Click to load diff…

added lib/git_gud_web/controllers/api/mobile/issue_controller.ex
+127 −0

Click to load diff…

added lib/git_gud_web/controllers/api/mobile/json.ex
+291 −0

Click to load diff…

added lib/git_gud_web/controllers/api/mobile/pull_request_controller.ex
+198 −0

Click to load diff…

added lib/git_gud_web/controllers/api/mobile/repository_controller.ex
+97 −0

Click to load diff…

added lib/git_gud_web/controllers/api/mobile/session_controller.ex
+29 −0

Click to load diff…

added lib/git_gud_web/controllers/api/mobile/wiki_controller.ex
+46 −0

Click to load diff…

modified lib/git_gud_web/live/personal_access_token_live/index.ex
+133 −15

Click to load diff…

modified lib/git_gud_web/router.ex
+41 −0

Click to load diff…

added test/git_gud_web/controllers/api/mobile/mobile_api_test.exs
+294 −0

Click to load diff…

added test/git_gud_web/controllers/api/mobile/mobile_ci_test.exs
+215 −0

Click to load diff…

added test/git_gud_web/live/api_token_scopes_test.exs
+86 −0

Click to load diff…

Parents: 88a7933