///| Git worktree administration (git worktree list/add/remove/prune)

///|
/// Information about a worktree.
pub(all) struct WorktreeInfo {
  /// Absolute path to the worktree directory
  path : String
  /// Current HEAD commit id
  head_id : @bit.ObjectId?
  /// Branch name if HEAD points to a branch, None if detached
  branch : String?
  /// True if the worktree is locked
  locked : Bool
  /// True if this is the main worktree (contains .git directory)
  is_main : Bool
  /// True if this entry represents a bare main repository
  is_bare : Bool
}

///|
/// List all worktrees in a repository.
/// Returns the main worktree first, followed by linked worktrees.
pub fn list_worktrees(
  fs : &@bit.RepoFileSystem,
  root : String,
) -> Array[WorktreeInfo] raise @bit.GitError {
  let result : Array[WorktreeInfo] = []
  let linked : Array[WorktreeInfo] = []
  let git_dir = resolve_git_dir(fs, root)
  let main_root = resolve_main_worktree_root(git_dir)
  let main_info = get_main_worktree_info(fs, main_root, git_dir)
  result.push(main_info)
  // Scan .git/worktrees/ for linked worktrees
  let worktrees_dir = join_path(git_dir, "worktrees")
  if !fs.is_dir(worktrees_dir) {
    return result
  }
  let entries = fs.readdir(worktrees_dir)
  for entry in entries {
    if entry == "." || entry == ".." {
      continue
    }
    let admin_dir = join_path(worktrees_dir, entry)
    if !fs.is_dir(admin_dir) {
      continue
    }
    match get_linked_worktree_info(fs, admin_dir) {
      Some(info) => linked.push(info)
      None => continue
    }
  }
  linked.sort_by(fn(a, b) { String::lexical_compare(a.path, b.path) })
  for info in linked {
    result.push(info)
  }
  result
}

///|
fn resolve_main_worktree_root(git_dir : String) -> String {
  if basename(git_dir) == ".git" {
    normalize_path(parent_dir(git_dir))
  } else {
    normalize_path(git_dir)
  }
}

///|
/// Check if a branch is checked out in any worktree.
/// Returns the worktree path if the branch is in use, None otherwise.
pub fn is_branch_checked_out(
  fs : &@bit.RepoFileSystem,
  root : String,
  branch : String,
) -> String? raise @bit.GitError {
  let worktrees = list_worktrees(fs, root)
  for wt in worktrees {
    match wt.branch {
      Some(b) => if b == branch { return Some(wt.path) }
      None => continue
    }
  }
  None
}

///|
/// Check if a branch is checked out in a worktree other than `root` (e.g.
/// after `git worktree add -f` lets two worktrees share one branch, and the
/// current worktree happens to be one of them). Returns the other
/// worktree's path if found, None otherwise.
pub fn is_branch_checked_out_elsewhere(
  fs : &@bit.RepoFileSystem,
  root : String,
  branch : String,
) -> String? raise @bit.GitError {
  let normalized_root = normalize_path(root)
  let worktrees = list_worktrees(fs, root)
  for wt in worktrees {
    match wt.branch {
      Some(b) =>
        if b == branch && normalize_path(wt.path) != normalized_root {
          return Some(wt.path)
        }
      None => continue
    }
  }
  None
}

///|
/// Create a new linked worktree.
pub fn create_worktree(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  worktree_path : String,
  commit_ish : String?,
  detach? : Bool = false,
  force? : Bool = false,
  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)
  // Resolve the absolute worktree path
  let abs_wt_path = if worktree_path.has_prefix("/") {
    worktree_path
  } else {
    join_path(root, worktree_path)
  }
  // Check worktree path doesn't already exist as a file
  if rfs.is_file(abs_wt_path) {
    raise @bit.GitError::InvalidObject(
      "'\{worktree_path}' already exists as a file",
    )
  }
  if rfs.is_dir(abs_wt_path) && worktree_dir_has_entries(rfs, abs_wt_path) {
    raise @bit.GitError::InvalidObject("'\{worktree_path}' already exists")
  }
  // Determine the worktree name (basename of path)
  let wt_name = resolve_worktree_admin_name(rfs, git_dir, abs_wt_path)
  // Check if admin directory already exists
  let worktrees_dir = join_path(common_git_dir, "worktrees")
  let admin_dir = join_path(worktrees_dir, wt_name)
  // Determine branch and commit
  let (branch, commit_id) = resolve_worktree_target(
    fs, rfs, root, git_dir, commit_ish, detach, new_branch, orphan,
  )
  // Check branch is not already checked out
  match branch {
    Some(b) => {
      if orphan &&
        resolve_ref(rfs, common_git_dir, "refs/heads/" + b) is Some(_) {
        raise @bit.GitError::InvalidObject(
          "a branch named '\{b}' already exists",
        )
      }
      if !force {
        match is_branch_checked_out(rfs, root, b) {
          Some(path) =>
            raise @bit.GitError::InvalidObject(
              "'\{b}' is already checked out at '\{path}'",
            )
          None => ()
        }
      }
    }
    None => ()
  }
  let use_relative_paths = relative_paths.unwrap_or(
    worktree_use_relative_paths_enabled(rfs, common_git_dir),
  )
  if use_relative_paths {
    worktree_enable_relative_paths(fs, rfs, common_git_dir)
  }
  // Create admin directory
  fs.mkdir_p(admin_dir)
  // Write gitdir file (points to worktree's .git file)
  let gitdir_path = join_path(admin_dir, "gitdir")
  let wt_git_file = join_path(abs_wt_path, ".git")
  if use_relative_paths {
    fs.write_string(
      gitdir_path,
      worktree_compute_relative_path(admin_dir, wt_git_file) + "\n",
    )
  } else {
    fs.write_string(gitdir_path, wt_git_file + "\n")
  }
  // Write HEAD file
  let head_path = join_path(admin_dir, "HEAD")
  match branch {
    Some(b) => fs.write_string(head_path, "ref: refs/heads/" + b + "\n")
    None =>
      match commit_id {
        Some(id) => fs.write_string(head_path, id.to_hex() + "\n")
        None =>
          raise @bit.GitError::InvalidObject(
            "No commit to create worktree from",
          )
      }
  }
  // Write commondir file
  let commondir_path = join_path(admin_dir, "commondir")
  fs.write_string(commondir_path, "../..\n")
  copy_worktree_config(fs, rfs, git_dir, admin_dir)
  // Create worktree directory
  fs.mkdir_p(abs_wt_path)
  // Create .git file in worktree
  if use_relative_paths {
    fs.write_string(
      wt_git_file,
      "gitdir: " + worktree_compute_relative_path(abs_wt_path, admin_dir) + "\n",
    )
  } else {
    fs.write_string(wt_git_file, "gitdir: " + admin_dir + "\n")
  }
  match commit_id {
    Some(id) => {
      let (should_log_head, _) = should_log_ref(rfs, admin_dir, "HEAD", false)
      if should_log_head {
        append_reflog(
          fs,
          rfs,
          admin_dir,
          "HEAD",
          @bit.ObjectId::zero(),
          id,
          "unknown",
          "unknown",
          0L,
          "+0000",
          "worktree add",
        )
      }
    }
    None => ()
  }
  // Checkout files to worktree
  match commit_id {
    Some(id) if checkout_files =>
      checkout_to_worktree(fs, rfs, git_dir, abs_wt_path, admin_dir, id)
    _ => ()
  }
}

