From 1e43def95472d9c906cff50534b38be2864690f4 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 15 Dec 2025 10:05:21 +0800 Subject: Update help documentation and move command functionality - Redesign move command to modify upstream mappings with support for erase operations - Add erased items support to align command and status display - Update help text to reflect new move mapping semantics and add erased item instructions - Add auto-update timeout configuration via JV_OUTDATED_MINUTES environment variable - Improve status display with separate structural and content change modes - Add force flag to hold/throw commands to skip pre-checks - Update completion scripts to include erased items in align command --- locales/help_docs/en.yml | 143 ++++--- locales/help_docs/zh-CN.yml | 142 +++++-- scripts/completions/bash/completion_jv.sh | 8 +- scripts/completions/powershell/completion_jv.ps1 | 8 +- scripts/jv_cli.ps1 | 8 + scripts/jv_cli.sh | 8 + src/bin/jv.rs | 471 +++++++++++++++++++---- src/bin/jvii.rs | 14 +- src/utils/env.rs | 25 ++ 9 files changed, 649 insertions(+), 178 deletions(-) diff --git a/locales/help_docs/en.yml b/locales/help_docs/en.yml index c47f39d..b133865 100644 --- a/locales/help_docs/en.yml +++ b/locales/help_docs/en.yml @@ -241,8 +241,7 @@ jv: export - Export files to other sheets [REMOTE] **FILE OPERATIONS**: - move - Safely rename files - move auto - Automatically handle local file moves or renames + move - Safely rename files [REMOTE] track - Track files to latest version [REMOTE] hold - Hold, sync and lock file [REMOTE] throw - Throw, sync and unlock file [REMOTE] @@ -295,6 +294,10 @@ jv: jv sheet align confirm - Confirm this file is lost jv sheet align lost confirm - Confirm all lost items + For erased items: + jv sheet align confirm - Confirm this file is erased + jv sheet align erased confirm - Confirm all erased items + jv sheet align --work - Use editor mode to align files Sheets are core concepts in JustEnoughVCS, each sheet represents an independent file collection. @@ -324,9 +327,9 @@ jv: Displays detailed information about current directory files, including: - File name, size, version number - Current file holder - - Latest version commit information + - Latest version update description - **Tip**: Use `jv here --desc` to view the last commit description for local files + **Tip**: Use `jv here --desc` to view the last update description for local files status: | **Display Current Sheet Status Information** @@ -374,15 +377,19 @@ jv: If you have made changes to the file but haven't tracked them, throwing will lose those changes. move: | - **Move Local Files** + **Move Mapping** **Usage**: - jv move - Safely rename or move files + jv move - Modify upstream mapping + jv move --erase - Erase upstream mapping - **Example**: - jv move old_name.txt new_name.txt - jv move src/old_dir/file.rs src/new_dir/file.rs + **Examples**: + jv move draft/character.png done/character.png - Move mapping + jv move character.png player.png - Rename + jv move . ../publish/ - Batch move + jv move temp/ --erase - Erase mapping - Safe move operations preserve file version history, while auto-move detects and handles all renames. + The move mapping operation modifies the upstream mapping and synchronizes the local structure (use `--only-remote` to cancel local modification) + After moving, you usually need `jv align moved remote` to synchronize the local structure to the upstream export: | **Export Files to Import Area of Other Sheets** @@ -476,8 +483,8 @@ jv: 3. For files not held unless the version is frozen, download logic will always be executed to get the latest version **CURRENT**: - **DOWN**: %{old_files} to update, %{download_files} to download - **UP** : %{new_files} to track, %{modified_files} to commit + **DOWN**: %{old_files} to sync, %{download_files} to download + **UP** : %{new_files} to track, %{modified_files} to update fail: std: @@ -485,6 +492,24 @@ jv: current_dir_name: Failed to get current directory name set_current_dir: Failed to set current directory to %{dir} + move: + rename_failed: | + **Warning**: Failed to move local file `%{from}` to `%{to}`: %{error} + + has_rename_failed: | + **Tip**: Because the file move was skipped, a deviation will occur. + After moving the file, be sure to use `jv align` to resolve the deviation. + + no_target_dir: | + You did not specify a target directory to move to! + Please use `jv move ` to move the mapping + or use `jv move --erase` to erase the mapping + + count_doesnt_match: | + You specified multiple mappings, but the target address is a single mapping. + Please use `jv move multiple_mappings directory/` to move multiple mappings + or use `jv move single_mapping mapping_name` to rename that mapping. + format_path: | Failed to format directory %{path}: %{error}. @@ -561,7 +586,9 @@ jv: 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 + **Tip**: + Add `--skip-failed` after the command to skip the current failed items and proceed with the operation + Add `--force` to ignore checks and proceed (UNSAFE) check_failed_details: In the %{num} selected files, %{failed} files failed pre-check! @@ -660,7 +687,7 @@ jv: remote_path: REMOTE_FILE no_changes: | - Great, there are no struct changes in the local workspace! + Great, no structural deviations in the local workspace, no alignment needed! docs: list: @@ -741,34 +768,43 @@ jv: %{dir_count} dir(s), %{file_count} file(s). Total %{size}. status: - header: | - Viewing sheet %{sheet_name}. - Before tracking file changes, please confirm: - - content: | - Structure changes: - %{moved_items}%{lost_items}%{created_items} - Content modifications: + struct_changes_display: | + Viewing sheet %{sheet_name} (%{h}h %{m}min %{s}secs ago). + + Now in structural change mode: + %{moved_items}%{lost_items}%{erased_items}%{created_items} + **Tip**: Use `jv align` to align moved, lost, and erased changes, + Use `jv track` to track created changes + + content_modifies_display: | + Viewing sheet %{sheet_name} (%{h}h %{m}min %{s}secs ago). + + Now in content change mode: %{modified_items} - Struct info from update %{h}h %{m}m %{s}s seconds ago - Use `jv update` to update from upstream + **Tip**: Use `jv track` to track your changes + + no_changes: | + Your workspace is synchronized with upstream, you can proceed with structural and content editing based on this state! created_item: | - + Created: %{path} + + Created: %{path} lost_item: | - - Lost: %{path} + - Lost: %{path} moved_item: | - > Moved: %{from} - To: %{to} + > Moved: Remote %{from} + Local %{to} + + erased_item: | + & Erased: %{path} modified_item: | - * Modified: %{path} + * Modified: %{path} invalid_modified_item: | - x Modified: %{path} (%{reason}) + x Modified: %{path} (%{reason}) invalid_modified_reasons: not_holder: Modified without holding @@ -914,36 +950,58 @@ jv: not_held: | You are not holding file %{path}! - This means you modified the file without holding it, and the upstream vault blocked your commit attempt + This means you modified the file without holding it, and the upstream vault blocked your update attempt (Sorry, JustEnoughVCS collaboration is based on serial editing - parallel editing and merging is not allowed) - **Tip**: If you really need to commit this file, you can follow these steps: - 1. First move the file outside the workspace and commit the correct version here + **Tip**: If you really need to update this file, you can follow these steps: + 1. First move the file outside the workspace and update the correct version here 2. Use `jv info --holder` to query the member currently editing it 3. Try to contact them, describe your situation, and wait for them to release editing rights 4. After editing rights are released, use `jv track ` to get the latest version from that member 5. Manually merge your backed-up version into the latest version - 6. Commit your modified latest version, then release editing rights + 6. Update your modified latest version, then release editing rights Finally: You can use `jv here` to check file status in the directory before editing files to ensure you can edit version_dismatch: | The base version of the file you edited does not match the version in the upstream vault! - Your version is %{version_current} while the upstream version is %{version_latest}, the upstream vault blocked your commit + Your version is %{version_current} while the upstream version is %{version_latest}, the upstream vault blocked your update **Tip**: - You can use `jv jump %{version_current}` to jump the version to your local version and commit again - If you don't want to force override the version, you can backup the file version, commit your local version to the latest, then manually merge the files and commit + You can use `jv jump %{version_current}` to jump the version to your local version and update again + If you don't want to force override the version, you can backup the file version, update your local version to the latest, then manually merge the files and update Finally: You can use `jv here` to check file status in the directory before editing files to ensure you can edit update_but_no_description: | - You are committing files to the latest version, but we don't know your modification content and new version number - You can use `jv track --desc -v ` to commit files - or use `jv track . --work` to enter the editor environment for committing + There are update items in the files you specified, but no information provided + You can use `jv track --desc -v ` to update files + or use `jv track . --work` to enter the editor environment for updating version_already_exist: | - The version %{version} of file %{path} you are committing already exists in the upstream vault, please use a different version number! + The version %{version} of file %{path} you are updating already exists in the upstream vault, please use a different version number! + + move: + success: | + Successfully modified mapping! + Upstream information has changed, please use `jv update` to sync to latest information + + mapping_not_found: | + Mapping `%{path}` does not exist! + Please check if the path you entered is correct, or use `jv update` to update workspace status + + invalid_move: + no_target: | + You did not specify a target address for the mapping `%{path}` to move to! + Please use `jv move ` to move the mapping + or use `jv move --erase` to erase the mapping + + duplicate_mapping: | + Move operation failed because target path `%{path}` already has a mapping! + Please change to another path, or erase the existing mapping first + + unknown: | + Unknown move operation result! jvii: hints: | @@ -977,9 +1035,8 @@ editor: # - Fill in the version after the arrow %{modified_lines} - ---------------------------------------------------------------- - # Update description + # Fill description here, tell others about the changes you made %{description} modified_line: diff --git a/locales/help_docs/zh-CN.yml b/locales/help_docs/zh-CN.yml index bfea30b..6a36a5d 100644 --- a/locales/help_docs/zh-CN.yml +++ b/locales/help_docs/zh-CN.yml @@ -190,7 +190,7 @@ jv: help: | **JustEnoughVCS 本地工作区命令** - 该程序将连接至上游库,用以同步、提交本地工作区文件的变化,以供协同创作 + 该程序将连接至上游库,用以同步、更新本地工作区文件的变化,以供协同创作 **常用别名**: jv u 下载最新信息,jv t 追踪文件,jv a 对齐文件结构到表,jv in/out 导入或导出文件 @@ -232,7 +232,7 @@ jv: export <文件> <表名称> - 导出文件到其他表 [远程] **文件操作**: - move <文件> <到> - 安全地重命名文件 + move <文件> <到> - 安全地重命名文件 [远程] track <文件> - 追踪文件内容到最新版本 [远程] hold <文件> - 拿取文件,同步版本并获得编辑权 [远程] throw <文件> - 丢弃文件,同步版本并放弃编辑权 [远程] @@ -286,9 +286,13 @@ jv: jv sheet align <项> confirm - 确认该文件已丢失 jv sheet align lost confirm - 确认所有丢失项 + 对于擦除项: + jv sheet align <项> confirm - 确认该文件已擦除 + jv sheet align erased confirm - 确认所有擦除项 + jv sheet align --work - 使用编辑器模式对齐文件 - 表是 JustEnoughVCS 中的核心概念,每个表代表一块独立的文件集合 + 表是 JustEnoughVCS 中的核心概念,每张表代表一块独立的文件结构 您可以在不同的表之间切换工作,或者将文件从一张表导出到另一张表 @@ -318,9 +322,9 @@ jv: 显示当前目录文件的详细信息,包括: - 文件名称、大小、版本号 - 文件当前的持有人 - - 文件最新版本的提交信息 + - 文件最新版本的更新信息 - **提示**:使用 `jv here --desc` 查看本地文件最后一次的提交信息 + **提示**:使用 `jv here --desc` 查看本地文件最后一次的更新信息 status: | **显示当前表的状态信息** @@ -369,16 +373,19 @@ jv: move: | - **移动本地文件** + **移动映射** **用法**: - jv move <源文件> <目标位置> - 安全地重命名或移动文件 + jv move <映射> <目标映射> - 修改上游映射 + jv move <映射> --erase - 擦除上游映射 **例如**: - jv move old_name.txt new_name.txt - jv move src/old_dir/file.rs src/new_dir/file.rs - - 安全移动操作会保持文件的版本历史,而自动移动会检测并处理所有重命名 + jv move draft/character.png done/character.png - 移动映射 + jv move character.png player.png - 重命名 + jv move . ../publish/ - 批量移动 + jv move temp/ --erase - 擦除映射 + 移动映射操作会修改上游的映射,并同步修改本地结构(使用 `--only-remote` 取消同步修改) + 在移动完成后,通常需要 `jv align moved remote` 将本地结构同步至上游 export: | **将文件导出至其他表的待导入区** @@ -476,8 +483,8 @@ jv: 3. 未持有文件,除非冻结版本,否则永远执行下载和更新最新版本 **当前**: - **下行**:%{old_files} 个待更新,%{download_files} 个待下载 - **上行**:%{new_files} 个待追踪,%{modified_files} 个待提交 + **下行**:%{old_files} 个待同步,%{download_files} 个待下载 + **上行**:%{new_files} 个待追踪,%{modified_files} 个待更新 fail: std: @@ -485,6 +492,24 @@ jv: current_dir_name: 无法获得当前目录的名称 set_current_dir: 无法设置到目录 %{dir} + move: + rename_failed: | + **警告**:移动本地文件 `%{from}` 至 `%{to}` 失败:%{error} + + has_rename_failed: | + **提示**:因为已跳过文件的移动,所以会产生偏差, + 在可移动文件后,请务必使用 `jv align` 解决偏差 + + no_target_dir: | + 您未指定需要移动的目录! + 请使用 `jv move <映射> <目标映射名>` 的方式移动映射 + 或使用 `jv move <映射> --erase` 将映射擦除 + + count_doesnt_match: | + 您指定了多个映射,但目标地址为单个映射 + 请使用 `jv move 多个映射 目录/` 来移动多个映射 + 或使用 `jv move 单个映射 映射名称` 来重命名该映射 + format_path: | 格式化目录 %{path} 失败:%{error}. @@ -561,7 +586,9 @@ jv: 在您选中的 %{num} 个文件中,存在预检查失败的项! 在命令后添加 `--details` 查看具体事项 - **提示**:命令后添加 `--skip-failed` 可跳过当前检查失败的项进行操作 + **提示**: + 添加 `--skip-failed` 可跳过当前检查失败的项进行操作 + 添加 `--force` 可无视检查进行操作 (不安全的操作) check_failed_details: | 在您选中的 %{num} 个文件中,有 %{failed} 个文件预先检查未通过! @@ -661,7 +688,7 @@ jv: remote_path: 远程文件 no_changes: | - 很好,本地工作区并没有结构变更! + 很好,本地工作区未产生结构偏差,无需对齐! docs: list: @@ -742,17 +769,24 @@ jv: %{dir_count} 目录、%{file_count} 文件,共计 %{size} status: - header: | - 您正在查看表 %{sheet_name} 的状态,在追踪文件变更之前,请确认: + struct_changes_display: | + 表 %{sheet_name} 的状态基于 %{h} 小时 %{m} 分钟 %{s} 秒前 + + 您的工作区处于结构变更状态: + %{moved_items}%{lost_items}%{erased_items}%{created_items} + **提示**:使用 `jv align` 对齐移动、丢失和擦除变更, + 使用 `jv track` 追踪创建变更 - content: | - 结构变更: - %{moved_items}%{lost_items}%{created_items} - 内容修改: + content_modifies_display: | + 表 %{sheet_name} 的状态基于 %{h} 小时 %{m} 分钟 %{s} 秒前 + + 您的工作区处于内容变更状态: %{modified_items} - 结构信息来自 %{h} 小时 %{m} 分钟 %{s} 秒前的更新 - 使用 `jv update` 从上游更新 + **提示**:使用 `jv track` 追踪您的变更 + + no_changes: | + 您的工作区与上游保持同步,可基于该状态进行结构、内容的编辑! created_item: | + 创建: %{path} @@ -761,8 +795,11 @@ jv: - 丢失: %{path} moved_item: | - > 移动: %{from} - 至: %{to} + > 移动:远程 %{from} + 本地 %{to} + + erased_item: | + & 擦除: %{path} modified_item: | * 修改: %{path} @@ -890,8 +927,8 @@ jv: 这意味着该表在上游库中已被删除,或该表不属于您 create_file_on_exist_path: | - 提交并创建文件失败! - 您要提交的文件路径 `%{path}` 在远程表中已存在,请更换至其他路径提交 + 创建文件失败! + 您要创建的文件路径 `%{path}` 在远程表中已存在,请更换至其他路径创建 update_failed: verify: @@ -913,36 +950,58 @@ jv: not_held: | 您并未持有文件 %{path}! - 这说明,您在未持有文件的时修改了它,并在尝试提交时被上游库阻拦 + 这说明,您在未持有文件的时修改了它,并在尝试更新时被上游库阻拦 (非常抱歉,JustEnoughVCS 协作基于串行编辑,并行编辑后合并是不允许的) - **提示**:如果您确实需要提交该文件,可以参考以下步骤: - 1. 首先将文件移动到工作区以外的地方,并重新在此处提交正确的版本 + **提示**:如果您确实需要更新该文件,可以参考以下步骤: + 1. 首先将文件移动到工作区以外的地方,并重新在此处更新正确的版本 2. 使用 `jv info <该文件> --holder` 查询正在编辑的成员 3. 尝试联系他,并描述您的情况,并等待该成员释放编辑权 4. 释放编辑权后,使用 `jv track <该文件>` 拿到该成员的最新版本 5. 手动地将您备份的版本合并至最新版本中 - 6. 将您修改的最新版提交,然后释放编辑权 + 6. 将您修改的最新版更新,然后释放编辑权 最后:您可以在编辑文件前,使用 `jv here` 查看所在目录的文件状态,以确保自己可以编辑 version_dismatch: | 您编辑的文件基准版本和上游库中的版本不匹配! - 您的版本是 %{version_current} 而上游版本是 %{version_latest},上游库禁止了您的提交 + 您的版本是 %{version_current} 而上游版本是 %{version_latest},上游库禁止了您的更新 **提示**: - 您可以使用 `jv jump <文件> %{version_current}` 将版本跳转至您的本地版本,并再次提交 - 若您不期望强制覆盖版本,可以选择将文件版本备份,并提交本地版本至最新后,再手动地合并文件并提交 + 您可以使用 `jv jump <文件> %{version_current}` 将版本跳转至您的本地版本,并再次更新 + 若您不期望强制覆盖版本,可以选择将文件版本备份,并更新本地版本至最新后,再手动地合并文件并更新 最后:您可以在编辑文件前,使用 `jv here` 查看所在目录的文件状态,以确保自己可以编辑 update_but_no_description: | - 您正在提交文件至最新版本,但是我们并不知道您的修改内容和新的版本号 - 使用 `jv track <文件> --desc <描述> -v <版本>` 提交文件 - 或使用 `jv track . --work` 进入编辑器环境提交 + 您指定的文件中存在更新项,但是您并未指定更新信息 + 使用 `jv track <文件> --desc <描述> -v <版本>` 更新文件 + 或使用 `jv track . --work` 进入编辑器环境更新 version_already_exist: | - 您正在提交的文件 %{path} 的版本 %{version} 在上游库中已存在,请使用其他版本号! + 您正在更新的文件 %{path} 的版本 %{version} 在上游库中已存在,请使用其他版本号! + + move: + success: | + 成功修改映射! + 上游信息已变更,请使用 `jv update` 同步至最新信息 + + mapping_not_found: | + 映射 `%{path}` 不存在! + 请检查您输入的路径是否正确,或使用 `jv update` 更新工作区状态 + + invalid_move: + no_target: | + 您未指定需要移动的映射 `%{path}` 的目标地址! + 请使用 `jv move <映射> <目标映射名>` 的方式移动映射 + 或使用 `jv move <映射> --erase` 将映射擦除 + + duplicate_mapping: | + 移动操作失败,因为目标路径 `%{path}` 已存在映射! + 请更换至其他路径,或先擦除已存在的映射 + + unknown: | + 未知的移动操作结果! jvii: hints: | @@ -970,13 +1029,12 @@ jvii: editor: update_editor: | - # 您正在使用编辑器模式追踪和提交文件 - # 以下文件将被提交:(行首添加 `#` 视为放弃提交,尾部箭头后请填写版本) + # 您正在使用编辑器模式追踪和更新文件 + # 以下文件将被更新:(行首添加 `#` 视为放弃更新,尾部箭头后请填写版本) %{modified_lines} - ---------------------------------------------------------------------- - # 请在后续填写提交描述,以告诉其他成员您做了什么 + # 此处填写更新描述,告诉其他成员您做了什么 %{description} modified_line: diff --git a/scripts/completions/bash/completion_jv.sh b/scripts/completions/bash/completion_jv.sh index ff600ed..be5afc6 100644 --- a/scripts/completions/bash/completion_jv.sh +++ b/scripts/completions/bash/completion_jv.sh @@ -99,7 +99,7 @@ _jv_completion() { ;; "align") if [[ $cword -eq 3 ]]; then - local align_items="lost moved" + local align_items="lost moved erased" local unsolved_items unsolved_items=$($cmd sheet align --unsolved --raw 2>/dev/null) COMPREPLY=($(compgen -W "$align_items $unsolved_items" -- "$cur")) @@ -115,6 +115,8 @@ _jv_completion() { align_operations="confirm $created_items" elif [[ "$item" == "moved" || "$item" == moved:* ]]; then align_operations="local remote" + elif [[ "$item" == "erased" || "$item" == erased:* ]]; then + align_operations="confirm" else align_operations="local remote confirm $created_items" fi @@ -129,7 +131,7 @@ _jv_completion() { # Completion align if [[ "$subcmd" == "align" ]]; then if [[ $cword -eq 2 ]]; then - local align_items="lost moved" + local align_items="lost moved erased" local unsolved_items unsolved_items=$($cmd sheet align --unsolved --raw 2>/dev/null) COMPREPLY=($(compgen -W "$align_items $unsolved_items" -- "$cur")) @@ -145,6 +147,8 @@ _jv_completion() { align_operations="confirm $created_items" elif [[ "$item" == "moved" || "$item" == moved:* ]]; then align_operations="local remote" + elif [[ "$item" == "erased" || "$item" == erased:* ]]; then + align_operations="confirm" else align_operations="local remote confirm $created_items" fi diff --git a/scripts/completions/powershell/completion_jv.ps1 b/scripts/completions/powershell/completion_jv.ps1 index 48ab3ec..b756fbd 100644 --- a/scripts/completions/powershell/completion_jv.ps1 +++ b/scripts/completions/powershell/completion_jv.ps1 @@ -89,7 +89,7 @@ Register-ArgumentCompleter -Native -CommandName jv -ScriptBlock { } "align" { if ($currentIndex -eq 3) { - $alignItems = @("lost", "moved") + $alignItems = @("lost", "moved", "erased") $unsolvedItems = & $cmd sheet align --unsolved --raw 2>$null $completions = $alignItems + $unsolvedItems return $completions | Where-Object { $_ -like "$wordToComplete*" } @@ -104,6 +104,8 @@ Register-ArgumentCompleter -Native -CommandName jv -ScriptBlock { $alignOperations = @("confirm") + $createdItems } elseif ($item -eq "moved" -or $item -like "moved:*") { $alignOperations = @("local", "remote") + } elseif ($item -eq "erased" -or $item -like "erased:*") { + $alignOperations = @("confirm") } else { $alignOperations = @("local", "remote", "confirm") + $createdItems } @@ -118,7 +120,7 @@ Register-ArgumentCompleter -Native -CommandName jv -ScriptBlock { # Completion for align command if ($subcmd -eq "align") { if ($currentIndex -eq 2) { - $alignItems = @("lost", "moved") + $alignItems = @("lost", "moved", "erased") $unsolvedItems = & $cmd sheet align --unsolved --raw 2>$null $completions = $alignItems + $unsolvedItems return $completions | Where-Object { $_ -like "$wordToComplete*" } @@ -133,6 +135,8 @@ Register-ArgumentCompleter -Native -CommandName jv -ScriptBlock { $alignOperations = @("confirm") + $createdItems } elseif ($item -eq "moved" -or $item -like "moved:*") { $alignOperations = @("local", "remote") + } elseif ($item -eq "erased" -or $item -like "erased:*") { + $alignOperations = @("confirm") } else { $alignOperations = @("local", "remote", "confirm") + $createdItems } diff --git a/scripts/jv_cli.ps1 b/scripts/jv_cli.ps1 index 8e16646..22836c7 100644 --- a/scripts/jv_cli.ps1 +++ b/scripts/jv_cli.ps1 @@ -13,6 +13,14 @@ $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Definition # Next `jv` command will auto-run `jv update` $env:JV_AUTO_UPDATE = "yes" +# Use JV_OUTDATED_MINUTES to set the expiration time (in minutes), requires JV_AUTO_UPDATE to be enabled +# Next time the `jv` command is used, if the content is outdated, `jv update` will be automatically executed +# When the set number is < 0, timeout-based update is disabled +# When the set number = 0, update runs every time (not recommended) +# When the set number > 0, update according to the specified time +# If not set, the default is -1 +# $env:JV_OUTDATED_MINUTES = "5" + # Use JV_TEXT_EDITOR to set text editor for `jv track --work` `jv align --work` # DEFAULT: $EDITOR environment variable, falling back to "jvii" if not set # $env:JV_TEXT_EDITOR = "nano" diff --git a/scripts/jv_cli.sh b/scripts/jv_cli.sh index e05df3d..d732d95 100644 --- a/scripts/jv_cli.sh +++ b/scripts/jv_cli.sh @@ -14,6 +14,14 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Next `jv` command will auto-run `jv update` export JV_AUTO_UPDATE=yes +# Use JV_OUTDATED_MINUTES to set the expiration time (in minutes), requires JV_AUTO_UPDATE to be enabled +# Next time the `jv` command is used, if the content is outdated, `jv update` will be automatically executed +# When the set number is < 0, timeout-based update is disabled +# When the set number = 0, update runs every time (not recommended) +# When the set number > 0, update according to the specified time +# If not set, the default is -1 +# export JV_OUTDATED_MINUTES=5 + # Use JV_TEXT_EDITOR to set text editor for `jv track --work` `jv align --work` # DEFAULT: $EDITOR environment variable, falling back to "jvii" if not set # export JV_TEXT_EDITOR=nano diff --git a/src/bin/jv.rs b/src/bin/jv.rs index 2aa7b92..f2f5269 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -4,7 +4,7 @@ use just_enough_vcs::{ utils::{ cfg_file::config::ConfigFile, data_struct::dada_sort::quick_sort_with_cmp, - string_proc::{self, snake_case}, + string_proc::{self, format_path::format_path, snake_case}, tcp_connection::instance::ConnectionInstance, }, vcs::{ @@ -14,8 +14,9 @@ use just_enough_vcs::{ proc_update_to_latest_info_action, }, sheet_actions::{ - DropSheetActionResult, MakeSheetActionResult, proc_drop_sheet_action, - proc_make_sheet_action, + DropSheetActionResult, EditMappingActionArguments, EditMappingActionResult, + EditMappingOperations, InvalidMoveReason, MakeSheetActionResult, OperationArgument, + proc_drop_sheet_action, proc_edit_mapping_action, proc_make_sheet_action, }, track_action::{ CreateTaskResult, NextVersion, SyncTaskResult, TrackFileActionArguments, @@ -34,9 +35,14 @@ use just_enough_vcs::{ current::{correct_current_dir, current_cfg_dir, current_local_path}, data::{ local::{ - LocalWorkspace, align::AlignTasks, cached_sheet::CachedSheet, config::LocalConfig, - file_status::AnalyzeResult, latest_file_data::LatestFileData, - latest_info::LatestInfo, vault_modified::check_vault_modified, + LocalWorkspace, + align::AlignTasks, + cached_sheet::CachedSheet, + config::LocalConfig, + file_status::{AnalyzeResult, FromRelativePathBuf}, + latest_file_data::LatestFileData, + latest_info::LatestInfo, + vault_modified::check_vault_modified, }, member::{Member, MemberId}, sheet::{SheetData, SheetMappingMetadata}, @@ -68,7 +74,7 @@ use just_enough_vcs_cli::{ }, utils::{ display::{SimpleTable, display_width, md, size_str}, - env::{current_locales, enable_auto_update}, + env::{auto_update_outdate, current_locales, enable_auto_update}, fs::move_across_partitions, globber::{GlobItem, Globber}, input::{confirm_hint, confirm_hint_or, input_with_editor, show_in_pager}, @@ -146,7 +152,7 @@ enum JustEnoughVcsWorkspaceCommand { /// Move or rename files safely #[command(alias = "mv")] - Move(MoveFileArgs), + Move(MoveMappingArgs), /// Export files to other worksheet #[command(alias = "out")] @@ -558,6 +564,10 @@ struct HoldFileArgs { /// Skip failed items #[arg(short = 'S', long)] skip_failed: bool, + + /// Skip check + #[arg(short = 'F', long)] + force: bool, } #[derive(Parser, Debug)] @@ -576,13 +586,31 @@ struct ThrowFileArgs { /// Skip failed items #[arg(short = 'S', long)] skip_failed: bool, + + /// Skip check + #[arg(short = 'F', long)] + force: bool, } #[derive(Parser, Debug)] -struct MoveFileArgs { +struct MoveMappingArgs { /// Show help information #[arg(short, long)] help: bool, + + /// Move mapping pattern + move_mapping_pattern: Option, + + /// To mapping pattern + to_mapping_pattern: Option, + + /// Erase + #[arg(short = 'e', long)] + erase: bool, + + /// Only modify upstream mapping + #[arg(short = 'r', long)] + only_remote: bool, } #[derive(Parser, Debug)] @@ -673,8 +701,20 @@ async fn main() { #[cfg(windows)] colored::control::set_virtual_terminal(true).unwrap(); + // Outdate update + let required_outdated_minutes = auto_update_outdate(); + let outdate_update_enabled = required_outdated_minutes >= 0; + // Auto update - if enable_auto_update() && check_vault_modified().await { + let enable_auto_update = enable_auto_update(); + + // The following conditions will trigger automatic update: + // 1. Auto-update is enabled + // 2. Vault has been modified OR (timeout update is enabled AND timeout is set to 0) + if enable_auto_update + && (check_vault_modified().await + || outdate_update_enabled && required_outdated_minutes == 0) + { // Record current directory let path = match current_dir() { Ok(path) => path, @@ -702,6 +742,36 @@ async fn main() { ); return; } + } else + // If automatic update and timeout update are enabled, + // but required time > 0 (not in disabled or always-update state) + if enable_auto_update && outdate_update_enabled && required_outdated_minutes > 0 { + // Read the last update time and calculate the duration + if let Some(local_cfg) = LocalConfig::read().await.ok() { + if let Some(local_dir) = current_local_path() { + if let Ok(latest_info) = LatestInfo::read_from(LatestInfo::latest_info_path( + &local_dir, + &local_cfg.current_account(), + )) + .await + { + if let Some(update_instant) = latest_info.update_instant { + let now = Instant::now(); + let duration_secs = now.duration_since(update_instant).as_secs(); + + if duration_secs > required_outdated_minutes as u64 * 60 { + // Update + // This will change the current current_dir + jv_update(UpdateArgs { + help: false, + silent: true, + }) + .await + } + } + } + }; + }; } let Ok(parser) = JustEnoughVcsWorkspace::try_parse() else { @@ -773,13 +843,14 @@ async fn main() { if let Some(instant) = latest_info.update_instant { let now = Instant::now(); let duration = now.duration_since(instant); - if duration.as_secs() > 60 * 15 { - // More than 15 minutes + + if duration.as_secs() > 60 * required_outdated_minutes.clamp(5, i64::MAX) as u64 { + // Automatically prompt if exceeding the set timeout (at least 5 minutes) let hours = duration.as_secs() / 3600; let minutes = (duration.as_secs() % 3600) / 60; - println!(); + println!( - "{}", + "\n{}", t!("jv.tip.outdated", hour = hours, minutes = minutes) .trim() .yellow() @@ -1053,6 +1124,10 @@ async fn main() { .await } JustEnoughVcsWorkspaceCommand::Align(sheet_align_args) => { + if sheet_align_args.help { + println!("{}", md(t!("jv.align"))); + return; + } jv_sheet_align(sheet_align_args).await } JustEnoughVcsWorkspaceCommand::As(args) => { @@ -1676,11 +1751,6 @@ async fn jv_status(_args: StatusArgs) { return; }; - println!( - "{}", - t!("jv.success.status.header", sheet_name = sheet_name) - ); - // Format created items let mut created_items: Vec = analyzed .created @@ -1696,21 +1766,40 @@ async fn jv_status(_args: StatusArgs) { }) .collect(); - // Format lost items - let mut lost_items: Vec = analyzed - .lost + // Format erased items + let mut erased_items: Vec = analyzed + .erased .iter() .map(|path| { t!( - "jv.success.status.lost_item", + "jv.success.status.erased_item", path = path.display().to_string() ) .trim() - .red() + .magenta() .to_string() }) .collect(); + // Format lost items + let mut lost_items: Vec = analyzed + .lost + .iter() + .filter_map(|path| { + let path_str = path.display().to_string(); + if analyzed.erased.contains(path) { + return None; + } else { + return Some( + t!("jv.success.status.lost_item", path = path_str) + .trim() + .red() + .to_string(), + ); + } + }) + .collect(); + // Format moved items let mut moved_items: Vec = analyzed .moved @@ -1792,13 +1881,16 @@ async fn jv_status(_args: StatusArgs) { }) .collect(); - let has_struct_changes = - !created_items.is_empty() || !lost_items.is_empty() || !moved_items.is_empty(); + let has_struct_changes = !created_items.is_empty() + || !lost_items.is_empty() + || !erased_items.is_empty() + || !moved_items.is_empty(); let has_file_modifications = !modified_items.is_empty(); if has_struct_changes { sort_paths(&mut created_items); sort_paths(&mut lost_items); + sort_paths(&mut erased_items); sort_paths(&mut moved_items); } if has_file_modifications { @@ -1812,57 +1904,58 @@ async fn jv_status(_args: StatusArgs) { let m = (duration.as_secs() % 3600) / 60; let s = duration.as_secs() % 60; - println!( - "{}", - md(t!( - "jv.success.status.content", - moved_items = if has_struct_changes { - if moved_items.is_empty() { + if has_struct_changes { + println!( + "{}", + md(t!( + "jv.success.status.struct_changes_display", + sheet_name = sheet_name, + moved_items = if moved_items.is_empty() { "".to_string() } else { moved_items.join("\n") + "\n" - } - } else { - t!("jv.success.status.no_structure_changes") - .trim() - .to_string() - + "\n" - }, - lost_items = if has_struct_changes { - if lost_items.is_empty() { + }, + lost_items = if lost_items.is_empty() { "".to_string() } else { lost_items.join("\n") + "\n" - } - } else { - "".to_string() - }, - created_items = if has_struct_changes { - if created_items.is_empty() { + }, + erased_items = if erased_items.is_empty() { + "".to_string() + } else { + erased_items.join("\n") + "\n" + }, + created_items = if created_items.is_empty() { "".to_string() } else { created_items.join("\n") + "\n" - } - } else { - "".to_string() - }, - modified_items = if has_file_modifications { - if modified_items.is_empty() { + }, + h = h, + m = m, + s = s + )) + .trim() + ); + } else if has_file_modifications { + println!( + "{}", + md(t!( + "jv.success.status.content_modifies_display", + sheet_name = sheet_name, + modified_items = if modified_items.is_empty() { "".to_string() } else { modified_items.join("\n") - } - } else { - t!("jv.success.status.no_file_modifications") - .trim() - .to_string() - }, - h = h, - m = m, - s = s - )) - .trim() - ); + }, + h = h, + m = m, + s = s + )) + .trim() + ); + } else { + println!("{}", md(t!("jv.success.status.no_changes"))); + } } async fn jv_sheet_list(args: SheetListArgs) { @@ -2246,6 +2339,7 @@ async fn jv_sheet_align(args: SheetAlignArgs) { align_tasks.created.iter().for_each(|i| println!("{}", i.0)); align_tasks.moved.iter().for_each(|i| println!("{}", i.0)); align_tasks.lost.iter().for_each(|i| println!("{}", i.0)); + align_tasks.erased.iter().for_each(|i| println!("{}", i.0)); return; } if args.list_created { @@ -2255,6 +2349,7 @@ async fn jv_sheet_align(args: SheetAlignArgs) { if args.list_unsolved { align_tasks.moved.iter().for_each(|i| println!("{}", i.0)); align_tasks.lost.iter().for_each(|i| println!("{}", i.0)); + align_tasks.erased.iter().for_each(|i| println!("{}", i.0)); return; } return; @@ -2270,7 +2365,7 @@ async fn jv_sheet_align(args: SheetAlignArgs) { }, ]); - let mut empty_count = 0; + let mut need_align = 0; if !align_tasks.created.is_empty() { align_tasks.created.iter().for_each(|(n, p)| { @@ -2280,8 +2375,6 @@ async fn jv_sheet_align(args: SheetAlignArgs) { "".to_string(), ]); }); - } else { - empty_count += 1; } if !align_tasks.lost.is_empty() { @@ -2292,8 +2385,18 @@ async fn jv_sheet_align(args: SheetAlignArgs) { "".to_string(), ]); }); - } else { - empty_count += 1; + need_align += 1; + } + + if !align_tasks.erased.is_empty() { + align_tasks.erased.iter().for_each(|(n, p)| { + table.push_item(vec![ + format!("& {}", n).magenta().to_string(), + p.display().to_string().magenta().to_string(), + "".to_string(), + ]); + }); + need_align += 1; } if !align_tasks.moved.is_empty() { @@ -2304,17 +2407,16 @@ async fn jv_sheet_align(args: SheetAlignArgs) { rp.display().to_string(), ]); }); - } else { - empty_count += 1; + need_align += 1; } - if empty_count == 3 { - println!("{}", md(t!("jv.success.sheet.align.no_changes").trim())); - } else { + if need_align > 0 { println!( "{}", md(t!("jv.success.sheet.align.list", tasks = table.to_string())) ); + } else { + println!("{}", md(t!("jv.success.sheet.align.no_changes").trim())); } return; @@ -2706,6 +2808,7 @@ async fn jv_hold(args: HoldFileArgs) { EditRightChangeBehaviour::Hold, args.show_fail_details, args.skip_failed, + args.force, ) .await; } @@ -2733,6 +2836,7 @@ async fn jv_throw(args: ThrowFileArgs) { EditRightChangeBehaviour::Throw, args.show_fail_details, args.skip_failed, + args.force, ) .await; } @@ -2742,6 +2846,7 @@ async fn jv_change_edit_right( behaviour: EditRightChangeBehaviour, show_fail_details: bool, mut skip_failed: bool, + force: bool, ) { // If both `--details` and `--skip-failed` are set, only enable `--details` if show_fail_details && skip_failed { @@ -2830,6 +2935,12 @@ async fn jv_change_edit_right( for file in files { let exists = file.exists(); + // If force is enabled, add to the list regardless + if force { + passed_files.push(file); + continue; + } + // Mapping exists let Some(cached_mapping) = cached_sheet.mapping().get(&file) else { let reason = t!( @@ -3082,8 +3193,214 @@ async fn jv_change_edit_right( } } -async fn jv_move(_args: MoveFileArgs) { - todo!() +async fn jv_move(args: MoveMappingArgs) { + let local_dir = match current_local_path() { + Some(dir) => dir, + None => { + eprintln!("{}", t!("jv.fail.workspace_not_found").trim()); + return; + } + }; + + let move_files = if let Some(from_pattern) = args.move_mapping_pattern.clone() { + let from = glob(from_pattern, &local_dir).await; + from.iter() + .filter_map(|f| PathBuf::from_str(f.0).ok()) + .collect::>() + } else { + println!("{}", md(t!("jv.move"))); + return; + }; + + let to_pattern = if args.to_mapping_pattern.is_some() { + args.to_mapping_pattern.unwrap() + } else { + if args.erase { + "".to_string() + } else { + eprintln!("{}", md(t!("jv.fail.move.no_target_dir"))); + return; + } + }; + + let is_to_pattern_a_dir = to_pattern.ends_with('/') || to_pattern.ends_with('\\'); + + let from_mappings = move_files + .iter() + .map(|f| f.display().to_string()) + .collect::>(); + + let base_path = Globber::from(&to_pattern).base().clone(); + let base_path = format_path(base_path.strip_prefix(&local_dir).unwrap().join("./")).unwrap(); + let to_path = base_path.join(to_pattern); + + let mut edit_mapping_args: EditMappingActionArguments = EditMappingActionArguments { + operations: HashMap::::new(), + }; + + if args.erase { + // Generate erase operation parameters + for from_mapping in from_mappings { + edit_mapping_args + .operations + .insert(from_mapping.into(), (EditMappingOperations::Erase, None)); + } + } else { + // Generate move operation parameters + // Single file move + if from_mappings.len() == 1 { + let from = from_mappings[0].clone(); + let to = if is_to_pattern_a_dir { + // Input is a directory, append the filename + format_path( + to_path + .join(from.strip_prefix(&base_path.display().to_string()).unwrap()) + .to_path_buf(), + ) + .unwrap() + } else { + // Input is a filename, use it directly + format_path(to_path.to_path_buf()).unwrap() + }; + + let from: PathBuf = from.into(); + // If the from path contains to_path, ignore it to avoid duplicate moves + if !from.starts_with(to_path) { + edit_mapping_args + .operations + .insert(from, (EditMappingOperations::Move, Some(to.clone()))); + } + } else + // Multiple file move + if from_mappings.len() > 1 && is_to_pattern_a_dir { + let to_path = format_path(to_path).unwrap(); + for p in &from_mappings { + let name = p.strip_prefix(&base_path.display().to_string()).unwrap(); + let to = format_path(to_path.join(name)) + .unwrap() + .display() + .to_string(); + + let from: PathBuf = p.into(); + // If the from path contains to_path, ignore it to avoid duplicate moves + if !from.starts_with(to_path.display().to_string()) { + edit_mapping_args + .operations + .insert(from, (EditMappingOperations::Move, Some(to.into()))); + } + } + } + if from_mappings.len() > 1 && !is_to_pattern_a_dir { + eprintln!("{}", md(t!("jv.fail.move.count_doesnt_match"))); + return; + } + + // NOTE + // if move_file_mappings.len() < 1 { + // This case has already been handled earlier: output Help + // } + } + + let local_cfg = match precheck().await { + Some(config) => config, + None => return, + }; + + let (pool, ctx) = match build_pool_and_ctx(&local_cfg).await { + Some(result) => result, + None => return, + }; + + match proc_edit_mapping_action( + &pool, + ctx, + EditMappingActionArguments { + operations: edit_mapping_args.operations.clone(), + }, + ) + .await + { + Ok(r) => match r { + EditMappingActionResult::Success => { + println!("{}", md(t!("jv.result.move.success"))); + + // If the operation succeeds and only_remote is not enabled, + // synchronize local moves + if !args.only_remote { + let erase_dir = local_dir + .join(CLIENT_FOLDER_WORKSPACE_ROOT_NAME) + .join(".temp") + .join("erased"); + + let mut skipped = 0; + for (from_relative, (operation, to_relative)) in edit_mapping_args.operations { + let from = local_dir.join(&from_relative); + let to = match operation { + EditMappingOperations::Move => local_dir.join(to_relative.unwrap()), + EditMappingOperations::Erase => erase_dir.join(&from_relative), + }; + if let Some(to_dir) = to.parent() { + let _ = fs::create_dir_all(to_dir).await; + } + if let Some(e) = fs::rename(&from, &to).await.err() { + eprintln!( + "{}", + md(t!( + "jv.fail.move.rename_failed", + from = from.display(), + to = to.display(), + error = e + )) + .yellow() + ); + skipped += 1; + } + } + if skipped > 0 { + eprintln!("{}", md(t!("jv.fail.move.has_rename_failed"))); + } + } + } + EditMappingActionResult::AuthorizeFailed(e) => { + eprintln!("{}", md(t!("jv.result.common.authroize_failed", err = e))) + } + EditMappingActionResult::MappingNotFound(path_buf) => { + eprintln!( + "{}", + md(t!( + "jv.result.move.mapping_not_found", + path = path_buf.display() + )) + ) + } + EditMappingActionResult::InvalidMove(invalid_move_reason) => { + match invalid_move_reason { + InvalidMoveReason::MoveOperationButNoTarget(path_buf) => { + eprintln!( + "{}", + md(t!( + "jv.result.move.invalid_move.no_target", + path = path_buf.display() + )) + ) + } + InvalidMoveReason::ContainsDuplicateMapping(path_buf) => { + eprintln!( + "{}", + md(t!( + "jv.result.move.invalid_move.duplicate_mapping", + path = path_buf.display() + )) + ) + } + } + } + EditMappingActionResult::Unknown => { + eprintln!("{}", md(t!("jv.result.move.unknown"))) + } + }, + Err(err) => handle_err(err), + } } async fn jv_export(_args: ExportFileArgs) { diff --git a/src/bin/jvii.rs b/src/bin/jvii.rs index 83d6162..168acac 100644 --- a/src/bin/jvii.rs +++ b/src/bin/jvii.rs @@ -2,13 +2,13 @@ use std::env; use std::fs; use std::io::{self, Write}; use std::path::PathBuf; -use std::time::{Duration, Instant}; +use std::time::Duration; use clap::{Parser, command}; use crossterm::{ QueueableCommand, cursor::MoveTo, - event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}, + event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, execute, style::{self, Color, Print, SetForegroundColor}, terminal::{ @@ -470,11 +470,6 @@ impl Editor { false } - #[cfg(not(windows))] - fn is_duplicate_event(&mut self, _key_event: &KeyEvent) -> bool { - false - } - #[cfg(windows)] fn should_skip_ime_event(&mut self, key_event: &KeyEvent) -> bool { // Check for IME composition markers @@ -513,11 +508,6 @@ impl Editor { } } - #[cfg(not(windows))] - fn should_skip_ime_event(&mut self, _key_event: &KeyEvent) -> bool { - false - } - fn handle_key_event(&mut self, key_event: KeyEvent, stdout: &mut io::Stdout) -> io::Result<()> { match key_event.code { KeyCode::Char('s') if key_event.modifiers.contains(KeyModifiers::CONTROL) => { diff --git a/src/utils/env.rs b/src/utils/env.rs index c96760b..e08f117 100644 --- a/src/utils/env.rs +++ b/src/utils/env.rs @@ -48,6 +48,31 @@ pub fn enable_auto_update() -> bool { false } +/// Gets the auto update expiration time based on environment variables. +/// +/// The function checks the JV_OUTDATED_MINUTES environment variable. +/// Requires JV_AUTO_UPDATE to be enabled. +/// Next time the `jv` command is used, if the content is outdated, `jv update` will be automatically executed. +/// +/// # Returns +/// - When the set number is < 0, timeout-based update is disabled +/// - When the set number = 0, update runs every time (not recommended) +/// - When the set number > 0, update according to the specified time +/// - If not set or conversion error occurs, the default is -1 +pub fn auto_update_outdate() -> i64 { + if !enable_auto_update() { + return -1; + } + + match std::env::var("JV_OUTDATED_MINUTES") { + Ok(value) => match value.trim().parse::() { + Ok(num) => num, + Err(_) => -1, + }, + Err(_) => -1, + } +} + /// Gets the default text editor based on environment variables. /// /// The function checks the JV_TEXT_EDITOR and EDITOR environment variables -- cgit