Skip to main content

try_rs/
utils.rs

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
11/// Checks if current directory is inside a git repository
12pub 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
37/// Checks if a path is a git worktree (not the main working tree)
38/// A worktree has a .git file (not directory) that points to the main repo
39pub fn is_git_worktree(path: &Path) -> bool {
40    let dot_git = path.join(".git");
41    // If .git is a file (not a directory), it's a worktree
42    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                // Use symlink_metadata to avoid following symlinks
156                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                // Symlinks and other special files are intentionally skipped
165            }
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
193// i've put this here since until now there is not really a library part
194pub enum SelectionResult {
195    /// A explicit folder that is guaranteed to exist already
196    Folder(String),
197    /// No existing match, a new folder should be created
198    New(String),
199    /// Nothing was selected in the UI, quit
200    None,
201}