feat: git sync, entry editing and password generator #2

Merged
schaefera merged 35 commits from sync-and-editing into main 2026-10-01 10:53:44 +00:00
Collaborator

Adds adding and editing entries, a configurable password generator, and automatic git sync with retries.

What's in it

  • Editor. Add and edit entries. The username and notes lines keep their layout, so pass and browserpass read the entry as before. Each save is committed with pass-style messages, encrypting to the nearest .gpg-id.
  • Generator. Length 8–128 (default 25), character classes, your own symbol set, and an option to leave out look-alike characters. The last-used options are remembered.
  • Sync. Fetch, then fast-forward or merge, then run the re-encryption guard, then push, retrying up to 3 times when another device pushes in between.
    • Local commits are the queue of changes waiting to be pushed.
    • Conflicts go to the newer change, judged per entry by the last commit that really changed it; merge commits and re-encryption commits don't count as changes.
    • Deletes follow the same rule: the later of an edit and a delete wins. A tie keeps the edit.
    • An entry renamed differently on two devices keeps both names.
  • Scheduling (SyncScheduler.qml). Syncs when the app opens or comes back to the foreground, and after each save.
    • Failed syncs are retried after 30 s, 1, 2, 5 and 15 minutes.
    • While offline, it retries 5 s after the network is back (Wi-Fi reports online before DNS and routing work).
    • A save or Sync now during a sync queues one more; other triggers are covered by the running sync.
    • Each trigger can be switched off in the new Settings page.
    • Each sync's start and outcome (counts and error messages only) go to the app log.
  • Feedback. Saves and sync results show as toasts; a single line at the bottom shows the sync state. Tapping it opens details only when there are some (a failure, resolved conflicts, skipped entries); after a failure the details offer Try again.
  • Safety after a crash.
    • An interrupted fast-forward or merge is finished on the next open, instead of the checked-out files being committed as new local edits.
    • Entry files are written atomically.
    • Entries this phone can't check (another person's key, an unreadable file, a missing public key) are reported and never block the sync.
  • Main.qml is split into one file per page or dialog.
  • The connectivity AppArmor group is added; click-review still passes.
  • docs/superpowers/ (session notes) is ignored rather than committed.

Review

An independent review of the sync, merge and re-encryption code found four data-loss bugs, each confirmed with a failing test. All four are fixed in a13d395, with regression tests:

  1. Merge and re-encryption commits made old content beat a newer edit.
  2. A crash during a fast-forward turned remote content into a local edit.
  3. One junk .gpg file blocked sync on every device.
  4. Entry writes weren't atomic, so a crash could leave a truncated file.

Testing on the phone found:

  • A new entry opened in edit mode, with a read-only name, because PageStack.push turns null into undefined. Fixed in 5ebd145; leftover TypeErrors in c164ca4.
  • Two status lines at the bottom after a save and a sync. Replaced with toasts in 343c975.
  • An entry deleted on the laptop came back from the phone, because an edit always beat a delete. Changed to the later change winning in 284b22c.
  • After reconnecting, the retry fired before the network worked, so a remote change only arrived on the next retry or Sync now. Fixed in fbcb7da.
  • Every launch ran two syncs. Fixed in 0e0cc5c.
  • The details dialog offered Sync now after a good sync, and opened empty when there was nothing to report. Fixed in b932d8b and 3abb6a0.

Checks

  • cargo make ci: 59 tests, 91% line coverage, clippy and audit clean
  • cargo make click builds and passes click-review
  • Installs and launches on the phone with no QML errors
  • First sync on the phone against pass.git succeeded
  • Generator: length, class switches, symbol set and look-alikes regenerate live
  • Add an entry on the phone → it saves, syncs, and appears on the laptop
  • Delete an entry on the laptop → it disappears from the phone after the next sync
  • Edit an entry on the phone (layout kept, pass show on the laptop)
  • Offline, then online (Wi-Fi off and on; airplane mode with Wi-Fi is unreliable on Ubuntu Touch)
  • The same entry edited on both devices → newer wins, and the details list it
  • Edit on one device, delete later on the other → the delete wins, the list updates by itself, and the details list it
  • Settings switches ("Sync after adding or editing" off, then Sync now)
  • Resume after more than 2 minutes in the background → one sync
  • Launch → one sync; tapping the sync line with nothing to report opens nothing

🤖 Generated with Claude Code

