From 4015ac6d594f971f83e9ff70578eb08fea390c80 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 26 Nov 2025 13:33:27 +0800 Subject: Add hold and throw commands for file edit rights - Implement `jv hold` and `jv throw` commands with file selection - Add pre-check validation for file existence, mapping, and edit rights - Support --details and --skip-failed flags for error handling - Add localization strings for both English and Chinese --- locales/help_docs/en.yml | 41 ++++ locales/help_docs/zh-CN.yml | 41 ++++ src/bin/jv.rs | 441 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 511 insertions(+), 12 deletions(-) diff --git a/locales/help_docs/en.yml b/locales/help_docs/en.yml index 548ce0f..af6cea3 100644 --- a/locales/help_docs/en.yml +++ b/locales/help_docs/en.yml @@ -542,6 +542,31 @@ jv: The current workspace is not stained, cannot perform the next operation! **Tip**: Please first use `jv direct ` to direct to an upstream vault + change_edit_right: + no_selection: No files selected! + check_failed: | + In the %{num} selected files, there are items that failed pre-check! + Add `--details` after the command to view specific details + + **Tip**: Add `--skip-failed` after the command to skip the current failed items and proceed with the operation + + check_failed_details: + In the %{num} selected files, %{failed} files failed pre-check! + %{items} + + check_fail_item: | + %{path} (%{reason}) + + check_fail_reason: + not_found_in_local: File Not Found + not_found_in_sheet: Mapping Not Found In Sheet + not_a_tracked_file: File Not Tracked + base_version_unmatch: Version Mismatch + not_holder: Not Holder + has_holder: Held by %{holder} + already_held: Already Held + already_modified: Already Modified + docs: not_found: Doc `%{docs_name}` not found! no_doc_dir: | @@ -809,6 +834,22 @@ jv: not_owner: | You are not the holder of sheet `%{name}`, cannot drop it! + change_edit_right: + failed: + none: | + Do nothing! + + success: + hold: | + Held %{num} files! + + throw: | + Threw %{num} files! + + mixed: | + Successfully modified edit rights for %{num} files! + Held %{num_hold}, Threw %{num_throw} + track: done: | Tracked %{count} files to latest! diff --git a/locales/help_docs/zh-CN.yml b/locales/help_docs/zh-CN.yml index 267e85c..2f496ae 100644 --- a/locales/help_docs/zh-CN.yml +++ b/locales/help_docs/zh-CN.yml @@ -544,6 +544,31 @@ jv: 当前工作区并未被染色,无法执行下一步操作! **提示**:请先使用 `jv direct <上游地址>` 定向到上游库 + change_edit_right: + no_selection: 您未选中任何文件! + check_failed: | + 在您选中的 %{num} 个文件中,存在预检查失败的项! + 在命令后添加 `--details` 查看具体事项 + + **提示**:命令后添加 `--skip-failed` 可跳过当前检查失败的项进行操作 + + check_failed_details: | + 在您选中的 %{num} 个文件中,有 %{failed} 个文件预先检查未通过! + %{items} + + check_fail_item: | + %{path}(%{reason}) + + check_fail_reason: + not_found_in_local: 文件未找到 + not_found_in_sheet: 表中不存在 + not_a_tracked_file: 文件未被跟踪 + base_version_unmatch: 基准版本不匹配 + not_holder: 期望丢弃,但不是持有者 + has_holder: 期望持有,但被 %{holder} 持有 + already_held: 文件已持有 + already_modified: 文件已修改 + docs: not_found: 文档 `%{docs_name}` 未找到! no_doc_dir: | @@ -815,6 +840,22 @@ jv: not_owner: | 您不是表 `%{name}` 的持有人,无法放弃该表! + change_edit_right: + failed: + none: | + 没有处理任何文件! + + success: + hold: | + 成功持有 %{num} 个文件! + + throw: | + 成功丢弃 %{num} 个文件! + + mixed: | + 成功修改 %{num} 个文件的编辑权! + 持有 %{num_hold},丢弃 %{num_throw} + track: done: | 追踪 %{count} 个文件至最新! diff --git a/src/bin/jv.rs b/src/bin/jv.rs index 9d7220a..ac6c5b1 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -22,6 +22,10 @@ use just_enough_vcs::{ TrackFileActionResult, UpdateDescription, UpdateTaskResult, VerifyFailReason, proc_track_file_action, }, + user_actions::{ + ChangeVirtualFileEditRightResult, EditRightChangeBehaviour, + proc_change_virtual_file_edit_right_action, + }, }, constants::{ CLIENT_FILE_TODOLIST, CLIENT_FILE_WORKSPACE, CLIENT_FOLDER_WORKSPACE_ROOT_NAME, @@ -30,9 +34,14 @@ use just_enough_vcs::{ current::{current_doc_dir, current_local_path}, data::{ local::{ - LocalWorkspace, align::AlignTasks, cached_sheet::CachedSheet, config::LocalConfig, - file_status::AnalyzeResult, latest_file_data::LatestFileData, - latest_info::LatestInfo, local_files::get_relative_paths, + LocalWorkspace, + align::AlignTasks, + cached_sheet::CachedSheet, + config::LocalConfig, + file_status::AnalyzeResult, + latest_file_data::LatestFileData, + latest_info::LatestInfo, + local_files::{RelativeFiles, get_relative_paths}, vault_modified::check_vault_modified, }, member::{Member, MemberId}, @@ -511,6 +520,17 @@ struct HoldFileArgs { /// Show help information #[arg(short, long)] help: bool, + + /// Hold files + hold_files: Option>, + + /// Show fail details + #[arg(short = 'd', long = "details")] + show_fail_details: bool, + + /// Skip failed items + #[arg(short = 'S', long)] + skip_failed: bool, } #[derive(Parser, Debug)] @@ -518,6 +538,17 @@ struct ThrowFileArgs { /// Show help information #[arg(short, long)] help: bool, + + /// Throw files + throw_files: Option>, + + /// Show fail details + #[arg(short = 'd', long = "details")] + show_fail_details: bool, + + /// Skip failed items + #[arg(short = 'S', long)] + skip_failed: bool, } #[derive(Parser, Debug)] @@ -1425,11 +1456,6 @@ async fn jv_status(_args: StatusArgs) { return; }; - let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet_name).await else { - eprintln!("{}", md(t!("jv.fail.read_cfg"))); - return; - }; - let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else { eprintln!("{}", md(t!("jv.fail.status.analyze")).trim()); return; @@ -2419,12 +2445,403 @@ async fn start_update_editor( update_info } -async fn jv_hold(_args: HoldFileArgs) { - todo!() +async fn jv_hold(args: HoldFileArgs) { + let hold_files = if let Some(files) = args.hold_files.clone() { + files + .iter() + .map(|f| current_dir().unwrap().join(f)) + .collect::>() + } else { + println!("{}", md(t!("jv.hold"))); + return; + }; + + jv_change_edit_right( + hold_files, + EditRightChangeBehaviour::Hold, + args.show_fail_details, + args.skip_failed, + ) + .await; } -async fn jv_throw(_args: ThrowFileArgs) { - todo!() +async fn jv_throw(args: ThrowFileArgs) { + let throw_files = if let Some(files) = args.throw_files.clone() { + files + .iter() + .map(|f| current_dir().unwrap().join(f)) + .collect::>() + } else { + println!("{}", md(t!("jv.throw"))); + return; + }; + + jv_change_edit_right( + throw_files, + EditRightChangeBehaviour::Throw, + args.show_fail_details, + args.skip_failed, + ) + .await; +} + +async fn jv_change_edit_right( + files: Vec, + behaviour: EditRightChangeBehaviour, + show_fail_details: bool, + mut skip_failed: bool, +) { + // If both `--details` and `--skip-failed` are set, only enable `--details` + if show_fail_details && skip_failed { + skip_failed = false; + } + + let Some(local_dir) = current_local_path() else { + eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); + return; + }; + + let Ok(local_cfg) = LocalConfig::read_from(local_dir.join(CLIENT_FILE_WORKSPACE)).await else { + eprintln!("{}", md(t!("jv.fail.read_cfg"))); + return; + }; + + let Some(local_workspace) = LocalWorkspace::init_current_dir(local_cfg.clone()) else { + eprintln!("{}", md(t!("jv.fail.workspace_not_found")).trim()); + return; + }; + + // Get files + let Ok(analyzed) = AnalyzeResult::analyze_local_status(&local_workspace).await else { + eprintln!("{}", md(t!("jv.fail.status.analyze")).trim()); + return; + }; + + let account = local_cfg.current_account(); + + let Ok(latest_file_data_path) = LatestFileData::data_path(&account) else { + eprintln!("{}", md(t!("jv.fail.read_cfg"))); + return; + }; + + let Ok(latest_file_data) = LatestFileData::read_from(&latest_file_data_path).await else { + eprintln!("{}", md(t!("jv.fail.read_cfg"))); + return; + }; + + let Some(sheet_name) = local_cfg.sheet_in_use().clone() else { + eprintln!("{}", md(t!("jv.fail.status.no_sheet_in_use")).trim()); + return; + }; + + let Ok(local_sheet) = local_workspace.local_sheet(&account, &sheet_name).await else { + eprintln!("{}", md(t!("jv.fail.read_cfg"))); + return; + }; + + let Ok(cached_sheet) = CachedSheet::cached_sheet_data(&sheet_name).await else { + eprintln!("{}", md(t!("jv.fail.read_cfg"))); + return; + }; + + // Precheck and filter + let Some(filtered_files) = get_relative_paths(&local_dir, &files).await else { + eprintln!( + "{}", + md(t!("jv.fail.track.parse_fail", param = "track_files")) + ); + return; + }; + let num = filtered_files.iter().len(); + if num < 1 { + eprintln!("{}", md(t!("jv.fail.change_edit_right.no_selection"))); + return; + } + + let mut passed_files = Vec::new(); + let mut details = Vec::new(); + let mut failed = 0; + + for file in filtered_files { + let full_path = local_dir.join(&file); + + // File exists + if !full_path.exists() { + if show_fail_details { + details.push( + t!( + "jv.fail.change_edit_right.check_fail_item", + path = file.display(), + reason = + t!("jv.fail.change_edit_right.check_fail_reason.not_found_in_local") + ) + .trim() + .to_string(), + ); + failed += 1; + continue; + } else { + eprintln!( + "{}", + md(t!("jv.fail.change_edit_right.check_failed", num = num)) + ); + return; + } + } + + // Mapping exists + if !cached_sheet.mapping().contains_key(&file) { + if show_fail_details { + details.push( + t!( + "jv.fail.change_edit_right.check_fail_item", + path = file.display(), + reason = + t!("jv.fail.change_edit_right.check_fail_reason.not_found_in_sheet") + ) + .trim() + .to_string(), + ); + failed += 1; + continue; + } else { + eprintln!( + "{}", + md(t!("jv.fail.change_edit_right.check_failed", num = num)) + ); + return; + } + } + + // Not tracked + let Ok(local_mapping) = local_sheet.mapping_data(&file) else { + if show_fail_details { + details.push( + t!( + "jv.fail.change_edit_right.check_fail_item", + path = file.display(), + reason = + t!("jv.fail.change_edit_right.check_fail_reason.not_a_tracked_file") + ) + .trim() + .to_string(), + ); + failed += 1; + continue; + } else { + eprintln!( + "{}", + md(t!("jv.fail.change_edit_right.check_failed", num = num)) + ); + return; + } + }; + + let vfid = local_mapping.mapping_vfid(); + let local_version = local_mapping.version_when_updated(); + + // Base version unmatch + if local_version + != latest_file_data + .file_version(vfid) + .unwrap_or(&String::default()) + { + if show_fail_details { + details.push( + t!( + "jv.fail.change_edit_right.check_fail_item", + path = file.display(), + reason = + t!("jv.fail.change_edit_right.check_fail_reason.base_version_unmatch") + ) + .trim() + .to_string(), + ); + failed += 1; + continue; + } else { + eprintln!( + "{}", + md(t!("jv.fail.change_edit_right.check_failed", num = num)) + ); + return; + } + } + + // Hold + let holder = latest_file_data.file_holder(vfid); + match behaviour { + EditRightChangeBehaviour::Hold => { + if holder.is_some_and(|h| h != &account) { + if show_fail_details { + details.push( + t!( + "jv.fail.change_edit_right.check_fail_item", + path = file.display(), + reason = t!( + "jv.fail.change_edit_right.check_fail_reason.has_holder", + holder = holder.unwrap() + ) + ) + .trim() + .to_string(), + ); + failed += 1; + continue; + } else { + eprintln!( + "{}", + md(t!("jv.fail.change_edit_right.check_failed", num = num)) + ); + return; + } + } + + if holder.is_some_and(|h| h == &account) { + if show_fail_details { + details.push( + t!( + "jv.fail.change_edit_right.check_fail_item", + path = file.display(), + reason = + t!("jv.fail.change_edit_right.check_fail_reason.already_held") + ) + .trim() + .to_string(), + ); + failed += 1; + continue; + } else { + eprintln!( + "{}", + md(t!("jv.fail.change_edit_right.check_failed", num = num)) + ); + return; + } + } + } + EditRightChangeBehaviour::Throw => { + if holder.is_some_and(|h| h != &account) { + if show_fail_details { + details.push( + t!( + "jv.fail.change_edit_right.check_fail_item", + path = file.display(), + reason = + t!("jv.fail.change_edit_right.check_fail_reason.not_holder") + ) + .trim() + .to_string(), + ); + failed += 1; + continue; + } else { + eprintln!( + "{}", + md(t!("jv.fail.change_edit_right.check_failed", num = num)) + ); + return; + } + } + if analyzed.modified.contains(&file) { + if show_fail_details { + details.push( + t!( + "jv.fail.change_edit_right.check_fail_item", + path = file.display(), + reason = t!( + "jv.fail.change_edit_right.check_fail_reason.already_modified" + ) + ) + .trim() + .to_string(), + ); + failed += 1; + continue; + } else { + eprintln!( + "{}", + md(t!("jv.fail.change_edit_right.check_failed", num = num)) + ); + return; + } + } + } + } + passed_files.push(file); + } + if failed > 0 && show_fail_details { + eprintln!( + "{}", + md(t!( + "jv.fail.change_edit_right.check_failed_details", + num = num, + failed = failed, + items = details.join("\n").trim() + )) + ); + return; + } + + if !(failed > 0 && skip_failed) && failed != 0 { + return; + } + + let (pool, ctx) = match build_pool_and_ctx(&local_cfg).await { + Some(result) => result, + None => return, + }; + + let passed = passed_files + .iter() + .map(|f| (f.clone(), behaviour.clone())) + .collect(); + + match proc_change_virtual_file_edit_right_action(&pool, ctx, (passed, true)).await { + Ok(r) => match r { + ChangeVirtualFileEditRightResult::Success { + success_hold, + success_throw, + } => { + if success_hold.len() > 0 && success_throw.len() == 0 { + println!( + "{}", + md(t!( + "jv.result.change_edit_right.success.hold", + num = success_hold.len() + )) + ) + } else if success_hold.len() == 0 && success_throw.len() > 0 { + println!( + "{}", + md(t!( + "jv.result.change_edit_right.success.throw", + num = success_throw.len() + )) + ) + } else if success_hold.len() > 0 && success_throw.len() > 0 { + println!( + "{}", + md(t!( + "jv.result.change_edit_right.success.mixed", + num = success_hold.len() + success_throw.len(), + num_hold = success_hold.len(), + num_throw = success_throw.len() + )) + ) + } else { + eprintln!("{}", md(t!("jv.result.change_edit_right.failed.none"))) + } + } + ChangeVirtualFileEditRightResult::AuthorizeFailed(e) => { + eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))) + } + ChangeVirtualFileEditRightResult::DoNothing => { + eprintln!("{}", md(t!("jv.result.change_edit_right.failed.none"))) + } + }, + Err(e) => handle_err(e), + } } async fn jv_move(_args: MoveFileArgs) { -- cgit