Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions src/commands/fetch_notes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use crate::error::GitAiError;
use crate::git::find_repository;
use crate::git::sync_authorship::{NotesExistence, fetch_authorship_notes};
use serde::Serialize;
use std::time::Instant;

#[derive(Debug, Serialize)]
struct FetchNotesJsonOutput {
remote: String,
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}

pub fn handle_fetch_notes(args: &[String]) {
let mut remote: Option<String> = None;
let mut json_output = false;

let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--json" => json_output = true,
"--remote" => {
i += 1;
if i >= args.len() {
eprintln!("Error: --remote requires a value");
std::process::exit(1);
}
if remote.is_some() {
eprintln!("Error: remote specified more than once");
std::process::exit(1);
}
remote = Some(args[i].clone());
}
"--help" | "-h" => {
print_fetch_notes_help();
return;
}
other if other.starts_with('-') => {
eprintln!("Error: unknown option '{}'", other);
eprintln!("Run 'git ai fetch-notes --help' for usage");
std::process::exit(1);
}
// Positional argument treated as remote name
_ => {
if remote.is_none() {
remote = Some(args[i].clone());
} else {
eprintln!("Error: unexpected argument '{}'", args[i]);
std::process::exit(1);
}
}
}
i += 1;
}

let repo = match find_repository(&Vec::<String>::new()) {
Ok(repo) => repo,
Err(e) => {
if json_output {
print_json_error("not_a_repository", &e.to_string(), remote.as_deref());
} else {
eprintln!("Error: not a git repository ({})", e);
}
std::process::exit(1);
}
};

// Resolve remote name: explicit arg > upstream tracking > default (origin)
let remote_name = match remote {
Some(r) => r,
None => match resolve_default_remote(&repo) {
Ok(r) => r,
Err(e) => {
if json_output {
print_json_error("no_remote", &e.to_string(), None);
} else {
eprintln!("Error: {}", e);
eprintln!(
"Hint: specify a remote with 'git ai fetch-notes <remote>' or 'git ai fetch-notes --remote <name>'"
);
}
std::process::exit(1);
}
},
};

if !json_output {
eprint!("Fetching authorship notes from '{}'...", remote_name);
}

let start = Instant::now();
match fetch_authorship_notes(&repo, &remote_name) {
Ok(notes_existence) => {
let elapsed = start.elapsed();
if json_output {
let status = match notes_existence {
NotesExistence::Found => "found".to_string(),
NotesExistence::NotFound => "not_found".to_string(),
};
let output = FetchNotesJsonOutput {
remote: remote_name,
status,
error: None,
};
println!(
"{}",
serde_json::to_string(&output).expect("failed to serialize JSON")
);
} else {
match notes_existence {
NotesExistence::Found => {
eprintln!(" done ({:.2}s).", elapsed.as_secs_f64());
}
NotesExistence::NotFound => {
eprintln!(" no notes found on remote ({:.2}s).", elapsed.as_secs_f64());
}
}
}
}
Err(e) => {
if json_output {
print_json_error("fetch_failed", &e.to_string(), Some(&remote_name));
} else {
eprintln!(" failed.");
eprintln!("Error: {}", e);
}
std::process::exit(1);
}
}
}

fn resolve_default_remote(repo: &crate::git::repository::Repository) -> Result<String, GitAiError> {
// Try upstream tracking remote first, then default remote
if let Ok(Some(upstream)) = repo.upstream_remote() {
return Ok(upstream);
}
if let Ok(Some(default)) = repo.get_default_remote() {
return Ok(default);
}
Err(GitAiError::Generic(
"could not determine a remote. No upstream tracking branch configured and no default remote found".to_string(),
))
}

fn print_json_error(status: &str, message: &str, remote: Option<&str>) {
let output = FetchNotesJsonOutput {
remote: remote.unwrap_or_default().to_string(),
status: status.to_string(),
error: Some(message.to_string()),
};
println!(
"{}",
serde_json::to_string(&output).expect("failed to serialize JSON")
);
}

fn print_fetch_notes_help() {
eprintln!("git ai fetch-notes - Synchronously fetch AI authorship notes from a remote");
eprintln!();
eprintln!("Usage: git ai fetch-notes [options] [<remote>]");
eprintln!();
eprintln!("Options:");
eprintln!(" <remote> Remote to fetch from (default: upstream or origin)");
eprintln!(" --remote <name> Explicit remote name");
eprintln!(" --json Output result as JSON");
eprintln!(" -h, --help Show this help message");
eprintln!();
eprintln!("Examples:");
eprintln!(" git ai fetch-notes Fetch from default remote");
eprintln!(" git ai fetch-notes upstream Fetch from 'upstream' remote");
eprintln!(" git ai fetch-notes --json Fetch and output JSON result");
}
6 changes: 6 additions & 0 deletions src/commands/git_ai_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ pub fn handle_git_ai(args: &[String]) {
"continue" => {
commands::continue_session::handle_continue(&args[1..]);
}
"fetch-notes" => {
commands::fetch_notes::handle_fetch_notes(&args[1..]);
}
"effective-ignore-patterns" => {
handle_effective_ignore_patterns_internal(&args[1..]);
}
Expand Down Expand Up @@ -348,6 +351,9 @@ fn print_help() {
eprintln!(" --launch Launch agent CLI with restored context");
eprintln!(" --clipboard Copy context to system clipboard");
eprintln!(" --json Output context as structured JSON");
eprintln!(" fetch-notes [remote] Synchronously fetch AI authorship notes");
eprintln!(" --remote <name> Explicit remote name (default: upstream or origin)");
eprintln!(" --json Output result as JSON");
eprintln!(" login Authenticate with Git AI");
eprintln!(" logout Clear stored credentials");
eprintln!(" whoami Show auth state and login identity");
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod daemon;
pub mod debug;
pub mod diff;
pub mod exchange_nonce;
pub mod fetch_notes;
pub mod flush_cas;
pub mod flush_metrics_db;
pub mod git_ai_handlers;
Expand Down
Loading
Loading