///|
/// Remove a linked worktree.
pub fn remove_worktree(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  worktree_path : String,
  force? : Bool = false,
  force_level? : Int = 0,
) -> Unit raise @bit.GitError {
  let git_dir = resolve_git_dir(rfs, root)
  let effective_force_level = worktree_effective_force_level(force, force_level)
  // Resolve the absolute worktree path
  let abs_wt_path = if worktree_path.has_prefix("/") {
    worktree_path
  } else {
    normalize_path(join_path(root, worktree_path))
  }
  // Find the admin directory for this worktree
  let admin_dir = find_worktree_admin_dir(rfs, git_dir, abs_wt_path)
  guard admin_dir is Some(admin) else {
    raise @bit.GitError::InvalidObject(
      worktree_not_found_message(worktree_path),
    )
  }
  // Check if locked
  let locked_path = join_path(admin, "locked")
  if rfs.is_file(locked_path) && effective_force_level < 2 {
    raise @bit.GitError::InvalidObject(
      "'\{worktree_path}' is locked; use --force to remove anyway",
    )
  }
  // Remove worktree directory if it exists
  if rfs.is_dir(abs_wt_path) {
    if worktree_contains_nested_git(rfs, abs_wt_path) {
      raise @bit.GitError::InvalidObject(
        "'\{worktree_path}' contains initialized submodules",
      )
    }
    if effective_force_level == 0 &&
      worktree_has_local_changes(rfs, abs_wt_path, admin) {
      raise @bit.GitError::InvalidObject(
        "'\{worktree_path}' contains modified or untracked files; use --force to delete it",
      )
    }
    remove_dir_recursive(fs, rfs, abs_wt_path)
  }
  // Remove admin directory
  remove_dir_recursive(fs, rfs, admin)
  prune_cleanup_worktrees_dir(fs, rfs, join_path(git_dir, "worktrees"), false)
}

///|
/// Prune worktrees with invalid gitdir references.
/// Returns list of pruned admin directory names.
pub fn prune_worktrees(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  dry_run? : Bool = false,
) -> Array[String] raise @bit.GitError {
  let detailed = prune_worktrees_detailed(fs, rfs, root, dry_run~)
  let pruned : Array[String] = []
  for entry in detailed {
    pruned.push(entry.name)
  }
  pruned
}

///|
pub(all) struct PrunedWorktreeEntry {
  name : String
  reason : String
}

///|
pub fn prune_worktrees_detailed(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  dry_run? : Bool = false,
  expire_before_sec? : Int64? = None,
) -> Array[PrunedWorktreeEntry] raise @bit.GitError {
  let git_dir = resolve_git_dir(rfs, root)
  let worktrees_dir = join_path(git_dir, "worktrees")
  let pruned : Array[PrunedWorktreeEntry] = []
  if !rfs.is_dir(worktrees_dir) {
    return pruned
  }
  let seen_worktree_roots : Map[String, Bool] = Map([])
  seen_worktree_roots[resolve_main_worktree_root(git_dir)] = true
  let entries = rfs.readdir(worktrees_dir)
  entries.sort_by(fn(a, b) { String::lexical_compare(a, b) })
  for entry in entries {
    if entry == "." || entry == ".." {
      continue
    }
    let admin_dir = join_path(worktrees_dir, entry)
    if !rfs.is_dir(admin_dir) {
      prune_record(
        fs, rfs, worktrees_dir, admin_dir, entry, "not a valid directory", pruned,
        dry_run,
      )
      continue
    }
    if rfs.is_file(join_path(admin_dir, "locked")) {
      continue
    }
    if prune_admin_dir_is_recent(admin_dir, expire_before_sec~) {
      continue
    }
    let gitdir_path = join_path(admin_dir, "gitdir")
    if !rfs.is_file(gitdir_path) {
      prune_record(
        fs, rfs, worktrees_dir, admin_dir, entry, "gitdir file does not exist", pruned,
        dry_run,
      )
      continue
    }
    let gitdir_content = read_trimmed_file(rfs, gitdir_path) catch {
      _ => {
        prune_record(
          fs, rfs, worktrees_dir, admin_dir, entry, "unable to read gitdir file",
          pruned, dry_run,
        )
        continue
      }
    }
    if gitdir_content.length() == 0 {
      prune_record(
        fs, rfs, worktrees_dir, admin_dir, entry, "invalid gitdir file", pruned,
        dry_run,
      )
      continue
    }
    let resolved_gitdir = prune_resolve_gitdir_path(admin_dir, gitdir_content)
    if resolved_gitdir.length() == 0 {
      prune_record(
        fs, rfs, worktrees_dir, admin_dir, entry, "invalid gitdir file", pruned,
        dry_run,
      )
      continue
    }
    if !rfs.is_file(resolved_gitdir) && !rfs.is_dir(resolved_gitdir) {
      prune_record(
        fs, rfs, worktrees_dir, admin_dir, entry, "gitdir file points to non-existent location",
        pruned, dry_run,
      )
      continue
    }
    let wt_root = normalize_path(parent_dir(resolved_gitdir))
    if seen_worktree_roots.contains(wt_root) {
      prune_record(
        fs, rfs, worktrees_dir, admin_dir, entry, "duplicate entry", pruned, dry_run,
      )
      continue
    }
    seen_worktree_roots[wt_root] = true
  }
  prune_cleanup_worktrees_dir(fs, rfs, worktrees_dir, dry_run)
  pruned
}

///|
fn prune_record(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  worktrees_dir : String,
  admin_path : String,
  entry : String,
  reason : String,
  pruned : Array[PrunedWorktreeEntry],
  dry_run : Bool,
) -> Unit raise @bit.GitError {
  if !dry_run {
    if rfs.is_dir(admin_path) {
      remove_dir_recursive(fs, rfs, admin_path)
    } else if rfs.is_file(admin_path) {
      fs.remove_file(admin_path)
    }
  }
  ignore(worktrees_dir)
  pruned.push({ name: entry, reason })
}

///|
fn prune_cleanup_worktrees_dir(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  worktrees_dir : String,
  dry_run : Bool,
) -> Unit raise @bit.GitError {
  if dry_run || !rfs.is_dir(worktrees_dir) {
    return
  }
  let mut has_entries = false
  for entry in rfs.readdir(worktrees_dir) {
    if entry != "." && entry != ".." {
      has_entries = true
      break
    }
  }
  if !has_entries {
    fs.remove_dir(worktrees_dir) catch {
      _ => ()
    }
  }
}

///|
fn prune_resolve_gitdir_path(
  admin_dir : String,
  gitdir_content : String,
) -> String {
  if gitdir_content.length() == 0 {
    return ""
  }
  if gitdir_content.has_prefix("/") {
    normalize_path(gitdir_content)
  } else {
    normalize_path(join_path(admin_dir, gitdir_content))
  }
}

///|
fn prune_admin_dir_is_recent(
  admin_dir : String,
  expire_before_sec? : Int64? = None,
) -> Bool {
  match expire_before_sec {
    Some(limit) => {
      let candidates = [
        admin_dir,
        join_path(admin_dir, "gitdir"),
        join_path(admin_dir, "HEAD"),
        join_path(admin_dir, "commondir"),
        join_path(admin_dir, "locked"),
      ]
      for path in candidates {
        match @bitio.lstat_entry_meta(path) {
          Some(meta) if meta.mtime_sec.unwrap_or(0).to_int64() > limit =>
            return true
          _ => ()
        }
      }
      false
    }
    None => false
  }
}

