1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::config::{self, hash_path};
5use crate::error::{Error, Result};
6use crate::git;
7
8pub struct SideRepo {
10 pub git_dir: PathBuf,
12 pub work_tree: PathBuf,
14 pub root_sha: String,
16}
17
18impl SideRepo {
19 pub fn open() -> Result<Self> {
25 let work_tree = git::repo_root()?;
26 let path_hash = hash_path(&work_tree);
27
28 let root_sha = if let Some(sha) = config::cache_lookup(&path_hash)? {
30 sha
31 } else {
32 let sha = git::initial_commit_sha()?;
34 config::cache_store(&path_hash, &sha)?;
35 sha
36 };
37
38 let base_path = config::paths_lookup(&root_sha)?
40 .unwrap_or_else(config::default_base_path);
41
42 let git_dir = base_path.join(&root_sha);
43
44 Ok(Self {
45 git_dir,
46 work_tree,
47 root_sha,
48 })
49 }
50
51 #[must_use]
53 pub fn is_initialized(&self) -> bool {
54 self.git_dir.exists() && self.git_dir.join("HEAD").exists()
55 }
56
57 pub fn ensure_initialized(&self) -> Result<()> {
63 if self.is_initialized() {
64 return Ok(());
65 }
66
67 if let Some(parent) = self.git_dir.parent() {
69 fs::create_dir_all(parent).map_err(|e| Error::CreateDir {
70 path: parent.to_path_buf(),
71 source: e,
72 })?;
73 }
74
75 let git_dir_str = self.git_dir.to_string_lossy();
77 git::run(&["init", "--bare", &git_dir_str])?;
78
79 Ok(())
80 }
81
82 pub fn git(&self, args: &[&str]) -> Result<String> {
88 git::run_with_paths(&self.git_dir, &self.work_tree, args)
89 }
90
91 #[must_use]
93 pub fn tracked_file(&self) -> PathBuf {
94 self.git_dir.join(".side-tracked")
95 }
96
97 pub fn stage(&self, path: &Path) -> Result<()> {
103 self.ensure_initialized()?;
104 let path_str = path.to_string_lossy();
105 self.git(&["add", "-f", &path_str])?;
106 Ok(())
107 }
108
109 pub fn stage_update(&self, paths: &[PathBuf]) {
112 if paths.is_empty() {
113 return;
114 }
115 if self.ensure_initialized().is_err() {
116 return;
117 }
118
119 let path_strs: Vec<String> = paths.iter().map(|p| p.to_string_lossy().into_owned()).collect();
120 let mut args: Vec<&str> = vec!["add", "-f", "-u", "--"];
121 args.extend(path_strs.iter().map(String::as_str));
122
123 let _ = self.git(&args);
125 }
126
127 pub fn stage_new(&self, paths: &[PathBuf]) -> Result<()> {
133 if paths.is_empty() {
134 return Ok(());
135 }
136 self.ensure_initialized()?;
137
138 let path_strs: Vec<String> = paths.iter().map(|p| p.to_string_lossy().into_owned()).collect();
139 let mut args: Vec<&str> = vec!["add", "-f", "--"];
140 args.extend(path_strs.iter().map(String::as_str));
141
142 self.git(&args)?;
143 Ok(())
144 }
145
146 pub fn commit(&self, message: &str) -> Result<()> {
152 self.ensure_initialized()?;
153
154 let has_staged = self.git(&["diff", "--cached", "--quiet"]).is_err();
157 if !has_staged {
158 return Err(Error::NothingToCommit);
159 }
160
161 self.git(&["commit", "-m", message])?;
162 Ok(())
163 }
164
165 pub fn status(&self) -> Result<String> {
171 if !self.is_initialized() {
172 return Ok(String::from("Side repo not initialized. Use 'git side add <path>' to start tracking files."));
173 }
174 self.git(&["status"])
175 }
176
177 pub fn log(&self, args: &[&str]) -> Result<String> {
183 if !self.is_initialized() {
184 return Ok(String::from("Side repo not initialized. No history yet."));
185 }
186
187 let mut log_args = vec!["log"];
188 log_args.extend(args);
189 self.git(&log_args)
190 }
191
192 pub fn unstage(&self, path: &Path) -> Result<()> {
198 if !self.is_initialized() {
199 return Ok(());
200 }
201 let path_str = path.to_string_lossy();
202 let _ = self.git(&["rm", "--cached", "-r", "--ignore-unmatch", &path_str]);
204 Ok(())
205 }
206
207 pub fn push(&self) -> Result<()> {
213 self.git(&["push", "-u", "--force", "origin", "main"])?;
214 Ok(())
215 }
216
217 pub fn stage_tracked_file(&self) -> Result<()> {
224 let tracked_file = self.tracked_file();
225 if !tracked_file.exists() {
226 return Ok(());
227 }
228
229 let tracked_path_str = tracked_file.to_string_lossy();
231 let sha = self.git(&["hash-object", "-w", &tracked_path_str])?;
232 let sha = sha.trim();
233
234 let cacheinfo = format!("100644,{sha},.side-tracked");
236 self.git(&["update-index", "--add", "--cacheinfo", &cacheinfo])?;
237
238 Ok(())
239 }
240}