///| Rebase and fast-forward helpers
///|
pub struct RebaseState {
onto : @bit.ObjectId
orig_head : @bit.ObjectId
head_name : String // "refs/heads/main" or "" for detached
todo : Array[@bit.ObjectId]
done : Array[@bit.ObjectId]
current : @bit.ObjectId?
message : String
author : String
author_time : Int64
author_tz : String
// old commit id -> new (rewritten) commit id, in application order.
// Mirrors git's .git/rebase-merge/rewritten-list.
rewritten : Array[(@bit.ObjectId, @bit.ObjectId)]
}
///|
pub struct RebaseResult {
status : RebaseStatus
commit_id : @bit.ObjectId?
conflicts : Array[String]
// old commit id -> new (rewritten) commit id, populated once the rebase
// completes. Empty for fast-forward / nothing-to-rebase results, since no
// commit ids change in those cases.
rewritten : Array[(@bit.ObjectId, @bit.ObjectId)]
}
///|
pub enum RebaseStatus {
Complete
Conflict
NothingToRebase
}
///|
/// Resolves the actual `.git` directory for `root`, following the
/// `gitdir: ` pointer file used by linked worktrees (where
/// `/.git` is a file, not a directory, and the real admin dir lives
/// under the main repo's `.git/worktrees/`).
fn rebase_resolve_git_dir(rfs : &@bit.RepoFileSystem, root : String) -> String {
let default_git_dir = join_path(root, ".git")
if rfs.is_file(default_git_dir) {
resolve_gitdir(rfs, default_git_dir)
} else {
default_git_dir
}
}
///|
pub async fn fast_forward_to(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
target : @bit.ObjectId,
run_filter_cmd? : FilterCmd,
) -> Unit raise @bit.GitError {
let git_dir = rebase_resolve_git_dir(rfs, root)
let db = ObjectDb::load(rfs, git_dir)
let tree_map = rebase_tree_map(db, rfs, target)
rebase_update_head(fs, rfs, git_dir, target)
rebase_write_worktree_filtered(fs, db, rfs, root, git_dir, tree_map, run_filter_cmd?)
let entries = rebase_map_to_index(db, rfs, tree_map)
write_index_entries(fs, git_dir, entries)
}
///|
/// Rebase current HEAD onto upstream commit (linear, no conflicts).
pub async fn rebase_onto(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
upstream : @bit.ObjectId,
run_filter_cmd? : FilterCmd,
) -> @bit.ObjectId raise @bit.GitError {
let git_dir = rebase_resolve_git_dir(rfs, root)
let head = resolve_head_commit(rfs, git_dir)
match head {
None => {
fast_forward_to(fs, rfs, root, upstream, run_filter_cmd?)
return upstream
}
Some(h) => {
if h == upstream {
return h
}
let db = ObjectDb::load(rfs, git_dir)
let chain = rebase_collect_chain(db, rfs, h, upstream)
let base_map = rebase_tree_map(db, rfs, upstream)
let mut parent = upstream
for commit_id in chain {
let info = rebase_parse_commit_full(db, rfs, commit_id)
let parent_id = if info.parents.length() > 0 {
info.parents[0]
} else {
raise @bit.GitError::InvalidObject("Cannot rebase root commit")
}
let parent_map = rebase_tree_map(db, rfs, parent_id)
let commit_map = rebase_tree_map_from_tree(db, rfs, info.tree)
let changes = rebase_diff_maps(parent_map, commit_map)
rebase_apply_changes(base_map, parent_map, changes)
let new_tree = rebase_write_tree_from_map(fs, rfs, git_dir, base_map)
let new_commit = @bit.Commit::new(
new_tree,
[parent],
info.author,
info.author_time,
info.author_tz,
info.committer,
info.commit_time,
info.committer_tz,
info.message,
)
let (new_id, compressed) = @bit.create_commit(new_commit)
rebase_write_object(fs, rfs, git_dir, new_id, compressed)
parent = new_id
}
rebase_update_head(fs, rfs, git_dir, parent)
rebase_write_worktree_filtered(fs, db, rfs, root, git_dir, base_map, run_filter_cmd?)
let entries = rebase_map_to_index(db, rfs, base_map)
write_index_entries(fs, git_dir, entries)
parent
}
}
}
///|
priv struct RebaseCommitInfo {
tree : @bit.ObjectId
parents : Array[@bit.ObjectId]
author : String
author_time : Int64
author_tz : String
committer : String
commit_time : Int64
committer_tz : String
message : String
}
///|
priv struct TreeEntryInfo {
id : @bit.ObjectId
mode : Int
}
///|
priv struct Change {
path : String
kind : ChangeKind
entry : TreeEntryInfo?
}
///|
priv enum ChangeKind {
Added
Modified
Deleted
}
///|
fn rebase_collect_chain(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
head : @bit.ObjectId,
upstream : @bit.ObjectId,
) -> Array[@bit.ObjectId] raise @bit.GitError {
// Find the merge base between head and upstream
let merge_base = find_merge_base(db, fs, head, upstream)
guard merge_base is Some(base) else {
raise @bit.GitError::InvalidObject("No common ancestor found")
}
// Collect commits from head to merge base (exclusive)
let list : Array[@bit.ObjectId] = []
let mut current = head
while current != base {
list.push(current)
let obj = db.get(fs, current)
match obj {
None => raise @bit.GitError::InvalidObject("Missing commit object")
Some(o) => {
if o.obj_type != @bit.ObjectType::Commit {
raise @bit.GitError::InvalidObject("Object is not a commit")
}
let info = @bit.parse_commit(o.data)
if info.parents.length() == 0 {
raise @bit.GitError::InvalidObject(
"Reached root without finding base",
)
}
current = info.parents[0]
}
}
}
list.rev()
}
///|
/// Collect all commits from head to the root (initial commit with no parents).
/// Returns commits in chronological order (oldest first).
fn rebase_collect_chain_to_root(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
head : @bit.ObjectId,
) -> Array[@bit.ObjectId] raise @bit.GitError {
let list : Array[@bit.ObjectId] = []
let mut current = head
while true {
list.push(current)
let obj = db.get(fs, current)
match obj {
None => raise @bit.GitError::InvalidObject("Missing commit object")
Some(o) => {
if o.obj_type != @bit.ObjectType::Commit {
raise @bit.GitError::InvalidObject("Object is not a commit")
}
let info = @bit.parse_commit(o.data)
if info.parents.length() == 0 {
break // Reached the root commit
}
current = info.parents[0]
}
}
}
list.rev()
}
///|
/// Find the merge base (common ancestor) of two commits.
fn find_merge_base(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
commit1 : @bit.ObjectId,
commit2 : @bit.ObjectId,
) -> @bit.ObjectId? raise @bit.GitError {
// Collect all ancestors of commit1
let ancestors1 : Map[String, Bool] = Map([])
collect_ancestors(db, fs, commit1, ancestors1)
// Walk commit2's history and find first common ancestor
let visited : Map[String, Bool] = Map([])
let queue : Array[@bit.ObjectId] = [commit2]
while queue.length() > 0 {
let c = queue.remove(0)
let hex = c.to_hex()
if visited.contains(hex) {
continue
}
visited[hex] = true
if ancestors1.contains(hex) {
return Some(c)
}
let obj = db.get(fs, c)
match obj {
None => continue
Some(o) => {
if o.obj_type != @bit.ObjectType::Commit {
continue
}
let info = @bit.parse_commit(o.data)
for parent in info.parents {
queue.push(parent)
}
}
}
}
None
}
///|
/// Collect all ancestors of a commit into a set.
fn collect_ancestors(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
commit : @bit.ObjectId,
out : Map[String, Bool],
) -> Unit raise @bit.GitError {
let queue : Array[@bit.ObjectId] = [commit]
while queue.length() > 0 {
let c = queue.remove(0)
let hex = c.to_hex()
if out.contains(hex) {
continue
}
out[hex] = true
let obj = db.get(fs, c)
match obj {
None => continue
Some(o) => {
if o.obj_type != @bit.ObjectType::Commit {
continue
}
let info = @bit.parse_commit(o.data)
for parent in info.parents {
queue.push(parent)
}
}
}
}
}
///|
/// Refuse to move HEAD onto `target_map` if doing so would silently
/// overwrite an untracked file on disk, mirroring the check an ordinary
/// checkout performs. Only paths `target_map` actually writes are checked
/// (not a full worktree scan), since those are the only ones about to be
/// clobbered by `rebase_write_worktree`.
fn rebase_check_untracked_conflicts(
rfs : &@bit.RepoFileSystem,
git_dir : String,
root : String,
target_map : Map[String, TreeEntryInfo],
) -> Unit raise @bit.GitError {
let index_paths : Map[String, Bool] = Map([])
for entry in read_index_entries(rfs, git_dir) {
index_paths[entry.path] = true
}
let conflicts : Array[String] = []
for pair in target_map.to_array() {
let path = pair.0
if !index_paths.contains(path) && rfs.is_file(join_path(root, path)) {
conflicts.push(path)
}
}
if conflicts.length() > 0 {
conflicts.sort()
let listed = conflicts.map(p => "\t" + p).join("\n")
raise @bit.GitError::InvalidObject(
"The following untracked working tree files would be overwritten by checkout:\n\{listed}\nPlease move or remove them before you switch branches.\nAborting",
)
}
}
///|
fn rebase_tree_map(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
commit_id : @bit.ObjectId,
) -> Map[String, TreeEntryInfo] raise @bit.GitError {
let result : Map[String, TreeEntryInfo] = Map([])
let commit_obj = db.get(fs, commit_id)
match commit_obj {
None => raise @bit.GitError::InvalidObject("Missing commit object")
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)
rebase_collect_tree(db, fs, info.tree, "", result)
}
}
result
}
///|
/// Build tree map directly from a tree ID (not a commit ID).
fn rebase_tree_map_from_tree(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
tree_id : @bit.ObjectId,
) -> Map[String, TreeEntryInfo] raise @bit.GitError {
let result : Map[String, TreeEntryInfo] = Map([])
rebase_collect_tree(db, fs, tree_id, "", result)
result
}
///|
fn rebase_collect_tree(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
tree_id : @bit.ObjectId,
prefix : String,
out : Map[String, TreeEntryInfo],
) -> Unit raise @bit.GitError {
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 rebase_is_tree_mode(entry.mode) {
rebase_collect_tree(db, fs, entry.id, path, out)
} else {
let mode = rebase_parse_octal(entry.mode)
out[path] = { id: entry.id, mode }
}
}
}
}
}
///|
fn rebase_diff_maps(
parent_map : Map[String, TreeEntryInfo],
commit_map : Map[String, TreeEntryInfo],
) -> Array[Change] {
let changes : Array[Change] = []
let paths : Map[String, Bool] = Map([])
for p in parent_map.keys() {
paths[p] = true
}
for p in commit_map.keys() {
paths[p] = true
}
let list = paths.keys().to_array()
list.sort()
for path in list {
match (parent_map.get(path), commit_map.get(path)) {
(None, Some(e)) =>
changes.push({ path, kind: ChangeKind::Added, entry: Some(e) })
(Some(_), None) =>
changes.push({ path, kind: ChangeKind::Deleted, entry: None })
(Some(p), Some(c)) =>
if p.id != c.id || p.mode != c.mode {
changes.push({ path, kind: ChangeKind::Modified, entry: Some(c) })
}
_ => ()
}
}
changes
}
///|
fn rebase_apply_changes(
base : Map[String, TreeEntryInfo],
parent_map : Map[String, TreeEntryInfo],
changes : Array[Change],
) -> Unit raise @bit.GitError {
for ch in changes {
let path = ch.path
match ch.kind {
ChangeKind::Added => {
if base.contains(path) {
raise @bit.GitError::InvalidObject("Rebase conflict: \{path}")
}
base[path] = ch.entry.unwrap()
}
ChangeKind::Deleted =>
match (base.get(path), parent_map.get(path)) {
(Some(b), Some(p)) => {
if b.id != p.id || b.mode != p.mode {
raise @bit.GitError::InvalidObject("Rebase conflict: \{path}")
}
base.remove(path)
}
(None, None) => ()
_ => raise @bit.GitError::InvalidObject("Rebase conflict: \{path}")
}
ChangeKind::Modified => {
let entry = ch.entry.unwrap()
match (base.get(path), parent_map.get(path)) {
(Some(b), Some(p)) => {
if b.id != p.id || b.mode != p.mode {
raise @bit.GitError::InvalidObject("Rebase conflict: \{path}")
}
base[path] = entry
}
(None, None) => base[path] = entry
_ => raise @bit.GitError::InvalidObject("Rebase conflict: \{path}")
}
}
}
}
}
///|
fn rebase_write_tree_from_map(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
map : Map[String, TreeEntryInfo],
) -> @bit.ObjectId raise @bit.GitError {
let entries : Array[IndexEntry] = []
for path, e in map {
entries.push({
path,
id: e.id,
mode: e.mode,
size: 0,
mtime_sec: 0,
mtime_nsec: 0,
intent_to_add: false,
dev: 0,
ino: 0,
uid: 0,
gid: 0,
})
}
entries.sort_by((a, b) => String::lexical_compare(a.path, b.path))
let rel_entries = entries.map(e => e)
rebase_write_tree_recursive(fs, rfs, git_dir, rel_entries)
}
///|
fn rebase_write_tree_recursive(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
entries : Array[IndexEntry],
) -> @bit.ObjectId raise @bit.GitError {
let file_entries : Array[@bit.TreeEntry] = []
let dir_map : Map[String, Array[IndexEntry]] = Map([])
for e in entries {
match rebase_split_first(e.path) {
(name, None) => {
let mode = rebase_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 = rebase_write_tree_recursive(fs, rfs, git_dir, list)
file_entries.push(@bit.TreeEntry::new("040000", dir_name, sub_id))
}
file_entries.sort_by((a, b) => String::lexical_compare(a.name, b.name))
let (tree_id, compressed) = @bit.create_tree(file_entries)
rebase_write_object(fs, rfs, git_dir, tree_id, compressed)
tree_id
}
///|
fn rebase_write_worktree(
fs : &@bit.FileSystem,
db : ObjectDb,
rfs : &@bit.RepoFileSystem,
root : String,
git_dir : String,
map : Map[String, TreeEntryInfo],
) -> Unit raise @bit.GitError {
let files : Map[String, TreeFileEntry] = Map([])
for path, info in map {
files[path] = { id: info.id, mode: info.mode }
}
tree_ops_remove_missing_paths(fs, rfs, root, git_dir, files, false)
rebase_write_worktree_except(fs, db, rfs, root, git_dir, map, [])
}
///|
/// Write worktree from map, skipping files in the except list.
fn rebase_write_worktree_except(
fs : &@bit.FileSystem,
db : ObjectDb,
rfs : &@bit.RepoFileSystem,
root : String,
git_dir : String,
map : Map[String, TreeEntryInfo],
except : Array[String],
) -> Unit raise @bit.GitError {
let autocrlf = read_autocrlf_setting(rfs, git_dir)
let core_eol = read_core_eol_setting(rfs, git_dir)
let except_set : Map[String, Bool] = Map([])
for path in except {
except_set[path] = true
}
for path, info in map {
if except_set.contains(path) {
continue // Skip conflicting files
}
let full = join_path(root, path)
if is_gitlink_mode_int(info.mode) {
fs.mkdir_p(full)
continue
}
if rfs.is_dir(full) {
let bit_marker = join_path(full, ".git")
if rfs.is_file(bit_marker) || rfs.is_dir(bit_marker) {
raise @bit.GitError::InvalidObject(
"Refusing to overwrite submodule: \{path}",
)
}
}
let obj = db.get(rfs, info.id)
match obj {
Some(o) => {
if o.obj_type != @bit.ObjectType::Blob {
raise @bit.GitError::InvalidObject("Object is not a blob")
}
let dir = rebase_parent_dir(full)
fs.mkdir_p(dir)
let attrs = resolve_eol_attrs(rfs, root, path)
let output = smudge_for_checkout(o.data, attrs, autocrlf, core_eol~)
fs.write_file(full, output)
}
None => raise @bit.GitError::InvalidObject("Missing blob object")
}
}
}
///|
/// Same as `rebase_write_worktree`, but also runs the external filter
/// driver's smudge command (from `.gitattributes`' `filter=name`) on each
/// blob before writing it, matching git's checkout-time content conversion.
async fn rebase_write_worktree_filtered(
fs : &@bit.FileSystem,
db : ObjectDb,
rfs : &@bit.RepoFileSystem,
root : String,
git_dir : String,
map : Map[String, TreeEntryInfo],
run_filter_cmd? : FilterCmd,
) -> Unit raise @bit.GitError {
let files : Map[String, TreeFileEntry] = Map([])
for path, info in map {
files[path] = { id: info.id, mode: info.mode }
}
tree_ops_remove_missing_paths(fs, rfs, root, git_dir, files, false)
rebase_write_worktree_except_filtered(
fs, db, rfs, root, git_dir, map, [], run_filter_cmd?,
)
}
///|
/// Same as `rebase_write_worktree_except`, but also runs the external
/// filter driver's smudge command on each blob before writing it.
async fn rebase_write_worktree_except_filtered(
fs : &@bit.FileSystem,
db : ObjectDb,
rfs : &@bit.RepoFileSystem,
root : String,
git_dir : String,
map : Map[String, TreeEntryInfo],
except : Array[String],
run_filter_cmd? : FilterCmd,
) -> Unit raise @bit.GitError {
let autocrlf = read_autocrlf_setting(rfs, git_dir)
let core_eol = read_core_eol_setting(rfs, git_dir)
let except_set : Map[String, Bool] = Map([])
for path in except {
except_set[path] = true
}
for path, info in map {
if except_set.contains(path) {
continue // Skip conflicting files
}
let full = join_path(root, path)
if is_gitlink_mode_int(info.mode) {
fs.mkdir_p(full)
continue
}
if rfs.is_dir(full) {
let bit_marker = join_path(full, ".git")
if rfs.is_file(bit_marker) || rfs.is_dir(bit_marker) {
raise @bit.GitError::InvalidObject(
"Refusing to overwrite submodule: \{path}",
)
}
}
let obj = db.get(rfs, info.id)
match obj {
Some(o) => {
if o.obj_type != @bit.ObjectType::Blob {
raise @bit.GitError::InvalidObject("Object is not a blob")
}
let dir = rebase_parent_dir(full)
fs.mkdir_p(dir)
let attrs = resolve_eol_attrs(rfs, root, path)
let filtered = apply_filter_smudge(
rfs, o.data, attrs, root, git_dir, run_filter_cmd,
)
let output = smudge_for_checkout(filtered, attrs, autocrlf, core_eol~)
fs.write_file(full, output)
}
None => raise @bit.GitError::InvalidObject("Missing blob object")
}
}
}
///|
fn rebase_map_to_index(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
map : Map[String, TreeEntryInfo],
) -> Array[IndexEntry] raise @bit.GitError {
let entries : Array[IndexEntry] = []
for path, e in map {
if is_gitlink_mode_int(e.mode) {
entries.push({
path,
id: e.id,
mode: e.mode,
size: 0,
mtime_sec: 0,
mtime_nsec: 0,
intent_to_add: false,
dev: 0,
ino: 0,
uid: 0,
gid: 0,
})
continue
}
let obj = db.get(fs, e.id)
match obj {
Some(o) => {
if o.obj_type != @bit.ObjectType::Blob {
raise @bit.GitError::InvalidObject("Object is not a blob")
}
entries.push({
path,
id: e.id,
mode: e.mode,
size: o.data.length(),
mtime_sec: 0,
mtime_nsec: 0,
intent_to_add: false,
dev: 0,
ino: 0,
uid: 0,
gid: 0,
})
}
None => raise @bit.GitError::InvalidObject("Missing blob object")
}
}
entries.sort_by((a, b) => String::compare(a.path, b.path))
entries
}
///|
fn rebase_update_head(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
commit_id : @bit.ObjectId,
) -> Unit raise @bit.GitError {
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 ref_path = join_path(git_dir, "refs/heads/" + name)
let dir = rebase_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 rebase_parse_commit_full(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
commit_id : @bit.ObjectId,
) -> RebaseCommitInfo raise @bit.GitError {
let obj = db.get(fs, commit_id)
match obj {
None => raise @bit.GitError::InvalidObject("Missing commit object")
Some(o) => {
if o.obj_type != @bit.ObjectType::Commit {
raise @bit.GitError::InvalidObject("Object is not a commit")
}
rebase_parse_commit_content(o.data)
}
}
}
///|
fn rebase_parse_commit_content(
data : Bytes,
) -> RebaseCommitInfo raise @bit.GitError {
let text = @utf8.decode_lossy(data[:])
let mut header = text
let mut message = ""
match text.find("\n\n") {
None => ()
Some(idx) => {
header = String::unsafe_substring(text, start=0, end=idx)
message = String::unsafe_substring(text, start=idx + 2, end=text.length())
}
}
let mut tree_id : @bit.ObjectId? = None
let parents : Array[@bit.ObjectId] = []
let mut author = ""
let mut author_time = 0L
let mut author_tz = "+0000"
let mut committer = ""
let mut commit_time = 0L
let mut committer_tz = "+0000"
for line_view in header.split("\n") {
let line = line_view.to_owned()
if line.has_prefix("tree ") {
let hex = String::unsafe_substring(line, start=5, end=line.length())
tree_id = hex |> @bit.ObjectId::from_hex |> Some
} else if line.has_prefix("parent ") {
let hex = String::unsafe_substring(line, start=7, end=line.length())
parents.push(@bit.ObjectId::from_hex(hex))
} else if line.has_prefix("author ") {
let rest = String::unsafe_substring(line, start=7, end=line.length())
let (name, time, tz) = rebase_parse_sig_line(rest)
author = name
author_time = time
author_tz = tz
} else if line.has_prefix("committer ") {
let rest = String::unsafe_substring(line, start=10, end=line.length())
let (name, time, tz) = rebase_parse_sig_line(rest)
committer = name
commit_time = time
committer_tz = tz
}
}
match tree_id {
None => raise @bit.GitError::InvalidObject("Missing tree in commit")
Some(tree) =>
{
tree,
parents,
author,
author_time,
author_tz,
committer,
commit_time,
committer_tz,
message,
}
}
}
///|
fn rebase_parse_sig_line(line : String) -> (String, Int64, String) {
let last = line.rev_find(" ")
if last is None {
return (line, 0L, "+0000")
}
let tz_idx = last.unwrap()
let before_tz = String::unsafe_substring(line, start=0, end=tz_idx)
let tz = String::unsafe_substring(line, start=tz_idx + 1, end=line.length())
let last2 = before_tz.rev_find(" ")
if last2 is None {
return (before_tz, 0L, tz)
}
let time_idx = last2.unwrap()
let name = String::unsafe_substring(before_tz, start=0, end=time_idx)
let time_str = String::unsafe_substring(
before_tz,
start=time_idx + 1,
end=before_tz.length(),
)
let time = rebase_parse_int64(time_str)
(name, time, tz)
}
///|
fn rebase_parse_int64(s : String) -> Int64 {
let mut result = 0L
for c in s {
if c < '0' || c > '9' {
continue
}
let digit = c.to_int() - '0'.to_int()
result = result * 10L + digit.to_int64()
}
result
}
///|
fn rebase_write_object(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
id : @bit.ObjectId,
compressed : Bytes,
) -> Unit raise @bit.GitError {
// A linked worktree's git dir has no objects/ of its own; write through
// to the shared object store (main repo's git dir via "commondir").
let git_dir = object_storage_git_dir(rfs, git_dir)
let hex = id.to_hex()
let dir = join_path(
git_dir,
"objects/" + String::unsafe_substring(hex, start=0, end=2),
)
fs.mkdir_p(dir)
let path = join_path(
git_dir,
"objects/" +
String::unsafe_substring(hex, start=0, end=2) +
"/" +
String::unsafe_substring(hex, start=2, end=hex.length()),
)
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
}
}
///|
fn rebase_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))
}
}
}
///|
fn rebase_mode_to_string(mode : Int) -> String {
@string_utils.mode_to_string(mode)
}
///|
fn rebase_is_tree_mode(mode : String) -> Bool {
mode == "40000" || mode == "040000"
}
///|
fn rebase_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
}
///|
fn rebase_parent_dir(path : String) -> String {
match path.rev_find("/") {
None => "/"
Some(0) => "/"
Some(i) => String::unsafe_substring(path, start=0, end=i)
}
}
// ============================================================================
// Rebase with conflict handling
// ============================================================================
///|
/// Check if a rebase is in progress.
pub fn is_rebase_in_progress(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> Bool {
let state_dir = join_path(git_dir, "rebase-merge")
fs.is_dir(state_dir)
}
///|
/// Save rebase state to .git/rebase-merge/
fn save_rebase_state(
fs : &@bit.FileSystem,
git_dir : String,
state : RebaseState,
) -> Unit raise @bit.GitError {
let state_dir = join_path(git_dir, "rebase-merge")
fs.mkdir_p(state_dir)
fs.write_string(join_path(state_dir, "onto"), state.onto.to_hex() + "\n")
fs.write_string(
join_path(state_dir, "orig-head"),
state.orig_head.to_hex() + "\n",
)
fs.write_string(join_path(state_dir, "head-name"), state.head_name + "\n")
// Write todo list
let todo_lines = state.todo.map(id => "pick " + id.to_hex())
fs.write_string(
join_path(state_dir, "git-rebase-todo"),
todo_lines.join("\n") + "\n",
)
// Write done list
let done_lines = state.done.map(id => "pick " + id.to_hex())
fs.write_string(
join_path(state_dir, "done"),
if done_lines.length() > 0 {
done_lines.join("\n") + "\n"
} else {
""
},
)
// Write current commit info
match state.current {
Some(id) => {
fs.write_string(join_path(state_dir, "stopped-sha"), id.to_hex() + "\n")
// REBASE_HEAD points to the commit being applied (used by hooks/tools)
fs.write_string(join_path(git_dir, "REBASE_HEAD"), id.to_hex() + "\n")
fs.write_string(join_path(state_dir, "message"), state.message)
fs.write_string(
join_path(state_dir, "author-script"),
"GIT_AUTHOR_NAME='\{state.author}'\nGIT_AUTHOR_DATE='\{state.author_time} \{state.author_tz}'\n",
)
}
None => ()
}
// Write rewritten-list: " " per line, matching git's
// own on-disk format (used to drive notes.rewrite.rebase copying).
if state.rewritten.length() > 0 {
let rewritten_lines = state.rewritten.map(pair => pair.0.to_hex() +
" " +
pair.1.to_hex())
fs.write_string(
join_path(state_dir, "rewritten-list"),
rewritten_lines.join("\n") + "\n",
)
}
}
///|
/// Load rebase state from .git/rebase-merge/
pub fn load_rebase_state(
fs : &@bit.RepoFileSystem,
git_dir : String,
) -> RebaseState? raise @bit.GitError {
let state_dir = join_path(git_dir, "rebase-merge")
if !fs.is_dir(state_dir) {
return None
}
let onto_hex = read_file_trimmed(fs, join_path(state_dir, "onto"))
let orig_head_hex = read_file_trimmed(fs, join_path(state_dir, "orig-head"))
let head_name = read_file_trimmed(fs, join_path(state_dir, "head-name"))
let todo = parse_todo_file(fs, join_path(state_dir, "git-rebase-todo"))
let done = parse_todo_file(fs, join_path(state_dir, "done"))
let current = if fs.is_file(join_path(state_dir, "stopped-sha")) {
let hex = read_file_trimmed(fs, join_path(state_dir, "stopped-sha"))
hex |> @bit.ObjectId::from_hex |> Some
} else {
None
}
let message = if fs.is_file(join_path(state_dir, "message")) {
@utf8.decode_lossy(fs.read_file(join_path(state_dir, "message"))[:])
} else {
""
}
let rewritten_list_path = join_path(state_dir, "rewritten-list")
let rewritten : Array[(@bit.ObjectId, @bit.ObjectId)] = if fs.is_file(
rewritten_list_path,
) {
let content = @utf8.decode_lossy(fs.read_file(rewritten_list_path)[:])
let out : Array[(@bit.ObjectId, @bit.ObjectId)] = []
for line_view in content.split("\n") {
let line = line_view.to_owned()
if line.length() == 0 {
continue
}
match line.find(" ") {
Some(idx) => {
let old_hex = String::unsafe_substring(line, start=0, end=idx)
let new_hex = String::unsafe_substring(
line,
start=idx + 1,
end=line.length(),
)
out.push(
(@bit.ObjectId::from_hex(old_hex), @bit.ObjectId::from_hex(new_hex)),
)
}
None => ()
}
}
out
} else {
[]
}
Some({
onto: @bit.ObjectId::from_hex(onto_hex),
orig_head: @bit.ObjectId::from_hex(orig_head_hex),
head_name,
todo,
done,
current,
message,
author: "",
author_time: 0L,
author_tz: "+0000",
rewritten,
})
}
///|
fn read_file_trimmed(fs : &@bit.RepoFileSystem, path : String) -> String {
let content = @utf8.decode_lossy(
(fs.read_file(path) catch { _ => Default::default() })[:],
)
let mut end = content.length()
while end > 0 {
let c = content.unsafe_get(end - 1)
if c == '\n' || c == '\r' || c == ' ' || c == '\t' {
end -= 1
} else {
break
}
}
String::unsafe_substring(content, start=0, end~)
}
///|
fn parse_todo_file(
fs : &@bit.RepoFileSystem,
path : String,
) -> Array[@bit.ObjectId] {
let result : Array[@bit.ObjectId] = []
if !fs.is_file(path) {
return result
}
let content = @utf8.decode_lossy(
(fs.read_file(path) catch { _ => Default::default() })[:],
)
for line_view in content.split("\n") {
let line = line_view.to_owned()
if line.length() == 0 || line.has_prefix("#") {
continue
}
// Format: "pick " or just ""
let parts = line.split(" ").map(v => v.to_owned()).collect()
if parts.length() >= 2 && parts[0] == "pick" {
let id = @bit.ObjectId::from_hex(parts[1]) catch { _ => continue }
result.push(id)
} else if parts.length() >= 1 &&
(parts[0].length() == 40 || parts[0].length() == 64) {
let id = @bit.ObjectId::from_hex(parts[0]) catch { _ => continue }
result.push(id)
}
}
result
}
///|
/// Clear rebase state (remove .git/rebase-merge/)
pub fn clear_rebase_state(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> Unit raise @bit.GitError {
let state_dir = join_path(git_dir, "rebase-merge")
if !rfs.is_dir(state_dir) {
return ()
}
// Remove all files in state_dir
let files = rfs.readdir(state_dir)
for file in files {
let path = join_path(state_dir, file)
if rfs.is_dir(path) {
fs.remove_dir(path) catch {
_ => ()
}
} else {
fs.remove_file(path) catch {
_ => ()
}
}
}
// Remove the directory
fs.remove_dir(state_dir) catch {
_ => ()
}
// Remove REBASE_HEAD from git_dir root (written on conflict)
let rebase_head = join_path(git_dir, "REBASE_HEAD")
if rfs.is_file(rebase_head) {
fs.remove_file(rebase_head) catch {
_ => ()
}
}
}
///|
/// Start a rebase operation. Returns RebaseResult indicating status.
pub async fn rebase_start(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
upstream : @bit.ObjectId,
run_filter_cmd? : FilterCmd,
) -> RebaseResult raise @bit.GitError {
rebase_start_with_onto(fs, rfs, root, upstream, upstream, run_filter_cmd?)
}
///|
/// Start a rebase operation with an explicit new base.
pub async fn rebase_start_with_onto(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
onto : @bit.ObjectId,
upstream : @bit.ObjectId,
force_rebase? : Bool = false,
run_filter_cmd? : FilterCmd,
) -> RebaseResult raise @bit.GitError {
let git_dir = rebase_resolve_git_dir(rfs, root)
// Check if rebase already in progress
if is_rebase_in_progress(rfs, git_dir) {
raise @bit.GitError::InvalidObject(
"A rebase is already in progress. Use --continue, --abort, or --skip.",
)
}
let head = resolve_head_commit(rfs, git_dir)
match head {
None => {
fast_forward_to(fs, rfs, root, onto, run_filter_cmd?)
return {
status: RebaseStatus::Complete,
commit_id: Some(onto),
conflicts: [],
rewritten: [],
}
}
Some(h) => {
if !force_rebase && h == onto && h == upstream {
return {
status: RebaseStatus::NothingToRebase,
commit_id: Some(h),
conflicts: [],
rewritten: [],
}
}
let db = ObjectDb::load(rfs, git_dir)
let chain = rebase_collect_chain(db, rfs, h, upstream)
if chain.length() == 0 {
if !force_rebase {
if h != onto {
fast_forward_to(fs, rfs, root, onto, run_filter_cmd?)
return {
status: RebaseStatus::Complete,
commit_id: Some(onto),
conflicts: [],
rewritten: [],
}
}
return {
status: RebaseStatus::NothingToRebase,
commit_id: Some(h),
conflicts: [],
rewritten: [],
}
}
}
// Get head name for later restoration
let head_name = match read_head_ref(rfs, git_dir) {
HeadRef::Branch(name) => "refs/heads/" + name
HeadRef::Detached(_) => ""
}
let onto_map = rebase_tree_map(db, rfs, onto)
rebase_check_untracked_conflicts(rfs, git_dir, root, onto_map)
// Create initial state
let state : RebaseState = {
onto,
orig_head: h,
head_name,
todo: chain,
done: [],
current: None,
message: "",
author: "",
author_time: 0L,
author_tz: "+0000",
rewritten: [],
}
save_rebase_state(fs, git_dir, state)
// Move HEAD to the new base before applying commits.
rebase_update_head(fs, rfs, git_dir, onto)
rebase_write_worktree_filtered(fs, db, rfs, root, git_dir, onto_map, run_filter_cmd?)
let entries = rebase_map_to_index(db, rfs, onto_map)
write_index_entries(fs, git_dir, entries)
// Start applying commits
rebase_apply_next(fs, rfs, root, state, run_filter_cmd?)
}
}
}
///|
/// Continue rebase after resolving conflicts.
pub async fn rebase_continue(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
run_filter_cmd? : FilterCmd,
) -> RebaseResult raise @bit.GitError {
let git_dir = rebase_resolve_git_dir(rfs, root)
let state = load_rebase_state(rfs, git_dir)
guard state is Some(s) else {
raise @bit.GitError::InvalidObject("No rebase in progress")
}
// Commit the resolved changes
guard s.current is Some(current_id) else {
raise @bit.GitError::InvalidObject("No commit to continue")
}
let db = ObjectDb::load(rfs, git_dir)
let info = rebase_parse_commit_full(db, rfs, current_id)
// Get current HEAD as parent
let head = resolve_head_commit(rfs, git_dir)
guard head is Some(parent) else {
raise @bit.GitError::InvalidObject("No HEAD commit")
}
// Build tree from current index
let entries = read_index_entries(rfs, git_dir)
let new_tree = write_tree_from_entries(fs, rfs, git_dir, entries, None)
// Create new commit with original author info
let new_commit = @bit.Commit::new(
new_tree,
[parent],
info.author,
info.author_time,
info.author_tz,
info.committer,
info.commit_time,
info.committer_tz,
info.message,
)
let (new_id, compressed) = @bit.create_commit(new_commit)
rebase_write_object(fs, rfs, git_dir, new_id, compressed)
rebase_update_head(fs, rfs, git_dir, new_id)
// Update state: move current to done, clear current
let new_done = s.done.copy()
new_done.push(current_id)
let new_todo = s.todo.copy()
if new_todo.length() > 0 {
ignore(new_todo.remove(0))
}
let new_rewritten = s.rewritten.copy()
new_rewritten.push((current_id, new_id))
let new_state : RebaseState = {
onto: s.onto,
orig_head: s.orig_head,
head_name: s.head_name,
todo: new_todo,
done: new_done,
current: None,
message: "",
author: "",
author_time: 0L,
author_tz: "+0000",
rewritten: new_rewritten,
}
save_rebase_state(fs, git_dir, new_state)
// Continue with next commits
rebase_apply_next(fs, rfs, root, new_state, run_filter_cmd?)
}
///|
/// Abort the current rebase and restore original state.
pub async fn rebase_abort(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
run_filter_cmd? : FilterCmd,
) -> Unit raise @bit.GitError {
let git_dir = rebase_resolve_git_dir(rfs, root)
let state = load_rebase_state(rfs, git_dir)
guard state is Some(s) else {
raise @bit.GitError::InvalidObject("No rebase in progress")
}
// Restore original HEAD
let head_path = join_path(git_dir, "HEAD")
if s.head_name.length() > 0 {
fs.write_string(head_path, "ref: " + s.head_name + "\n")
let ref_path = join_path(git_dir, s.head_name)
let dir = rebase_parent_dir(ref_path)
fs.mkdir_p(dir)
fs.write_string(ref_path, s.orig_head.to_hex() + "\n")
} else {
fs.write_string(head_path, s.orig_head.to_hex() + "\n")
}
// Restore worktree to original HEAD
let db = ObjectDb::load(rfs, git_dir)
let tree_map = rebase_tree_map(db, rfs, s.orig_head)
rebase_write_worktree_filtered(fs, db, rfs, root, git_dir, tree_map, run_filter_cmd?) catch {
_ => ()
}
let entries = rebase_map_to_index(db, rfs, tree_map) catch { _ => [] }
write_index_entries(fs, git_dir, entries) catch {
_ => ()
}
// Clear rebase state
clear_rebase_state(fs, rfs, git_dir)
}
///|
/// Skip the current commit and continue rebase.
pub async fn rebase_skip(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
run_filter_cmd? : FilterCmd,
) -> RebaseResult raise @bit.GitError {
let git_dir = rebase_resolve_git_dir(rfs, root)
let state = load_rebase_state(rfs, git_dir)
guard state is Some(s) else {
raise @bit.GitError::InvalidObject("No rebase in progress")
}
guard s.current is Some(_) else {
raise @bit.GitError::InvalidObject("No commit to skip")
}
// Drop the current commit from the remaining todo list.
let new_todo = s.todo.copy()
if new_todo.length() > 0 {
ignore(new_todo.remove(0))
}
let new_state : RebaseState = {
onto: s.onto,
orig_head: s.orig_head,
head_name: s.head_name,
todo: new_todo,
done: s.done,
current: None,
message: "",
author: "",
author_time: 0L,
author_tz: "+0000",
rewritten: s.rewritten,
}
save_rebase_state(fs, git_dir, new_state)
// Reset worktree to current HEAD
let db = ObjectDb::load(rfs, git_dir)
let head = resolve_head_commit(rfs, git_dir)
guard head is Some(h) else {
raise @bit.GitError::InvalidObject("No HEAD commit")
}
let tree_map = rebase_tree_map(db, rfs, h)
rebase_write_worktree_filtered(fs, db, rfs, root, git_dir, tree_map, run_filter_cmd?)
let entries = rebase_map_to_index(db, rfs, tree_map)
write_index_entries(fs, git_dir, entries)
// Continue with next commits
rebase_apply_next(fs, rfs, root, new_state, run_filter_cmd?)
}
///|
fn rebase_tree_entry_matches(
lhs : TreeEntryInfo?,
rhs : TreeEntryInfo?,
) -> Bool {
match (lhs, rhs) {
(Some(left), Some(right)) => left.id == right.id && left.mode == right.mode
(None, None) => true
_ => false
}
}
///|
fn rebase_compute_patch_id(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
commit_id : @bit.ObjectId,
) -> String {
let obj = db.get(fs, commit_id) catch { _ => return commit_id.to_hex() }
match obj {
Some(o) if o.obj_type == @bit.ObjectType::Commit => {
let info = @bit.parse_commit(o.data) catch {
_ => return commit_id.to_hex()
}
let tree_entries = rebase_get_patch_tree_entries(db, fs, info.tree, "")
let parent_entries : Array[(String, String)] = if info.parents.length() >
0 {
let parent_obj = db.get(fs, info.parents[0]) catch {
_ => return commit_id.to_hex()
}
match parent_obj {
Some(parent_commit) if parent_commit.obj_type ==
@bit.ObjectType::Commit => {
let parent_info = @bit.parse_commit(parent_commit.data) catch {
_ => return commit_id.to_hex()
}
rebase_get_patch_tree_entries(db, fs, parent_info.tree, "")
}
_ => []
}
} else {
[]
}
let diff = rebase_compute_patch_tree_diff(parent_entries, tree_entries)
diff.sort_by((a, b) => a.0.compare(b.0))
let parts : Array[String] = []
for item in diff {
let (path, change) = item
parts.push(path + ":" + change)
}
let content = parts.join("\n")
let bytes = Bytes::from_array(
FixedArray::makei(content.length(), fn(i) {
content[i].to_int().to_byte()
}),
)
@bit.sha1(bytes).to_hex()
}
_ => commit_id.to_hex()
}
}
///|
fn rebase_get_patch_tree_entries(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
tree_id : @bit.ObjectId,
prefix : String,
) -> Array[(String, String)] {
let result : Array[(String, String)] = []
let obj = db.get(fs, tree_id) catch { _ => return result }
match obj {
Some(o) if o.obj_type == @bit.ObjectType::Tree => {
let entries = @bit.parse_tree(o.data) catch { _ => return result }
for entry in entries {
let path = if prefix.length() == 0 {
entry.name
} else {
prefix + "/" + entry.name
}
if entry.mode.has_prefix("04") {
for sub in rebase_get_patch_tree_entries(db, fs, entry.id, path) {
result.push(sub)
}
} else {
result.push((path, entry.id.to_hex() + ":" + entry.mode))
}
}
}
_ => ()
}
result
}
///|
fn rebase_compute_patch_tree_diff(
parent : Array[(String, String)],
current : Array[(String, String)],
) -> Array[(String, String)] {
let parent_map : Map[String, String] = Map([])
for item in parent {
let (path, id) = item
parent_map[path] = id
}
let diff : Array[(String, String)] = []
for item in current {
let (path, id) = item
match parent_map.get(path) {
Some(parent_id) => if parent_id != id { diff.push((path, "M:" + id)) }
None => diff.push((path, "A:" + id))
}
}
let current_map : Map[String, String] = Map([])
for item in current {
let (path, id) = item
current_map[path] = id
}
for item in parent {
let (path, _) = item
if !current_map.contains(path) {
diff.push((path, "D"))
}
}
diff
}
///|
fn rebase_patch_id_exists_in_ancestors(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
commit_id : @bit.ObjectId,
head_id : @bit.ObjectId,
) -> Bool raise @bit.GitError {
let target_patch_id = rebase_compute_patch_id(db, fs, commit_id)
let visited : Map[String, Bool] = Map([])
let queue : Array[@bit.ObjectId] = [head_id]
while queue.length() > 0 {
let current = queue.remove(0)
let hex = current.to_hex()
if visited.contains(hex) {
continue
}
visited[hex] = true
if rebase_compute_patch_id(db, fs, current) == target_patch_id {
return true
}
let obj = db.get(fs, current)
match obj {
Some(o) if o.obj_type == @bit.ObjectType::Commit => {
let info = @bit.parse_commit(o.data)
for parent in info.parents {
queue.push(parent)
}
}
_ => ()
}
}
false
}
///|
fn rebase_change_already_applied(
db : ObjectDb,
rfs : &@bit.RepoFileSystem,
base : Map[String, TreeEntryInfo],
parent_map : Map[String, TreeEntryInfo],
ch : Change,
) -> Bool {
let current = base.get(ch.path)
match ch.kind {
ChangeKind::Added => rebase_tree_entry_matches(current, ch.entry)
ChangeKind::Deleted => current is None
ChangeKind::Modified =>
match (current, parent_map.get(ch.path), ch.entry) {
(Some(base_entry), Some(parent_entry), Some(target_entry)) => {
if base_entry.id == target_entry.id &&
base_entry.mode == target_entry.mode {
return true
}
if base_entry.mode != parent_entry.mode ||
base_entry.mode != target_entry.mode {
return false
}
let base_data = rebase_get_blob_data(db, rfs, base_entry.id)
let parent_data = rebase_get_blob_data(db, rfs, parent_entry.id)
let target_data = rebase_get_blob_data(db, rfs, target_entry.id)
if @diff3.is_binary_content(base_data) ||
@diff3.is_binary_content(parent_data) ||
@diff3.is_binary_content(target_data) {
return false
}
let base_content = get_blob_content(db, rfs, base_entry.id)
let parent_content = get_blob_content(db, rfs, parent_entry.id)
let target_content = get_blob_content(db, rfs, target_entry.id)
let merge_result = @diff3.content_merge(
parent_content,
base_content,
target_content,
@diff3.ContentMergeOptions::default(),
)
!merge_result.has_conflicts && merge_result.content == base_content
}
_ => false
}
}
}
///|
fn rebase_changes_already_applied(
db : ObjectDb,
rfs : &@bit.RepoFileSystem,
base : Map[String, TreeEntryInfo],
parent_map : Map[String, TreeEntryInfo],
changes : Array[Change],
) -> Bool {
if changes.length() == 0 {
return false
}
for ch in changes {
if !rebase_change_already_applied(db, rfs, base, parent_map, ch) {
return false
}
}
true
}
///|
/// Apply the next commit in the todo list.
async fn rebase_apply_next(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
root : String,
state : RebaseState,
run_filter_cmd? : FilterCmd,
) -> RebaseResult raise @bit.GitError {
let git_dir = rebase_resolve_git_dir(rfs, root)
let db = ObjectDb::load(rfs, git_dir)
if state.todo.length() == 0 {
// All done, finalize rebase
// Restore branch ref if applicable
let head = resolve_head_commit(rfs, git_dir)
guard head is Some(final_id) else {
raise @bit.GitError::InvalidObject("No HEAD after rebase")
}
let final_tree_map = rebase_tree_map(db, rfs, final_id)
rebase_write_worktree_filtered(fs, db, rfs, root, git_dir, final_tree_map, run_filter_cmd?)
let final_entries = rebase_map_to_index(db, rfs, final_tree_map)
write_index_entries(fs, git_dir, final_entries)
clear_rebase_state(fs, rfs, git_dir)
if state.head_name.length() > 0 {
let head_path = join_path(git_dir, "HEAD")
fs.write_string(head_path, "ref: " + state.head_name + "\n")
let ref_path = join_path(git_dir, state.head_name)
fs.write_string(ref_path, final_id.to_hex() + "\n")
}
return {
status: RebaseStatus::Complete,
commit_id: head,
conflicts: [],
rewritten: state.rewritten,
}
}
let commit_id = state.todo[0]
let info = rebase_parse_commit_full(db, rfs, commit_id)
let parent_id = if info.parents.length() > 0 {
info.parents[0]
} else {
raise @bit.GitError::InvalidObject("Cannot rebase root commit")
}
// Get current HEAD as the base
let head = resolve_head_commit(rfs, git_dir)
guard head is Some(base_id) else {
raise @bit.GitError::InvalidObject("No HEAD commit")
}
let base_map = rebase_tree_map(db, rfs, base_id)
let parent_map = rebase_tree_map(db, rfs, parent_id)
let commit_map = rebase_tree_map_from_tree(db, rfs, info.tree)
let changes = rebase_diff_maps(parent_map, commit_map)
if rebase_patch_id_exists_in_ancestors(db, rfs, commit_id, base_id) ||
rebase_changes_already_applied(db, rfs, base_map, parent_map, changes) {
let new_todo = state.todo.copy()
ignore(new_todo.remove(0))
let new_state : RebaseState = {
onto: state.onto,
orig_head: state.orig_head,
head_name: state.head_name,
todo: new_todo,
done: state.done,
current: None,
message: "",
author: "",
author_time: 0L,
author_tz: "+0000",
rewritten: state.rewritten,
}
save_rebase_state(fs, git_dir, new_state)
return rebase_apply_next(fs, rfs, root, new_state, run_filter_cmd?)
}
// Try to apply changes, detecting conflicts
let conflicts = rebase_apply_changes_with_conflicts(
db, rfs, fs, root, base_map, parent_map, changes,
)
if conflicts.length() > 0 {
// Conflict detected - save state and stop
let new_state : RebaseState = {
onto: state.onto,
orig_head: state.orig_head,
head_name: state.head_name,
todo: state.todo,
done: state.done,
current: Some(commit_id),
message: info.message,
author: info.author,
author_time: info.author_time,
author_tz: info.author_tz,
rewritten: state.rewritten,
}
save_rebase_state(fs, git_dir, new_state)
// Write partially merged state to worktree and index (skip conflicting files)
rebase_write_worktree_except_filtered(
fs, db, rfs, root, git_dir, base_map, conflicts, run_filter_cmd?,
)
let entries = rebase_map_to_index(db, rfs, base_map)
write_index_entries(fs, git_dir, entries)
return {
status: RebaseStatus::Conflict,
commit_id: Some(commit_id),
conflicts,
rewritten: state.rewritten,
}
}
// No conflicts - create new commit
let new_tree = rebase_write_tree_from_map(fs, rfs, git_dir, base_map)
let new_commit = @bit.Commit::new(
new_tree,
[base_id],
info.author,
info.author_time,
info.author_tz,
info.committer,
info.commit_time,
info.committer_tz,
info.message,
)
let (new_id, compressed) = @bit.create_commit(new_commit)
rebase_write_object(fs, rfs, git_dir, new_id, compressed)
rebase_update_head(fs, rfs, git_dir, new_id)
// Update state and continue
let new_done = state.done.copy()
new_done.push(commit_id)
let new_todo = state.todo.copy()
ignore(new_todo.remove(0))
let new_rewritten = state.rewritten.copy()
new_rewritten.push((commit_id, new_id))
let new_state : RebaseState = {
onto: state.onto,
orig_head: state.orig_head,
head_name: state.head_name,
todo: new_todo,
done: new_done,
current: None,
message: "",
author: "",
author_time: 0L,
author_tz: "+0000",
rewritten: new_rewritten,
}
save_rebase_state(fs, git_dir, new_state)
// Recursively apply next
rebase_apply_next(fs, rfs, root, new_state, run_filter_cmd?)
}
///|
/// Apply changes with conflict detection and conflict markers.
fn rebase_apply_changes_with_conflicts(
db : ObjectDb,
rfs : &@bit.RepoFileSystem,
fs : &@bit.FileSystem,
root : String,
base : Map[String, TreeEntryInfo],
parent_map : Map[String, TreeEntryInfo],
changes : Array[Change],
) -> Array[String] raise @bit.GitError {
let conflicts : Array[String] = []
for ch in changes {
let path = ch.path
match ch.kind {
ChangeKind::Added =>
if base.contains(path) {
// Conflict: file exists in base but commit adds it
let base_entry = base.get(path).unwrap()
let new_entry = ch.entry.unwrap()
if base_entry.id != new_entry.id {
// Write conflict markers
write_conflict_file(
db,
rfs,
fs,
root,
path,
Some(base_entry),
None,
Some(new_entry),
)
conflicts.push(path)
}
// If same content, just keep base version
} else {
base[path] = ch.entry.unwrap()
}
ChangeKind::Deleted =>
match (base.get(path), parent_map.get(path)) {
(Some(b), Some(p)) =>
if b.id != p.id || b.mode != p.mode {
// Conflict: base modified, but commit deletes
conflicts.push(path)
// Keep base version with marker
write_conflict_file(
db,
rfs,
fs,
root,
path,
Some(b),
Some(p),
None,
)
} else {
base.remove(path)
}
(None, None) => ()
(Some(b), None) => {
// File in base but not in parent - conflict
conflicts.push(path)
write_conflict_file(db, rfs, fs, root, path, Some(b), None, None)
}
(None, Some(_)) =>
// File in parent but not base - already deleted, nothing to do
()
}
ChangeKind::Modified => {
let entry = ch.entry.unwrap()
match (base.get(path), parent_map.get(path)) {
(Some(b), Some(p)) =>
if b.id != p.id || b.mode != p.mode {
// Try content-level merge before falling back to conflict markers
let merge_base_content = get_blob_content(db, rfs, p.id)
let ours_content = get_blob_content(db, rfs, b.id)
let theirs_content = get_blob_content(db, rfs, entry.id)
let ours_data = rebase_get_blob_data(db, rfs, b.id)
let theirs_data = rebase_get_blob_data(db, rfs, entry.id)
if @diff3.is_binary_content(ours_data) ||
@diff3.is_binary_content(theirs_data) {
write_conflict_file(
db,
rfs,
fs,
root,
path,
Some(b),
Some(p),
Some(entry),
)
conflicts.push(path)
} else {
let merge_result = @diff3.content_merge(
merge_base_content,
ours_content,
theirs_content,
@diff3.ContentMergeOptions::default(),
)
if merge_result.has_conflicts {
// Write conflict markers
let full_path = join_path(root, path)
let dir = rebase_parent_dir(full_path)
fs.mkdir_p(dir)
fs.write_string(full_path, merge_result.content)
conflicts.push(path)
} else {
// Clean merge - create new blob and update base map
let content_bytes = @utf8.encode(merge_result.content)
let (blob_id, compressed) = @bit.create_blob(content_bytes)
rebase_write_object(
fs,
rfs,
rebase_resolve_git_dir(rfs, root),
blob_id,
compressed,
)
base[path] = { id: blob_id, mode: entry.mode }
}
}
} else {
// Base matches parent, apply modification
base[path] = entry
}
(None, None) => base[path] = entry
(None, Some(p)) => {
// File was deleted in base but modified in commit
write_conflict_file(
db,
rfs,
fs,
root,
path,
None,
Some(p),
Some(entry),
)
conflicts.push(path)
base[path] = entry
}
(Some(b), None) => {
// File added in base, modified in commit (from nothing to something)
if b.id != entry.id {
write_conflict_file(
db,
rfs,
fs,
root,
path,
Some(b),
None,
Some(entry),
)
conflicts.push(path)
}
// Keep entry as the result
base[path] = entry
}
}
}
}
}
conflicts
}
///|
/// Write a file with conflict markers using content_merge with diff3 style.
fn write_conflict_file(
db : ObjectDb,
rfs : &@bit.RepoFileSystem,
fs : &@bit.FileSystem,
root : String,
path : String,
ours : TreeEntryInfo?,
base : TreeEntryInfo?,
theirs : TreeEntryInfo?,
) -> Unit raise @bit.GitError {
let full_path = join_path(root, path)
let dir = rebase_parent_dir(full_path)
fs.mkdir_p(dir)
let ours_content = match ours {
Some(e) => get_blob_content(db, rfs, e.id)
None => ""
}
let base_content = match base {
Some(e) => get_blob_content(db, rfs, e.id)
None => ""
}
let theirs_content = match theirs {
Some(e) => get_blob_content(db, rfs, e.id)
None => ""
}
let opts : @diff3.ContentMergeOptions = {
conflict_style: @diff3.ConflictStyle::Diff3,
ours_label: "HEAD",
base_label: "parent",
theirs_label: "incoming",
marker_size: 7,
favor: @diff3.MergeFavor::NoFavor,
}
let result = @diff3.content_merge(
base_content, ours_content, theirs_content, opts,
)
fs.write_string(full_path, result.content)
}
///|
fn rebase_get_blob_data(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
id : @bit.ObjectId,
) -> Bytes {
let obj = db.get(fs, id) catch { _ => return Default::default() }
match obj {
Some(o) =>
if o.obj_type == @bit.ObjectType::Blob {
o.data
} else {
Default::default()
}
None => Default::default()
}
}
///|
fn get_blob_content(
db : ObjectDb,
fs : &@bit.RepoFileSystem,
id : @bit.ObjectId,
) -> String {
let obj = db.get(fs, id) catch { _ => return "" }
match obj {
Some(o) =>
if o.obj_type == @bit.ObjectType::Blob {
@utf8.decode_lossy(o.data[:])
} else {
""
}
None => ""
}
}