///|
fn if_function_arguments_at_least(
expression : String,
function_name : String,
min_arity : Int,
) -> Array[String]? {
let trimmed = strip_if_outer_parens(expression)
let lower = trimmed.to_lower()
let prefix = function_name.to_lower() + "("
guard lower.has_prefix(prefix) && trimmed.has_suffix(")") else { return None }
let inner = if_text_slice(trimmed, prefix.length(), trimmed.length() - 1)
guard split_top_level_if_arguments(inner) is Some(args) else { return None }
if args.length() < min_arity {
None
} else {
Some(args)
}
}
///|
fn if_hash_files_function_supported(text : String) -> Bool {
match if_function_arguments_at_least(text, "hashFiles", 1) {
Some(args) => {
for arg in args {
if !if_condition_supported(arg) {
return false
}
}
true
}
None => false
}
}
///|
fn if_hash_files_join(base : String, child : String) -> String {
if child.has_prefix("/") {
return child
}
if base.length() == 0 || base == "." {
child
} else if base.has_suffix("/") {
base + child
} else {
base + "/" + child
}
}
///|
fn if_hash_files_is_dir(path : String) -> Bool {
try @xfs.is_dir(path) catch {
_ => false
} noraise {
value => value
}
}
///|
fn if_hash_files_normalize_pattern(pattern : String) -> String {
let trimmed = pattern.trim(chars=" \t\r\n").to_owned()
let without_prefix = if trimmed.has_prefix("./") {
if_text_slice(trimmed, 2, trimmed.length())
} else if trimmed.has_prefix("/") {
if_text_slice(trimmed, 1, trimmed.length())
} else {
trimmed
}
if without_prefix.has_suffix("/") {
if_text_slice(without_prefix, 0, without_prefix.length() - 1)
} else {
without_prefix
}
}
///|
fn if_hash_files_pattern_matches(
pattern : String,
relative_path : String,
) -> Bool {
let normalized = if_hash_files_normalize_pattern(pattern)
if normalized.length() == 0 {
return false
}
if wildcard_match(normalized, relative_path) {
return true
}
if normalized.has_prefix("**/") && normalized.length() > 3 {
return if_hash_files_pattern_matches(
if_text_slice(normalized, 3, normalized.length()),
relative_path,
)
}
false
}
///|
fn if_hash_files_collect_files(
current_abs : String,
relative_prefix : String,
files : Array[String],
) -> Unit {
let entries = @xfs.read_dir(current_abs) catch { _ => [] }
entries.sort_by(String::lexical_compare)
for entry in entries {
let next_abs = if_hash_files_join(current_abs, entry)
let next_relative = if relative_prefix.length() == 0 {
entry
} else {
relative_prefix + "/" + entry
}
if if_hash_files_is_dir(next_abs) {
if_hash_files_collect_files(next_abs, next_relative, files)
} else {
files.push(next_relative)
}
}
}
///|
fn if_hash_files_selected_paths(
workspace_root : String,
patterns : Array[String],
) -> Array[String] {
if workspace_root.length() == 0 || !@xfs.path_exists(workspace_root) {
return []
}
let workspace_abs = absolute_exec_path(workspace_root)
let all_files : Array[String] = []
if_hash_files_collect_files(workspace_abs, "", all_files)
let selected : Map[String, Bool] = {}
for raw_pattern in patterns {
let trimmed = raw_pattern.trim(chars=" \t\r\n").to_owned()
if trimmed.length() == 0 {
continue
}
let excluded = trimmed.has_prefix("!")
let pattern = if excluded {
if_text_slice(trimmed, 1, trimmed.length())
} else {
trimmed
}
for relative_path in all_files {
if if_hash_files_pattern_matches(pattern, relative_path) {
if excluded {
selected.remove(relative_path)
} else {
selected[relative_path] = true
}
}
}
}
let values : Array[String] = []
for relative_path, _ in selected {
values.push(relative_path)
}
values.sort_by(String::lexical_compare)
values
}
///|
fn if_hash_files_digest(
workspace_root : String,
patterns : Array[String],
) -> String {
let matched = if_hash_files_selected_paths(workspace_root, patterns)
if matched.length() == 0 {
return ""
}
let workspace_abs = absolute_exec_path(workspace_root)
let final_hash = @crypto.SHA256::new()
for relative_path in matched {
let absolute_path = if_hash_files_join(workspace_abs, relative_path)
let bytes = @xfs.read_file_to_bytes(absolute_path) catch { _ => continue }
final_hash.update(@crypto.sha256(bytes))
}
@crypto.bytes_to_hex_string(final_hash.finalize())
}
///|
fn if_hash_files_function_value(
text : String,
ctx : IfConditionContext,
) -> ConditionValue? {
match if_function_arguments_at_least(text, "hashFiles", 1) {
Some(args) => {
let patterns : Array[String] = []
for arg in args {
patterns.push(
if_condition_value_to_string(if_condition_atom_value(arg, ctx)),
)
}
let digest = if_hash_files_digest(ctx.workspace_root, patterns)
Some(if_condition_value_from_json(Json::string(digest)))
}
None => None
}
}