///|
/// Git モジュール構造の作成
///
/// サブディレクトリを git submodule 風の構造で初期化し、
/// 標準の git コマンドが透過的に動作するようにする。
///|
/// gitlink mode (160000 octal = submodule)
let gitlink_mode : Int = 57344
///|
/// サブディレクトリをモジュールとして初期化
pub fn init_module(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
repo_root : String,
subdir_path : String,
) -> Unit raise SubdirError {
let normalized_path = normalize_subdir_path(subdir_path)
let module_name = path_to_module_name(normalized_path)
let module_dir = git_dir + "/modules/" + module_name
let subdir_abs = repo_root + "/" + normalized_path
// 0. 現在の HEAD コミットを取得(gitlink 用)
let head_commit_id = resolve_head_commit_for_module(rfs, git_dir)
// 1. modules ディレクトリを作成
fs.mkdir_p(module_dir) catch {
_ => raise IoError("failed to create module directory")
}
fs.mkdir_p(module_dir + "/refs/heads") catch {
_ => ()
}
fs.mkdir_p(module_dir + "/refs/tags") catch {
_ => ()
}
// 2. HEAD を作成(親の HEAD を参照)
let parent_head = read_parent_head(rfs, git_dir)
let head_content = parent_head.unwrap_or("ref: refs/heads/main\n")
fs.write_file(module_dir + "/HEAD", string_to_bytes_mod(head_content)) catch {
_ => raise IoError("failed to write HEAD")
}
// 3. config を作成
// モジュールは常に .git/modules// にあるため、
// repo root へは常に 3 レベル上がる
let rel_worktree = "../../../" + normalized_path
let config_content = build_module_config(rel_worktree, normalized_path)
let depth = count_path_depth(normalized_path)
fs.write_file(module_dir + "/config", string_to_bytes_mod(config_content)) catch {
_ => raise IoError("failed to write config")
}
// 4. objects へのシンボリックリンク(または共有参照)
// MoonBit の FileSystem には symlink がないので、代わりに alternates を使う
let objects_dir = module_dir + "/objects"
fs.mkdir_p(objects_dir) catch {
_ => ()
}
fs.mkdir_p(objects_dir + "/info") catch {
_ => ()
}
let alternates_content = git_dir + "/objects\n"
fs.write_file(
objects_dir + "/info/alternates",
string_to_bytes_mod(alternates_content),
) catch {
_ => raise IoError("failed to write alternates")
}
// 5. サブディレクトリに .git ファイルを作成
let gitdir_rel = build_gitdir_path(depth, module_name)
let gitfile_content = "gitdir: " + gitdir_rel + "\n"
fs.write_file(subdir_abs + "/.git", string_to_bytes_mod(gitfile_content)) catch {
_ => raise IoError("failed to write .git file")
}
// 6. exclude ファイルを作成(.git ファイルを除外しない設定)
let info_dir = module_dir + "/info"
fs.mkdir_p(info_dir) catch {
_ => ()
}
fs.write_file(info_dir + "/exclude", string_to_bytes_mod("")) catch {
_ => ()
}
// 7. filter driver スクリプトを作成
setup_filter_script(fs, rfs, git_dir, normalized_path) catch {
e => raise e
}
// 8. .gitattributes に filter パターンを追加
setup_gitattributes_filter(fs, rfs, repo_root, normalized_path) catch {
e => raise e
}
ignore(head_commit_id)
}
///|
/// Filter driver スクリプトを作成
fn setup_filter_script(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
) -> Unit raise SubdirError {
let filter_name = "moongit-subdir-" + path_to_module_name(subdir_path)
let hooks_dir = git_dir + "/moongit-filters"
fs.mkdir_p(hooks_dir) catch {
_ => ()
}
// clean filter スクリプト(警告を出してパススルー)
let clean_script = hooks_dir + "/" + filter_name + "-clean"
let clean_content = "#!/bin/sh\ncat\n"
fs.write_file(clean_script, string_to_bytes_mod(clean_content)) catch {
_ => raise IoError("failed to write clean script")
}
// pre-commit hook を設定
setup_precommit_hook(fs, rfs, git_dir, subdir_path) catch {
_ => ()
}
// smudge filter スクリプト(checkout 時に実行、パススルー)
let smudge_script = hooks_dir + "/" + filter_name + "-smudge"
let smudge_content = "#!/bin/sh\ncat\n"
fs.write_file(smudge_script, string_to_bytes_mod(smudge_content)) catch {
_ => raise IoError("failed to write smudge script")
}
// .git/config に filter 設定を追加
setup_filter_config(
fs, rfs, git_dir, filter_name, clean_script, smudge_script,
) catch {
e => raise e
}
}
///|
/// .git/config に filter 設定を追加
fn setup_filter_config(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
filter_name : String,
clean_script : String,
smudge_script : String,
) -> Unit raise SubdirError {
let config_path = git_dir + "/config"
let config = rfs.read_file(config_path) catch { _ => b"" }
let mut content = bytes_to_string_mod(config)
let section_header = "[filter \"" + filter_name + "\"]"
if content.contains(section_header) {
return ()
}
if content.length() > 0 && !content.has_suffix("\n") {
content = content + "\n"
}
content = content + section_header + "\n"
content = content + "\tclean = sh " + clean_script + "\n"
content = content + "\tsmudge = sh " + smudge_script + "\n"
fs.write_file(config_path, string_to_bytes_mod(content)) catch {
_ => raise IoError("failed to write config")
}
}
///|
/// .gitattributes に filter パターンを追加
fn setup_gitattributes_filter(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
repo_root : String,
subdir_path : String,
) -> Unit raise SubdirError {
let filter_name = "moongit-subdir-" + path_to_module_name(subdir_path)
let gitattributes_path = repo_root + "/.gitattributes"
let mut content = if rfs.is_file(gitattributes_path) {
bytes_to_string_mod(rfs.read_file(gitattributes_path) catch { _ => b"" })
} else {
""
}
let pattern = subdir_path + "/** filter=" + filter_name
if content.contains(pattern) {
return ()
}
if content.length() > 0 && !content.has_suffix("\n") {
content = content + "\n"
}
content = content + "# moongit subdir: " + subdir_path + "\n"
content = content + pattern + "\n"
fs.write_file(gitattributes_path, string_to_bytes_mod(content)) catch {
_ => raise IoError("failed to write .gitattributes")
}
}
///|
/// pre-commit hook を設定
fn setup_precommit_hook(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
) -> Unit raise SubdirError {
let hooks_dir = git_dir + "/hooks"
fs.mkdir_p(hooks_dir) catch {
_ => ()
}
let hook_path = hooks_dir + "/pre-commit"
let marker = "# moongit-subdir-check: " + subdir_path
// 既存の hook を読み込み
let mut content = if rfs.is_file(hook_path) {
bytes_to_string_mod(rfs.read_file(hook_path) catch { _ => b"" })
} else {
"#!/bin/sh\n"
}
// 既に設定済みかチェック
if content.contains(marker) {
return ()
}
// hook スクリプトを追加
if !content.has_suffix("\n") {
content = content + "\n"
}
content = content + "\n" + marker + "\n"
content = content +
"if git diff --cached --name-only | grep -q '^" +
subdir_path +
"/'; then\n"
content = content +
" echo \"Error: '" +
subdir_path +
"' is managed by moongit. Direct git commit is not allowed.\" >&2\n"
content = content +
" echo \"Use 'moongit subdir commit " +
subdir_path +
" -m ' instead.\" >&2\n"
content = content + " exit 1\n"
content = content + "fi\n"
fs.write_file(hook_path, string_to_bytes_mod(content)) catch {
_ => raise IoError("failed to write pre-commit hook")
}
}
///|
/// pre-commit hook から subdir チェックを削除
fn remove_precommit_hook(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
) -> Unit raise SubdirError {
let hooks_dir = git_dir + "/hooks"
let hook_path = hooks_dir + "/pre-commit"
if !rfs.is_file(hook_path) {
return ()
}
let content = bytes_to_string_mod(
rfs.read_file(hook_path) catch {
_ => return ()
},
)
let marker = "# moongit-subdir-check: " + subdir_path
if !content.contains(marker) {
return ()
}
// マーカーから fi までを削除
let new_content = remove_hook_section(content, marker)
if new_content == "#!/bin/sh\n" || new_content.length() < 15 {
// ほぼ空になった場合は削除
fs.remove_file(hook_path) catch {
_ => ()
}
} else {
fs.write_file(hook_path, string_to_bytes_mod(new_content)) catch {
_ => raise IoError("failed to update pre-commit hook")
}
}
}
///|
/// hook からセクションを削除
fn remove_hook_section(content : String, marker : String) -> String {
let result = StringBuilder::new()
let mut in_section = false
for line_view in content.split("\n") {
let line = line_view.to_owned()
if line == marker {
in_section = true
continue
}
if in_section {
if line == "fi" {
in_section = false
continue
}
continue
}
// 空行が連続しないように
let current = result.to_string()
if line.length() == 0 && current.has_suffix("\n\n") {
continue
}
result.write_string(line)
result.write_string("\n")
}
trim_trailing_newlines(result.to_string())
}
///|
/// Filter driver を設定して git add をブロック
fn _setup_filter_driver(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
repo_root : String,
subdir_path : String,
) -> Unit raise SubdirError {
let filter_name = "moongit-subdir-" + path_to_module_name(subdir_path)
// 1. .git/config に filter を追加
add_filter_to_config(fs, rfs, git_dir, filter_name, subdir_path) catch {
e => raise e
}
// 2. .gitattributes にパターンを追加
add_filter_to_gitattributes(fs, rfs, repo_root, filter_name, subdir_path) catch {
e => raise e
}
}
///|
/// .git/config に filter 設定を追加
fn add_filter_to_config(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
filter_name : String,
subdir_path : String,
) -> Unit raise SubdirError {
let config_path = git_dir + "/config"
let mut config = if rfs.is_file(config_path) {
bytes_to_string_mod(rfs.read_file(config_path) catch { _ => b"" })
} else {
""
}
// 既に設定済みかチェック
let section_header = "[filter \"" + filter_name + "\"]"
if config.contains(section_header) {
return ()
}
// filter セクションを追加
let filter_section = StringBuilder::new()
if config.length() > 0 && !config.has_suffix("\n") {
filter_section.write_string("\n")
}
filter_section.write_string("[filter \"")
filter_section.write_string(filter_name)
filter_section.write_string("\"]\n")
filter_section.write_string("\tclean = echo \"Error: '")
filter_section.write_string(subdir_path)
filter_section.write_string(
"' is managed by moongit subdir. Use 'moongit subdir commit' instead.\" >&2 && exit 1\n",
)
filter_section.write_string("\tsmudge = cat\n")
filter_section.write_string("\trequired = true\n")
config = config + filter_section.to_string()
fs.write_file(config_path, string_to_bytes_mod(config)) catch {
_ => raise IoError("failed to write config")
}
}
///|
/// .gitattributes に filter パターンを追加
fn add_filter_to_gitattributes(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
repo_root : String,
filter_name : String,
subdir_path : String,
) -> Unit raise SubdirError {
let gitattributes_path = repo_root + "/.gitattributes"
let mut content = if rfs.is_file(gitattributes_path) {
bytes_to_string_mod(rfs.read_file(gitattributes_path) catch { _ => b"" })
} else {
""
}
// パターンを構築
let pattern = subdir_path + "/** filter=" + filter_name
// 既に設定済みかチェック
if content.contains(pattern) {
return ()
}
// パターンを追加
if content.length() > 0 && !content.has_suffix("\n") {
content = content + "\n"
}
// コメントを追加して分かりやすく
content = content + "# moongit subdir: " + subdir_path + "\n"
content = content + pattern + "\n"
fs.write_file(gitattributes_path, string_to_bytes_mod(content)) catch {
_ => raise IoError("failed to write .gitattributes")
}
}
///|
/// Filter driver を削除
fn cleanup_filter_driver(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
repo_root : String,
subdir_path : String,
) -> Unit {
let filter_name = "moongit-subdir-" + path_to_module_name(subdir_path)
// 1. filter スクリプトを削除
let hooks_dir = git_dir + "/moongit-filters"
fs.remove_file(hooks_dir + "/" + filter_name + "-clean") catch {
_ => ()
}
fs.remove_file(hooks_dir + "/" + filter_name + "-smudge") catch {
_ => ()
}
// hooks_dir が空なら削除
let entries = rfs.readdir(hooks_dir) catch { _ => [] }
if entries.length() == 0 {
fs.remove_dir(hooks_dir) catch {
_ => ()
}
}
// 2. .git/config から filter を削除
remove_filter_from_config(fs, rfs, git_dir, filter_name) catch {
_ => ()
}
// 3. .gitattributes からパターンを削除
remove_filter_from_gitattributes(fs, rfs, repo_root, filter_name, subdir_path) catch {
_ => ()
}
// 4. pre-commit hook から削除
remove_precommit_hook(fs, rfs, git_dir, subdir_path) catch {
_ => ()
}
}
///|
/// .git/config から filter セクションを削除
fn remove_filter_from_config(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
filter_name : String,
) -> Unit raise SubdirError {
let config_path = git_dir + "/config"
if !rfs.is_file(config_path) {
return ()
}
let content = bytes_to_string_mod(
rfs.read_file(config_path) catch {
_ => return ()
},
)
let section_header = "[filter \"" + filter_name + "\"]"
if !content.contains(section_header) {
return ()
}
// セクションを削除
let new_content = remove_config_section(
content,
"filter \"" + filter_name + "\"",
)
fs.write_file(config_path, string_to_bytes_mod(new_content)) catch {
_ => raise IoError("failed to update config")
}
}
///|
/// config からセクションを削除
fn remove_config_section(content : String, section_name : String) -> String {
let result = StringBuilder::new()
let section_header = "[" + section_name + "]"
let mut in_section = false
let mut prev_was_empty = false
for line_view in content.split("\n") {
let line = line_view.to_owned()
if line == section_header {
in_section = true
continue
}
if in_section {
// 次のセクションまたは空行でセクション終了
if line.has_prefix("[") {
in_section = false
result.write_string(line)
result.write_string("\n")
prev_was_empty = false
} else if line.has_prefix("\t") || line.has_prefix(" ") {
// セクション内の設定行はスキップ
continue
} else if line.length() == 0 {
// 空行でセクション終了
in_section = false
}
continue
}
// 連続した空行を避ける
if line.length() == 0 {
if prev_was_empty {
continue
}
prev_was_empty = true
} else {
prev_was_empty = false
}
result.write_string(line)
result.write_string("\n")
}
let s = result.to_string()
// 末尾の余分な改行を削除
trim_trailing_newlines(s)
}
///|
/// 末尾の連続改行を1つに
fn trim_trailing_newlines(s : String) -> String {
let chars = s.to_array()
let mut end = chars.length()
while end > 1 && chars[end - 1] == '\n' && chars[end - 2] == '\n' {
end -= 1
}
if end == chars.length() {
s
} else {
String::unsafe_substring(s, start=0, end~)
}
}
///|
/// .gitattributes から filter パターンを削除
fn remove_filter_from_gitattributes(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
repo_root : String,
filter_name : String,
subdir_path : String,
) -> Unit raise SubdirError {
let gitattributes_path = repo_root + "/.gitattributes"
if !rfs.is_file(gitattributes_path) {
return ()
}
let content = bytes_to_string_mod(
rfs.read_file(gitattributes_path) catch {
_ => return ()
},
)
let pattern_line = subdir_path + "/** filter=" + filter_name
let comment_line = "# moongit subdir: " + subdir_path
if !content.contains(pattern_line) {
return ()
}
// パターンとコメントを削除
let result = StringBuilder::new()
for line_view in content.split("\n") {
let line = line_view.to_owned()
if line == pattern_line || line == comment_line {
continue
}
if line.length() > 0 || result.to_string().length() > 0 {
result.write_string(line)
result.write_string("\n")
}
}
let new_content = trim_trailing_newlines(result.to_string())
if new_content.length() == 0 {
// 空になった場合は削除
fs.remove_file(gitattributes_path) catch {
_ => ()
}
} else {
fs.write_file(gitattributes_path, string_to_bytes_mod(new_content)) catch {
_ => raise IoError("failed to update .gitattributes")
}
}
}
///|
/// HEAD コミットを解決
fn resolve_head_commit_for_module(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> @bit.ObjectId? {
let head_path = git_dir + "/HEAD"
let content = rfs.read_file(head_path) catch { _ => return None }
let head_str = content |> bytes_to_string_mod |> trim_newlines_mod
if head_str.has_prefix("ref: ") {
let ref_path = skip_chars_mod(head_str, 5)
let ref_content = rfs.read_file(git_dir + "/" + ref_path) catch {
_ => return None
}
let hex = ref_content |> bytes_to_string_mod |> trim_newlines_mod
(hex |> @bit.ObjectId::from_hex |> Some) catch {
_ => None
}
} else {
(head_str |> @bit.ObjectId::from_hex |> Some) catch {
_ => None
}
}
}
///|
/// .gitmodules にサブモジュールエントリを追加
fn _add_to_gitmodules(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
repo_root : String,
subdir_path : String,
) -> Unit raise SubdirError {
let gitmodules_path = repo_root + "/.gitmodules"
let mut content = if rfs.is_file(gitmodules_path) {
bytes_to_string_mod(rfs.read_file(gitmodules_path) catch { _ => b"" })
} else {
""
}
// 既に登録されているかチェック
let section_header = "[submodule \"" + subdir_path + "\"]"
if content.contains(section_header) {
return ()
}
// 新しいエントリを追加
let entry = StringBuilder::new()
if content.length() > 0 && !content.has_suffix("\n") {
entry.write_string("\n")
}
entry.write_string("[submodule \"")
entry.write_string(subdir_path)
entry.write_string("\"]\n")
entry.write_string("\tpath = ")
entry.write_string(subdir_path)
entry.write_string("\n")
entry.write_string("\turl = .\n") // 自己参照
content = content + entry.to_string()
fs.write_file(gitmodules_path, string_to_bytes_mod(content)) catch {
_ => raise IoError("failed to write .gitmodules")
}
}
///|
/// .gitmodules からサブモジュールエントリを削除
fn _remove_from_gitmodules(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
repo_root : String,
subdir_path : String,
) -> Unit raise SubdirError {
let gitmodules_path = repo_root + "/.gitmodules"
if !rfs.is_file(gitmodules_path) {
return ()
}
let content = bytes_to_string_mod(
rfs.read_file(gitmodules_path) catch {
_ => return ()
},
)
let section_header = "[submodule \"" + subdir_path + "\"]"
if !content.contains(section_header) {
return ()
}
// セクションを削除
let new_content = remove_gitmodules_section(content, subdir_path)
if new_content.length() == 0 || new_content == "\n" {
// .gitmodules が空になった場合は削除
fs.remove_file(gitmodules_path) catch {
_ => ()
}
} else {
fs.write_file(gitmodules_path, string_to_bytes_mod(new_content)) catch {
_ => raise IoError("failed to update .gitmodules")
}
}
}
///|
/// .gitmodules からセクションを削除
fn remove_gitmodules_section(content : String, subdir_path : String) -> String {
let result = StringBuilder::new()
let section_header = "[submodule \"" + subdir_path + "\"]"
let mut in_section = false
for line_view in content.split("\n") {
let line = line_view.to_owned()
if line == section_header {
in_section = true
continue
}
if in_section {
// 次のセクションの開始でセクション終了
if line.has_prefix("[") {
in_section = false
result.write_string(line)
result.write_string("\n")
}
// セクション内の行はスキップ
continue
}
result.write_string(line)
result.write_string("\n")
}
// 末尾の余分な改行を削除
let s = result.to_string()
if s.has_suffix("\n\n") {
String::unsafe_substring(s, start=0, end=s.length() - 1)
} else {
s
}
}
///|
/// index に gitlink エントリを追加
fn _add_gitlink_to_index(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
commit_id : @bit.ObjectId,
) -> Unit raise SubdirError {
// 現在の index を読み込む
let entries = @lib.read_index_entries(rfs, git_dir) catch { _ => [] }
// subdir_path 以下のエントリを削除し、gitlink を追加
let new_entries : Array[@lib.IndexEntry] = []
for entry in entries {
// subdir_path 内のファイルは除外
if entry.path.has_prefix(subdir_path + "/") || entry.path == subdir_path {
continue
}
new_entries.push(entry)
}
// gitlink エントリを追加
new_entries.push(
@lib.IndexEntry::new(subdir_path, commit_id, gitlink_mode, 0),
)
// index を書き込む
@lib.write_index_entries(fs, git_dir, new_entries) catch {
_ => raise IoError("failed to update index")
}
}
///|
/// index から gitlink エントリを削除し、サブディレクトリのファイルを復元
fn _remove_gitlink_from_index(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
) -> Unit raise SubdirError {
let entries = @lib.read_index_entries(rfs, git_dir) catch { _ => [] }
// gitlink エントリを削除
let new_entries : Array[@lib.IndexEntry] = []
for entry in entries {
if entry.path == subdir_path && entry.mode == gitlink_mode {
continue
}
new_entries.push(entry)
}
@lib.write_index_entries(fs, git_dir, new_entries) catch {
_ => raise IoError("failed to update index")
}
}
///|
fn trim_newlines_mod(s : String) -> String {
let chars = s.to_array()
let mut end = chars.length()
while end > 0 {
let c = chars[end - 1]
if c == '\n' || c == '\r' || c == ' ' || c == '\t' {
end -= 1
} else {
break
}
}
if end == chars.length() {
s
} else {
String::unsafe_substring(s, start=0, end~)
}
}
///|
fn skip_chars_mod(s : String, n : Int) -> String {
if n >= s.length() {
""
} else {
String::unsafe_substring(s, start=n, end=s.length())
}
}
///|
/// モジュールが初期化済みかチェック
pub fn is_module_initialized(
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
) -> Bool {
let normalized_path = normalize_subdir_path(subdir_path)
let module_name = path_to_module_name(normalized_path)
let module_dir = git_dir + "/modules/" + module_name
rfs.is_dir(module_dir) && rfs.is_file(module_dir + "/HEAD")
}
///|
/// モジュールを削除
pub fn deinit_module(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
repo_root : String,
subdir_path : String,
) -> Unit {
let normalized_path = normalize_subdir_path(subdir_path)
let module_name = path_to_module_name(normalized_path)
let module_dir = git_dir + "/modules/" + module_name
let subdir_abs = repo_root + "/" + normalized_path
// 1. .git ファイルを削除
fs.remove_file(subdir_abs + "/.git") catch {
_ => ()
}
// 2. モジュールディレクトリを削除
if rfs.is_dir(module_dir) {
remove_module_dir(fs, rfs, module_dir)
}
// 3. filter スクリプトと設定を削除
cleanup_filter_driver(fs, rfs, git_dir, repo_root, normalized_path)
}
///|
/// モジュールの状態を親リポジトリと同期
pub fn sync_module(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
) -> Unit raise SubdirError {
let normalized_path = normalize_subdir_path(subdir_path)
let module_name = path_to_module_name(normalized_path)
let module_dir = git_dir + "/modules/" + module_name
// 親の HEAD を取得してモジュールの HEAD を更新
let parent_head = read_parent_head(rfs, git_dir)
match parent_head {
Some(ref_str) =>
fs.write_file(module_dir + "/HEAD", string_to_bytes_mod(ref_str)) catch {
_ => raise IoError("failed to sync HEAD")
}
None => ()
}
}
///|
/// 初期化済みモジュール一覧を取得
pub fn list_modules(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> Array[String] {
let modules_dir = git_dir + "/modules"
if !rfs.is_dir(modules_dir) {
return []
}
let entries = rfs.readdir(modules_dir) catch { _ => return [] }
let result : Array[String] = []
for entry in entries {
let module_path = modules_dir + "/" + entry
if rfs.is_dir(module_path) && rfs.is_file(module_path + "/HEAD") {
// モジュール名をパスに戻す
result.push(module_name_to_path(entry))
}
}
result
}
///|
/// パスをモジュール名に変換(/ を _ に)
fn path_to_module_name(path : String) -> String {
let result = StringBuilder::new()
for c in path {
if c == '/' {
result.write_char('_')
} else {
result.write_char(c)
}
}
result.to_string()
}
///|
/// モジュール名をパスに戻す(_ を / に)
fn module_name_to_path(name : String) -> String {
let result = StringBuilder::new()
for c in name {
if c == '_' {
result.write_char('/')
} else {
result.write_char(c)
}
}
result.to_string()
}
///|
/// パスの深さをカウント
fn count_path_depth(path : String) -> Int {
let mut count = 0
for c in path {
if c == '/' {
count += 1
}
}
count + 1
}
///|
/// 相対パスを構築(../ を depth 回)
fn _build_relative_path(depth : Int, subdir_path : String) -> String {
let result = StringBuilder::new()
for _ in 0.. String {
let result = StringBuilder::new()
for _ in 0.. String {
let config = StringBuilder::new()
config.write_string("[core]\n")
config.write_string("\trepositoryformatversion = 0\n")
config.write_string("\tfilemode = true\n")
config.write_string("\tbare = false\n")
config.write_string("\tworktree = ")
config.write_string(worktree_path)
config.write_string("\n")
config.write_string("[subdir]\n")
config.write_string("\tpath = ")
config.write_string(subdir_path)
config.write_string("\n")
config.write_string("\tactive = true\n")
config.to_string()
}
///|
/// 親リポジトリの HEAD を読み取り
fn read_parent_head(rfs : &@bit.RepoFileSystem, git_dir : String) -> String? {
let head_path = git_dir + "/HEAD"
let content = rfs.read_file(head_path) catch { _ => return None }
content |> bytes_to_string_mod |> Some
}
///|
/// モジュールディレクトリを再帰的に削除
fn remove_module_dir(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
path : String,
) -> Unit {
let entries = rfs.readdir(path) catch { _ => return () }
for entry in entries {
let child_path = path + "/" + entry
if rfs.is_dir(child_path) {
remove_module_dir(fs, rfs, child_path)
} else {
fs.remove_file(child_path) catch {
_ => ()
}
}
}
fs.remove_dir(path) catch {
_ => ()
}
}
///|
fn string_to_bytes_mod(s : String) -> Bytes {
let bytes : Array[Byte] = []
for c in s {
bytes.push(c.to_int().to_byte())
}
Bytes::from_array(bytes)
}
///|
fn bytes_to_string_mod(bytes : Bytes) -> String {
let result = StringBuilder::new()
for i in 0..