///| Working tree operations: status, add, commit
///|
pub struct Status {
staged_added : Array[String]
staged_modified : Array[String]
staged_deleted : Array[String]
unstaged_modified : Array[String]
unstaged_deleted : Array[String]
untracked : Array[String]
}
///|
/// Get status similar to `git status --porcelain` buckets.
pub async fn status(
fs : &@bit.RepoFileSystem,
root : String,
run_filter_cmd? : FilterCmd,
) -> Status raise @bit.GitError {
let profile = profile_enabled()
let mut t0 = profile_start(profile)
let git_dir = join_path(root, ".git")
let actual_git_dir = if fs.is_file(git_dir) {
resolve_gitdir(fs, git_dir)
} else {
git_dir
}
let common_git_dir = resolve_common_git_dir(fs, actual_git_dir)
let autocrlf = read_autocrlf_setting(fs, common_git_dir)
let skip_worktree_paths = read_skip_worktree_paths(fs, actual_git_dir)
let racy_git = racy_git_enabled()
let (index_entries, cache_tree) = read_index_entries_with_cache_tree(
fs, actual_git_dir,
)
t0 = profile_lap(profile, "status read_index_entries", t0)
let (unstaged_modified, unstaged_deleted, untracked) = index_guided_status_walk(
fs,
root,
actual_git_dir,
index_entries,
skip_worktree_paths,
autocrlf,
racy_git,
run_filter_cmd?,
)
t0 = profile_lap(profile, "status index_guided_walk", t0)
// Build index_map for staged changes detection
let index_map : Map[String, IndexEntry] = Map([])
for e in index_entries {
if skip_worktree_paths.contains(e.path) {
continue
}
index_map[e.path] = e
}
let staged_added : Array[String] = []
let staged_modified : Array[String] = []
let staged_deleted : Array[String] = []
collect_staged_changes_from_head(
fs, actual_git_dir, cache_tree, index_map, skip_worktree_paths, staged_modified,
staged_deleted,
)
t0 = profile_lap(profile, "status read_head_entries", t0)
for path in index_map.keys() {
staged_added.push(path)
}
staged_added.sort()
staged_modified.sort()
staged_deleted.sort()
unstaged_modified.sort()
unstaged_deleted.sort()
untracked.sort()
ignore(profile_lap(profile, "status sort", t0))
{
staged_added,
staged_modified,
staged_deleted,
unstaged_modified,
unstaged_deleted,
untracked,
}
}
///|
/// Format status in porcelain-like lines.
pub async fn status_porcelain(
fs : &@bit.RepoFileSystem,
root : String,
) -> Array[String] raise @bit.GitError {
let s = status(fs, root)
status_porcelain_from(s)
}
///|
pub fn status_porcelain_from(s : Status) -> Array[String] {
let tracked_count = s.staged_added.length() +
s.staged_modified.length() +
s.staged_deleted.length() +
s.unstaged_modified.length() +
s.unstaged_deleted.length()
if tracked_count == 0 && s.untracked.length() == 0 {
return []
}
let xmap : Map[String, Char] = Map([], capacity=tracked_count)
let ymap : Map[String, Char] = Map([], capacity=tracked_count)
for p in s.staged_added {
xmap[p] = 'A'
}
for p in s.staged_modified {
xmap[p] = 'M'
}
for p in s.staged_deleted {
xmap[p] = 'D'
}
for p in s.unstaged_modified {
ymap[p] = 'M'
}
for p in s.unstaged_deleted {
ymap[p] = 'D'
}
// xmap.keys() ∪ ymap.keys() — paths touched by either side.
let list : Array[String] = Array::new(capacity=tracked_count)
for p in xmap.keys() {
list.push(p)
}
for p in ymap.keys() {
if !xmap.contains(p) {
list.push(p)
}
}
list.sort()
let out : Array[String] = Array::new(
capacity=list.length() + s.untracked.length(),
)
for p in list {
let x = xmap.get(p).unwrap_or(' ')
let y = ymap.get(p).unwrap_or(' ')
out.push("\{x}\{y} \{p}")
}
for p in s.untracked {
out.push("?? \{p}")
}
out
}
///|
/// Add files to index (like `git add`).
pub fn add_paths(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
paths : Array[String],
algo? : @object.HashAlgorithm = @object.HashAlgorithm::Sha1,
) -> Unit raise @bit.GitError {
let git_dir = join_path(root, ".git")
let actual_git_dir = if rfs.is_file(git_dir) {
resolve_gitdir(rfs, git_dir)
} else {
git_dir
}
let common_git_dir = resolve_common_git_dir(rfs, actual_git_dir)
let effective_algo = match algo {
@object.HashAlgorithm::Sha1 => repo_hash_algorithm(rfs, common_git_dir)
_ => algo
}
let autocrlf = read_autocrlf_setting(rfs, common_git_dir)
let entries = read_index_entries(rfs, actual_git_dir)
let map : Map[String, IndexEntry] = Map([])
for e in entries {
map[e.path] = e
}
let all_paths = expand_paths(rfs, root, paths)
for path in all_paths {
let abs = join_path(root, path)
if rfs.is_file(abs) {
let content = rfs.read_file(abs)
let attrs = resolve_eol_attrs(rfs, root, path)
let storage_content = match attrs.filter {
Some("lfs") => lfs_clean_content(fs, rfs, common_git_dir, content)
_ => content
}
let normalized = clean_for_storage(storage_content, attrs, autocrlf)
let id = write_loose_object(
fs,
common_git_dir,
@bit.ObjectType::Blob,
normalized,
algo=effective_algo,
)
compat_record_blob_mapping(fs, rfs, common_git_dir, id, normalized)
let size = content.length()
let (mtime_sec, mtime_nsec, dev, ino, uid, gid) = match
@bitio.worktree_entry_meta_sync(rfs, abs) {
Some(info) =>
(
info.mtime_sec.unwrap_or(0),
info.mtime_nsec.unwrap_or(0),
info.dev.unwrap_or(0),
info.ino.unwrap_or(0),
info.uid.unwrap_or(0),
info.gid.unwrap_or(0),
)
None => (0, 0, 0, 0, 0, 0)
}
map[path] = {
path,
id,
mode: default_file_mode(),
size,
mtime_sec,
mtime_nsec,
intent_to_add: false,
dev,
ino,
uid,
gid,
}
} else {
map.remove(path)
}
}
let out = map.values().to_array()
write_index_entries_preserving_cache_tree(fs, rfs, actual_git_dir, out)
}
///|
/// Add files to index with worktree mode detection (native).
pub async fn add_paths_async(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
paths : Array[String],
run_filter_cmd? : FilterCmd,
warnings? : Array[String],
) -> Unit raise @bit.GitError {
let git_dir = join_path(root, ".git")
let actual_git_dir = if rfs.is_file(git_dir) {
resolve_gitdir(rfs, git_dir)
} else {
git_dir
}
let common_git_dir = resolve_common_git_dir(rfs, actual_git_dir)
let effective_algo = repo_hash_algorithm(rfs, common_git_dir)
let autocrlf = read_autocrlf_setting(rfs, common_git_dir)
let core_filemode = read_core_filemode_setting(rfs, common_git_dir)
let entries = read_index_entries(rfs, actual_git_dir)
let map : Map[String, IndexEntry] = Map([])
for e in entries {
map[e.path] = e
}
let all_paths = expand_paths_async(rfs, root, paths)
for path in all_paths {
let abs = join_path(root, path)
let meta = @bitio.worktree_entry_meta(rfs, abs)
match meta {
None => map.remove(path)
Some(info) => {
// Skip unchanged files: if stat matches index entry, no re-hash needed
match map.get(path) {
Some(existing) => {
let stat_matches = match
(info.size, info.mtime_sec, info.mtime_nsec) {
(Some(sz), Some(ms), Some(mn)) =>
sz == existing.size &&
(existing.mtime_sec != 0 || existing.mtime_nsec != 0) &&
ms == existing.mtime_sec &&
mn == existing.mtime_nsec &&
(!core_filemode || info.mode == existing.mode)
_ => false
}
if stat_matches {
continue
}
}
_ => ()
}
match info.kind {
@bitio.WorktreeKindMeta::Regular => {
let content = rfs.read_file(abs)
let attrs = resolve_eol_attrs(rfs, root, path)
let storage_content = match attrs.filter {
Some("lfs") => lfs_clean_content(fs, rfs, common_git_dir, content)
Some(name) =>
match run_filter_cmd {
Some(run_cmd) => {
let (clean_cmd, _) = read_filter_commands(
rfs, common_git_dir, name,
)
match clean_cmd {
Some(cmd) =>
(run_cmd.run)(root, actual_git_dir, cmd, content)
None => content
}
}
None => content
}
None => content
}
let normalized = clean_for_storage(storage_content, attrs, autocrlf)
let entry_size = match attrs.filter {
Some("lfs") => content.length()
_ => storage_content.length()
}
// Collect CRLF conversion warnings
match warnings {
Some(warns) =>
if !is_binary_bytes(storage_content) {
collect_crlf_warnings(
warns, path, storage_content, normalized, attrs, autocrlf,
)
}
None => ()
}
let id = write_loose_object(
fs,
common_git_dir,
@bit.ObjectType::Blob,
normalized,
algo=effective_algo,
)
compat_record_blob_mapping(fs, rfs, common_git_dir, id, normalized)
let size = entry_size
let (mtime_sec, mtime_nsec) = info.mtime()
let dev = info.dev.unwrap_or(0)
let ino = info.ino.unwrap_or(0)
let uid = info.uid.unwrap_or(0)
let gid = info.gid.unwrap_or(0)
let mode = if core_filemode {
info.mode
} else {
match map.get(path) {
Some(existing) => existing.mode
None => default_file_mode()
}
}
map[path] = {
path,
id,
mode,
size,
mtime_sec,
mtime_nsec,
intent_to_add: false,
dev,
ino,
uid,
gid,
}
}
@bitio.WorktreeKindMeta::Symlink =>
match @bitio.read_symlink_target_path(abs) {
Some(target) => {
let bytes = @utf8.encode(target)
let id = write_loose_object(
fs,
common_git_dir,
@bit.ObjectType::Blob,
bytes,
algo=effective_algo,
)
compat_record_blob_mapping(fs, rfs, common_git_dir, id, bytes)
let size = bytes.length()
let (mtime_sec, mtime_nsec) = info.mtime()
let dev = info.dev.unwrap_or(0)
let ino = info.ino.unwrap_or(0)
let uid = info.uid.unwrap_or(0)
let gid = info.gid.unwrap_or(0)
map[path] = {
path,
id,
mode: info.mode,
size,
mtime_sec,
mtime_nsec,
intent_to_add: false,
dev,
ino,
uid,
gid,
}
}
None => ()
}
}
}
}
}
let out = map.values().to_array()
write_index_entries_preserving_cache_tree(fs, rfs, actual_git_dir, out)
}
///|
/// @bit.Commit current index and update HEAD.
pub fn commit(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
message : String,
author : String,
author_timestamp : Int64,
committer? : String = author,
committer_timestamp? : Int64 = author_timestamp,
allow_empty? : Bool = false,
timezone? : String = "+0000",
encoding? : String = "UTF-8",
author_timezone? : String? = None,
committer_timezone? : String? = None,
algo? : @object.HashAlgorithm = @object.HashAlgorithm::Sha1,
) -> @bit.ObjectId raise @bit.GitError {
let git_dir = join_path(root, ".git")
let actual_git_dir = if rfs.is_file(git_dir) {
resolve_gitdir(rfs, git_dir)
} else {
git_dir
}
let common_git_dir = resolve_common_git_dir(rfs, actual_git_dir)
let effective_algo = match algo {
@object.HashAlgorithm::Sha1 => repo_hash_algorithm(rfs, common_git_dir)
_ => algo
}
let parent = resolve_head_commit(rfs, actual_git_dir)
let entries = read_index_entries(rfs, actual_git_dir)
let tree_id = if entries.length() == 0 {
if !allow_empty {
raise @bit.GitError::InvalidObject("Empty index")
}
match parent {
Some(parent_id) => {
let db = ObjectDb::load_lazy(rfs, common_git_dir)
let parent_tree = match db.get(rfs, parent_id) {
Some(obj) if obj.obj_type == @bit.ObjectType::Commit =>
(@bit.parse_commit(obj.data).tree |> Some) catch {
_ => None
}
_ => None
}
match parent_tree {
Some(tree) => tree
None => {
let (empty_tree_id, empty_tree_compressed) = @object.create_tree_with_algo(
effective_algo,
[],
)
write_object_bytes(
fs, common_git_dir, empty_tree_id, empty_tree_compressed,
)
empty_tree_id
}
}
}
None => {
let (empty_tree_id, empty_tree_compressed) = @object.create_tree_with_algo(
effective_algo,
[],
)
write_object_bytes(
fs, common_git_dir, empty_tree_id, empty_tree_compressed,
)
empty_tree_id
}
}
} else {
write_tree_from_index(
fs,
rfs,
common_git_dir,
entries,
missing_ok=true,
algo=effective_algo,
)
}
let parents = match parent {
Some(p) => [p]
None => []
}
let resolved_author_timezone = author_timezone.unwrap_or(timezone)
let resolved_committer_timezone = committer_timezone.unwrap_or(timezone)
let commit = @bit.Commit::new(
tree_id,
parents,
author,
author_timestamp,
resolved_author_timezone,
committer,
committer_timestamp,
resolved_committer_timezone,
message,
encoding~,
)
let (commit_id, compressed) = @object.create_commit_with_algo(
effective_algo, commit,
)
write_object_bytes(fs, common_git_dir, commit_id, compressed)
compat_record_commit_mapping(fs, rfs, common_git_dir, commit_id, commit)
update_head_ref(fs, rfs, actual_git_dir, commit_id)
append_head_update_reflogs(
fs,
rfs,
actual_git_dir,
parent.unwrap_or(@bit.ObjectId::zero()),
commit_id,
if parent is None {
"commit (initial): " + worktree_commit_subject(message)
} else {
"commit: " + worktree_commit_subject(message)
},
committer~,
timestamp=committer_timestamp,
timezone=resolved_committer_timezone,
)
if entries.length() > 0 {
write_index_entries_with_tree(fs, rfs, actual_git_dir, entries, tree_id)
}
commit_id
}
///|
fn expand_paths(
fs : &@bit.RepoFileSystem,
root : String,
paths : Array[String],
) -> Array[String] raise @bit.GitError {
let result : Array[String] = []
let all = list_working_files(fs, root)
for p in paths {
let path = normalize_rel_path(p)
// Handle "." specially - add all files
if path == "." {
for f in all {
result.push(f)
}
continue
}
let abs = join_path(root, path)
if fs.is_dir(abs) {
let prefix = if path.has_suffix("/") { path } else { path + "/" }
for f in all {
if f.has_prefix(prefix) {
result.push(f)
}
}
} else {
result.push(path)
}
}
result
}
///|
async fn expand_paths_async(
fs : &@bit.RepoFileSystem,
root : String,
paths : Array[String],
) -> Array[String] raise @bit.GitError {
let result : Array[String] = []
let all = list_working_files_async(fs, root)
for p in paths {
let path = normalize_rel_path(p)
// Handle "." specially - add all files
if path == "." {
for f in all {
result.push(f)
}
continue
}
let abs = join_path(root, path)
if fs.is_dir(abs) {
let prefix = if path.has_suffix("/") { path } else { path + "/" }
for f in all {
if f.has_prefix(prefix) {
result.push(f)
}
}
} else {
result.push(path)
}
}
result
}
///|
fn normalize_rel_path(path : String) -> String raise @bit.GitError {
if path == "." {
return "."
}
let normalized = if path.has_prefix("./") {
String::unsafe_substring(path, start=2, end=path.length())
} else {
path
}
if normalized == "." || normalized == "" {
return "."
}
normalize_repo_path(normalized) catch {
_ => raise @bit.GitError::InvalidObject("invalid path: " + path)
}
}
///|
fn default_file_mode() -> Int {
33188 // 0o100644
}
///|
pub enum AutoCrlf {
Off
Input
On
}
///|
pub fn read_autocrlf_setting(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> AutoCrlf {
let mut value : AutoCrlf? = None
match @bitio.env_get("HOME") {
None => ()
Some(home) => {
let candidates : Array[String] = []
if @bitio.env_get("XDG_CONFIG_HOME") is Some(base) {
candidates.push(base + "/git/config")
}
candidates.push(home + "/.gitconfig")
for path in candidates {
if fs.is_file(path) {
value = read_autocrlf_from_config(fs, path)
break
}
}
}
}
let local_path = join_path(git_dir, "config")
if fs.is_file(local_path) {
match read_autocrlf_from_config(fs, local_path) {
Some(v) => value = Some(v)
None => ()
}
}
// GIT_CONFIG_OVERRIDES has highest priority (set by `git -c key=value`)
match @bitio.env_get("GIT_CONFIG_OVERRIDES") {
Some(env_value) =>
for line_view in env_value.split("\n") {
let line = line_view.to_owned().trim().to_owned()
if line.to_lower().has_prefix("core.autocrlf=") {
let v = String::unsafe_substring(line, start=14, end=line.length())
.trim()
.to_owned()
.to_lower()
value = Some(parse_autocrlf_value(v))
}
}
None => ()
}
match value {
Some(v) => v
None => AutoCrlf::Off
}
}
///|
fn read_autocrlf_from_config(
fs : &@bit.RepoFileSystem,
path : String,
) -> AutoCrlf? {
let bytes = fs.read_file(path) catch { _ => return None }
let content = @utf8.decode_lossy(bytes[:])
parse_autocrlf_from_content(content)
}
///|
fn parse_autocrlf_from_content(content : String) -> AutoCrlf? {
let mut section = ""
let mut value : AutoCrlf? = None
for line_view in content.split("\n") {
let line = line_view.trim().to_owned()
if line.length() == 0 {
continue
}
if line.has_prefix("#") || line.has_prefix(";") {
continue
}
if line.has_prefix("[") && line.has_suffix("]") {
let inner = String::unsafe_substring(line, start=1, end=line.length() - 1)
let inner_trim = inner.trim().to_owned()
let section_name = match inner_trim.find(" ") {
Some(i) => String::unsafe_substring(inner_trim, start=0, end=i)
None => inner_trim
}
section = section_name.to_lower()
continue
}
if section != "core" {
continue
}
match line.find("=") {
None => ()
Some(idx) => {
let key = String::unsafe_substring(line, start=0, end=idx)
.trim()
.to_owned()
.to_lower()
if key != "autocrlf" {
continue
}
let raw_value = String::unsafe_substring(
line,
start=idx + 1,
end=line.length(),
)
.trim()
.to_owned()
.to_lower()
value = raw_value |> parse_autocrlf_value |> Some
}
}
}
value
}
///|
pub fn read_core_eol_setting(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> EolAttr {
let mut value : EolAttr = EolAttr::Unspecified
match @bitio.env_get("HOME") {
None => ()
Some(home) => {
let candidates : Array[String] = []
if @bitio.env_get("XDG_CONFIG_HOME") is Some(base) {
candidates.push(base + "/git/config")
}
candidates.push(home + "/.gitconfig")
for path in candidates {
if fs.is_file(path) {
match read_core_eol_from_config(fs, path) {
Some(v) => value = v
None => ()
}
break
}
}
}
}
let local_path = join_path(git_dir, "config")
if fs.is_file(local_path) {
match read_core_eol_from_config(fs, local_path) {
Some(v) => value = v
None => ()
}
}
// GIT_CONFIG_OVERRIDES has highest priority
match @bitio.env_get("GIT_CONFIG_OVERRIDES") {
Some(env_value) =>
for line_view in env_value.split("\n") {
let line = line_view.to_owned().trim().to_owned()
if line.to_lower().has_prefix("core.eol=") {
let v = String::unsafe_substring(line, start=9, end=line.length())
.trim()
.to_owned()
.to_lower()
value = parse_eol_value(v)
}
}
None => ()
}
value
}
///|
fn read_core_eol_from_config(
fs : &@bit.RepoFileSystem,
path : String,
) -> EolAttr? {
let bytes = fs.read_file(path) catch { _ => return None }
let content = @utf8.decode_lossy(bytes[:])
let mut section = ""
let mut value : EolAttr? = None
for line_view in content.split("\n") {
let line = line_view.trim().to_owned()
if line.length() == 0 {
continue
}
if line.has_prefix("#") || line.has_prefix(";") {
continue
}
if line.has_prefix("[") && line.has_suffix("]") {
let inner = String::unsafe_substring(line, start=1, end=line.length() - 1)
let inner_trim = inner.trim().to_owned()
let section_name = match inner_trim.find(" ") {
Some(i) => String::unsafe_substring(inner_trim, start=0, end=i)
None => inner_trim
}
section = section_name.to_lower()
continue
}
if section != "core" {
continue
}
match line.find("=") {
None => ()
Some(idx) => {
let key = String::unsafe_substring(line, start=0, end=idx)
.trim()
.to_owned()
.to_lower()
if key != "eol" {
continue
}
let raw_value = String::unsafe_substring(
line,
start=idx + 1,
end=line.length(),
)
.trim()
.to_owned()
.to_lower()
value = Some(parse_eol_value(raw_value))
}
}
}
value
}
///|
fn parse_eol_value(value : String) -> EolAttr {
match value {
"lf" => EolAttr::Lf
"crlf" => EolAttr::Crlf
"native" => EolAttr::Lf // On Linux/macOS, native = LF
_ => EolAttr::Unspecified
}
}
///|
fn parse_autocrlf_value(value : String) -> AutoCrlf {
match value {
"true" | "yes" | "1" => AutoCrlf::On
"input" => AutoCrlf::Input
_ => AutoCrlf::Off
}
}
///|
fn read_core_filemode_setting(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> Bool {
let local_path = join_path(git_dir, "config")
if !fs.is_file(local_path) {
return true
}
match read_core_filemode_from_config(fs, local_path) {
Some(v) => v
None => true
}
}
///|
/// 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,
path : String,
) -> Bool? {
let bytes = fs.read_file(path) catch { _ => return None }
let content = @utf8.decode_lossy(bytes[:])
let mut section = ""
let mut value : Bool? = None
for line_view in content.split("\n") {
let line = line_view.trim().to_owned()
if line.length() == 0 {
continue
}
if line.has_prefix("#") || line.has_prefix(";") {
continue
}
if line.has_prefix("[") && line.has_suffix("]") {
let inner = String::unsafe_substring(line, start=1, end=line.length() - 1)
let inner_trim = inner.trim().to_owned()
let section_name = match inner_trim.find(" ") {
Some(i) => String::unsafe_substring(inner_trim, start=0, end=i)
None => inner_trim
}
section = section_name.to_lower()
continue
}
if section != "core" {
continue
}
match line.find("=") {
Some(idx) => {
let key = String::unsafe_substring(line, start=0, end=idx)
.trim()
.to_owned()
.to_lower()
if key != "filemode" {
continue
}
let raw_value = String::unsafe_substring(
line,
start=idx + 1,
end=line.length(),
)
.trim()
.to_owned()
.to_lower()
if raw_value == "true" ||
raw_value == "yes" ||
raw_value == "on" ||
raw_value == "1" {
value = Some(true)
} else if raw_value == "false" ||
raw_value == "no" ||
raw_value == "off" ||
raw_value == "0" {
value = Some(false)
}
}
None => ()
}
}
value
}
///|
fn racy_git_enabled() -> Bool {
match @bitio.env_get("BIT_RACY_GIT") {
Some(v) => v != "" && v != "0"
None => false
}
}
///|
pub fn normalize_worktree_content(
content : Bytes,
autocrlf : AutoCrlf,
) -> Bytes {
match autocrlf {
Off => content
Input | On =>
if is_binary_bytes(content) {
content
} else {
normalize_crlf_to_lf(content)
}
}
}
///|
pub fn is_binary_bytes(content : Bytes) -> Bool {
let limit = if content.length() > 8000 { 8000 } else { content.length() }
for i in 0.. Unit {
// Check if content was actually changed (CRLF→LF normalization happened)
let has_crlf_in_original = bytes_contains_crlf(original)
if has_crlf_in_original && original.length() != normalized.length() {
// CRLF was replaced by LF in the repository
// Now check if smudge would convert back to CRLF
let would_crlf = match attrs.text {
Unset => false
Set | Auto =>
match attrs.eol {
Crlf => true
Lf | Unspecified => autocrlf is On
}
Unspecified => autocrlf is On
}
if would_crlf {
warns.push(
"warning: in the working copy of '\{path}', CRLF will be replaced by LF the next time Git touches it",
)
} else {
warns.push(
"warning: in the working copy of '\{path}', CRLF will be replaced by LF",
)
}
} else if !has_crlf_in_original && bytes_contains_lf(original) {
// File has LF only. Check if smudge would add CRLF
let would_crlf = match attrs.text {
Unset => false
Set | Auto =>
match attrs.eol {
Crlf => true
Lf | Unspecified => autocrlf is On
}
Unspecified => autocrlf is On
}
if would_crlf {
warns.push(
"warning: in the working copy of '\{path}', LF will be replaced by CRLF",
)
}
}
}
///|
fn bytes_contains_crlf(content : Bytes) -> Bool {
let mut i = 0
while i + 1 < content.length() {
if content[i] == b'\r' && content[i + 1] == b'\n' {
return true
}
i += 1
}
false
}
///|
fn bytes_contains_lf(content : Bytes) -> Bool {
for i in 0.. Bytes {
let out : Array[Byte] = []
let mut i = 0
let mut changed = false
while i < content.length() {
if content[i] == b'\r' &&
i + 1 < content.length() &&
content[i + 1] == b'\n' {
out.push(b'\n')
i += 2
changed = true
continue
}
out.push(content[i])
i += 1
}
if !changed {
return content
}
Bytes::from_array(FixedArray::makei(out.length(), i => out[i]))
}
///|
pub fn write_loose_object(
fs : &@bit.FileSystem,
git_dir : String,
obj_type : @bit.ObjectType,
content : Bytes,
algo? : @object.HashAlgorithm = @object.HashAlgorithm::Sha1,
) -> @bit.ObjectId raise @bit.GitError {
let (id, compressed) = @object.create_object_with_algo(
algo, obj_type, content,
)
write_object_bytes(fs, git_dir, id, compressed)
id
}
///|
pub fn write_object_bytes(
fs : &@bit.FileSystem,
git_dir : String,
id : @bit.ObjectId,
compressed : Bytes,
) -> Unit raise @bit.GitError {
let hex = id.to_hex()
let path = join_path(
git_dir,
"objects/" +
String::unsafe_substring(hex, start=0, end=2) +
"/" +
String::unsafe_substring(hex, start=2, end=hex.length()),
)
let dir = join_path(
git_dir,
"objects/" + String::unsafe_substring(hex, start=0, end=2),
)
fs.mkdir_p(dir)
fs.write_file(path, compressed) catch {
// Loose objects may already exist as read-only files.
@bit.GitError::IoError(msg) if msg.contains("Permission denied") => {
fs.remove_file(path) catch {
_ => raise @bit.GitError::IoError(msg)
}
fs.write_file(path, compressed)
}
err => raise err
}
}
///|
pub fn resolve_head_commit(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> @bit.ObjectId? raise @bit.GitError {
match read_head_ref(fs, git_dir) {
Branch(name) => resolve_ref(fs, git_dir, "refs/heads/" + name)
Detached(id) => Some(id)
}
}
///|
pub fn update_head_ref(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
commit_id : @bit.ObjectId,
) -> Unit raise @bit.GitError {
let common_git_dir = resolve_common_git_dir(rfs, git_dir)
let head_path = join_path(git_dir, "HEAD")
let head = read_head_ref(rfs, git_dir) catch {
_ => HeadRef::Detached(commit_id)
}
match head {
Branch(name) => {
let refname = "refs/heads/" + name
if rfs.is_dir(join_path(common_git_dir, "reftable")) {
update_ref_reftable(fs, rfs, common_git_dir, refname, commit_id)
} else {
let ref_path = join_path(common_git_dir, refname)
let dir = parent_dir(ref_path)
fs.mkdir_p(dir)
fs.write_string(ref_path, commit_id.to_hex() + "\n")
}
}
Detached(_) => fs.write_string(head_path, commit_id.to_hex() + "\n")
}
}
///|
fn append_head_update_reflogs(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
old_id : @bit.ObjectId,
new_id : @bit.ObjectId,
message : String,
committer? : String = "unknown ",
timestamp? : Int64 = 0L,
timezone? : String = reflog_env_timezone(),
) -> Unit raise @bit.GitError {
let (author, email) = reflog_parse_identity(committer)
let (should_log_head, _) = should_log_ref(rfs, git_dir, "HEAD", false)
if should_log_head {
append_reflog(
fs, rfs, git_dir, "HEAD", old_id, new_id, author, email, timestamp, timezone,
message,
)
}
match read_head_ref(rfs, git_dir) {
Branch(name) => {
let refname = "refs/heads/" + name
let common_git_dir = resolve_common_git_dir(rfs, git_dir)
let (should_log_branch, _) = should_log_ref(
rfs, common_git_dir, refname, false,
)
if should_log_branch {
append_reflog(
fs, rfs, common_git_dir, refname, old_id, new_id, author, email, timestamp,
timezone, message,
)
}
}
Detached(_) => ()
}
}
///|
fn update_ref_reftable(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
commit_id : @bit.ObjectId,
) -> Unit raise @bit.GitError {
// Determine next update_index
let tables = @reftable.load_tables(rfs, git_dir)
let max_index = if tables.length() > 0 {
let last = tables[tables.length() - 1]
match @reftable.parse_header(last) {
Some(h) => h.max_update_index
None => 0UL
}
} else {
0UL
}
let next_index = max_index + 1UL
let record : @reftable.RefRecord = {
refname,
update_index: next_index,
value: @reftable.RefValue::Val1(commit_id),
}
let table_data = @reftable.write_reftable(
[record],
4096,
next_index,
next_index,
)
// Write table file
let reftable_dir = git_dir + "/reftable"
let pad_hex = reftable_index_to_hex12(next_index)
let table_name = "0x" + pad_hex + "-0x" + pad_hex + "-00000000.ref"
let table_path = reftable_dir + "/" + table_name
fs.write_file(table_path, table_data)
// Update tables.list
let existing_names = @reftable.read_tables_list(rfs, git_dir)
existing_names.push(table_name)
let tables_list = existing_names.join("\n") + "\n"
fs.write_string(reftable_dir + "/tables.list", tables_list)
}
///|
fn reftable_index_to_hex12(value : UInt64) -> String {
let hex_chars : FixedArray[Byte] = [
b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c',
b'd', b'e', b'f',
]
let buf = StringBuilder::new()
for i = 11; i >= 0; i = i - 1 {
let shift = i * 4
let nibble = ((value >> shift.to_uint64().to_int()) & 0xfUL).to_int()
buf.write_char(Int::unsafe_to_char(hex_chars[nibble].to_int()))
}
buf.to_string()
}
///|
pub fn write_tree_from_index(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
entries : Array[IndexEntry],
prefix? : String? = None,
missing_ok? : Bool = false,
algo? : @object.HashAlgorithm = @object.HashAlgorithm::Sha1,
) -> @bit.ObjectId raise @bit.GitError {
let object_git_dir = resolve_common_git_dir(rfs, git_dir)
let effective_algo = match algo {
@object.HashAlgorithm::Sha1 => repo_hash_algorithm(rfs, object_git_dir)
_ => algo
}
// If prefix is specified, filter entries and strip prefix from paths
let rel_entries = match prefix {
Some(p) => {
// Ensure prefix ends with /
let prefix_with_slash = if p.has_suffix("/") { p } else { p + "/" }
let result : Array[IndexEntry] = []
for e in entries {
if worktree_is_zero_object_id(e.id) {
continue
}
if e.path.has_prefix(prefix_with_slash) {
// Strip the prefix from the path
let new_path = String::unsafe_substring(
e.path,
start=prefix_with_slash.length(),
end=e.path.length(),
)
result.push({
path: new_path,
id: e.id,
mode: e.mode,
size: e.size,
mtime_sec: e.mtime_sec,
mtime_nsec: e.mtime_nsec,
intent_to_add: e.intent_to_add,
dev: e.dev,
ino: e.ino,
uid: e.uid,
gid: e.gid,
})
}
}
result
}
None => {
let result : Array[IndexEntry] = []
for e in entries {
if worktree_is_zero_object_id(e.id) {
continue
}
result.push(e)
}
result
}
}
let object_db = if missing_ok {
None
} else {
Some(ObjectDb::load_lazy(rfs, object_git_dir))
}
write_tree_from_entries(
fs,
rfs,
object_git_dir,
rel_entries,
object_db,
algo=effective_algo,
)
}
///|
fn write_tree_from_entries(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
entries : Array[IndexEntry],
object_db : ObjectDb?,
algo? : @object.HashAlgorithm = @object.HashAlgorithm::Sha1,
) -> @bit.ObjectId raise @bit.GitError {
let effective_algo = match algo {
@object.HashAlgorithm::Sha1 => repo_hash_algorithm(rfs, git_dir)
_ => algo
}
let file_entries : Array[@bit.TreeEntry] = []
let dir_map : Map[String, Array[IndexEntry]] = Map([])
for e in entries {
if worktree_is_zero_object_id(e.id) {
continue
}
if !is_gitlink_mode_int(e.mode) {
match object_db {
Some(db) =>
match db.get(rfs, e.id) {
None =>
raise @bit.GitError::InvalidObject(
"Object not found: " + e.id.to_hex(),
)
Some(_) => ()
}
None => ()
}
}
match split_first(e.path) {
(name, None) => {
let mode = @string_utils.mode_to_string(e.mode)
file_entries.push(@bit.TreeEntry::new(mode, name, e.id))
}
(name, Some(rest)) =>
match dir_map.get(name) {
Some(list) =>
list.push({
path: rest,
id: e.id,
mode: e.mode,
size: e.size,
mtime_sec: e.mtime_sec,
mtime_nsec: e.mtime_nsec,
intent_to_add: e.intent_to_add,
dev: e.dev,
ino: e.ino,
uid: e.uid,
gid: e.gid,
})
None =>
dir_map[name] = [
{
path: rest,
id: e.id,
mode: e.mode,
size: e.size,
mtime_sec: e.mtime_sec,
mtime_nsec: e.mtime_nsec,
intent_to_add: e.intent_to_add,
dev: e.dev,
ino: e.ino,
uid: e.uid,
gid: e.gid,
},
]
}
}
}
for dir_name, list in dir_map {
let sub_id = write_tree_from_entries(
fs,
rfs,
git_dir,
list,
object_db,
algo=effective_algo,
)
file_entries.push(@bit.TreeEntry::new("40000", dir_name, sub_id))
}
// Git sorts tree entries by name, with directories having "/" appended for comparison
file_entries.sort_by(fn(a, b) {
let a_key = if a.mode == "40000" { a.name + "/" } else { a.name }
let b_key = if b.mode == "40000" { b.name + "/" } else { b.name }
compare_strings_lexicographic(a_key, b_key)
})
let (tree_id, compressed) = @object.create_tree_with_algo(
effective_algo, file_entries,
)
write_object_bytes(fs, git_dir, tree_id, compressed)
compat_record_tree_mapping(fs, rfs, git_dir, tree_id, file_entries)
tree_id
}
///|
fn worktree_is_zero_object_id(id : @bit.ObjectId) -> Bool {
id.is_zero()
}
///|
fn split_first(path : String) -> (String, String?) {
match path.find("/") {
None => (path, None)
Some(idx) => {
let name = String::unsafe_substring(path, start=0, end=idx)
let rest = String::unsafe_substring(
path,
start=idx + 1,
end=path.length(),
)
(name, Some(rest))
}
}
}
///|
/// Lexicographic string comparison (byte-by-byte ASCII order)
fn compare_strings_lexicographic(a : String, b : String) -> Int {
let a_len = a.length()
let b_len = b.length()
let min_len = if a_len < b_len { a_len } else { b_len }
for i in 0.. b_char {
return 1
}
}
// If all characters are equal, shorter string comes first
if a_len < b_len {
-1
} else if a_len > b_len {
1
} else {
0
}
}
// mode_to_string and to_octal_string are defined in string_utils.mbt
///|
fn trim_worktree_text(s : String) -> String {
let mut start = 0
let mut end = s.length()
while start < end {
let c = s[start]
if c == ' ' || c == '\n' || c == '\r' || c == '\t' {
start += 1
} else {
break
}
}
while end > start {
let c = s[end - 1]
if c == ' ' || c == '\n' || c == '\r' || c == '\t' {
end -= 1
} else {
break
}
}
String::unsafe_substring(s, start~, end~)
}
///|
fn resolve_worktree_common_git_dir(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> String {
let commondir_path = join_path(git_dir, "commondir")
if !fs.is_file(commondir_path) {
return git_dir
}
let raw = @utf8.decode_lossy(
(fs.read_file(commondir_path) catch { _ => Default::default() })[:],
)
let rel = trim_worktree_text(raw)
if rel.length() == 0 {
return git_dir
}
if rel.has_prefix("/") {
normalize_path(rel)
} else {
normalize_path(join_path(git_dir, rel))
}
}
///|
fn collect_staged_changes_from_head(
fs : &@bit.RepoFileSystem,
git_dir : String,
cache_tree : IndexCacheTree?,
index_map : Map[String, IndexEntry],
skip_worktree_paths : Map[String, Bool],
staged_modified : Array[String],
staged_deleted : Array[String],
) -> Unit raise @bit.GitError {
let head = resolve_head_commit(fs, git_dir)
match head {
None => return
Some(commit_id) => {
let common_git_dir = resolve_worktree_common_git_dir(fs, git_dir)
let db = ObjectDb::load_lazy(fs, common_git_dir)
// `git status` does not re-hash HEAD tree/commit objects on read (git
// trusts the object store); skip the per-object SHA verification that
// otherwise runs for every loose object walked here.
db.set_skip_verify(true)
let commit_obj = db.get(fs, commit_id)
match commit_obj {
None => return
Some(obj) => {
if obj.obj_type != @bit.ObjectType::Commit {
raise @bit.GitError::InvalidObject("Object is not a commit")
}
let info = @bit.parse_commit(obj.data)
collect_staged_changes_db(
db,
fs,
info.tree,
"",
cache_tree,
index_map,
skip_worktree_paths,
staged_modified,
staged_deleted,
)
}
}
}
}
}
///|
fn collect_staged_changes_db(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
tree_id : @bit.ObjectId,
prefix : String,
cache_tree : IndexCacheTree?,
index_map : Map[String, IndexEntry],
skip_worktree_paths : Map[String, Bool],
staged_modified : Array[String],
staged_deleted : Array[String],
) -> Unit raise @bit.GitError {
match cache_tree {
Some(tree) if index_cache_tree_matches_tree(tree, tree_id) => {
index_map_remove_tree_paths(index_map, prefix)
return
}
_ => ()
}
let tree_obj = db.get(fs, tree_id)
match tree_obj {
None => raise @bit.GitError::InvalidObject("Missing tree object")
Some(obj) => {
if obj.obj_type != @bit.ObjectType::Tree {
raise @bit.GitError::InvalidObject("Object is not a tree")
}
let entries = @bit.parse_tree(obj.data)
for entry in entries {
let path = if prefix.length() == 0 {
entry.name
} else {
prefix + "/" + entry.name
}
if skip_worktree_paths.contains(path) {
continue
}
if is_tree_mode(entry.mode) {
collect_staged_changes_db(
db,
fs,
entry.id,
path,
index_cache_tree_child(cache_tree, entry.name),
index_map,
skip_worktree_paths,
staged_modified,
staged_deleted,
)
} else {
let mode = worktree_parse_octal(entry.mode)
match index_map.get(path) {
Some(index_entry) => {
if index_entry.id != entry.id || index_entry.mode != mode {
staged_modified.push(path)
}
index_map.remove(path)
}
None => staged_deleted.push(path)
}
}
}
}
}
}
///|
fn index_cache_tree_matches_tree(
cache_tree : IndexCacheTree,
tree_id : @bit.ObjectId,
) -> Bool {
match cache_tree.oid {
Some(cache_tree_id) =>
cache_tree.entry_count >= 0 && cache_tree_id == tree_id
None => false
}
}
///|
fn index_cache_tree_child(
cache_tree : IndexCacheTree?,
name : String,
) -> IndexCacheTree? {
match cache_tree {
None => None
Some(tree) => {
for subtree in tree.subtrees {
if subtree.name == name {
return Some(subtree)
}
}
None
}
}
}
///|
fn index_map_remove_tree_paths(
index_map : Map[String, IndexEntry],
prefix : String,
) -> Unit {
let paths : Array[String] = []
for path in index_map.keys() {
if prefix.length() == 0 || path.has_prefix(prefix + "/") {
paths.push(path)
}
}
for path in paths {
index_map.remove(path)
}
}
///|
fn worktree_parse_octal(s : String) -> Int {
let mut result = 0
for c in s {
if c < '0' || c > '7' {
continue
}
result = result * 8 + (c.to_int() - '0'.to_int())
}
result
}
///|
/// Commit with amend (replace the last commit).
pub fn commit_amend(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
message : String,
author : String,
author_timestamp : Int64,
committer? : String = author,
committer_timestamp? : Int64 = author_timestamp,
timezone? : String = "+0000",
encoding? : String = "UTF-8",
author_timezone? : String? = None,
committer_timezone? : String? = None,
algo? : @object.HashAlgorithm = @object.HashAlgorithm::Sha1,
) -> @bit.ObjectId raise @bit.GitError {
let git_dir = join_path(root, ".git")
let actual_git_dir = if rfs.is_file(git_dir) {
resolve_gitdir(rfs, git_dir)
} else {
git_dir
}
let common_git_dir = resolve_common_git_dir(rfs, actual_git_dir)
let effective_algo = match algo {
@object.HashAlgorithm::Sha1 => repo_hash_algorithm(rfs, common_git_dir)
_ => algo
}
let entries = read_index_entries(rfs, actual_git_dir)
if entries.length() == 0 {
raise @bit.GitError::InvalidObject("Empty index")
}
let tree_id = write_tree_from_index(
fs,
rfs,
common_git_dir,
entries,
algo=effective_algo,
)
// Get the parent of the current HEAD (grandparent of new commit)
let current_head = resolve_head_commit(rfs, actual_git_dir)
let parents = match current_head {
Some(head_id) => {
// Get parent of current HEAD
let db = ObjectDb::load_lazy(rfs, common_git_dir)
let obj = db.get(rfs, head_id)
match obj {
Some(o) => {
let info = @bit.parse_commit(o.data)
info.parents
}
None => []
}
}
None => []
}
let resolved_author_timezone = author_timezone.unwrap_or(timezone)
let resolved_committer_timezone = committer_timezone.unwrap_or(timezone)
let commit = @bit.Commit::new(
tree_id,
parents,
author,
author_timestamp,
resolved_author_timezone,
committer,
committer_timestamp,
resolved_committer_timezone,
message,
encoding~,
)
let (commit_id, compressed) = @object.create_commit_with_algo(
effective_algo, commit,
)
write_object_bytes(fs, common_git_dir, commit_id, compressed)
compat_record_commit_mapping(fs, rfs, common_git_dir, commit_id, commit)
update_head_ref(fs, rfs, actual_git_dir, commit_id)
append_head_update_reflogs(
fs,
rfs,
actual_git_dir,
current_head.unwrap_or(@bit.ObjectId::zero()),
commit_id,
"commit (amend): " + worktree_commit_subject(message),
committer~,
timestamp=committer_timestamp,
timezone=resolved_committer_timezone,
)
commit_id
}
///|
fn worktree_commit_subject(message : String) -> String {
match message.find("\n") {
Some(idx) => String::unsafe_substring(message, start=0, end=idx)
None => message
}
}
///|
fn reflog_parse_identity(identity : String) -> (String, String) {
let trimmed = identity.trim().to_owned()
match trimmed.find("<") {
Some(start) =>
match trimmed.find(">") {
Some(end) if end > start => {
let name = String::unsafe_substring(trimmed, start=0, end=start)
.trim()
.to_owned()
let email = String::unsafe_substring(trimmed, start=start + 1, end~)
(if name.length() == 0 { "unknown" } else { name }, email)
}
_ => (trimmed, "unknown")
}
None => (trimmed, "unknown")
}
}
///|
fn reflog_env_timezone() -> String {
"+0000"
}
///|
/// Remove files from index (and optionally working tree).
pub fn rm_paths(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
paths : Array[String],
cached? : Bool = false,
force? : Bool = false,
recursive? : Bool = false,
) -> Unit raise @bit.GitError {
let git_dir = join_path(root, ".git")
let actual_git_dir = if rfs.is_file(git_dir) {
resolve_gitdir(rfs, git_dir)
} else {
git_dir
}
let entries = read_index_entries(rfs, actual_git_dir)
let skip_worktree_paths = read_skip_worktree_paths(rfs, actual_git_dir)
let map : Map[String, IndexEntry] = Map([])
for e in entries {
map[e.path] = e
}
// Expand paths if recursive
let all_paths = if recursive {
expand_rm_paths(rfs, root, paths, map)
} else {
paths
}
let removed_submodule_paths : Array[String] = []
ignore(force)
for path in all_paths {
let norm_path = normalize_rel_path(path)
let tracked_entry = map.get(norm_path)
match map.get(norm_path) {
Some(entry) if is_gitlink_mode_int(entry.mode) =>
removed_submodule_paths.push(norm_path)
_ => ()
}
// Remove from index
map.remove(norm_path)
// Remove from working tree unless --cached
if !cached {
let abs = join_path(root, norm_path)
if rfs.is_file(abs) {
fs.remove_file(abs)
rm_prune_empty_parent_dirs(fs, rfs, root, norm_path)
} else if tracked_entry is Some(entry) &&
is_gitlink_mode_int(entry.mode) &&
rfs.is_dir(abs) {
remove_worktree_path_recursive(fs, rfs, abs)
rm_prune_empty_parent_dirs(fs, rfs, root, norm_path)
}
}
}
let out : Array[IndexEntry] = Array::new(capacity=map.length())
let remaining_skip_worktree_paths : Map[String, Bool] = Map([])
let remaining_skip_paths : Array[String] = []
for path, entry in map {
out.push(entry)
if skip_worktree_paths.contains(path) {
remaining_skip_worktree_paths[path] = true
remaining_skip_paths.push(path)
}
}
write_skip_worktree_paths(fs, actual_git_dir, remaining_skip_paths)
write_index_entries_with_skip_worktree(
fs, actual_git_dir, out, remaining_skip_worktree_paths,
)
if !cached {
rm_update_gitmodules_after_submodule_removal(
fs, rfs, root, removed_submodule_paths,
)
}
}
///|
fn rm_update_gitmodules_after_submodule_removal(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
removed_submodule_paths : Array[String],
) -> Unit raise @bit.GitError {
if removed_submodule_paths.length() == 0 {
return
}
let gitmodules_path = join_path(root, ".gitmodules")
if !rfs.is_file(gitmodules_path) {
return
}
let removed_paths : Map[String, Bool] = Map([])
for path in removed_submodule_paths {
removed_paths[path] = true
}
let content = decode_bytes_lossy(rfs.read_file(gitmodules_path))
let updated = rm_remove_gitmodules_sections_by_path(content, removed_paths)
if updated == content {
return
}
fs.write_string(gitmodules_path, updated)
add_paths(fs, rfs, root, [".gitmodules"])
}
///|
fn rm_remove_gitmodules_sections_by_path(
content : String,
removed_paths : Map[String, Bool],
) -> String {
let out : Array[String] = []
let mut section_lines : Array[String] = []
let mut in_submodule = false
let mut current_path : String? = None
let mut have_section = false
let flush_section = fn() {
if !have_section {
return
}
if in_submodule &&
current_path is Some(path) &&
removed_paths.contains(path) {
return
}
for line in section_lines {
out.push(line)
}
}
for line_view in content.split("\n") {
let line = line_view.to_owned()
let trimmed = line.trim(chars=" \t").to_owned()
if trimmed.has_prefix("[") && trimmed.has_suffix("]") {
flush_section()
section_lines = [line]
have_section = true
in_submodule = trimmed.has_prefix("[submodule \"") &&
trimmed.has_suffix("\"]")
current_path = None
continue
}
if !have_section {
out.push(line)
continue
}
section_lines.push(line)
if in_submodule && trimmed.has_prefix("path = ") {
current_path = Some(
String::unsafe_substring(trimmed, start=7, end=trimmed.length()),
)
}
}
flush_section()
out.join("\n")
}
///|
fn rm_prune_empty_parent_dirs(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
path : String,
) -> Unit {
let mut current = match path.rev_find("/") {
Some(idx) => String::unsafe_substring(path, start=0, end=idx)
None => return
}
while current.length() > 0 {
let abs = join_path(root, current)
if !rfs.is_dir(abs) {
return
}
let entries = rfs.readdir(abs) catch { _ => return }
if entries.length() > 0 {
return
}
fs.remove_dir(abs) catch {
_ => return
}
current = match current.rev_find("/") {
Some(idx) => String::unsafe_substring(current, start=0, end=idx)
None => return
}
}
}
///|
fn expand_rm_paths(
rfs : &@bit.RepoFileSystem,
root : String,
paths : Array[String],
index_map : Map[String, IndexEntry],
) -> Array[String] raise @bit.GitError {
let result : Array[String] = []
for p in paths {
let norm_path = normalize_rel_path(p)
if norm_path == "." || norm_path == "" {
for entry_path in index_map.keys() {
result.push(entry_path)
}
continue
}
let abs = join_path(root, norm_path)
if rfs.is_dir(abs) {
// Add all index entries under this directory
let prefix = if norm_path.has_suffix("/") {
norm_path
} else {
norm_path + "/"
}
for entry_path in index_map.keys() {
if entry_path.has_prefix(prefix) || entry_path == norm_path {
result.push(entry_path)
}
}
} else {
result.push(norm_path)
}
}
result
}
///|
/// Move/rename a file in the index and working tree.
pub fn mv_path(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
source : String,
dest : String,
force? : Bool = false,
) -> Unit raise @bit.GitError {
let git_dir = join_path(root, ".git")
let actual_git_dir = if rfs.is_file(git_dir) {
resolve_gitdir(rfs, git_dir)
} else {
git_dir
}
let src_norm = normalize_rel_path(source)
let mut dst_norm = normalize_rel_path(dest)
let src_abs = join_path(root, src_norm)
let mut dst_abs = join_path(root, dst_norm)
// Check source exists
if !rfs.is_file(src_abs) && !rfs.is_dir(src_abs) {
raise @bit.GitError::InvalidObject("source '\{source}' does not exist")
}
// If dest is a directory, move source into it (like `mv file dir/`)
if rfs.is_dir(dst_abs) {
let basename = match src_norm.rev_find("/") {
Some(i) =>
String::unsafe_substring(src_norm, start=i + 1, end=src_norm.length())
None => src_norm
}
dst_norm = join_path(dst_norm, basename)
dst_abs = join_path(root, dst_norm)
}
// Check dest doesn't exist (unless force)
if (rfs.is_file(dst_abs) || rfs.is_dir(dst_abs)) && !force {
raise @bit.GitError::InvalidObject("destination '\{dest}' already exists")
}
// Read index
let entries = read_index_entries(rfs, actual_git_dir)
let map : Map[String, IndexEntry] = Map([])
for e in entries {
map[e.path] = e
}
// Find source entry
guard map.get(src_norm) is Some(src_entry) else {
raise @bit.GitError::InvalidObject("'\{source}' not in index")
}
// Remove source, add dest
map.remove(src_norm)
map[dst_norm] = {
path: dst_norm,
id: src_entry.id,
mode: src_entry.mode,
size: src_entry.size,
mtime_sec: src_entry.mtime_sec,
mtime_nsec: src_entry.mtime_nsec,
intent_to_add: src_entry.intent_to_add,
dev: src_entry.dev,
ino: src_entry.ino,
uid: src_entry.uid,
gid: src_entry.gid,
}
// Move file on disk
if rfs.is_dir(src_abs) {
mv_copy_directory_recursive(fs, rfs, src_abs, dst_abs)
let src_gitfile = join_path(src_abs, ".git")
let dst_gitfile = join_path(dst_abs, ".git")
if rfs.is_file(src_gitfile) && rfs.is_file(dst_gitfile) {
mv_rewrite_gitfile_after_directory_move(
fs, rfs, src_abs, dst_abs, dst_gitfile,
)
}
remove_worktree_path_recursive(fs, rfs, src_abs)
} else {
let content = rfs.read_file(src_abs)
let dst_dir = parent_dir(dst_abs)
if dst_dir.length() > 0 {
fs.mkdir_p(dst_dir)
}
fs.write_file(dst_abs, content)
fs.remove_file(src_abs)
}
// Write index
let out = map.values().to_array()
write_index_entries(fs, actual_git_dir, out)
if is_gitlink_mode_int(src_entry.mode) {
mv_update_submodule_paths_after_move(
fs, rfs, root, actual_git_dir, src_norm, dst_norm,
)
}
}
///|
fn mv_update_submodule_paths_after_move(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
git_dir : String,
src_path : String,
dst_path : String,
) -> Unit raise @bit.GitError {
let gitmodules_path = join_path(root, ".gitmodules")
if rfs.is_file(gitmodules_path) {
let content = decode_bytes_lossy(rfs.read_file(gitmodules_path))
let updated = mv_update_submodule_path_in_config(
content, src_path, dst_path,
)
if updated != content {
fs.write_string(gitmodules_path, updated)
add_paths(fs, rfs, root, [".gitmodules"])
}
}
let config_path = join_path(git_dir, "config")
if rfs.is_file(config_path) {
let content = decode_bytes_lossy(rfs.read_file(config_path))
let updated = mv_update_submodule_path_in_config(
content, src_path, dst_path,
)
if updated != content {
fs.write_string(config_path, updated)
}
}
}
///|
fn mv_update_submodule_path_in_config(
content : String,
src_path : String,
dst_path : String,
) -> String {
let out : Array[String] = []
let mut in_submodule = false
for line_view in content.split("\n") {
let line = line_view.to_owned()
let trimmed = line.trim()
if trimmed.has_prefix("[") && trimmed.has_suffix("]") {
in_submodule = trimmed.has_prefix("[submodule \"")
out.push(line)
continue
}
if in_submodule && trimmed == "path = " + src_path {
out.push("\tpath = " + dst_path)
} else {
out.push(line)
}
}
out.join("\n")
}
///|
fn mv_copy_directory_recursive(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
src : String,
dst : String,
) -> Unit raise @bit.GitError {
fs.mkdir_p(dst)
for entry in rfs.readdir(src) {
let src_child = join_path(src, entry)
let dst_child = join_path(dst, entry)
if rfs.is_dir(src_child) {
mv_copy_directory_recursive(fs, rfs, src_child, dst_child)
} else if rfs.is_file(src_child) {
fs.write_file(dst_child, rfs.read_file(src_child))
}
}
}
///|
fn mv_rewrite_gitfile_after_directory_move(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
src_abs : String,
dst_abs : String,
dst_gitfile : String,
) -> Unit raise @bit.GitError {
let content = trim_worktree_text(
@string_utils.decode_bytes(rfs.read_file(dst_gitfile)),
)
if !content.has_prefix("gitdir: ") {
return ()
}
let rel_gitdir = String::unsafe_substring(
content,
start=8,
end=content.length(),
)
let modules_dir = normalize_path(src_abs + "/" + rel_gitdir)
let rewritten = tree_ops_get_rel_path(dst_abs, modules_dir)
fs.write_string(dst_gitfile, "gitdir: \{rewritten}\n")
}
///|
/// LF→CRLF expansion (smudge direction).
pub fn convert_lf_to_crlf(content : Bytes) -> Bytes {
let out : Array[Byte] = []
let mut i = 0
let mut changed = false
while i < content.length() {
let b = content[i]
if b == b'\n' && (i == 0 || content[i - 1] != b'\r') {
out.push(b'\r')
out.push(b'\n')
changed = true
} else {
out.push(b)
}
i += 1
}
if !changed {
return content
}
Bytes::from_array(FixedArray::makei(out.length(), fn(i) { out[i] }))
}
///|
/// Smudge: blob→working tree expansion.
pub fn smudge_worktree_content(content : Bytes, autocrlf : AutoCrlf) -> Bytes {
match autocrlf {
Off | Input => content
On =>
if is_binary_bytes(content) {
content
} else {
convert_lf_to_crlf(content)
}
}
}