Skip to main content

garden/
git.rs

1use crate::{cmd, constants, errors, model, path};
2
3/// Return Ok(garden::model::GitTreeDetails) for the specified path on success
4/// or Err(garden::errors::CommandError) when Git commands error out.
5pub fn worktree_details(
6    pathbuf: &std::path::Path,
7) -> Result<model::GitTreeDetails, errors::CommandError> {
8    let mut worktree_count = 0;
9    let cmd = ["git", "worktree", "list", "--porcelain"];
10    let path = path::abspath(pathbuf);
11    let exec = cmd::exec_in_dir(&cmd, &path);
12    let output = cmd::stdout_to_string(exec)?;
13    let worktree_token = "worktree ";
14    let branch_token = "branch refs/heads/";
15    let bare_token = "bare";
16    let mut parent_path = std::path::PathBuf::new();
17    let mut branch = String::new();
18    let mut is_current = false;
19    let mut is_bare = false;
20
21    for line in output.lines() {
22        if let Some(worktree) = line.strip_prefix(worktree_token) {
23            let worktree_path = std::path::PathBuf::from(worktree);
24            let current_path = path::abspath(&worktree_path);
25            is_current = current_path == path;
26            // The first worktree is the "parent" worktree.
27            if worktree_count == 0 {
28                parent_path = current_path;
29            }
30            worktree_count += 1;
31        } else if is_current && line.starts_with(branch_token) {
32            branch = line[branch_token.len()..].to_string();
33        } else if is_current && line == bare_token {
34            // Is this a bare repository?
35            is_bare = true;
36        }
37    }
38
39    // 0 or 1 worktrees implies that this is a regular worktree.
40    // 0 doesn't happen in practice.
41    if worktree_count < 2 {
42        return Ok(model::GitTreeDetails {
43            branch,
44            tree_type: match is_bare {
45                true => model::GitTreeType::Bare,
46                false => model::GitTreeType::Tree,
47            },
48        });
49    }
50    if path == parent_path {
51        return Ok(model::GitTreeDetails {
52            branch,
53            tree_type: model::GitTreeType::Parent,
54        });
55    }
56
57    Ok(model::GitTreeDetails {
58        branch,
59        tree_type: model::GitTreeType::Worktree(parent_path),
60    })
61}
62
63/// Return the current branch names for the specified repository path.
64pub fn branches(path: &std::path::Path) -> Vec<String> {
65    let mut branches: Vec<String> = Vec::new();
66    let cmd = [
67        "git",
68        "for-each-ref",
69        "--format=%(refname:short)",
70        "refs/heads",
71    ];
72    let exec = cmd::exec_in_dir(&cmd, &path);
73    if let Ok(output) = cmd::stdout_to_string(exec) {
74        branches.append(
75            &mut output
76                .lines()
77                .filter(|x| !x.is_empty())
78                .map(|x| x.to_string())
79                .collect::<Vec<String>>(),
80        );
81    }
82
83    branches
84}
85
86/// Return the current branch name for the specified repository path.
87pub(crate) fn branch(path: &std::path::Path) -> Option<String> {
88    let cmd = ["git", "symbolic-ref", "--quiet", "--short", "HEAD"];
89    let exec = cmd::exec_in_dir(&cmd, &path);
90    if let Ok(output) = cmd::stdout_to_string(exec) {
91        if !output.is_empty() {
92            return Some(output);
93        }
94    }
95    // Detached head? Show an abbreviated commit ID. This respects `git config core.abbrev`.
96    let cmd = ["git", "rev-parse", "--short", "HEAD"];
97    let exec = cmd::exec_in_dir(&cmd, &path);
98    if let Ok(output) = cmd::stdout_to_string(exec) {
99        if !output.is_empty() {
100            return Some(output);
101        }
102    }
103    // Unknown branch is an empty string.
104    None
105}
106
107/// Return the root of the current repository when inside a Git repository.
108pub(crate) fn current_worktree_path(
109    path: &std::path::Path,
110) -> Result<String, errors::CommandError> {
111    let cmd = ["git", "rev-parse", "--show-toplevel"];
112    let exec = cmd::exec_in_dir(&cmd, &path);
113
114    cmd::stdout_to_string(exec)
115}
116
117/// Return a sensible default name for a tree. Parse the URL or use the path basename.
118pub(crate) fn name_from_url_or_path(url: &str, path: &std::path::Path) -> String {
119    if !url.is_empty() {
120        if let Some(name) = url
121            .rsplit('/')
122            .next()
123            .map(|name| name.trim_end_matches(".git"))
124        {
125            return name.to_string();
126        }
127    }
128    path.file_name()
129        .map(|basename| basename.to_string_lossy().to_string())
130        .unwrap_or(string!(constants::DOT))
131}