///|
/// Lock a worktree to prevent it from being pruned.
pub fn lock_worktree(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  worktree_path : String,
  reason~ : String?,
) -> Unit raise @bit.GitError {
  let git_dir = resolve_git_dir(rfs, root)
  // Resolve absolute path
  let abs_wt_path = if worktree_path.has_prefix("/") {
    worktree_path
  } else {
    normalize_path(join_path(root, worktree_path))
  }
  // Find admin directory
  let admin_dir = find_worktree_admin_dir(rfs, git_dir, abs_wt_path)
  guard admin_dir is Some(admin) else {
    raise @bit.GitError::InvalidObject(
      worktree_not_found_message(worktree_path),
    )
  }
  // Check if already locked
  let locked_path = join_path(admin, "locked")
  if rfs.is_file(locked_path) {
    raise @bit.GitError::InvalidObject("'\{worktree_path}' is already locked")
  }
  // Write lock file
  let content = reason.map(value => value + "\n").unwrap_or("")
  fs.write_string(locked_path, content)
}

///|
/// Unlock a worktree.
pub fn unlock_worktree(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  worktree_path : String,
) -> Unit raise @bit.GitError {
  let git_dir = resolve_git_dir(rfs, root)
  // Resolve absolute path
  let abs_wt_path = if worktree_path.has_prefix("/") {
    worktree_path
  } else {
    normalize_path(join_path(root, worktree_path))
  }
  // Find admin directory
  let admin_dir = find_worktree_admin_dir(rfs, git_dir, abs_wt_path)
  guard admin_dir is Some(admin) else {
    raise @bit.GitError::InvalidObject(
      worktree_not_found_message(worktree_path),
    )
  }
  // Check if locked
  let locked_path = join_path(admin, "locked")
  if !rfs.is_file(locked_path) {
    raise @bit.GitError::InvalidObject("'\{worktree_path}' is not locked")
  }
  // Remove lock file
  fs.remove_file(locked_path)
}

///|
/// Get lock reason for a worktree.
pub fn get_worktree_lock_reason(
  rfs : &@bit.RepoFileSystem,
  root : String,
  worktree_path : String,
) -> String? raise @bit.GitError {
  let git_dir = resolve_git_dir(rfs, root)
  let abs_wt_path = if worktree_path.has_prefix("/") {
    worktree_path
  } else {
    normalize_path(join_path(root, worktree_path))
  }
  let admin_dir = find_worktree_admin_dir(rfs, git_dir, abs_wt_path)
  guard admin_dir is Some(admin) else { return None }
  let locked_path = join_path(admin, "locked")
  if !rfs.is_file(locked_path) {
    return None
  }
  let content = read_trimmed_file(rfs, locked_path)
  if content.length() == 0 {
    Some("")
  } else {
    Some(content)
  }
}

///|
/// Move a worktree to a new location.
pub fn move_worktree(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  source_path : String,
  dest_path : String,
  force? : Bool = false,
  force_level? : Int = 0,
  relative_paths? : Bool? = None,
) -> Unit raise @bit.GitError {
  let git_dir = resolve_git_dir(rfs, root)
  let effective_force_level = worktree_effective_force_level(force, force_level)
  // Resolve absolute paths
  let abs_src = if source_path.has_prefix("/") {
    source_path
  } else {
    normalize_path(join_path(root, source_path))
  }
  let requested_abs_dst = if dest_path.has_prefix("/") {
    dest_path
  } else {
    normalize_path(join_path(root, dest_path))
  }
  let abs_dst = if rfs.is_dir(requested_abs_dst) {
    normalize_path(join_path(requested_abs_dst, basename(abs_src)))
  } else {
    requested_abs_dst
  }
  // Find admin directory
  let admin_dir = find_worktree_admin_dir(rfs, git_dir, abs_src)
  guard admin_dir is Some(admin) else {
    raise @bit.GitError::InvalidObject(worktree_not_found_message(source_path))
  }
  // Check if locked (unless force)
  let locked_path = join_path(admin, "locked")
  if rfs.is_file(locked_path) && effective_force_level < 2 {
    raise @bit.GitError::InvalidObject(
      "'\{source_path}' is locked; use --force to move anyway",
    )
  }
  if worktree_contains_nested_git(rfs, abs_src) {
    raise @bit.GitError::InvalidObject(
      "'\{source_path}' contains initialized submodules",
    )
  }
  match find_worktree_admin_dir(rfs, git_dir, abs_dst) {
    Some(existing_admin) => {
      let dest_locked = rfs.is_file(join_path(existing_admin, "locked"))
      let required_force = if dest_locked { 2 } else { 1 }
      if effective_force_level < required_force {
        if dest_locked {
          raise @bit.GitError::InvalidObject(
            "'\{dest_path}' is locked; use --force to move anyway",
          )
        }
        raise @bit.GitError::InvalidObject("'\{dest_path}' already exists")
      }
      remove_dir_recursive(fs, rfs, existing_admin)
      prune_cleanup_worktrees_dir(
        fs,
        rfs,
        join_path(git_dir, "worktrees"),
        false,
      )
    }
    None => ()
  }
  // Check destination doesn't exist
  if rfs.is_dir(abs_dst) || rfs.is_file(abs_dst) {
    raise @bit.GitError::InvalidObject("'\{dest_path}' already exists")
  }
  // Update gitdir file to point to new location
  let gitdir_path = join_path(admin, "gitdir")
  let new_git_file = join_path(abs_dst, ".git")
  let use_relative_paths = relative_paths.unwrap_or(
    worktree_use_relative_paths_enabled(rfs, git_dir),
  )
  if use_relative_paths {
    fs.write_string(
      gitdir_path,
      worktree_compute_relative_path(admin, new_git_file) + "\n",
    )
  } else {
    fs.write_string(gitdir_path, new_git_file + "\n")
  }
  // Move the worktree directory (by copying and removing)
  copy_dir_recursive_wt(fs, rfs, abs_src, abs_dst)
  remove_dir_recursive(fs, rfs, abs_src)
  // Update .git file in new location
  if use_relative_paths {
    fs.write_string(
      new_git_file,
      "gitdir: " + worktree_compute_relative_path(abs_dst, admin) + "\n",
    )
  } else {
    fs.write_string(new_git_file, "gitdir: " + admin + "\n")
  }
}

///|
fn copy_dir_recursive_wt(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  src : String,
  dst : String,
) -> Unit raise @bit.GitError {
  fs.mkdir_p(dst)
  let entries = rfs.readdir(src)
  for entry in entries {
    if entry == "." || entry == ".." {
      continue
    }
    let src_path = join_path(src, entry)
    let dst_path = join_path(dst, entry)
    if rfs.is_dir(src_path) {
      copy_dir_recursive_wt(fs, rfs, src_path, dst_path)
    } else if rfs.is_file(src_path) {
      let content = rfs.read_file(src_path)
      fs.write_file(dst_path, content)
    }
  }
}

///|
pub(all) struct WorktreeRepairResult {
  repaired : Array[String]
  warnings : Array[String]
} derive(Eq, Debug)

///|
fn worktree_repair_resolve_requested_path(
  root : String,
  path : String,
) -> String {
  if path.has_prefix("/") {
    normalize_path(path)
  } else {
    normalize_path(join_path(root, path))
  }
}

///|
fn worktree_repair_gitfile_target(
  fs : &@bit.RepoFileSystem,
  worktree_path : String,
) -> String? raise @bit.GitError {
  let git_file = join_path(worktree_path, ".git")
  if !fs.is_file(git_file) {
    return None
  }
  let content = read_trimmed_file(fs, git_file)
  if !content.has_prefix("gitdir: ") {
    return None
  }
  let raw = String::unsafe_substring(content, start=8, end=content.length())
  Some(
    if raw.has_prefix("/") {
      normalize_path(raw)
    } else {
      normalize_path(join_path(worktree_path, raw))
    },
  )
}

