1use std::ffi::OsString;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5use std::time::SystemTime;
6
7use chrono::{Local, NaiveDate, NaiveDateTime};
8
9const DATE_PREFIX_FORMAT: &str = "%Y-%m-%d";
10
11pub fn is_inside_git_repo<P: AsRef<Path>>(path: P) -> bool {
13 Command::new("git")
14 .args(["rev-parse", "--is-inside-work-tree"])
15 .current_dir(path.as_ref())
16 .output()
17 .map(|output| output.status.success())
18 .unwrap_or(false)
19}
20
21pub fn is_git_worktree_locked(path: &Path) -> bool {
22 let dot_git = path.join(".git");
23 if dot_git.is_file() {
24 let parent = parse_dot_git(&dot_git);
25 match parent {
26 Ok(parent_path) => {
27 return parent_path.join("locked").exists();
28 }
29 Err(_) => {
30 return false;
31 }
32 }
33 }
34 false
35}
36
37pub fn is_git_worktree(path: &Path) -> bool {
40 let dot_git = path.join(".git");
41 dot_git.is_file()
43}
44
45fn parse_dot_git(dot_git: &Path) -> std::io::Result<PathBuf> {
46 Ok(first_line(&std::fs::read(dot_git)?).into())
47}
48
49#[cfg(unix)]
50pub fn first_line(bytes: &[u8]) -> OsString {
51 use std::os::unix::ffi::OsStringExt;
52 OsString::from_vec(
53 bytes
54 .iter()
55 .copied()
56 .skip_while(|&b| b != b' ')
57 .skip(1)
58 .take_while(|&b| b != b'\n')
59 .collect::<Vec<_>>(),
60 )
61}
62
63#[cfg(not(unix))]
64pub fn first_line(bytes: &[u8]) -> OsString {
65 let vec: Vec<u8> = bytes
66 .iter()
67 .copied()
68 .skip_while(|&b| b != b' ')
69 .skip(1)
70 .take_while(|&b| b != b'\n')
71 .collect();
72 OsString::from(String::from_utf8_lossy(&vec).to_string())
73}
74
75pub fn remove_git_worktree(path_to_remove: &Path) -> std::io::Result<std::process::Output> {
76 Command::new("git")
77 .args(["worktree", "remove", "."])
78 .current_dir(path_to_remove)
79 .output()
80}
81
82pub fn expand_path(path_str: &str) -> PathBuf {
83 if (path_str.starts_with("~/") || (cfg!(windows) && path_str.starts_with("~\\")))
84 && let Some(home) = dirs::home_dir()
85 {
86 return home.join(&path_str[2..]);
87 }
88 PathBuf::from(path_str)
89}
90
91pub fn is_git_url(s: &str) -> bool {
92 s.starts_with("http://")
93 || s.starts_with("https://")
94 || s.starts_with("git@")
95 || s.starts_with("ssh://")
96 || s.ends_with(".git")
97}
98
99pub fn extract_repo_name(url: &str) -> String {
100 let clean_url = url.trim_end_matches('/').trim_end_matches(".git");
101 if let Some(last_part) = clean_url.rsplit(['/', ':']).next()
102 && !last_part.is_empty()
103 {
104 return last_part.to_string();
105 }
106 "cloned-repo".to_string()
107}
108
109#[cfg(unix)]
110pub fn get_free_disk_space_mb(path: &Path) -> Option<u64> {
111 use std::ffi::CString;
112 use std::mem::MaybeUninit;
113 use std::os::unix::ffi::OsStrExt;
114
115 let c_path = CString::new(path.as_os_str().as_bytes()).ok()?;
116 let mut stat: MaybeUninit<libc::statvfs> = MaybeUninit::uninit();
117
118 unsafe {
119 if libc::statvfs(c_path.as_ptr(), stat.as_mut_ptr()) == 0 {
120 let stat = stat.assume_init();
121 let free_bytes = (stat.f_bavail as u64) * (stat.f_frsize as u64);
122 return Some(free_bytes / (1024 * 1024));
123 }
124 }
125 None
126}
127
128#[cfg(not(unix))]
129pub fn get_free_disk_space_mb(_path: &Path) -> Option<u64> {
130 None
131}
132
133pub fn extract_prefix_date(name: &str) -> Option<(SystemTime, String)> {
134 let (lhs, rhs) = name.split_once(' ')?;
135 let naive_date = NaiveDate::parse_from_str(lhs, DATE_PREFIX_FORMAT).ok()?;
136 let dt: NaiveDateTime = naive_date.into();
137 let dt_local = dt.and_local_timezone(Local).single()?;
138 Some((dt_local.into(), rhs.into()))
139}
140
141pub fn generate_prefix_date() -> String {
142 let now = Local::now();
143 now.format("%Y-%m-%d").to_string()
144}
145
146pub fn get_folder_size_mb(path: &Path) -> u64 {
147 fn dir_size(path: &Path) -> u64 {
148 let mut stack = vec![path.to_path_buf()];
149 let mut size = 0u64;
150 while let Some(dir) = stack.pop() {
151 let Ok(entries) = fs::read_dir(&dir) else {
152 continue;
153 };
154 for entry in entries.flatten() {
155 let Ok(meta) = entry.metadata() else {
157 continue;
158 };
159 if meta.is_dir() {
160 stack.push(entry.path());
161 } else if meta.is_file() {
162 size += meta.len();
163 }
164 }
166 }
167 size
168 }
169 dir_size(path) / (1024 * 1024)
170}
171
172pub fn matching_folders(name: &str, path: &PathBuf) -> Vec<String> {
173 let mut result = vec![];
174 if let Ok(read_dir) = fs::read_dir(&path) {
175 for entry in read_dir.flatten() {
176 if let Ok(metadata) = entry.metadata()
177 && metadata.is_dir()
178 {
179 let filename = entry.file_name().to_string_lossy().to_string();
180 if filename == name {
181 result.push(filename);
182 } else if let Some((_, stripped_name)) = extract_prefix_date(&filename)
183 && name == stripped_name
184 {
185 result.push(filename);
186 }
187 }
188 }
189 }
190 result
191}
192
193pub enum SelectionResult {
195 Folder(String),
197 New(String),
199 None,
201}