///| Remote helpers for CLI/core sharing
///|
fn remote_decode_bytes_lossy(data : Bytes) -> String {
@utf8.decode_lossy(data[:])
}
///|
fn remote_trim_string(s : String) -> String {
@string_utils.trim_string(s)
}
///|
pub fn is_valid_remote_name(name : String) -> Bool {
if name.length() == 0 {
return false
}
if name.has_suffix("/") || name.has_suffix(".lock") {
return false
}
if name.contains("..") || name.contains("@{") {
return false
}
let invalid = [" ", "\t", "\n", "\r", "~", "^", ":", "?", "*", "[", "\\"]
for bad in invalid {
if name.contains(bad) {
return false
}
}
true
}
///|
fn compare_strings_lex(a : String, b : String) -> Int {
if a == b {
return 0
}
let a_chars : Array[Char] = []
for c in a {
a_chars.push(c)
}
let b_chars : Array[Char] = []
for c in b {
b_chars.push(c)
}
let mut i = 0
let alen = a_chars.length()
let blen = b_chars.length()
while i < alen && i < blen {
let ac = a_chars[i]
let bc = b_chars[i]
if ac < bc {
return -1
}
if ac > bc {
return 1
}
i += 1
}
if alen < blen {
-1
} else if alen > blen {
1
} else {
0
}
}
///|
pub fn sort_strings_lex(items : Array[String]) -> Unit {
let n = items.length()
if n < 2 {
return ()
}
for i in 0.. 0 {
let tmp = items[i]
items[i] = items[j]
items[j] = tmp
}
}
}
}
///|
pub fn sort_branch_configs_by_name(items : Array[BranchConfig]) -> Unit {
let n = items.length()
if n < 2 {
return ()
}
for i in 0.. 0 {
let tmp = items[i]
items[i] = items[j]
items[j] = tmp
}
}
}
}
///|
pub fn collect_local_branches(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> Array[String] {
let out : Array[String] = []
let refs = list_refs_with_ids(fs, git_dir, Some("refs/heads/"))
for name, _ in refs {
let short = String::unsafe_substring(name, start=11, end=name.length())
out.push(short)
}
out.sort()
out
}
///|
pub fn is_remote_namespace(dst : String, remote_name : String) -> Bool {
let prefix = "refs/remotes/" + remote_name + "/"
dst.has_prefix(prefix) || dst == "refs/remotes/" + remote_name
}
///|
pub fn format_remove_warning(branches : Array[String]) -> String {
if branches.length() == 0 {
return ""
}
let lines : Array[String] = []
if branches.length() == 1 {
lines.push(
"Note: A branch outside the refs/remotes/ hierarchy was not removed;",
)
lines.push("to delete it, use:")
} else {
lines.push(
"Note: Some branches outside the refs/remotes/ hierarchy were not removed;",
)
lines.push("to delete them, use:")
}
for name in branches {
lines.push(" git branch -d " + name)
}
lines.join("\n") + "\n"
}
///|
pub fn split_whitespace_all(s : String) -> Array[String] {
let result : Array[String] = []
let current = StringBuilder::new()
let mut in_word = false
for c in s {
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
if in_word {
result.push(current.to_string())
current.reset()
in_word = false
}
} else {
current.write_char(c)
in_word = true
}
}
if in_word {
result.push(current.to_string())
}
result
}
///|
fn parse_bool_value(value : String) -> Bool? {
let lower = value.to_lower()
if lower == "true" || lower == "1" || lower == "yes" {
Some(true)
} else if lower == "false" || lower == "0" || lower == "no" {
Some(false)
} else {
None
}
}
///|
pub fn remote_skip_default_update(
blocks : Array[ConfigBlock],
name : String,
) -> Bool {
for block in blocks {
if block.section == Some("remote") && block.name == Some(name) {
for line in block.lines {
match parse_config_kv(line) {
Some((key, value)) if key == "skipdefaultupdate" =>
return parse_bool_value(value).unwrap_or(false)
_ => ()
}
}
}
}
false
}
///|
pub enum RemoteHeadState {
Missing
Detached(String)
Symref(String)
Other(String)
}
///|
pub fn read_local_remote_head_state(
fs : &@bit.RepoFileSystem,
head_path : String,
remote_name : String,
) -> RemoteHeadState {
if !fs.is_file(head_path) {
return RemoteHeadState::Missing
}
let data = fs.read_file(head_path) catch {
_ => return RemoteHeadState::Missing
}
let line = data |> remote_decode_bytes_lossy |> remote_trim_string
if line.has_prefix("ref: ") {
let target = String::unsafe_substring(line, start=5, end=line.length())
let prefix = "refs/remotes/" + remote_name + "/"
if target.has_prefix(prefix) {
let branch = String::unsafe_substring(
target,
start=prefix.length(),
end=target.length(),
)
RemoteHeadState::Symref(branch)
} else {
RemoteHeadState::Other(target)
}
} else {
RemoteHeadState::Detached(line)
}
}
///|
pub fn read_remote_head_branch(
fs : &@bit.RepoFileSystem,
remote_git_dir : String,
) -> String? {
let head_path = remote_git_dir + "/HEAD"
if !fs.is_file(head_path) {
return None
}
let data = fs.read_file(head_path) catch { _ => return None }
let line = data |> remote_decode_bytes_lossy |> remote_trim_string
if line.has_prefix("ref: ") {
let refname = String::unsafe_substring(line, start=5, end=line.length())
if refname.has_prefix("refs/heads/") {
Some(String::unsafe_substring(refname, start=11, end=refname.length()))
} else {
None
}
} else {
None
}
}
///|
pub fn update_follow_remote_head(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
name : String,
) -> Unit raise @bit.GitError {
let config_path = git_dir + "/config"
guard rfs.is_file(config_path) else { return () }
let content = remote_decode_bytes_lossy(rfs.read_file(config_path))
let blocks = parse_config_blocks(content)
let new_blocks : Array[ConfigBlock] = []
let mut changed = false
for block in blocks {
if block.section == Some("remote") && block.name == Some(name) {
let updated = block
let lines : Array[String] = []
for line in updated.lines {
match parse_config_kv(line) {
Some((key, value)) if key == "followremotehead" =>
if value.to_lower() == "always" {
lines.push("\tfollowRemoteHEAD = warn")
changed = true
} else {
lines.push(line)
}
_ => lines.push(line)
}
}
updated.lines = lines
new_blocks.push(updated)
} else {
new_blocks.push(block)
}
}
if changed {
fs.write_string(config_path, render_config_blocks(new_blocks))
}
}
///|
pub fn read_config_content(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> String {
let config_path = git_dir + "/config"
if rfs.is_file(config_path) {
let data = rfs.read_file(config_path) catch { _ => return "" }
remote_decode_bytes_lossy(data)
} else {
""
}
}
///|
/// Read a key from already-decoded config content. Lets the caller
/// share a single read across multiple key lookups when several
/// `read_config_value` calls happen in a row against the same file
/// (see `rev_parse`'s object-format cluster).
pub fn read_config_value_from_content(
content_str : String,
section : String,
name : String,
) -> String? {
if content_str.length() == 0 {
return None
}
let section_lower = section.to_lower()
let name_lower = name.to_lower()
let mut in_section = false
let lines = split_by_newline(content_str)
for line in lines {
let trimmed = remote_trim_string(line)
if trimmed.has_prefix("[") && trimmed.has_suffix("]") {
let sec = String::unsafe_substring(
trimmed,
start=1,
end=trimmed.length() - 1,
)
in_section = sec.to_lower() == section_lower
} else if in_section {
match trimmed.find("=") {
Some(idx) => {
let k = remote_trim_string(
String::unsafe_substring(trimmed, start=0, end=idx),
)
let v = remote_trim_string(
String::unsafe_substring(
trimmed,
start=idx + 1,
end=trimmed.length(),
),
)
if k.to_lower() == name_lower {
return Some(v)
}
}
None => ()
}
}
}
None
}
///|
pub fn legacy_remote_exists(
rfs : &@bit.RepoFileSystem,
git_dir : String,
name : String,
) -> Bool {
rfs.is_file(git_dir + "/remotes/" + name) ||
rfs.is_file(git_dir + "/branches/" + name)
}
///|
pub fn match_url_pattern(pattern : String, value : String) -> Bool {
if pattern == value {
return true
}
let value_chars : Array[Char] = []
for c in value {
value_chars.push(c)
}
let tokens : Array[(Char, Bool)] = []
for c in pattern {
if c == '+' && tokens.length() > 0 {
let (ch, _repeat) = tokens.pop().unwrap_or(('.', false))
tokens.push((ch, true))
} else {
tokens.push((c, false))
}
}
let mut vi = 0
for token in tokens {
let (ch, repeat) = token
if repeat {
if vi >= value_chars.length() {
return false
}
let mut matched = false
while vi < value_chars.length() {
let vc = value_chars[vi]
if vc != ch {
break
}
matched = true
vi += 1
}
if !matched {
return false
}
} else {
if vi >= value_chars.length() || value_chars[vi] != ch {
return false
}
vi += 1
}
}
vi == value_chars.length()
}
///|
fn split_by_newline(s : String) -> Array[String] {
let result : Array[String] = []
let mut start = 0
let len = s.length()
for i in 0.. String? {
guard fs.is_file(config_path) else { return None }
let content = fs.read_file(config_path) catch { _ => return None }
let content_str = remote_decode_bytes_lossy(content)
// Simple config parsing - look for key = value in section
let section_lower = section.to_lower()
let name_lower = name.to_lower()
let mut in_section = false
let lines = split_by_newline(content_str)
for line in lines {
let trimmed = remote_trim_string(line)
if trimmed.has_prefix("[") && trimmed.has_suffix("]") {
let sec = String::unsafe_substring(
trimmed,
start=1,
end=trimmed.length() - 1,
)
in_section = sec.to_lower() == section_lower
} else if in_section {
match trimmed.find("=") {
Some(idx) => {
let k = remote_trim_string(
String::unsafe_substring(trimmed, start=0, end=idx),
)
let v = remote_trim_string(
String::unsafe_substring(
trimmed,
start=idx + 1,
end=trimmed.length(),
),
)
if k.to_lower() == name_lower {
return Some(v)
}
}
None => ()
}
}
}
None
}
///|
/// Read all values for a given section/key (multi-valued config support).
pub fn read_config_values(
fs : &@bit.RepoFileSystem,
config_path : String,
section : String,
name : String,
) -> Array[String] {
let result : Array[String] = []
guard fs.is_file(config_path) else { return result }
let content = fs.read_file(config_path) catch { _ => return result }
let content_str = remote_decode_bytes_lossy(content)
let section_lower = section.to_lower()
let name_lower = name.to_lower()
let mut in_section = false
let lines = split_by_newline(content_str)
for line in lines {
let trimmed = remote_trim_string(line)
if trimmed.has_prefix("[") && trimmed.has_suffix("]") {
let sec = String::unsafe_substring(
trimmed,
start=1,
end=trimmed.length() - 1,
)
in_section = sec.to_lower() == section_lower
} else if in_section {
match trimmed.find("=") {
Some(idx) => {
let k = remote_trim_string(
String::unsafe_substring(trimmed, start=0, end=idx),
)
let v = remote_trim_string(
String::unsafe_substring(
trimmed,
start=idx + 1,
end=trimmed.length(),
),
)
if k.to_lower() == name_lower {
result.push(v)
}
}
None => ()
}
}
}
result
}
///|
pub fn read_config_bool(
fs : &@bit.RepoFileSystem,
config_path : String,
section : String,
name : String,
) -> Bool? {
match read_config_value(fs, config_path, section, name) {
Some(v) => {
let lower = v.to_lower()
if lower == "true" || lower == "1" || lower == "yes" {
Some(true)
} else if lower == "false" || lower == "0" || lower == "no" {
Some(false)
} else {
None
}
}
None => None
}
}
///|
/// Parse config overrides from GIT_CONFIG_OVERRIDES environment variable
pub fn parse_config_overrides() -> Map[String, String] {
let config : Map[String, String] = Map([])
let overrides = @bitio.env_get("GIT_CONFIG_OVERRIDES")
guard overrides is Some(env_value) else { return config }
// Parse key=value pairs separated by newlines
for line in split_by_newline(env_value) {
if line.length() == 0 {
continue
}
match line.find("=") {
Some(idx) => {
let key = String::unsafe_substring(line, start=0, end=idx).to_lower()
let value = String::unsafe_substring(
line,
start=idx + 1,
end=line.length(),
)
config[key] = value
}
None =>
// Key without = means the key is present but with no value
config[line.to_lower()] = ""
}
}
config
}
///|
pub fn get_protocol_version() -> Int {
let bit_protocol = @bitio.env_get("GIT_PROTOCOL")
match bit_protocol {
Some(val) =>
if val.contains("version=2") {
2
} else if val.contains("version=1") {
1
} else {
0
}
None => 0
}
}
///|
pub fn build_upload_pack_config_with_overrides(
fs : &@bit.RepoFileSystem,
root : String,
overrides : Map[String, String],
) -> UploadPackConfig {
let git_dir = if root.has_suffix(".git") { root } else { root + "/.git" }
let config_path = git_dir + "/config"
// Read allow_filter from file, then check cmdline override
let allow_filter = match overrides.get("uploadpack.allowfilter") {
Some(v) => {
let lower = v.to_lower()
lower == "true" || lower == "1" || lower == "yes"
}
None =>
read_config_bool(fs, config_path, "uploadpack", "allowfilter").unwrap_or(
true,
)
}
// Read allow_ref_in_want from file, then check cmdline override
let allow_ref_in_want = match overrides.get("uploadpack.allowrefinwant") {
Some(v) => {
let lower = v.to_lower()
lower == "true" || lower == "1" || lower == "yes"
}
None =>
read_config_bool(fs, config_path, "uploadpack", "allowrefinwant").unwrap_or(
false,
)
}
// Read blobpackfileuri from cmdline first, then file
// Note: -c uploadpack.blobpackfileuri (without =) means key is present but empty
// -c uploadpack.blobpackfileuri=anything means key has value
let blob_packfile_uri = match overrides.get("uploadpack.blobpackfileuri") {
Some(v) => {
// `-c key` (without =) → value is "true" from parse_global_options
// `-c key=anything` → value is the URI string
// Boolean-like values ("true", "false", "yes", "no") are not valid URIs
let lower = v.to_lower()
if v.length() == 0 ||
lower == "true" ||
lower == "false" ||
lower == "yes" ||
lower == "no" {
None
} else {
Some(v)
}
}
None =>
match
read_config_value(fs, config_path, "uploadpack", "blobpackfileuri") {
Some(v) => if v.length() > 0 { Some(v) } else { None }
None => None
}
}
UploadPackConfig::new(allow_filter, blob_packfile_uri, allow_ref_in_want)
}