///|
fn worktree_repair_target_is_repository(
  fs : &@bit.RepoFileSystem,
  target : String,
) -> Bool {
  if !fs.is_dir(target) {
    return false
  }
  if !fs.is_file(join_path(target, "HEAD")) {
    return false
  }
  let common = resolve_commondir(fs, target) catch { _ => target }
  fs.is_dir(join_path(common, "objects"))
}

///|
fn worktree_repair_target_exists(
  fs : &@bit.RepoFileSystem,
  target : String,
) -> Bool {
  fs.is_dir(target) || fs.is_file(target)
}

///|
fn worktree_repair_find_admin_dir(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  worktree_path : String,
) -> String? raise @bit.GitError {
  match find_worktree_admin_dir(fs, git_dir, worktree_path) {
    Some(admin_dir) => Some(admin_dir)
    None =>
      match worktree_repair_gitfile_target(fs, worktree_path) {
        Some(target) => {
          let candidate = join_path(
            join_path(git_dir, "worktrees"),
            basename(target),
          )
          if fs.is_dir(candidate) {
            Some(candidate)
          } else {
            None
          }
        }
        None => None
      }
  }
}

///|
fn worktree_repair_collect_paths(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  root : String,
  worktree_paths : Array[String],
) -> Array[String] raise @bit.GitError {
  if worktree_paths.length() > 0 {
    return worktree_paths.map(path => {
      worktree_repair_resolve_requested_path(root, path)
    })
  }
  let worktrees_dir = join_path(git_dir, "worktrees")
  let paths : Array[String] = []
  if fs.is_dir(worktrees_dir) {
    let entries = fs.readdir(worktrees_dir)
    for entry in entries {
      if entry == "." || entry == ".." {
        continue
      }
      let admin_dir = join_path(worktrees_dir, entry)
      let gitdir_path = join_path(admin_dir, "gitdir")
      if !fs.is_file(gitdir_path) {
        continue
      }
      let content = read_trimmed_file(fs, gitdir_path)
      paths.push(worktree_admin_worktree_path(admin_dir, content))
    }
  }
  let current_git_file = join_path(root, ".git")
  if fs.is_dir(root) && fs.is_file(current_git_file) && !paths.contains(root) {
    paths.push(root)
  }
  paths
}

///|
fn worktree_repair_relative_paths_enabled(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
) -> Bool {
  let common_git_dir = resolve_commondir(fs, git_dir) catch { _ => git_dir }
  let config_path = join_path(common_git_dir, "config")
  read_config_bool(fs, config_path, "worktree", "useRelativePaths").unwrap_or(
    false,
  )
}

///|
fn worktree_repair_expected_gitdir_content(
  admin_dir : String,
  worktree_path : String,
  use_relative_paths : Bool,
) -> String {
  let git_file = join_path(worktree_path, ".git")
  if use_relative_paths {
    worktree_compute_relative_path(admin_dir, git_file) + "\n"
  } else {
    git_file + "\n"
  }
}

///|
fn worktree_repair_expected_gitfile_content(
  admin_dir : String,
  worktree_path : String,
  use_relative_paths : Bool,
) -> String {
  if use_relative_paths {
    "gitdir: " + worktree_compute_relative_path(worktree_path, admin_dir) + "\n"
  } else {
    "gitdir: " + admin_dir + "\n"
  }
}

///|
fn worktree_repair_read_file(
  fs : &@bit.RepoFileSystem,
  path : String,
) -> String raise @bit.GitError {
  @utf8.decode_lossy(fs.read_file(path)[:])
}

///|
pub fn repair_worktree_detailed(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  worktree_paths? : Array[String] = [],
  relative_paths? : Bool? = None,
) -> WorktreeRepairResult raise @bit.GitError {
  let git_dir = resolve_git_dir(rfs, root)
  let repaired : Array[String] = []
  let warnings : Array[String] = []
  let worktrees_dir = join_path(git_dir, "worktrees")
  let explicit_paths = worktree_paths.length() > 0
  if !rfs.is_dir(worktrees_dir) && !explicit_paths {
    return { repaired, warnings }
  }
  let paths_to_check = worktree_repair_collect_paths(
    rfs, git_dir, root, worktree_paths,
  )
  let use_relative_paths = relative_paths.unwrap_or(
    worktree_repair_relative_paths_enabled(rfs, git_dir),
  )
  for wt_path in paths_to_check {
    let wt_exists = rfs.is_dir(wt_path)
    let wt_is_file = rfs.is_file(wt_path)
    let wt_git_file = join_path(wt_path, ".git")
    let admin_dir = worktree_repair_find_admin_dir(rfs, git_dir, wt_path)
    if explicit_paths {
      if wt_is_file {
        raise @bit.GitError::InvalidObject("'\{wt_path}' is not a directory")
      }
      if !wt_exists {
        raise @bit.GitError::InvalidObject("'\{wt_path}' is not a valid path")
      }
      if rfs.is_dir(wt_git_file) {
        raise @bit.GitError::InvalidObject(".git is not a file")
      }
      guard admin_dir is Some(admin) else {
        match worktree_repair_gitfile_target(rfs, wt_path) {
          Some(target) =>
            if worktree_repair_target_exists(rfs, target) {
              raise @bit.GitError::InvalidObject(
                ".git file does not reference a repository",
              )
            } else {
              raise @bit.GitError::InvalidObject(".git file broken")
            }
          None =>
            if rfs.is_file(wt_git_file) {
              raise @bit.GitError::InvalidObject(".git file broken")
            } else {
              raise @bit.GitError::InvalidObject(
                "'\{wt_path}' is not a valid path",
              )
            }
        }
      }
      match worktree_repair_gitfile_target(rfs, wt_path) {
        Some(target) =>
          if target != admin &&
            worktree_repair_target_exists(rfs, target) &&
            !worktree_repair_target_is_repository(rfs, target) {
            raise @bit.GitError::InvalidObject(
              ".git file does not reference a repository",
            )
          }
        None => ()
      }
      let expected_gitdir = worktree_repair_expected_gitdir_content(
        admin, wt_path, use_relative_paths,
      )
      let gitdir_path = join_path(admin, "gitdir")
      if !rfs.is_file(gitdir_path) {
        fs.write_string(gitdir_path, expected_gitdir)
        warnings.push("gitdir unreadable: \{gitdir_path}")
        repaired.push("gitdir: \{wt_path}")
      } else {
        let current = worktree_repair_read_file(rfs, gitdir_path)
        if current != expected_gitdir {
          fs.write_string(gitdir_path, expected_gitdir)
          warnings.push("gitdir incorrect: \{gitdir_path}")
          repaired.push("gitdir: \{wt_path}")
        }
      }
      let expected_gitfile = worktree_repair_expected_gitfile_content(
        admin, wt_path, use_relative_paths,
      )
      if !rfs.is_file(wt_git_file) {
        fs.write_string(wt_git_file, expected_gitfile)
        warnings.push(".git file broken: \{wt_path}")
        repaired.push(".git: \{wt_path}")
      } else {
        let current = worktree_repair_read_file(rfs, wt_git_file)
        let status = match worktree_repair_gitfile_target(rfs, wt_path) {
          Some(target) =>
            if target != admin {
              if worktree_repair_target_is_repository(rfs, target) {
                Some("incorrect")
              } else {
                Some("broken")
              }
            } else if current != expected_gitfile {
              Some("incorrect")
            } else {
              None
            }
          None => Some("broken")
        }
        match status {
          Some(kind) => {
            fs.write_string(wt_git_file, expected_gitfile)
            warnings.push(".git file \{kind}: \{wt_path}")
            repaired.push(".git: \{wt_path}")
          }
          None => ()
        }
      }
      continue
    }
    if wt_is_file {
      raise @bit.GitError::InvalidObject("'\{wt_path}' is not a directory")
    }
    if !wt_exists {
      continue
    }
    if rfs.is_dir(wt_git_file) {
      raise @bit.GitError::InvalidObject(".git is not a file")
    }
    guard admin_dir is Some(admin) else { continue }
    let expected_gitdir = worktree_repair_expected_gitdir_content(
      admin, wt_path, use_relative_paths,
    )
    let gitdir_path = join_path(admin, "gitdir")
    if !rfs.is_file(gitdir_path) {
      fs.write_string(gitdir_path, expected_gitdir)
      warnings.push("gitdir unreadable: \{gitdir_path}")
      repaired.push("gitdir: \{wt_path}")
    } else {
      let current = worktree_repair_read_file(rfs, gitdir_path)
      if current != expected_gitdir {
        fs.write_string(gitdir_path, expected_gitdir)
        warnings.push("gitdir incorrect: \{gitdir_path}")
        repaired.push("gitdir: \{wt_path}")
      }
    }
    let expected_gitfile = worktree_repair_expected_gitfile_content(
      admin, wt_path, use_relative_paths,
    )
    if !rfs.is_file(wt_git_file) {
      fs.write_string(wt_git_file, expected_gitfile)
      warnings.push(".git file broken: \{wt_path}")
      repaired.push(".git: \{wt_path}")
    } else {
      let current = worktree_repair_read_file(rfs, wt_git_file)
      let status = match worktree_repair_gitfile_target(rfs, wt_path) {
        Some(target) =>
          if target != admin {
            if worktree_repair_target_is_repository(rfs, target) {
              Some("incorrect")
            } else {
              Some("broken")
            }
          } else if current != expected_gitfile {
            Some("incorrect")
          } else {
            None
          }
        None => Some("broken")
      }
      match status {
        Some(kind) => {
          fs.write_string(wt_git_file, expected_gitfile)
          warnings.push(".git file \{kind}: \{wt_path}")
          repaired.push(".git: \{wt_path}")
        }
        None => ()
      }
    }
  }
  { repaired, warnings }
}

