securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use crate::cli::UI;
use crate::ops::oplog;
use anyhow::Result;
use std::path::Path;

pub fn list(path: &Path, sort: Option<&str>, pattern: Option<&str>, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;
    let tag_names = repo.tag_names(None)?;

    let mut tags: Vec<String> = tag_names.iter().flatten().map(|s| s.to_string()).collect();

    // Filter by pattern if provided (glob-style: * matches anything)
    if let Some(pat) = pattern {
        tags.retain(|t| glob_match(pat, t));
    }

    match sort {
        Some("v:refname") => tags.sort_by(|a, b| version_cmp(a, b)),
        Some("-v:refname") => tags.sort_by(|a, b| version_cmp(b, a)),
        Some("-refname") => tags.sort_by(|a, b| b.cmp(a)),
        Some("refname") => tags.sort(),
        _ => {} // default: alphabetical from libgit2
    }

    for tag in &tags {
        ui.list_item(tag);
    }

    Ok(())
}

/// Simple glob matching: `*` matches any sequence of characters, `?` matches one character.
fn glob_match(pattern: &str, text: &str) -> bool {
    let p: Vec<char> = pattern.chars().collect();
    let t: Vec<char> = text.chars().collect();
    glob_match_inner(&p, &t, 0, 0)
}

fn glob_match_inner(pattern: &[char], text: &[char], mut pi: usize, mut ti: usize) -> bool {
    while pi < pattern.len() {
        match pattern[pi] {
            '*' => {
                // Skip consecutive stars
                while pi < pattern.len() && pattern[pi] == '*' {
                    pi += 1;
                }
                if pi == pattern.len() {
                    return true;
                }
                // Try matching rest of pattern at each position
                for i in ti..=text.len() {
                    if glob_match_inner(pattern, text, pi, i) {
                        return true;
                    }
                }
                return false;
            }
            '?' => {
                if ti >= text.len() {
                    return false;
                }
                pi += 1;
                ti += 1;
            }
            c => {
                if ti >= text.len() || text[ti] != c {
                    return false;
                }
                pi += 1;
                ti += 1;
            }
        }
    }
    ti == text.len()
}

fn version_cmp(a: &str, b: &str) -> std::cmp::Ordering {
    let a = a.strip_prefix('v').unwrap_or(a);
    let b = b.strip_prefix('v').unwrap_or(b);

    let a_parts: Vec<&str> = a.split(['.', '-']).collect();
    let b_parts: Vec<&str> = b.split(['.', '-']).collect();

    for (ap, bp) in a_parts.iter().zip(b_parts.iter()) {
        let ord = match (ap.parse::<u64>(), bp.parse::<u64>()) {
            (Ok(an), Ok(bn)) => an.cmp(&bn),
            _ => ap.cmp(bp),
        };
        if ord != std::cmp::Ordering::Equal {
            return ord;
        }
    }
    a_parts.len().cmp(&b_parts.len())
}

pub fn create(
    path: &Path,
    name: &str,
    message: Option<&str>,
    target: Option<&str>,
    ui: &UI,
) -> Result<()> {
    oplog::with_oplog(path, "tag", &format!("create '{}'", name), || {
        create_inner(path, name, message, target, ui)
    })
}

fn create_inner(
    path: &Path,
    name: &str,
    message: Option<&str>,
    target: Option<&str>,
    ui: &UI,
) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    let obj = if let Some(t) = target {
        repo.revparse_single(t)?
    } else {
        repo.head()?.peel(git2::ObjectType::Commit)?
    };

    if let Some(msg) = message {
        let sig = repo.signature()?;
        repo.tag(name, &obj, &sig, msg, false)?;
        ui.success(format!("Created annotated tag '{}'", name));
    } else {
        repo.tag_lightweight(name, &obj, false)?;
        ui.success(format!("Created tag '{}'", name));
    }

    Ok(())
}

pub fn delete(path: &Path, name: &str, ui: &UI) -> Result<()> {
    oplog::with_oplog(path, "tag", &format!("delete '{}'", name), || {
        delete_inner(path, name, ui)
    })
}

fn delete_inner(path: &Path, name: &str, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;
    repo.tag_delete(name)?;
    ui.success(format!("Deleted tag '{}'", name));
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_version_cmp() {
        assert_eq!(version_cmp("v1.0.0", "v1.0.1"), std::cmp::Ordering::Less);
        assert_eq!(version_cmp("v1.0.1", "v1.0.0"), std::cmp::Ordering::Greater);
        assert_eq!(version_cmp("v1.0.0", "v1.0.0"), std::cmp::Ordering::Equal);
        assert_eq!(version_cmp("v2.0.0", "v10.0.0"), std::cmp::Ordering::Less);
        assert_eq!(version_cmp("1.0.0", "v1.0.0"), std::cmp::Ordering::Equal);
        assert_eq!(version_cmp("v0.4.2", "v0.4.10"), std::cmp::Ordering::Less);
    }

    #[test]
    fn test_glob_match_basic() {
        assert!(glob_match("*", "anything"));
        assert!(glob_match("v*", "v1.0.0"));
        assert!(glob_match("v0.*", "v0.4.5"));
        assert!(!glob_match("v1.*", "v0.4.5"));
        assert!(glob_match("v0.4.*", "v0.4.5"));
        assert!(!glob_match("v0.4.*", "v0.5.0"));
    }

    #[test]
    fn test_glob_match_question_mark() {
        assert!(glob_match("v?.0.0", "v1.0.0"));
        assert!(!glob_match("v?.0.0", "v10.0.0"));
    }

    #[test]
    fn test_glob_match_exact() {
        assert!(glob_match("v1.0.0", "v1.0.0"));
        assert!(!glob_match("v1.0.0", "v1.0.1"));
    }

    #[test]
    fn test_glob_match_empty() {
        assert!(glob_match("*", ""));
        assert!(glob_match("", ""));
        assert!(!glob_match("a", ""));
    }
}