Repository navigation
Fix #10: checkout sets the executable bit, and status/diff report mode changes - #15
Merged
Merged
Conversation
Checkout, clone, restore, switch and reset must apply a 100755 entry's mode, and status and diff must report a mode-only change the way git does. These fail on main.
… report mode changes Add an optional FileSystem.chmod, implement it for MemoryFileSystem and TreeBackedFs, and have checkoutEntry apply or clear the executable bit. Compare modes as well as hashes when diffing the index against the worktree and against HEAD, and in unpack-trees' same(), so a mode-only change is reported and checked out instead of silently committed.
worktreeMode in lib/index.ts is now the one place that derives a worktree entry's git mode, mirroring git's ce_mode_from_stat. Without chmod the exec bit cannot be written back, so a tracked regular file keeps its index mode and a new file is staged as 100644. Before this, every 100755 file showed modified after clone on such a filesystem and commit -a flipped it to 100644.
WORKTREE_MUST_BE_UPTODATE compared only the worktree hash, so chmod -x followed by switch silently overwrote the change. PathState now exposes the worktree mode (from the same lstat the hash uses) and the check fails when it differs from the index mode, printing git's "Your local changes ... would be overwritten by checkout" and exiting 1.
saveStash compared worktree blobs to the index by hash alone, so chmod -x on a tracked executable printed "No local changes to save". It now uses worktreeMode for both the dirty check and the recorded worktree tree. Popping that stash still leaves the mode untouched because merge-ort's triviallyResolve compares hashes only; that gap stays open.
The FileSystem.chmod contract says it follows symlinks, and MemoryFileSystem does. TreeBackedFs returned early on a symlink instead.
Each test fails when one of these is reverted: the chmod(0o644) clear in checkoutEntry (on a filesystem whose writeFile keeps the old mode), the mode compare in hasStagedChanges (describe --dirty), hasModeChange in formatBinaryDiff, and the symlink stand-in rule in worktreeMode.
…ansion#10) Saving a mode-only change into the stash while stash pop cannot restore it (merge-ort triviallyResolve compares hashes only) would drop the change from the worktree. Leave stash as it was on main and track the gap separately.
Contributor
Author
|
@blindmansion could you review this when you have a moment? Checkout now sets the executable bit on |
Owner
|
I'll need to take a closer look at this one and run the larger oracle suites, I should be able to get this in over the weekend. |
Contributor
Author
Sure thanks @blindmansion, can you also look at other PR as well pls ? quite a small change |
This was referenced Oct 14, 2026
Hazzng
added a commit
to Hazzng/sql-fs
that referenced
this pull request
Oct 14, 2026
Carry blindmansion/just-git#15 (fixes #10) and #14 (fixes #12) until a just-git release includes them. git status and git diff report a chmod-only change, a branch switch applies a mode-only difference and refuses to overwrite a local one, and git ls-tree works. The wrapper keeps applying index modes because just-git's merge still leaves a conflicted file with its old mode.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #10
Summary
git clone,git checkout -- <path>,git restore,git switch,git checkout <branch>andgit reset --hardnow write100755blobs as executable files, and write100644blobs over an executable file as non-executable.git statusandgit diffreport a mode-only change (old mode 100755/new mode 100644), both unstaged and staged, the way git does withcore.fileMode=true.git switchandgit checkout <branch>refuse to overwrite a local mode-only change. The issue's repro now printsrestored 755,nothing to commit, working tree cleanandcloned 755, matching git 2.34.1.A
FileSystemwithoutchmodis treated the way git treats a filesystem that cannot hold the exec bit (core.fileMode=false): a tracked regular file keeps its index mode whateverstatreports, and a new file is staged as100644. Before this, every100755file showed modified right aftercloneon such a filesystem andcommit -aflipped it to100644.Root cause
Several places assumed "same hash means unchanged", and nothing could write the bit.
FileSystemhad nochmod, socheckoutEntrycould onlywriteFileand the worktree never got the bit.gitModeFromFileStatthen derived100644fromstaton the nextgit add, which is where the phantom100755 -> 100644commit came from.diffIndexToWorkTreereported "modified" only when the worktree hash differed from the index.getStagedChanges,collectCachedindiff.tsandhasStagedChangescompared index entries to HEAD by hash alone. A mode-only change was invisible tostatusanddiffuntil it was committed.twowayMergeandonewayMergeinunpack-trees.tscompared tree entries by hash, so switching to a branch where only the mode differs kept the old file untouched. TheWORKTREE_MUST_BE_UPTODATEcheck compared only the worktree hash, so a localchmod -xwas silently overwritten byswitch.The fix
FileSystemgains an optionalchmod?(path, mode)with the same signature as just-bash'sIFileSystem.chmod.MemoryFileSystemandTreeBackedFsimplement it; both keep the file-type bits and replace only the permission bits, and both follow a symlink to its target.TreeBackedFscopies a tree-backed entry into its overlay with the new mode.checkoutEntrycallschmod(path, 0o755)after writing a100755entry, andchmod(path, 0o644)when a100644entry replaces a file that had an exec bit.worktreeMode(fs, indexEntry, stat)inlib/index.tsis the one place that derives a worktree path's git mode, mirroring git'sce_mode_from_stat. It applies the symlink stand-in rule for filesystems without symlinks and thefileMode=falserule for filesystems withoutchmod.diffIndexToWorkTree,addFileToIndex, the index stat cache,refreshIndexStatsAfterCheckoutandunpack-treesall go through it.WorkTreeDiffcarriesworktreeMode.diffIndexToWorkTreemarks an entry modified when hash or mode differs, so every consumer (status, diff,commit -a, reset, describe, the dirty-worktree checks) inherits the rule. The diff command usesworktreeModefornew mode, and the formatter no longer drops a patch whose only change is the mode, for text or binary files.unpack-trees.tsgets asame(hash, mode, hash, mode)helper andPathState.indexMode, used in the one-way and two-way decision rules.PathStatealso exposes the worktree mode, andWORKTREE_MUST_BE_UPTODATEfails when it differs from the index mode, printing git'sYour local changes to the following files would be overwritten by checkoutand exiting 1.isExecutableModeandisRegularFileModejoinisSymlinkModeandisSubmoduleModeinsrc/lib/symlink.ts.core.fileModeis still not read from config. The presence ofchmodon theFileSystemdecides it.Note for embedders. Every custom
FileSystemadapter written before this change lackschmod, so it now gets thefileMode=falsebehaviour. An adapter backed by a real disk, whosestatdoes report exec bits, used to stage a new executable file as100755and now stages it as100644. Addingchmodto the adapter restores exec-bit tracking.initstill writescore.filemode=trueon these filesystems.Tests
Added in
test/commands/*.test.tsunder "executable bit" describes, sharing asetupExecBitRepohelper intest/util.ts(an executablerun.shonmain, the same content as100644onplain).checkout -- <path>,restore,restore --source,switch,checkout <branch>,reset --hard,reset --hard <branch>andcloneleave the right permission bits and a cleangit status --short.git add -A && git commitafter a checkout exits 1 withnothing to commit, working tree clean. A staged mode-only change commits, andgit describe --dirtyreports it.chmod -xon a tracked executable printsM run.sh, the long status,old mode 100755/new mode 100644fromgit diff,git diff HEADandgit diff --cachedafteradd, andrun.sh | 0from--stat. A content plus mode change prints the mode lines followed by anindexline without a mode, as git does. A mode-only change on a binary file prints only the mode lines.git mvkeeps the source file's permission bits, so moving an executable shows as a pure rename (R run.sh -> bin.sh). It used to copy the file and lose the bit, which the new mode-aware status would have reported as a change.chmod -xthengit switch plainorgit switch contentexits 1 with git's exact refusal, leaves the bit and HEAD alone. A unit test covers theWORKTREE_MUST_BE_UPTODATEmode-only case.test/commands/file-mode.test.ts: on aMemoryFileSystemwithchmodremoved,clone+status+commit -a+add -Akeep100755with a clean status, and a new file whosestathas an exec bit is added as100644. On aMemoryFileSystemwith symlink support removed, status and diff are clean after cloning a120000entry.MemoryFileSystem.chmodandTreeBackedFs.chmodunit tests (including following tree-backed and overlay symlinks),checkoutEntryagainst aMemoryFileSystemwithchmodremoved, and against one whosewriteFilekeeps the old mode (as real disks do) to pin thechmod(0o644)clear.bun test: 3521 pass, 29 skip, 0 fail. Fourcoreoracle traces replay with the same two known and two failed steps asmainatfaa98c5(both failures are message-text differences between local git 2.34.1 and the 2.53 target, unrelated to modes).Open
triviallyResolvecompares the three stages by hash only, so a mode-only change on one side is lost:cherry-pickof a mode-only commit says "The previous cherry-pick is now empty",mergedrops the other side's mode change, andrebaseloses it. Real git applies the mode in all three.git stashafterchmod -xstill printsNo local changes to savewhilestatusshowsM, as on main. Saving the mode change is easy, butstash popwould not restore it until the merge-ort gap above is fixed, so the change would leave the worktree. Both should land together.create mode 100644 run.shanddelete mode 100755 run.shfor a mode-only commit where git printsmode change 100755 => 100644 run.sh.FileSystemthat haschmodbut whosestatreturnsmode: 0shows every100755file as modified after checkout. No bundled filesystem does this.