Adds adding and editing entries, a configurable password generator, and automatic git sync with retries. ## What's in it - **Editor.** Add and edit entries. The username and notes lines keep their layout, so `pass` and browserpass read the entry as before. Each save is committed with pass-style messages, encrypting to the nearest `.gpg-id`. - **Generator.** Length 8–128 (default 25), character classes, your own symbol set, and an option to leave out look-alike characters. The last-used options are remembered. - **Sync.** Fetch, then fast-forward or merge, then run the re-encryption guard, then push, retrying up to 3 times when another device pushes in between. - Local commits are the queue of changes waiting to be pushed. - Conflicts go to the newer change, judged per entry by the last commit that really changed it; merge commits and re-encryption commits don't count as changes. - Deletes follow the same rule: the later of an edit and a delete wins. A tie keeps the edit. - An entry renamed differently on two devices keeps both names. - **Scheduling (`SyncScheduler.qml`).** Syncs when the app opens or comes back to the foreground, and after each save. - Failed syncs are retried after 30 s, 1, 2, 5 and 15 minutes. - While offline, it retries 5 s after the network is back (Wi-Fi reports online before DNS and routing work). - A save or **Sync now** during a sync queues one more; other triggers are covered by the running sync. - Each trigger can be switched off in the new Settings page. - Each sync's start and outcome (counts and error messages only) go to the app log. - **Feedback.** Saves and sync results show as toasts; a single line at the bottom shows the sync state. Tapping it opens details only when there are some (a failure, resolved conflicts, skipped entries); after a failure the details offer **Try again**. - **Safety after a crash.** - An interrupted fast-forward or merge is finished on the next open, instead of the checked-out files being committed as new local edits. - Entry files are written atomically. - Entries this phone can't check (another person's key, an unreadable file, a missing public key) are reported and never block the sync. - `Main.qml` is split into one file per page or dialog. - The `connectivity` AppArmor group is added; `click-review` still passes. - `docs/superpowers/` (session notes) is ignored rather than committed. ## Review An independent review of the sync, merge and re-encryption code found four data-loss bugs, each confirmed with a failing test. All four are fixed in a13d395, with regression tests: 1. Merge and re-encryption commits made old content beat a newer edit. 2. A crash during a fast-forward turned remote content into a local edit. 3. One junk `.gpg` file blocked sync on every device. 4. Entry writes weren't atomic, so a crash could leave a truncated file. Testing on the phone found: - A new entry opened in edit mode, with a read-only name, because `PageStack.push` turns `null` into `undefined`. Fixed in 5ebd145; leftover TypeErrors in c164ca4. - Two status lines at the bottom after a save and a sync. Replaced with toasts in 343c975. - An entry deleted on the laptop came back from the phone, because an edit always beat a delete. Changed to the later change winning in 284b22c. - After reconnecting, the retry fired before the network worked, so a remote change only arrived on the next retry or **Sync now**. Fixed in fbcb7da. - Every launch ran two syncs. Fixed in 0e0cc5c. - The details dialog offered **Sync now** after a good sync, and opened empty when there was nothing to report. Fixed in b932d8b and 3abb6a0. ## Checks - [x] `cargo make ci`: 59 tests, 91% line coverage, clippy and audit clean - [x] `cargo make click` builds and passes click-review - [x] Installs and launches on the phone with no QML errors - [x] First sync on the phone against `pass.git` succeeded - [x] Generator: length, class switches, symbol set and look-alikes regenerate live - [x] Add an entry on the phone → it saves, syncs, and appears on the laptop - [x] Delete an entry on the laptop → it disappears from the phone after the next sync - [x] Edit an entry on the phone (layout kept, `pass show` on the laptop) - [x] Offline, then online (Wi-Fi off and on; airplane mode with Wi-Fi is unreliable on Ubuntu Touch) - [x] The same entry edited on both devices → newer wins, and the details list it - [x] Edit on one device, delete later on the other → the delete wins, the list updates by itself, and the details list it - [x] Settings switches ("Sync after adding or editing" off, then **Sync now**) - [x] Resume after more than 2 minutes in the background → one sync - [x] Launch → one sync; tapping the sync line with nothing to report opens nothing 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keys without an email author commits as touchpass@device.invalid, since
libgit2 refuses an empty email.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renames to two different names keep both and are reported as
RenamedApart rather than as two edit-over-delete resolutions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entries whose recipients can't be resolved here (public key missing
from the keyring) are reported as skipped instead of failing the sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- merges count as changing a path only if they differ from every parent,
  and re-encryption commits never count, so carried-over or re-encrypted
  old content no longer beats a newer edit
