use crate::context::CommandContext;
use crate::db::{self, Database};
use crate::error::Result;
use crate::jj::JjClient;
use crate::models::CritiqueStatus;
use crate::storage::MetadataStore;
use std::io::{self, Write};
fn sync_meta_to_bookmark(jj_client: &JjClient, store: &MetadataStore) -> Result<()> {
use crate::storage::META_BOOKMARK;
use std::fs;
let sync_config = store.load_config().unwrap_or_default().sync;
let meta_path = store.meta_path();
let sync_path = jj_client.repo_root().join(".jj").join("jjj-sync");
if !sync_path.join(".jj").exists() {
let sync_str = sync_path
.to_str()
.ok_or_else(|| crate::error::JjjError::PathError(sync_path.clone()))?;
let revision = if jj_client.bookmark_exists(META_BOOKMARK)? {
META_BOOKMARK
} else {
"root()"
};
let ws_prefix = Some(sync_config.workspace_prefix());
jj_client.execute_workspace(ws_prefix, "add", &[sync_str, "-r", revision])?;
}
let sync_client = JjClient::with_root(sync_path.clone())?;
let ws_prefix = Some(sync_config.workspace_prefix());
let _ = sync_client.execute_workspace(ws_prefix, "update-stale", &[]);
for dir in &["problems", "solutions", "critiques", "milestones"] {
let src_dir = meta_path.join(dir);
let dst_dir = sync_path.join(dir);
fs::create_dir_all(&dst_dir)?;
if dst_dir.exists() {
for entry in (fs::read_dir(&dst_dir)?).flatten() {
let _ = fs::remove_file(entry.path());
}
}
if src_dir.exists() {
for entry in (fs::read_dir(&src_dir)?).flatten() {
let dst = dst_dir.join(entry.file_name());
fs::copy(entry.path(), dst)?;
}
}
}
let config_src = meta_path.join("config.toml");
if config_src.exists() {
fs::copy(&config_src, sync_path.join("config.toml"))?;
}
let events_src = meta_path.join("events.jsonl");
if events_src.exists() {
fs::copy(&events_src, sync_path.join("events.jsonl"))?;
}
sync_client.describe("jjj: sync metadata")?;
sync_client.execute(&["new"])?;
let commit_id = sync_client
.execute(&["log", "--no-graph", "-r", "@-", "-T", "commit_id"])?
.trim()
.to_string();
if jj_client.bookmark_exists(META_BOOKMARK)? {
jj_client.execute(&[
"--ignore-working-copy",
"bookmark",
"set",
META_BOOKMARK,
"-r",
&commit_id,
"--allow-backwards",
])?;
} else {
jj_client.execute(&[
"--ignore-working-copy",
"bookmark",
"create",
META_BOOKMARK,
"-r",
&commit_id,
])?;
}
Ok(())
}
fn prompt_yes_no(message: &str) -> bool {
print!("{} [Y/n] ", message);
if io::stdout().flush().is_err() {
return false;
}
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
return false;
}
let input = input.trim().to_lowercase();
input.is_empty() || input == "y" || input == "yes"
}
pub fn execute(
ctx: &CommandContext,
bookmarks: Vec<String>,
remote: &str,
no_prompt: bool,
dry_run: bool,
) -> Result<()> {
let store = &ctx.store;
let jj_client = ctx.jj();
let sync_config = store.load_config().unwrap_or_default().sync;
let has_git = jj_client.has_git_backend();
let push_cmd = match sync_config.resolve_push(has_git) {
Some(cmd) => cmd,
None => {
println!("No sync backend configured and no git backend detected.");
println!("Configure [sync] push 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)?;
println!("Syncing database to files...");
db::dump_to_markdown(&db, store)?;
println!("Validating metadata...");
let errors = db::validate(&db)?;
if !errors.is_empty() {
println!("Validation errors:");
for error in &errors {
println!(" \u{2717} {}", error);
}
return Err(crate::error::JjjError::Validation(
"Push aborted. Fix errors and retry.".to_string(),
));
}
println!(" \u{2713} All checks passed");
store.commit_changes("jjj: sync database before push")?;
}
sync_meta_to_bookmark(jj_client, store)?;
if dry_run {
println!("Would push to {}:", remote);
for b in &bookmarks {
println!(" {}", b);
}
println!(" jjj");
return Ok(());
}
for bookmark in &bookmarks {
println!("Pushing {}...", bookmark);
let vars = [("bookmark", bookmark.as_str()), ("remote", remote)];
if jj_client.execute_sync_command(&push_cmd, &vars).is_err() {
let retry = format!("{} --allow-new", push_cmd);
jj_client.execute_sync_command(&retry, &vars)?;
}
}
println!("Pushing jjj...");
let vars = [("bookmark", "jjj"), ("remote", remote)];
if jj_client.execute_sync_command(&push_cmd, &vars).is_err() {
let retry = format!("{} --allow-new", push_cmd);
jj_client.execute_sync_command(&retry, &vars)?;
}
println!("Pushed to {}.", remote);
if db_path.exists() {
let db = Database::open(&db_path)?;
db::set_dirty(&db, false)?;
}
if !no_prompt {
check_and_prompt_approve_solve(ctx)?;
}
Ok(())
}
fn check_and_prompt_approve_solve(ctx: &CommandContext) -> Result<()> {
let store = &ctx.store;
let solutions = store.list_solutions()?;
let user = store.jj_client.user_name().unwrap_or_default();
for solution in solutions
.iter()
.filter(|s| s.is_submitted() && s.assignee.as_deref() == Some(&user))
{
let critiques = store.list_critiques_for_solution(&solution.id)?;
let open_critiques: Vec<_> = critiques
.iter()
.filter(|c| c.status == CritiqueStatus::Open)
.collect();
if open_critiques.is_empty() && !critiques.is_empty() {
if prompt_yes_no(&format!(
"All critiques on {} \"{}\" resolved. Approve solution?",
solution.id, solution.title
)) {
crate::domain::approve_solution(&ctx.store, &solution.id, false, None)?;
println!(" Solution {} approved.", solution.id);
}
}
}
Ok(())
}