1use std::{collections::HashSet, process::Command};
2
3use anyhow::{Result, anyhow, bail};
4use git2::{BranchType, ErrorCode, Repository};
5
6use crate::app::CommandExt as _;
7
8pub fn current_branch() -> Result<String> {
9 let output = run_and_check_output(&["rev-parse", "--abbrev-ref", "HEAD"])?;
10 Ok(output.trim_end().to_string())
11}
12
13pub fn checkout_or_create_branch(branch_name: &str) -> Result<()> {
14 if branch_exists(branch_name)? {
15 checkout_branch(branch_name)?;
16 } else {
17 create_branch(branch_name)?;
18 }
19 Ok(())
20}
21
22pub fn merge_branch(branch_name: &str) -> Result<()> {
23 let _output = run_and_check_output(&["merge", "--ff", branch_name])?;
24 Ok(())
25}
26
27pub fn tag_version(version: &str) -> Result<()> {
28 let _output = run_and_check_output(&["tag", "--annotate", version, "--message", version])?;
29 Ok(())
30}
31
32pub fn push_branch(branch_name: &str) -> Result<()> {
33 let _output = run_and_check_output(&["push", "origin", branch_name])?;
34 Ok(())
35}
36
37pub fn changed_files() -> Result<Vec<String>> {
38 let mut files = HashSet::new();
39
40 let output = run_and_check_output(&["diff", "--name-status", "origin/master..."])?;
44 for line in output.lines() {
45 if !is_warning_line(line)
46 && let Some((_, path)) = line.split_once('\t')
47 {
48 files.insert(path.to_string());
49 }
50 }
51
52 let output = run_and_check_output(&["diff", "--name-only", "HEAD"])?;
54 for line in output.lines() {
55 if !is_warning_line(line) {
56 files.insert(line.to_string());
57 }
58 }
59
60 let output = run_and_check_output(&["ls-files", "--others", "--exclude-standard"])?;
62 for line in output.lines() {
63 files.insert(line.to_string());
64 }
65
66 let mut sorted = Vec::from_iter(files);
67 sorted.sort();
68
69 Ok(sorted)
70}
71
72pub fn list_files() -> Result<Vec<String>> {
73 Ok(run_and_check_output(&["ls-files"])?
74 .lines()
75 .map(str::to_owned)
76 .collect())
77}
78
79pub fn get_git_sha() -> Result<String> {
80 run_and_check_output(&["rev-parse", "--short", "HEAD"])
81 .map(|output| output.trim_end().to_string())
82}
83
84pub fn get_modified_files() -> Result<Vec<String>> {
86 let args = vec![
87 "ls-files",
88 "--full-name",
89 "--modified",
90 "--others",
91 "--exclude-standard",
92 ];
93 Ok(run_and_check_output(&args)?
94 .lines()
95 .map(str::to_owned)
96 .collect())
97}
98
99pub fn set_config_value(key: &str, value: &str) -> Result<String> {
100 Command::new("git")
101 .args(["config", key, value])
102 .stdout(std::process::Stdio::null())
103 .check_output()
104}
105
106pub fn check_git_repository_clean() -> Result<bool> {
108 Ok(Command::new("git")
109 .args(["diff-index", "--quiet", "HEAD"])
110 .stdout(std::process::Stdio::null())
111 .status()
112 .map(|status| status.success())?)
113}
114
115pub fn add_files_in_current_dir() -> Result<String> {
116 Command::new("git").args(["add", "."]).check_output()
117}
118
119pub fn commit(commit_message: &str) -> Result<String> {
121 Command::new("git")
122 .args(["commit", "--all", "--message", commit_message])
123 .check_output()
124}
125
126pub fn push() -> Result<String> {
128 Command::new("git").args(["push"]).check_output()
129}
130
131pub fn push_and_set_upstream(branch_name: &str) -> Result<String> {
132 Command::new("git")
133 .args(["push", "-u", "origin", branch_name])
134 .check_output()
135}
136
137pub fn clone(repo_url: &str) -> Result<String> {
138 Command::new("git").args(["clone", repo_url]).check_output()
140}
141
142fn find_repo() -> Result<Repository, git2::Error> {
145 Repository::discover(".")
146}
147
148pub fn branch_exists(branch: &str) -> Result<bool> {
149 let repo = find_repo()?;
150
151 let exists = {
154 match repo.find_branch(branch, BranchType::Local) {
155 Ok(_) => true,
156 Err(e) if e.code() == ErrorCode::NotFound => false,
157 Err(e) => bail!(e),
158 }
159 };
160
161 Ok(exists)
162}
163pub fn checkout_branch(branch_name: &str) -> Result<()> {
164 let _output = run_and_check_output(&["checkout", branch_name])?;
165 Ok(())
166}
167
168pub fn checkout_main_branch() -> Result<()> {
169 let _output = run_and_check_output(&["switch", "master"])?;
170 Ok(())
171}
172
173pub fn create_branch(branch_name: &str) -> Result<()> {
174 let repo = find_repo()?;
175
176 let head_ref = repo.head()?;
177 let target_oid = head_ref
178 .target()
179 .ok_or_else(|| anyhow!("HEAD is not pointing at a valid commit"))?;
180 let target_commit = repo.find_commit(target_oid)?;
181
182 let branch = repo.branch(branch_name, &target_commit, false)?;
183 let reference = branch.into_reference();
184 let full_ref_name = reference
185 .name()
186 .ok_or_else(|| git2::Error::from_str("branch reference has no name"))?;
187 repo.set_head(full_ref_name)?;
188 repo.checkout_head(None)?;
189
190 Ok(())
191}
192
193pub fn run_and_check_output(args: &[&str]) -> Result<String> {
194 Command::new("git").in_repo().args(args).check_output()
195}
196
197fn is_warning_line(line: &str) -> bool {
198 line.starts_with("warning: ") || line.contains("original line endings")
199}
200
201pub fn git_ls_files(pattern: Option<&str>) -> Result<Vec<String>> {
203 let args = match pattern {
204 Some(p) => vec!["ls-files", p],
205 None => vec!["ls-files"],
206 };
207
208 let output = run_and_check_output(&args)?;
209 Ok(output.lines().map(str::to_owned).collect())
210}