- an advance interrupted between checkout and branch update is finished on
  the next open instead of committed as new local changes
- entry files are written atomically (temp file under .git, then rename)
- entries that can't be read or checked are skipped by the re-encryption
  guard instead of failing every sync; skipped entries are reported once
- unrelated histories give a clear error

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The connectivity policy group lets NetworkWatch read NetworkingStatus;
the plugin's one start-up query of the private interface is still denied,
which is harmless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: sync, editing and the generator
All checks were successful
CI / ci (pull_request) Successful in 47m51s
CI / audit (pull_request) Successful in 15s
bff45d3f0e
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qml: editor treats an undefined secret as adding
Some checks failed
CI / audit (pull_request) Has been cancelled
CI / ci (pull_request) Has been cancelled
5ebd145b43
PageStack.push turns a null property into undefined, so a new entry was
opened as an edit: read-only name, and saving would fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qml: one-off messages as toasts; the sync line is the only status
Some checks failed
CI / audit (pull_request) Has been cancelled
CI / ci (pull_request) Has been cancelled
343c9758bf
Saved/copied/cleared/locked confirmations and errors float over the list
and fade out, instead of a second status line under the sync status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
passcore: the later of an edit and a delete wins
Some checks failed
CI / ci (pull_request) Has been cancelled
CI / audit (pull_request) Has been cancelled
284b22c9b4
An edit used to beat a delete regardless of time, which brought back an
entry deleted on the laptop minutes after an unsynced edit on the phone.
Deletes now compete by the time of the deleting commit; a tie keeps the
edit. Reported as NewerDelete when the delete wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
po: update template
Some checks failed
CI / audit (pull_request) Has been cancelled
CI / ci (pull_request) Has been cancelled
6d22c5ffbe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Editor: read the secret only when there is one
Some checks failed
CI / audit (pull_request) Has been cancelled
CI / ci (pull_request) Has been cancelled
c164ca4e91
The fields checked editing and then read secret, which the log showed
failing with TypeErrors once secret was undefined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sync: retry 5 s after coming back online, and log each sync
Some checks failed
CI / audit (pull_request) Has been cancelled
CI / ci (pull_request) Has been cancelled
fbcb7da58f
Lomiri reports online as soon as the Wi-Fi associates, before DNS and
routing work, so the immediate retry failed and the next one came 30 s
later. Wait a moment and restart the backoff instead. Log each sync's
start and outcome (counts and error messages only) to the app log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sync: queue a follow-up sync only for a save or Sync now
Some checks failed
CI / audit (pull_request) Has been cancelled
CI / ci (pull_request) Has been cancelled
0e0cc5c161
Start-up and becoming active both triggered a sync at launch, so every
launch ran two. The running sync already covers automatic triggers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sync details: informative after a good sync
Some checks failed
CI / audit (pull_request) Has been cancelled
CI / ci (pull_request) Has been cancelled
b932d8b242
The dialog offered Sync now even when there was nothing to do. It now
only offers Try again after a failed sync; Sync now stays in the header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sync line: only open details when there are any
Some checks failed
CI / ci (pull_request) Successful in 47m50s
CI / audit (pull_request) Has been cancelled
3abb6a0a4a
A tap after a sync with nothing to report opened an empty dialog. Now it
does nothing; skipped entries get a "tap for details" hint like
resolved conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot changed title from WIP: Git sync, entry editing and password generator to Git sync, entry editing and password generator 2026-09-30 15:06:09 +00:00
claude-bot changed title from Git sync, entry editing and password generator to feat: git sync, entry editing and password generator 2026-09-30 15:10:13 +00:00
chore: keep superpowers session docs out of the repo
Some checks failed
CI / ci (pull_request) Successful in 48m23s
CI / audit (pull_request) Has been cancelled
55eeb198ac
The spec and plan are notes for Claude Code sessions, not project
documentation. Untrack them and ignore docs/superpowers/; the files
stay on disk for local sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chore: use SPDX license identifiers instead of GPL header blocks
Some checks failed
CI / audit (pull_request) Has been cancelled
CI / ci (pull_request) Has been cancelled
e25599082e
The 15-line GPL notice in each source file only restated LICENSE. One
SPDX line keeps each file's license (GPL-3.0-only) machine-readable at
a fraction of the noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chore(po): update template line references
Some checks failed
CI / audit (pull_request) Has been cancelled
CI / ci (pull_request) Has been cancelled
ac0ebe1d2b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An edit only checked that the entry existed, so saving after a sync had
brought in a newer version (e.g. a password rotated on the laptop)
committed on top of it and pushed, silently replacing it everywhere.

