///|
/// Sparse checkout support for subdirectory modules
///
/// サブディレクトリモジュール内で sparse checkout を有効にし、
/// 特定のファイル/パターンのみをワークツリーに展開する。
///|
/// モジュールの sparse checkout が有効かチェック
pub fn is_module_sparse_enabled(
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
let config_path = module_dir + "/config"
if !rfs.is_file(config_path) {
return false
}
let content = rfs.read_file(config_path) catch { _ => return false }
let text = bytes_to_string_sparse(content)
let mut in_core = false
for line_view in text.split("\n") {
let line = sparse_trim(line_view.to_owned())
if line == "[core]" {
in_core = true
continue
}
if line.has_prefix("[") {
in_core = false
continue
}
if in_core && line.has_prefix("sparseCheckout") {
if line.contains("true") || line.contains("= true") {
return true
}
}
}
false
}
///|
/// モジュールの sparse checkout パターンを読み取り
pub fn read_module_sparse_patterns(
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
) -> Array[String] {
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 sparse_path = module_dir + "/info/sparse-checkout"
if !rfs.is_file(sparse_path) {
return []
}
let content = rfs.read_file(sparse_path) catch { _ => return [] }
let text = bytes_to_string_sparse(content)
let patterns : Array[String] = []
for line_view in text.split("\n") {
let line = sparse_trim(line_view.to_owned())
if line.length() > 0 && !line.has_prefix("#") {
patterns.push(line)
}
}
patterns
}
///|
/// モジュールの sparse checkout パターンを書き込み
pub fn write_module_sparse_patterns(
fs : &@bit.FileSystem,
git_dir : String,
subdir_path : String,
patterns : Array[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 info_dir = module_dir + "/info"
fs.mkdir_p(info_dir) catch {
_ => raise IoError("failed to create info directory")
}
let sparse_path = module_dir + "/info/sparse-checkout"
let content = build_sparse_content(patterns)
fs.write_file(sparse_path, string_to_bytes_sparse(content)) catch {
_ => raise IoError("failed to write sparse-checkout file")
}
}
///|
/// モジュールの sparse checkout を初期化
pub fn init_module_sparse(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
cone? : Bool = false,
) -> 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
// モジュールが初期化済みかチェック
if !rfs.is_dir(module_dir) {
raise IoError("module not initialized: " + subdir_path)
}
// config に sparseCheckout = true を追加
let config_path = module_dir + "/config"
let mut config = if rfs.is_file(config_path) {
bytes_to_string_sparse(rfs.read_file(config_path) catch { _ => b"" })
} else {
"[core]\n"
}
// sparseCheckout = true を追加
if !config.contains("sparseCheckout") {
if config.contains("[core]") {
config = config.replace(
old="[core]",
new="[core]\n\tsparseCheckout = true",
)
} else {
config = config + "[core]\n\tsparseCheckout = true\n"
}
}
// cone mode を追加
if cone && !config.contains("sparseCheckoutCone") {
if config.contains("[core]") {
config = config.replace(
old="sparseCheckout = true",
new="sparseCheckout = true\n\tsparseCheckoutCone = true",
)
}
}
fs.write_file(config_path, string_to_bytes_sparse(config)) catch {
_ => raise IoError("failed to update config")
}
// 初期パターン(ルートのみ)を設定
let info_dir = module_dir + "/info"
fs.mkdir_p(info_dir) catch {
_ => ()
}
let sparse_path = module_dir + "/info/sparse-checkout"
if !rfs.is_file(sparse_path) {
fs.write_file(sparse_path, string_to_bytes_sparse("/*\n!/*/\n")) catch {
_ => raise IoError("failed to write initial sparse-checkout")
}
}
}
///|
/// モジュールの sparse checkout パターンを設定(既存を置換)
pub fn set_module_sparse_patterns(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
patterns : Array[String],
) -> Unit raise SubdirError {
// sparse checkout が初期化されていなければ初期化
if !is_module_sparse_enabled(rfs, git_dir, subdir_path) {
init_module_sparse(fs, rfs, git_dir, subdir_path) catch {
e => raise e
}
}
write_module_sparse_patterns(fs, git_dir, subdir_path, patterns) catch {
e => raise e
}
}
///|
/// モジュールの sparse checkout にパターンを追加
pub fn add_module_sparse_patterns(
fs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
subdir_path : String,
new_patterns : Array[String],
) -> Unit raise SubdirError {
// sparse checkout が初期化されていなければ初期化
if !is_module_sparse_enabled(rfs, git_dir, subdir_path) {
init_module_sparse(fs, rfs, git_dir, subdir_path) catch {
e => raise e
}
}
let existing = read_module_sparse_patterns(rfs, git_dir, subdir_path)
for p in new_patterns {
if !existing.contains(p) {
existing.push(p)
}
}
write_module_sparse_patterns(fs, git_dir, subdir_path, existing) catch {
e => raise e
}
}
///|
/// モジュールの sparse checkout を無効化
pub fn disable_module_sparse(
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
// config を更新
let config_path = module_dir + "/config"
if rfs.is_file(config_path) {
let config = bytes_to_string_sparse(
rfs.read_file(config_path) catch {
_ => b""
},
)
let new_config = config
.replace(old="sparseCheckout = true", new="sparseCheckout = false")
.replace(
old="sparseCheckoutCone = true",
new="sparseCheckoutCone = false",
)
fs.write_file(config_path, string_to_bytes_sparse(new_config)) catch {
_ => raise IoError("failed to update config")
}
}
}
///|
/// パスが sparse パターンにマッチするかチェック
pub fn matches_module_sparse(path : String, patterns : Array[String]) -> Bool {
if patterns.length() == 0 {
return true // パターンがない場合はすべて含める
}
let mut included = false
for pattern in patterns {
if pattern.has_prefix("!") {
// 否定パターン
let neg_pattern = if pattern.length() > 1 {
String::unsafe_substring(pattern, start=1, end=pattern.length())
} else {
""
}
if sparse_path_matches(path, neg_pattern) {
included = false
}
} else if sparse_path_matches(path, pattern) {
included = true
}
}
included
}
///|
/// パターンマッチ(シンプルな glob 対応)
fn sparse_path_matches(path : String, pattern : String) -> Bool {
let normalized_pattern = if pattern.has_prefix("/") &&
pattern != "/*" &&
pattern != "!/*/" {
String::unsafe_substring(pattern, start=1, end=pattern.length())
} else {
pattern
}
// "/*" - ルート直下のファイル
if normalized_pattern == "/*" {
return !path.contains("/")
}
// "!/*/" - ルート直下のディレクトリを除外
if normalized_pattern == "!/*/" {
return path.contains("/")
}
// "**" - 任意のパス(先にチェック)
if normalized_pattern.contains("**") {
return match_double_star(path, normalized_pattern)
}
// "dir/" - ディレクトリとその中身
if normalized_pattern.has_suffix("/") {
let dir = String::unsafe_substring(
normalized_pattern,
start=0,
end=normalized_pattern.length() - 1,
)
return path == dir || path.has_prefix(dir + "/")
}
// "dir/*" - ディレクトリ直下のファイル
if normalized_pattern.has_suffix("/*") {
let dir = String::unsafe_substring(
normalized_pattern,
start=0,
end=normalized_pattern.length() - 2,
)
if path.has_prefix(dir + "/") {
let rest = String::unsafe_substring(
path,
start=dir.length() + 1,
end=path.length(),
)
return !rest.contains("/")
}
return false
}
// "*.ext" - 拡張子マッチ
if normalized_pattern.has_prefix("*") {
let suffix = String::unsafe_substring(
normalized_pattern,
start=1,
end=normalized_pattern.length(),
)
return path.has_suffix(suffix)
}
// "prefix*" - プレフィックスマッチ
if normalized_pattern.has_suffix("*") {
let prefix = String::unsafe_substring(
normalized_pattern,
start=0,
end=normalized_pattern.length() - 1,
)
return path.has_prefix(prefix)
}
// 完全一致またはプレフィックス
path == normalized_pattern || path.has_prefix(normalized_pattern + "/")
}
///|
/// Double star パターンマッチ(例: "**/test*.mbt")
fn match_double_star(path : String, pattern : String) -> Bool {
let parts = pattern.split("**")
let parts_arr : Array[String] = []
for p in parts {
parts_arr.push(p.to_owned())
}
if parts_arr.length() != 2 {
return false
}
let prefix = parts_arr[0]
let suffix = parts_arr[1]
// プレフィックスチェック
if prefix.length() > 0 && !path.has_prefix(prefix) {
return false
}
// サフィックスのパス部分を取得(ファイル名のみ)
let filename = get_filename(path)
// サフィックスがスラッシュで始まる場合は除去
let suffix_pattern = if suffix.has_prefix("/") {
String::unsafe_substring(suffix, start=1, end=suffix.length())
} else {
suffix
}
// サフィックスにワイルドカードが含まれる場合
if suffix_pattern.contains("*") {
return match_simple_glob(filename, suffix_pattern)
}
// 単純なサフィックスマッチ
suffix.length() == 0 || path.has_suffix(suffix)
}
///|
/// ファイル名を取得(パスの最後の部分)
fn get_filename(path : String) -> 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 {
path
} else {
String::unsafe_substring(path, start=last_slash + 1, end=path.length())
}
}
///|
/// シンプルなグロブマッチ("test*.mbt" のような形式)
fn match_simple_glob(filename : String, pattern : String) -> Bool {
// パターン内の * の位置を探す
let chars = pattern.to_array()
let mut star_pos = -1
for i = 0; i < chars.length(); i = i + 1 {
if chars[i] == '*' {
star_pos = i
break
}
}
if star_pos < 0 {
// * がない場合は完全一致
return filename == pattern
}
// * の前と後で分割
let before = if star_pos == 0 {
""
} else {
String::unsafe_substring(pattern, start=0, end=star_pos)
}
let after = if star_pos + 1 >= pattern.length() {
""
} else {
String::unsafe_substring(pattern, start=star_pos + 1, end=pattern.length())
}
// プレフィックスとサフィックスをチェック
(before.length() == 0 || filename.has_prefix(before)) &&
(after.length() == 0 || filename.has_suffix(after))
}
///|
/// sparse checkout パターンファイルの内容を構築
fn build_sparse_content(patterns : Array[String]) -> String {
let result = StringBuilder::new()
result.write_string("# Sparse checkout patterns\n")
for p in patterns {
result.write_string(p)
result.write_string("\n")
}
result.to_string()
}
///|
fn sparse_trim(s : String) -> String {
let mut start = 0
let mut end = s.length()
while start < end {
let c = s.unsafe_get(start)
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
start += 1
} else {
break
}
}
while end > start {
let c = s.unsafe_get(end - 1)
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
end -= 1
} else {
break
}
}
if start == 0 && end == s.length() {
s
} else {
String::unsafe_substring(s, start~, end~)
}
}
///|
fn bytes_to_string_sparse(bytes : Bytes) -> String {
let result = StringBuilder::new()
for i in 0.. Bytes {
let bytes : Array[Byte] = []
for c in s {
bytes.push(c.to_int().to_byte())
}
Bytes::from_array(bytes)
}