///| Ref resolution helpers (rev-parse/show-ref)
///|
pub fn rev_parse(
fs : &@types.RepoFileSystem,
git_dir : String,
spec : String,
) -> @object.ObjectId? raise @object.GitError {
if spec.has_prefix(":/") && spec.length() > 2 {
let needle = substring_revparse(spec, 2, spec.length())
return resolve_commit_message_search_all_refs(fs, git_dir, needle)
}
// :N:path — index stage entry lookup (e.g. :0:file, :2:file)
// :path — shorthand for :0:path
if spec.has_prefix(":") && !spec.has_prefix(":/") {
let rest = substring_revparse(spec, 1, spec.length())
let (target_stage, path) = if rest.length() >= 2 {
let ch = rest[0]
if (ch == '0' || ch == '1' || ch == '2' || ch == '3') && rest[1] == ':' {
let stage = ch.to_int() - '0'.to_int()
(stage, substring_revparse(rest, 2, rest.length()))
} else {
(0, rest)
}
} else {
(0, rest)
}
if path.length() > 0 {
return resolve_index_stage_entry(fs, git_dir, path, target_stage)
}
}
match parse_commit_message_search_spec(spec) {
Some((base_spec, needle)) =>
return resolve_commit_message_search(fs, git_dir, base_spec, needle)
None => ()
}
match spec.find(":") {
Some(idx) =>
if idx > 0 && idx + 1 <= spec.length() {
let rev_part = substring_revparse(spec, 0, idx)
let path_part = substring_revparse(spec, idx + 1, spec.length())
let base = rev_parse(fs, git_dir, rev_part)
guard base is Some(base_id) else { return None }
return resolve_path_in_commit(fs, git_dir, base_id, path_part)
}
None => ()
}
// ^{} — peel tags to the underlying object (any type)
if spec.has_suffix("^{}") && spec.length() > 3 {
let base_spec = substring_revparse(spec, 0, spec.length() - 3)
let base = rev_parse(fs, git_dir, base_spec)
guard base is Some(base_id) else { return None }
return peel_tags(fs, git_dir, base_id)
}
match parse_peel_commit_spec(spec) {
Some(base_spec) => {
let base = rev_parse(fs, git_dir, base_spec)
guard base is Some(base_id) else { return None }
return peel_object_to_commit(fs, git_dir, base_id)
}
None => ()
}
match parse_peel_tree_spec(spec) {
Some(base_spec) => {
let base = rev_parse(fs, git_dir, base_spec)
guard base is Some(base_id) else { return None }
return peel_object_to_tree(fs, git_dir, base_id)
}
None => ()
}
// ^{tag} — verify object is a tag
if spec.has_suffix("^{tag}") && spec.length() > 6 {
let base_spec = substring_revparse(spec, 0, spec.length() - 6)
let base = rev_parse(fs, git_dir, base_spec)
guard base is Some(base_id) else { return None }
let db = @bitlib.ObjectDb::load(fs, git_dir)
let obj = db.get(fs, base_id)
guard obj is Some(o) else { return None }
if o.obj_type == @object.ObjectType::Tag {
return Some(base_id)
}
return None
}
// Parse suffix (^, ^^, ~n, ^n)
let (base_spec, suffix) = parse_rev_suffix(spec)
// Resolve base reference
let base_id = match resolve_base_ref(fs, git_dir, base_spec) {
Some(id) => id
None => return None
}
match suffix {
None => Some(base_id)
Some(parsed_suffix) => apply_rev_suffix(fs, git_dir, base_id, parsed_suffix)
}
}
///|
fn parse_commit_message_search_spec(spec : String) -> (String, String)? {
let marker = "^{/"
match spec.find(marker) {
Some(idx) if idx > 0 &&
spec.has_suffix("}") &&
idx + marker.length() <= spec.length() - 1 =>
Some(
(
substring_revparse(spec, 0, idx),
substring_revparse(spec, idx + marker.length(), spec.length() - 1),
),
)
_ => None
}
}
///|
fn resolve_commit_message_search(
fs : &@types.RepoFileSystem,
git_dir : String,
base_spec : String,
needle : String,
) -> @object.ObjectId? raise @object.GitError {
if needle.length() == 0 {
return None
}
// "!" prefix handling: "!!" = literal "!", "!-" = negation, bare "!" or "!X" = invalid
if needle.has_prefix("!") {
if needle.has_prefix("!!") {
// "!!" prefix means literal "!" search (strip one "!")
let base = rev_parse(fs, git_dir, base_spec)
guard base is Some(base_id) else { return None }
let actual_needle = substring_revparse(needle, 1, needle.length())
return find_reachable_commit_by_message_from_many(
fs,
git_dir,
[base_id],
actual_needle,
)
}
if needle.has_prefix("!-") {
// "!-" prefix means negation
let actual_needle = substring_revparse(needle, 2, needle.length())
if actual_needle.length() == 0 {
raise @object.GitError::InvalidObject(
"Invalid search pattern: \\{base_spec}^{{/!-}}",
)
}
let base = rev_parse(fs, git_dir, base_spec)
guard base is Some(base_id) else { return None }
return find_reachable_commit_by_message_from_many(
fs,
git_dir,
[base_id],
actual_needle,
negate=true,
)
}
// Bare "!" or "!X" (not "!!" or "!-") is invalid
raise @object.GitError::InvalidObject(
"Invalid search pattern: \\{base_spec}^{{/\\{needle}}}",
)
}
let base = rev_parse(fs, git_dir, base_spec)
guard base is Some(base_id) else { return None }
find_reachable_commit_by_message_from_many(fs, git_dir, [base_id], needle)
}
///|
fn resolve_commit_message_search_all_refs(
fs : &@types.RepoFileSystem,
git_dir : String,
needle : String,
) -> @object.ObjectId? raise @object.GitError {
if needle.length() == 0 {
return None
}
let starts : Array[@object.ObjectId] = []
let seen_start : Map[String, Bool] = Map([])
let refs = show_ref(fs, git_dir)
for item in refs {
let (_, id) = item
let hex = id.to_hex()
if !seen_start.contains(hex) {
seen_start[hex] = true
starts.push(id)
}
}
match @bitlib.resolve_head_commit(fs, git_dir) {
Some(id) => {
let hex = id.to_hex()
if !seen_start.contains(hex) {
seen_start[hex] = true
starts.push(id)
}
}
None => ()
}
if starts.length() == 0 {
return None
}
// !-prefix means negation: find commit whose message does NOT contain the pattern
if needle.has_prefix("!-") {
let actual_needle = substring_revparse(needle, 2, needle.length())
return find_reachable_commit_by_message_from_many(
fs,
git_dir,
starts,
actual_needle,
negate=true,
)
}
find_reachable_commit_by_message_from_many(fs, git_dir, starts, needle)
}
///|
fn find_reachable_commit_by_message_from_many(
fs : &@types.RepoFileSystem,
git_dir : String,
starts : Array[@object.ObjectId],
needle : String,
negate? : Bool = false,
) -> @object.ObjectId? raise @object.GitError {
let db = @bitlib.ObjectDb::load(fs, git_dir)
let queue : Array[@object.ObjectId] = []
for start in starts {
queue.push(start)
}
let seen : Map[String, Bool] = Map([])
let mut best_id : @object.ObjectId? = None
let mut best_time = -1L
while queue.length() > 0 {
let current = queue.unsafe_pop()
let hex = current.to_hex()
if seen.contains(hex) {
continue
}
seen[hex] = true
match db.get(fs, current) {
Some(obj) =>
if obj.obj_type == @object.ObjectType::Commit {
let contains = commit_message_contains(obj.data, needle)
let matches = if negate { !contains } else { contains }
if matches {
let ts = parse_commit_committer_timestamp(obj.data)
if best_id is None || ts > best_time {
best_id = Some(current)
best_time = ts
}
}
let info = @repo.parse_commit(obj.data)
for parent in info.parents {
if !seen.contains(parent.to_hex()) {
queue.push(parent)
}
}
}
None => ()
}
}
best_id
}
///|
fn commit_message_contains(data : Bytes, needle : String) -> Bool {
let text = @utf8.decode_lossy(data[:])
match text.find("\n\n") {
Some(idx) => {
let msg = substring_revparse(text, idx + 2, text.length())
regex_like_contains(msg, needle)
}
None => false
}
}
///|
/// Simple regex-like substring search supporting `.` as any-char and `\\` escapes.
/// This matches git's basic regex behavior for `^{/pattern}`.
fn regex_like_contains(text : String, pattern : String) -> Bool {
if pattern.length() == 0 {
return true
}
let text_chars = text.to_array()
let pat_chars = pattern.to_array()
for start in 0.. Bool {
let mut ti = start
let mut pi = 0
while pi < pattern.length() {
if ti >= text.length() {
return false
}
let pc = pattern[pi]
if pc == '\\' {
if pi + 1 < pattern.length() {
if text[ti] != pattern[pi + 1] {
return false
}
ti += 1
pi += 2
continue
}
if text[ti] != '\\' {
return false
}
ti += 1
pi += 1
continue
}
if pc == '.' {
ti += 1
pi += 1
continue
}
if text[ti] != pc {
return false
}
ti += 1
pi += 1
}
true
}
///|
fn parse_commit_committer_timestamp(data : Bytes) -> Int64 {
let text = @utf8.decode_lossy(data[:])
for line_view in text.split("\n") {
let line = line_view.to_owned()
if line.length() == 0 {
break
}
if line.has_prefix("committer ") {
let rest = substring_revparse(line, 10, line.length())
return parse_identity_timestamp(rest)
}
}
0L
}
///|
fn parse_identity_timestamp(identity : String) -> Int64 {
let last_space = identity.rev_find(" ")
guard last_space is Some(tz_idx) else { return 0L }
let before_tz = substring_revparse(identity, 0, tz_idx)
let second_last = before_tz.rev_find(" ")
guard second_last is Some(ts_idx) else { return 0L }
let ts = substring_revparse(before_tz, ts_idx + 1, before_tz.length())
parse_revparse_int64(ts)
}
///|
fn parse_revparse_int64(s : String) -> Int64 {
let mut value = 0L
for c in s {
if c < '0' || c > '9' {
return value
}
let digit = c.to_int() - '0'.to_int()
value = value * 10L + digit.to_int64()
}
value
}
///|
fn peel_tags(
fs : &@types.RepoFileSystem,
git_dir : String,
id : @object.ObjectId,
) -> @object.ObjectId? raise @object.GitError {
let db = @bitlib.ObjectDb::load(fs, git_dir)
let mut current = id
for _ in 0..<64 {
let obj = db.get(fs, current)
guard obj is Some(o) else { return Some(current) }
match o.obj_type {
@object.ObjectType::Tag =>
match parse_tag_object_target(o.data) {
Some(next) => current = next
None => return Some(current)
}
_ => return Some(current)
}
}
Some(current)
}
///|
fn parse_peel_commit_spec(spec : String) -> String? {
let suffix = "^{commit}"
if spec.has_suffix(suffix) && spec.length() > suffix.length() {
return Some(substring_revparse(spec, 0, spec.length() - suffix.length()))
}
None
}
///|
fn parse_peel_tree_spec(spec : String) -> String? {
let suffix = "^{tree}"
if spec.has_suffix(suffix) && spec.length() > suffix.length() {
return Some(substring_revparse(spec, 0, spec.length() - suffix.length()))
}
None
}
///|
fn peel_object_to_commit(
fs : &@types.RepoFileSystem,
git_dir : String,
id : @object.ObjectId,
) -> @object.ObjectId? raise @object.GitError {
let db = @bitlib.ObjectDb::load(fs, git_dir)
let mut current = id
for _ in 0..<8 {
let obj = db.get(fs, current)
guard obj is Some(o) else { return None }
match o.obj_type {
@object.ObjectType::Commit => return Some(current)
@object.ObjectType::Tag =>
match parse_tag_object_target(o.data) {
Some(next) => current = next
None => return None
}
_ => return None
}
}
None
}
///|
fn peel_object_to_tree(
fs : &@types.RepoFileSystem,
git_dir : String,
id : @object.ObjectId,
) -> @object.ObjectId? raise @object.GitError {
let db = @bitlib.ObjectDb::load(fs, git_dir)
let mut current = id
for _ in 0..<8 {
let obj = db.get(fs, current)
guard obj is Some(o) else { return None }
match o.obj_type {
@object.ObjectType::Tree => return Some(current)
@object.ObjectType::Commit => return Some(@repo.parse_commit(o.data).tree)
@object.ObjectType::Tag =>
match parse_tag_object_target(o.data) {
Some(next) => current = next
None => return None
}
_ => return None
}
}
None
}
///|
fn parse_tag_object_target(data : Bytes) -> @object.ObjectId? {
let text = @utf8.decode_lossy(data[:])
for line_view in text.split("\n") {
let line = line_view.to_owned()
if line.length() == 0 {
break
}
if line.has_prefix("object ") {
let hex = substring_revparse(line, 7, line.length())
let id = @object.ObjectId::from_hex(hex) catch { _ => return None }
return Some(id)
}
}
None
}
///|
/// Parse revision suffix like ^, ^^, ~n
fn parse_rev_suffix(spec : String) -> (String, String?) {
let chars = spec.to_array()
let len = chars.length()
if len == 0 {
return (spec, None)
}
// Find where suffix starts - must start with ^ or ~
let mut suffix_start = -1
for i = 0; i < len; i = i + 1 {
let c = chars[i]
if c == '^' || c == '~' {
suffix_start = i
break
}
}
if suffix_start < 0 {
return (spec, None)
}
// Validate suffix - only ^, ~, and digits after the first ^ or ~
for i = suffix_start; i < len; i = i + 1 {
let c = chars[i]
if !(c == '^' || c == '~' || (c >= '0' && c <= '9')) {
// Invalid suffix character - no suffix
return (spec, None)
}
}
let base = substring_revparse(spec, 0, suffix_start)
let suffix = substring_revparse(spec, suffix_start, len)
(base, Some(suffix))
}
///|
/// Apply revision suffix operators.
fn apply_rev_suffix(
fs : &@types.RepoFileSystem,
git_dir : String,
start : @object.ObjectId,
suffix : String,
) -> @object.ObjectId? raise @object.GitError {
let db = @bitlib.ObjectDb::load(fs, git_dir)
let chars = suffix.to_array()
let mut current = start
let mut i = 0
while i < chars.length() {
let op = chars[i]
if op == '^' {
i += 1
let mut parent_index = 0
let mut has_digits = false
while i < chars.length() && chars[i] >= '0' && chars[i] <= '9' {
has_digits = true
parent_index = parent_index * 10 + (chars[i].to_int() - '0'.to_int())
i += 1
}
if has_digits {
// `^0` peels to the commit (dereference tags).
if parent_index == 0 {
current = peel_to_commit_id(db, fs, current)
continue
}
} else {
parent_index = 1
}
let next = commit_nth_parent(db, fs, git_dir, current, parent_index)
guard next is Some(parent_id) else { return None }
current = parent_id
continue
}
if op == '~' {
i += 1
let mut steps = 0
let mut has_digits = false
while i < chars.length() && chars[i] >= '0' && chars[i] <= '9' {
has_digits = true
steps = steps * 10 + (chars[i].to_int() - '0'.to_int())
i += 1
}
if !has_digits {
steps = 1
}
for _ in 0.. @object.ObjectId raise @object.GitError {
let mut current = id
for _ in 0..<8 {
let obj = db.get(fs, current)
guard obj is Some(o) else { break }
match o.obj_type {
@object.ObjectType::Commit => return current
@object.ObjectType::Tag =>
match parse_tag_object_target(o.data) {
Some(next) => current = next
None => break
}
_ => break
}
}
current
}
///|
fn commit_nth_parent(
db : @bitlib.ObjectDb,
fs : &@types.RepoFileSystem,
git_dir : String,
commit_id : @object.ObjectId,
nth_parent : Int,
) -> @object.ObjectId? raise @object.GitError {
if nth_parent <= 0 {
return Some(commit_id)
}
// Peel tags to reach commit
let peeled = peel_to_commit_id(db, fs, commit_id)
let obj = db.get(fs, peeled)
guard obj is Some(o) else { return None }
if o.obj_type != @object.ObjectType::Commit {
return None
}
let info = @repo.parse_commit(o.data)
let parents = match revparse_read_graft_parents(fs, git_dir, peeled) {
Some(grafted) => grafted
None => info.parents
}
if nth_parent > parents.length() {
return None
}
Some(parents[nth_parent - 1])
}
///|
/// Read `.git/info/grafts` and return the grafted parent list for `commit_id`
/// if a graft entry exists for it. Each line is ` ...`.
fn revparse_read_graft_parents(
fs : &@types.RepoFileSystem,
git_dir : String,
commit_id : @object.ObjectId,
) -> Array[@object.ObjectId]? {
let graft_path = @repo.join_path(git_dir, "info/grafts")
if !fs.is_file(graft_path) {
return None
}
let raw = fs.read_file(graft_path) catch { _ => return None }
let content = @utf8.decode_lossy(raw[:])
let commit_hex = commit_id.to_hex()
for line_view in content.split("\n") {
let line = @string_utils.trim_string(line_view.to_owned())
if line.length() == 0 || line.has_prefix("#") {
continue
}
let tokens : Array[String] = []
for token_view in line.split(" ") {
let token = @string_utils.trim_string(token_view.to_owned())
if token.length() > 0 {
tokens.push(token)
}
}
if tokens.length() == 0 || tokens[0] != commit_hex {
continue
}
let parents : Array[@object.ObjectId] = []
for i in 1.. continue
}
parents.push(parent_id)
}
}
return Some(parents)
}
None
}
///|
fn substring_revparse(s : String, start : Int, end : Int) -> String {
let chars = s.to_array()
let result = StringBuilder::new()
for i = start; i < end && i < chars.length(); i = i + 1 {
result.write_char(chars[i])
}
result.to_string()
}
///|
/// Resolve :N:path by looking up index stage entries.
/// Parses the index binary format directly to find the entry with
/// matching path and stage number.
fn resolve_index_stage_entry(
fs : &@types.RepoFileSystem,
git_dir : String,
path : String,
stage : Int,
) -> @object.ObjectId? raise @object.GitError {
let index_path = git_dir + "/index"
if !fs.is_file(index_path) {
return None
}
let data = fs.read_file(index_path)
if data.length() < 12 {
return None
}
if data[0] != b'D' || data[1] != b'I' || data[2] != b'R' || data[3] != b'C' {
return None
}
let version = revparse_read_u32_be(data, 4)
let num_entries = revparse_read_u32_be(data, 8)
let mut pos = 12
for _ in 0.. data.length() {
break
}
let id_pos = pos + 40
let id_bytes : FixedArray[Byte] = FixedArray::make(20, b'\x00')
for i in 0..<20 {
id_bytes[i] = data[id_pos + i]
}
let id = @object.ObjectId::new(id_bytes)
let flags_pos = id_pos + 20
let flags = (data[flags_pos].to_int() << 8) | data[flags_pos + 1].to_int()
let entry_stage = (flags >> 12) & 0x3
let name_len = flags & 0x0FFF
let has_extended = version >= 3 && (flags & 0x4000) != 0
let mut path_start = flags_pos + 2
if has_extended {
path_start += 2
}
let actual_len = if name_len < 0x0FFF {
name_len
} else {
let mut len = 0
while path_start + len < data.length() &&
data[path_start + len] != b'\x00' {
len += 1
}
len
}
let entry_path_buf = StringBuilder::new()
for i in 0.. Int {
(data[pos].to_int() << 24) |
(data[pos + 1].to_int() << 16) |
(data[pos + 2].to_int() << 8) |
data[pos + 3].to_int()
}
///|
fn resolve_abbrev_ref_id(
fs : &@types.RepoFileSystem,
git_dir : String,
spec : String,
) -> @object.ObjectId? raise @object.GitError {
if spec.length() < 4 || spec.length() >= 64 || !@bithash.is_hex_string(spec) {
return None
}
let needle = @bithash.lower_hex_string(spec)
let refs = show_ref(fs, git_dir)
let db = @bitlib.ObjectDb::load(fs, git_dir)
let queue : Array[@object.ObjectId] = []
let seen : Map[String, Bool] = Map([])
for item in refs {
let (_, id) = item
queue.push(id)
}
match @bitlib.resolve_head_commit(fs, git_dir) {
Some(id) => queue.push(id)
None => ()
}
let mut matched : @object.ObjectId? = None
while queue.length() > 0 {
let id = match queue.pop() {
Some(oid) => oid
None => break
}
let hex = id.to_hex()
if seen.contains(hex) {
continue
}
seen[hex] = true
if hex.has_prefix(needle) {
match matched {
None => matched = Some(id)
Some(prev) => if prev != id { return None }
}
}
match db.get(fs, id) {
Some(obj) =>
if obj.obj_type == @object.ObjectType::Commit {
let info = @repo.parse_commit(obj.data)
for parent in info.parents {
queue.push(parent)
}
}
None => ()
}
}
matched
}
///|
/// Parse a spec containing @{upstream}, @{u}, @{push}, or @{p} (case-insensitive).
/// Returns (branch_name_or_empty, "upstream"|"push", rest_after_closing_brace) or None.
/// Finds the LAST @{ occurrence to handle branch names containing @.
pub fn parse_upstream_push_spec(spec : String) -> (String, String, String)? {
let chars = spec.to_array()
let len = chars.length()
// Scan right-to-left for @{ that contains upstream/u/push/p.
// We iterate from right to left so that branch names with @ are handled:
// e.g. fun@ny@{u} → finds @{u} at the end, branch = "fun@ny"
// For chained specs like my-side@{u}@{1}, we need the FIRST matching @{.
// Strategy: collect all @{ positions, then try from leftmost to rightmost.
let at_brace_positions : Array[Int] = []
for i = 0; i < len - 1; i = i + 1 {
if chars[i] == '@' && chars[i + 1] == '{' {
at_brace_positions.push(i)
}
}
// Try each @{ position from left to right
for pos in at_brace_positions {
// Find matching closing brace
let mut close_idx = -1
for i = pos + 2; i < len; i = i + 1 {
if chars[i] == '}' {
close_idx = i
break
}
}
if close_idx < 0 {
continue
}
// Extract content between @{ and }
let content = substring_revparse(spec, pos + 2, close_idx)
let content_lower = content.to_lower()
let kind = if content_lower == "upstream" || content_lower == "u" {
"upstream"
} else if content_lower == "push" || content_lower == "p" {
"push"
} else {
continue
}
let branch = substring_revparse(spec, 0, pos)
// If branch contains ':', this is a tree:path spec, not upstream/push
if branch.find(":") is Some(_) {
return None
}
let rest = substring_revparse(spec, close_idx + 1, len)
return Some((branch, kind, rest))
}
None
}
///|
/// Resolve @{upstream} for a branch. Returns the upstream tracking ref name.
/// Errors with descriptive messages matching git behavior.
pub fn resolve_upstream_ref(
fs : &@types.RepoFileSystem,
git_dir : String,
branch : String,
) -> String raise @object.GitError {
// If branch is empty, use HEAD
let branch_name = if branch == "" || branch == "HEAD" || branch == "@" {
match @bitlib.read_head_ref(fs, git_dir) {
@bitlib.HeadRef::Branch(name) => name
@bitlib.HeadRef::Detached(_) =>
raise @object.GitError::InvalidObject("HEAD does not point to a branch")
}
// Strip refs/heads/ prefix if present
} else if branch.has_prefix("refs/heads/") {
// refs/heads/X@{upstream} should NOT resolve (git behavior)
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch + "'",
)
} else {
branch
}
// Check branch exists
let branch_ref = "refs/heads/" + branch_name
let branch_exists = match @bitlib.resolve_ref(fs, git_dir, branch_ref) {
Some(_) => true
None => false
}
if !branch_exists {
raise @object.GitError::InvalidObject(
"no such branch: '" + branch_name + "'",
)
}
// Read config
let (_, branches) = @bitlib.read_repo_config(fs, git_dir)
let cfg = match branches.get(branch_name) {
Some(c) => c
None =>
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
let remote = match cfg.remote {
Some(r) => r
None =>
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
if cfg.merges.length() == 0 {
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
let merge = cfg.merges[0]
// Convert merge ref to tracking ref
if merge.has_prefix("refs/heads/") {
let branch_part = substring_revparse(merge, 11, merge.length())
// Check if this remote actually fetches this ref
// For remote "." (local tracking), the ref is the merge ref itself
if remote == "." {
return merge
}
let tracking_ref = "refs/remotes/" + remote + "/" + branch_part
// Verify the remote-tracking ref exists (or at least could exist)
// git checks if the remote's fetch refspec maps this
let (remotes, _) = @bitlib.read_repo_config(fs, git_dir)
match remotes.get(remote) {
Some(remote_cfg) => {
// Check if any fetch refspec maps the merge ref
let fetch_specs = @bitlib.parse_fetch_refspecs(remote_cfg.fetch)
let mut mapped = false
for spec in fetch_specs {
if spec.negative {
continue
}
if @bitlib.refspec_map_dst(spec.src, spec.dst, merge) is Some(_) {
mapped = true
break
}
}
if !mapped {
raise @object.GitError::InvalidObject(
"upstream branch '" +
merge +
"' not stored as a remote-tracking branch",
)
}
}
None => ()
}
tracking_ref
} else {
merge
}
}
///|
/// Resolve @{push} for a branch. Returns the push tracking ref name.
pub fn resolve_push_ref(
fs : &@types.RepoFileSystem,
git_dir : String,
branch : String,
) -> String raise @object.GitError {
// If branch is empty, use HEAD
let branch_name = if branch == "" || branch == "HEAD" || branch == "@" {
match @bitlib.read_head_ref(fs, git_dir) {
@bitlib.HeadRef::Branch(name) => name
@bitlib.HeadRef::Detached(_) =>
raise @object.GitError::InvalidObject("HEAD does not point to a branch")
}
} else if branch.has_prefix("refs/heads/") {
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch + "'",
)
} else {
branch
}
// Read config
let (remotes, branches) = @bitlib.read_repo_config(fs, git_dir)
// Determine push remote
// Priority: branch..pushRemote > remote.pushDefault > branch..remote
let content = @bitlib.read_config_content(fs, git_dir)
let blocks = @bitlib.parse_config_blocks(content)
let mut push_remote : String? = None
// 1. branch..pushRemote
for block in blocks {
if block.section == Some("branch") && block.name == Some(branch_name) {
for line in block.lines {
match @bitlib.parse_config_kv(line) {
Some((key, value)) if key == "pushremote" => {
push_remote = Some(value)
break
}
_ => ()
}
}
}
}
// 2. remote.pushDefault
if push_remote is None {
for block in blocks {
if block.section == Some("remote") && block.name is None {
for line in block.lines {
match @bitlib.parse_config_kv(line) {
Some((key, value)) if key == "pushdefault" => {
push_remote = Some(value)
break
}
_ => ()
}
}
}
}
}
// 3. branch..remote
if push_remote is None {
match branches.get(branch_name) {
Some(cfg) => push_remote = cfg.remote
None => ()
}
}
let remote = match push_remote {
Some(r) => r
None =>
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
// Check if remote exists
guard remotes.get(remote) is Some(remote_cfg) else {
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
// Read push.default
let mut push_default_str : String? = None
for block in blocks {
if block.section == Some("push") && block.name is None {
for line in block.lines {
match @bitlib.parse_config_kv(line) {
Some((key, value)) if key == "default" => {
push_default_str = Some(value)
break
}
_ => ()
}
}
}
}
let push_default = match push_default_str {
Some(v) =>
match v.to_lower() {
"upstream" => "upstream"
"current" => "current"
"matching" => "matching"
"nothing" => "nothing"
_ => "simple"
}
None => "simple"
}
// Try push refspecs first (applies regardless of push.default)
let local_ref = "refs/heads/" + branch_name
let push_specs = @bitlib.parse_push_refspecs(remote_cfg.push)
for spec in push_specs {
match @bitlib.refspec_map_dst(spec.src, spec.dst, local_ref) {
Some(dst) =>
if dst.has_prefix("refs/heads/") {
let dst_branch = substring_revparse(dst, 11, dst.length())
return "refs/remotes/" + remote + "/" + dst_branch
} else {
return "refs/remotes/" + remote + "/" + branch_name
}
None => continue
}
}
// Determine the push destination ref based on push.default
match push_default {
"nothing" =>
raise @object.GitError::InvalidObject(
"push has no destination (push.default is 'nothing')",
)
"upstream" => {
let cfg = match branches.get(branch_name) {
Some(c) => c
None =>
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
guard cfg.merges.length() > 0 else {
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
let merge = cfg.merges[0]
if merge.has_prefix("refs/heads/") {
let bp = substring_revparse(merge, 11, merge.length())
"refs/remotes/" + remote + "/" + bp
} else {
"refs/remotes/" + remote + "/" + branch_name
}
}
"simple" => {
// simple: like upstream if same remote and same branch name
let cfg = branches.get(branch_name)
let upstream_remote = match cfg {
Some(c) => c.remote
None => None
}
// Check if triangular (push remote != upstream remote)
let is_triangular = match upstream_remote {
Some(ur) => ur != remote
None => true
}
if is_triangular {
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
// Check upstream branch name matches local branch name
let upstream_branch = match cfg {
Some(c) =>
if c.merges.length() > 0 {
let m = c.merges[0]
if m.has_prefix("refs/heads/") {
Some(substring_revparse(m, 11, m.length()))
} else {
None
}
} else {
None
}
None => None
}
match upstream_branch {
Some(ub) =>
if ub != branch_name {
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
None =>
raise @object.GitError::InvalidObject(
"no upstream configured for branch '" + branch_name + "'",
)
}
"refs/remotes/" + remote + "/" + branch_name
}
_ =>
// "current" or "matching" — push to same branch name on remote
"refs/remotes/" + remote + "/" + branch_name
}
}
///|
/// Resolve base reference (without suffix)
fn resolve_base_ref(
fs : &@types.RepoFileSystem,
git_dir : String,
spec : String,
) -> @object.ObjectId? raise @object.GitError {
if spec == "HEAD" || spec == "" || spec == "@" {
return @bitlib.resolve_head_commit(fs, git_dir)
}
if @bitlib.resolve_ref(fs, git_dir, spec) is Some(id) {
return Some(id)
}
if spec.has_prefix("refs/") {
return @bitlib.resolve_ref(fs, git_dir, spec)
}
if spec.has_prefix("heads/") ||
spec.has_prefix("tags/") ||
spec.has_prefix("remotes/") {
return @bitlib.resolve_ref(fs, git_dir, "refs/" + spec)
}
if (spec.length() == 40 || spec.length() == 64) &&
@bithash.is_hex_string(spec) {
match @object.ObjectId::from_hex(spec) {
id => return Some(id)
}
}
if resolve_abbrev_ref_id(fs, git_dir, spec) is Some(id) {
return Some(id)
}
if @bitlib.resolve_ref(fs, git_dir, "refs/heads/" + spec) is Some(id) {
return Some(id)
}
if @bitlib.resolve_ref(fs, git_dir, "refs/remotes/" + spec) is Some(id) {
return Some(id)
}
if @bitlib.resolve_ref(fs, git_dir, "refs/tags/" + spec) is Some(id) {
return Some(id)
}
None
}
///|
/// Resolve path within a commit/tree
fn resolve_path_in_commit(
fs : &@types.RepoFileSystem,
git_dir : String,
base_id : @object.ObjectId,
path : String,
) -> @object.ObjectId? raise @object.GitError {
let db = @bitlib.ObjectDb::load(fs, git_dir)
let obj = db.get(fs, base_id)
match obj {
None => return None
Some(o) => {
let tree_id = match o.obj_type {
@object.ObjectType::Commit => @repo.parse_commit(o.data).tree
@object.ObjectType::Tree => base_id
_ => return None
}
// Empty path means "return the tree itself" (e.g., HEAD:)
if path.length() == 0 {
return Some(tree_id)
}
let entry = @bitlib.find_tree_entry(db, fs, tree_id, path)
match entry {
Some(e) => Some(e.id)
None => None
}
}
}
}
///|
/// List all refs (loose + packed).
pub fn show_ref(
fs : &@types.RepoFileSystem,
git_dir : String,
) -> Array[(String, @object.ObjectId)] raise @object.GitError {
let out : Map[String, @object.ObjectId] = Map([])
let refs_dir = @repo.join_path(git_dir, "refs")
if fs.is_dir(refs_dir) {
ref_collect_loose(fs, git_dir, refs_dir, "refs", out)
}
let packed = @repo.join_path(git_dir, "packed-refs")
if fs.is_file(packed) {
ref_collect_packed(fs, packed, out)
}
let result : Array[(String, @object.ObjectId)] = Array::new(
capacity=out.length(),
)
for name, id in out {
result.push((name, id))
}
result.sort_by(fn(a, b) {
let la = a.0.to_array()
let lb = b.0.to_array()
let min_len = if la.length() < lb.length() {
la.length()
} else {
lb.length()
}
for i in 0.. Array[String] raise @object.GitError {
let refs = show_ref(fs, git_dir)
let lines : Array[String] = []
for item in refs {
let (name, id) = item
lines.push("\{id.to_hex()} \{name}")
}
lines
}
///|
fn ref_collect_loose(
fs : &@types.RepoFileSystem,
git_dir : String,
dir : String,
prefix : String,
out : Map[String, @object.ObjectId],
) -> Unit raise @object.GitError {
let entries = fs.readdir(dir)
for name in entries {
let path = @repo.join_path(dir, name)
let rel = if prefix == "" { name } else { prefix + "/" + name }
if fs.is_dir(path) {
ref_collect_loose(fs, git_dir, path, rel, out)
} else if fs.is_file(path) {
let line = ref_read_line(fs, path)
if line.has_prefix("ref: ") {
let target = String::unsafe_substring(line, start=5, end=line.length())
let resolved = @bitlib.resolve_ref(fs, git_dir, target) catch {
_ => None
}
match resolved {
Some(id) => out[rel] = id
None => ()
}
} else {
out[rel] = @object.ObjectId::from_hex(line)
}
}
}
}
///|
fn ref_collect_packed(
fs : &@types.RepoFileSystem,
packed_path : String,
out : Map[String, @object.ObjectId],
) -> Unit raise @object.GitError {
let text = @utf8.decode_lossy(fs.read_file(packed_path)[:])
for line_view in text.split("\n") {
let line = ref_trim_line(line_view.to_owned())
if line.length() == 0 {
continue
}
if line.has_prefix("#") || line.has_prefix("^") {
continue
}
let space = line.find(" ")
match space {
None => continue
Some(idx) => {
if idx + 1 >= line.length() {
continue
}
let id_hex = String::unsafe_substring(line, start=0, end=idx)
let name = String::unsafe_substring(
line,
start=idx + 1,
end=line.length(),
)
if !out.contains(name) {
out[name] = @object.ObjectId::from_hex(id_hex)
}
}
}
}
}
///|
fn ref_read_line(
fs : &@types.RepoFileSystem,
path : String,
) -> String raise @object.GitError {
let text = @utf8.decode_lossy(fs.read_file(path)[:])
for line_view in text.split("\n") {
let line = ref_trim_line(line_view.to_owned())
if line.length() > 0 {
return line
}
}
raise @object.GitError::InvalidObject("Empty ref: \{path}")
}
///|
fn ref_trim_line(line : String) -> String {
let mut s = line
if s.has_suffix("\r") {
s = String::unsafe_substring(s, start=0, end=s.length() - 1)
}
s
}