Android: Resolve Merge Conflicts #337

Closed
opened 2026-10-25 22:06:35 +01:00 by kfickel · 2 comments
Owner
  • Don't fail if there are new remote commits, but merge goes without problems
  • Prettier "Stream is okay" display (do a mock-up for that)
  • Merge Conflict editor
* Don't fail if there are new remote commits, but merge goes without problems * Prettier "Stream is okay" display (do a mock-up for that) * Merge Conflict editor
Collaborator

Plan: Android — merge on pull, stream check card, conflict resolver

The design reference is the handover attached to this issue: 337-handover.tar.gz (plan/SPEC.md, plan/screens/*.png, plan/mockup/changes-screen-mockup.html). Read SPEC.md and look at the screenshots first. This plan maps it onto the code, and it wins where the two differ. The places where it differs are listed under Decisions.

Problems Merge conflict Resolver Edit by hand

Decisions (agreed during refinement)

  1. "Can't read file" lists the problems the core really has. The core never fails to parse a note: unreadable means only an I/O or encoding failure, and an unclosed code fence is valid Markdown. So the group lists:

    • the unreadable files (from the store),
    • the file-name problems of fileProblems (an undated file, an unconfigured file type),
    • a new core check for leftover conflict markers, with their line.

    There is no code-fence lint and no quick fixes. The editor banner shows the message and "Go to line N".

  2. A pull with uncommitted changes commits them first. If an author is set, the open changes are committed with the commitSummary message, then the merge runs. Without an author nothing is touched, and the pull says it needs an author first.

  3. Automatic merging only goes as far as a clean merge. The background pull of the reminder worker, and a rejected auto-push, merge and push again only when the merge is clean. On a conflict they abort without changing anything and show a warning: a banner on the Stream and Changes tabs, plus the amber dot. The conflict state only ever comes from a pull the user starts. The background never commits for the user: with uncommitted files it only fast-forwards, and warns if a merge would be needed.

  4. The hunk logic lives in the Rust core, on plain text, and is bound through streamd-ffi. JGit only merges, writes the markers and stages files.

1. Core (crates/streamd-core)

New module document/conflicts.rs (pure, no I/O):

  • conflicts(text) -> Option<Vec<ConflictHunk>>: each hunk has its index, its start_line (the <<<<<<< line, 1-based), the ours and theirs lines, and heading, the nearest Markdown heading above it (## 16:30), if there is one. A diff3 base section (|||||||) is skipped. Returns None when the markers are malformed (unbalanced, nested); the resolver then offers only "Edit by hand".
  • enum ConflictChoice { Ours, Theirs, Both }. Both puts ours first.
  • resolve_conflicts(text, choices: &[ConflictChoice]) -> Result<String, CoreError>: replaces every hunk by its choice and keeps everything else byte for byte, line endings included. A count that doesn't match is an error.
  • conflict_markers(text) -> Vec<usize>: the lines of every marker line (<<<<<<< , =======, >>>>>>> , |||||||). Only lines that are part of a conflict block count, so that a lone =======, such as a setext heading underline, is no marker.

Stream check, api::stream_check(project, store) -> StreamCheck. It reads every file once and returns:

  • notes: the number of listed files,
  • tags: the number of configured tags,
  • unreadable: each file with its message,
  • misnamed: each file with its fileProblems,
  • marked: each file with its marker lines (the first line is what a row shows),
  • unplaced: each tag with its file count and its first file, from unplaced_tags.

It reuses unplaced_tags_with and file_problems, so there is no second wording. document/problems.rs gets the message of a file with leftover markers.

Tests: parsing (simple, two hunks, diff3, CRLF, malformed, a lone =======), resolve_conflicts for every choice, marker lines, and stream_check on a MemoryStore (with with_unreadable).

2. Bindings (crates/streamd-ffi)

  • Records ConflictHunk and StreamCheck with its row records. The enum ConflictChoice.
  • Free functions conflicts, resolveConflicts and conflictMarkers, which need no project, and project.streamCheck(store).
  • Line numbers cross as u32. Conversion tests, as for the other records.

3. Git layer (android/.../git/)

GitRepository / JGitRepository:

  • pull(remote) merges in place of fast-forward only: setFastForward(FF), with the message Merge origin/<branch> and the author of the settings, which pull now takes. Pull becomes UP_TO_DATE, UPDATED (a fast-forward), MERGED (a clean merge commit), CONFLICTED and DIVERGED. DIVERGED is kept for "a merge is needed but was not allowed", which a pull with allowMerge = false (the background) returns.
  • After a conflicting merge, JGitRepository rewrites the labels of the markers in each conflicted file once: JGit writes HEAD / refs/remotes/origin/<branch>, the files get <<<<<<< this phone / >>>>>>> origin/<branch>. Only the label of those two marker lines changes.
  • abortMerge(): a hard reset to HEAD, then clear MERGE_HEAD and MERGE_MSG.
  • merge(): MergeState? is null when not merging. Otherwise it has the conflicted files: the path, the hunk count (the marker blocks in the working file), whether the file is resolved (staged, no longer in status().conflicting), and the commit time of the last commit that touched the path on each side (HEAD and MERGE_HEAD). The merge state survives an app restart because it is on disk.
  • markResolved(path): git add the path.
  • finishMerge(author): commits the index as the merge commit with MERGE_MSG.
  • RepositoryStatus gets merging: MergeState?, read in status(), so the existing flows carry it.

RepositoryConnection:

  • pull() (from the user): with uncommitted files, commit them first (decision 2; PullState.NeedsAuthor when no author is set). Then merge. MERGED pushes if pushAfterCommit is on. CONFLICTED leaves the repository merging and keeps pulledAt. The stream reloads on UPDATED, MERGED and CONFLICTED, since the files changed.
  • backgroundPull() for notify/Reminders.kt: with a clean tree it merges and pushes. On a conflict it aborts at once and sets PullState.NeedsYou. With a dirty tree it only fast-forwards, and DIVERGED sets NeedsYou too.
  • commitSaved: returns at once while merging, so auto-commit is paused. When its push is REJECTED, it pulls like the background does (clean merge and push again, else abort and NeedsYou).
  • written(id) while merging: if the file is conflicted and has no markers left, it is staged. StreamAccess asks the session for conflictMarkers of the written text and calls repository.resolved(id). The marker check stays in the core.
  • New: abortMerge() (SyncState.Aborted), resolve(path, text) (writes and stages), conflictText(path) (reads a file of the repository, also outside the notes folder), and finishMerge(). Finishing commits with the author, pushes if the toggle is on (SyncState.Merged(pushed)), then commits any saves made during the merge as an ordinary commit when every save is committed.
  • RepositoryActions / ChangesActions get abortMerge, finishMerge and the resolver calls. RepositoryAdapter implements them and reloads the stream after an abort, a resolve or a finish.

4. Stream check card (spec §1)

  • StreamHealth is replaced by a plain StreamCheck: the counts and the rows of each group, and checkedAt. StreamSession.check() binds project.streamCheck(store) on Dispatchers.IO and keeps the last result for the badge.
  • The Changes screen asks for the check every time it is shown, and again whenever the StreamVersion changes (after a pull, a merge or a resolve, and on coming back from the editor or the resolver). It shows the "Checking" state only after 150 ms without a result.
  • StatusCard loses the green sentence. A new StreamCheckCard (in ui/Changes.kt, or ui/StreamCheck.kt if it grows) follows the mockup:
    • Checking: a neutral border, a spinning icon, N notes · M tags.
    • Valid: the positive tint and border, a plain tick, … · checked on open, HH:mm.
    • Problems: the warning tint, or error when a file can't be read or has markers. The groups are CAN'T READ FILE (unreadable, markers, file-name problems; trailing "Open") and NOT DEFINED IN .STREAMD.TOML (@Tag used in N notes, the first file +N; trailing "Define"). Errors come first.
  • The colours come from StreamdTheme.colors and the colour scheme, never from the mockup's hex values. New icons (tick, tag, file, merge, check-circle) are Material Symbols vector drawables, as Icons.kt describes.
  • The badge of the Changes tab becomes an amber dot (changesBadge → changesDot) when there are uncommitted files, unpushed commits, a merge, a NeedsYou pull, or problems in the last check.

5. Clickable problems (spec §2)

  • Undefined tag: opens the form of a new tag with its name filled in. ConfigRequest gets a prefill that ConfigEditScreen puts into the name of a new tag; the dimension starts at tagStart. Back returns to Changes. This builds on the existing forms; there is no new Tags screen.
  • File row: opens the editor (CaptureRequest(file, line)), which selects the failing line on opening. A banner above the text gives the message, line N · Streamd can't read this file (or the wording of the problem), and "Go to line N". The editor has no gutter, so the marked line gets a red tint through the existing VisualTransformation, not a gutter number.
  • The check of the open file runs again as the user types, debounced, on the core's conflictMarkers and fileProblems. When it passes, the banner turns green: "File reads cleanly · save to clear the problem".
  • Saving checks the stream again. If problems remain, the user stays in the editor with the snackbar "Saved · still N problems". If the save cleared the last one: "Saved file.md · stream is valid".
  • An unreadable file can't be opened in the editor; its row has no "Open" and shows the message only.

6. Merge conflicts (spec §3, §4)

Changes screen while status.merging != null:

  • The subtitle gets · merging. The pull icon, the pull to refresh and the "Pull" menu item are disabled.
  • The status line says "Pull needs your help." The sync card says "Merge conflict with origin/", pull stopped HH:mm · N conflicts in M files, and the red note, worded with "another device", since the app doesn't know the device.
  • The section "Conflicted files" (X of Y resolved) replaces the commit section. It has a row per file (red merge icon or green check, the path in monospace, N conflicts / resolved, a chevron) that opens the resolver.
  • The buttons are "Abort" (outlined) and "Finish merge & push", or "Finish merge" when push is off. Finish is disabled until every file is resolved.
  • The overflow menu has Pull, Push (only with unpushed commits), Abort merge (only while merging), a divider, Remote settings, and the existing Discard, which is disabled while merging. Abort shows a snackbar. The auto-commit switch shows "Paused until the merge is finished."
  • A NeedsYou pull shows a warning banner ("origin/ and this phone changed the same lines. Pull here to resolve them.") with a Pull action, on Changes and on the Stream tab, where the banner of a failed pull is.

The resolver, ui/Conflict.kt, is a new destination without the bottom bar (ConflictRequest(path).route):

  • ConflictScreen loads conflictText and the hunks. ConflictContent takes the hunks, the choices and the callbacks, for the screenshot tests.
  • The top bar has back, "Resolve conflict" and the path. Below is the intro line, then a card per hunk: "Conflict i of n", the heading, a THIS PHONE block (the primary colour, with its time) and an ORIGIN/ block (the tertiary colour, with its time). The buttons are "Keep phone", "Keep origin" and "Keep both", the last one emphasised. A chosen hunk collapses to a green RESULT block with "Change".
  • The choices are kept in rememberSaveable. The bottom bar has "Edit by hand" (outlined) and "Mark resolved", which is enabled when every hunk has a choice. It runs resolveConflicts, then resolve(path, text), and goes back.
  • Malformed markers (conflicts gives None) show only "Edit by hand".
  • Edit by hand opens the editor on the file (files in the notes folder only; elsewhere the button is disabled with a hint). The banner says "N conflicts still marked in this file · keep the lines you want, delete the marker lines", every marker line is tinted, and the subtitle gets · merging. Saving with no markers left stages the file through written (§3).
  • A new interface Resolving and its plain types (Hunk, Choice, ConflictFile) live in git/, beside RepositoryActions. A ConflictsAdapter combines RepositoryConnection (read, write, stage) with StreamSession (conflicts, resolveConflicts, conflictMarkers, the only calls to the bindings).

7. Tests

  • Core: as in §1. FFI: the conversions.
  • JGitRepository on a bare repository in a temporary folder: a fast-forward, a clean merge (MERGED, two parents), a conflict (CONFLICTED, relabelled markers, merge() lists the file), markResolved + finishMerge, abortMerge (the working tree and HEAD as before the pull), and a background pull with a dirty tree (DIVERGED, nothing touched).
  • RepositoryConnection on the fake GitRepository: commit-first on pull, NeedsAuthor, auto-commit paused while merging, a rejected auto-push that merges cleanly or aborts with NeedsYou, written staging a file without markers, and finishing with and without push.
  • Pure UI logic: changesDot, the grouping and order of the check rows, and the counting of X of Y resolved.
  • Screenshot tests (one scene each): ChangesScreenshots for checking, valid, problems, merging (0 of 2), all resolved and NeedsYou. ConflictScreenshots for no choice yet, one chosen and malformed. CaptureScreenshots for the problem banner, the green banner and the marker banner. Record them with updateDebugScreenshotTest and look at every image.

8. Documentation

  • REQUIREMENTS R35: merge on pull, commit-first, the background/auto-push rule, the conflict state, the resolver, abort and finish, auto-commit paused. Also the stream check (§1/§2) where the Changes tab is specified.
  • README (the Changes tab, merging).
  • AGENTS.md: the new core module and API, the FFI functions, the git/ additions, ui/Conflict.kt, ConflictsAdapter, Resolving, StreamCheck replacing StreamHealth, and the dot badge.

Order of commits (suggested)

  1. feat(core): conflict hunks and the stream check, with the FFI bindings.
  2. feat(android): merge on pull, abort and finish a merge: the git layer and RepositoryConnection, with tests.
  3. feat(android): stream check card with clickable problems.
  4. feat(android): resolve merge conflicts on the phone: the resolver, edit by hand and the merging state of Changes.
  5. docs: …, if not folded into the commits above.

nix flake check must pass, and nix build .#streamd-app too (it runs the unit and screenshot tests).

Out of scope

As in the spec: choices finer than a whole hunk, a diff viewer change, and the other screens. Also, from the decisions: lints for valid Markdown such as unclosed fences, and quick fixes.

# Plan: Android — merge on pull, stream check card, conflict resolver The design reference is the handover attached to this issue: [337-handover.tar.gz](https://git.konstantinfickel.de/attachments/a9d64882-bbfc-456a-b900-44be6377649d) (`plan/SPEC.md`, `plan/screens/*.png`, `plan/mockup/changes-screen-mockup.html`). **Read `SPEC.md` and look at the screenshots first.** This plan maps it onto the code, and it wins where the two differ. The places where it differs are listed under **Decisions**. | Problems | Merge conflict | Resolver | Edit by hand | |---|---|---|---| | ![](https://git.konstantinfickel.de/attachments/90a32465-6c53-4a18-9280-51b43a4cfe5b) | ![](https://git.konstantinfickel.de/attachments/a3f6e231-2e6e-46a5-900a-2f8470ae97aa) | ![](https://git.konstantinfickel.de/attachments/ff4e8ada-3345-4a2e-92d1-0206990c93ea) | ![](https://git.konstantinfickel.de/attachments/b35d2d14-e898-48b5-991d-70a57b13a1a8) | ## Decisions (agreed during refinement) 1. **"Can't read file" lists the problems the core really has.** The core never fails to parse a note: `unreadable` means only an I/O or encoding failure, and an unclosed code fence is valid Markdown. So the group lists: - the unreadable files (from the store), - the file-name problems of `fileProblems` (an undated file, an unconfigured file type), - a **new core check for leftover conflict markers**, with their line. There is **no code-fence lint and no quick fixes**. The editor banner shows the message and "Go to line N". 2. **A pull with uncommitted changes commits them first.** If an author is set, the open changes are committed with the `commitSummary` message, then the merge runs. Without an author nothing is touched, and the pull says it needs an author first. 3. **Automatic merging only goes as far as a clean merge.** The background pull of the reminder worker, and a rejected auto-push, merge and push again only when the merge is clean. On a conflict they abort without changing anything and **show a warning**: a banner on the Stream and Changes tabs, plus the amber dot. The conflict state only ever comes from a pull the user starts. The background never commits for the user: with uncommitted files it only fast-forwards, and warns if a merge would be needed. 4. **The hunk logic lives in the Rust core**, on plain text, and is bound through `streamd-ffi`. JGit only merges, writes the markers and stages files. ## 1. Core (`crates/streamd-core`) New module `document/conflicts.rs` (pure, no I/O): - `conflicts(text) -> Option<Vec<ConflictHunk>>`: each hunk has its `index`, its `start_line` (the `<<<<<<<` line, 1-based), the `ours` and `theirs` lines, and `heading`, the nearest Markdown heading above it (`## 16:30`), if there is one. A diff3 base section (`|||||||`) is skipped. Returns `None` when the markers are malformed (unbalanced, nested); the resolver then offers only "Edit by hand". - `enum ConflictChoice { Ours, Theirs, Both }`. `Both` puts ours first. - `resolve_conflicts(text, choices: &[ConflictChoice]) -> Result<String, CoreError>`: replaces every hunk by its choice and keeps everything else byte for byte, line endings included. A count that doesn't match is an error. - `conflict_markers(text) -> Vec<usize>`: the lines of every marker line (`<<<<<<< `, `=======`, `>>>>>>> `, `|||||||`). Only lines that are part of a conflict block count, so that a lone `=======`, such as a setext heading underline, is no marker. Stream check, `api::stream_check(project, store) -> StreamCheck`. It reads every file once and returns: - `notes`: the number of listed files, - `tags`: the number of configured tags, - `unreadable`: each file with its message, - `misnamed`: each file with its `fileProblems`, - `marked`: each file with its marker lines (the first line is what a row shows), - `unplaced`: each tag with its file count and its first file, from `unplaced_tags`. It reuses `unplaced_tags_with` and `file_problems`, so there is no second wording. `document/problems.rs` gets the message of a file with leftover markers. Tests: parsing (simple, two hunks, diff3, CRLF, malformed, a lone `=======`), `resolve_conflicts` for every choice, marker lines, and `stream_check` on a `MemoryStore` (with `with_unreadable`). ## 2. Bindings (`crates/streamd-ffi`) - Records `ConflictHunk` and `StreamCheck` with its row records. The enum `ConflictChoice`. - Free functions `conflicts`, `resolveConflicts` and `conflictMarkers`, which need no project, and `project.streamCheck(store)`. - Line numbers cross as `u32`. Conversion tests, as for the other records. ## 3. Git layer (`android/.../git/`) `GitRepository` / `JGitRepository`: - `pull(remote)` merges in place of fast-forward only: `setFastForward(FF)`, with the message `Merge origin/<branch>` and the author of the settings, which `pull` now takes. `Pull` becomes `UP_TO_DATE`, `UPDATED` (a fast-forward), `MERGED` (a clean merge commit), `CONFLICTED` and `DIVERGED`. `DIVERGED` is kept for "a merge is needed but was not allowed", which a pull with `allowMerge = false` (the background) returns. - After a conflicting merge, `JGitRepository` rewrites the labels of the markers in each conflicted file once: JGit writes `HEAD` / `refs/remotes/origin/<branch>`, the files get `<<<<<<< this phone` / `>>>>>>> origin/<branch>`. Only the label of those two marker lines changes. - `abortMerge()`: a hard reset to `HEAD`, then clear `MERGE_HEAD` and `MERGE_MSG`. - `merge(): MergeState?` is null when not merging. Otherwise it has the conflicted files: the path, the hunk count (the marker blocks in the working file), whether the file is resolved (staged, no longer in `status().conflicting`), and the commit time of the last commit that touched the path on each side (`HEAD` and `MERGE_HEAD`). The merge state survives an app restart because it is on disk. - `markResolved(path)`: `git add` the path. - `finishMerge(author)`: commits the index as the merge commit with `MERGE_MSG`. - `RepositoryStatus` gets `merging: MergeState?`, read in `status()`, so the existing flows carry it. `RepositoryConnection`: - `pull()` (from the user): with uncommitted files, commit them first (decision 2; `PullState.NeedsAuthor` when no author is set). Then merge. `MERGED` pushes if `pushAfterCommit` is on. `CONFLICTED` leaves the repository merging and keeps `pulledAt`. The stream reloads on `UPDATED`, `MERGED` and `CONFLICTED`, since the files changed. - `backgroundPull()` for `notify/Reminders.kt`: with a clean tree it merges and pushes. On a conflict it aborts at once and sets `PullState.NeedsYou`. With a dirty tree it only fast-forwards, and `DIVERGED` sets `NeedsYou` too. - `commitSaved`: returns at once while merging, so auto-commit is paused. When its push is `REJECTED`, it pulls like the background does (clean merge and push again, else abort and `NeedsYou`). - `written(id)` while merging: if the file is conflicted and has no markers left, it is staged. `StreamAccess` asks the session for `conflictMarkers` of the written text and calls `repository.resolved(id)`. The marker check stays in the core. - New: `abortMerge()` (`SyncState.Aborted`), `resolve(path, text)` (writes and stages), `conflictText(path)` (reads a file of the repository, also outside the notes folder), and `finishMerge()`. Finishing commits with the author, pushes if the toggle is on (`SyncState.Merged(pushed)`), then commits any saves made during the merge as an ordinary commit when every save is committed. - `RepositoryActions` / `ChangesActions` get `abortMerge`, `finishMerge` and the resolver calls. `RepositoryAdapter` implements them and reloads the stream after an abort, a resolve or a finish. ## 4. Stream check card (spec §1) - `StreamHealth` is replaced by a plain `StreamCheck`: the counts and the rows of each group, and `checkedAt`. `StreamSession.check()` binds `project.streamCheck(store)` on `Dispatchers.IO` and **keeps the last result** for the badge. - The Changes screen asks for the check every time it is shown, and again whenever the `StreamVersion` changes (after a pull, a merge or a resolve, and on coming back from the editor or the resolver). It shows the "Checking" state only after 150 ms without a result. - `StatusCard` loses the green sentence. A new `StreamCheckCard` (in `ui/Changes.kt`, or `ui/StreamCheck.kt` if it grows) follows the mockup: - **Checking:** a neutral border, a spinning icon, `N notes · M tags`. - **Valid:** the `positive` tint and border, a plain tick, `… · checked on open, HH:mm`. - **Problems:** the `warning` tint, or `error` when a file can't be read or has markers. The groups are **CAN'T READ FILE** (unreadable, markers, file-name problems; trailing "Open") and **NOT DEFINED IN .STREAMD.TOML** (`@Tag used in N notes`, the first file `+N`; trailing "Define"). Errors come first. - The colours come from `StreamdTheme.colors` and the colour scheme, never from the mockup's hex values. New icons (tick, tag, file, merge, check-circle) are Material Symbols vector drawables, as `Icons.kt` describes. - The badge of the Changes tab becomes an **amber dot** (`changesBadge` → `changesDot`) when there are uncommitted files, unpushed commits, a merge, a `NeedsYou` pull, or problems in the last check. ## 5. Clickable problems (spec §2) - **Undefined tag:** opens the form of a **new tag** with its name filled in. `ConfigRequest` gets a `prefill` that `ConfigEditScreen` puts into the name of a new tag; the dimension starts at `tagStart`. Back returns to Changes. This builds on the existing forms; there is no new Tags screen. - **File row:** opens the editor (`CaptureRequest(file, line)`), which selects the failing line on opening. A banner above the text gives the message, `line N · Streamd can't read this file` (or the wording of the problem), and "Go to line N". The editor has no gutter, so the marked line gets a red tint through the existing `VisualTransformation`, not a gutter number. - The check of the open file runs again as the user types, debounced, on the core's `conflictMarkers` and `fileProblems`. When it passes, the banner turns green: "File reads cleanly · save to clear the problem". - **Saving** checks the stream again. If problems remain, the user stays in the editor with the snackbar "Saved · still N problems". If the save cleared the last one: "Saved file.md · stream is valid". - An unreadable file can't be opened in the editor; its row has no "Open" and shows the message only. ## 6. Merge conflicts (spec §3, §4) Changes screen while `status.merging != null`: - The subtitle gets ` · merging`. The pull icon, the pull to refresh and the "Pull" menu item are disabled. - The status line says "Pull needs your help." The sync card says "Merge conflict with origin/<branch>", `pull stopped HH:mm · N conflicts in M files`, and the red note, worded with "another device", since the app doesn't know the device. - The section "Conflicted files" (`X of Y resolved`) replaces the commit section. It has a row per file (red merge icon or green check, the path in monospace, `N conflicts` / `resolved`, a chevron) that opens the resolver. - The buttons are "Abort" (outlined) and "Finish merge & push", or "Finish merge" when push is off. Finish is disabled until every file is resolved. - The overflow menu has Pull, Push (only with unpushed commits), Abort merge (only while merging), a divider, Remote settings, and the existing Discard, which is disabled while merging. Abort shows a snackbar. The auto-commit switch shows "Paused until the merge is finished." - A `NeedsYou` pull shows a warning banner ("origin/<branch> and this phone changed the same lines. Pull here to resolve them.") with a Pull action, on Changes and on the Stream tab, where the banner of a failed pull is. The resolver, `ui/Conflict.kt`, is a new destination without the bottom bar (`ConflictRequest(path).route`): - `ConflictScreen` loads `conflictText` and the hunks. `ConflictContent` takes the hunks, the choices and the callbacks, for the screenshot tests. - The top bar has back, "Resolve conflict" and the path. Below is the intro line, then a card per hunk: "Conflict i of n", the heading, a **THIS PHONE** block (the primary colour, with its time) and an **ORIGIN/<BRANCH>** block (the tertiary colour, with its time). The buttons are "Keep phone", "Keep origin" and "Keep both", the last one emphasised. A chosen hunk collapses to a green **RESULT** block with "Change". - The choices are kept in `rememberSaveable`. The bottom bar has "Edit by hand" (outlined) and "Mark resolved", which is enabled when every hunk has a choice. It runs `resolveConflicts`, then `resolve(path, text)`, and goes back. - Malformed markers (`conflicts` gives `None`) show only "Edit by hand". - **Edit by hand** opens the editor on the file (files in the notes folder only; elsewhere the button is disabled with a hint). The banner says "N conflicts still marked in this file · keep the lines you want, delete the marker lines", every marker line is tinted, and the subtitle gets ` · merging`. Saving with no markers left stages the file through `written` (§3). - A new interface `Resolving` and its plain types (`Hunk`, `Choice`, `ConflictFile`) live in `git/`, beside `RepositoryActions`. A `ConflictsAdapter` combines `RepositoryConnection` (read, write, stage) with `StreamSession` (`conflicts`, `resolveConflicts`, `conflictMarkers`, the only calls to the bindings). ## 7. Tests - **Core:** as in §1. **FFI:** the conversions. - **`JGitRepository`** on a bare repository in a temporary folder: a fast-forward, a clean merge (`MERGED`, two parents), a conflict (`CONFLICTED`, relabelled markers, `merge()` lists the file), `markResolved` + `finishMerge`, `abortMerge` (the working tree and `HEAD` as before the pull), and a background pull with a dirty tree (`DIVERGED`, nothing touched). - **`RepositoryConnection`** on the fake `GitRepository`: commit-first on pull, `NeedsAuthor`, auto-commit paused while merging, a rejected auto-push that merges cleanly or aborts with `NeedsYou`, `written` staging a file without markers, and finishing with and without push. - **Pure UI logic:** `changesDot`, the grouping and order of the check rows, and the counting of `X of Y resolved`. - **Screenshot tests** (one scene each): `ChangesScreenshots` for checking, valid, problems, merging (0 of 2), all resolved and `NeedsYou`. `ConflictScreenshots` for no choice yet, one chosen and malformed. `CaptureScreenshots` for the problem banner, the green banner and the marker banner. Record them with `updateDebugScreenshotTest` and look at every image. ## 8. Documentation - REQUIREMENTS **R35**: merge on pull, commit-first, the background/auto-push rule, the conflict state, the resolver, abort and finish, auto-commit paused. Also the stream check (§1/§2) where the Changes tab is specified. - README (the Changes tab, merging). - AGENTS.md: the new core module and API, the FFI functions, the `git/` additions, `ui/Conflict.kt`, `ConflictsAdapter`, `Resolving`, `StreamCheck` replacing `StreamHealth`, and the dot badge. ## Order of commits (suggested) 1. `feat(core): conflict hunks and the stream check`, with the FFI bindings. 2. `feat(android): merge on pull, abort and finish a merge`: the git layer and `RepositoryConnection`, with tests. 3. `feat(android): stream check card with clickable problems`. 4. `feat(android): resolve merge conflicts on the phone`: the resolver, edit by hand and the merging state of Changes. 5. `docs: …`, if not folded into the commits above. `nix flake check` must pass, and `nix build .#streamd-app` too (it runs the unit and screenshot tests). ## Out of scope As in the spec: choices finer than a whole hunk, a diff viewer change, and the other screens. Also, from the decisions: lints for valid Markdown such as unclosed fences, and quick fixes.
Collaborator

Implementation note

Implemented in #339 (branch 337_merge-conflicts, 6 commits).

  • Time: about 50 minutes for the implementation (17:38–18:27), after about 25 minutes of refinement. That includes a few minutes waiting for GPG signing to be unblocked.
  • Tokens: about 285k for the implementation, about 450k for the whole session including refinement.

Findings during the implementation

  • JGit MERGE_MSG: after a conflict, JGit writes # Conflicts: comment lines into MERGE_MSG. Unlike the git CLI it doesn't strip them, so they would end up in the commit message. The merge commit therefore takes its message from the stored label (Merge origin/<branch>) instead.
  • MERGING_RESOLVED: once every conflict is staged, JGit reports the repository state as MERGING_RESOLVED instead of MERGING. Both count as a merge that waits.
  • Conflicts are forgotten once staged: Git no longer lists a staged file as conflicting, so "X of Y resolved" couldn't be read from the status. The marked files and their hunk counts are kept in .git/STREAMD_CONFLICTS beside MERGE_HEAD.
  • Marker labels: JGit labels its markers HEAD and refs/remotes/origin/<branch>. They are rewritten to this phone and origin/<branch> right after the merge. The core's parser accepts any label anyway.
  • Merge commit author: the merge runs with setCommit(false) and the commit is made by hand, so it gets the configured author. Otherwise JGit would use the empty user configuration of the app.
  • Unreadable files: the spec's "can't read file" parse errors don't exist in the core, as found during refinement. The group shows unreadable files and leftover conflict markers instead, and misnamed files got a group of their own.
  • Clean merges after a rejected auto-push: the commit first stages the file, then the background-style pull merges cleanly and pushes again. Tested on the fake repository; not tried on a device.

Not done or different

  • No quick fixes and no code-fence lint, as agreed.
  • No gutter in the editor; lines are tinted instead.
  • Nothing was tried on a device or emulator.
## Implementation note Implemented in #339 (branch `337_merge-conflicts`, 6 commits). - **Time:** about 50 minutes for the implementation (17:38–18:27), after about 25 minutes of refinement. That includes a few minutes waiting for GPG signing to be unblocked. - **Tokens:** about 285k for the implementation, about 450k for the whole session including refinement. ### Findings during the implementation - **JGit `MERGE_MSG`:** after a conflict, JGit writes `# Conflicts:` comment lines into `MERGE_MSG`. Unlike the git CLI it doesn't strip them, so they would end up in the commit message. The merge commit therefore takes its message from the stored label (`Merge origin/<branch>`) instead. - **`MERGING_RESOLVED`:** once every conflict is staged, JGit reports the repository state as `MERGING_RESOLVED` instead of `MERGING`. Both count as a merge that waits. - **Conflicts are forgotten once staged:** Git no longer lists a staged file as conflicting, so "X of Y resolved" couldn't be read from the status. The marked files and their hunk counts are kept in `.git/STREAMD_CONFLICTS` beside `MERGE_HEAD`. - **Marker labels:** JGit labels its markers `HEAD` and `refs/remotes/origin/<branch>`. They are rewritten to `this phone` and `origin/<branch>` right after the merge. The core's parser accepts any label anyway. - **Merge commit author:** the merge runs with `setCommit(false)` and the commit is made by hand, so it gets the configured author. Otherwise JGit would use the empty user configuration of the app. - **Unreadable files:** the spec's "can't read file" parse errors don't exist in the core, as found during refinement. The group shows unreadable files and leftover conflict markers instead, and misnamed files got a group of their own. - **Clean merges after a rejected auto-push:** the commit first stages the file, then the background-style pull merges cleanly and pushes again. Tested on the fake repository; not tried on a device. ### Not done or different - No quick fixes and no code-fence lint, as agreed. - No gutter in the editor; lines are tinted instead. - Nothing was tried on a device or emulator.
Sign in to join this conversation.
No labels
android
cli
epic
planned
tui
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
kfickel/streamd#337
No description provided.