///|
/// Repair worktree administrative files.
pub fn repair_worktree(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  worktree_paths? : Array[String] = [],
) -> Array[String] raise @bit.GitError {
  repair_worktree_detailed(fs, rfs, root, worktree_paths~).repaired
}

///|
/// Check if a worktree is prunable and return the reason.
pub fn get_worktree_prunable_reason(
  rfs : &@bit.RepoFileSystem,
  root : String,
  worktree_path : String,
) -> String? raise @bit.GitError {
  let git_dir = resolve_git_dir(rfs, root)
  let abs_wt_path = if worktree_path.has_prefix("/") {
    worktree_path
  } else {
    normalize_path(join_path(root, worktree_path))
  }
  let admin_dir = find_worktree_admin_dir(rfs, git_dir, abs_wt_path)
  guard admin_dir is Some(admin) else { return None }
  // Check if locked - locked worktrees are not prunable
  let locked_path = join_path(admin, "locked")
  if rfs.is_file(locked_path) {
    return None
  }
  // Check if gitdir file points to non-existent location
  let gitdir_path = join_path(admin, "gitdir")
  if !rfs.is_file(gitdir_path) {
    return Some("gitdir file is missing")
  }
  let gitdir_content = read_trimmed_file(rfs, gitdir_path)
  let wt_git_dir = parent_dir(gitdir_content)
  if !rfs.is_dir(wt_git_dir) {
    return Some("gitdir file points to non-existent location")
  }
  None
}

///|
/// Resolve the actual git directory, handling .git files (for worktrees).
fn resolve_git_dir(
  fs : &@bit.RepoFileSystem,
  root : String,
) -> String raise @bit.GitError {
  if fs.is_file(join_path(root, "HEAD")) &&
    fs.is_dir(join_path(root, "objects")) {
    return root
  }
  let bit_path = join_path(root, ".git")
  if fs.is_dir(bit_path) {
    return bit_path
  }
  if fs.is_file(bit_path) {
    // .git is a file pointing to the real git directory
    let content = read_trimmed_file(fs, bit_path)
    if content.has_prefix("gitdir: ") {
      let target = String::unsafe_substring(
        content,
        start=8,
        end=content.length(),
      )
      // Resolve relative paths
      if target.has_prefix("/") {
        return resolve_commondir(fs, target)
      } else {
        let abs = join_path(root, target)
        return resolve_commondir(fs, abs)
      }
    }
  }
  raise @bit.GitError::InvalidObject("Not a git repository: \{root}")
}

///|
/// Resolve commondir to get the main git directory.
fn resolve_commondir(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
) -> String raise @bit.GitError {
  let commondir_path = join_path(git_dir, "commondir")
  if fs.is_file(commondir_path) {
    let rel = read_trimmed_file(fs, commondir_path)
    if rel.has_prefix("/") {
      return rel
    } else {
      return normalize_path(join_path(git_dir, rel))
    }
  }
  git_dir
}

///|
fn copy_worktree_config(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  source_git_dir : String,
  dest_git_dir : String,
) -> Unit raise @bit.GitError {
  let common_git_dir = resolve_commondir(rfs, source_git_dir)
  let config_path = join_path(common_git_dir, "config")
  if !read_config_bool(rfs, config_path, "extensions", "worktreeconfig").unwrap_or(
      false,
    ) {
    return
  }
  let source_path = join_path(source_git_dir, "config.worktree")
  if !rfs.is_file(source_path) {
    return
  }
  let content = decode_bytes_lossy(rfs.read_file(source_path))
  let filtered = filter_worktree_config_content(content)
  if filtered.length() == 0 {
    return
  }
  fs.write_string(join_path(dest_git_dir, "config.worktree"), filtered)
}

///|
fn filter_worktree_config_content(content : String) -> String {
  let lines : Array[String] = []
  let mut current_section = ""
  for line_view in content.split("\n") {
    let line = line_view.to_owned()
    let trimmed = worktree_trim_whitespace(line)
    if trimmed.has_prefix("[") && trimmed.has_suffix("]") {
      let header = String::unsafe_substring(
        trimmed,
        start=1,
        end=trimmed.length() - 1,
      )
      let section_name = match header.find(" ") {
        Some(idx) => String::unsafe_substring(header, start=0, end=idx)
        None => header
      }
      current_section = section_name.to_lower()
      lines.push(line)
      continue
    }
    if current_section == "core" {
      match filtered_worktree_key_name(trimmed) {
        Some("bare") => continue
        Some("worktree") => continue
        _ => ()
      }
    }
    lines.push(line)
  }
  if lines.length() == 0 {
    ""
  } else {
    lines.join("\n") + "\n"
  }
}

///|
fn filtered_worktree_key_name(line : String) -> String? {
  if line.length() == 0 || line.has_prefix("#") || line.has_prefix(";") {
    return None
  }
  match line.find("=") {
    Some(idx) =>
      Some(
        worktree_trim_whitespace(
          String::unsafe_substring(line, start=0, end=idx),
        ).to_lower(),
      )
    None => None
  }
}

