Skip to content

Commit 1982bbe

Browse files
authored
Added an example demonstrating 3 simultaneous git clones
Added an example demonstrating 3 simultaneous git clones
2 parents d6a3df7 + 4c111ab commit 1982bbe

File tree

3 files changed

+350
-2
lines changed

3 files changed

+350
-2
lines changed

Cargo.lock

Lines changed: 105 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ tracing = "0.1"
1717
thiserror = "1.0"
1818
nix = { version = "0.29", features = ["signal", "process"] } # For killpg
1919
libc = "0.2" # For setpgid and signal constants
20-
2120
# Dependencies also used by the example(s)
2221
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } # For logging setup
2322
tempfile = "3" # For creating temporary directories in examples/tests
2423
anyhow = "1.0" # For simple error handling in examples
24+
futures = "0.3" # For simulataneous_git_clone example
2525
# Dependency on the library itself (needed for building examples within the workspace)
2626
# This line might not be strictly necessary if cargo automatically detects it,
2727
# but explicitly listing it can sometimes help.
@@ -39,4 +39,11 @@ anyhow = "1.0" # For simple error handling in examples
3939
name = "git_clone_kernel"
4040
path = "examples/git_clone_kernel.rs"
4141
# Example requires tokio runtime features if not enabled globally in [dependencies]
42-
# required-features = ["full"] # Uncomment if tokio features are restricted in [dependencies]
42+
# required-features = ["full"] # Uncomment if tokio features are restricted in [dependencies]
43+
44+
# Declare the example binary target
45+
[[example]]
46+
name = "simultaneous_git_clone"
47+
path = "examples/simultaneous_git_clone.rs"
48+
# Example requires tokio runtime features if not enabled globally in [dependencies]
49+
# required-features = ["full"] # uncomment if tokio features are restricted in [dependencies]

