1use std::path::Path;
2use std::path::PathBuf;
3use std::process::Command;
4
5use bstr::ByteSlice;
6
7use crate::error::CargoResult;
8use crate::ops::cmd::call_on_path;
9
10pub fn fetch(dir: &Path, remote: &str, branch: &str) -> CargoResult<()> {
11 Command::new("git")
12 .arg("fetch")
13 .arg(remote)
14 .arg(branch)
15 .current_dir(dir)
16 .output()
17 .map(|_| ())
18 .map_err(|_| anyhow::format_err!("`git` not found"))
19}
20
21pub fn is_behind_remote(dir: &Path, remote: &str, branch: &str) -> CargoResult<bool> {
22 let repo = git2::Repository::discover(dir)?;
23
24 let branch_id = repo.revparse_single(branch)?.id();
25
26 let remote_branch = format!("{remote}/{branch}");
27 let behind = match repo.revparse_single(&remote_branch) {
28 Ok(o) => {
29 let remote_branch_id = o.id();
30
31 let base_id = repo.merge_base(remote_branch_id, branch_id)?;
32
33 log::trace!("{remote_branch}: {remote_branch_id}");
34 log::trace!("merge base: {base_id}");
35
36 base_id != remote_branch_id
37 }
38 Err(err) => {
39 let _ = crate::ops::shell::warn(format!("push target `{remote_branch}` doesn't exist"));
40 log::trace!("error {err}");
41 false
42 }
43 };
44
45 Ok(behind)
46}
47
48pub fn is_local_unchanged(dir: &Path, remote: &str, branch: &str) -> CargoResult<bool> {
49 let repo = git2::Repository::discover(dir)?;
50
51 let branch_id = repo.revparse_single(branch)?.id();
52
53 let remote_branch = format!("{remote}/{branch}");
54 let unchanged = match repo.revparse_single(&remote_branch) {
55 Ok(o) => {
56 let remote_branch_id = o.id();
57
58 let base_id = repo.merge_base(remote_branch_id, branch_id)?;
59
60 log::trace!("{remote_branch}: {remote_branch_id}");
61 log::trace!("merge base: {base_id}");
62
63 base_id == branch_id
64 }
65 Err(err) => {
66 let _ = crate::ops::shell::warn(format!("push target `{remote_branch}` doesn't exist"));
67 log::trace!("error {err}");
68 false
69 }
70 };
71
72 Ok(unchanged)
73}
74
75pub fn current_branch(dir: &Path) -> CargoResult<String> {
76 let repo = git2::Repository::discover(dir)?;
77
78 let resolved = repo.head()?.resolve()?;
79 let name = resolved.shorthand().unwrap_or("HEAD");
80 Ok(name.to_owned())
81}
82
83pub fn is_dirty(dir: &Path) -> CargoResult<Option<Vec<String>>> {
84 let repo = git2::Repository::discover(dir)?;
85
86 let mut entries = Vec::new();
87
88 let state = repo.state();
89 let dirty_state = state != git2::RepositoryState::Clean;
90 if dirty_state {
91 entries.push(format!("Dirty because of state {state:?}"));
92 }
93
94 let mut options = git2::StatusOptions::new();
95 options
96 .show(git2::StatusShow::IndexAndWorkdir)
97 .include_untracked(true);
98 let statuses = repo.statuses(Some(&mut options))?;
99 let dirty_tree = !statuses.is_empty();
100 if dirty_tree {
101 for status in statuses.iter() {
102 let path = bytes2path(status.path_bytes());
103 entries.push(format!("{} ({:?})", path.display(), status.status()));
104 }
105 }
106
107 if entries.is_empty() {
108 Ok(None)
109 } else {
110 Ok(Some(entries))
111 }
112}
113
114pub fn changed_files(dir: &Path, tag: &str) -> CargoResult<Option<Vec<PathBuf>>> {
115 let root = top_level(dir)?;
116
117 let output = Command::new("git")
118 .arg("diff")
119 .arg(format!("{tag}..HEAD"))
120 .arg("--name-only")
121 .arg("--exit-code")
122 .arg("--")
123 .arg(".")
124 .current_dir(dir)
125 .output()?;
126 match output.status.code() {
127 Some(0) => Ok(Some(Vec::new())),
128 Some(1) => {
129 let paths = output
130 .stdout
131 .lines()
132 .map(|l| root.join(l.to_path_lossy()))
133 .collect();
134 Ok(Some(paths))
135 }
136 _ => Ok(None), }
138}
139
140pub fn commit_all(dir: &Path, msg: &str, sign: bool, dry_run: bool) -> CargoResult<bool> {
141 let repo = git2::Repository::discover(dir)?;
142 let mut options = git2::StatusOptions::new();
143 options
144 .show(git2::StatusShow::IndexAndWorkdir)
145 .include_untracked(true);
146 let statuses = repo.statuses(Some(&mut options))?;
147 let dirty_tree = !statuses.is_empty();
148
149 if dirty_tree || dry_run {
150 call_on_path(
151 vec!["git", "commit", if sign { "-S" } else { "" }, "-am", msg],
152 dir,
153 dry_run,
154 )
155 } else {
156 log::debug!("No files changed, skipping commit");
157 Ok(true)
158 }
159}
160
161pub fn tag(dir: &Path, name: &str, msg: &str, sign: bool, dry_run: bool) -> CargoResult<bool> {
162 let mut cmd = vec!["git", "tag", name];
163 if !msg.is_empty() {
164 cmd.extend(["-a", "-m", msg]);
165 if sign {
166 cmd.push("-s");
167 }
168 }
169 call_on_path(cmd, dir, dry_run)
170}
171
172pub fn tag_exists(dir: &Path, name: &str) -> CargoResult<bool> {
173 let repo = git2::Repository::discover(dir)?;
174
175 let names = repo.tag_names(Some(name))?;
176 Ok(!names.is_empty())
177}
178
179pub fn find_last_tag(dir: &Path, glob: &globset::GlobMatcher) -> Option<String> {
180 let repo = git2::Repository::discover(dir).ok()?;
181 let mut tags: std::collections::HashMap<git2::Oid, String> = Default::default();
182 repo.tag_foreach(|id, name| {
183 let name = String::from_utf8_lossy(name);
184 let name = name.strip_prefix("refs/tags/").unwrap_or(&name);
185 if glob.is_match(name) {
186 let name = name.to_owned();
187 let tag = repo.find_tag(id);
188 let target = tag.and_then(|t| t.target());
189 let commit = target.and_then(|t| t.peel_to_commit());
190 if let Ok(commit) = commit {
191 tags.insert(commit.id(), name);
192 }
193 }
194 true
195 })
196 .ok()?;
197
198 let mut revwalk = repo.revwalk().ok()?;
199 revwalk.simplify_first_parent().ok()?;
200 revwalk.set_sorting(git2::Sort::NONE).ok()?;
202 revwalk.push_head().ok()?;
203 let name = revwalk.find_map(|id| {
204 let id = id.ok()?;
205 tags.remove(&id)
206 })?;
207 Some(name)
208}
209
210pub fn push<'s>(
211 dir: &Path,
212 remote: &str,
213 refs: impl IntoIterator<Item = &'s str>,
214 options: impl IntoIterator<Item = &'s str>,
215 dry_run: bool,
216) -> CargoResult<bool> {
217 let mut command = vec!["git", "push", "--atomic"];
220
221 for option in options {
222 command.push("--push-option");
223 command.push(option);
224 }
225
226 command.push(remote);
227
228 let mut is_empty = true;
229 for ref_ in refs {
230 command.push(ref_);
231 is_empty = false;
232 }
233 if is_empty {
234 return Ok(true);
235 }
236
237 call_on_path(command, dir, dry_run)
238}
239
240pub fn top_level(dir: &Path) -> CargoResult<PathBuf> {
241 let repo = git2::Repository::discover(dir)?;
242
243 repo.workdir()
244 .map(|p| p.to_owned())
245 .ok_or_else(|| anyhow::format_err!("bare repos are unsupported"))
246}
247
248pub fn git_version() -> CargoResult<()> {
249 Command::new("git")
250 .arg("--version")
251 .output()
252 .map(|_| ())
253 .map_err(|_| anyhow::format_err!("`git` not found"))
254}
255
256#[cfg(unix)]
258pub fn bytes2path(b: &[u8]) -> &Path {
259 use std::os::unix::prelude::OsStrExt;
260 Path::new(std::ffi::OsStr::from_bytes(b))
261}
262
263#[cfg(windows)]
265pub fn bytes2path(b: &[u8]) -> &std::path::Path {
266 use std::str;
267 std::path::Path::new(str::from_utf8(b).unwrap())
268}