///|
fn get_main_worktree_info(
  fs : &@bit.RepoFileSystem,
  root : String,
  git_dir : String,
) -> WorktreeInfo raise @bit.GitError {
  let is_bare = normalize_path(root) == normalize_path(git_dir) &&
    basename(git_dir) != ".git" &&
    fs.is_file(join_path(git_dir, "HEAD")) &&
    fs.is_dir(join_path(git_dir, "objects"))
  if is_bare {
    return {
      path: normalize_path(root),
      head_id: None,
      branch: None,
      locked: false,
      is_main: true,
      is_bare: true,
    }
  }
  let head_ref = read_head_ref(fs, git_dir)
  let (head_id, branch) = match head_ref {
    Branch(name) =>
      match resolve_ref(fs, git_dir, "refs/heads/" + name) {
        Some(id) => (Some(id), Some(name))
        None => (None, None)
      }
    Detached(id) => (Some(id), None)
  }
  {
    path: normalize_path(root),
    head_id,
    branch,
    locked: false,
    is_main: true,
    is_bare: false,
  }
}

///|
fn get_linked_worktree_info(
  fs : &@bit.RepoFileSystem,
  admin_dir : String,
) -> WorktreeInfo? raise @bit.GitError {
  // Read gitdir to find worktree path
  let gitdir_path = join_path(admin_dir, "gitdir")
  if !fs.is_file(gitdir_path) {
    return None
  }
  let gitdir_content = read_trimmed_file(fs, gitdir_path)
  // gitdir points to worktree/.git file, get parent for worktree path
  let wt_path = worktree_admin_worktree_path(admin_dir, gitdir_content)
  // Read HEAD
  let head_path = join_path(admin_dir, "HEAD")
  let head_content = match read_worktree_head_content(fs, head_path) {
    Some(content) => content
    None => return None
  }
  let (head_id, branch) = if head_content.has_prefix("ref: ") {
    let refname = String::unsafe_substring(
      head_content,
      start=5,
      end=head_content.length(),
    )
    let branch_name = if refname.has_prefix("refs/heads/") {
      String::unsafe_substring(refname, start=11, end=refname.length())
    } else {
      refname
    }
    // Resolve ref from main git directory
    let common_dir = resolve_commondir_from_admin(fs, admin_dir)
    match resolve_ref(fs, common_dir, refname) {
      Some(id) => (Some(id), Some(branch_name))
      None => (None, None)
    }
  } else {
    // Detached HEAD
    let id = @bit.ObjectId::from_hex(head_content)
    (Some(id), None)
  }
  // Check if locked
  let locked = fs.is_file(join_path(admin_dir, "locked"))
  Some({
    path: wt_path,
    head_id,
    branch,
    locked,
    is_main: false,
    is_bare: false,
  })
}

///|
fn read_worktree_head_content(
  fs : &@bit.RepoFileSystem,
  head_path : String,
) -> String? raise @bit.GitError {
  if fs.is_file(head_path) {
    return Some(read_trimmed_file(fs, head_path))
  }
  match @bitio.read_symlink_target_path(head_path) {
    Some(target) =>
      if target.has_prefix("refs/") {
        Some("ref: " + target)
      } else {
        None
      }
    None => None
  }
}

///|
fn worktree_dir_has_entries(
  fs : &@bit.RepoFileSystem,
  path : String,
) -> Bool raise @bit.GitError {
  if !fs.is_dir(path) {
    return false
  }
  for entry in fs.readdir(path) {
    if entry != "." && entry != ".." {
      return true
    }
  }
  false
}

///|
fn resolve_worktree_admin_name(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  abs_wt_path : String,
) -> String {
  let worktrees_dir = join_path(git_dir, "worktrees")
  let base = worktree_sanitize_admin_component(basename(abs_wt_path))
  let mut index = 0
  let mut candidate = base
  while fs.is_dir(join_path(worktrees_dir, candidate)) {
    index += 1
    candidate = base + index.to_string()
  }
  candidate
}

///|
fn worktree_admin_is_forbidden_ref_char(ch : Char) -> Bool {
  ch.is_control() ||
  ch == ':' ||
  ch == '?' ||
  ch == '[' ||
  ch == '\\' ||
  ch == '^' ||
  ch == '~' ||
  ch == ' ' ||
  ch == '\t' ||
  ch == '*'
}

///|
fn worktree_strip_lock_suffixes(name : String) -> String {
  let mut current = name
  let mut done = false
  while !done {
    match current.strip_suffix(".lock") {
      Some(stripped) => current = stripped.to_owned()
      None => done = true
    }
  }
  current
}

///|
fn worktree_sanitize_admin_component(name : String) -> String {
  if name == "" {
    return name
  }
  let out : Array[Char] = []
  let mut last = Default::default()
  for ch in name.to_array() {
    if ch == '.' && last == '.' {
      last = ch
      continue
    }
    let out_ch = if ch == '{' && last == '@' {
      '-'
    } else if worktree_admin_is_forbidden_ref_char(ch) {
      '-'
    } else {
      ch
    }
    out.push(out_ch)
    last = ch
  }
  if out.length() == 0 {
    return name
  }
  if name.has_prefix(".") {
    out[0] = '-'
  }
  let sanitized = worktree_strip_lock_suffixes(String::from_array(out[:]))
  match sanitized.strip_suffix(".") {
    Some(stripped) => stripped.to_owned()
    None => sanitized
  }
}

///|
fn resolve_commondir_from_admin(
  fs : &@bit.RepoFileSystem,
  admin_dir : String,
) -> String raise @bit.GitError {
  let commondir_path = join_path(admin_dir, "commondir")
  if fs.is_file(commondir_path) {
    let rel = read_trimmed_file(fs, commondir_path)
    if rel.has_prefix("/") {
      return rel
    } else {
      return normalize_path(join_path(admin_dir, rel))
    }
  }
  // Default: ../.. from admin_dir
  normalize_path(join_path(admin_dir, "../.."))
}

///|
fn resolve_worktree_target(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  git_dir : String,
  commit_ish : String?,
  detach : Bool,
  new_branch : String?,
  orphan : Bool,
) -> (String?, @bit.ObjectId?) raise @bit.GitError {
  let resolved_commit_ish = match commit_ish {
    Some("-") =>
      match load_previous_checkout_location(rfs, git_dir) {
        Some(prev) => Some(prev)
        None =>
          raise @bit.GitError::InvalidObject(
            "No previous checkout target available",
          )
      }
    _ => commit_ish
  }
  // If new branch requested, create it
  match new_branch {
    Some(branch_name) => {
      if orphan {
        return (Some(branch_name), None)
      }
      let target = match resolved_commit_ish {
        Some(spec) => rev_parse(rfs, git_dir, spec)
        None =>
          match rev_parse(rfs, git_dir, "HEAD") {
            Some(id) => Some(id)
            None => resolve_head_commit(rfs, git_dir)
          }
      }
      guard target is Some(id) else {
        raise @bit.GitError::InvalidObject(
          "Cannot resolve commit for new branch",
        )
      }
      create_branch_at(
        fs,
        rfs,
        root,
        branch_name,
        id,
        start_point=resolved_commit_ish,
      )
      return (Some(branch_name), Some(id))
    }
    None => ()
  }
  match resolved_commit_ish {
    None => {
      // Use current HEAD
      let head = read_head_ref(rfs, git_dir)
      match head {
        Branch(name) => {
          let id = resolve_ref(rfs, git_dir, "refs/heads/" + name)
          if detach {
            (None, id)
          } else {
            (Some(name), id)
          }
        }
        Detached(id) => (None, Some(id))
      }
    }
    Some(spec) => {
      if spec == "HEAD" || spec == "@" {
        return match read_head_ref(rfs, git_dir) {
          Branch(name) =>
            (None, resolve_ref(rfs, git_dir, "refs/heads/" + name))
          Detached(id) => (None, Some(id))
        }
      }
      // Try to resolve as branch first
      if !detach {
        let branch_ref = "refs/heads/" + spec
        match resolve_ref(rfs, git_dir, branch_ref) {
          Some(id) => return (Some(spec), Some(id))
          None => ()
        }
      }
      // Resolve as commit
      let id = rev_parse(rfs, git_dir, spec)
      (None, id)
    }
  }
}

