Skip to main content

shipd_core/
git.rs

1use git2::{BranchType, Repository};
2use std::path::Path;
3
4/// Open a git repository starting from `path`, searching upward.
5pub fn open_repo(path: &Path) -> Result<Repository, git2::Error> {
6    Repository::discover(path)
7}
8
9/// Get the current branch name (e.g., "main", "shipd/3-fix-bug").
10pub fn current_branch(repo: &Repository) -> Option<String> {
11    let head = repo.head().ok()?;
12    let shorthand = head.shorthand()?;
13    Some(shorthand.to_string())
14}
15
16/// Create a new branch from HEAD and check it out.
17pub fn create_and_checkout_branch(repo: &Repository, name: &str) -> Result<(), git2::Error> {
18    let head_commit = repo.head()?.peel_to_commit()?;
19    let branch = repo.branch(name, &head_commit, false)?;
20
21    let refname = branch.into_reference().name().unwrap_or("").to_string();
22
23    repo.set_head(&refname)?;
24    repo.checkout_head(Some(git2::build::CheckoutBuilder::default().safe()))?;
25
26    Ok(())
27}
28
29/// Get recent commits on a specific branch (up to `limit`).
30pub struct CommitInfo {
31    pub hash: String,
32    pub message: String,
33    pub author: String,
34    pub time: String,
35}
36
37pub fn branch_commits(
38    repo: &Repository,
39    branch_name: &str,
40    limit: usize,
41) -> Result<Vec<CommitInfo>, git2::Error> {
42    let branch = repo.find_branch(branch_name, BranchType::Local)?;
43    let branch_ref = branch.into_reference();
44    let oid = branch_ref
45        .target()
46        .ok_or_else(|| git2::Error::from_str("branch has no target"))?;
47
48    let mut revwalk = repo.revwalk()?;
49    revwalk.push(oid)?;
50    revwalk.set_sorting(git2::Sort::TIME)?;
51
52    let mut commits = Vec::new();
53
54    for (i, oid_result) in revwalk.enumerate() {
55        if i >= limit {
56            break;
57        }
58
59        let oid = oid_result?;
60        let commit = repo.find_commit(oid)?;
61        let author = commit.author();
62
63        commits.push(CommitInfo {
64            hash: oid.to_string()[..7].to_string(),
65            message: commit
66                .message()
67                .unwrap_or("")
68                .lines()
69                .next()
70                .unwrap_or("")
71                .to_string(),
72            author: author.name().unwrap_or("unknown").to_string(),
73            time: format_commit_time(commit.time()),
74        });
75    }
76
77    Ok(commits)
78}
79
80fn format_commit_time(time: git2::Time) -> String {
81    let timestamp = time.seconds();
82    let now = std::time::SystemTime::now()
83        .duration_since(std::time::UNIX_EPOCH)
84        .unwrap_or_default()
85        .as_secs() as i64;
86
87    let diff = now - timestamp;
88
89    if diff < 60 {
90        "just now".to_string()
91    } else if diff < 3600 {
92        format!("{} min ago", diff / 60)
93    } else if diff < 86400 {
94        format!("{} hours ago", diff / 3600)
95    } else {
96        format!("{} days ago", diff / 86400)
97    }
98}
99
100/// Turn a task title into a slug for branch naming.
101/// "Fix login bug!" → "fix-login-bug"
102pub fn slugify(title: &str) -> String {
103    title
104        .to_lowercase()
105        .chars()
106        .map(|c| if c.is_alphanumeric() { c } else { '-' })
107        .collect::<String>()
108        .split('-')
109        .filter(|s| !s.is_empty())
110        .collect::<Vec<&str>>()
111        .join("-")
112}
113
114/// Build the branch name for a task: "shipd/<id>-<slug>"
115pub fn task_branch_name(task_id: i64, title: &str) -> String {
116    let slug = slugify(title);
117    format!("shipd/{task_id}-{slug}")
118}