neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
Android client, and the mobile API it speaks
ea1f5be · Gabriel Morell · 2026-09-12 14:58
Message
{commit_body(@commit)}
Files changed
added
android/.gitignore
+12
−0
| 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
| 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