///|
fn load_previous_checkout_location(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
) -> String? raise @bit.GitError {
  let path = join_path(git_dir, "bit-prev-checkout")
  if !fs.is_file(path) {
    return None
  }
  let prev = worktree_trim_whitespace(read_trimmed_file(fs, path))
  if prev.length() == 0 {
    None
  } else {
    Some(prev)
  }
}

///|
fn checkout_to_worktree(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  worktree_path : String,
  admin_dir : String,
  commit_id : @bit.ObjectId,
) -> Unit raise @bit.GitError {
  let autocrlf = read_autocrlf_setting(rfs, git_dir)
  let core_eol = read_core_eol_setting(rfs, git_dir)
  let db = ObjectDb::load(rfs, git_dir)
  let files = collect_tree_files_from_commit(db, rfs, commit_id)
  // Write files to worktree
  for path, entry in files {
    let abs_path = join_path(worktree_path, path)
    if is_gitlink_mode_int(entry.mode) {
      fs.mkdir_p(abs_path)
      continue
    }
    let dir = parent_dir(abs_path)
    if dir != worktree_path {
      fs.mkdir_p(dir)
    }
    let obj = db.get(rfs, entry.id)
    match obj {
      Some(o) => {
        let attrs = resolve_eol_attrs(rfs, worktree_path, path)
        let output = smudge_for_checkout(o.data, attrs, autocrlf, core_eol~)
        fs.write_file(abs_path, output)
      }
      None => ()
    }
  }
  // Write index for the worktree
  let entries = tree_files_to_index(db, rfs, files)
  let index_path = join_path(admin_dir, "index")
  write_index_entries_to_path(fs, index_path, entries)
}

///|
fn find_worktree_admin_dir(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  worktree_path : String,
) -> String? raise @bit.GitError {
  let worktrees_dir = join_path(git_dir, "worktrees")
  if !fs.is_dir(worktrees_dir) {
    return None
  }
  let normalized_worktree_path = normalize_path(worktree_path)
  let entries = fs.readdir(worktrees_dir)
  for entry in entries {
    if entry == "." || entry == ".." {
      continue
    }
    let admin_dir = join_path(worktrees_dir, entry)
    if !fs.is_dir(admin_dir) {
      continue
    }
    let gitdir_path = join_path(admin_dir, "gitdir")
    if !fs.is_file(gitdir_path) {
      continue
    }
    let gitdir_content = read_trimmed_file(fs, gitdir_path)
    let wt_path = worktree_admin_worktree_path(admin_dir, gitdir_content)
    if wt_path == worktree_path || wt_path == normalized_worktree_path {
      return Some(admin_dir)
    }
  }
  None
}

///|
fn worktree_effective_force_level(force : Bool, force_level : Int) -> Int {
  if force_level > 0 {
    force_level
  } else if force {
    1
  } else {
    0
  }
}

///|
fn worktree_not_found_message(worktree_path : String) -> String {
  "'\{worktree_path}' is not a working tree"
}

///|
fn worktree_use_relative_paths_enabled(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
) -> Bool {
  let common_git_dir = resolve_commondir(fs, git_dir) catch { _ => git_dir }
  let config_path = join_path(common_git_dir, "config")
  if read_config_bool(fs, config_path, "worktree", "useRelativePaths").unwrap_or(
      false,
    ) {
    return true
  }
  read_config_bool(fs, config_path, "extensions", "relativeworktrees").unwrap_or(
    false,
  )
}

///|
fn worktree_compute_relative_path(
  from_dir : String,
  to_path : String,
) -> String {
  let from_parts = normalize_path(from_dir)
    .split("/")
    .map(fn(v) { v.to_owned() })
    .to_array()
  let to_parts = normalize_path(to_path)
    .split("/")
    .map(fn(v) { v.to_owned() })
    .to_array()
  let mut common = 0
  let min_len = if from_parts.length() < to_parts.length() {
    from_parts.length()
  } else {
    to_parts.length()
  }
  while common < min_len && from_parts[common] == to_parts[common] {
    common += 1
  }
  let result : Array[String] = []
  for i = common; i < from_parts.length(); i = i + 1 {
    if from_parts[i] != "" {
      result.push("..")
    }
  }
  for i = common; i < to_parts.length(); i = i + 1 {
    result.push(to_parts[i])
  }
  if result.length() == 0 {
    "."
  } else {
    result.join("/")
  }
}

///|
fn worktree_enable_relative_paths(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
) -> Unit raise @bit.GitError {
  let config_path = join_path(git_dir, "config")
  let content = read_config_content(rfs, git_dir)
  let blocks = if worktree_trim_whitespace(content).length() == 0 {
    []
  } else {
    parse_config_blocks(content)
  }
  let with_core = worktree_upsert_top_level_config_key(
    blocks, "core", "repositoryformatversion", "1",
  )
  let with_extensions = worktree_upsert_top_level_config_key(
    with_core, "extensions", "relativeworktrees", "true",
  )
  fs.write_string(config_path, render_config_blocks(with_extensions))
}

///|
fn worktree_upsert_top_level_config_key(
  blocks : Array[ConfigBlock],
  section : String,
  key : String,
  value : String,
) -> Array[ConfigBlock] {
  let updated : Array[ConfigBlock] = []
  let mut found_section = false
  for block in blocks {
    if block.section == Some(section) && block.name is None {
      found_section = true
      let lines : Array[String] = []
      let mut found_key = false
      for line in block.lines {
        match parse_config_kv(line) {
          Some((existing_key, _)) if existing_key == key => {
            lines.push("\t\{key} = \{value}")
            found_key = true
          }
          _ => lines.push(line)
        }
      }
      if !found_key {
        lines.push("\t\{key} = \{value}")
      }
      updated.push({
        header: block.header,
        section: block.section,
        name: block.name,
        lines,
      })
    } else {
      updated.push(block)
    }
  }
  if !found_section {
    updated.push({
      header: Some("[\{section}]"),
      section: Some(section),
      name: None,
      lines: ["\t\{key} = \{value}"],
    })
  }
  updated
}

///|
fn worktree_contains_nested_git(
  fs : &@bit.RepoFileSystem,
  path : String,
  is_root? : Bool = true,
) -> Bool raise @bit.GitError {
  if !fs.is_dir(path) {
    return false
  }
  for entry in fs.readdir(path) {
    if entry == "." || entry == ".." {
      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 &&
        !is_symlink &&
        (fs.is_file(child_path) || fs.is_dir(child_path)) {
        return true
      }
      continue
    }
    if !is_symlink &&
      fs.is_dir(child_path) &&
      worktree_contains_nested_git(fs, child_path, is_root=false) {
      return true
    }
  }
  false
}

