Skip to content

Commit 1acd373

Browse files
committed
feat: git operation progress now displayed to user when using gix
Signed-off-by: Patrick Casey <[email protected]>
1 parent 6077a6e commit 1acd373

File tree

9 files changed

+571
-287
lines changed

9 files changed

+571
-287
lines changed

Cargo.lock

+291-187
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hipcheck/Cargo.toml

+27-4
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@ env_logger = { version = "0.11.6" }
4545
flate2 = "1.1.0"
4646
fs_extra = "1.3.0"
4747
futures = "0.3.31"
48-
gix = { version = "0.70.0", features = [
48+
gix = { version = "0.71.0", default-features = false, features = [
4949
"basic",
50+
"dirwalk",
51+
"worktree-mutation",
5052
"max-control",
51-
"zlib-stock",
5253
# use reqwest/rustls to avoid needing openssl
5354
"blocking-http-transport-reqwest-rust-tls",
55+
"zlib-stock",
5456
] }
5557
# Include with both a `path` and `version` reference.
5658
# Local builds will use the `path` dependency, which may be a newer
@@ -75,6 +77,15 @@ num-traits = "0.2.19"
7577
ordered-float = { version = "5.0.0", features = ["serde"] }
7678
packageurl = "0.4.1"
7779
pathbuf = "1.0.0"
80+
prodash = { version = "29.0.0", default-features = false, features = [
81+
"render-line",
82+
"render-line-autoconfigure",
83+
"render-line-crossterm",
84+
"unit-bytes",
85+
"unit-duration",
86+
"unit-human",
87+
] }
88+
prost = "0.13.5"
7889
rand = "0.9.0"
7990
rayon = "1.10.0"
8091
regex = "1.11.1"
@@ -104,8 +115,8 @@ spdx-rs = "0.5.0"
104115
strum = "0.27.1"
105116
strum_macros = "0.27.1"
106117
tabled = "0.18.0"
107-
tar = "0.4.43"
108-
tempfile = "3.17.0"
118+
tar = "0.4.44"
119+
tempfile = "3.17.1"
109120
tokio = { version = "1.44.1", features = [
110121
"rt",
111122
"rt-multi-thread",
@@ -136,6 +147,18 @@ serde_with = "3.12.0"
136147
hipcheck-workspace-hack = { version = "0.1", path = "../library/hipcheck-workspace-hack" }
137148
gomod-rs = "0.1.1"
138149

150+
# This is needed to force reqwest, which is used by gix, to build reqwest with support
151+
# for rustls-tls-native-roots, which is needed to automatically configure system certificates
152+
[dependencies.reqwest]
153+
version = "0.12"
154+
default-features = false
155+
features = [
156+
"blocking",
157+
"http2",
158+
"rustls-tls-native-roots",
159+
"macos-system-configuration",
160+
]
161+
139162
[build-dependencies]
140163

141164
anyhow = "1.0.97"

hipcheck/src/shell/spinner_phase.rs

+7
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ impl SpinnerPhase {
125125

126126
self.bar.finish_and_clear()
127127
}
128+
129+
/// Hide the progress bar temporarily, execute `f`, then redraw the progress bar
130+
///
131+
/// Useful for external code that writes to the standard output.
132+
pub fn suspend<F: FnOnce() -> R, R>(&self, f: F) -> R {
133+
self.bar.suspend(f)
134+
}
128135
}
129136

130137
/// A spinner phase tracking an [Iterator].

hipcheck/src/source/git.rs

+20-19
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
// SPDX-License-Identifier: Apache-2.0
2-
3-
//! Git related types and implementations for pulling/cloning source repos.
4-
52
use crate::{
63
error::{Error as HcError, Result as HcResult},
74
hc_error,
85
};
9-
use gix::{bstr::ByteSlice, refs::FullName, remote, ObjectId};
10-
use std::path::Path;
6+
use gix::{bstr::ByteSlice, refs::FullName, remote, ObjectId, Repository};
7+
use std::{path::Path, sync::Arc};
118
use url::Url;
129

1310
/// default options to use when fetching a repo with `gix`
@@ -22,9 +19,9 @@ fn fetch_options(url: &Url, dest: &Path) -> gix::clone::PrepareFetch {
2219
.expect("fetch options must be valid to perform a clone")
2320
}
2421

25-
/// fast-forward HEAD of current repo to a new_commit
22+
/// fast-forward HEAD of repo to a new object ID (SHA1)
2623
///
27-
/// returns new ObjectId (hash) of updated HEAD upon success
24+
/// returns new ObjectId (SHA1) of updated HEAD upon success
2825
fn fast_forward_to_hash(
2926
repo: &gix::Repository,
3027
current_head: gix::Head,
@@ -35,7 +32,7 @@ fn fast_forward_to_hash(
3532
.ok_or_else(|| hc_error!("Could not determine hash of current HEAD"))?;
3633

3734
if current_id == new_object_id {
38-
log::debug!("skipping fast-forward, IDs match");
35+
log::debug!("skipping fast-forward, HEAD already correct");
3936
return Ok(current_id.into());
4037
}
4138
let edit = gix::refs::transaction::RefEdit {
@@ -72,15 +69,20 @@ fn fast_forward_to_hash(
7269
}
7370

7471
/// Clone a repo from the given url to a destination path in the filesystem.
75-
pub fn clone(url: &Url, dest: &Path) -> HcResult<()> {
72+
pub fn clone(
73+
url: &Url,
74+
dest: &Path,
75+
progress_root: Arc<prodash::tree::Root>,
76+
) -> HcResult<Repository> {
77+
let mut progress = progress_root.add_child("clone");
7678
log::debug!("attempting to clone {} to {:?}", url.as_str(), dest);
7779
std::fs::create_dir_all(dest)?;
7880
let mut fetch_options = fetch_options(url, dest);
79-
let (mut checkout, _) = fetch_options
80-
.fetch_then_checkout(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)?;
81-
let _ = checkout.main_worktree(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)?;
82-
log::info!("Successfully cloned {} to {:?}", url.as_str(), dest);
83-
Ok(())
81+
let (mut checkout, _) =
82+
fetch_options.fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;
83+
let (repo, _) = checkout.main_worktree(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;
84+
log::debug!("Successfully cloned {} to {:?}", url.as_str(), dest);
85+
Ok(repo)
8486
}
8587

8688
/// For a given repo, checkout a particular ref in a detached HEAD state.
@@ -140,10 +142,9 @@ pub fn checkout(repo_path: &Path, refspec: Option<String>) -> HcResult<gix::Obje
140142
Err(HcError::msg("target is ambiguous"))
141143
}
142144

143-
/// TODO: redo commit history to add support for fetch/clone/checkout separately
144-
/// TODO: add support for visual progress indicators
145145
/// Perform a `git fetch` for all remotes in the repo.
146-
pub fn fetch(repo_path: &Path) -> HcResult<()> {
146+
pub fn fetch(repo_path: &Path, progress_root: Arc<prodash::tree::Root>) -> HcResult<()> {
147+
let mut progress = progress_root.add_child("fetch");
147148
log::debug!("Fetching: {:?}", repo_path);
148149
let repo = gix::open(repo_path)?;
149150
let remote_names = repo.remote_names();
@@ -152,8 +153,8 @@ pub fn fetch(repo_path: &Path) -> HcResult<()> {
152153
let remote = repo.find_remote(remote_name.as_bstr())?;
153154
remote
154155
.connect(gix::remote::Direction::Fetch)?
155-
.prepare_fetch(gix::progress::Discard, Default::default())?
156-
.receive(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)?;
156+
.prepare_fetch(&mut progress, Default::default())?
157+
.receive(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;
157158
}
158159
Ok(())
159160
}

hipcheck/src/source/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22

33
pub mod git;
4+
mod progress;
45

56
use crate::{
67
error::{Context, Error, Result},
@@ -12,6 +13,8 @@ use pathbuf::pathbuf;
1213
use std::path::{Path, PathBuf};
1314
use url::{Host, Url};
1415

16+
pub use progress::GitProgressRenderHandle;
17+
1518
/// Creates a RemoteGitRepo struct from a given git URL by idenfitying if it is from a known host (currently only GitHub) or not
1619
pub fn get_remote_repo_from_url(url: Url) -> Result<RemoteGitRepo> {
1720
match url.host() {

hipcheck/src/source/progress.rs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use std::{sync::Arc, time::Duration};
2+
3+
use prodash::{
4+
render::line::{JoinHandle, StreamKind},
5+
tree::Root,
6+
};
7+
8+
use crate::shell::{verbosity::Verbosity, Shell};
9+
10+
/// holds handle to thread rendering gix progress, if `Verbosity` allows for displaying progress
11+
pub struct GitProgressRenderHandle {
12+
_join_handle: Option<JoinHandle>,
13+
}
14+
15+
impl GitProgressRenderHandle {
16+
/// Create a handle to the thread responsible for rendering git operation progress, if the
17+
/// Shell Verbosity allows for output, otherwise, do nothing
18+
pub fn new(root: Arc<Root>) -> Self {
19+
let join_handle = match Shell::get_verbosity() {
20+
Verbosity::Normal => {
21+
let render_line = prodash::render::line(
22+
std::io::stderr(),
23+
Arc::downgrade(&root),
24+
prodash::render::line::Options {
25+
frames_per_second: 30.0,
26+
keep_running_if_progress_is_empty: false,
27+
// prevent spamming of short-lived tasks
28+
initial_delay: Some(Duration::from_millis(500)),
29+
// prevent too many layers of output by stopping at 3
30+
level_filter: Some(0..=3),
31+
..Default::default()
32+
}
33+
.auto_configure(StreamKind::Stderr),
34+
);
35+
Some(render_line)
36+
}
37+
Verbosity::Quiet | Verbosity::Silent => None,
38+
};
39+
40+
Self {
41+
_join_handle: join_handle,
42+
}
43+
}
44+
}

hipcheck/src/target/resolve.rs

+30-13
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
shell::spinner_phase::SpinnerPhase,
1212
source::{
1313
build_unknown_remote_clone_dir, clone_local_repo_to_cache, get_remote_repo_from_url, git,
14-
try_resolve_remote_for_local,
14+
try_resolve_remote_for_local, GitProgressRenderHandle,
1515
},
1616
target::{multi::resolve_go_mod, types::*},
1717
};
@@ -72,6 +72,17 @@ impl TargetResolver {
7272
}
7373
}
7474

75+
/// Hide the progress bar temporarily, execute `f`, then redraw the progress bar
76+
///
77+
/// Useful for external code that writes to the standard output.
78+
pub fn suspend_status<F: FnOnce() -> R, R>(&self, f: F) -> R {
79+
if let Some(phase) = &self.config.phase {
80+
phase.suspend(f)
81+
} else {
82+
f()
83+
}
84+
}
85+
7586
/// Accessor method to ensure immutability of `config` field
7687
pub fn get_config(&self) -> &TargetResolverConfig {
7788
&self.config
@@ -89,7 +100,6 @@ impl TargetResolver {
89100
// if ref provided on CLI, use that
90101
Some(refspec.clone())
91102
} else if let Some(pkg) = &self.package {
92-
// Open the repo with git2.
93103
let repo: gix::Repository = gix::open(repo_path)?;
94104

95105
let cmt = {
@@ -248,16 +258,24 @@ impl ResolveRepo for RemoteGitRepo {
248258
}
249259
};
250260

251-
// Clone remote repo if not exists
252-
if path.exists().not() {
253-
t.update_status("cloning");
254-
git::clone(&self.url, &path)
255-
.map_err(|e| hc_error!("failed to clone remote repository {}", e))?;
256-
} else {
257-
t.update_status("pulling");
258-
}
259-
// Whether we cloned or not, we need to fetch so we get tags
260-
git::fetch(&path).context("failed to fetch updates from remote repository")?;
261+
// NOTE: suspend_status is needed, otherwise the Shell ProgressBars and progress bars
262+
// tracking git operation progress fight over the terminal
263+
t.suspend_status(|| -> std::result::Result<(), crate::Error> {
264+
// when this goes out of scope, the render thread will die
265+
let progress_root = prodash::tree::Root::new();
266+
// do not drop this until the end of the scope, otherwise the render thread will die
267+
let _handle = GitProgressRenderHandle::new(progress_root.clone());
268+
269+
// Clone remote repo if not exists
270+
if path.exists().not() {
271+
git::clone(&self.url, &path, progress_root.clone())
272+
.context("failed to clone remote repository")?;
273+
}
274+
// Whether we cloned or not, we need to fetch so we get tags
275+
git::fetch(&path, progress_root.clone())
276+
.context("failed to fetch updates from remote repository")?;
277+
Ok(())
278+
})?;
261279

262280
let refspec = t.get_checkout_target(&path)?;
263281
let git_ref = git::checkout(&path, refspec)?.to_string();
@@ -335,7 +353,6 @@ fn fuzzy_match_package_version<'a>(
335353

336354
log::debug!("Fuzzy matching package version '{version}'");
337355

338-
// TODO: remove git2 usage
339356
let potential_tags = [
340357
version.clone(),
341358
format!("v{version}"),

0 commit comments

Comments
 (0)