diff --git a/Cargo.lock b/Cargo.lock index d7b2847..a8b2c18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,11 +360,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe 0.1.6", + "openssl-sys", + "url", +] + [[package]] name = "gitea_mirror" version = "0.1.0" dependencies = [ "clap", + "git2", "reqwest", "serde", "serde_json", @@ -702,6 +718,46 @@ version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "litemap" version = "0.8.1" @@ -773,12 +829,30 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-probe" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -820,6 +894,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1037,7 +1117,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.0", "rustls-pki-types", "schannel", "security-framework", @@ -1646,6 +1726,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index c16a98b..f7e88bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ serde_json = "1.0" toml = "0.9" tracing = "0.1" tracing-subscriber = "0.3" +git2 = { version = "0.19", features = ["vendored-libgit2"] } diff --git a/src/main.rs b/src/main.rs index 7f49f53..3996e17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::fs; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use tracing::{Level, error, info, instrument, warn}; +use git2::{Cred, Direction, Remote, RemoteCallbacks}; #[derive(Parser, Debug)] #[command(name = "gitea-mirror")] @@ -30,6 +31,10 @@ struct Args { /// Do not delete repositories from Gitea. #[clap(long, default_value_t = false)] no_delete: bool, + + /// Verify if repositories are fetchable (checks connectivity and presence of commits). + #[clap(long, default_value_t = false)] + verify_canfetch: bool, } #[derive(Deserialize, Debug, Clone)] @@ -154,6 +159,26 @@ async fn main() -> Result<(), Box> { .await?; let existing_set: HashSet = existing_repos.into_iter().collect(); + // --- Verify Existing Repos if requested --- + if args.verify_canfetch { + info!("Verifying accessibility of existing repositories..."); + let mut verification_failed = false; + for name in &existing_set { + let repo_url = format!("{}/{}/{}.git", config.gitea_url, owner_name, name); + // Use the API key for auth if needed + match verify_repo_accessible(&repo_url, &owner_name, Some(&final_api_key)) { + Ok(_) => info!("Verified [OK]: {}", name), + Err(e) => { + error!("Verified [FAIL]: {} - {}", name, e); + verification_failed = true; + } + } + } + if verification_failed { + return Err("Verification of existing repositories failed. Please investigate the errors above.".into()); + } + } + // 4. Calculate Diff let mut to_add: Vec<(String, String)> = desired_repos .iter() @@ -237,6 +262,7 @@ async fn main() -> Result<(), Box> { } // 7. Execute + let mut migration_verification_failed = false; // Additions for (name, url) in to_add { info!("Migrating {}...", name); @@ -250,7 +276,20 @@ async fn main() -> Result<(), Box> { }; match create_migration(&http_client, &config.gitea_url, &final_api_key, &payload).await { - Ok(_) => info!("Successfully migrated {}", name), + Ok(_) => { + info!("Successfully migrated {}", name); + // Verify after migration if requested + if args.verify_canfetch { + let repo_url = format!("{}/{}/{}.git", config.gitea_url, owner_name, name); + match verify_repo_accessible(&repo_url, &owner_name, Some(&final_api_key)) { + Ok(_) => info!("Verified [OK] (Post-Migration): {}", name), + Err(e) => { + error!("Verified [FAIL] (Post-Migration): {} - {}", name, e); + migration_verification_failed = true; + } + } + } + }, Err(e) => error!("Failed to migrate {}: {}", name, e), } } @@ -276,12 +315,44 @@ async fn main() -> Result<(), Box> { info!("Skipping deletions due to --no-delete flag."); } + if migration_verification_failed { + return Err("Verification of migrated repositories failed. Please investigate the errors above.".into()); + } + info!("Process completed."); Ok(()) } // --- Helpers --- +fn verify_repo_accessible(url: &str, username: &str, api_key: Option<&str>) -> Result<(), String> { + let mut callbacks = RemoteCallbacks::new(); + if let Some(key) = api_key { + let key = key.to_string(); + let user = username.to_string(); + callbacks.credentials(move |_url, _username_from_url, _allowed_types| { + Cred::userpass_plaintext(&user, &key) + }); + } + + // Create a detached remote (no local repo needed) + let mut remote = Remote::create_detached(url).map_err(|e| format!("Invalid Remote URL: {}", e))?; + + // Attempt to connect and fetch list of refs + // connect_auth handles authentication if needed + remote.connect_auth(Direction::Fetch, Some(callbacks), None) + .map_err(|e| format!("Connection/Auth failed: {}", e))?; + + // List refs to ensure it's a valid git repo and accessible + let list = remote.list().map_err(|e| format!("Failed to list refs: {}", e))?; + + if list.is_empty() { + return Err("Repository is empty (no refs found)".to_string()); + } + + Ok(()) +} + #[instrument(skip(path))] fn load_config(path: &Path) -> Result> { let content = fs::read_to_string(path)?; diff --git a/vibe_coding_log/session_2026_01_14_verify_canfetch.md b/vibe_coding_log/session_2026_01_14_verify_canfetch.md new file mode 100644 index 0000000..3a2161a --- /dev/null +++ b/vibe_coding_log/session_2026_01_14_verify_canfetch.md @@ -0,0 +1,21 @@ +# Session 2026-01-14: Verify CanFetch Flag + +**Date**: 2026-01-14 +**Model**: Gemini Pro (CLI Agent) +**Goal**: Verify migration error detection and add a `--verify_canfetch` flag to check repository accessibility (fetching) without using the `git` executable. + +## Plan +1. Add `git2` dependency to `Cargo.toml`. +2. Add `--verify_canfetch` flag to `Args`. +3. Implement a helper function `verify_repo_accessible(url: &str)` using `git2`. +4. Integrate this check into the "Existing Repos" discovery phase and the "Post-Migration" phase. +5. Refine error handling to exit on verification failures. + +## Outcome +- Added `git2` v0.19 to `Cargo.toml`. +- Implemented `verify_repo_accessible` using `git2::Remote::create_detached` and `connect_auth`. +- Added `--verify_canfetch` CLI flag. +- The tool now optionally verifies that existing repositories and newly migrated ones are reachable and non-empty (contain refs). +- Implemented strict error handling for verifications: + - If "Existing Repos" verification fails for *any* repo, the tool exits with an error *before* calculating/printing the execution plan. + - If "Post-Migration" verification fails for any repo, the tool continues to attempt other migrations but exits with an error at the very end to indicate failure.