///| Gitattributes text/eol attribute processing
///|
pub enum TextAttr {
Auto
Set
Unset
Unspecified
}
///|
pub enum EolAttr {
Lf
Crlf
Unspecified
}
///|
pub(all) struct FilterCmd {
run : async (String, String, String, Bytes) -> Bytes raise @bit.GitError
}
///|
pub struct FileEolAttrs {
text : TextAttr
eol : EolAttr
filter : String?
}
///|
pub fn resolve_diff_driver(
rfs : &@bit.RepoFileSystem,
root : String,
path : String,
) -> String? {
let attr_path = join_path(root, ".gitattributes")
guard rfs.is_file(attr_path) else { return None }
let content = @utf8.decode_lossy(
(rfs.read_file(attr_path) catch { _ => Default::default() })[:],
)
let normalized = gitattr_normalize_path(path)
let mut diff_driver : String? = None
for line_view in content.split("\n") {
let line = line_view.to_owned().trim().to_owned()
if line.length() == 0 || line.has_prefix("#") {
continue
}
let words : Array[String] = []
for token_view in line.split(" ") {
let token = token_view.to_owned().trim().to_owned()
if token.length() > 0 {
words.push(token)
}
}
if words.length() < 2 {
continue
}
if !gitattr_pattern_matches(normalized, words[0]) {
continue
}
let mut j = 1
while j < words.length() {
let token = words[j]
if token == "-diff" {
diff_driver = None
} else if token.has_prefix("diff=") {
let value = String::unsafe_substring(token, start=5, end=token.length())
diff_driver = if value.length() == 0 { None } else { Some(value) }
}
j += 1
}
}
diff_driver
}
///|
pub fn read_diff_function_patterns(
rfs : &@bit.RepoFileSystem,
git_dir : String,
diff_driver : String,
) -> (String?, String?) {
let section = "diff \"" + diff_driver + "\""
let mut funcname : String? = None
let mut xfuncname : String? = None
match @bitio.env_get("HOME") {
None => ()
Some(home) => {
let candidates : Array[String] = []
if @bitio.env_get("XDG_CONFIG_HOME") is Some(base) {
candidates.push(base + "/git/config")
}
candidates.push(home + "/.gitconfig")
for path in candidates {
if rfs.is_file(path) {
match read_config_value(rfs, path, section, "funcname") {
Some(v) => funcname = Some(gitattributes_unquote_config_value(v))
None => ()
}
match read_config_value(rfs, path, section, "xfuncname") {
Some(v) => xfuncname = Some(gitattributes_unquote_config_value(v))
None => ()
}
break
}
}
}
}
let local_path = join_path(git_dir, "config")
if rfs.is_file(local_path) {
match read_config_value(rfs, local_path, section, "funcname") {
Some(v) => funcname = Some(gitattributes_unquote_config_value(v))
None => ()
}
match read_config_value(rfs, local_path, section, "xfuncname") {
Some(v) => xfuncname = Some(gitattributes_unquote_config_value(v))
None => ()
}
}
(funcname, xfuncname)
}
///|
/// Parse .gitattributes and resolve eol attributes for a given path.
pub fn resolve_eol_attrs(
rfs : &@bit.RepoFileSystem,
root : String,
path : String,
) -> FileEolAttrs {
let attr_path = join_path(root, ".gitattributes")
if !rfs.is_file(attr_path) {
return {
text: TextAttr::Unspecified,
eol: EolAttr::Unspecified,
filter: None,
}
}
let content = @utf8.decode_lossy(
(rfs.read_file(attr_path) catch { _ => Default::default() })[:],
)
let normalized = gitattr_normalize_path(path)
let mut text : TextAttr = TextAttr::Unspecified
let mut eol : EolAttr = EolAttr::Unspecified
let mut filter : String? = None
for line_view in content.split("\n") {
let line = line_view.to_owned().trim().to_owned()
if line.length() == 0 || line.has_prefix("#") {
continue
}
let words : Array[String] = []
for token_view in line.split(" ") {
let token = token_view.to_owned().trim().to_owned()
if token.length() > 0 {
words.push(token)
}
}
if words.length() < 2 {
continue
}
if !gitattr_pattern_matches(normalized, words[0]) {
continue
}
let mut j = 1
while j < words.length() {
let token = words[j]
if token == "text" {
text = TextAttr::Set
} else if token == "-text" {
text = TextAttr::Unset
} else if token.has_prefix("text=") {
let value = String::unsafe_substring(token, start=5, end=token.length()).to_lower()
if value == "auto" {
text = TextAttr::Auto
} else {
text = TextAttr::Set
}
} else if token == "eol" || token.has_prefix("eol=") {
if token.has_prefix("eol=") {
let value = String::unsafe_substring(
token,
start=4,
end=token.length(),
).to_lower()
if value == "lf" {
eol = EolAttr::Lf
} else if value == "crlf" {
eol = EolAttr::Crlf
}
}
} else if token.has_prefix("filter=") {
let value = String::unsafe_substring(token, start=7, end=token.length())
if value.length() > 0 {
filter = Some(value)
}
} else if token == "crlf" {
// Legacy: crlf is equivalent to text
text = TextAttr::Set
} else if token == "-crlf" {
// Legacy: -crlf is equivalent to -text
text = TextAttr::Unset
}
j += 1
}
}
// git: eol=lf/crlf implicitly sets text (like "text eol=lf")
if text is Unspecified && !(eol is Unspecified) {
text = Set
}
{ text, eol, filter }
}
///|
fn gitattr_normalize_path(path : String) -> String {
if path.has_prefix("/") {
String::unsafe_substring(path, start=1, end=path.length())
} else if path.has_prefix("./") {
String::unsafe_substring(path, start=2, end=path.length())
} else {
path
}
}
///|
fn gitattributes_unquote_config_value(value : String) -> String {
if value.length() >= 2 && value.has_prefix("\"") && value.has_suffix("\"") {
String::unsafe_substring(value, start=1, end=value.length() - 1)
} else {
value
}
}
///|
/// Pattern matching for gitattributes (same as cat_file_attr_pattern_matches).
fn gitattr_pattern_matches(path : String, pattern : String) -> Bool {
if pattern == path {
return true
}
if pattern.has_prefix("*.") {
return path.has_suffix(
String::unsafe_substring(pattern, start=1, end=pattern.length()),
)
}
match pattern.find("*") {
Some(idx) => {
let prefix = String::unsafe_substring(pattern, start=0, end=idx)
let suffix = String::unsafe_substring(
pattern,
start=idx + 1,
end=pattern.length(),
)
path.has_prefix(prefix) &&
path.has_suffix(suffix) &&
path.length() >= prefix.length() + suffix.length()
}
None => false
}
}
///|
/// Clean: WT→blob normalization considering gitattributes text/eol.
pub fn clean_for_storage(
content : Bytes,
attrs : FileEolAttrs,
autocrlf : AutoCrlf,
) -> Bytes {
match attrs.text {
Unset => content // -text: no conversion
Set =>
// text: always normalize CRLF→LF
if is_binary_bytes(content) {
content
} else {
normalize_crlf_to_lf(content)
}
Auto =>
// text=auto: normalize only if not binary
if is_binary_bytes(content) {
content
} else {
normalize_crlf_to_lf(content)
}
Unspecified =>
// No text attribute: fall back to core.autocrlf
normalize_worktree_content(content, autocrlf)
}
}
///|
/// Smudge: blob→WT expansion considering gitattributes text/eol.
pub fn smudge_for_checkout(
content : Bytes,
attrs : FileEolAttrs,
autocrlf : AutoCrlf,
core_eol? : EolAttr = Unspecified,
) -> Bytes {
match attrs.text {
Unset => content // -text: no conversion
Set | Auto => {
let is_bin = is_binary_bytes(content)
if is_bin && attrs.text is Auto {
return content
}
if is_bin {
return content
}
match attrs.eol {
Lf => content // eol=lf: keep LF
Crlf => convert_lf_to_crlf(content) // eol=crlf: expand
Unspecified =>
// No eol attribute: check autocrlf, then core.eol
if autocrlf is On {
convert_lf_to_crlf(content)
} else {
match core_eol {
Crlf => convert_lf_to_crlf(content)
_ => content // Lf or Unspecified: keep LF (native on Linux/macOS)
}
}
}
}
Unspecified =>
// No text attribute: fall back to core.autocrlf
smudge_worktree_content(content, autocrlf)
}
}
///|
/// Check if .gitattributes has unsupported attributes (filter=lfs, working-tree-encoding).
/// Generic filter= (other than lfs) is now handled natively.
pub fn check_unsupported_gitattributes(
rfs : &@bit.RepoFileSystem,
root : String,
) -> String? {
let attr_path = join_path(root, ".gitattributes")
if !rfs.is_file(attr_path) {
return None
}
let content = @utf8.decode_lossy(
(rfs.read_file(attr_path) catch { _ => Default::default() })[:],
)
for line_view in content.split("\n") {
let line = line_view.to_owned().trim().to_owned()
if line.length() == 0 || line.has_prefix("#") {
continue
}
for token_view in line.split(" ") {
let token = token_view.to_owned().trim().to_owned()
if token.length() == 0 {
continue
}
// filter=lfs is handled natively by bit (LFS pointer resolution)
if token.has_prefix("working-tree-encoding=") {
return Some(
"fatal: working-tree-encoding is not supported in standalone mode",
)
}
}
}
None
}
///|
/// Read filter clean/smudge commands from git config.
pub fn read_filter_commands(
rfs : &@bit.RepoFileSystem,
git_dir : String,
filter_name : String,
) -> (String?, String?) {
let section = "filter \"" + filter_name + "\""
let mut clean : String? = None
let mut smudge : String? = None
// Check global config first (lower priority)
match @bitio.env_get("HOME") {
None => ()
Some(home) => {
let candidates : Array[String] = []
if @bitio.env_get("XDG_CONFIG_HOME") is Some(base) {
candidates.push(base + "/git/config")
}
candidates.push(home + "/.gitconfig")
for path in candidates {
if rfs.is_file(path) {
match read_config_value(rfs, path, section, "clean") {
Some(v) => clean = Some(v)
None => ()
}
match read_config_value(rfs, path, section, "smudge") {
Some(v) => smudge = Some(v)
None => ()
}
break
}
}
}
}
// Local config overrides global
let local_path = join_path(git_dir, "config")
if rfs.is_file(local_path) {
match read_config_value(rfs, local_path, section, "clean") {
Some(v) => clean = Some(v)
None => ()
}
match read_config_value(rfs, local_path, section, "smudge") {
Some(v) => smudge = Some(v)
None => ()
}
}
(clean, smudge)
}
///|
/// Apply the external filter driver's smudge command (blob -> worktree),
/// e.g. from `.gitattributes`' `filter=name`. Returns `content` unchanged
/// when no filter is configured for the path, the filter is `lfs` (handled
/// separately by pointer resolution), or no `run_filter_cmd` callback /
/// smudge command is available.
pub async fn apply_filter_smudge(
rfs : &@bit.RepoFileSystem,
content : Bytes,
attrs : FileEolAttrs,
root : String,
git_dir : String,
run_filter_cmd : FilterCmd?,
) -> Bytes raise @bit.GitError {
match (attrs.filter, run_filter_cmd) {
(Some(name), Some(run_cmd)) if name != "lfs" => {
let (_, smudge_cmd) = read_filter_commands(rfs, git_dir, name)
match smudge_cmd {
Some(cmd) => (run_cmd.run)(root, git_dir, cmd, content)
None => content
}
}
_ => content
}
}
///|
/// Check if .gitattributes has any filter= attributes (for storage runtime bypass).
pub fn has_any_filter_attributes(
rfs : &@bit.RepoFileSystem,
root : String,
) -> Bool {
let attr_path = join_path(root, ".gitattributes")
if !rfs.is_file(attr_path) {
return false
}
let content = @utf8.decode_lossy(
(rfs.read_file(attr_path) catch { _ => Default::default() })[:],
)
for line_view in content.split("\n") {
let line = line_view.to_owned().trim().to_owned()
if line.length() == 0 || line.has_prefix("#") {
continue
}
for token_view in line.split(" ") {
let token = token_view.to_owned().trim().to_owned()
if token.length() == 0 {
continue
}
if token.has_prefix("filter=") {
return true
}
}
}
false
}