///|
fn worktree_has_local_changes(
  fs : &@bit.RepoFileSystem,
  worktree_path : String,
  admin_dir : String,
) -> Bool raise @bit.GitError {
  let index_entries = read_index_entries(fs, admin_dir)
  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)
    if is_gitlink_mode_int(entry.mode) {
      gitlink_paths.push(entry.path)
      continue
    }
    let file_path = join_path(worktree_path, entry.path)
    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
            }
        }
      }
    }
    match db.get(fs, entry.id) {
      Some(obj) if obj.obj_type == @bit.ObjectType::Blob =>
        if file_bytes != obj.data {
          return true
        }
      _ => return true
    }
  }
  worktree_has_untracked_files(
    fs, worktree_path, "", tracked_paths, gitlink_paths,
  )
}

///|
fn worktree_has_untracked_files(
  fs : &@bit.RepoFileSystem,
  worktree_path : String,
  rel_path : String,
  tracked_paths : Array[String],
  gitlink_paths : Array[String],
) -> Bool raise @bit.GitError {
  let abs_path = if rel_path.length() == 0 {
    worktree_path
  } else {
    join_path(worktree_path, rel_path)
  }
  if !fs.is_dir(abs_path) {
    return false
  }
  for entry in fs.readdir(abs_path) {
    if entry == "." || entry == ".." {
      continue
    }
    if rel_path.length() == 0 && entry == ".git" {
      continue
    }
    let child_rel = if rel_path.length() == 0 {
      entry
    } else {
      rel_path + "/" + entry
    }
    let child_abs = join_path(abs_path, entry)
    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
        }
        continue
      }
      if tracked_paths.contains(child_rel) {
        return true
      }
      if worktree_has_tracked_descendant(tracked_paths, child_rel) {
        if worktree_has_untracked_files(
            fs, worktree_path, child_rel, tracked_paths, gitlink_paths,
          ) {
          return true
        }
      } else {
        return true
      }
    } else if (is_symlink || fs.is_file(child_abs)) &&
      !tracked_paths.contains(child_rel) {
      return true
    }
  }
  false
}

///|
fn worktree_admin_worktree_path(
  admin_dir : String,
  gitdir_content : String,
) -> String {
  if gitdir_content.length() == 0 {
    ""
  } else {
    prune_resolve_gitdir_path(admin_dir, gitdir_content)
    |> parent_dir
    |> normalize_path
  }
}

///|
fn worktree_has_tracked_descendant(
  tracked_paths : Array[String],
  prefix : String,
) -> Bool {
  let normalized = prefix + "/"
  for path in tracked_paths {
    if path.has_prefix(normalized) {
      return true
    }
  }
  false
}

///|
fn remove_dir_recursive(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  path : String,
) -> Unit raise @bit.GitError {
  if !rfs.is_dir(path) {
    if rfs.is_file(path) {
      fs.remove_file(path)
    }
    return
  }
  let entries = rfs.readdir(path)
  for entry in entries {
    if entry == "." || entry == ".." {
      continue
    }
    let child_path = join_path(path, entry)
    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)
    }
  }
  fs.remove_dir(path)
}

///|
fn read_trimmed_file(
  fs : &@bit.RepoFileSystem,
  path : String,
) -> String raise @bit.GitError {
  let content = @utf8.decode_lossy(fs.read_file(path)[:])
  worktree_trim_whitespace(content)
}

///|
fn worktree_trim_whitespace(s : String) -> String {
  let mut start = 0
  let mut end = s.length()
  while start < end {
    let c = s.unsafe_get(start)
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      start += 1
    } else {
      break
    }
  }
  while end > start {
    let c = s.unsafe_get(end - 1)
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      end -= 1
    } else {
      break
    }
  }
  if start == 0 && end == s.length() {
    s
  } else {
    String::unsafe_substring(s, start~, end~)
  }
}

///|
fn basename(path : String) -> String {
  match path.rev_find("/") {
    None => path
    Some(idx) =>
      if idx + 1 >= path.length() {
        // Path ends with /
        let trimmed = String::unsafe_substring(path, start=0, end=idx)
        basename(trimmed)
      } else {
        String::unsafe_substring(path, start=idx + 1, end=path.length())
      }
  }
}

///|
fn normalize_path(path : String) -> String {
  let parts : Array[String] = []
  for part_view in path.split("/") {
    let part = part_view.to_owned()
    if part == "" || part == "." {
      continue
    } else if part == ".." {
      if parts.length() > 0 && parts[parts.length() - 1] != ".." {
        let _ = parts.pop()
      } else if !path.has_prefix("/") {
        parts.push(part)
      }
    } else {
      parts.push(part)
    }
  }
  let result = parts.join("/")
  if path.has_prefix("/") {
    "/" + result
  } else {
    result
  }
}

///|
/// Write index entries to a specific path (for worktree index).
fn write_index_entries_to_path(
  fs : &@bit.FileSystem,
  path : String,
  entries : Array[IndexEntry],
) -> Unit raise @bit.GitError {
  let sorted = entries.copy()
  sorted.sort_by((a, b) => String::compare(a.path, b.path))
  // Serialize index
  let buf : Array[Byte] = []
  // Header: DIRC + version 2 + entry count
  buf.push(b'D')
  buf.push(b'I')
  buf.push(b'R')
  buf.push(b'C')
  // Version 2
  buf.push(0)
  buf.push(0)
  buf.push(0)
  buf.push(2)
  // Entry count (big endian)
  let count = sorted.length()
  buf.push(((count >> 24) & 0xff).to_byte())
  buf.push(((count >> 16) & 0xff).to_byte())
  buf.push(((count >> 8) & 0xff).to_byte())
  buf.push((count & 0xff).to_byte())
  for entry in sorted {
    // ctime, mtime (8 bytes each)
    for _ in 0..<16 {
      buf.push(0)
    }
    // dev, ino (4 bytes each)
    for _ in 0..<8 {
      buf.push(0)
    }
    // mode (4 bytes big endian)
    let mode = entry.mode
    buf.push(((mode >> 24) & 0xff).to_byte())
    buf.push(((mode >> 16) & 0xff).to_byte())
    buf.push(((mode >> 8) & 0xff).to_byte())
    buf.push((mode & 0xff).to_byte())
    // uid, gid (4 bytes each)
    for _ in 0..<8 {
      buf.push(0)
    }
    // size (4 bytes big endian)
    let size = entry.size
    buf.push(((size >> 24) & 0xff).to_byte())
    buf.push(((size >> 16) & 0xff).to_byte())
    buf.push(((size >> 8) & 0xff).to_byte())
    buf.push((size & 0xff).to_byte())
    // object id (20 bytes)
    for b in entry.id.bytes {
      buf.push(b)
    }
    // flags (2 bytes): name length in lower 12 bits
    let name_len = entry.path.length()
    let flags = if name_len > 0xfff { 0xfff } else { name_len }
    buf.push(((flags >> 8) & 0xff).to_byte())
    buf.push((flags & 0xff).to_byte())
    // path name
    for c in entry.path {
      buf.push(c.to_int().to_byte())
    }
    buf.push(0) // NUL terminator
    // Pad to 8-byte boundary (62 = fixed part before path)
    let entry_len = 62 + entry.path.length() + 1
    let padding = (8 - entry_len % 8) % 8
    for _ in 0.. buf[i]))
  let checksum = @bit.sha1(data)
  for b in checksum.bytes {
    buf.push(b)
  }
  let final_data = Bytes::from_array(
    FixedArray::makei(buf.length(), i => buf[i]),
  )
  fs.write_file(path, final_data)
}