gitkraft_core/features/repo/
ops.rs1use std::path::Path;
4
5use anyhow::{Context, Result};
6use git2::Repository;
7
8use super::types::{RepoInfo, RepoState};
9
10pub fn open_repo(path: &Path) -> Result<Repository> {
15 Repository::discover(path)
16 .with_context(|| format!("failed to open repository at {}", path.display()))
17}
18
19pub fn init_repo(path: &Path) -> Result<Repository> {
21 Repository::init(path)
22 .with_context(|| format!("failed to init repository at {}", path.display()))
23}
24
25pub fn clone_repo(url: &str, path: &Path) -> Result<Repository> {
31 Repository::clone(url, path)
32 .with_context(|| format!("failed to clone '{url}' into {}", path.display()))
33}
34
35pub fn get_repo_info(repo: &Repository) -> Result<RepoInfo> {
37 let path = repo.path().to_path_buf();
38 let workdir = repo.workdir().map(|p| p.to_path_buf());
39 let is_bare = repo.is_bare();
40 let state: RepoState = repo.state().into();
41
42 let head_branch = repo.head().ok().and_then(|reference| {
43 if reference.is_branch() {
44 reference.shorthand().map(String::from)
45 } else {
46 reference.target().map(|oid| {
48 let s = oid.to_string();
49 s[..7.min(s.len())].to_string()
50 })
51 }
52 });
53
54 Ok(RepoInfo {
55 path,
56 workdir,
57 head_branch,
58 is_bare,
59 state,
60 })
61}
62
63pub fn checkout_commit_detached(repo: &Repository, oid_str: &str) -> Result<()> {
65 let oid = git2::Oid::from_str(oid_str).with_context(|| format!("invalid OID: {oid_str}"))?;
66 let commit = repo
67 .find_commit(oid)
68 .with_context(|| format!("commit {oid_str} not found"))?;
69 repo.set_head_detached(oid)
70 .context("failed to detach HEAD")?;
71 repo.checkout_tree(
72 commit.as_object(),
73 Some(git2::build::CheckoutBuilder::new().force()),
74 )
75 .context("failed to checkout commit tree")?;
76 Ok(())
77}
78
79pub fn revert_commit(workdir: &std::path::Path, oid_str: &str) -> Result<()> {
81 let output = std::process::Command::new("git")
82 .current_dir(workdir)
83 .args(["revert", "--no-edit", oid_str])
84 .output()
85 .context("failed to spawn git")?;
86 if !output.status.success() {
87 let stderr = String::from_utf8_lossy(&output.stderr);
88 anyhow::bail!("{}", stderr.trim());
89 }
90 Ok(())
91}
92
93pub fn reset_to_commit(workdir: &std::path::Path, oid_str: &str, mode: &str) -> Result<()> {
100 let flag = format!("--{mode}");
101 let output = std::process::Command::new("git")
102 .current_dir(workdir)
103 .args(["reset", &flag, oid_str])
104 .output()
105 .context("failed to spawn git")?;
106 if !output.status.success() {
107 let stderr = String::from_utf8_lossy(&output.stderr);
108 anyhow::bail!("{}", stderr.trim());
109 }
110 Ok(())
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use tempfile::TempDir;
117
118 #[test]
119 fn init_and_open() {
120 let tmp = TempDir::new().unwrap();
121 let repo = init_repo(tmp.path()).unwrap();
122 assert!(!repo.is_bare());
123
124 let reopened = open_repo(tmp.path()).unwrap();
125 assert_eq!(
126 repo.path().canonicalize().unwrap(),
127 reopened.path().canonicalize().unwrap(),
128 );
129 }
130
131 #[test]
132 fn repo_info_on_fresh_repo() {
133 let tmp = TempDir::new().unwrap();
134 let repo = init_repo(tmp.path()).unwrap();
135 let info = get_repo_info(&repo).unwrap();
136
137 assert!(!info.is_bare);
138 assert_eq!(info.state, RepoState::Clean);
139 assert!(info.head_branch.is_none());
141 assert!(info.workdir.is_some());
142 }
143
144 #[test]
145 fn repo_info_with_commit() {
146 let tmp = TempDir::new().unwrap();
147 let repo = init_repo(tmp.path()).unwrap();
148
149 let sig = git2::Signature::now("Test", "test@test.com").unwrap();
151 let tree_oid = {
152 let mut index = repo.index().unwrap();
153 index.write_tree().unwrap()
154 };
155 let tree = repo.find_tree(tree_oid).unwrap();
156 repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
157 .unwrap();
158
159 let info = get_repo_info(&repo).unwrap();
160 assert!(info.head_branch.is_some());
162 }
163}