///|
/// サブディレクトリでのコミット機能
///
/// サブディレクトリの変更を元リポジトリにコミットする。
/// 1. サブディレクトリの Fs から新しいツリーを構築
/// 2. 元リポジトリのルートツリーでサブディレクトリ部分を置換
/// 3. 新しいコミットを作成
///|
/// サブディレクトリの変更を元リポジトリにコミット
pub fn SubdirRepo::commit(
self : SubdirRepo,
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
message : String,
author? : String = "author ",
timestamp? : Int64 = 0L,
) -> @bit.ObjectId raise SubdirError {
// 変更がなければエラー
if !self.is_dirty() {
raise IoError("nothing to commit")
}
// 親コミットを取得
let parent_commit = match self.current_commit {
Some(id) => id
None => raise IoError("no parent commit")
}
// Fs からサブディレクトリの新しいツリーを構築
let new_subdir_tree = @fs.build_tree_from_working(
fs,
rfs,
self.git_dir,
self.bitfs.base_tree(),
self.bitfs.working,
) catch {
_ => raise IoError("failed to build tree")
}
// 元リポジトリのルートツリーを取得し、サブディレクトリ部分を置換
let db = @lib.ObjectDb::load(rfs, self.git_dir) catch {
_ => raise IoError("failed to load object db")
}
let parent_obj = db.get(rfs, parent_commit) catch {
_ => raise CommitNotFound(parent_commit)
}
let parent_data = match parent_obj {
Some(o) => o
None => raise CommitNotFound(parent_commit)
}
let parent_info = @bit.parse_commit(parent_data.data) catch {
_ => raise CommitNotFound(parent_commit)
}
// ルートツリーを取得してサブディレクトリを置換
let new_root_tree = replace_subdir_tree(
fs,
rfs,
db,
self.git_dir,
parent_info.tree,
self.subdir_path,
new_subdir_tree,
) catch {
e => raise e
}
// 新しいコミットを作成
let new_commit_id = create_commit_object(
fs,
self.git_dir,
new_root_tree,
parent_commit,
message,
author,
timestamp,
) catch {
e => raise e
}
// HEAD(またはブランチ参照)を更新
update_head_ref(fs, rfs, self.git_dir, new_commit_id) catch {
e => raise e
}
// インデックスを新しいコミットに合わせて更新
update_index_from_commit(fs, rfs, self.git_dir, new_commit_id) catch {
e => raise e
}
// ワーキング層をクリア
self.bitfs.discard_changes()
new_commit_id
}
///|
/// ルートツリーのサブディレクトリ部分を置換して新しいツリーを作成
fn replace_subdir_tree(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
db : @lib.ObjectDb,
git_dir : String,
root_tree : @bit.ObjectId,
subdir_path : String,
new_subdir_tree : @bit.ObjectId,
) -> @bit.ObjectId raise SubdirError {
let path_parts = split_subdir_path(subdir_path)
if path_parts.length() == 0 {
// サブディレクトリがルートの場合、そのまま返す
return new_subdir_tree
}
// 再帰的にツリーを置換
replace_tree_recursive(
fs, rfs, db, git_dir, root_tree, path_parts, 0, new_subdir_tree,
)
}
///|
/// ツリーを再帰的に置換
fn replace_tree_recursive(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
db : @lib.ObjectDb,
git_dir : String,
current_tree : @bit.ObjectId,
path_parts : Array[String],
depth : Int,
new_leaf_tree : @bit.ObjectId,
) -> @bit.ObjectId raise SubdirError {
// 現在のツリーを読み込む
let tree_obj = db.get(rfs, current_tree) catch {
_ => raise TreeNotFound(current_tree)
}
let tree_data = match tree_obj {
Some(o) => o
None => raise TreeNotFound(current_tree)
}
let entries = @bit.parse_tree(tree_data.data) catch {
_ => raise TreeNotFound(current_tree)
}
// 新しいエントリリストを構築
let new_entries : Array[@bit.TreeEntry] = []
let target_name = path_parts[depth]
let is_last = depth == path_parts.length() - 1
for entry in entries {
if entry.name == target_name {
// 対象のエントリを置換
if is_last {
// 最後のパス要素なら、新しいツリーで置換
new_entries.push(
@bit.TreeEntry::new(entry.mode, entry.name, new_leaf_tree),
)
} else {
// まだパスが続く場合は再帰
let new_child_tree = replace_tree_recursive(
fs,
rfs,
db,
git_dir,
entry.id,
path_parts,
depth + 1,
new_leaf_tree,
) catch {
e => raise e
}
new_entries.push(
@bit.TreeEntry::new(entry.mode, entry.name, new_child_tree),
)
}
} else {
// 対象外のエントリはそのまま
new_entries.push(entry)
}
}
// 新しいツリーオブジェクトを作成して保存
let tree_content = build_tree_content(new_entries)
let tree_id = write_object(fs, git_dir, "tree", tree_content) catch {
_ => raise IoError("failed to write tree object")
}
tree_id
}
///|
/// ツリーエントリからツリーオブジェクトの内容を構築
fn build_tree_content(entries : Array[@bit.TreeEntry]) -> Bytes {
// エントリをソート(Git のツリーは名前でソートされている必要がある)
let sorted = entries.copy()
sorted.sort_by(fn(a, b) {
// ディレクトリは名前の後に "/" を追加してソート
let a_name = if a.mode == "040000" || a.mode == "40000" {
a.name + "/"
} else {
a.name
}
let b_name = if b.mode == "040000" || b.mode == "40000" {
b.name + "/"
} else {
b.name
}
a_name.compare(b_name)
})
// バイナリ形式で構築: "mode name\0<20-byte-sha>"
let parts : Array[Bytes] = []
for entry in sorted {
// モードと名前
let header = entry.mode + " " + entry.name
let header_bytes = string_to_bytes_local(header)
// null バイト
let null_byte = Bytes::from_array([b'\x00'])
// SHA-1 の 20 バイト
let sha_bytes = entry.id.to_bytes()
// 結合
parts.push(header_bytes)
parts.push(null_byte)
parts.push(sha_bytes)
}
// すべてを結合
concat_bytes(parts)
}
///|
/// 複数の Bytes を結合
fn concat_bytes(parts : Array[Bytes]) -> Bytes {
let bytes : Array[Byte] = []
for part in parts {
for i in 0.. Bytes {
let chars = s.to_array()
let bytes : Array[Byte] = []
for i in 0.. @bit.ObjectId raise SubdirError {
// ヘッダー: "type size\0"
let header = obj_type + " " + content.length().to_string()
let header_bytes = string_to_bytes_local(header)
let null_byte = Bytes::from_array([b'\x00'])
// フルデータ
let full_data = concat_bytes([header_bytes, null_byte, content])
// SHA-1 ハッシュを計算
let hash = @bit.sha1(full_data)
let hex = hash.to_hex()
// オブジェクトパス: first 2 chars / rest
let dir_name = hex_substring(hex, 0, 2)
let file_name = hex_substring(hex, 2, 40)
let dir_path = git_dir + "/objects/" + dir_name
let file_path = dir_path + "/" + file_name
// ディレクトリ作成
fs.mkdir_p(dir_path) catch {
_ => ()
}
// zlib 圧縮して書き込み
let compressed = @zlib.zlib_compress(full_data)
fs.write_file(file_path, compressed) catch {
_ => raise IoError("failed to write object")
}
hash
}
///|
/// 文字列から部分文字列を取得
fn hex_substring(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()
}
///|
/// コミットオブジェクトを作成
fn create_commit_object(
fs : &@bit.FileSystem,
git_dir : String,
tree_id : @bit.ObjectId,
parent_id : @bit.ObjectId,
message : String,
author : String,
timestamp : Int64,
) -> @bit.ObjectId raise SubdirError {
// コミット内容を構築
let content = StringBuilder::new()
content.write_string("tree ")
content.write_string(tree_id.to_hex())
content.write_string("\n")
content.write_string("parent ")
content.write_string(parent_id.to_hex())
content.write_string("\n")
content.write_string("author ")
content.write_string(author)
content.write_string(" ")
content.write_string(timestamp.to_string())
content.write_string(" +0000\n")
content.write_string("committer ")
content.write_string(author)
content.write_string(" ")
content.write_string(timestamp.to_string())
content.write_string(" +0000\n")
content.write_string("\n")
content.write_string(message)
content.write_string("\n")
let content_bytes = string_to_bytes_local(content.to_string())
write_object(fs, git_dir, "commit", content_bytes)
}
///|
/// HEAD(またはブランチ参照)を新しいコミットに更新
fn update_head_ref(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
commit_id : @bit.ObjectId,
) -> Unit raise SubdirError {
let head_path = git_dir + "/HEAD"
let head_bytes = rfs.read_file(head_path) catch {
_ => raise IoError("failed to read HEAD")
}
let head_content = bytes_to_string_commit(head_bytes)
// HEAD が "ref: refs/heads/..." 形式かチェック
let trimmed = trim_head_content(head_content)
if trimmed.has_prefix("ref: ") {
// シンボリック参照の場合、参照先のファイルを更新
let ref_path = skip_prefix_commit(trimmed, 5) // "ref: " を除去
let ref_file = git_dir + "/" + ref_path
// 親ディレクトリが存在することを確認
let ref_dir = get_parent_dir(ref_file)
fs.mkdir_p(ref_dir) catch {
_ => ()
}
// 参照を更新
let commit_hex = commit_id.to_hex() + "\n"
fs.write_string(ref_file, commit_hex) catch {
_ => raise IoError("failed to update ref")
}
} else {
// detached HEAD の場合、HEAD を直接更新
let commit_hex = commit_id.to_hex() + "\n"
fs.write_string(head_path, commit_hex) catch {
_ => raise IoError("failed to update HEAD")
}
}
}
///|
fn bytes_to_string_commit(data : Bytes) -> String {
let result = StringBuilder::new()
for b in data {
result.write_char(b.to_int().unsafe_to_char())
}
result.to_string()
}
///|
fn trim_head_content(s : String) -> String {
let chars = s.to_array()
let mut end = chars.length()
while end > 0 && (chars[end - 1] == '\n' || chars[end - 1] == '\r') {
end -= 1
}
let result = StringBuilder::new()
for i in 0.. String {
let chars = s.to_array()
if n >= chars.length() {
return ""
}
let result = StringBuilder::new()
for i in n.. String {
let chars = path.to_array()
let mut last_slash = -1
for i = 0; i < chars.length(); i = i + 1 {
if chars[i] == '/' {
last_slash = i
}
}
if last_slash <= 0 {
return "/"
}
let result = StringBuilder::new()
for i in 0.. @bit.ObjectId raise SubdirError {
create_commit_object(
fs, git_dir, tree_id, parent_id, message, author, timestamp,
)
}
///|
/// インデックスを新しいコミットに合わせて更新
fn update_index_from_commit(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
commit_id : @bit.ObjectId,
) -> Unit raise SubdirError {
let db = @lib.ObjectDb::load(rfs, git_dir) catch {
_ => raise IoError("failed to load object db")
}
let files = @lib.collect_tree_files_from_commit(db, rfs, commit_id) catch {
_ => raise IoError("failed to collect tree files")
}
let entries = @lib.tree_files_to_index(db, rfs, files) catch {
_ => raise IoError("failed to convert to index entries")
}
@lib.write_index_entries(fs, git_dir, entries) catch {
_ => raise IoError("failed to write index")
}
}