Add verify-canfetch flag and git2 integration for repo accessibility checks

```
│  Agent powering down. Goodbye!
│
│  Interaction Summary
│  Session ID:                 9010bd84-1fb3-489d-ae48-89d5e7570b34
│  Tool Calls:                 26 ( ✓ 25 x 1 )
│  Success Rate:               96.2%
│  User Agreement:             96.2% (26 reviewed)
│  Code Changes:               +116 -23
│
│  Performance
│  Wall Time:                  59m 38s
│  Agent Active:               9m 53s
│    » API Time:               4m 22s (44.1%)
│    » Tool Time:              5m 31s (55.9%)
│
│
│  Model Usage                 Reqs   Input Tokens   Cache Reads  Output Tokens
│  ────────────────────────────────────────────────────────────────────────────
│  gemini-2.5-flash-lite          6         10,569             0            596
│  gemini-3-pro-preview          23        114,081       377,312         10,537
│  gemini-3-flash-preview         7         88,472        83,196            436
│
│  Savings Highlight: 460,508 (68.4%) of input tokens were served from the cache, reducing costs.
```
This commit is contained in:
2026-01-14 21:00:50 -05:00
parent 568e5ece49
commit 20ffe86776
4 changed files with 181 additions and 2 deletions

88
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"] }

View File

@@ -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<dyn std::error::Error>> {
.await?;
let existing_set: HashSet<String> = 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<dyn std::error::Error>> {
}
// 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<dyn std::error::Error>> {
};
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<dyn std::error::Error>> {
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<Config, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?;

View File

@@ -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.