///| Pure commit helper functions — string/bytes manipulation with no IO.
///|
/// Trim leading/trailing whitespace from a string.
fn commit_trim_string(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~)
}
}
///|
/// Strip comment lines and trailing/leading blank lines from a commit message.
pub fn commit_strip_message(msg : String) -> String {
let lines : Array[String] = []
for line_view in msg.split("\n") {
let line = line_view.to_owned()
let trimmed = commit_trim_string(line)
// Skip comment lines (lines starting with #)
if trimmed.has_prefix("#") {
continue
}
lines.push(line)
}
// Strip trailing empty lines
while lines.length() > 0 {
let last = lines[lines.length() - 1]
if commit_trim_string(last).length() == 0 {
let _ = lines.pop()
} else {
break
}
}
// Strip leading empty lines
while lines.length() > 0 && commit_trim_string(lines[0]).length() == 0 {
let _ = lines.remove(0)
}
lines.join("\n")
}
///|
/// Check if a line is a trailer line (e.g., "Signed-off-by: ...").
pub fn commit_is_trailer_line(line : String) -> Bool {
let trimmed = commit_trim_string(line)
// A trailer line matches "Token: Value" or "Token #Value"
match trimmed.find(": ") {
Some(idx) => {
// Token must be non-empty and contain only word chars and hyphens
let token = String::unsafe_substring(trimmed, start=0, end=idx)
if token.length() == 0 {
return false
}
for c in token {
if !((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-') {
return false
}
}
true
}
None => false
}
}
///|
/// Apply Signed-off-by trailer to a commit message.
/// The `committer` parameter is the committer identity string (e.g. "Name ").
pub fn commit_apply_signoff(msg : String, committer~ : String) -> String {
let signoff_line = "Signed-off-by: " + committer
// Check if signoff already present at the end
let lines : Array[String] = []
for line_view in msg.split("\n") {
lines.push(line_view.to_owned())
}
// Strip trailing newlines for checking
let mut last_nonblank = lines.length() - 1
while last_nonblank >= 0 &&
commit_trim_string(lines[last_nonblank]).length() == 0 {
last_nonblank -= 1
}
if last_nonblank >= 0 &&
commit_trim_string(lines[last_nonblank]) == signoff_line {
return msg
}
// Determine if we need a blank line separator.
// A blank line is needed if the last non-blank line is not a trailer.
let need_blank = if last_nonblank >= 0 {
!commit_is_trailer_line(lines[last_nonblank])
} else {
true
}
let result = StringBuilder::new()
// Preserve original message
if msg.has_suffix("\n") {
result.write_string(msg)
} else {
result.write_string(msg)
result.write_char('\n')
}
if need_blank {
result.write_char('\n')
}
result.write_string(signoff_line)
result.write_char('\n')
result.to_string()
}
///|
/// Decode commit message bytes, handling ISO-8859-1 and UTF-8 encodings.
pub fn decode_commit_message_bytes(data : Bytes, encoding : String) -> String {
if is_iso_8859_1_encoding(encoding) {
let out = StringBuilder::new()
for b in data {
out.write_char(b.to_int().unsafe_to_char())
}
out.to_string()
} else {
@utf8.decode_lossy(data[:])
}
}
///|
fn is_iso_8859_1_encoding(encoding : String) -> Bool {
let normalized = normalize_commit_encoding_name(encoding)
normalized == "iso88591" || normalized == "latin1" || normalized == "latin"
}
///|
fn normalize_commit_encoding_name(value : String) -> String {
let out = StringBuilder::new()
for c in commit_trim_string(value).to_lower() {
if c == '-' || c == '_' || c == ' ' {
continue
}
out.write_char(c)
}
out.to_string()
}
///|
/// Parse a tag object string to extract the target object ID.
/// Returns the ObjectId of the tagged object (from the "object " line).
pub fn parse_tag_object(data : String) -> @bit.ObjectId? raise Error {
for line_view in data.split("\n") {
let line = line_view.to_owned()
if line.has_prefix("object ") {
let hex = String::unsafe_substring(line, start=7, end=line.length())
return hex |> commit_trim_string |> @bit.ObjectId::from_hex |> Some
}
}
None
}