///| Git reflog implementation
///|
#warnings("-deprecated")
fn reflog_parse_int64(value : StringView) -> Int64 raise {
@strconv.parse_int64(value)
}
///|
pub struct ReflogEntry {
old_id : @bit.ObjectId
new_id : @bit.ObjectId
author : String
email : String
timestamp : Int64
timezone : String
message : String
}
///|
/// Check if a reflog exists for the given ref.
pub fn reflog_exists(
fs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
) -> Bool {
let reflog_path = get_reflog_path(git_dir, refname)
fs.is_file(reflog_path)
}
///|
/// Get the reflog file path for a given ref.
pub fn get_reflog_path(git_dir : String, refname : String) -> String {
if refname == "HEAD" {
git_dir + "/logs/HEAD"
} else {
git_dir + "/logs/" + refname
}
}
///|
/// Read reflog entries for a given ref.
pub fn read_reflog(
fs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
) -> Array[ReflogEntry] raise @bit.GitError {
let reflog_path = get_reflog_path(git_dir, refname)
if !fs.is_file(reflog_path) {
return []
}
let content = @utf8.decode_lossy(fs.read_file(reflog_path)[:])
let entries : Array[ReflogEntry] = []
for line_view in content.split("\n") {
let line = line_view.to_owned()
if line.length() == 0 {
continue
}
match parse_reflog_line(line) {
Some(entry) => entries.push(entry)
None => continue
}
}
entries
}
///|
/// Parse a single reflog line.
fn parse_reflog_line(line : String) -> ReflogEntry? {
// Format: <> \t
// git omits the tab + message entirely for messageless updates, so a line
// without a tab is valid and carries an empty message.
let (prefix, message) = match line.find("\t") {
Some(idx) =>
(
String::unsafe_substring(line, start=0, end=idx),
String::unsafe_substring(line, start=idx + 1, end=line.length()),
)
None => (line, "")
}
// Parse prefix: old_sha new_sha author_name timestamp tz
let parts : Array[String] = []
for p in prefix.split(" ") {
parts.push(p.to_owned())
}
if parts.length() < 5 {
return None
}
let old_id = @bit.ObjectId::from_hex(parts[0]) catch { _ => return None }
let new_id = @bit.ObjectId::from_hex(parts[1]) catch { _ => return None }
// Author name can have spaces, email is in angle brackets
// Find the email (surrounded by < >)
let mut author = ""
let mut email = ""
let mut timestamp : Int64 = 0L
let mut timezone = "+0000"
let mut i = 2
// Collect author name until we hit <
while i < parts.length() {
if parts[i].has_prefix("<") {
// Found email start
let email_part = parts[i]
email = String::unsafe_substring(
email_part,
start=1,
end=email_part.length() - 1,
)
i += 1
break
}
if author.length() > 0 {
author = author + " "
}
author = author + parts[i]
i += 1
}
// Next should be timestamp and timezone
if i < parts.length() {
timestamp = reflog_parse_int64(parts[i]) catch { _ => 0L }
i += 1
}
if i < parts.length() {
timezone = parts[i]
}
Some({ old_id, new_id, author, email, timestamp, timezone, message })
}
///|
/// Append a reflog entry.
pub fn append_reflog(
wfs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
old_id : @bit.ObjectId,
new_id : @bit.ObjectId,
author : String,
email : String,
timestamp : Int64,
timezone : String,
message : String,
) -> Unit raise @bit.GitError {
let reflog_path = get_reflog_path(git_dir, refname)
// Ensure parent directory exists
let parent_dir = match reflog_path.rev_find("/") {
None => ""
Some(i) => String::unsafe_substring(reflog_path, start=0, end=i)
}
if parent_dir.length() > 0 && !rfs.is_dir(parent_dir) {
wfs.mkdir_p(parent_dir)
}
// Format: <> \t\n
// git only emits the tab + message when the message is non-empty; an empty
// reason produces a line ending right after the timezone (matching
// log_ref_write_fd's `if (msg && *msg)` guard).
let prefix = "\{old_id.to_hex()} \{new_id.to_hex()} \{author} <\{email}> \{timestamp} \{timezone}"
let entry = if message.length() > 0 {
"\{prefix}\t\{message}\n"
} else {
"\{prefix}\n"
}
// Append to file
if rfs.is_file(reflog_path) {
let existing = @utf8.decode_lossy(rfs.read_file(reflog_path)[:])
wfs.write_string(reflog_path, existing.to_string() + entry)
} else {
wfs.write_string(reflog_path, entry)
}
}
///|
/// Create an empty reflog file for a ref.
pub fn create_reflog(
wfs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
) -> Unit raise @bit.GitError {
let reflog_path = get_reflog_path(git_dir, refname)
let parent_dir = match reflog_path.rev_find("/") {
None => ""
Some(i) => String::unsafe_substring(reflog_path, start=0, end=i)
}
if parent_dir.length() > 0 && !rfs.is_dir(parent_dir) {
wfs.mkdir_p(parent_dir)
}
if !rfs.is_file(reflog_path) {
wfs.write_string(reflog_path, "")
}
}
///|
/// Delete a reflog file.
pub fn delete_reflog(
wfs : &@bit.FileSystem,
rfs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
) -> Unit raise @bit.GitError {
let reflog_path = get_reflog_path(git_dir, refname)
if rfs.is_file(reflog_path) {
wfs.remove_file(reflog_path)
}
}
///|
fn trim_whitespace(s : String) -> String {
@string_utils.trim_string(s)
}
///|
/// Check if reflogs should be created for a ref based on config.
/// Returns: (should_log, always_log)
/// - should_log: true if this ref should have reflogs
/// - always_log: true if core.logAllRefUpdates=always
pub fn should_log_ref(
fs : &@bit.RepoFileSystem,
git_dir : String,
refname : String,
is_bare : Bool,
) -> (Bool, Bool) raise @bit.GitError {
// Read core.logAllRefUpdates from config
let config_path = git_dir + "/config"
let mut log_all_ref_updates : String? = None
if fs.is_file(config_path) {
let content = @utf8.decode_lossy(fs.read_file(config_path)[:])
let mut in_core = false
for line_view in content.split("\n") {
let line = trim_whitespace(line_view.to_owned())
if line.has_prefix("[core]") {
in_core = true
continue
}
if line.has_prefix("[") {
in_core = false
continue
}
if in_core {
if line.has_prefix("logAllRefUpdates") ||
line.has_prefix("logallrefupdates") {
let eq_idx = line.find("=")
match eq_idx {
Some(i) =>
log_all_ref_updates = Some(
trim_whitespace(
String::unsafe_substring(line, start=i + 1, end=line.length()),
),
)
None => ()
}
}
}
}
}
match log_all_ref_updates {
Some("always") => (true, true)
Some("true") | Some("1") =>
// Log branch/remote refs only, not arbitrary refs.
(
refname.has_prefix("refs/heads/") ||
refname.has_prefix("refs/remotes/") ||
(!is_bare && refname == "HEAD"),
false,
)
Some("false") | Some("0") => (false, false)
_ =>
if is_bare {
(false, false)
} else {
(
refname.has_prefix("refs/heads/") ||
refname.has_prefix("refs/remotes/") ||
refname == "HEAD",
false,
)
}
}
}