diff --git a/modules/bit/cmd/bit/wbtest_env_lock_shared.mbt b/modules/bit/cmd/bit/wbtest_env_lock_shared.mbt index 1c58c49c..6639155e 100644 --- a/modules/bit/cmd/bit/wbtest_env_lock_shared.mbt +++ b/modules/bit/cmd/bit/wbtest_env_lock_shared.mbt @@ -41,8 +41,9 @@ fn wbtest_resolve_real_git(repo_root : String) -> String { ///| /// Resolve the freshly built `bit.exe` for wbtests. The native binary lives -/// under a debug or release build directory depending on how it was compiled, -/// so prefer whichever exists (debug first, since that is the default build). +/// under a debug or release build directory depending on how it was compiled. +/// Prefer the variant matching the running test binary so a stale build of the +/// other variant cannot affect a whitebox test. fn wbtest_resolve_bit_exe(repo_root : String) -> String { let fs : &@bitcore.RepoFileSystem = OsFs::new() let debug_rel = "/_build/native/debug/build/mizchi/bit/bit.exe" @@ -52,6 +53,7 @@ fn wbtest_resolve_bit_exe(repo_root : String) -> String { // module dir), then the caller-supplied repo_root. let roots : Array[String] = [] let exe = @env.args()[0] + let running_release = exe.contains("/_build/native/release/") match exe.find("/_build/") { Some(i) => roots.push(String::unsafe_substring(exe, start=0, end=i)) None => () @@ -59,15 +61,24 @@ fn wbtest_resolve_bit_exe(repo_root : String) -> String { roots.push(repo_root) for root in roots { let debug_path = root + debug_rel - if fs.is_file(debug_path) { - return debug_path - } let release_path = root + release_rel - if fs.is_file(release_path) { - return release_path + if running_release { + if fs.is_file(release_path) { + return release_path + } + if fs.is_file(debug_path) { + return debug_path + } + } else { + if fs.is_file(debug_path) { + return debug_path + } + if fs.is_file(release_path) { + return release_path + } } } - // Not found anywhere: return the primary debug candidate so the caller's + // Not found anywhere: return the matching primary candidate so the caller's // existence check fails with a clear, actionable path. - roots[0] + debug_rel + roots[0] + (if running_release { release_rel } else { debug_rel }) } diff --git a/modules/bit/cmd/bit/worktree.mbt b/modules/bit/cmd/bit/worktree.mbt index 1710c1a1..7231709f 100644 --- a/modules/bit/cmd/bit/worktree.mbt +++ b/modules/bit/cmd/bit/worktree.mbt @@ -902,7 +902,18 @@ async fn handle_worktree_add(args : Array[String]) -> Unit raise Error { new_branch~, relative_paths~, orphan~, + checkout_files=!no_checkout, ) + if !orphan && !no_checkout { + let worktree_git_dir = resolve_git_dir(fs, resolved_wt_path) + match @bitlib.resolve_head_commit(fs, worktree_git_dir) { + Some(commit_id) => + @bitlibnative.apply_worktree_modes_from_commit( + fs, fs, resolved_wt_path, common_git_dir, commit_id, + ) + None => () + } + } if lock { @bitlib.lock_worktree(fs, fs, root, resolved_wt_path, reason=lock_reason) } diff --git a/modules/bit/cmd/bit/worktree_wbtest.mbt b/modules/bit/cmd/bit/worktree_wbtest.mbt index 6a54a29c..6226356f 100644 --- a/modules/bit/cmd/bit/worktree_wbtest.mbt +++ b/modules/bit/cmd/bit/worktree_wbtest.mbt @@ -34,7 +34,7 @@ async fn worktree_wbtest_collect_shim_git( "SHIM_REAL_GIT": wbtest_resolve_real_git(repo_root), "SHIM_EXEC_PATH": repo_root + "/third_party/git", "SHIM_MOON": shim_moon, - "SHIM_CMDS": "worktree fetch", + "SHIM_CMDS": "worktree fetch status diff", "SHIM_STRICT": "1", } let git_args : Array[String] = ["-C", worktree_wbtest_current_shim_cwd()] @@ -367,6 +367,282 @@ async fn worktree_wbtest_case_explicit_head_creates_detached_worktree() -> Unit cleanup_tree(fs, root) } +///| +async fn worktree_wbtest_case_preserves_executable_mode() -> Unit raise Error { + @bitnative.init_native_io() + let fs = OsFs::new() + let root = "/tmp/bit-test-worktree-executable-mode-" + + get_current_timestamp().to_string() + cleanup_tree(fs, root) + defer cleanup_tree(fs, root) + fs.mkdir_p(root) + let repo_dir = root + "/repo" + let worktree_dir = root + "/worker" + + let prev_shim_cwd = @sys.get_env_var("GIT_SHIM_CWD") + let prev_shim_pwd = @sys.get_env_var("GIT_SHIM_PWD") + let prev_caller_pwd = @sys.get_env_var("GIT_SHIM_CALLER_PWD") + let prev_git_dir = @sys.get_env_var("GIT_DIR") + let prev_work_tree = @sys.get_env_var("GIT_WORK_TREE") + defer add_test_restore_env("GIT_SHIM_CWD", prev_shim_cwd) + defer add_test_restore_env("GIT_SHIM_PWD", prev_shim_pwd) + defer add_test_restore_env("GIT_SHIM_CALLER_PWD", prev_caller_pwd) + defer add_test_restore_env("GIT_DIR", prev_git_dir) + defer add_test_restore_env("GIT_WORK_TREE", prev_work_tree) + + add_test_run_git(root, ["init", "--initial-branch=main", "-q", "repo"]) + add_test_run_git(repo_dir, ["config", "user.name", "Test User"]) + add_test_run_git(repo_dir, ["config", "user.email", "test@example.com"]) + let executable_path = repo_dir + "/hook.sh" + fs.write_string(executable_path, "#!/bin/sh\necho ok\n") + @test.assert_eq( + @process.run("chmod", ["+x", executable_path], inherit_env=true), + 0, + ) + add_test_run_git(repo_dir, ["add", "hook.sh"]) + add_test_run_git(repo_dir, ["commit", "-q", "-m", "base"]) + + @sys.set_env_var("GIT_SHIM_CWD", repo_dir) + @sys.set_env_var("GIT_SHIM_PWD", repo_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", repo_dir) + @sys.unset_env_var("GIT_DIR") + @sys.unset_env_var("GIT_WORK_TREE") + + worktree_wbtest_run_shim_git([ + "worktree", "add", "-b", "repro/mode", worktree_dir, "HEAD", + ]) + + let mode = @bitio.lstat_entry_meta(worktree_dir + "/hook.sh").map(meta => { + meta.mode + }) + @test.assert_eq(mode, Some(0o100755)) + @test.assert_eq( + add_test_git_stdout(worktree_dir, ["status", "--porcelain"]), + "", + ) + + @sys.set_env_var("GIT_SHIM_CWD", worktree_dir) + @sys.set_env_var("GIT_SHIM_PWD", worktree_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", worktree_dir) + let (status_code, status_out, _) = worktree_wbtest_collect_shim_git([ + "status", "--porcelain", + ]) + @test.assert_eq(status_code, 0) + @test.assert_eq(status_out, "") + let (quiet_code, quiet_out, _) = worktree_wbtest_collect_shim_git([ + "diff", "--quiet", + ]) + @test.assert_eq(quiet_code, 0) + @test.assert_eq(quiet_out, "") + let (diff_code, diff_out, _) = worktree_wbtest_collect_shim_git(["diff"]) + @test.assert_eq(diff_code, 0) + @test.assert_eq(diff_out, "") + + @sys.set_env_var("GIT_SHIM_CWD", repo_dir) + @sys.set_env_var("GIT_SHIM_PWD", repo_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", repo_dir) + worktree_wbtest_run_shim_git(["worktree", "remove", worktree_dir]) + assert_false(fs.is_dir(worktree_dir)) +} + +///| +async fn worktree_wbtest_case_detects_mode_only_change() -> Unit raise Error { + @bitnative.init_native_io() + let fs = OsFs::new() + let root = "/tmp/bit-test-worktree-mode-only-" + + get_current_timestamp().to_string() + cleanup_tree(fs, root) + defer cleanup_tree(fs, root) + fs.mkdir_p(root) + let repo_dir = root + "/repo" + let worktree_dir = root + "/worker" + + let prev_shim_cwd = @sys.get_env_var("GIT_SHIM_CWD") + let prev_shim_pwd = @sys.get_env_var("GIT_SHIM_PWD") + let prev_caller_pwd = @sys.get_env_var("GIT_SHIM_CALLER_PWD") + let prev_git_dir = @sys.get_env_var("GIT_DIR") + let prev_work_tree = @sys.get_env_var("GIT_WORK_TREE") + defer add_test_restore_env("GIT_SHIM_CWD", prev_shim_cwd) + defer add_test_restore_env("GIT_SHIM_PWD", prev_shim_pwd) + defer add_test_restore_env("GIT_SHIM_CALLER_PWD", prev_caller_pwd) + defer add_test_restore_env("GIT_DIR", prev_git_dir) + defer add_test_restore_env("GIT_WORK_TREE", prev_work_tree) + + add_test_run_git(root, ["init", "--initial-branch=main", "-q", "repo"]) + add_test_run_git(repo_dir, ["config", "user.name", "Test User"]) + add_test_run_git(repo_dir, ["config", "user.email", "test@example.com"]) + let executable_path = repo_dir + "/hook.sh" + fs.write_string(executable_path, "#!/bin/sh\necho ok\n") + @test.assert_eq( + @process.run("chmod", ["+x", executable_path], inherit_env=true), + 0, + ) + add_test_run_git(repo_dir, ["add", "hook.sh"]) + add_test_run_git(repo_dir, ["commit", "-q", "-m", "base"]) + + @sys.set_env_var("GIT_SHIM_CWD", repo_dir) + @sys.set_env_var("GIT_SHIM_PWD", repo_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", repo_dir) + @sys.unset_env_var("GIT_DIR") + @sys.unset_env_var("GIT_WORK_TREE") + worktree_wbtest_run_shim_git([ + "worktree", "add", "-b", "repro/mode-only", worktree_dir, "HEAD", + ]) + + @test.assert_eq( + @process.run("chmod", ["-x", worktree_dir + "/hook.sh"], inherit_env=true), + 0, + ) + @sys.set_env_var("GIT_SHIM_CWD", worktree_dir) + @sys.set_env_var("GIT_SHIM_PWD", worktree_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", worktree_dir) + let (status_code, status_out, _) = worktree_wbtest_collect_shim_git([ + "status", "--porcelain", + ]) + @test.assert_eq(status_code, 0) + @test.assert_eq(status_out, " M hook.sh\n") + let (diff_code, _, _) = worktree_wbtest_collect_shim_git(["diff", "--quiet"]) + @test.assert_eq(diff_code, 1) + + @sys.set_env_var("GIT_SHIM_CWD", repo_dir) + @sys.set_env_var("GIT_SHIM_PWD", repo_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", repo_dir) + let (remove_code, _, _) = worktree_wbtest_collect_shim_git([ + "worktree", "remove", worktree_dir, + ]) + @test.assert_eq(remove_code, 1) + assert_true(fs.is_dir(worktree_dir)) + + @test.assert_eq( + @process.run("chmod", ["+x", worktree_dir + "/hook.sh"], inherit_env=true), + 0, + ) + worktree_wbtest_run_shim_git(["worktree", "remove", worktree_dir]) +} + +///| +async fn worktree_wbtest_case_symlink_stays_clean() -> Unit raise Error { + @bitnative.init_native_io() + let fs = OsFs::new() + let root = "/tmp/bit-test-worktree-symlink-clean-" + + get_current_timestamp().to_string() + cleanup_tree(fs, root) + defer cleanup_tree(fs, root) + fs.mkdir_p(root) + let repo_dir = root + "/repo" + let worktree_dir = root + "/worker" + + let prev_shim_cwd = @sys.get_env_var("GIT_SHIM_CWD") + let prev_shim_pwd = @sys.get_env_var("GIT_SHIM_PWD") + let prev_caller_pwd = @sys.get_env_var("GIT_SHIM_CALLER_PWD") + let prev_git_dir = @sys.get_env_var("GIT_DIR") + let prev_work_tree = @sys.get_env_var("GIT_WORK_TREE") + defer add_test_restore_env("GIT_SHIM_CWD", prev_shim_cwd) + defer add_test_restore_env("GIT_SHIM_PWD", prev_shim_pwd) + defer add_test_restore_env("GIT_SHIM_CALLER_PWD", prev_caller_pwd) + defer add_test_restore_env("GIT_DIR", prev_git_dir) + defer add_test_restore_env("GIT_WORK_TREE", prev_work_tree) + + add_test_run_git(root, ["init", "--initial-branch=main", "-q", "repo"]) + add_test_run_git(repo_dir, ["config", "user.name", "Test User"]) + add_test_run_git(repo_dir, ["config", "user.email", "test@example.com"]) + fs.write_string(repo_dir + "/hook.sh", "#!/bin/sh\necho ok\n") + @asyncfs.symlink(target="hook.sh", repo_dir + "/hook-link") + add_test_run_git(repo_dir, ["add", "hook.sh", "hook-link"]) + add_test_run_git(repo_dir, ["commit", "-q", "-m", "base"]) + + @sys.set_env_var("GIT_SHIM_CWD", repo_dir) + @sys.set_env_var("GIT_SHIM_PWD", repo_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", repo_dir) + @sys.unset_env_var("GIT_DIR") + @sys.unset_env_var("GIT_WORK_TREE") + worktree_wbtest_run_shim_git([ + "worktree", "add", "-b", "repro/symlink", worktree_dir, "HEAD", + ]) + + @test.assert_eq( + @bitio.read_symlink_target_path(worktree_dir + "/hook-link"), + Some("hook.sh"), + ) + @test.assert_eq( + add_test_git_stdout(worktree_dir, ["status", "--porcelain"]), + "", + ) + @sys.set_env_var("GIT_SHIM_CWD", worktree_dir) + @sys.set_env_var("GIT_SHIM_PWD", worktree_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", worktree_dir) + let (status_code, status_out, _) = worktree_wbtest_collect_shim_git([ + "status", "--porcelain", + ]) + @test.assert_eq(status_code, 0) + @test.assert_eq(status_out, "") + let (diff_code, _, _) = worktree_wbtest_collect_shim_git(["diff", "--quiet"]) + @test.assert_eq(diff_code, 0) + + @sys.set_env_var("GIT_SHIM_CWD", repo_dir) + @sys.set_env_var("GIT_SHIM_PWD", repo_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", repo_dir) + worktree_wbtest_run_shim_git(["worktree", "remove", worktree_dir]) + assert_false(fs.is_dir(worktree_dir)) +} + +///| +async fn worktree_wbtest_case_no_checkout_leaves_files_absent() -> Unit raise Error { + @bitnative.init_native_io() + let fs = OsFs::new() + let root = "/tmp/bit-test-worktree-no-checkout-" + + get_current_timestamp().to_string() + cleanup_tree(fs, root) + defer cleanup_tree(fs, root) + fs.mkdir_p(root) + let repo_dir = root + "/repo" + let worktree_dir = root + "/worker-no-checkout" + + let prev_shim_cwd = @sys.get_env_var("GIT_SHIM_CWD") + let prev_shim_pwd = @sys.get_env_var("GIT_SHIM_PWD") + let prev_caller_pwd = @sys.get_env_var("GIT_SHIM_CALLER_PWD") + let prev_git_dir = @sys.get_env_var("GIT_DIR") + let prev_work_tree = @sys.get_env_var("GIT_WORK_TREE") + defer add_test_restore_env("GIT_SHIM_CWD", prev_shim_cwd) + defer add_test_restore_env("GIT_SHIM_PWD", prev_shim_pwd) + defer add_test_restore_env("GIT_SHIM_CALLER_PWD", prev_caller_pwd) + defer add_test_restore_env("GIT_DIR", prev_git_dir) + defer add_test_restore_env("GIT_WORK_TREE", prev_work_tree) + + add_test_run_git(root, ["init", "--initial-branch=main", "-q", "repo"]) + add_test_run_git(repo_dir, ["config", "user.name", "Test User"]) + add_test_run_git(repo_dir, ["config", "user.email", "test@example.com"]) + fs.write_string(repo_dir + "/init.t", "main\n") + add_test_run_git(repo_dir, ["add", "init.t"]) + add_test_run_git(repo_dir, ["commit", "-q", "-m", "base"]) + + @sys.set_env_var("GIT_SHIM_CWD", repo_dir) + @sys.set_env_var("GIT_SHIM_PWD", repo_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", repo_dir) + @sys.unset_env_var("GIT_DIR") + @sys.unset_env_var("GIT_WORK_TREE") + worktree_wbtest_run_shim_git([ + "worktree", "add", "--no-checkout", "-b", "repro/no-checkout", worktree_dir, + "HEAD", + ]) + + assert_true(fs.is_file(worktree_dir + "/.git")) + assert_false(fs.is_file(worktree_dir + "/init.t")) + assert_false( + fs.is_file(repo_dir + "/.git/worktrees/worker-no-checkout/index"), + ) + add_test_run_git(worktree_dir, ["reset", "--hard"]) + @test.assert_eq( + @utf8.decode_lossy(fs.read_file(worktree_dir + "/init.t")[:]), + "main\n", + ) + + @sys.set_env_var("GIT_SHIM_CWD", repo_dir) + @sys.set_env_var("GIT_SHIM_PWD", repo_dir) + @sys.set_env_var("GIT_SHIM_CALLER_PWD", repo_dir) + worktree_wbtest_run_shim_git(["worktree", "remove", worktree_dir]) +} + ///| async fn worktree_wbtest_case_prune_keeps_recent_checkout() -> Unit raise Error { @bitnative.init_native_io() @@ -1341,6 +1617,10 @@ async test "worktree: add command path cases stay compatible" { worktree_wbtest_case_lock_reason() worktree_wbtest_case_detach_defaults_to_head() worktree_wbtest_case_explicit_head_creates_detached_worktree() + worktree_wbtest_case_preserves_executable_mode() + worktree_wbtest_case_detects_mode_only_change() + worktree_wbtest_case_symlink_stays_clean() + worktree_wbtest_case_no_checkout_leaves_files_absent() worktree_wbtest_case_prune_keeps_recent_checkout() worktree_wbtest_case_existing_branch_dwim() worktree_wbtest_case_force_allows_current_branch_dwim() diff --git a/modules/bit_lib/src/ignore_worktree.mbt b/modules/bit_lib/src/ignore_worktree.mbt index 417b5ec5..20fb8875 100644 --- a/modules/bit_lib/src/ignore_worktree.mbt +++ b/modules/bit_lib/src/ignore_worktree.mbt @@ -118,12 +118,12 @@ fn walk_dir_typed( let is_dir = if d_type == 4 { true } else if d_type == 10 { - // Symlink: check if it points to a directory via stat - let child_path = @bit.join_path(root, child_rel) - fs.is_dir(child_path) + // A symlink is a worktree entry even when its target is a directory. + false } else if d_type == 0 { // DT_UNKNOWN: fall back to stat let child_path = @bit.join_path(root, child_rel) + @bitio.read_symlink_target_path(child_path) is None && fs.is_dir(child_path) } else { false @@ -185,7 +185,8 @@ fn walk_dir_fallback( } let child_rel = if rel == "" { name } else { rel + "/" + name } let child_path = @bit.join_path(root, child_rel) - let is_dir = fs.is_dir(child_path) + let is_dir = @bitio.read_symlink_target_path(child_path) is None && + fs.is_dir(child_path) if matcher.is_ignored(child_rel, is_dir) { if is_dir && matcher.has_negation() { walk_dir(fs, root, child_rel, matcher, out) diff --git a/modules/bit_lib/src/worktree.mbt b/modules/bit_lib/src/worktree.mbt index bc4419b8..de420e38 100644 --- a/modules/bit_lib/src/worktree.mbt +++ b/modules/bit_lib/src/worktree.mbt @@ -856,6 +856,15 @@ fn read_core_filemode_setting( } } +///| +/// Return whether executable-bit changes are significant for this repository. +pub fn core_filemode_enabled( + fs : &@bit.RepoFileSystem, + git_dir : String, +) -> Bool { + read_core_filemode_setting(fs, git_dir) +} + ///| fn read_core_filemode_from_config( fs : &@bit.RepoFileSystem, diff --git a/modules/bit_lib/src/worktree_admin.mbt b/modules/bit_lib/src/worktree_admin.mbt index 8f303dde..0f2c0791 100644 --- a/modules/bit_lib/src/worktree_admin.mbt +++ b/modules/bit_lib/src/worktree_admin.mbt @@ -120,6 +120,7 @@ pub fn create_worktree( new_branch~ : String?, relative_paths? : Bool? = None, orphan? : Bool = false, + checkout_files? : Bool = true, ) -> Unit raise @bit.GitError { let git_dir = resolve_git_dir(rfs, root) let common_git_dir = resolve_commondir(rfs, git_dir) @@ -238,9 +239,9 @@ pub fn create_worktree( } // Checkout files to worktree match commit_id { - Some(id) => + Some(id) if checkout_files => checkout_to_worktree(fs, rfs, git_dir, abs_wt_path, admin_dir, id) - None => () + _ => () } } @@ -1802,13 +1803,17 @@ fn worktree_contains_nested_git( continue } let child_path = join_path(path, entry) + let is_symlink = @bitio.read_symlink_target_path(child_path) is Some(_) if entry == ".git" { - if !is_root && (fs.is_file(child_path) || fs.is_dir(child_path)) { + if !is_root && + !is_symlink && + (fs.is_file(child_path) || fs.is_dir(child_path)) { return true } continue } - if fs.is_dir(child_path) && + if !is_symlink && + fs.is_dir(child_path) && worktree_contains_nested_git(fs, child_path, is_root=false) { return true } @@ -1826,6 +1831,7 @@ fn worktree_has_local_changes( let tracked_paths : Array[String] = [] let gitlink_paths : Array[String] = [] let common_git_dir = resolve_commondir_from_admin(fs, admin_dir) + let core_filemode = core_filemode_enabled(fs, common_git_dir) let db = ObjectDb::load_lazy(fs, common_git_dir) for entry in index_entries { tracked_paths.push(entry.path) @@ -1834,10 +1840,25 @@ fn worktree_has_local_changes( continue } let file_path = join_path(worktree_path, entry.path) - if !fs.is_file(file_path) { - return true + let file_bytes = match @bitio.worktree_entry_meta_sync(fs, file_path) { + None => return true + Some(info) => { + let expected_kind = entry.mode & 0o170000 + let observed_kind = info.mode & 0o170000 + if expected_kind != observed_kind || + (core_filemode && info.mode != entry.mode) { + return true + } + match info.kind { + @bitio.WorktreeKindMeta::Regular => fs.read_file(file_path) + @bitio.WorktreeKindMeta::Symlink => + match @bitio.read_symlink_target_path(file_path) { + Some(target) => @utf8.encode(target) + None => return true + } + } + } } - let file_bytes = fs.read_file(file_path) match db.get(fs, entry.id) { Some(obj) if obj.obj_type == @bit.ObjectType::Blob => if file_bytes != obj.data { @@ -1880,7 +1901,8 @@ fn worktree_has_untracked_files( rel_path + "/" + entry } let child_abs = join_path(abs_path, entry) - if fs.is_dir(child_abs) { + let is_symlink = @bitio.read_symlink_target_path(child_abs) is Some(_) + if !is_symlink && fs.is_dir(child_abs) { if gitlink_paths.contains(child_rel) { if worktree_dir_has_entries(fs, child_abs) { return true @@ -1899,7 +1921,8 @@ fn worktree_has_untracked_files( } else { return true } - } else if fs.is_file(child_abs) && !tracked_paths.contains(child_rel) { + } else if (is_symlink || fs.is_file(child_abs)) && + !tracked_paths.contains(child_rel) { return true } } @@ -1952,7 +1975,9 @@ fn remove_dir_recursive( continue } let child_path = join_path(path, entry) - if rfs.is_dir(child_path) { + if @bitio.read_symlink_target_path(child_path) is Some(_) { + fs.remove_file(child_path) + } else if rfs.is_dir(child_path) { remove_dir_recursive(fs, rfs, child_path) } else { fs.remove_file(child_path) diff --git a/modules/bit_runtime/src/storage_runtime.mbt b/modules/bit_runtime/src/storage_runtime.mbt index c13b9866..e986ce13 100644 --- a/modules/bit_runtime/src/storage_runtime.mbt +++ b/modules/bit_runtime/src/storage_runtime.mbt @@ -774,6 +774,11 @@ fn storage_is_tree_mode(mode : String) -> Bool { mode == "040000" || mode == "40000" } +///| +fn storage_file_mode_kind(mode : Int) -> Int { + mode & 0o170000 +} + ///| fn storage_resolve_common_git_dir( rfs : &@bit.RepoFileSystem, @@ -891,6 +896,8 @@ fn storage_collect_status_snapshot( root : String, ) -> StorageStatusSnapshot raise Error { let git_dir = storage_resolve_git_dir(rfs, root) + let common_git_dir = storage_resolve_common_git_dir(rfs, git_dir) + let core_filemode = @bitlib.core_filemode_enabled(rfs, common_git_dir) let entries = @bitlib.read_index_entries(rfs, git_dir) let index_map : Map[String, @bitlib.IndexEntry] = Map([]) for entry in entries { @@ -906,9 +913,41 @@ fn storage_collect_status_snapshot( match index_map.get(rel_path) { Some(index_entry) => { let abs_path = root + "/" + rel_path - let content = rfs.read_file(abs_path) catch { _ => continue } - let worktree_id = @bit.hash_blob(content) - if worktree_id != index_entry.id { + let mut changed = false + let content : Bytes? = match + @io.worktree_entry_meta_sync(rfs, abs_path) { + None => None + Some(info) => { + let kind_changed = storage_file_mode_kind(info.mode) != + storage_file_mode_kind(index_entry.mode) + if kind_changed || (core_filemode && info.mode != index_entry.mode) { + changed = true + } + match info.kind { + @io.WorktreeKindMeta::Regular => { + let bytes = rfs.read_file(abs_path) catch { + _ => { + changed = true + Bytes::new(0) + } + } + Some(bytes) + } + @io.WorktreeKindMeta::Symlink => + @io.read_symlink_target_path(abs_path).map(fn(target) { + @utf8.encode(target) + }) + } + } + } + match content { + Some(bytes) => + if @bit.hash_blob(bytes) != index_entry.id { + changed = true + } + None => changed = true + } + if changed { unstaged_modified.push(rel_path) } }