examples/simultaneous_git_clone.rs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
use anyhow::{Context, Result};
2+
use command_timeout::{run_command_with_timeout, CommandError, CommandOutput};
3+
use std::collections::HashMap;
4+
use std::os::unix::process::ExitStatusExt;
5+
use std::path::{Path, PathBuf};
6+
use std::process::Command;
7+
use std::time::Duration;
8+
use tempfile::Builder;
9+
use tracing::{error, info, warn, Level};
10+
use tracing_subscriber::FmtSubscriber;
11+
12+
// Define the Git repository URL
13+
const CCXR: &str = "https://github.com/CCExtractor/ccextractor.git";
14+
const SAMPLE_PLATFORM: &str = "https://github.com/CCExtractor/sample-platform.git";
15+
const FLUTTERGUI: &str = "https://github.com/CCExtractor/ccextractorfluttergui.git";
16+
17+
// Run like this:
18+
// RUST_LOG=debug cargo run --example simultaneous_git_clone
19+
20+
#[tokio::main]
21+
async fn main() -> Result<(), anyhow::Error> {
22+
// Using anyhow for simple example error handling
23+
// Initialize tracing subscriber
24+
let subscriber = FmtSubscriber::builder()
25+
.with_max_level(Level::INFO) // Default to INFO level
26+
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) // Allow RUST_LOG override
27+
.finish();
28+
tracing::subscriber::set_global_default(subscriber)
29+
.expect("Setting default tracing subscriber failed");
30+
31+
info!("Starting simultaneous git clone example...");
32+
33+
// --- Timeout Configuration ---
34+
let repos = vec![
35+
(
36+
CCXR,
37+
Duration::from_secs(10),
38+
Duration::from_secs(300),
39+
Duration::from_secs(60),
40+
),
41+
(
42+
SAMPLE_PLATFORM,
43+
Duration::from_secs(15),
44+
Duration::from_secs(600),
45+
Duration::from_secs(120),
46+
),
47+
(
48+
FLUTTERGUI,
49+
Duration::from_secs(20),
50+
Duration::from_secs(900),
51+
Duration::from_secs(180),
52+
),
53+
];
54+
info!("Configured repositories and timeouts:");
55+
// -----------------------------
56+
57+
//Print repos and their timeouts
58+
for (url, min, max, activity) in &repos {
59+
info!(
60+
"Repo: '{}', Min Timeout: {:?}, Max Timeout: {:?}, Activity Timeout: {:?}",
61+
url, min, max, activity
62+
);
63+
}
64+
65+
// Create a temp directories to clone into
66+
let mut results_summary: HashMap<&str, Result<(), anyhow::Error>> = HashMap::new();
67+
let clone_futures = repos.iter().map(|(url, min, max, activity)| {
68+
let url_clone = *url; // Clone the URL for use in the async block
69+
async move {
70+
let dir = Builder::new()
71+
.prefix(&format!(
72+
"clone_{}",
73+
url.split('/').last().unwrap_or("repo")
74+
))
75+
.tempdir()
76+
.context("Failed to create temporary directory");
77+
78+
let result = match dir {
79+
Ok(dir) => clone_repository(url, dir.path(), *min, *max, *activity).await,
80+
Err(e) => Err(e),
81+
};
82+
(url_clone, result)
83+
}
84+
});
85+
86+
// Run all clone operations concurrently
87+
let results = futures::future::join_all(clone_futures).await;
88+
89+
// Collect results into the summary
90+
for (url, result) in results {
91+
results_summary.insert(url, result);
92+
}
93+
94+
// Log the summary of results
95+
info!("Clone operations summary:");
96+
for (url, result) in &results_summary {
97+
match result {
98+
Ok(_) => info!("SUCCESS: {}", url),
99+
Err(e) => error!("FAILED: {} - {:?}", url, e),
100+
}
101+
}
102+
103+
// Log overall status
104+
let failed_count = results_summary.values().filter(|r| r.is_err()).count();
105+
if failed_count > 0 {
106+
error!("{} repositories failed to clone.", failed_count);
107+
} else {
108+
info!("All repositories cloned successfully!");
109+
}
110+
111+
Ok(())
112+
}
113+
114+
// Prepare the git clone command
115+
async fn clone_repository(
116+
repo_url: &str,
117+
target_path: &Path,
118+
min_timeout: Duration,
119+
max_timeout: Duration,
120+
activity_timeout: Duration,
121+
) -> Result<(), anyhow::Error> {
122+
// Using anyhow for simple example error handling
123+
124+
//log clone initialization with timeouts
125+
info!(
126+
"Preparing to clone '{}' into directory '{}'",
127+
repo_url,
128+
target_path.display()
129+
);
130+
info!(
131+
"Timeouts: min={:?}, max={:?}, activity={:?}",
132+
min_timeout, max_timeout, activity_timeout
133+
);
134+
135+
//build git clone command
136+
let mut cmd = Command::new("git");
137+
cmd.arg("clone")
138+
.arg("--progress") // Explicitly ask for progress output on stderr
139+
.arg(repo_url)
140+
.arg(target_path); // Clone into the target path
141+
142+
// Run the command using the library function
143+
let result = run_command_with_timeout(cmd, min_timeout, max_timeout, activity_timeout).await;
144+
145+
//error handling
146+
match result {
147+
Ok(output) => {
148+
handle_command_output(output, repo_url, &target_path.to_path_buf());
149+
}
150+
Err(e) => {
151+
// Log specific details first
152+
match &e {
153+
// Borrow e here to allow using it later
154+
CommandError::Spawn(io_err) => error!("Failed to spawn git: {}", io_err),
155+
CommandError::Io(io_err) => error!("IO error reading output: {}", io_err),
156+
CommandError::Kill(io_err) => error!("Error sending kill signal: {}", io_err),
157+
CommandError::Wait(io_err) => error!("Error waiting for command exit: {}", io_err),
158+
// Use 'ref msg' to borrow the string instead of moving it
159+
CommandError::InvalidTimeout(ref msg) => error!("Invalid timeout config: {}", msg),
160+
CommandError::StdoutPipe => error!("Failed to get stdout pipe from command"),
161+
CommandError::StderrPipe => error!("Failed to get stderr pipe from command"),
162+
}
163+
error!("Command execution failed."); // General failure message
164+
165+
// Print path even on error
166+
warn!(
167+
"Clone operation failed. Directory may be incomplete or empty: {}",
168+
target_path.display()
169+
);
170+
171+
// Now convert the original error (which is still valid) and return
172+
return Err(e.into());
173+
}
174+
}
175+
176+
Ok(())
177+
}
178+
179+
fn handle_command_output(output: CommandOutput, repo_url: &str, target_path: &PathBuf) {
180+
info!("Finished cloning '{}'.", repo_url);
181+
info!("Total Duration: {:?}", output.duration);
182+
info!("Timed Out: {}", output.timed_out);
183+
184+
if let Some(status) = output.exit_status {
185+
if status.success() {
186+
info!("Exit Status: {} (Success)", status);
187+
} else {
188+
warn!("Exit Status: {} (Failure)", status);
189+
if let Some(code) = status.code() {
190+
warn!("Exit Code: {}", code);
191+
}
192+
// signal() is now available because ExitStatusExt is in scope
193+
if let Some(signal) = status.signal() {
194+
warn!("Terminated by Signal: {}", signal);
195+
}
196+
}
197+
} else {
198+
warn!("Exit Status: None (Killed by timeout, status unavailable?)");
199+
}
200+
201+
info!("Stdout Length: {} bytes", output.stdout.len());
202+
if !output.stdout.is_empty() {
203+
// Print snippet or full output (be cautious with large output)
204+
info!(
205+
"Stdout (first 1KB):\n---\n{}...\n---",
206+
String::from_utf8_lossy(&output.stdout.iter().take(1024).cloned().collect::<Vec<_>>())
207+
);
208+
}
209+
210+
info!("Stderr Length: {} bytes", output.stderr.len());
211+
if !output.stderr.is_empty() {
212+
// Git clone progress usually goes to stderr
213+
warn!(
214+
"Stderr (first 1KB):\n---\n{}...\n---",
215+
String::from_utf8_lossy(&output.stderr.iter().take(1024).cloned().collect::<Vec<_>>())
216+
);
217+
// For full stderr: warn!("Stderr:\n{}", String::from_utf8_lossy(&output.stderr));
218+
}
219+
220+
if output.exit_status.map_or(false, |s| s.success()) && !output.timed_out {
221+
info!("---> Clone completed successfully for '{}'! <---", repo_url);
222+
} else if output.timed_out {
223+
error!("---> Clone FAILED due to timeout for '{}'! <---", repo_url);
224+
} else {
225+
error!(
226+
"---> Clone FAILED with non-zero exit status for '{}'! <---",
227+
repo_url
228+
);
229+
}
230+
231+
//log success with directory location for each clone
232+
info!(
233+
"Clone operation finished. Directory location: {}",
234+
target_path.display()
235+
);
236+
}

0 commit comments

Comments
 (0)