1 Commits
v0.3 ... main

Author SHA1 Message Date
ae347d7506 Add repo_owner for where the migrated repos should go to
All checks were successful
Cargo Build & Test / Rust project - latest (1.90) (push) Successful in 2m11s
Generated with Claude 4.5 Sonnet (Gemini 2.5 Pro was stubborn about
wanting to refactor the world even when it was unrelated). Prompt was;
```
Given the attached rust code, and example toml configuration file;
... SNIP ...

Can you add the ability to specify the owner of the migrated repo, such
that if not provided then to use the user who owns the API key, or if
provided then to use said owner? I think gitea refers to that as the
"repo_owner" in it's swagger based API docs? Note, this would be one
configuration for all the repos in the config toml file, not on a
per-repo basis.
```

which didn't work since it tried to fetch a uid which doesn't exist for
organizations, so lets prompt it to fix that and give it a helping hand;
```
Ah shoot, if I am using a migration to a new organization which the
user who owns the API key has permissions to modify, then I am getting
a 401 return code. Did you assume the target will always be a user
rather than also being an organization? Also, keep in mind, I think
giteas migration API wants a user string, rather than a user ID, if
that's the case then I think we can remove the entire
`get_user_id_by_username()` function?
```
2025-10-06 19:17:28 -04:00
2 changed files with 27 additions and 17 deletions

View File

@@ -4,6 +4,10 @@ gitea_url = "https://gitmirror.hak8or.com"
# Your Gitea API key (generate one from User Settings -> Applications) # Your Gitea API key (generate one from User Settings -> Applications)
api_key = "API_KEY_GOES_HERE" api_key = "API_KEY_GOES_HERE"
# Optional: specify the owner username for all migrated repos
# If not specified, uses the user who owns the API key
repo_owner = "mirror_org"
# A list of remote git repositories to mirror. # A list of remote git repositories to mirror.
repos = [ repos = [
{ url = "https://gitea.hak8or.com/hak8or/gitea_mirror.git" }, { url = "https://gitea.hak8or.com/hak8or/gitea_mirror.git" },

View File

@@ -43,6 +43,7 @@ struct Config {
api_key: String, api_key: String,
repos: Option<Vec<RepoConfig>>, repos: Option<Vec<RepoConfig>>,
organizations: Option<Vec<OrgConfig>>, organizations: Option<Vec<OrgConfig>>,
repo_owner: Option<String>, // Optional owner username/org for all migrated repos
} }
// Represents the payload for creating a migration in Gitea. // Represents the payload for creating a migration in Gitea.
@@ -50,16 +51,16 @@ struct Config {
struct MigrateRepoPayload<'a> { struct MigrateRepoPayload<'a> {
clone_addr: &'a str, clone_addr: &'a str,
repo_name: &'a str, repo_name: &'a str,
repo_owner: &'a str, // Username or organization name
mirror: bool, mirror: bool,
private: bool, private: bool,
description: &'a str, description: &'a str,
uid: i64, // The user ID of the owner. We'll fetch this.
} }
// Represents a user as returned by the Gitea API. // Represents a user as returned by the Gitea API.
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct GiteaUser { struct GiteaUser {
id: i64, login: String,
} }
/// Entry point of the application. /// Entry point of the application.
@@ -77,12 +78,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = load_config(&args.config)?; let config = load_config(&args.config)?;
let http_client = reqwest::Client::new(); let http_client = reqwest::Client::new();
// Fetch the Gitea user ID for the authenticated user. // Determine the owner (either from repo_owner or authenticated user)
let user_id = get_gitea_user_id(&http_client, &config.gitea_url, &config.api_key).await?; let owner_name = if let Some(owner) = &config.repo_owner {
info!( info!("Using specified repo_owner: {}", owner);
"Successfully authenticated and retrieved user ID: {}", owner.clone()
user_id } else {
); info!("No repo_owner specified, fetching authenticated user");
get_authenticated_username(&http_client, &config.gitea_url, &config.api_key).await?
};
info!("Using owner '{}' for all migrated repositories", owner_name);
// Process repositories from the static list. // Process repositories from the static list.
if let Some(repos) = &config.repos { if let Some(repos) = &config.repos {
@@ -90,7 +95,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
process_repo( process_repo(
&repo_config.url, &repo_config.url,
repo_config.rename.as_deref(), repo_config.rename.as_deref(),
user_id, &owner_name,
&http_client, &http_client,
&config, &config,
args.dry_run, args.dry_run,
@@ -119,7 +124,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
process_repo( process_repo(
&url, &url,
None, // No rename support for orgs None, // No rename support for orgs
user_id, &owner_name,
&http_client, &http_client,
&config, &config,
args.dry_run, args.dry_run,
@@ -145,13 +150,13 @@ fn load_config(path: &Path) -> Result<Config, Box<dyn std::error::Error>> {
Ok(config) Ok(config)
} }
/// Fetches the authenticated user's ID from Gitea. /// Fetches the authenticated user's login name from Gitea.
#[instrument(skip(http_client, gitea_url, api_key))] #[instrument(skip(http_client, gitea_url, api_key))]
async fn get_gitea_user_id( async fn get_authenticated_username(
http_client: &reqwest::Client, http_client: &reqwest::Client,
gitea_url: &str, gitea_url: &str,
api_key: &str, api_key: &str,
) -> Result<i64, reqwest::Error> { ) -> Result<String, reqwest::Error> {
let url = format!("{}/api/v1/user", gitea_url); let url = format!("{}/api/v1/user", gitea_url);
let user: GiteaUser = http_client let user: GiteaUser = http_client
.get(&url) .get(&url)
@@ -161,7 +166,8 @@ async fn get_gitea_user_id(
.error_for_status()? .error_for_status()?
.json() .json()
.await?; .await?;
Ok(user.id) info!("Authenticated as user: {}", user.login);
Ok(user.login)
} }
/// Checks if a repository already exists in Gitea for the user. /// Checks if a repository already exists in Gitea for the user.
@@ -276,11 +282,11 @@ async fn fetch_org_repos(
} }
/// Core logic to process a single repository. /// Core logic to process a single repository.
#[instrument(skip(user_id, http_client, config, dry_run))] #[instrument(skip(owner_name, http_client, config, dry_run))]
async fn process_repo( async fn process_repo(
repo_url: &str, repo_url: &str,
rename: Option<&str>, rename: Option<&str>,
user_id: i64, owner_name: &str,
http_client: &reqwest::Client, http_client: &reqwest::Client,
config: &Config, config: &Config,
dry_run: bool, dry_run: bool,
@@ -301,10 +307,10 @@ async fn process_repo(
let payload = MigrateRepoPayload { let payload = MigrateRepoPayload {
clone_addr: repo_url, clone_addr: repo_url,
repo_name, repo_name,
repo_owner: owner_name,
mirror: true, mirror: true,
private: false, // Defaulting to public, change if needed private: false, // Defaulting to public, change if needed
description: "", description: "",
uid: user_id,
}; };
if let Err(e) = create_migration(http_client, config, &payload).await { if let Err(e) = create_migration(http_client, config, &payload).await {
error!("Failed to create migration for '{}': {}", repo_name, e); error!("Failed to create migration for '{}': {}", repo_name, e);