Releases: maxkalem/obsidian-native-git-bridge
Release list
0.6.5
Native Git Bridge 0.6.5
This release needs the runner reinstalled in Termux (v15): open the companion app and use Update runner, or run the install command from Settings. Copy main.js and styles.css when updating by hand.
Re-running the installer also repairs a credential file that 0.6.4 could damage. If a pull or a fetch has started failing with could not read Username, that is the fix, and it needs no other action.
A clone that needs a password now runs where passwords live
Cloning a private repository answered "no credentials" and there was no way to give it any. Authentication in this plugin is per repository and lives inside Termux, so a repository that does not exist yet has nowhere to read a token from, and the runner never permits a prompt. Every clone into a fresh vault ended there.
The clone is now a plain git clone command you paste into Termux. The plugin builds it, copies it to the clipboard and opens Termux; git asks for the username and the token itself, with its own progress meter, and saves what you type in the profile's own credential store. Press Continue in Obsidian afterwards and the runner adopts the download instead of fetching it a second time: it checks that the repository in the scratch directory came from the URL you asked for and that its download finished, then lands it in the vault the same way it always has, without overwriting a single file that was already there.
An interrupted paste costs nothing. The command wipes its own scratch directory first, so running it again starts clean, and nothing is queued until you press Continue, so nothing can expire while you are typing a token.
A clone that already has credentials is unchanged and still runs through the companion in the background. The command is offered only where it is needed: a first clone over https, a re-clone whose credentials Termux does not hold, or a clone that came back with an authentication failure.
An hour is the new budget for a clone, up from fifteen minutes, and the same for the request behind it.
Credentials stay in Termux
A re-clone no longer reuses credentials out of the vault. 0.6.4 copied the old repository's credential configuration onto the new clone's command line, which kept working setups working, but the file it read them from lives inside the vault, on shared storage, where any app with the storage permission can read it and whatever else syncs the vault carries it away. That path is gone. Credentials are entered in Termux, stored in Termux, and reused from Termux, and a re-clone that finds none there hands you the command instead.
A remote URL with a token in it is refused on both sides. https://token@host/repo.git used to pass validation because only user:password@ was checked, and a clone from such a URL wrote the token into the new repository's config. Any userinfo in an https URL is now rejected, with a message naming the shape.
Sparse checkout survives a re-clone
Sparse configuration lives inside .git, so replacing the repository erases it. Hiding a path again then failed on a repository where sparse checkout was off, with advice to run a git command in Termux by hand. Hide path now enables sparse checkout itself, seeded so that the first exclusion can never mean "hide everything", and re-adding the paths is the whole recovery.
Protected paths that git no longer reports raise a question rather than a silent answer. The plugin remembers which paths this device protects; when the repository comes back without them, it offers to hide and protect them again, or to release the protection. Dropping them silently would unguard what the safety gate exists for, and keeping them silently would block every sync over paths that are now visible.
Fixes
- Every operation on a vault with no repository opens the setup window instead of a bare "no repository" error. Refresh, pull, push, sync, commit, staging, diffs and history all reach it now. A vault that has a repository the runner cannot use keeps its honest error message.
- The repository footprint switches press on a fresh launch. They were disabled until some other action happened to fetch a status, which read as buttons that do not work; each command now fetches the state it needs.
- A leftover
index.lockhas a button. An interrupted operation could leave one behind, and every git command afterwards failed with "Another git process seems to be running" until the file was removed by hand in Termux. Failure windows that name that error now offer Delete the stale lock, after a confirmation that says it ends any Termux session still running. - A clone killed by Android says so. The system stops a large download once Termux is no longer the visible window, and git's own output shows nothing wrong; the failure now names the cause and what to do about it.
- An interactive run in Termux shows git's progress as it happens, instead of a silent terminal and a timer.
- A failed bootstrap clone reported one failure, not four. The status collected after a failure ran in a profile that has no repository yet, and three
fatal: not a git repositorylines went into the log as if something else had gone wrong too. - Buttons in windows with a text field sit at the bottom, centred, on every platform, and on a phone the window itself is placed so the button stays above the on-screen keyboard.
- The clone instructions no longer fill the screen with a wrapped command. It sits in a section you can open, and is still part of Copy details.
Full Changelog: 0.6.4...0.6.5
0.6.4
Native Git Bridge 0.6.4
This release needs the runner reinstalled in Termux (v14): open the companion app and use Update runner, or run the install command from Settings. Copy main.js and styles.css when updating by hand.
Take the disk back
A new command shows where your .git went and cleans it up. Interrupted downloads leave multi-gigabyte temporary files nothing ever collects, and every repair download adds a full copy of the history without removing the old one — a ~4 GB vault was found carrying 20 GB of .git/objects this way. Clean up repository storage scans first and shows the real numbers, then removes the leftovers and repacks everything reachable into one pack. Nothing any branch, tag, reflog or the index can reach is touched, reflogs are never expired, and the verdict says exactly what was freed.
Content that only unpushed commits hold is never shed. The remote could not give it back, so the cleanup waits and says why instead of quietly destroying the one copy.
Hold less on this device
Sparse checkout hides files from the vault, not from .git — until now. The packs still carry every version of every file, hidden or not, which is why a 200 MB working set can sit on gigabytes of history. Two per-device switches in Settings → Repository footprint change that:
Partial clone (blob:none) fetches file content on demand and never downloads the content of files your sparse checkout hides. Show again and old file versions then need the network — the switch says so before it does anything. When sparse checkout is active, the plugin offers this once.
Shallow history keeps only the newest commits on this device (how many is configurable; the default is 100). The history panels reach only what stays, and enabling it clears this device's reflog — kept, it would pin the old commits and the cut would free nothing.
Both switches show the repository's actual state and move only after Termux confirms; a change that fails changes nothing on screen. Space returns with the next storage cleanup.
Cloning asks how much this device should hold. A new device can start as a lightweight partial clone at a fraction of the size, and re-cloning over an existing repository now authenticates with the credentials that repository already uses — passed to git on the command line, written nowhere.
Stop tracking a file
A tracked file ignores every ignore rule, so workspace-mobile.json kept landing in every commit no matter what .gitignore said. The plugin now says so the moment you add a rule for a tracked file, and offers Stop tracking (keep the file) — also in the file's git menu. The file stays on disk, a deletion enters the index for you to commit, and the confirmation names the consequences up front: other devices' pulls remove their copy (or report an ordinary conflict), and without an ignore rule the next sync stages the file right back.
Fixes
- The state line no longer freezes at its last tick in the history panels after opening a diff from there.
- Changing a
.gitignore, sparse or exclude rule refreshes the git panel on every route, instead of leaving the old state on screen until a manual refresh. - The live view of the output panel has its own tab now, so switching to a log and back no longer needs the undiscoverable tap-the-active-tab gesture; the console field also gained a wrap-long-lines toggle, set apart from the tabs.
- A correctly updated runner is no longer branded "NEWER than this plugin expects". The advice compared against the minimum instead of the shipped version, so every healthy installation saw a red warning; the settings chip says "needs vN" only when the runner is actually below it.
- Modal titles fit one line on a phone. The worst offender was a failure window titled with the whole question it was failing.
- The installer upgrades git instead of keeping a years-old one, reports the version it ended at, and warns when the package repository itself is outdated — including the abandoned Play Store Termux build, which it now detects by name. It also installs the runner and enables the companion permission even when no vault exists yet, so a brand-new device finishes setup from inside Obsidian.
- A repair backup branch can be deleted on demand (Delete repair backup branch in the palette), not only from the once-a-day reminder.
Full Changelog: 0.6.3...0.6.4
0.6.3
Native Git Bridge 0.6.3
This release needs the runner reinstalled in Termux (v13): open the companion app and use Update runner, or run the install command from Settings. Copy main.js and styles.css when updating by hand.
Sync can always finish
A file changed here and on the remote no longer traps sync. Git refuses to merge over uncommitted changes to a file the merge also touches — whether or not the content would conflict — and sync used to stop there forever. It now asks git which paths the merge brings in, and commits first only when they collide with local work; an ordinary sync is unchanged and produces no merge commit. A real disagreement becomes a normal conflict, which the panes already resolve per block.
Merges no longer disturb the sparse checkout. A merge has to materialise excluded files to merge them; sync and pull now put the sparse state back afterwards, so the safety gate no longer blocks the very operation that created those files.
Filenames other machines cannot check out are caught before they are committed. A long Cyrillic or emoji name commits and pushes cleanly from Android, and then every other clone fails with "Filename too long". Sync and commit now check every file against the limits other systems enforce, list the offenders, and offer to shorten the names automatically — through Obsidian's own rename, so links keep working.
See what Termux is doing
A new output panel shows git's own output while it runs. Tap the state line in the git panel or the history panel (or use the command palette) during any operation. The runner streams its progress as it works, so a clone or a repair shows transfer percentages instead of a bare seconds counter, and a wait can be read against the request's budget.
Three log tabs, left of the refresh button: earlier operations (kept 24 hours), the Termux runner log, and the plugin's own operation log — each in a dark console field, in both themes. The panel opens on the live operation and never opens empty.
The state line reads the same on every panel, with the runner's current step on its own line under it, so the layout never jumps when the runner starts talking.
Repairing a damaged repository
Android routinely kills Termux mid-write, and the residue is an object store git cannot read: object file … is empty, unable to read tree. The repair for that state was rebuilt end to end.
It runs as short steps instead of one long action, so an interrupted repair loses one step, not the whole run, and a repair interrupted by closing Obsidian offers to continue on the next launch.
It downloads what is missing, not the history. The targeted step asks the remote for the missing objects by name — measured at kilobytes against a multi-gigabyte vault. Downloading the whole history again is a separate step and always asks first.
It tells the truth about local-only damage. Missing objects that survive a full refetch and belong to unpushed commits were never on the remote; no download can bring them back, and re-cloning would throw those commits away. The repair now says so and offers Rebuild on the remote state instead: the branch moves to what the remote has, every file on disk stays exactly as it is, the content of the local commits becomes ordinary uncommitted changes for the next sync, and the old history stays reachable under a backup branch. The panel offers to delete the backup once you have checked nothing is lost, and reminds about it daily until it is gone.
Fixes
- "Abort merge failed" now explains itself and offers Reapply sparse rules, which is the actual way out when the sparse checkout drifted from the index.
- Short operation timeouts no longer condemn healthy bridges. Network operations get a 120-second floor whatever the setting says, and the bridge check names the likely cause first instead of sending you to reinstall a working runner.
- Tokens cannot reach logs or the vault. A personal access token travels as the URL's username, and the redaction only knew the
user:passwordform; every log, result and progress stream now strips any userinfo and the token shapes the hosts issue. Credential configuration is passed to git on the command line and never written into a file inside the vault. - A conflict looks like a conflict on every surface, the context menu names the file it is about, a rename reveals as three readable lines, and deleting to Obsidian's trash is no longer presented as a move.
- The status panel never presents an unread repository as clean, budgets its rows on huge vaults, and a result that arrived while Obsidian was closed is reported instead of dropped.
- The shareable log bundle collects the plugin log, the runner log and the recent progress streams into one file, redacted, for reporting a problem.
Full Changelog: 0.6.2...0.6.3
0.6.2
Update notes 0.6.2
Requires a Termux runner update (v11 → v12). Open the companion app and tap Update runner, or re-run the install command from the plugin settings. Nothing needs re-pairing.
Staging one hunk at a time
The diff pane now has buttons on every hunk. Which ones depend on what the pane is showing: the staged side of a file offers Unstage hunk, the unstaged side offers Stage hunk and Discard hunk, and a diff from history offers neither, since there is nothing there to stage.
Staging a hunk leaves the file alone, so a file with two edits and one of them staged appears in both groups of the git panel: the staged edit under Staged changes, the other under Changes. That is git's own behaviour, and it is what makes a commit of half your work possible.
Select lines beside those buttons switches to picking: checkboxes appear in the line-number column, and the buttons become Stage selected and Unstage selected. Unpicked lines are left exactly as they are, including unpicked removals — a distinction that is easy to get wrong and would otherwise delete lines you did not choose.
Discard is the only one that removes work rather than moving it, and the only one that asks first.
Opening another file leaves picking mode, because the ticks are positions in the diff that was on screen and mean nothing in the next one. Settings → Keep line selection when opening another file turns that off if you would rather stay in the mode; the ticks are dropped either way.
Restoring one block now lands staged
Restore this block in the file history still refuses when the surrounding text has drifted, but the restored block now goes straight into the index. Other edits in the same file stay unstaged, because the plugin stages that block specifically rather than the whole path.
The button itself has moved into the hunk's own header row and looks like the buttons in the diff pane, because it is the same kind of control. It used to sit in a row of its own, aligned to the far right of a table as wide as the longest line of code, which on a phone put it off the edge of the screen.
A diff limit you control
Settings → Diff size limit, default 100 KB, per device. A large diff is expensive to draw: every line costs about a dozen elements, so this is a property of the phone rather than of the vault.
The runner keeps whole hunks within the limit and never a partial one, so what arrives is always a valid patch and every hunk button on it works. When something was left out, the pane says so — "Showing 12 of 40 hunks" — and offers to fetch the whole diff for that one diff, after telling you how many lines that is. The setting is not touched, and the next diff starts from it again.
Diffs render without any third-party code
The diff panes no longer use diff2html. They parse the unified diff and build the view directly, which removed the plugin's last runtime dependency and 92 KB of bundle. Nothing in the plugin executes generated code any more, and the file Obsidian loads contains only this repository's own source.
Reading a diff
Every hunk says which lines of the file it is, next to the buttons that act on it, and a rule separates one hunk from the next across the whole pane, line numbers included. Two hunks used to run together and read as one continuous stretch of the file.
Settings → Compare changed lines by chooses what gets highlighted inside a line that changed. Words is the default and suits prose: "brown" becoming "red" is one word replaced, where the old character comparison rendered it as <del>b</del>r<del>own</del> because the two share an "r". Characters suits a path, an identifier or a number, where one letter is the whole edit. The choice applies to the diff pane, the file history and the conflict pane.
Line numbers past 99 no longer touch each other. 832 beside 887 used to render as 832887, which reads as one six-digit number.
In picking mode the checkboxes now form a straight column at the left edge, instead of shifting sideways with the width of the numbers next to them.
File at commit is shown like a diff, with numbered lines and wrapping, instead of one block of unnumbered text.
The diff, conflict and history panes have room to scroll past the last line. Android's gesture bar and Obsidian's floating toolbar used to sit on top of it.
Conflicts
The two sides of a block are now compared line by line, and what differs is highlighted the same way it is in a diff. The pane has always said that local and remote disagree without saying about what, which on a block of prose is the only question worth answering.
The side labels read Local (HEAD) and Remote (branch), with a long branch name or a commit hash abbreviated. The buttons are Keep Local and Keep Remote. The branch used to be in the button, which made it as wide as the branch name and, on a phone, wide enough to cover the text beside it.
Blocks are separated by the same air the diff pane gives its hunks.
Termux: which vaults are still paired
The installer now ends by listing every profile on the device: how many there are, which one belongs to the vault you just installed for, and, for each, the folder it points at, with a note when that folder is gone or is no longer a git repository.
A vault that was deleted or moved leaves its profile behind by design, so the number of profiles can quietly exceed the number of vaults on the phone. Nothing ever said so. The installer deletes nothing — a profile holds that vault's token — but it now tells you which file to remove if you want to.
Fixes
- A note line starting with
--or++disappeared from its diff. Removing-- signatureproduces the diff line--- signature, which the parser read as a file header. Adding++ listhad the same problem. - Every diff had a phantom blank line at the bottom, with a line number of its own.
- A diff of a merge commit showed nothing at all.
- The history panel's refresh button never animated, and each refresh left another timer running behind the scenes until the panel was closed. The same leak affected the file-history and diff panes.
- A failed
discard all,reset,create repositoryorcloneleft the panel showing a state that no longer existed. - Diff truncation was locale-dependent. The old limit counted characters in one environment and bytes in another, and could cut a multi-byte character in half, leaving a broken glyph on the last visible line.
- The Select lines button could not be reached unless line wrapping was on. It was pushed to the far end of the hunk's controls, and without wrapping, "the far end" is past the right edge of a diff wider than the screen.
- The reduced-motion block of the stylesheet was malformed and is now written correctly.
Internals
Runner v12 adds apply-patch (one patch, to the index or the working tree, forward or reversed) and a hunk-aligned budget for diff-file reporting hunksShown, hunksTotal, diffBytesTotal and diffBytesLimit. 492 unit tests, 454 e2e checks against real repositories, including all three patch directions and every refusal: a second path in the patch, a protected path, a traversal path, and a patch that no longer applies.
The character comparison is a second pass over the stretches the word comparison already found, not a separate comparison of the whole line. A line-wide character comparison costs O(n·m) and would give up on any paragraph longer than a few hundred characters, reporting it as wholly changed.
The project is GPL-3.0-only and always was: the LICENSE file has held the GPLv3 text since the first commit. package.json, the README and parts of the documentation said MIT, which was wrong and is corrected. The MIT notice that travels with diff2html's stylesheet stays where it is.
Full Changelog: 0.6.1...0.6.2
0.6.1
Update notes 0.6.1
Requires a Termux runner update (v9 → v11). Open the companion app and tap Update runner, or re-run the install command from the plugin settings. Nothing needs re-pairing: profiles and tokens are untouched.
0.6.0 was prepared but never published, so this release carries it too.
Several repositories on one device
Each vault gets its own profile and its own token (profiles/<id>.conf); an existing single-repository config migrates itself. One runner drains every profile, oldest request first, and git is pinned per profile with GIT_CEILING_DIRECTORIES and a toplevel check, so a vault nested inside another repository can never leak into it — the outer repository excludes it locally instead. A vault that moved is found again by its profile marker, a new one pairs itself, and a dead profile answers REPO_MISSING rather than failing silently.
A vault's repository, from nothing
Settings → Repository for this vault → Set up repository, also in the setup guide and the palette. It reads what the vault actually is and offers only the steps that apply: create a repository here, add or change the remote, clone an existing one, or re-clone a broken one. Creating and cloning are also palette commands of their own, so you can go straight to the one you mean.
A vault that is not a repository yet can be paired before it has one, so none of this needs a terminal. Until a repository exists, that profile answers only the actions that create one.
Cloning into a vault that already has files
A vault always holds at least .obsidian/, which a plain git clone refuses, and this is where other tools ask you to delete your configuration. Here nothing you already have is written over: the repository is cloned without a checkout and moved in, its tree goes into the index leaving the working tree alone, and only files the vault does not have are written out. You end up with a complete checkout plus your own versions of the overlapping files, listed as ordinary local changes — open each one, read the diff, then commit to keep yours or discard to take the repository's.
"Create a repository, then point it at my existing remote" lands in exactly the same place as cloning: same commit, branch, upstream and files. If both sides have a history, the plugin says so at once instead of letting refusing to merge unrelated histories surprise you later. A remote whose HEAD names a branch it does not have is handled too — plain git clone gives up there; the runner picks a branch and sets its upstream.
Re-cloning only replaces anything after the new clone succeeds, and the old repository is set aside, not deleted: full history, still a valid git directory, so you can attach it as a remote and cherry-pick from it. Because that copy is invisible and can be large, the plugin mentions it once a day with its size and three answers, one of which is "stop reminding me about this one".
Installing and updating without a network
The Termux scripts are now inside the plugin folder, so a device that has the plugin has everything needed to install or update the runner:
bash "<vault>/.obsidian/plugins/native-git-bridge/termux/bootstrap.sh" "<vault>"
Nothing to download, no version to pass, nothing that depends on GitHub being reachable. NGB_BASE_URL accepts a plain directory path or a file:// URL as well as https://. When the plugin arrives through vault sync the matching runner arrives with it, so the version handshake can be satisfied without a release at all.
Remote URLs
Validated identically in the plugin and in the runner, passed to git as an argv element, redacted in every log and result. Accepted: https://, ssh://, git@host:owner/repo.git, file:///absolute/path. Refused: anything starting with -, plain http://, git://, ext::…, whitespace, non-ASCII, and any URL carrying a password — a secret must never reach a request file inside the vault.
Getting out of states that had no way out
Two of these were reported from a real device, and neither could be escaped without opening Termux.
A protected sparse path stuck in the index. Stage a file, then add its directory to the sparse exclusions: the reapply takes the file off disk and leaves the index entry behind. Because sparse checkout sets skip-worktree, git stops looking at the worktree and reports a bare A — the index says "added", and nothing says the file is gone. The safety gate blocked every commit, push and sync; the "delete these files" repair moved nothing, because there was no file; and unstaging was refused, because the path was protected.
The safety report now reads both status columns and says what it actually found, and the repair button names what it will do: Delete files locally, Remove from index, or Delete and unstage. Removing from the index is the one write permitted on a protected path, and only for paths the last commit does not contain — so it can undo a staged addition and can never turn into a deletion of committed content. Anything tracked in HEAD is still listed with a reason and left to you.
An unfinished merge with nothing left to resolve. Every pull answered "a merge is already in progress", the Conflicts group was empty because you had already resolved everything, and Abort merge lived only in that group's menu — so there was no way out at all. The status panel now shows a banner whenever a merge or rebase is unfinished, regardless of how many conflicts remain, with Commit merge (prefilled with git's own merge message) and Abort merge. A refresh no longer erases it. Unfinished rebases are recognised too, with Continue and Abort; nothing in the plugin starts a rebase, but one started in Termux used to be invisible here.
Panels
The status and history panels are now a fixed top, a scrolling list and a fixed bottom bar. On a phone the git buttons sit in the bottom bar within thumb reach and the branch state stays on top; the merge banner sits under the branch line where it cannot be scrolled past. The history panel uses the same shell, so refresh and the layout toggle are in the same corners as the status panel's, and each panel has a button that opens the other. Scroll position survives a refresh, so the auto-refresh timer no longer throws you back to the top mid-scroll.
Termux installer output
The installer no longer wraps its own text at a fixed width. On a phone that wrapped a second time at the terminal edge, in the middle of words. Every message is now folded to the width the terminal actually reports, breaking on spaces, and lines meant to be copied — commands, the pairing token, paths — are printed whole.
Internals
Runner v11 adds init-repo, set-remote, clone-into-vault, adopt-remote, unstage-protected, abort-rebase, continue-rebase, the REPO_EXISTS error, the bootstrap profile state and rebaseInProgress in status. 353 unit tests, 408 e2e checks against real repositories, including the sparse-index state above end to end and both exits from an unfinished rebase.
Full Changelog: 0.6.0...0.6.1
0.6.0
Update notes 0.6.0
Requires a Termux runner update (v9 → v10). Open the companion app and tap Update runner, or re-run the install command from the plugin settings. Existing single-vault setups are migrated automatically and keep their token: there is nothing to re-pair.
More than one vault on a device
The Termux side used to hold exactly one repository, so installing for a second vault silently overwrote the first, and that vault's requests were never answered again (a TIMEOUT and "Runner has written here: NO" in the bridge check, which points at the wrong suspect). Every paired vault now has a profile of its own: its own repository, its own runtime folder, its own token, mode 600, one file each.
- One runner run drains every profile, oldest request first across all of them, still one at a time. The trigger the companion sends carries no vault identity by design, so the runner looks at all the queues instead of being told which one to look at.
- A token is valid for its own profile only. A request file copied into another vault's runtime folder is rejected, and a request naming another vault's profile is rejected too. The plugin never sends a repository path; it sends an opaque profile id that the runner looks up in files only Termux can write.
- Removing or breaking one vault leaves the others working: its own queued requests are answered with the new
REPO_MISSINGerror so that vault's panel stops waiting, and the rest of the run continues. - Your existing configuration is turned into a profile on the first run of the new runner, with the same token, and the old
configis kept asconfig.legacy.
A vault inside another vault
Main/ and Main/Projects/ABCproject/ can both be vaults and both be repositories. The inner repository is excluded from the outer one through the outer repository's .git/info/exclude: device-local, never synced, and no tracked file such as .gitignore is touched. The installer prints what it wrote where; the runner re-checks it on every run, so a vault paired from the plugin is covered too.
Each side's operations are pinned to its own repository with GIT_CEILING_DIRECTORIES and a top-level check, not just by changing directory. If the inner .git disappears, the operation fails with REPO_MISSING instead of quietly acting on the repository above it.
Pairing a second vault, moving one, deleting one
- Pair this vault, in the settings and the setup guide, asks Termux for a profile without re-running the installer. The plugin leaves a pairing request in its runtime folder and wakes the runner; the token is generated in Termux and comes back. Nothing secret ever travels towards Termux, and nothing the request contains is trusted.
- A vault you moved is found again by the marker the runner leaves in its runtime folder, and keeps its profile and token.
- A vault you deleted is reported as a broken profile and is never re-pointed at some other repository.
- The bridge check now says which profile serves this vault, and "runner installed for a different vault" became "no profile for this vault", with the two ways to fix it.
Credentials per repository
Two vaults can use two accounts. The installer configures credential.helper with a credential file of this repository's own, or core.sshCommand with a separate key, locally for the repository it is configuring. As before, credentials never reach the plugin, a result file or any log.
Status panel
- The Staged and Untracked group buttons sit in the same columns as every folder row and file row below them. A header used to render only its real buttons, which parked "unstage all" and "stage all" in the file rows' open-file column and made the whole group look misaligned.
Diff, file history and conflicts
- With line wrapping on, the
+and−markers stayed in the number gutter where they belong. They were escaping into the file content whenever a line number reached three digits, because the wrapped layout guessed the gutter's width instead of measuring it. - Deleted characters are highlighted in a solid #AA1414 with white text. The translucent red before it washed out against the red row on a phone, which is exactly where the changed characters are hardest to find.
- New setting: custom colours for the diff and conflict panes, off by default. Switching it on reveals pickers for the added/deleted line backgrounds, the added/deleted character highlights and the two conflict sides, configured separately for the light and the dark theme, with a Reset per theme. While it is off the panes follow your theme exactly as before.
- Whitespace glyphs now work in the conflict pane too. A whitespace-only difference is a common reason two sides of a conflict look identical.
- Line wrapping, whitespace glyphs and the colours apply to all three file views and take effect immediately, without a Termux round trip.
- One surface per question. Every route to a diff or a history opens the panes: the command palette used to open older, plainer modals while the context menu opened the panels. The file history panel gained the one thing those modals had that it did not, a per-commit eye button showing the file as it was at that commit.
- The file history panel reloads when you open it again for the same file (it used to redisplay a stale list), hides Load more once the history is exhausted, keeps the diffs it already fetched so a colour or theme change costs no round trip, and re-measures after a rotation.
- The repository history no longer shows a blank panel while its first page loads; all four panels use the same "runner is working" indicator, the same path header and the same wording for an empty or failed diff.
Sparse checkout safety
Delete files locally, offered when the safety check blocks on files that are only new here, now moves all of them to the trash. git reports a fully untracked folder as a single dir/ line, so one entry was deleted and the rest were quietly left behind. The runner reports those files individually, the plugin expands folders file by file, names anything it could not move instead of logging it away, and re-runs the check so you can see the state is clear.
Internals
Runner v10: profiles (profiles/<id>.conf), automatic migration of the single-repo config, one run drains every profile, profileId in requests and results, REPO_MISSING, per-profile git pinning, nested-vault exclusion, relocation and self-pairing, a global single-instance lock, and git status -uall in the safety check. Reasoning and the rejected alternatives are in ADR-002; the protocol, threat model and limitations are updated in the same release. 262 unit tests, 272 e2e checks.
Full Changelog: 0.5.10...0.6.0
0.5.10
Update notes 0.5.10
Requires a Termux runner update (v8 → v9). Open the companion app and tap Update runner, or re-run the install command from the plugin settings.
One context menu everywhere
The Git menu is now described in one place and rendered identically by file rows, folder rows, group headers and the file explorer, in a fixed order: stage/unstage, discard, keep local / keep remote, open diff or conflict view, open file history, open in default app, copy path, then the .gitignore / sparse / .git-exclude entries. Every entry states its scope, and bulk titles carry the number of paths they will touch.
- A target now has exactly one state, so a staged row no longer offers Stage when the same file was edited again afterwards. In the file explorer, where there is no row context, the state is inferred with the precedence conflicted, unstaged, untracked, staged.
- Folders and groups get the same entries applied to their contents; only single paths can flip a config rule off, because a mixed selection has no single state to flip.
- New entries: Copy path, and Abort merge on the conflicts group.
File history
A file's own history panel, opened from the context menu or the command palette:
- the full path on one line at the top;
- the commits that touched the file, each with what happened to it:
added, +3 −0,+25 −12,renamed from A/f.md,deleted; - a Restore file button per commit; its label may be clipped on a narrow screen, its icon never is;
- tapping a commit expands the diff it introduced for that file, with the same "runner is working" indicator the status panel uses;
- each diff block carries a Restore this block button in a bar of its own, so it cannot be confused with the diff text. A block is restored only when the current file still contains it verbatim; if the file drifted, the plugin refuses and says so instead of guessing where the block belongs.
Status panel
- Renames and copies are shown as moves: the row keeps the old path (
← old-name) instead of looking like an unrelated addition. Note that git itself reports an unstaged move as a deletion plus an untracked file; the rename only exists once the change is staged. - The fetch, pull and push icons are one family: a cloud with an arrow down (pull), an arrow up (push) and a question mark (fetch, which only asks the remote).
- Their activity animation is a real highlight now: a band a quarter of the icon high, in the accent colour, gliding along the glyph. The previous version dragged one slice of the icon downwards and stepped in thirds.
- File counts are centred in the status column.
Internals
Runner v9: file-log uses --raw --numstat, so each commit reports the change letter, both sides of a rename and the added/deleted counts. 229 unit tests, 221 e2e checks.
Full Changelog: 0.5.9...0.5.10
0.5.9
Update notes 0.5.9
Requires a Termux runner update (v7 → v8). Open the companion app and tap Update runner, or re-run the install command from the plugin settings. Until the versions match, the plugin repeats the warning on every result.
Safety fix ⚠️ (please update)
Per-path actions did not exclude protected sparse paths that lay below the requested path. Staging a parent folder in the tree layout, or picking "Git: Stage" on a folder, therefore ran git add over a protected subdirectory and put its files in the index; the sparse safety gate then blocked the commit, which is how it surfaced. stage-file, unstage-file and discard-file now carry the same :(exclude) specs that stage-all always used. Two related defects went with it: discarding an untracked folder failed instead of removing the untracked files inside it, and a folder whose only tracked content was a protected subdirectory was misclassified as tracked.
Sparse safety window
Two recovery buttons, side by side on a phone:
- Delete files locally moves the listed files to Obsidian's trash (reversible, git history untouched). Offered only for paths that are new here (untracked or newly added). A tracked path is never offered, because deleting it would create the staged deletion the check exists to block; the note under the buttons says how many such paths are in the list.
- Unprotect path removes the sparse exclusion for the directories the violations actually fall under, so they are checked out and committed like any other directory.
Both ask for confirmation and state the consequence first.
Status and history panels
- File counts moved to the right-hand column, aligned with the change-letter column of file rows, in group headers, folder rows and commit rows. Space is reserved for two digits; larger numbers shrink, anything above 9999 shows
9999+. Tapping a clamped number shows the exact figure for three seconds, and press-and-hold keeps it open. - Folder rows in the tree layout now use the same slot layout as file rows, so the action buttons line up in one column instead of drifting left.
Diff pane
- Wrapped lines follow the pane width instead of the longest line in the file. The rendering table used an automatic layout, so wrapping still left the pane scrolling sideways.
- Display preferences (line wrapping, invisible characters) are re-applied whenever the pane re-renders or is resized. The pane is reused for every diff, and applying them once per DOM build silently dropped the whitespace glyphs after a few files.
Commands
- Command names no longer repeat the plugin name.
Native Git Bridge: Native Git: Fetchis nowNative Git Bridge: Fetch. Command ids are unchanged, so existing hotkeys keep working. - New: Discard all local changes (keep staged). Unstaged edits go back to the staged version (or to HEAD). Staged changes and untracked files are kept.
- New: Reset everything to HEAD (staged and local changes). The effect of
git reset --hard, written as a pathspec restore so protected sparse paths stay excluded, which a literal--hardcannot do. HEAD is not moved and untracked files are not deleted.
Bridge check
- "Update companion app" appears only when the companion reported an older version. It used to appear whenever a companion had ever answered, so a matched plugin and companion were told to update.
- Result windows no longer carry a Close button; the window's ✕ closes them.
Internals
Runner v8: protected-path excludes on per-path actions, discard-all, reset-all.
Full Changelog: 0.5.8...0.5.9
0.5.8
Update notes 0.5.8
Termux runner update (v6 → v7): Open the companion app and tap Update runner, or re-run the install command from the plugin settings. The companion detects the new runner itself after the update; until the versions match, the plugin repeats the warning on every result. The companion app also changed in this release: install the new APK.
Status panel
- Tree/list layout toggle. The icon shows the current layout; tapping switches to the other. It occupies the slot previously used by the operation log button.
- Folder rows carry the same actions as file rows and apply them only to files in that group's state. Staging a folder under Changes stages tracked changes only; unstaging a folder unstages only what was staged. A collapsed folder shows the number of files it holds in that state.
- Discarding a folder in Untracked moves the new files to Obsidian's trash after confirmation, so the action is reversible.
- A staged row now opens the
HEAD → stageddiff, an unstaged row thestaged → working treediff. Previously both showed the same diff for a file that was staged and then edited again. - The operation log moved to Settings → Advanced. The command palette entry is unchanged.
- New setting Auto-refresh status (seconds), off by default. While the status panel is open, it runs a status at that interval to pick up changes made outside the plugin. Each refresh wakes Termux.
- Failed operations refresh the panel as well. A rejected pull or a new conflict no longer leaves stale state on screen.
History panel
- The same tree/list toggle, applied to each commit's changed files.
Diff pane
- Fixed a phantom indent and a
+/-prefix that wrapped onto its own line. The renderer's template whitespace was being drawn as file content. - The
+/-prefix moved into the line-number gutter and no longer scrolls with the code. Numbers are more compact, and unchanged lines show both numbers on one line. - New setting Show invisible characters:
·space,→tab,␍CR. Copying from the diff copies the glyphs. - Long lines in result windows wrap instead of being clipped.
Conflict resolution
- Rows carrying the side label and Keep button are visually separated from file text: own background, colour-coded edge, interface-font chip, and the button pinned right so it cannot cover a line.
- New setting Show raw conflict markers. Off (default): the
<<<<<<</=======/>>>>>>>lines stay hidden under the action rows. On: they appear as numbered file lines, and labels and buttons render as separate rows marked▸in the gutter, so an empty gutter cannot be mistaken for a wrapped line. - The resolution pane closes once the file is resolved.
- Committing after a resolution prefills git's prepared merge message (
Merge branch … # Conflicts: …). Sync uses it automatically. - One line number per file line; wrapped lines continue with an empty gutter.
Dialogs
- Cancel buttons removed. The ✕ closes the dialog and counts as declining. A single action button with an icon remains: top-left on mobile (matching the panel toolbar), bottom-centre on desktop.
Companion app
- Versions are listed as companion, plugin, runner, with the lagging part highlighted in red.
- An outdated runner no longer reports "all steps done". The screen states the problem and offers Update runner, which copies the release-pinned install command and opens Termux.
- The runner reports its version to the app's test trigger, so the warning clears immediately after an update.
Internals
Runner v7: INDEX pseudo-refs for diff-file; stage-file mode argument (all | update); status fields on failed mutating actions; mergeMsg and mergeInProgress in status; NGB_RUNNER_VERSION on stdout for the companion's probe.
Full Changelog: 0.5.7...0.5.8
0.5.7
Update notes 0.5.7
Warning
This release requires a Termux runner update (v5 → v6): after updating the plugin, re-run the install command. The version handshake will remind you until the runner matches. The companion app also changed — install the new APK from this release.
Manual merge-conflict resolution
- Conflicted files are marked with a warning icon in the status panel.
- Tapping a text conflict opens a resolution pane: the file with real line numbers, each conflict block showing Local — yours (HEAD) against Remote — theirs (branch or commit), with per-block Keep local (yours) / Keep remote (<commit>) buttons on the marker rows. Lines wrap, so the buttons never scroll out of reach. Once all blocks are resolved, one tap marks the file resolved (stages it).
- Files the pane cannot display (binary) open a context menu instead; the Git menu for any conflicted file now offers whole-file keep local / keep remote (confirmed, runner v6
git checkout --ours|--theirs+ mark resolved, protected paths refused) and Open in default app. - As always, nothing is ever resolved automatically — every choice is an explicit tap, and
abort mergeremains available. - Blocks that are still unresolved are written back with Obsidian-safe markers (
-<<<<<<</-=======/->>>>>>>), so a half-resolved note no longer renders as headings and blockquote soup in the editor. Both marker forms are understood; git semantics are unaffected.
Diff pane
- Fixed: line numbers now scroll vertically together with the content (they are sticky table cells instead of absolutely positioned overlays); they still stay visible while scrolling horizontally.
- New setting "Wrap long lines in diffs" — wraps instead of horizontal scrolling (shared, cosmetic, stored in data.json).
Companion app
- Versions are shown stacked — companion, plugin, runner — with the lagging part highlighted in red.
- An outdated runner no longer shows "all steps done": the screen explains the problem and shows an Update runner button that copies the release-pinned install command and opens Termux.
Internals
- Runner v6: new
resolve-conflictaction; commit-ish validation unchanged otherwise. 174 unit tests, 178 e2e checks.
Full Changelog: 0.5.6...0.5.7