///|
pub(all) struct StorageInitArgs {
quiet : Bool
default_branch : String
ref_format : String
object_format : String
}
///|
pub(all) struct StorageAddArgs {
add_all : Bool
paths : Array[String]
}
///|
pub(all) struct StorageCommitArgs {
message : String
all_modified : Bool
allow_empty : Bool
quiet : Bool
encoding : String
}
///|
pub(all) struct StorageStatusArgs {
porcelain : Bool
short : Bool
show_branch : Bool
}
///|
pub(all) struct StorageLogArgs {
oneline : Bool
max_count : Int
}
///|
pub(all) struct StorageHashObjectArgs {
write_object : Bool
stdin_mode : Bool
stdin_paths : Bool
no_filters : Bool
literally : Bool
obj_type : @bit.ObjectType
paths : Array[String]
object_format : String
path_hint : String
}
///|
pub(all) struct StorageWriteTreeArgs {
prefix : String?
missing_ok : Bool
}
///|
pub(all) struct StorageUpdateRefArgs {
delete_mode : Bool
refname : String
new_value : String?
old_value : String?
}
///|
pub(all) enum StorageCommand {
Init(StorageInitArgs)
Add(StorageAddArgs)
Commit(StorageCommitArgs)
Status(StorageStatusArgs)
Log(StorageLogArgs)
HashObject(StorageHashObjectArgs)
WriteTree(StorageWriteTreeArgs)
UpdateRef(StorageUpdateRefArgs)
}
///|
pub fn run_storage_command(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
command : StorageCommand,
) -> Unit raise Error {
match command {
StorageCommand::Init(args) => storage_handle_init(fs, rfs, root, args)
StorageCommand::Add(args) => storage_handle_add(fs, rfs, root, args)
StorageCommand::Commit(args) => storage_handle_commit(fs, rfs, root, args)
StorageCommand::Status(args) => storage_handle_status(rfs, root, args)
StorageCommand::Log(args) => storage_handle_log(rfs, root, args)
StorageCommand::HashObject(args) =>
storage_handle_hash_object(fs, rfs, root, args)
StorageCommand::WriteTree(args) =>
storage_handle_write_tree(fs, rfs, root, args)
StorageCommand::UpdateRef(args) =>
storage_handle_update_ref(fs, rfs, root, args)
}
}
///|
fn storage_trim_string(s : String) -> String {
@string_utils.trim_string(s)
}
///|
fn storage_decode_bytes(data : Bytes) -> String {
@utf8.decode_lossy(data[:])
}
///|
fn storage_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("/") {
if result.length() == 0 {
"/"
} else {
"/" + result
}
} else if result.length() == 0 {
"."
} else {
result
}
}
///|
fn storage_is_bare_repo_dir(rfs : &@bit.RepoFileSystem, path : String) -> Bool {
rfs.is_file(path + "/HEAD") && rfs.is_dir(path + "/objects")
}
///|
fn storage_default_repo_marker_path(
rfs : &@bit.RepoFileSystem,
root : String,
) -> String {
if storage_is_bare_repo_dir(rfs, root) {
root
} else {
root + "/.git"
}
}
///|
fn storage_resolve_git_dir(rfs : &@bit.RepoFileSystem, root : String) -> String {
let default_git_dir = storage_default_repo_marker_path(rfs, root)
match @sys.get_env_var("GIT_DIR") {
Some(dir) => {
let resolved = if dir.has_prefix("/") {
dir
} else {
storage_normalize_path(root + "/" + dir)
}
if rfs.is_file(resolved) {
@bitlib.resolve_gitdir(rfs, resolved)
} else if rfs.is_dir(resolved) {
resolved
} else if storage_is_bare_repo_dir(rfs, root) {
root
} else {
resolved
}
}
None =>
if rfs.is_file(default_git_dir) {
@bitlib.resolve_gitdir(rfs, default_git_dir)
} else if rfs.is_dir(default_git_dir) {
default_git_dir
} else if storage_is_bare_repo_dir(rfs, root) {
root
} else {
default_git_dir
}
}
}
///|
fn storage_parse_git_env_timestamp(raw : String) -> Int64? {
let mut token : String? = None
for part_view in raw.split(" ") {
let part = part_view.to_owned()
if part.length() > 0 {
token = Some(part)
break
}
}
match token {
Some(part) =>
if part.has_prefix("@") {
let numeric = String::unsafe_substring(part, start=1, end=part.length())
let parsed = @string.parse_int64(numeric) catch { _ => return None }
Some(parsed)
} else {
// Plain integer epoch first; otherwise fall back to a human-readable
// ISO date such as "2005-05-26 23:30" (git accepts these in
// GIT_AUTHOR_DATE / GIT_COMMITTER_DATE).
let parsed = @string.parse_int64(part) catch { _ => -1L }
if parsed >= 0L {
return Some(parsed)
}
storage_parse_iso_date_timestamp(raw)
}
None => None
}
}
///|
/// Parse a human-readable ISO date into a Unix timestamp (seconds).
/// Formats: "2005-05-26 23:30", "2005-05-26T23:00", "2005-05-26 23:00:30",
/// optionally followed by a timezone offset such as " -0500".
fn storage_parse_iso_date_timestamp(raw : String) -> Int64? {
let s = raw.trim().to_owned()
if s.length() < 16 {
return None
}
if s[4] != '-' || s[7] != '-' {
return None
}
let sep = s[10]
if sep != ' ' && sep != 'T' {
return None
}
let year = @string.parse_int64(String::unsafe_substring(s, start=0, end=4)) catch {
_ => return None
}
let month = @string.parse_int64(String::unsafe_substring(s, start=5, end=7)) catch {
_ => return None
}
let day = @string.parse_int64(String::unsafe_substring(s, start=8, end=10)) catch {
_ => return None
}
let time_str = String::unsafe_substring(s, start=11, end=s.length())
let mut time_end = time_str.length()
for i = 0; i < time_str.length(); i = i + 1 {
let c = time_str[i]
if c == ' ' || c == '+' || c == '-' {
time_end = i
break
}
}
let time_part = String::unsafe_substring(time_str, start=0, end=time_end)
if time_part.length() < 5 || time_part[2] != ':' {
return None
}
let hour = @string.parse_int64(
String::unsafe_substring(time_part, start=0, end=2),
) catch {
_ => return None
}
let minute = @string.parse_int64(
String::unsafe_substring(time_part, start=3, end=5),
) catch {
_ => return None
}
let second = if time_part.length() >= 8 && time_part[5] == ':' {
@string.parse_int64(String::unsafe_substring(time_part, start=6, end=8)) catch {
_ => 0L
}
} else {
0L
}
let days = storage_days_from_epoch(year, month, day)
let mut ts = days * 86400L + hour * 3600L + minute * 60L + second
let after_time = String::unsafe_substring(
s,
start=11 + time_end,
end=s.length(),
)
ts = ts - storage_parse_iso_tz_offset_seconds(after_time)
Some(ts)
}
///|
fn storage_parse_iso_tz_offset_seconds(s : String) -> Int64 {
let t = s.trim().to_owned()
if t.length() < 5 {
return 0L
}
let sign : Int64 = if t[0] == '-' {
-1L
} else if t[0] == '+' {
1L
} else {
return 0L
}
let (hh_str, mm_str) = if t.length() >= 6 && t[3] == ':' {
(
String::unsafe_substring(t, start=1, end=3),
String::unsafe_substring(t, start=4, end=6),
)
} else {
(
String::unsafe_substring(t, start=1, end=3),
String::unsafe_substring(t, start=3, end=5),
)
}
let hh = @string.parse_int64(hh_str) catch { _ => return 0L }
let mm = @string.parse_int64(mm_str) catch { _ => return 0L }
sign * (hh * 3600L + mm * 60L)
}
///|
fn storage_days_from_epoch(year : Int64, month : Int64, day : Int64) -> Int64 {
let month_days : Array[Int64] = [
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
]
let y = year
let m = month
let mut days = 365L * (y - 1970L)
if y > 1970L {
days = days + (y - 1969L) / 4L - (y - 1901L) / 100L + (y - 1601L) / 400L
} else {
days = days - (1970L - y) / 4L + (1970L - y) / 100L - (1970L - y) / 400L
}
days = days + month_days[(m - 1L).to_int()]
if m > 2L {
let is_leap = (y % 4L == 0L && y % 100L != 0L) || y % 400L == 0L
if is_leap {
days = days + 1L
}
}
days + day - 1L
}
///|
fn storage_get_current_timestamp() -> Int64 {
// @env.now() returns milliseconds as UInt64; convert to seconds as Int64
let ms = @env.now()
(ms / 1000UL).reinterpret_as_int64()
}
///|
fn storage_get_author_string() -> String {
match @sys.get_env_var("GIT_AUTHOR_NAME") {
Some(name) => {
let email = @sys.get_env_var("GIT_AUTHOR_EMAIL").unwrap_or("unknown")
name + " <" + email + ">"
}
None => storage_get_user_from_config()
}
}
///|
fn storage_get_user_from_config() -> String {
// env BIT_COMMIT_AUTHOR is set by cmd/bit commit handler
// to pass resolved author from git config
match @sys.get_env_var("BIT_COMMIT_AUTHOR") {
Some(a) if a.length() > 0 => return a
_ => ()
}
match @sys.get_env_var("USER") {
Some(user) => user + " <" + user + "@localhost>"
None => "Unknown "
}
}
///|
fn storage_get_committer_string() -> String {
match @sys.get_env_var("GIT_COMMITTER_NAME") {
Some(name) => {
let email = @sys.get_env_var("GIT_COMMITTER_EMAIL").unwrap_or("unknown")
name + " <" + email + ">"
}
None =>
// env BIT_COMMIT_COMMITTER is set by cmd/bit commit handler
match @sys.get_env_var("BIT_COMMIT_COMMITTER") {
Some(c) if c.length() > 0 => c
_ => storage_get_author_string()
}
}
}
///|
fn storage_get_commit_timestamp() -> Int64 {
match @sys.get_env_var("GIT_COMMITTER_DATE") {
Some(raw) =>
match storage_parse_git_env_timestamp(raw) {
Some(ts) => ts
None => storage_get_current_timestamp()
}
None => storage_get_current_timestamp()
}
}
///|
fn storage_get_author_timestamp() -> Int64 {
match @sys.get_env_var("GIT_AUTHOR_DATE") {
Some(raw) =>
match storage_parse_git_env_timestamp(raw) {
Some(ts) => ts
None => storage_get_commit_timestamp()
}
None => storage_get_commit_timestamp()
}
}
///|
fn storage_is_valid_timezone_token(token : String) -> Bool {
if token.length() != 5 {
return false
}
let sign = token[0]
if sign != '+' && sign != '-' {
return false
}
for i in 1..<5 {
let ch = token[i]
if ch < '0' || ch > '9' {
return false
}
}
true
}
///|
fn storage_parse_git_env_timezone(raw : String) -> String? {
let fields : Array[String] = []
for part_view in raw.split(" ") {
let part = part_view.to_owned()
if part.length() > 0 {
fields.push(part)
}
}
if fields.length() < 2 {
return None
}
let tz = fields[fields.length() - 1]
if storage_is_valid_timezone_token(tz) {
Some(tz)
} else {
None
}
}
///|
fn storage_get_commit_timezone() -> String {
match @sys.get_env_var("GIT_COMMITTER_DATE") {
Some(raw) =>
match storage_parse_git_env_timezone(raw) {
Some(tz) => tz
None => "+0000"
}
None => "+0000"
}
}
///|
fn storage_get_author_timezone() -> String {
match @sys.get_env_var("GIT_AUTHOR_DATE") {
Some(raw) =>
match storage_parse_git_env_timezone(raw) {
Some(tz) => tz
None => storage_get_commit_timezone()
}
None => storage_get_commit_timezone()
}
}
///|
fn storage_hash_object_validate_content(
obj_type : @bit.ObjectType,
content : Bytes,
) -> Unit raise @bit.GitError {
match obj_type {
@bit.ObjectType::Commit => {
let text = @utf8.decode_lossy(content[:])
if !text.has_prefix("tree ") {
raise @bit.GitError::InvalidObject("corrupt commit")
}
}
@bit.ObjectType::Tag => {
let text = @utf8.decode_lossy(content[:])
if !text.has_prefix("object ") {
raise @bit.GitError::InvalidObject("corrupt tag")
}
}
_ => ()
}
}
///|
fn storage_read_all_stdin() -> Bytes raise Error {
raise @bit.GitError::InvalidObject(
"hash-object: stdin is not supported in storage runtime",
)
}
///|
fn storage_is_reftable_repo(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> Bool {
if rfs.is_dir(git_dir + "/reftable") {
return true
}
match
@bitlib.read_config_value(
rfs,
git_dir + "/config",
"extensions",
"refstorage",
) {
Some(value) => value.to_lower() == "reftable"
None => false
}
}
///|
fn storage_parent_dir(path : String) -> String {
if path.length() == 0 || path == "/" {
return "/"
}
match path.rev_find("/") {
None => ""
Some(0) => "/"
Some(i) => String::unsafe_substring(path, start=0, end=i)
}
}
///|
fn storage_read_ref_value(
rfs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
) -> @bit.ObjectId? {
storage_read_ref_value_inner(rfs, git_dir, refname, 0)
}
///|
fn storage_read_ref_value_inner(
rfs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
depth : Int,
) -> @bit.ObjectId? {
if depth > 8 {
return None
}
let ref_path = git_dir + "/" + refname
if rfs.is_file(ref_path) {
let content = storage_decode_bytes(rfs.read_file(ref_path)) catch {
_ => return None
}
let hex = storage_trim_string(content)
if hex.has_prefix("ref: ") {
let target = String::unsafe_substring(hex, start=5, end=hex.length())
return storage_read_ref_value_inner(rfs, git_dir, target, depth + 1)
}
(@bit.ObjectId::from_hex(hex) |> Some) catch {
_ => None
}
} else {
@bitlib.resolve_ref(rfs, git_dir, refname) catch {
_ => None
}
}
}
///|
fn storage_fail_unsupported_mode(
cmd : String,
detail : String,
) -> Unit raise Error {
raise @bit.GitError::InvalidObject(
cmd + ": mode is not supported in storage runtime: " + detail,
)
}
///|
fn storage_runtime_is_quiet() -> Bool {
match @sys.get_env_var("BIT_STORAGE_RUNTIME_QUIET") {
Some(value) => {
let normalized = storage_trim_string(value).to_lower()
normalized == "1" || normalized == "true" || normalized == "yes"
}
None => false
}
}
///|
fn storage_print_line(text : String) -> Unit {
if storage_runtime_is_quiet() {
return
}
println(text)
}
///|
fn storage_handle_init(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
args : StorageInitArgs,
) -> Unit raise Error {
let ref_format = args.ref_format.to_lower()
let object_format = args.object_format.to_lower()
if ref_format != "files" {
storage_fail_unsupported_mode("init", "--ref-format=" + ref_format)
}
if object_format != "sha1" {
storage_fail_unsupported_mode("init", "--object-format=" + object_format)
}
let default_branch = if args.default_branch.length() == 0 {
"main"
} else {
args.default_branch
}
let opts : @bitlib.InitOptions = {
default_branch,
bare: false,
separate_git_dir: None,
template_dir: None,
ref_format,
object_format,
is_reinit: false,
git_dir: None,
work_tree: None,
shared: None,
}
@bitrepo.init_repo_with_options_with_repo_fs(fs, rfs, root, opts)
if !args.quiet {
let git_dir = storage_resolve_git_dir(rfs, root)
storage_print_line("Initialized empty Git repository in " + git_dir + "/")
}
}
///|
fn storage_handle_add(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
args : StorageAddArgs,
) -> Unit raise Error {
let git_dir = storage_resolve_git_dir(rfs, root)
let algo = storage_resolve_hash_algorithm(rfs, git_dir, "")
if args.add_all {
let add_all_paths = storage_collect_add_all_paths(rfs, root)
if add_all_paths.length() > 0 {
@bitlib.add_paths(fs, rfs, root, add_all_paths, algo~)
}
return
}
if args.paths.length() == 0 {
raise @bit.GitError::InvalidObject("add requires pathspec")
}
@bitlib.add_paths(fs, rfs, root, args.paths, algo~)
}
///|
fn storage_collect_add_all_paths(
rfs : &@bit.RepoFileSystem,
root : String,
) -> Array[String] raise Error {
let snapshot = storage_collect_status_snapshot(rfs, root)
let paths : Array[String] = []
for p in snapshot.untracked {
paths.push(p)
}
for p in snapshot.unstaged_modified {
paths.push(p)
}
for p in snapshot.unstaged_deleted {
paths.push(p)
}
paths
}
///|
fn storage_collect_commit_all_modified_paths(
rfs : &@bit.RepoFileSystem,
root : String,
) -> Array[String] raise Error {
let snapshot = storage_collect_status_snapshot(rfs, root)
let paths : Array[String] = []
for p in snapshot.unstaged_modified {
paths.push(p)
}
for p in snapshot.unstaged_deleted {
paths.push(p)
}
paths
}
///|
fn storage_handle_commit(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
args : StorageCommitArgs,
) -> Unit raise Error {
let git_dir = storage_resolve_git_dir(rfs, root)
let algo = storage_resolve_hash_algorithm(rfs, git_dir, "")
guard args.message.length() > 0 else {
raise @bit.GitError::InvalidObject(
"storage runtime commit requires -m/--message",
)
}
if args.all_modified {
let all_modified_paths = storage_collect_commit_all_modified_paths(
rfs, root,
)
if all_modified_paths.length() > 0 {
@bitlib.add_paths(fs, rfs, root, all_modified_paths)
}
}
let author_timestamp = storage_get_author_timestamp()
let author_timezone = storage_get_author_timezone()
let timestamp = storage_get_commit_timestamp()
let timezone = storage_get_commit_timezone()
let committer = storage_get_committer_string()
let committer_timestamp = timestamp
let commit_id = @bitlib.commit(
fs,
rfs,
root,
args.message,
storage_get_author_string(),
author_timestamp,
committer~,
committer_timestamp~,
allow_empty=args.allow_empty,
timezone~,
encoding=args.encoding,
author_timezone=Some(author_timezone),
committer_timezone=Some(timezone),
algo~,
)
if !args.quiet {
let short_id = String::unsafe_substring(commit_id.to_hex(), start=0, end=7)
let first_line = match args.message.find("\n") {
Some(idx) => String::unsafe_substring(args.message, start=0, end=idx)
None => args.message
}
storage_print_line("[" + short_id + "] " + first_line)
}
}
///|
priv struct StorageStatusSnapshot {
staged_added : Array[String]
staged_modified : Array[String]
staged_deleted : Array[String]
unstaged_modified : Array[String]
unstaged_deleted : Array[String]
untracked : Array[String]
}
///|
fn storage_parse_octal_mode(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
}
///|
fn storage_is_tree_mode(mode : String) -> Bool {
mode == "040000" || mode == "40000"
}
///|
fn storage_file_mode_kind(mode : Int) -> Int {
mode & 0o170000
}
///|
fn storage_resolve_common_git_dir(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> String {
let commondir_path = git_dir + "/commondir"
if !rfs.is_file(commondir_path) {
return git_dir
}
let raw = storage_decode_bytes(
rfs.read_file(commondir_path) catch {
_ => Default::default()
},
)
let rel = storage_trim_string(raw)
if rel.length() == 0 {
return git_dir
}
if rel.has_prefix("/") {
storage_normalize_path(rel)
} else {
storage_normalize_path(git_dir + "/" + rel)
}
}
///|
fn storage_collect_staged_changes_db(
db : @bitlib.ObjectDb,
rfs : &@bit.RepoFileSystem,
tree_id : @bit.ObjectId,
prefix : String,
index_map : Map[String, @bitlib.IndexEntry],
staged_modified : Array[String],
staged_deleted : Array[String],
) -> Unit raise Error {
let tree_obj = db.get(rfs, 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 storage_is_tree_mode(entry.mode) {
storage_collect_staged_changes_db(
db,
rfs,
entry.id,
path,
index_map,
staged_modified,
staged_deleted,
)
} else {
let mode = storage_parse_octal_mode(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 storage_collect_staged_changes_from_head(
rfs : &@bit.RepoFileSystem,
git_dir : String,
index_map : Map[String, @bitlib.IndexEntry],
staged_modified : Array[String],
staged_deleted : Array[String],
) -> Unit raise Error {
match @bitlib.resolve_head_commit(rfs, git_dir) {
None => ()
Some(commit_id) => {
let common_git_dir = storage_resolve_common_git_dir(rfs, git_dir)
let db = @bitlib.ObjectDb::load_lazy(rfs, common_git_dir)
match db.get(rfs, commit_id) {
None => ()
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)
storage_collect_staged_changes_db(
db,
rfs,
info.tree,
"",
index_map,
staged_modified,
staged_deleted,
)
}
}
}
}
}
///|
fn storage_collect_status_snapshot(
rfs : &@bit.RepoFileSystem,
root : String,
) -> StorageStatusSnapshot raise Error {
let git_dir = storage_resolve_git_dir(rfs, root)
let common_git_dir = storage_resolve_common_git_dir(rfs, git_dir)
let core_filemode = @bitlib.core_filemode_enabled(rfs, common_git_dir)
let entries = @bitlib.read_index_entries(rfs, git_dir)
let index_map : Map[String, @bitlib.IndexEntry] = Map([])
for entry in entries {
index_map[entry.path] = entry
}
let files = @bitlib.list_working_files(rfs, root)
files.sort()
let visited : Map[String, Bool] = Map([])
let untracked : Array[String] = []
let unstaged_modified : Array[String] = []
for rel_path in files {
visited[rel_path] = true
match index_map.get(rel_path) {
Some(index_entry) => {
let abs_path = root + "/" + rel_path
let mut changed = false
let content : Bytes? = match
@io.worktree_entry_meta_sync(rfs, abs_path) {
None => None
Some(info) => {
let kind_changed = storage_file_mode_kind(info.mode) !=
storage_file_mode_kind(index_entry.mode)
if kind_changed || (core_filemode && info.mode != index_entry.mode) {
changed = true
}
match info.kind {
@io.WorktreeKindMeta::Regular => {
let bytes = rfs.read_file(abs_path) catch {
_ => {
changed = true
Bytes::new(0)
}
}
Some(bytes)
}
@io.WorktreeKindMeta::Symlink =>
@io.read_symlink_target_path(abs_path).map(fn(target) {
@utf8.encode(target)
})
}
}
}
match content {
Some(bytes) =>
if @bit.hash_blob(bytes) != index_entry.id {
changed = true
}
None => changed = true
}
if changed {
unstaged_modified.push(rel_path)
}
}
None => untracked.push(rel_path)
}
}
let unstaged_deleted : Array[String] = []
for entry in entries {
if visited.get(entry.path) is Some(_) {
continue
}
let abs_path = root + "/" + entry.path
if !rfs.is_file(abs_path) {
unstaged_deleted.push(entry.path)
}
}
let staged_index_map : Map[String, @bitlib.IndexEntry] = Map([])
for entry in entries {
staged_index_map[entry.path] = entry
}
let staged_modified : Array[String] = []
let staged_deleted : Array[String] = []
storage_collect_staged_changes_from_head(
rfs, git_dir, staged_index_map, staged_modified, staged_deleted,
)
let staged_added : Array[String] = []
for path in staged_index_map.keys() {
staged_added.push(path)
}
staged_added.sort()
staged_modified.sort()
staged_deleted.sort()
unstaged_modified.sort()
unstaged_deleted.sort()
untracked.sort()
let snapshot : StorageStatusSnapshot = {
staged_added,
staged_modified,
staged_deleted,
unstaged_modified,
unstaged_deleted,
untracked,
}
snapshot
}
///|
fn storage_status_porcelain_branch_line(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> String raise Error {
match @bitlib.read_head_ref(rfs, git_dir) {
@bitlib.HeadRef::Branch(name) => "## " + name
@bitlib.HeadRef::Detached(id) => {
let short_id = String::unsafe_substring(id.to_hex(), start=0, end=7)
"## HEAD (detached at " + short_id + ")"
}
}
}
///|
fn storage_status_porcelain_lines(
rfs : &@bit.RepoFileSystem,
root : String,
show_branch : Bool,
) -> Array[String] raise Error {
let git_dir = storage_resolve_git_dir(rfs, root)
let out : Array[String] = []
if show_branch {
out.push(storage_status_porcelain_branch_line(rfs, git_dir))
}
let status = storage_collect_status_snapshot(rfs, root)
let xmap : Map[String, Char] = Map([])
let ymap : Map[String, Char] = Map([])
for p in status.staged_added {
xmap[p] = 'A'
}
for p in status.staged_modified {
xmap[p] = 'M'
}
for p in status.staged_deleted {
xmap[p] = 'D'
}
for p in status.unstaged_modified {
ymap[p] = 'M'
}
for p in status.unstaged_deleted {
ymap[p] = 'D'
}
let paths : Map[String, Bool] = Map([])
for p in status.staged_added {
paths[p] = true
}
for p in status.staged_modified {
paths[p] = true
}
for p in status.staged_deleted {
paths[p] = true
}
for p in status.unstaged_modified {
paths[p] = true
}
for p in status.unstaged_deleted {
paths[p] = true
}
let lines : Array[String] = []
let merged_paths = paths.keys().to_array()
merged_paths.sort()
for path in merged_paths {
let x = xmap.get(path).unwrap_or(' ')
let y = ymap.get(path).unwrap_or(' ')
lines.push("\{x}\{y} \{path}")
}
for path in status.untracked {
lines.push("?? \{path}")
}
for line in lines {
out.push(line)
}
out
}
///|
fn storage_status_text_lines(
rfs : &@bit.RepoFileSystem,
root : String,
) -> Array[String] raise Error {
let git_dir = storage_resolve_git_dir(rfs, root)
let out : Array[String] = []
match @bitlib.read_head_ref(rfs, git_dir) {
@bitlib.HeadRef::Branch(name) => out.push("On branch " + name)
@bitlib.HeadRef::Detached(id) => {
let short_id = String::unsafe_substring(id.to_hex(), start=0, end=7)
out.push("HEAD detached at " + short_id)
}
}
let status = storage_collect_status_snapshot(rfs, root)
let has_staged = status.staged_added.length() > 0 ||
status.staged_modified.length() > 0 ||
status.staged_deleted.length() > 0
let has_unstaged = status.unstaged_modified.length() > 0 ||
status.unstaged_deleted.length() > 0
if has_staged {
out.push("")
out.push("Changes to be committed:")
for path in status.staged_added {
out.push(" staged: " + path)
}
for path in status.staged_modified {
out.push(" staged: " + path)
}
for path in status.staged_deleted {
out.push(" staged: " + path)
}
}
if has_unstaged {
out.push("")
out.push("Changes not staged for commit:")
for path in status.unstaged_modified {
out.push(" modified: " + path)
}
for path in status.unstaged_deleted {
out.push(" deleted: " + path)
}
}
if status.untracked.length() > 0 {
out.push("")
out.push("Untracked files:")
for path in status.untracked {
out.push(" " + path)
}
}
if !has_staged && !has_unstaged && status.untracked.length() == 0 {
out.push("")
out.push("nothing to commit, working tree clean")
}
out
}
///|
fn storage_handle_status(
rfs : &@bit.RepoFileSystem,
root : String,
args : StorageStatusArgs,
) -> Unit raise Error {
let lines = if args.porcelain || args.short {
storage_status_porcelain_lines(rfs, root, args.show_branch)
} else {
storage_status_text_lines(rfs, root)
}
for line in lines {
storage_print_line(line)
}
}
///|
fn storage_handle_log(
rfs : &@bit.RepoFileSystem,
root : String,
args : StorageLogArgs,
) -> Unit raise Error {
let git_dir = storage_resolve_git_dir(rfs, root)
if args.oneline {
let lines = @bitlib.log_head_oneline(rfs, git_dir, max_count=args.max_count)
for line in lines {
storage_print_line(line)
}
return
}
let entries = @bitlib.log_head(rfs, git_dir, max_count=args.max_count)
for entry in entries {
storage_print_line("commit " + entry.id.to_hex())
storage_print_line("Author: " + entry.author)
storage_print_line("")
storage_print_line(" " + entry.message)
storage_print_line("")
}
}
///|
fn storage_handle_hash_object(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
args : StorageHashObjectArgs,
) -> Unit raise Error {
let cwd = match @sys.get_env_var("GIT_SHIM_PWD") {
Some(path) => path
None =>
match @env.current_dir() {
Some(path) => path
None => root
}
}
let git_dir = if args.write_object {
storage_resolve_git_dir(rfs, root)
} else {
root + "/.git"
}
// compatObjectFormat is ignored — we write sha1 objects regardless.
ignore(storage_read_compat_object_format(rfs, git_dir))
let algo = storage_resolve_hash_algorithm(rfs, git_dir, args.object_format)
if args.stdin_mode {
let content = storage_read_all_stdin()
if !args.literally {
storage_hash_object_validate_content(args.obj_type, content)
}
let id = @object.hash_object_content_with_algo(algo, args.obj_type, content)
if args.write_object {
let (_, compressed) = @object.create_object_with_algo(
algo,
args.obj_type,
content,
)
@bitlib.write_object_bytes(fs, git_dir, id, compressed)
}
storage_print_line(id.to_hex())
if args.paths.length() == 0 && !args.stdin_paths {
return
}
}
if args.stdin_paths {
let stdin_content = storage_read_all_stdin()
let text = @string_utils.decode_bytes(stdin_content)
let autocrlf = @bitlib.read_autocrlf_setting(rfs, git_dir)
for line_view in text.split("\n") {
let line = line_view.to_owned()
if line.length() == 0 {
continue
}
let abs = if line.has_prefix("/") {
storage_normalize_path(line)
} else {
storage_normalize_path(cwd + "/" + line)
}
let content = rfs.read_file(abs)
let prefix_len = root.length() + 1
let rel_path = if abs.has_prefix(root + "/") {
abs[prefix_len:].to_owned()
} else {
abs
}
let attrs = if args.no_filters {
@bitlib.resolve_eol_attrs(rfs, root, "")
} else {
@bitlib.resolve_eol_attrs(rfs, root, rel_path)
}
let normalized = if args.no_filters {
content
} else {
@bitlib.clean_for_storage(content, attrs, autocrlf)
}
let id = @object.hash_object_content_with_algo(
algo,
args.obj_type,
normalized,
)
if args.write_object {
let (_, compressed) = @object.create_object_with_algo(
algo,
args.obj_type,
normalized,
)
@bitlib.write_object_bytes(fs, git_dir, id, compressed)
}
storage_print_line(id.to_hex())
}
return
}
let autocrlf = @bitlib.read_autocrlf_setting(rfs, git_dir)
for path in args.paths {
let abs = if path.has_prefix("/") {
storage_normalize_path(path)
} else {
storage_normalize_path(cwd + "/" + path)
}
let content = rfs.read_file(abs)
let prefix_len = root.length() + 1
let rel_path = if abs.has_prefix(root + "/") {
abs[prefix_len:].to_owned()
} else {
abs
}
let filter_rel = if args.path_hint.length() > 0 {
args.path_hint
} else {
rel_path
}
let attrs = if args.no_filters {
@bitlib.resolve_eol_attrs(rfs, root, "")
} else {
@bitlib.resolve_eol_attrs(rfs, root, filter_rel)
}
let normalized = if args.no_filters {
content
} else {
@bitlib.clean_for_storage(content, attrs, autocrlf)
}
if !args.literally {
storage_hash_object_validate_content(args.obj_type, normalized)
}
let id = @object.hash_object_content_with_algo(
algo,
args.obj_type,
normalized,
)
if args.write_object {
let (_, compressed) = @object.create_object_with_algo(
algo,
args.obj_type,
normalized,
)
@bitlib.write_object_bytes(fs, git_dir, id, compressed)
}
storage_print_line(id.to_hex())
}
}
///|
fn storage_read_compat_object_format(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> String? {
let overrides = @bitlib.parse_config_overrides()
match overrides.get("extensions.compatobjectformat") {
Some(value) => Some(value)
None =>
@bitlib.read_config_value(
rfs,
git_dir + "/config",
"extensions",
"compatobjectformat",
)
}
}
///|
fn storage_resolve_hash_algorithm(
rfs : &@bit.RepoFileSystem,
git_dir : String,
explicit_format : String,
) -> @object.HashAlgorithm {
if explicit_format.length() > 0 {
return if explicit_format.to_lower() == "sha256" {
@object.HashAlgorithm::Sha256
} else {
@object.HashAlgorithm::Sha1
}
}
match
@bitlib.read_config_value(
rfs,
git_dir + "/config",
"extensions",
"objectformat",
) {
Some(value) =>
if value.to_lower() == "sha256" {
@object.HashAlgorithm::Sha256
} else {
@object.HashAlgorithm::Sha1
}
None => @object.HashAlgorithm::Sha1
}
}
///|
fn storage_handle_write_tree(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
args : StorageWriteTreeArgs,
) -> Unit raise Error {
let git_dir = storage_resolve_git_dir(rfs, root)
let algo = storage_resolve_hash_algorithm(rfs, git_dir, "")
let entries = @bitlib.read_index_entries(rfs, git_dir)
let tree_id = @bitlib.write_tree_from_index(
fs,
rfs,
git_dir,
entries,
prefix=args.prefix,
missing_ok=args.missing_ok,
algo~,
)
storage_print_line(tree_id.to_hex())
}
///|
fn storage_handle_update_ref(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
args : StorageUpdateRefArgs,
) -> Unit raise Error {
let git_dir = storage_resolve_git_dir(rfs, root)
if storage_is_reftable_repo(rfs, git_dir) {
storage_fail_unsupported_mode("update-ref", "reftable repository")
}
if args.delete_mode {
storage_verify_old_ref_value(rfs, git_dir, args.refname, args.old_value)
let ref_path = git_dir + "/" + args.refname
if rfs.is_file(ref_path) {
fs.remove_file(ref_path)
}
return
}
guard args.new_value is Some(raw_new_value) else {
raise @bit.GitError::InvalidObject(
"usage: update-ref []",
)
}
let new_value = if raw_new_value == "@" { "HEAD" } else { raw_new_value }
storage_verify_old_ref_value(rfs, git_dir, args.refname, args.old_value)
let new_id = storage_resolve_object_id(rfs, git_dir, new_value)
let ref_path = git_dir + "/" + args.refname
let dir = storage_parent_dir(ref_path)
if dir.length() > 0 && !rfs.is_dir(dir) {
fs.mkdir_p(dir)
}
fs.write_string(ref_path, new_id.to_hex() + "\n")
}
///|
fn storage_verify_old_ref_value(
rfs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
old_value : String?,
) -> Unit raise Error {
match old_value {
Some(expected_hex) => {
let expected = @bit.ObjectId::from_hex(expected_hex)
let current = storage_read_ref_value(rfs, git_dir, refname)
match current {
Some(cur_id) =>
if cur_id.to_hex() != expected.to_hex() {
raise @bit.GitError::InvalidObject(
"update-ref oldvalue mismatch for " + refname,
)
}
None =>
if expected.to_hex() != @bit.ObjectId::zero().to_hex() {
raise @bit.GitError::InvalidObject(
"update-ref expected existing ref: " + refname,
)
}
}
}
None => ()
}
}
///|
fn storage_resolve_object_id(
rfs : &@bit.RepoFileSystem,
git_dir : String,
spec : String,
) -> @bit.ObjectId raise Error {
@bit.ObjectId::from_hex(spec) catch {
_ =>
match @bitrepo.rev_parse(rfs, git_dir, spec) {
Some(id) => id
None => raise @bit.GitError::InvalidObject("unknown revision: " + spec)
}
}
}