use std::collections::HashSet;
use std::fs;
use std::path::Path;
use crate::context::CommandContext;
use crate::db::{self, Database};
use crate::error::Result;
use crate::storage::MetadataStore;
fn merge_events_jsonl(local_path: &Path, remote_content: &str) {
let existing: HashSet<String> = if local_path.exists() {
fs::read_to_string(local_path)
.unwrap_or_default()
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.to_string())
.collect()
} else {
HashSet::new()
};
let new_lines: Vec<&str> = remote_content
.lines()
.filter(|l| !l.trim().is_empty() && !existing.contains(*l))
.collect();
if new_lines.is_empty() {
return;
}
use std::io::Write;
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(local_path)
{
for line in &new_lines {
let _ = writeln!(file, "{}", line);
}
}
}
pub fn execute(ctx: &CommandContext, remote: &str) -> Result<()> {
let jj_client = ctx.jj();
let sync_config = ctx.store.load_config().unwrap_or_default().sync;
let has_git = jj_client.has_git_backend();
let fetch_cmd = match sync_config.resolve_fetch(has_git) {
Some(cmd) => cmd,
None => {
println!("No sync backend configured and no git backend detected.");
println!("Configure [sync] fetch in config.toml for custom sync commands.");
return Ok(());
}
};
let db_path = jj_client.repo_root().join(".jj").join("jjj.db");
if db_path.exists() {
let db = Database::open(&db_path)?;
if db::is_dirty(&db)? {
println!("Saving local changes before fetch...");
db::dump_to_markdown(&db, &ctx.store)?;
ctx.store
.commit_changes("Sync local changes before fetch")?;
}
}
let solutions_before = ctx.store.list_solutions().unwrap_or_default().len();
let critiques_before = ctx.store.list_critiques().unwrap_or_default().len();
println!("Fetching from {}...", remote);
let vars = [("remote", remote), ("bookmark", "jjj")];
jj_client.execute_sync_command(&fetch_cmd, &vars)?;
if let Some(track_cmd) = sync_config.resolve_track(has_git) {
let _ = jj_client.execute_sync_command(&track_cmd, &vars);
}
let meta_path = jj_client.repo_root().join(".jj").join("jjj-meta");
if jj_client.bookmark_exists("jjj")? {
fs::create_dir_all(&meta_path)?;
for dir in &["problems", "solutions", "critiques", "milestones"] {
fs::create_dir_all(meta_path.join(dir))?;
if let Ok(listing) = jj_client.execute(&[
"file",
"list",
"-r",
"jjj",
&format!("{}/", dir),
]) {
for file_path in listing.lines().filter(|l| !l.trim().is_empty()) {
if let Ok(content) = jj_client.execute(&[
"file",
"show",
"-r",
"jjj",
file_path,
]) {
let local_path = meta_path.join(file_path);
let _ = fs::write(&local_path, &content);
}
}
}
}
if let Ok(content) =
jj_client.execute(&["file", "show", "-r", "jjj", "config.toml"])
{
let _ = fs::write(meta_path.join("config.toml"), &content);
}
if let Ok(remote_events) =
jj_client.execute(&["file", "show", "-r", "jjj", "events.jsonl"])
{
merge_events_jsonl(&meta_path.join("events.jsonl"), &remote_events);
}
}
let ws_prefix = sync_config.workspace.as_deref();
let _ = jj_client.execute_workspace(ws_prefix, "update-stale", &[]);
println!("Rebuilding database...");
if db_path.exists() {
fs::remove_file(&db_path)?;
}
let db = Database::open(&db_path)?;
let store_after = MetadataStore::new(jj_client.clone())?;
db::load_from_markdown(&db, &store_after)?;
let solutions_after = store_after.list_solutions().unwrap_or_default().len();
let critiques_after = store_after.list_critiques().unwrap_or_default().len();
let new_solutions = solutions_after.saturating_sub(solutions_before);
let new_critiques = critiques_after.saturating_sub(critiques_before);
println!("Fetched from {}.", remote);
if new_solutions > 0 {
println!(" {} new solution(s)", new_solutions);
}
if new_critiques > 0 {
println!(" {} new critique(s)", new_critiques);
}
if new_solutions == 0 && new_critiques == 0 {
println!(" No new jjj changes.");
}
Ok(())
}