omni_dev/git/
repository.rs1use crate::git::CommitInfo;
4use anyhow::{Context, Result};
5use git2::{Repository, Status};
6
7pub struct GitRepository {
9 repo: Repository,
10}
11
12#[derive(Debug)]
14pub struct WorkingDirectoryStatus {
15 pub clean: bool,
17 pub untracked_changes: Vec<FileStatus>,
19}
20
21#[derive(Debug)]
23pub struct FileStatus {
24 pub status: String,
26 pub file: String,
28}
29
30impl GitRepository {
31 pub fn open() -> Result<Self> {
33 let repo = Repository::open(".").context("Not in a git repository")?;
34
35 Ok(Self { repo })
36 }
37
38 pub fn open_at<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
40 let repo = Repository::open(path).context("Failed to open git repository")?;
41
42 Ok(Self { repo })
43 }
44
45 pub fn get_working_directory_status(&self) -> Result<WorkingDirectoryStatus> {
47 let statuses = self
48 .repo
49 .statuses(None)
50 .context("Failed to get repository status")?;
51
52 let mut untracked_changes = Vec::new();
53
54 for entry in statuses.iter() {
55 if let Some(path) = entry.path() {
56 let status_flags = entry.status();
57 let status_str = format_status_flags(status_flags);
58
59 untracked_changes.push(FileStatus {
60 status: status_str,
61 file: path.to_string(),
62 });
63 }
64 }
65
66 let clean = untracked_changes.is_empty();
67
68 Ok(WorkingDirectoryStatus {
69 clean,
70 untracked_changes,
71 })
72 }
73
74 pub fn is_working_directory_clean(&self) -> Result<bool> {
76 let status = self.get_working_directory_status()?;
77 Ok(status.clean)
78 }
79
80 pub fn path(&self) -> &std::path::Path {
82 self.repo.path()
83 }
84
85 pub fn workdir(&self) -> Option<&std::path::Path> {
87 self.repo.workdir()
88 }
89
90 pub fn repository(&self) -> &Repository {
92 &self.repo
93 }
94
95 pub fn get_commits_in_range(&self, range: &str) -> Result<Vec<CommitInfo>> {
97 let mut commits = Vec::new();
98
99 if range == "HEAD" {
100 let head = self.repo.head().context("Failed to get HEAD")?;
102 let commit = head
103 .peel_to_commit()
104 .context("Failed to peel HEAD to commit")?;
105 commits.push(CommitInfo::from_git_commit(&self.repo, &commit)?);
106 } else if range.contains("..") {
107 let parts: Vec<&str> = range.split("..").collect();
109 if parts.len() != 2 {
110 anyhow::bail!("Invalid range format: {}", range);
111 }
112
113 let start_spec = parts[0];
114 let end_spec = parts[1];
115
116 let start_obj = self
118 .repo
119 .revparse_single(start_spec)
120 .with_context(|| format!("Failed to parse start commit: {}", start_spec))?;
121 let end_obj = self
122 .repo
123 .revparse_single(end_spec)
124 .with_context(|| format!("Failed to parse end commit: {}", end_spec))?;
125
126 let start_commit = start_obj
127 .peel_to_commit()
128 .context("Failed to peel start object to commit")?;
129 let end_commit = end_obj
130 .peel_to_commit()
131 .context("Failed to peel end object to commit")?;
132
133 let mut walker = self.repo.revwalk().context("Failed to create revwalk")?;
135 walker
136 .push(end_commit.id())
137 .context("Failed to push end commit")?;
138 walker
139 .hide(start_commit.id())
140 .context("Failed to hide start commit")?;
141
142 for oid in walker {
143 let oid = oid.context("Failed to get commit OID from walker")?;
144 let commit = self
145 .repo
146 .find_commit(oid)
147 .context("Failed to find commit")?;
148
149 if commit.parent_count() > 1 {
151 continue;
152 }
153
154 commits.push(CommitInfo::from_git_commit(&self.repo, &commit)?);
155 }
156
157 commits.reverse();
159 } else {
160 let obj = self
162 .repo
163 .revparse_single(range)
164 .with_context(|| format!("Failed to parse commit: {}", range))?;
165 let commit = obj
166 .peel_to_commit()
167 .context("Failed to peel object to commit")?;
168 commits.push(CommitInfo::from_git_commit(&self.repo, &commit)?);
169 }
170
171 Ok(commits)
172 }
173}
174
175fn format_status_flags(flags: Status) -> String {
177 let mut status = String::new();
178
179 if flags.contains(Status::INDEX_NEW) {
180 status.push('A');
181 } else if flags.contains(Status::INDEX_MODIFIED) {
182 status.push('M');
183 } else if flags.contains(Status::INDEX_DELETED) {
184 status.push('D');
185 } else if flags.contains(Status::INDEX_RENAMED) {
186 status.push('R');
187 } else if flags.contains(Status::INDEX_TYPECHANGE) {
188 status.push('T');
189 } else {
190 status.push(' ');
191 }
192
193 if flags.contains(Status::WT_NEW) {
194 status.push('?');
195 } else if flags.contains(Status::WT_MODIFIED) {
196 status.push('M');
197 } else if flags.contains(Status::WT_DELETED) {
198 status.push('D');
199 } else if flags.contains(Status::WT_TYPECHANGE) {
200 status.push('T');
201 } else if flags.contains(Status::WT_RENAMED) {
202 status.push('R');
203 } else {
204 status.push(' ');
205 }
206
207 status
208}