artifactize_core/
repository.rs1use serde::{Deserialize, Serialize};
4use std::{
5 ffi::OsStr,
6 path::{Path, PathBuf},
7};
8
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct Identity {
12 #[serde(
13 default,
14 skip_serializing_if = "Option::is_none",
15 with = "crate::platform::path_serde::option"
16 )]
17 pub common_dir: Option<PathBuf>,
18 #[serde(
19 default,
20 skip_serializing_if = "Option::is_none",
21 with = "crate::platform::path_serde::option"
22 )]
23 pub worktree_path: Option<PathBuf>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub branch: Option<String>,
26}
27
28fn git(path: &Path, args: &[&str]) -> Option<Vec<u8>> {
29 let args = [OsStr::new("-C"), path.as_os_str()]
30 .into_iter()
31 .chain(args.iter().map(OsStr::new));
32 crate::process::tool_output("git", args)
33}
34
35fn git_path(path: &Path, arg: &str) -> Option<PathBuf> {
36 let output = git(path, &["rev-parse", "--path-format=absolute", arg])?;
37 let text = String::from_utf8(output).ok()?;
38 let path = PathBuf::from(
40 text.strip_suffix("\r\n")
41 .or_else(|| text.strip_suffix('\n'))
42 .unwrap_or(&text),
43 );
44 crate::platform::canonicalize(&path).ok()
45}
46
47pub fn identify(path: &Path) -> Identity {
49 let common_dir = git_path(path, "--git-common-dir");
50 let worktree_path = git_path(path, "--show-toplevel");
51 if common_dir.is_none() || worktree_path.is_none() {
52 return Identity::default();
53 }
54 let branch = git(path, &["symbolic-ref", "--quiet", "--short", "HEAD"])
55 .and_then(|bytes| String::from_utf8(bytes).ok())
56 .map(|text| text.trim_end_matches(['\r', '\n']).to_owned());
57 Identity {
58 common_dir,
59 worktree_path,
60 branch,
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Worktree {
66 pub path: PathBuf,
67 pub branch: Option<String>,
68}
69
70pub fn worktrees(common_dir: &Path) -> Vec<Worktree> {
72 let args = [OsStr::new("--git-dir"), common_dir.as_os_str()]
73 .into_iter()
74 .chain(["worktree", "list", "--porcelain", "-z"].map(OsStr::new));
75 crate::process::tool_output("git", args)
76 .map(|output| parse_worktrees(&output))
77 .unwrap_or_default()
78}
79
80fn parse_worktrees(bytes: &[u8]) -> Vec<Worktree> {
81 let mut trees: Vec<Worktree> = Vec::new();
82 for field in bytes.split(|byte| *byte == 0) {
83 if let Some(path) = field.strip_prefix(b"worktree ") {
84 let path = crate::platform::path_from_bytes(path);
85 trees.push(Worktree { path, branch: None });
86 } else if let Some(branch) = field.strip_prefix(b"branch refs/heads/")
87 && let Some(tree) = trees.last_mut()
88 {
89 tree.branch = Some(String::from_utf8_lossy(branch).into_owned());
90 }
91 }
92 trees
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 #[test]
99 fn porcelain_paths_are_not_split_on_whitespace() {
100 let trees = parse_worktrees(
101 b"worktree /a space\nline\0HEAD abc\0branch refs/heads/main\0\0worktree /detached\0HEAD def\0detached\0\0",
102 );
103 assert_eq!(trees[0].path, PathBuf::from("/a space\nline"));
104 assert_eq!(trees[0].branch.as_deref(), Some("main"));
105 assert_eq!(trees[1].branch, None);
106 }
107}