decrypt now returns the entry's version (its blob id) and Change::Edit
carries the version the editor opened; write_entry refuses with
Error::Changed if the file differs or is gone. The repository is opened
(finishing an interrupted sync) before that check and before resolving
recipients, so both see the store as it really is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep both when the same entry is added on two devices
Some checks failed
CI / ci (pull_request) Has been cancelled
CI / audit (pull_request) Has been cancelled
e1a3733c5f
With no common ancestor the conflict fell into "newer edit wins", so
adding web/example offline on the phone and on the laptop kept only one
of the two secrets and reported it as "kept newer".

Now the remote's version keeps the name and the local one moves to the
first free <name>-conflict[-n], both reported as AddedOnBoth, like
renames to different names. Non-entry files such as .gpg-id still
resolve newer-wins. Regenerates the translation template for the new
sync-details strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add QML tests for SyncScheduler and EntryEditor
All checks were successful
CI / qml (pull_request) Successful in 3m34s
CI / ci (pull_request) Successful in 48m29s
CI / audit (pull_request) Successful in 21s
0c33254ace
Qt Quick Test cases under tests/qml, run against fake SyncService and
PassStore objects: the scheduler's start/resume/after-change triggers,
follow-up syncs, backoff and offline handling, and the editor's add,
generated and edit writes (including the entry version an edit passes),
save gating and error display.

They need Lomiri, so they run in clickable's amd64 image under Xvfb via
tests/qml/run.sh: `cargo make qml-test` locally, a new `qml` CI job.
Not `clickable test`, whose xvfb-startup wrapper always exits 0, nor
Qt's offscreen platform, which crashes with Lomiri.

For testability, SyncScheduler.online is writable (tests go offline
without Lomiri.Connectivity, which reports offline in a container) and
EntryEditor exposes canSave and names its fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
schaefera approved these changes 2026-09-30 16:22:11 +00:00
Dismissed
Run the QML tests after the Rust job, not beside it
All checks were successful
CI / ci (pull_request) Successful in 47m38s
CI / audit (pull_request) Successful in 15s
CI / qml (pull_request) Successful in 34s
5f1f1969ae
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clearer names and comments in sync conflict resolution
All checks were successful
CI / ci (pull_request) Successful in 23m54s
CI / audit (pull_request) Successful in 11s
CI / qml (pull_request) Successful in 27s
46231e4fc4
Spell out what each local means (winning_side, side_edited,
edit_commit_time, local_copy_path, ...) and name the deciders after what
they do (resolve_newer, resolve_edit_vs_delete). Document Side and
Reason. The edit-vs-delete helper takes its two paths as separate
arguments, so clippy now allows 4. Ignore .idea/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clean up after a killed app, walk with walkdir, app ID touchpass.aps
All checks were successful
CI / ci (pull_request) Successful in 44m49s
CI / audit (pull_request) Successful in 15s
CI / qml (pull_request) Successful in 38s
001802c3b6
- Error is now PasscoreError, in its own module (error.rs), with clearer
  names throughout passcore and the bridge.
- open_or_init removes the *.lock files libgit2 leaves when the app is
  killed mid-write; one left behind failed every later save and sync.
- Directory walks use walkdir. Store::entries still follows symlinked
  folders, as pass ls does, but no longer lists everything again through
  a link back up. build.rs lists files sorted, so the .pot is stable.
- App ID touchpass.austinschaefer -> touchpass.aps (data and settings
  folders, gettext domain, po/touchpass.aps.pot). An installed copy
  becomes a separate app that starts empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
schaefera approved these changes 2026-10-01 09:54:31 +00:00
schaefera deleted branch sync-and-editing 2026-10-01 10:53:44 +00:00
Sign in to join this conversation.
No reviewers
No labels
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.

Dependencies

No dependencies set

Reference
schaefera/TouchPass!2
No description provided.