1use git2::{BranchType, Repository};
2use std::path::Path;
3
4pub fn open_repo(path: &Path) -> Result<Repository, git2::Error> {
6 Repository::discover(path)
7}
8
9pub 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
16pub 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
29pub 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
100pub 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
114pub fn task_branch_name(task_id: i64, title: &str) -> String {
116 let slug = slugify(title);
117 format!("shipd/{task_id}-{slug}")
118}