diff --git a/l10n/CMakeLists.txt b/l10n/CMakeLists.txt index dfd68fc63..70b2ad4b2 100644 --- a/l10n/CMakeLists.txt +++ b/l10n/CMakeLists.txt @@ -41,9 +41,51 @@ target_include_directories(translation PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(translation Qt6::Core) # install language files +# +# Standard Qt widget strings (buttons such as "Cancel"/"OK", +# "Show Details…"/"Hide Details…", file dialogs, etc.) are translated by +# qtbase_.qm, NOT by the application's gittyup_.ts. We must ship +# these files so they match the exact Qt that builds the app. +# +# Strategy (version-safe): +# 1. Copy qtbase_.qm from the translations directory of the Qt that is +# actually used to compile this build (derived from Qt6::Core's location), +# so the translation always matches the linked Qt version. +# 2. Only if that Qt does not ship its own translations (some distro packages +# strip them), fall back to the copies bundled in this source directory. +# The resolved file is also copied into the build l10n directory so that +# non-installed (debug) builds can load it. + +# Resolve the translations directory of the Qt we are building against. +set(QT_TRANS_DIR "") +get_target_property(_qt_core_loc Qt6::Core IMPORTED_LOCATION) +if(_qt_core_loc) + get_filename_component(_qt_bin "${_qt_core_loc}" DIRECTORY) + get_filename_component(_qt_prefix "${_qt_bin}" DIRECTORY) + set(QT_TRANS_DIR "${_qt_prefix}/translations") +endif() +if(NOT IS_DIRECTORY "${QT_TRANS_DIR}" AND DEFINED QT_TRANSLATIONS_DIR) + set(QT_TRANS_DIR "${QT_TRANSLATIONS_DIR}") +endif() foreach(LANGUAGE ${LANGUAGES}) - set(QT_QM_FILES ${QT_QM_FILES} ${QT_TRANSLATIONS_DIR}/qtbase_${LANGUAGE}.qm) + set(QT_QM "") + if(EXISTS "${QT_TRANS_DIR}/qtbase_${LANGUAGE}.qm") + set(QT_QM "${QT_TRANS_DIR}/qtbase_${LANGUAGE}.qm") + elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/qtbase_${LANGUAGE}.qm") + set(QT_QM "${CMAKE_CURRENT_SOURCE_DIR}/qtbase_${LANGUAGE}.qm") + endif() + + if(QT_QM) + get_filename_component(QM_NAME ${QT_QM} NAME) + # Available to non-installed (debug) builds that read from the build l10n dir. + configure_file(${QT_QM} + "${CMAKE_BINARY_DIR}/l10n/${QM_NAME}" + COPYONLY) + install(FILES ${QT_QM} + DESTINATION ${L10N_INSTALL_DIR} + COMPONENT ${GITTYUP_NAME}) + endif() if(APPLE) get_target_property(DIR gittyup RUNTIME_OUTPUT_DIRECTORY) @@ -55,15 +97,6 @@ foreach(LANGUAGE ${LANGUAGES}) endif() endforeach() -foreach(QM_FILE ${QT_QM_FILES}) - if(EXISTS ${QM_FILE}) - install( - FILES ${QM_FILE} - DESTINATION ${L10N_INSTALL_DIR} - COMPONENT ${GITTYUP_NAME}) - endif() -endforeach() - foreach(QM_FILE ${QM_FILES}) install( FILES ${QM_FILE} diff --git a/l10n/gittyup_zh_CN.ts b/l10n/gittyup_zh_CN.ts index d978f4a24..217505247 100644 --- a/l10n/gittyup_zh_CN.ts +++ b/l10n/gittyup_zh_CN.ts @@ -40,27 +40,27 @@ Account - + Connection failed 连接失败 - + <b>Note:</b> Basic authentication is not supported if you have two-factor authentication enabled. Use a <a href='https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/'>personal access token</a> in the password field instead. <b>注意:</b> 如果启用了双重身份验证,则不支持基本身份验证。在密码字段中使用<a href='https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/'>个人访问令牌</a>代替。 - + <b>Note:</b> Only Basic authentication is currently supported <b>Note:</b> 目前仅支持基本认证 - + <b>Note:</b> Basic authentication is not supported. Use a <a href='https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html'>personal access token</a> in the password field instead. <b>注意:</b> 不支持基本身份验证。在密码字段中使用<a href='https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html'>个人访问令牌</a>代替。 - + Authentication failed 验证失败 @@ -158,162 +158,162 @@ AdvancedSearchWidget - + Author: 作者: - + Author name 作者姓名 - + Email: 电子邮件: - + Author email 作者电子邮件 - + Message: 信息: - + Commit message 提交讯息 - + Date: 日期: - + Specific commit date 具体提交日期 - + After: 之后: - + Commits after date 在此日期之后提交 - + Before: 之前: - + Commits before date 在此日期之前提交 - + File: 文件: - + File name 文件名称 - + Path: 路径: - + File path 文件路径 - + Scope: 范围: - + Hunk header text 补丁片段的头信息 - + Context: 内容: - + Diff context (white) 差异内容(白色) - + Addition: 追加: - + Diff addition (green) 差异追加(绿色) - + Deletion: 删除: - + Diff deletion (red) 差异删除(红色) - + Comment: 注释: - + Source code comment 源代码注释 - + String: 字符串: - + Source code string literal 源代码字符串文字 - + Identifier: 识别: - + Source code identifier 源代码标识符 - + Search 搜索 - + Return 返回 @@ -349,12 +349,12 @@ Application - + SSL Errors SSL错误 - + Failed to set up SSL session. Do you want to ignore these errors? 无法设置SSL会话。您要忽略这些错误吗? @@ -362,17 +362,17 @@ AuthorCommitterDate - + Author/Committer: 作者/提交者: - + Author: 作者: - + Committer: 提交者: @@ -394,27 +394,27 @@ BlameEditor - + Untitled 无标题 - + Not Tracked 未追踪 - + HEAD - + Working Copy 工作副本 - + Save File 保存文件 @@ -422,12 +422,12 @@ BlameMargin - + Not Committed 未提交 - + Invalid Signature 签名无效 @@ -546,27 +546,27 @@ CommitDetail - + Copy 复制 - + Range: 范围: - + Id: ID: - + initial commit 初次提交 - + Parents: 父级: @@ -624,120 +624,120 @@ CommitEditor - + T 提示 - + <b>Commit Message:</b> <b>提交消息:</b> - - + + Spell Check Language 拼写检查语言 - + The dictionary '%1' is invalid 字典 '%1' 无效 - + Spell checking is disabled. 拼写检查被禁用。 - + The choosen dictionary '%1.dic' is not a valid hunspell dictionary. 选定的字典 '%1.dic' 不是有效的 hunspell 字典。 - - + + Invalid dictionary '%1.dic' 无效的字典 '%1.dic' - + Edit User Dictionary 编辑用户字典 - + Stage All 全部暂存 - + Unstage All 全部取消暂存 - - + + Commit 提交 - + Abort rebasing 中止变基操作(rebasing) - + Continue rebasing 继续变基操作(rebasing) - + Abort Merge 中止合并(Merge) - + %1 %1 - + %1 and %2 %1 和 %2 - + %1, %2, and %3 %1, %2, 和 %3 - + Merge 合并 - + Revert 还原 - + Cherry-pick 拣选操作 - + Rebase 变基(Rebase) - + Abort %1 中止%1 - + Commit Rebase 提交变基(Rebase) @@ -758,47 +758,47 @@ 更新%1,%2和%3多个文件... - + Nothing staged 没有暂存 - + %1 of %2 file staged 已暂存%2个文件中的%1个 - + %1 of %2 files staged 已暂存%2个文件中的%1个 - + %1 file partially staged %1文件已部分暂存 - + %1 files partially staged 部分暂存了%1个文件 - + %1 unresolved conflict %1个未解决的冲突 - + %1 unresolved conflicts %1个未解决的冲突 - + all conflicts resolved 所有冲突已解决 - + Commit Merge 提交合并 @@ -806,42 +806,42 @@ CommitList - + Remove Untracked Files 删除未跟踪的文件 - + Apply 应用 - + Pop 填充(Pop) - + Drop 丢弃(Drop) - + Unstar 取消星标 - + Star 星标 - + Add Tag... 添加标签... - + New Branch... 新建分支... @@ -854,83 +854,83 @@ 删除分支%1 - + Rename Branch 重命名分支 - + Delete Branch 删除分支 - + Delete Tag 删除标签 - + Merge... 合并... - + Rebase... 变基... - + Squash... 聚合(Squash)... - + Revert 还原 - + Cherry-pick 拣选操作 - - + + Checkout %1 检出%1 - + Checkout 检出 - + Local branch is already checked out 本地分支已检出 - + This is a bare repository 这是一个纯仓库 - + Reset 重置 - + Soft 软性 - + Mixed 混合 - + Hard 硬性 @@ -938,12 +938,12 @@ CommitModel - + Uncommitted changes 未提交的更改 - + Checking for uncommitted changes 检查未提交的更改 @@ -951,7 +951,7 @@ CommitToolBar - + Show All Branches 显示所有分支 @@ -961,42 +961,47 @@ 显示选定的分支 - + + Show Selected Branch, First Parent Only + 仅显示选中分支的第一个父提交 + + + Sort by Date 按日期排序 - + Sort Topologically 拓扑排序 - + Show Graph 显示图 - + Show Clean Status 显示清理状态 - + Compact Mode 紧凑模式 - + Show Author 显示作者 - + Show Date 显示日期 - + Show Id 显示Id号 @@ -1004,56 +1009,117 @@ ConfigDialog - + Esc 退出 - + General 一般 - + Diff 比较 - + Remotes 远程 - + Branches 分支 - + Submodules 子模块 - + Search 搜索 - + Plugins 插件 - + LFS LFS - + Edit Config File... 编辑配置文件... + + CredentialHelper + + + Caching the credentials in the RAM. Required to enter credentials on every startup + 在内存(RAM)中缓存凭据。每次启动都需要重新输入凭据 + + + + Storing the credentials unencrypted on disk, protected only by filesystem permissions <a href="https://git-scm.com/docs/git-credential-store">git-credential-store</a> + 将凭据以明文形式存储在磁盘上,仅由文件系统权限保护 <a href="https://git-scm.com/docs/git-credential-store">git 凭据存储</a> + + + + MacOS credential manager + MacOS 凭据管理器 + + + + Windows credential manager + Windows 凭据管理器 + + + + Secret Service D-Bus client library + Secret Service D-Bus 客户端库 + + + + Prefer <a href="https://wiki.gnome.org/Projects/Libsecret">libsecret</a> over gnome-keyring if available + 若可用,优先使用 <a href="https://wiki.gnome.org/Projects/Libsecret">libsecret</a> 而非 gnome-keyring + + + cache + 缓存 + + + git-credential-store + Git 凭据存储 + + + store + 存储 + + + osxkeychain + macOS 钥匙串 + + + wincred + Windows 凭据管理器 + + + libsecret + Secret Service 密钥服务 + + + gnome-keyring + GNOME 钥匙环 + + DateSelectionGroupWidget @@ -1147,17 +1213,17 @@ 推送 - + Push canceled. 推送已取消。 - + Unable to push to %1 - %2 无法推送到%1-%2 - + The branch is not fully merged. Deleting it may cause some commits to be lost. 分支未完全合并。删除它可能会导致某些提交丢失。 @@ -1195,22 +1261,22 @@ 推送 - + Push canceled. 推送已取消。 - + Unable to push to %1 - %2 无法推送到%1-%2 - + Delete Tag 删除标签 - + delete tag 删除标签 @@ -1218,25 +1284,25 @@ DetailView - - + + Author: 作者: - + reset 重置 - + Here you can set the author used for committing These settings will not be saved permanently 在这里,您可以设置用于提交的作者 这些设置不会永久保存 - + Email: 电子邮件: @@ -1244,57 +1310,57 @@ These settings will not be saved permanently DiffPanel - + lines 线 - + System Locale 系统区域设置 - + Wrap lines 自动换行 - + Context lines: 内容行: - + Wrap lines: 自动换行: - + Character Encoding: 字符编码: - + Ignore Whitespace (-w) 忽略空格(-w) - + Added files 添加的文件 - + Deleted files 删除的文件 - + Whitespace: 空格: - + Auto Collapse: 自动收合: @@ -1302,10 +1368,15 @@ These settings will not be saved permanently DiffTool - + External Diff 外部比较 + + + External Diff to Working Copy + 与工作副本的外部差异对比 + DiffTreeModel @@ -1318,12 +1389,12 @@ These settings will not be saved permanently DiffView - + Add new file 追加新文件 - + Or drag files here to copy into the repository 或将文件拖到此处以复制到仓库中 @@ -1331,22 +1402,22 @@ These settings will not be saved permanently DoubleTreeWidget - + Blame 追溯 - + Show Blame Editor 显示追溯编辑器 - + Diff 比较 - + Show Diff View 显示差异视图 @@ -1403,57 +1474,57 @@ These settings will not be saved permanently EditorPanel - + Show whitespace 显示空白字符 - + Tabs 标签页 - + Spaces 空间 - + Show heat map 显示热图 - + Font: 字体: - + Font size: 字体大小: - + Whitespace: 空格: - + Indent using: 缩进使用: - + Indent width: 缩进宽度: - + Tab width: 标签页宽度: - + Blame margin: 追踪边际: @@ -1525,234 +1596,237 @@ These settings will not be saved permanently 所选文件没有%1版本。 - + + Bash Not Found 找不到Bash - + + Bash was not found on your PATH. 在您的PATH上找不到Bash。 - + + Bash is required to execute external tools. Bash是执行外部工具所必需的。 - + Stage 暂存 - + Unstage 取消暂存 - + Discard Changes 放弃更改 - + Discard Changes? 您确定要放弃更改吗? - + Are you sure you want to discard changes in the selected files? 您确定要放弃所选文件中的更改吗? - + This action cannot be undone. 此操作无法撤消。 - + (Submodule) (子模块) - + %1 files %1个文件 - + Discard 丢弃 - + discard 丢弃 - + Remove Untracked Files 删除未跟踪的文件 - + Ignore 忽略 - + Checkout 检出 - + Unlock 解除锁定 - + Lock 锁定 - + Copy File Name 复制文件名 - + Filter History 筛选记录 - + Navigate to 导航至 - + Next Revision 下一个修订 - + next 下一个 - + Previous Revision 上一个修订 - + previous 上一个 - + Unset Executable 未设置可执行文件 - + Set Executable 设置可执行 - + Save Selected Version as ... 将所选版本另存为... - + Select new file directory 选择新文件目录 - + Saving files 正在保存文件 - + Saving files of selected version to disk 将所选版本的文件保存到磁盘上 - + Save file 保存文件 - + Invalid Blob 无效的二进制对象 - + Open this version 打开此版本 - + Opening file 正在打开文件 - + Open 打开 - + open file 打开文件 - + Blob is invalid. 二进制对象是无效的。 - - + + Unable to checkout bare repositories 无法检出裸仓库 - + Unable to open files from bare repository 无法从裸存储库中打开文件 - + The file is already in the current working directory 文件已在当前工作目录中 - + edit 编辑 - + diff 比较 - + merge 合并 - + External Tool Not Found 找不到外部工具 - + Failed to execute external %1 tool. 无法执行外部%1工具。 @@ -1827,107 +1901,107 @@ These settings will not be saved permanently FileWidget - + LFS LFS - - + + Unlock 解除锁定 - - + + Lock 锁定 - - + + Show Object 显示对象 - + Edit File 编辑文件 - + Discard File 丢弃文件 - + Directory 目录 - + File 文件 - + Remove %1? 删除%1? - + Discard Changes? 您确定要放弃更改吗? - + Are you sure you want to remove '%1'? 您确定要删除'%1'吗? - + Are you sure you want to discard all changes in '%1'? 您确定要放弃'%1'中的所有更改吗? - + This action cannot be undone. 此操作无法撤消。 - + Remove %1 删除%1 - + Discard Changes 放弃更改 - + Discard 丢弃 - + discard 丢弃 - - + + Collapse File 收合文件 - - + + Expand File 展开文件 - + Show Pointer 显示指针 @@ -1968,20 +2042,20 @@ These settings will not be saved permanently GeneralPanel - - + + Fetch every 获取每个 - - + + minutes - - + + Push after each commit 每次提交后推送 @@ -1990,18 +2064,18 @@ These settings will not be saved permanently 拉取后更新子模块 - - + + Prune when fetching 获取时清除 - + No translation 没有翻译 - + Store credentials in secure storage 将凭证存储在安全存储中 @@ -2010,60 +2084,85 @@ These settings will not be saved permanently 允许收集使用情况数据 - - + + Update submodules after pull and clone 在拉取和克隆后更新子模块 - + <a href='view'>View privacy policy</a> <a href='view'>查看隐私政策</a> - - + + User name: 用户名: - - + + User email: 用户电子邮件: - - + + Automatic actions: 自动动作: - - + + Language: 语言: - + Credentials: - 证书: + 凭据: - + Credential store type: 凭据存储类型: - + + Available Credential stores: + 可用的凭据存储: + + + Only allow a single running instance 仅允许单个正在运行的实例 - + Single instance: 单个实例: + + + No credential store set + 未设置凭据存储 + + + + No credential store is set. Go to the application settings to set the desired credential store + 尚未设置凭据存储。请前往应用设置选择所需的凭据存储 + + + + + + + + + Available Credential stores + 可用的凭据存储 + Usage reporting: 使用情况报告: @@ -2099,7 +2198,7 @@ These settings will not be saved permanently Header - + Filter %1 筛选%1 @@ -2112,67 +2211,54 @@ These settings will not be saved permanently 连接中 - - HotkeyModel - - - Action - 操作 - - - - Keys - 快捷键 - - HunkWidget - - + + Save 保存 - - + + Undo 撤消 - - + + Use Ours 使用我们的 - - + + Use Theirs 用他们的 - + Edit Hunk 编辑补丁片段 - + Discard Hunk 放弃补丁片段 - + Discard selected lines? 放弃所选行? - + Are you sure you want to discard the changes in hunk from line %1 to %2 in '%3'? 你是否确认要放弃'%3'中从%1到%2行的片段更改? - + Discard selected lines 放弃所选行 @@ -2181,7 +2267,7 @@ These settings will not be saved permanently 您确定要放弃补丁片段吗? - + Are you sure you want to remove '%1'? 您确定要删除'%1'吗? @@ -2190,39 +2276,41 @@ These settings will not be saved permanently 您确定要舍弃'%2'中从行%1开始的块吗? - + This action cannot be undone. 此操作无法撤消。 - - + + Collapse Hunk 收合补丁片段 - - + + Expand Hunk 展开补丁片段 - + Esc 退出 - + Fix 固定 - + Edit 编辑 - + + + No newline at end of file 文件末尾没有换行符 @@ -2279,12 +2367,12 @@ These settings will not be saved permanently LfsPanel - + Initialize LFS 初始化LFS - + Specify a glob pattern for tracking large files. Generally, large files are greater than 500kB, change frequently, @@ -2309,92 +2397,92 @@ Examples - + Pattern: 模式: - + Track 跟踪 - + days - + Fetch LFS objects from all references for the past 从过去的所有引用中获取LFS对象 - + reference days or 参照天数或 - + commit days 提交天数 - + View Environment 查看环境 - + git-lfs env (read only) git-lfs env(只读) - + Deinitialize LFS 取消初始化LFS - + Deinitialize LFS? 您确定要取消初始化LFS吗? - + Are you sure you want uninstall LFS from this repository? 您确定要从此仓库卸载LFS吗? - + Deinitialize 取消初始化 - + Server URL: 服务器地址URL: - + Prune Offset: 清除偏移: - + Fetch Recent: 获取最近: - + Advanced: 高级: - + Included patterns: 包含的规则: - + Excluded patterns: 忽略的规则: @@ -2478,7 +2566,7 @@ Examples LogView - + Copy 复制 @@ -2486,59 +2574,59 @@ Examples MainWindow - + Invalid Git Repository 无效的Git仓库 - + %1 does not contain a valid git repository. %1没有包含有效的git仓库。 - - + + %1 - %2 %1 - %2 - + ahead: %1 前进:%1 - + behind: %1 后面:%1 - + up-to-date 最新 - - + + %1 (%2) %1 (%2) - + MERGING 合并 - + REVERTING 还原 - + CHERRY-PICKING 拣选操作 - + REBASING 变基 @@ -3126,6 +3214,134 @@ Examples Abort %1 中止%1 + + Clone Repository + 克隆仓库 + + + Initialize New Repository + 初始化新仓库 + + + Configure Repository + 配置仓库 + + + Configure Remotes + 配置远程 + + + Configure Branches + 配置分支 + + + Configure Submodules + 配置子模块 + + + Check For Updates + 检查更新 + + + Plugin Documentation + 插件文档 + + + Show Repository Chooser + 显示仓库选择器 + + + Toggle Menu Bar + 切换菜单栏 + + + Toggle Log + 切换日志 + + + Toggle Maximize + 切换最大化 + + + Toggle Tree View + 切换树状视图 + + + Update + 更新 + + + Fetch From + 获取自 + + + Pull From + 拉取自 + + + Push To + 推送至 + + + Checkout + 检出 + + + New + 新建 + + + Rename + 重命名 + + + Pop + 弹出 + + + Stage Selected Lines + 暂存所选行 + + + Unstage Selected Lines + 取消暂存所选行 + + + Discard Selected Changes + 放弃所选更改 + + + Select Next Commit Down + 选择下一条提交(向下) + + + Select Next Commit Up + 选择上一条提交(向上) + + + Move Half Page Down + 向下移动半页 + + + Move Half Page Up + 向上移动半页 + + + Open Terminal + 打开终端 + + + Open File Manager + 打开文件管理器 + + + DiffView + 差异视图 + + + CommitList + 提交列表 + MergeDialog @@ -3199,12 +3415,12 @@ Examples MiscPanel - + Path to SSH config file: SSH配置文件的路径: - + Path to default / fallback SSH key file: 默认/后备SSH密钥文件的路径: @@ -3260,7 +3476,7 @@ Examples Kind - + 类型 @@ -3317,7 +3533,7 @@ Examples PullRequestButton - + Create Pull Request 新建拉取请求 @@ -3373,37 +3589,60 @@ Examples QObject - + + Expand all 全部展开 - + + Collapse all 全部折叠 - + Staged Files 暂存的文件 - + + Unstaged Files 未暂存的文件 - + Committed Files 提交的文件 - + Workdir Files 工作区文件 - + + Single View + 单栏视图 + + + + List View + 列表视图 + + + + Multi Column + 多列视图 + + + + Hide Untracked Files + 隐藏未跟踪文件 + + + Your global GIT configuration is invalid, Gittyup won't run properly until this is fixed 你的全局Git配置无效,修复此问题前Gittyup将无法正常运行 @@ -3466,17 +3705,17 @@ Would you like to fix the merge conflict and continue? ReferenceModel - + Branches 分支 - + Remotes 远程 - + Tags 标签 @@ -3484,57 +3723,57 @@ Would you like to fix the merge conflict and continue? ReferenceView - + Branch 分支 - + Remote 远程 - + Tag 标签 - + Checkout 检出 - + Rename 重命名 - + Delete 删除 - + Push Tag to %1 将标签推送到%1 - + New Local Branch 新建本地分支 - + Merge... 合并... - + Rebase... 变基... - + Squash... 聚合(Squash)... @@ -3542,7 +3781,7 @@ Would you like to fix the merge conflict and continue? RefreshButton - + Refresh 刷新 @@ -3550,7 +3789,7 @@ Would you like to fix the merge conflict and continue? RemoteButton - + 999+ 999+ @@ -3558,59 +3797,59 @@ Would you like to fix the merge conflict and continue? RemoteCallbacks - + remote: %1 远程:%1 - + failed to execute pre-push hook: bash not found 无法执行pre-push钩子:未找到bash - + failed to execute pre-push hook: %1 无法执行pre-push钩子:%1 - + HTTPS Credentials HTTPS凭证 - + SSH Passphrase SSH密码 - + Username: 用户名: - + Password: 密码: - + Passphrase: 密码短语: - - + + authentication canceled 身份验证已取消 - + From %1 从%1 - - + + To %1 至%1 @@ -3762,17 +4001,17 @@ Would you like to fix the merge conflict and continue? RemotesPanel - + Delete Remote? 您确定要删除远程吗? - + Are you sure you want to delete '%1'? 您确定要删除'%1'吗? - + Delete 删除 @@ -3837,12 +4076,12 @@ Would you like to fix the merge conflict and continue? RepoView - + Indexing... 索引... - + Search 搜索 @@ -3851,128 +4090,128 @@ Would you like to fix the merge conflict and continue? 索引器工作进程崩溃。如果此问题仍然存在,请通过support@gitahead.com与我们联系。 - + Indexer Crashed 索引器崩溃 - + Stage Directory? 您确定要暂存目录吗? - + Are you sure you want to stage '%1'? 您确定要暂存'%1'吗? - + This will result in the addition of %1 files. 这将导致添加%1个文件。 - + more than 100 超过100 - + Stage Directory 暂存目录 - + Stop prompting to stage directories 停止提示暂存目录 - + Stage Large File? 暂存大文件? - + Are you sure you want to stage '%1' with a size of %2? 您确定要暂存大小为%2的'%1'吗? - + Stage 暂存 - + Track with LFS 使用LFS跟踪 - + This repository has LFS enabled. Do you want to track the file with LFS instead? 该仓库已启用LFS。您是否要使用LFS跟踪文件? - + Stop prompting to stage large files 停止提示暂存大文件 - + Esc 退出 - + stage 暂存 - + Git LFS was not found on the PATH. <a href='https://git-lfs.github.com'>Install Git LFS</a> to use LFS integration. 在PATH上找不到Git LFS。 <a href='https://git-lfs.github.com'>安装Git LFS </a>以使用LFS集成。 - + untracked file 未跟踪的文件 - + untracked files 未跟踪的文件 - + Remove Untracked Files 删除未跟踪的文件 - + Remove %1 %2? 移除 %1 %2? - + This action cannot be undone. 此操作无法撤消。 - + Remove 删除 - - + + Certificate Error 证书错误 - + SSL verification disabled for this repository 已对此存储库禁用SSL验证 - + [http] sslVerify = false @@ -3983,12 +4222,12 @@ was added to %1/config 已添加到 %1/config - + SSL verification disabled for all git repositories 对所有git存储库禁用SSL验证 - + [http] sslVerify = false @@ -3999,298 +4238,303 @@ was added to %1 已添加到 %1 - + Pull Request 拉取请求 - + Create 新建 - + create pull request 新建拉取请求 - - - + + + Git LFS Git LFS - + Initialize 初始化 - + initialize 初始化 - + Git LFS initialized. Git LFS已初始化。 - + Deinitialize 取消初始化 - + deinitialize 取消初始化 - + Git LFS Deinitialized. Git LFS已初始化。 - + Lock 锁定 - + Unlock 解除锁定 - - + + Unable to %1 '%2' - %3 无法%1'%2'-%3 - + Unable to %1 - %2 无法%1-%2 - + %1 remotes %1个远程 - + Fetch All 获取全部 - + Fetch 获取 - - - + + + <i>no remote</i> <i>没有远程</i> - + Unable to fetch. No upstream is configured for the current branch, and there isn't a remote called 'origin'. 无法获取。当前分支未配置上游,也没有名为“origin”的远程服务器。 - - + + Fetch canceled. 获取已取消。 - + fetch from 获取自 - + You may disable ssl verification <a href='action:sslverifyrepo'>for this repository</a> or overall disable ssl verification <a href='action:sslverifygit'>for all repositories</a>. 你可以针对此仓库禁用SSL验证<a href='action:sslverifyrepo'>for this repository</a>,或对所有仓库全局禁用SSL验证<a href='action:sslverifygit'>for all repositories</a>。 - - - + + + Everything up-to-date. 一切都是最新的。 - - - + + + <i>no branch</i> <i>没有分支</i> - + %1 from %2 来自%2的%1 - + Pull 拉取 - + <i>no upstream</i> <i>没有上游</i> - + Merge 合并 - + %1 into %2 %1到%2 - + Fast-forward 快进 - + %2 to %1 %2至%1 - - + + Rebase 变基 - + %2 on %1 %1上的%2 - + The repository is empty. 该库为空。 - - + + The current branch '%1' has no upstream branch. 当前分支'%1'没有上游分支。 - - + + Already up-to-date. 已经是最新的。 - + Unable to fast-forward. 无法快进。 - + fast-forward 快进 - + You may be able to reconcile your changes with the conflicting files by <a href='action:stash'>stashing</a> before you <a href='%1'>fast-forward</a>. Then <a href='action:unstash'>unstash</a> to restore your changes. 在<a href='%1'>快进</a>之前,您可以通过<a href='action:stash'>备份未提交修改到贮藏区</a>来协调更改与冲突的文件。然后用<a href='action:unstash'>应用备份并从贮藏区删除</a>来恢复您的更改。 - + If you want to create a new merge commit instead of fast-forwarding, you can <a href='%1'>merge without fast-forwarding </a> instead. 如果要创建新的合并提交而不是快进,则可用<a href='%1'>合并而不快进</a>代替。 - - - - + + + + merge 合并 - - + + You may be able to rebase by <a href='action:stash'>stashing</a> before trying to <a href='action:merge'>merge</a>. Then <a href='action:unstash'>unstash</a> to restore your changes. 在尝试<a href='action:mash'>合并</a>之前,您可以通过<a href='action:stash'>备份未提交修改到贮藏区</a>进行调整。然后用<a href='action:unstash'>应用备份并从贮藏区删除</a>来恢复您的更改。 - - - + + + Abort 中止 - + Some merged files have unstaged changes 某些合并的文件具有未暂存的更改 - + abort merge 中止 - - - + + + revert 还原 - - - + + + cherry-pick 拣选操作 - - - + + + rebase 变基 - + Continue ongoing rebase 继续正在进行的变基 - + + + + + + Invalid head. 头部无效。 - + You may be able to rebase by <a href='action:stash'>stashing</a> before trying to <a href='action:rebase'>rebase</a>. Then <a href='action:unstash'>unstash</a> to restore your changes. 在尝试<a href='action:rebase'>变基</a>之前,您可以通过<a href='action:stash'>备份未提交修改到贮藏区</a>进行调整。然后用<a href='action:unstash'>应用备份并从贮藏区删除</a>来恢复您的更改。 - - + + %1/%2 %1/%2 - + %1 - %2 %1 - %2 - + Apply 应用 @@ -4299,29 +4543,29 @@ was added to %1 有合并冲突。 变基已中止 - + %1 - %2 <i>already applied</i> %1 - %2 <i>已应用</i> - + %1 - %2 as %3 %1 - %2作为%3 - - - + + + squash 聚合(Squash) - + Revert 还原 - + Revert "%1" This reverts commit %2. @@ -4330,17 +4574,17 @@ This reverts commit %2. 这将还原提交%2。 - + <i>detached HEAD</i> <i>游离的头</i> - + %1 on %2 %2上的%1 - + Cherry-pick 拣选操作 @@ -4349,99 +4593,99 @@ This reverts commit %2. 强制推送? - + Are you sure you want to force push? 您确定要强制推送吗? - + The remote will lose any commits that are reachable only from the overwritten reference. Dropped commits may be unexpectedly reintroduced by clones that already contain those commits locally. 远程将丢失所有只能从覆盖的引用访问的提交。已在本地包含那些提交的克隆可能会意外地重新引入已删除的提交。 - + Force Push 强制推送 - + <i>no reference</i> <i>没有引用</i> - + Push 推送 - + Push (Force) 推送(强制) - - + + %1 to %2 %1至%2 - - + + You are not currently on a branch. 您目前不在一个分支上。 - + Create a commit to add the default '%1' branch. 新建一个提交以添加默认的'%1'分支。 - + You can <a href='action:checkout'>checkout</a> a branch then <a href='action:push'>push</a> again, or <a href='action:push-to'>push to an explicit branch</a>. 您可以先<a href='action:checkout'>检出</a>分支,然后再次<a href='action:push'>推送</a>,或<a href='action:push-to'>推送到显式分支</a>。 - + The current branch '%1' has no default remote. 当前分支'%1'没有默认的远程分支。 - + You may want to <a href='action:add-remote?name=origin'>add a remote named 'origin'</a>. Then <a href='action:push?set-upstream=true'>push and set the current branch's upstream</a> to begin tracking a remote branch called 'origin/%1'. 您可能想要执行<a href='action:add-remote?name=origin'>添加一个名为“origin”的远程仓库</a>。然后<a href='action:push?set-upstream=true'>推送并设置为当前分支的上游</a>,以开始跟踪名为“origin/%1”的远程分支。 - + You can also <a href='action:push-to'>push to an explicit URL</a> if you don't want to track a remote branch. 如果您不想跟踪远程分支,也可以<a href='action:push-to'>推送到显式URL </a>。 - + To begin tracking a remote branch called '%1', <a href='action:push?set-upstream=true'>push and set the current branch's upstream</a>. 要开始跟踪名为'%1'的远程分支,请<a href='action:push?set-upstream=true'> 推送并设置当前分支的上游</a>。 - + To push without setting up tracking information, <a href='action:push?ref=%1'>push '%2'</a> explicitly. 要在不设置跟踪信息的情况下进行推送,请显式地<a href='action:push?ref=%1'>推送'%2'</a>。 - + Push canceled. 推送已取消。 - + push to 推送到 - + The tag update may cause the remote to lose commits. 标签更新可能会导致远程分支丢失提交。 - + If you want to risk the remote losing commits, you can <a href='action:push?ref=%1&to=%2&force=true'>force push</a>. 如果您想冒远程分支丢失提交的风险,可以<a href='action:push?ref=%1&to=%2&force=true'>强制推送</a>。 @@ -4454,227 +4698,227 @@ This reverts commit %2. 如果您确实希望远程分支丢失提交,则可以<a href='action:push?force=true'>强制推送</a>。 - + Commit? 提交? - + Are you sure you want to commit on a detached HEAD? 您确定要提交到游离的HEAD吗? - + <p>You are in a detached HEAD state. You can still commit, but the new commit will not be reachable from any branch. If you want to commit to an existing branch, checkout the branch first.</p> <p>您处于分离的HEAD状态。您仍然可以提交,但是新的提交将无法从任何分支访问。如果要提交到现有分支,请首先检出该分支。</p> - - + + Commit 提交 - - + + <i>no commit</i> <i>没有提交</i> - + commit 提交 - + This commit was signed with a generated user name and email. 该提交已使用生成的用户名和电子邮件进行了签名。 - + Consider setting the user name and email in <a href='action:config?global=true'>global settings</a>. 请考虑在<a href='action:config?global=true'>全局设置</a>中设置用户名和电子邮件。 - + If you want to limit the name and email settings to this repository, <a href='action:config'>edit repository settings</a> instead. 如果要限制此仓库的名称和电子邮件设置,请<a href='action:config'>编辑仓库设置</a>。 - + After settings have been updated, <a href='action:amend'> amend this commit</a> to record the new user name and email. 设置更新后,<a href='action:amend'>修改此提交</a>以记录新的用户名和电子邮件。 - + file 文件 - + files 多个文件 - + %1 - %2 %3 %1 - %2 %3 - - + + Checkout 检出 - + Checkout Detached HEAD? 检出游离的HEAD? - + Checkout Detached HEAD 检出游离的HEAD - + Checking out remote branch '%1' will result in a detached HEAD state. Do you want to reset the existing local branch '%2' to this commit instead? 检出远程分支'%1'将导致游离的HEAD状态。您是否想将现有的本地分支'%2'重置为此提交? - + Reset Local Branch 重置本地分支 - + Checking out remote branch '%1' will result in a detached HEAD state. Do you want to create a new local branch called '%2' to track it instead? 检出远程分支'%1'将导致游离的HEAD状态。您是否要新建一个名为'%2'的新本地分支来跟踪它? - + Create a local branch to start tracking remote changes and make new commits. Check out the detached HEAD to temporarily put your working directory into the state of the remote branch. 新建本地分支以开始跟踪远程更改并进行新的提交。检出游离的HEAD,以将您的工作目录暂时置于远程分支的状态。 - + Create Local Branch 新建本地分支 - + checkout 检出 - + You may be able to reconcile your changes with the conflicting files by <a href='action:stash'>stashing</a> before you <a href='action:checkout?%1'>checkout '%2'</a>. Then <a href='action:unstash'>unstash</a> to restore your changes. 在执行<a href='action:checkout?%1'>检出'%2'</a>之前,您可以通过<a href='action:stash'>备份未提交修改到贮藏区</a>使协调更改与冲突的文件。然后用<a href='action:unstash'>应用备份并从贮藏区删除</a>来恢复您的更改。 - + New Branch 新建分支 - + create new branch 创建新的分支 - + (no branch) (无分支) - + WIP on %1: %2 %3 WIP 在 %1:%2 %3 - + <i>working directory</i> <i>工作目录</i> - + Stash 贮藏 - + stash 贮藏 - + Apply Stash 应用备份 - + apply stash 应用备份 - + Drop Stash 从贮藏区删除备份 - + drop stash 从贮藏区删除备份 - + Pop Stash 应用备份并从贮藏区删除 - + pop stash 应用备份并从贮藏区删除 - + %1 as %2 %1作为%2 - + Tag 标签 - + tag 标签 - - + + Amend 修改 - + Amending commit %1 修改提交%1 - + %1 to %2 update ref %1至%2 - - - - - - + + + + + + Reset 重置 @@ -4691,166 +4935,166 @@ This reverts commit %2. 您确定要修改'%1'吗? - + Are you sure you want to reset '%1' to '%2'? 您确定要将“%1”重置为“%2”吗? - + <p>Some commits may become unreachable from the current branch.</p> <p>某些提交可能会从当前分支变为不可访问。</p> - + <p>Resetting will cause you to lose uncommitted changes. Untracked and ignored files will not be affected.</p> <p>重置将导致您丢失未提交的更改。未跟踪和忽略的文件不会受到影响。</p> - + <p>Your branch appears to be up-to-date with its upstream branch. Resetting may cause your branch history to diverge from the remote branch history.</p> <p>您的分支似乎是其上游分支的最新信息。重置可能会导致您的分支历史记录与远程分支历史记录有所不同。</p> - - + + amend 修改 - + reset 重置 - - + + %1 of %2 submodules %1个子模块(共%2个) - - + + Update 更新 - - + + update submodule 更新子模块 - + The indexer worker process crashed. If this problem persists please contact us at <TODO: replace.support@gitahead.com>. 索引器工作进程已崩溃。如果此问题持续出现,请通过<TODO: replace.support@gitahead.com>联系我们。 - + Please resolve conflicts before continue 请在继续之前解决冲突 - + Conflict 冲突 - + Rebase finished 变基已完成 - + Force Push to %1? 是否强制推送到%1? - + You may want to integrate remote commits first by <a href='action:pull'>pulling</a>. Then <a href='action:push?to=%1'>push</a> again. 你可以先点击<a href='action:pull'>拉取</a>合并远程提交,然后再次点击 <a href='action:push?to=%1'>推送</a>。 - + If you really want the remote to lose commits, you may be able to <a href='action:push?to=%1&force=true'>force push</a>. 如果你真的想让远程仓库丢失提交记录,也许可以<a href='action:push?to=%1&force=true'>强制推送</a>. - + Reset canceled. 重置已取消。 - + Untouched 未修改 - + Invalid Submodule Repository 无效的子模块仓库 - + The submodule '%1' doesn't have a valid repository. You may need to init and/or update the submodule to check out a repository. 子模块'%1'没有有效的仓库。您可能需要初始化和/或更新子模块以检出仓库。 - + No terminal executable found 未找到终端可执行程序 - + No terminal executable was found. Please configure a terminal in the configuration. 未找到终端可执行文件。请在配置中配置终端。 - + Open Configuration 打开配置 - + There was a merge conflict. 发生合并冲突。 - + Resolve conflicts, then commit to conclude the %1. See <a href='expand'>details</a>. 解决冲突,然后提交以结束%1。请参见<a href='expand'>详细信息</a>。 - + Resolve conflicts in each conflicted (!) file in one of the following ways: 通过以下方式中任意一种来解决每个冲突(!)文件中的冲突: - + 1. Click the 'Ours' or 'Theirs' button to choose the correct change. Then click the 'Save' button to apply. 1.点击“我们的”或“他们的”按钮以选择正确的更改。然后点击“保存”按钮以应用。 - + 2. Edit the file in the editor to make a different change. Remember to remove conflict markers. 2.在编辑器中编辑文件以进行其他更改。记住要删除冲突标记。 - + 3. Use an external merge tool. Right-click on the files in the list and choose 'External Merge'. 3.使用外部合并工具。右键单击列表中的文件,然后选择“外部合并”。 - + After all conflicts in the file are resolved, click the check box to mark it as resolved. 解决文件中的所有冲突后,单击复选框将其标记为已解决。 - + After all conflicted files are staged, commit to conclude the %1. 暂存所有有冲突的文件后,提交以结束%1。 - + You can <a href='action:abort'>abort</a> the %1 to return the repository to its previous state. 您可以<a href='action:abort'>中止</a>%1以便将仓库恢复到之前的状态。 @@ -4858,12 +5102,12 @@ This reverts commit %2. Repository - + Unknown error 未知错误 - + git-lfs not found 找不到git-lfs @@ -4879,32 +5123,32 @@ This reverts commit %2. SearchPanel - + Enable indexing 启用索引 - + terms 条款 - + lines - + Limit commits to: 限制提交到: - + Diff context: 差异内容: - + Remove Index 删除索引 @@ -4912,32 +5156,32 @@ This reverts commit %2. Settings - + Prompt to edit stash message before stashing 在执行贮存操作前提示编辑贮存消息 - + Prompt to edit commit message before merging 合并前提示编辑提交消息 - + Prompt to edit commit message before reverting 还原前提示编辑提交消息 - + Prompt to edit commit message before cherry-picking 在拣选操作之前提示编辑提交消息 - + Prompt to stage directories 提示暂存目录 - + Prompt to stage large files 提示暂存大文件 @@ -4945,67 +5189,67 @@ This reverts commit %2. SettingsDialog - + Esc 退出 - + Global git settings can be overridden for each repository in the corresponding repository configuration page. 可以在相应的仓库配置页面中为每个仓库覆盖全局git设置。 - + Edit Config File... 编辑配置文件... - + General 一般 - + Diff 比较 - + Tools 工具 - + Window 窗口 - + Editor 编辑器 - + Update 更新 - + Plugins 插件 - + Misc 其他 - + Hotkeys 快捷键 - + Terminal 终端 @@ -5036,89 +5280,89 @@ This reverts commit %2. SideBar - + Close 关闭 - - - + + + Remove 删除 - + Authorize 授权 - + Clone Repository 克隆仓库 - + Open Existing Repository 打开现有仓库 - + Open Repository 打开仓库 - + Initialize New Repository 初始化新仓库 - + Add %1 Account 添加%1帐户 - + <p>Are you sure you want to remove the remote repository association for %1?</p><p>The local clone itself will not be affected.</p> <p>确定要删除%1的远程仓库关联吗?</p><p>本地克隆本身不会受到影响。</p> - + Remove Repository Association? 删除仓库关联? - + Clear All Recent 清除所有最近的 - + Show Full Path 显示完整路径 - + Filter Non-existent Paths 过滤不存在​​的路径 - + Refresh Remote Accounts 刷新远程帐户 - + Show Full Name 显示全名 - + <p>Are you sure you want to remove the %1 account for '%2'?</p><p>Only the account association will be removed. Remote configurations and local clones will not be affected.</p> <p>确定要删除“%2”的%1个帐户吗?</p><p>仅会删除帐户关联。远程配置和本地克隆将不受影响。</p> - + Remove Account? 删除帐户? @@ -5265,12 +5509,12 @@ This reverts commit %2. 取消初始化子模块? - + The submodule working directory contains uncommitted changes that will be lost if you continue. 子模块工作目录包含未提交的更改,如果您继续,这些更改将丢失。 - + Deinitialize 取消初始化 @@ -5393,23 +5637,23 @@ x (number) determines the number of maximum files shown TerminalPanel - - + + Install 安装 - + Name: 名称: - + Location: 位置: - + Uninstall 卸载 @@ -5417,32 +5661,32 @@ x (number) determines the number of maximum files shown TextEdit - + Replace... 替换... - + Replace All... 替换全部... - + Ignore 忽略 - + Ignore All 忽略全部 - + Add to User Dictionary 添加到用户字典 - + Do not Ignore 不忽略 @@ -5492,93 +5736,93 @@ x (number) determines the number of maximum files shown ToolBar - + Show repository sidebar 显示仓库侧栏 - + Previous 退回 - + Next 前进 - + Fetch 获取 - + Pull 拉取 - + Merge 合并 - + Rebase 变基 - + Push 推送 - + Checkout 检出 - + Stash 备份未提交修改到贮藏区 - + Pop Stash 应用备份并从贮藏区删除 - + Open Terminal 打开终端 - + Open file manager 打开文件管理器 - + Configure Settings 配置设定 - + Repository settings 存储库设置 - + Application settings 应用程序设置 - - + + Show Log 显示日志 - + Double Tree View 双树状视图 @@ -5587,17 +5831,17 @@ x (number) determines the number of maximum files shown 差异视图 - + Tree View 树状视图 - + Show Starred Commits 显示加注星标的提交 - + Hide Log 隐藏日志 @@ -5605,42 +5849,42 @@ x (number) determines the number of maximum files shown ToolsPanel - + Keep backup of merge files (.orig) 保留合并文件(.orig)的备份 - + External editor: 外部编辑器: - + External diff: 外部差异比较: - + External merge: 外部合并: - + Backup files: 备份文件: - + Terminal emulator command: 终端模拟器命令: - + File manager command: 文件管理器命令: - + Configure 配置 @@ -5780,22 +6024,22 @@ x (number) determines the number of maximum files shown UpdatePanel - + Check for updates automatically 自动检查更新 - + Automatically download and install updates 自动下载并安装更新 - + Check Now 现在检查 - + Software Update: 软件更新: @@ -5826,24 +6070,20 @@ x (number) determines the number of maximum files shown 安装程序无法启动 - The disk image failed to mount successfully - 磁盘映像安装失败 + 磁盘映像安装失败 - The existing bundle could not be moved to the trash - 现有的包无法移至垃圾箱 + 现有的包无法移至垃圾箱 - The new bundle could not be copied into place - 新的包无法复制到位 + 新的包无法复制到位 - - - + + Helper application failed to start 帮助应用程序无法启动 @@ -5858,22 +6098,22 @@ x (number) determines the number of maximum files shown 无法检查更新 - + Unable to download update 无法下载更新 - + Unable to open temporary file 无法打开临时文件 - + Unable to install update 无法安装更新 - + Some windows failed to close. You can download the binary manually from %1 一些窗口无法关闭。您可以从%1手动下载二进制文件 @@ -5882,13 +6122,13 @@ x (number) determines the number of maximum files shown 一些窗口无法关闭 - + Unknown install error 未知的安装错误 - - + + Installer script failed: %1 安装脚本失败:%1 @@ -5896,112 +6136,112 @@ x (number) determines the number of maximum files shown WindowPanel - + Add New Theme 添加新主题 - + Edit Current Theme 编辑当前主题 - + Create Theme 创建主题 - + Theme Name 主题名称 - + Restart? 重启? - + The application must be restarted for the theme change to take effect. 必须重启应用程序,主题更改才能生效。 - + Do you want to restart now? 您要立即重启吗? - + Restart 重启 - + Later 推后 - + Show full repository path 显示完整的仓库路径 - + Hide automatically 自动隐藏 - + Open submodules in tabs 在标签页中打开子模块 - + Open all repositories in tabs 在标签页中打开所有仓库 - + Hide Menubar 隐藏菜单栏 - + Show Avatars 显示头像 - + Show Window Maximized when opened 打开时最大化显示窗口 - + Theme: 主题: - + Title: 标题: - + Log: 日志: - + Tabs: 标签页: - + View: 查看: - + Prompt: 提示: @@ -6009,31 +6249,491 @@ x (number) determines the number of maximum files shown _FileWidget::Header - - + + Use Theirs: Delete 以他们的删除操作为准 - - + + Use Ours: Delete 以我们的删除操作为准 - + both: %1 两者:%1 - + ours: %1 我们的:%1 - + theirs: %1 他们的:%1 + + HotkeyModel + + Action + 动作 + + + Keys + 快捷键 + + + + File + 文件 + + + Edit + 编辑 + + + View + 视图 + + + Repository + 仓库 + + + Remote + 远程 + + + Branch + 分支 + + + Stash + 贮藏 + + + History + 历史 + + + Window + 窗口 + + + Tools + 工具 + + + DiffView + 差异视图 + + + CommitList + 提交列表 + + + + New File + 新建文件 + + + New Window + 新建窗口 + + + Clone Repository + 克隆仓库 + + + Initialize New Repository + 初始化新仓库 + + + Open Repository + 打开仓库 + + + Close + 关闭 + + + Save + 保存 + + + Exit + 退出 + + + Undo + 撤销 + + + Redo + 重做 + + + Cut + 剪切 + + + Copy + 复制 + + + Paste + 粘贴 + + + Select All + 全选 + + + Find + 查找 + + + Find Next + 查找下一个 + + + Find Previous + 查找上一个 + + + Use Selection for Find + 使用所选内容查找 + + + Refresh + 刷新 + + + Toggle Log + 切换日志 + + + Toggle Maximize + 切换最大化 + + + Toggle Tree View + 切换树状视图 + + + Toggle Menu Bar + 切换菜单栏 + + + Configure Repository + 配置仓库 + + + Stage All + 全部暂存 + + + Unstage All + 全部取消暂存 + + + Commit + 提交 + + + Amend Commit + 修补提交 + + + Remove all LFS locks + 移除所有 LFS 锁定 + + + Initialize LFS + 初始化 LFS + + + Configure + 配置 + + + Fetch + 获取 + + + Fetch All + 获取全部 + + + Fetch From + 从…获取 + + + Pull + 拉取 + + + Pull From + 从…拉取 + + + Push + 推送 + + + Push To + 推送到… + + + New + 新建 + + + Rename + 重命名 + + + Checkout Current + 检出当前 + + + Checkout + 检出 + + + Merge + 合并 + + + Rebase + 变基 + + + Abort Merge + 中止合并 + + + Update All + 全部更新 + + + Update + 更新 + + + Show Stashes + 显示贮藏 + + + Pop + 弹出 + + + Back + 后退 + + + Forward + 前进 + + + Show Previous Tab + 显示上一个标签页 + + + Show Next Tab + 显示下一个标签页 + + + Show Repository Chooser + 显示仓库选择器 + + + Options + 选项 + + + Stage Selected Lines + 暂存所选行 + + + Unstage Selected Lines + 取消暂存所选行 + + + Discard Selected Changes + 放弃所选更改 + + + Open Terminal + 打开终端 + + + Open File Manager + 打开文件管理器 + + + Move Half Page Down + 向下移动半页 + + + Move Half Page Up + 向上移动半页 + + + Select Next Commit Down + 选择下一条提交 + + + Select Next Commit Up + 选择上一条提交 + + + + Plugins + + Banned words file + 禁用词文件 + + + Comment + 注释 + + + banned word used in comment + 注释中使用了禁用词 + + + Banned words are not allowed in comments. + 注释中不允许使用禁用词。 + + + String + 字符串 + + + banned word used in string literal + 字符串字面量中使用了禁用词 + + + Banned words are not allowed in string literals. + 字符串字面量中不允许使用禁用词。 + + + Identifier + 标识符 + + + banned word used as identifier + 标识符中使用了禁用词 + + + Banned words are not allowed as identifiers. + 标识符中不允许使用禁用词。 + + + Unfinished keywords + 未完成关键字 + + + Unfinished + 未完成 + + + unfinished comment keyword added + 注释中添加了未完成关键字 + + + Flag comments that contain 'TODO', 'FIXME', 'TBD', etc. + 标记包含 'TODO'、'FIXME'、'TBD' 等的注释。 + + + Soft limit + 软限制 + + + Hard limit + 硬限制 + + + Soft Limit + 软限制 + + + line exceeds length limit + 行超出长度限制 + + + Lines should not extend past column %1. + 行不应超过第 %1 列。 + + + Hard Limit + 硬限制 + + + Lines must not extend past column %1. + 行不得超出第 %1 列。 + + + In indentation + 缩进中 + + + Everywhere + 任意位置 + + + Disallow tabs + 禁止制表符 + + + Ignore blank lines + 忽略空行 + + + Tabs + 制表符 + + + tab character added + 添加了制表符 + + + Tab characters are not allowed. + 不允许使用制表符。 + + + Trailing + 行尾 + + + trailing whitespace added + 行尾添加了空白字符 + + + Trailing whitespace is not allowed. + 不允许行尾出现空白字符。 + + + Banned + 禁用词 + + + Comments + 注释 + + + Line Length + 行长度 + + + Whitespace + 空白字符 + + diff --git a/pack/CMakeLists.txt b/pack/CMakeLists.txt index da279bb9f..1c7f1e108 100644 --- a/pack/CMakeLists.txt +++ b/pack/CMakeLists.txt @@ -14,13 +14,20 @@ if(WIN32) endif() # Install Qt plugins. -set(QT_PLUGINS QJpegPlugin) +# QJpegPlugin is needed for image thumbnails; the SVG/ICO/GIF imageformat +# plugins and the SVG icon engine are required for the UI to render correctly. +set(QT_PLUGINS QJpegPlugin QSvgPlugin QICOPlugin QGifPlugin QSvgIconPlugin) + +# Qt 6 moved the SSL/TLS implementation out of QtNetwork into a plugin. Without +# it every https request fails with "TLS initialization failed", which breaks +# the update check and the remote hosting accounts. +set(QT_PLUGINS ${QT_PLUGINS} QTlsBackendOpenSSLPlugin QTlsBackendCertOnlyPlugin) if(APPLE) set(QT_PLUGINS ${QT_PLUGINS} QCocoaIntegrationPlugin QMacStylePlugin) elseif(GITTYUP_PACK_WIN64) set(QT_PLUGINS ${QT_PLUGINS} QWindowsIntegrationPlugin - QModernWindowsStylePlugin) + QModernWindowsStylePlugin QSchannelBackendPlugin) else() set(QT_PLUGINS ${QT_PLUGINS} QXcbIntegrationPlugin QComposePlatformInputContextPlugin) @@ -36,7 +43,7 @@ endif() if(FLATPAK) qt_import_plugins(gittyup INCLUDE ${QT_PLUGINS}) -elseif(NOT USE_SYSTEM_QT) +elseif(GITTYUP_PACK_WIN64 OR NOT USE_SYSTEM_QT) foreach(QT_PLUGIN ${QT_PLUGINS}) if(NOT TARGET Qt6::${QT_PLUGIN}) continue() @@ -144,7 +151,53 @@ endif() # Install SSL libraries. if(NOT APPLE) - if(NOT USE_SYSTEM_OPENSSL) + # On Windows the installer must be self-contained. Qt's OpenSSL TLS backend + # loads "libssl-3-x64.dll" / "libcrypto-3-x64.dll" at runtime; the system + # OpenSSL install is frequently a *different* major version (e.g. 4.x) which Qt + # will not load, so we bundle the 3.x DLLs that ship with the openssl submodule. + if(GITTYUP_PACK_WIN64) + set(_openssl_src "${CMAKE_SOURCE_DIR}/dep/openssl/openssl") + foreach(_ssl_name libssl-3-x64 libcrypto-3-x64) + set(_ssl_dll "${_openssl_src}/${_ssl_name}.dll") + if(EXISTS "${_ssl_dll}") + # Copy into build dir. + execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_ssl_dll}" + ${CMAKE_BINARY_DIR}) + install( + FILES "${_ssl_dll}" + DESTINATION ${INSTALL_LIBDIR} + PERMISSIONS + OWNER_READ + OWNER_WRITE + OWNER_EXECUTE + GROUP_READ + GROUP_EXECUTE + WORLD_READ + WORLD_EXECUTE + COMPONENT ${GITTYUP_NAME}) + endif() + endforeach() + + # Qt support libraries required by the GUI (D3D/ANGLE software fallback). + get_filename_component(_qt_bin_dir "${Qt6_DIR}/../../../bin" ABSOLUTE) + foreach(_qt_support d3dcompiler_47 opengl32sw dxcompiler dxil) + set(_support_dll "${_qt_bin_dir}/${_qt_support}.dll") + if(EXISTS "${_support_dll}") + install( + FILES "${_support_dll}" + DESTINATION ${INSTALL_LIBDIR} + PERMISSIONS + OWNER_READ + OWNER_WRITE + OWNER_EXECUTE + GROUP_READ + GROUP_EXECUTE + WORLD_READ + WORLD_EXECUTE + COMPONENT ${GITTYUP_NAME}) + endif() + endforeach() + elseif(NOT USE_SYSTEM_OPENSSL) if(GITTYUP_PACK_WIN64) set(SSL_LIB_SUFFIX "-3-x64.dll") else() @@ -175,22 +228,23 @@ if(NOT APPLE) # libssh2 (submodule) installs its DLL without COMPONENT ${GITTYUP_NAME}; # CPack only installs that component, so ship the DLL from Gittyup's pack - # rules. - if(GITTYUP_PACK_WIN64 - AND NOT USE_SYSTEM_LIBSSH2 - AND TARGET libssh2_shared) - install( - FILES $ - DESTINATION ${INSTALL_LIBDIR} - PERMISSIONS - OWNER_READ - OWNER_WRITE - OWNER_EXECUTE - GROUP_READ - GROUP_EXECUTE - WORLD_READ - WORLD_EXECUTE - COMPONENT ${GITTYUP_NAME}) + # rules. The imported target exists for both the system and the bundled build. + if(GITTYUP_PACK_WIN64 AND TARGET libssh2::libssh2) + get_target_property(_libssh2_dll libssh2::libssh2 LOCATION) + if(_libssh2_dll AND EXISTS "${_libssh2_dll}") + install( + FILES "${_libssh2_dll}" + DESTINATION ${INSTALL_LIBDIR} + PERMISSIONS + OWNER_READ + OWNER_WRITE + OWNER_EXECUTE + GROUP_READ + GROUP_EXECUTE + WORLD_READ + WORLD_EXECUTE + COMPONENT ${GITTYUP_NAME}) + endif() endif() if(GITTYUP_PACK_WIN64 OR APPLE) @@ -241,8 +295,14 @@ elseif(GITTYUP_PACK_WIN64) set(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut \ \\\"$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${GITTYUP_NAME}.lnk\\\" \ + \\\"$INSTDIR\\\\${CMAKE_INSTALL_BINDIR}\\\\${GITTYUP_EXECUTABLE_NAME}.exe\\\" + CreateShortCut \ + \\\"$DESKTOP\\\\${GITTYUP_NAME}.lnk\\\" \ \\\"$INSTDIR\\\\${CMAKE_INSTALL_BINDIR}\\\\${GITTYUP_EXECUTABLE_NAME}.exe\\\"" ) + # Remove the desktop shortcut on uninstall so it does not linger. + set(CPACK_NSIS_DELETE_ICONS_EXTRA + "Delete \\\"$DESKTOP\\\\${GITTYUP_NAME}.lnk\\\"") set(CPACK_NSIS_EXECUTABLES_DIRECTORY ${CMAKE_INSTALL_BINDIR}) set(CPACK_NSIS_MUI_FINISHPAGE_RUN ${GITTYUP_EXECUTABLE_NAME}.exe) diff --git a/src/app/Application.cpp b/src/app/Application.cpp index 4c0e0646e..ea534d8a3 100644 --- a/src/app/Application.cpp +++ b/src/app/Application.cpp @@ -39,6 +39,15 @@ bool Application::mIsInTest = false; +// Used to detect whether the process was started from an interactive +// terminal. See isLaunchedFromTerminal() below. +#include +#if defined(Q_OS_WIN) +#include +#else +#include +#endif + #if defined(Q_OS_LINUX) #include @@ -282,39 +291,57 @@ void Application::autoUpdate() { } } -bool Application::restoreWindows() { -#ifdef Q_OS_MAC - // Check for connection to a terminal. - if (!isatty(fileno(stdin))) - QDir::setCurrent(Settings::appDir().path()); +namespace { + +// Returns true if the process is attached to an interactive terminal. +// +// Only in that case may the current working directory be interpreted as the +// repository to open. GUI launches (desktop or start menu shortcut, file +// association, shell context menu, "Open with Gittyup", ...) inherit an +// essentially arbitrary working directory, which must not be mistaken for a +// command line invocation. +bool isLaunchedFromTerminal() { +#if defined(Q_OS_WIN) + return _isatty(_fileno(stdin)); +#else + return isatty(fileno(stdin)); #endif +} + +} // namespace + +bool Application::restoreWindows() { + // Distinguish two different app modes. Normal mode restores the session + // from the last normal mode invocation and saves it again on exit. Command + // line mode loads a single repository given on the command line (or, when + // started from a terminal, taken from the working directory). Windows + // opened in command line mode aren't saved to settings. + // FIXME: Save subsequently opened windows? QDir dir = QDir::current(); - if (!mPositionalArguments.isEmpty()) { + bool explicitRepo = !mPositionalArguments.isEmpty(); + if (explicitRepo) { // Check for command line repo. QString arg = mPositionalArguments.first(); - if (QFileInfo(arg).isAbsolute()) { + if (QFileInfo(arg).isAbsolute() || !dir.cd(arg)) dir.setPath(arg); - } else { - dir.cd(arg); - } } - // Distinguish two different app modes. The normal mode is entered when the - // current directory is the same as the app directory (e.g. because the user - // launched the app by double-clicking in the GUI). It restores the session - // from the last normal mode invocation. Command line mode tries to load a - // repository from the current directory. Windows opened in this mode aren't - // saved to settings. FIXME: Save subsequently opened windows? - - // Load command line arg. - if (dir != Settings::appDir()) { + // Never derive the repository from the working directory unless Gittyup was + // actually started from a terminal. Otherwise launching from a shortcut or + // from the shell context menu would silently skip session restore (and + // session saving), making the previously opened repositories disappear. + bool useWorkingDir = + !explicitRepo && isLaunchedFromTerminal() && dir != Settings::appDir(); + + // Load command line arg. Fall through to normal mode when it doesn't point + // at a valid repository, e.g. after "Open with Gittyup" on a plain folder. + if ((explicitRepo || useWorkingDir) && + git::Repository::open(dir.path(), true).isValid()) { if (MainWindow *win = MainWindow::open(dir.path(), false)) { win->currentView()->setPathspec(mPathspec); return true; } - - return false; } // Save on exit. diff --git a/src/cred/CredentialHelper.h b/src/cred/CredentialHelper.h index 4096f1eb2..d68aaa99b 100644 --- a/src/cred/CredentialHelper.h +++ b/src/cred/CredentialHelper.h @@ -17,6 +17,7 @@ // It is used for both authenticating git remote connections and // securely storing passwords associated with host accounts. class CredentialHelper : public QObject { + Q_OBJECT public: // Username can be supplied by the caller to lookup a specific // account or filled in by the helper to get the first account diff --git a/src/dialogs/HotkeysPanel.cpp b/src/dialogs/HotkeysPanel.cpp index a31d98fc6..91972c8ad 100644 --- a/src/dialogs/HotkeysPanel.cpp +++ b/src/dialogs/HotkeysPanel.cpp @@ -10,6 +10,7 @@ #include "HotkeysPanel.h" #include "ui/HotkeyManager.h" #include +#include #include #include #include @@ -263,10 +264,10 @@ class HotkeyModel : public QAbstractItemModel { switch ((ColumnIndex)section) { case ColumnIndex::Label: - return QVariant(tr("Action")); + return QVariant(QCoreApplication::translate("HotkeyModel", "Action")); case ColumnIndex::Keys: - return QVariant(tr("Keys")); + return QVariant(QCoreApplication::translate("HotkeyModel", "Keys")); } return QVariant(); @@ -399,8 +400,10 @@ HotkeysPanel::HotkeysPanel(QWidget *parent) : QTreeView(parent) { int subPos = path.lastIndexOf('/'); - group = - new HotkeyGroupData(group, tr(path.mid(subPos + 1).toUtf8()), group); + group = new HotkeyGroupData( + group, QCoreApplication::translate( + "HotkeyModel", path.mid(subPos + 1).toUtf8().constData()), + group); group->group()->addChildData(group); Q_ASSERT(!groups.contains(path)); @@ -409,7 +412,10 @@ HotkeysPanel::HotkeysPanel(QWidget *parent) : QTreeView(parent) { // Add hotkey to group group->addChildData(new HotkeyKeyData( - group, tr(label.mid(lastSep + 1).toUtf8()), hotkey, manager)); + group, + QCoreApplication::translate("HotkeyModel", + label.mid(lastSep + 1).toUtf8().constData()), + hotkey, manager)); } setModel(new HotkeyModel(this, root)); diff --git a/src/dialogs/PluginsPanel.cpp b/src/dialogs/PluginsPanel.cpp index 80231abeb..3c45c8e0d 100644 --- a/src/dialogs/PluginsPanel.cpp +++ b/src/dialogs/PluginsPanel.cpp @@ -19,6 +19,7 @@ #include #include #include +#include PluginsPanel::PluginsPanel(const git::Repository &repo, QWidget *parent) : QTreeWidget(parent), mRepo(repo) { @@ -43,7 +44,9 @@ void PluginsPanel::refresh() { bold.setBold(true); foreach (PluginRef plugin, Plugin::plugins(mRepo)) { - QTreeWidgetItem *root = new QTreeWidgetItem(this, {plugin->name()}); + QTreeWidgetItem *root = new QTreeWidgetItem( + this, {QCoreApplication::translate( + "Plugins", plugin->name().toUtf8().constData())}); root->setData(Name, Qt::UserRole, QVariant::fromValue(plugin)); root->setFont(Name, bold); @@ -68,7 +71,8 @@ void PluginsPanel::refresh() { button->setEnabled(!keys.isEmpty()); connect(button, &QPushButton::clicked, [this, plugin, keys] { QDialog dialog; - dialog.setWindowTitle(tr("%1 Options").arg(plugin->name())); + dialog.setWindowTitle(tr("%1 Options").arg(QCoreApplication::translate( + "Plugins", plugin->name().toUtf8().constData()))); QFormLayout *layout = new QFormLayout(&dialog); foreach (const QString &key, keys) { diff --git a/src/dialogs/SettingsDialog.cpp b/src/dialogs/SettingsDialog.cpp index 786f7a058..486ea1067 100644 --- a/src/dialogs/SettingsDialog.cpp +++ b/src/dialogs/SettingsDialog.cpp @@ -126,6 +126,8 @@ class GeneralPanel : public QWidget { [] { AboutDialog::openSharedInstance(AboutDialog::Privacy); }); mCredentialStoresDescription = new QLabel(); + // Allow the hyperlinks in the credential store descriptions to be clicked. + mCredentialStoresDescription->setOpenExternalLinks(true); QFormLayout *form = new QFormLayout; form->addRow(tr("User name:"), mName); @@ -209,7 +211,9 @@ class GeneralPanel : public QWidget { git::Config config = git::Config::global(); mAvailableStores->setEnabled(checked); if (checked) { - auto store = mAvailableStores->currentText(); + QVariant data = mAvailableStores->currentData(); + auto store = + data.isValid() ? data.toString() : mAvailableStores->currentText(); config.setValue("credential.helper", store); } else { config.remove("credential.helper"); @@ -219,9 +223,11 @@ class GeneralPanel : public QWidget { }); connect(mAvailableStores, &QComboBox::currentTextChanged, - [](const QString &text) { + [this](const QString &text) { git::Config config = git::Config::global(); - config.setValue("credential.helper", text); + QVariant data = mAvailableStores->currentData(); + config.setValue("credential.helper", + data.isValid() ? data.toString() : text); delete CredentialHelper::instance(); }); @@ -276,16 +282,24 @@ class GeneralPanel : public QWidget { QString info = tr("") + ""; for (const auto &helper : CredentialHelper::getAvailableHelperInformation()) { + QString displayName = QCoreApplication::translate( + "CredentialHelper", helper.name.toUtf8().constData()); info += QStringLiteral("
%1%2") - .arg(helper.name, helper.description); - mAvailableStores->addItem(helper.name); + .arg(displayName, helper.description); + // Store the real git helper identifier in userData so that the + // config value stays untranslated while the UI shows a translation. + mAvailableStores->addItem(displayName, helper.name); } info += "
"; mCredentialStoresDescription->setText(info); mAvailableStores->setToolTip(tr("Available Credential stores")); mAvailableStores->setWhatsThis(info); mAvailableStores->setEditable(true); - mAvailableStores->setCurrentText(currentHelper); + int idx = mAvailableStores->findData(currentHelper); + if (idx >= 0) + mAvailableStores->setCurrentIndex(idx); + else + mAvailableStores->setCurrentText(currentHelper); mSingleInstance->setChecked( settings->value(Setting::Id::AllowSingleInstanceOnly).toBool()); @@ -875,8 +889,6 @@ SettingsDialog::SettingsDialog(Index index, QWidget *parent) // Create central stack widget. StackedWidget *stack = new StackedWidget(this); - connect(stack, &StackedWidget::currentChanged, this, - &SettingsDialog::adjustSize); QString text = tr("Global git settings can be overridden for each repository in " @@ -1020,6 +1032,16 @@ SettingsDialog::SettingsDialog(Index index, QWidget *parent) // Select the requested index. actions->actions().at(index)->trigger(); + // Keep the window from resizing when switching panels: reserve space for + // the largest panel so every tab fits without changing the window size. + QSize max; + for (int i = 0; i < stack->count(); ++i) { + QSize sh = stack->widget(i)->sizeHint(); + max.setWidth(qMax(max.width(), sh.width())); + max.setHeight(qMax(max.height(), sh.height())); + } + stack->setMinimumSize(max); + setMinimumWidth(toolbar->sizeHint().width()); } diff --git a/src/git/CMakeLists.txt b/src/git/CMakeLists.txt index a8638b32a..99d9e3ad0 100644 --- a/src/git/CMakeLists.txt +++ b/src/git/CMakeLists.txt @@ -37,4 +37,15 @@ else() Qt6::Network) endif() +# Remote.cpp includes directly, so this target needs libssh2's +# include directory (it is not always propagated transitively through libgit2). +# Prefer the imported target when available; fall back to the variables set by +# the bundled/FindPkgLibraries paths. +if(TARGET libssh2::libssh2) + target_link_libraries(git libssh2::libssh2) +else() + target_include_directories(git PRIVATE ${LIBSSH2_INCLUDE_DIRS}) + target_link_libraries(git ${LIBSSH2_LIBRARIES}) +endif() + set_target_properties(git PROPERTIES AUTOMOC ON) diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp index b6bb50505..e3bc0501b 100644 --- a/src/plugins/Plugin.cpp +++ b/src/plugins/Plugin.cpp @@ -600,7 +600,8 @@ void Plugin::setOptionValue(const QString &key, const QVariant &value) { QStringList Plugin::optionKeys() const { return mOptions.keys(); } QString Plugin::optionText(const QString &key) const { - return mOptions.value(key).text; + return QCoreApplication::translate( + "Plugins", mOptions.value(key).text.toUtf8().constData()); } QVariant Plugin::optionValue(const QString &key) const { @@ -625,7 +626,10 @@ Plugin::OptionKind Plugin::optionKind(const QString &key) const { } QStringList Plugin::optionOpts(const QString &key) const { - return mOptions.value(key).opts; + QStringList opts = mOptions.value(key).opts; + for (QString &opt : opts) + opt = QCoreApplication::translate("Plugins", opt.toUtf8().constData()); + return opts; } void Plugin::defineDiagnostic(const QString &key, DiagnosticKind kind, @@ -647,15 +651,23 @@ Plugin::DiagnosticKind Plugin::diagnosticKind(const QString &key) const { } QString Plugin::diagnosticName(const QString &key) const { - return mDiagnostics.value(key).name; + return QCoreApplication::translate( + "Plugins", mDiagnostics.value(key).name.toUtf8().constData()); } QString Plugin::diagnosticMessage(const QString &key) const { - return mDiagnostics.value(key).message; + return QCoreApplication::translate( + "Plugins", mDiagnostics.value(key).message.toUtf8().constData()); } QString Plugin::diagnosticDescription(const QString &key) const { - return mDiagnostics.value(key).description; + QString translated = QCoreApplication::translate( + "Plugins", mDiagnostics.value(key).description.toUtf8().constData()); + // Replace %1 with the option value when the diagnostic key names an option + // (e.g. the soft/hard line-length limits, defined with a %1 placeholder). + if (translated.contains("%1") && mOptions.contains(key)) + translated = translated.arg(optionValue(key).toInt()); + return translated; } bool Plugin::hunk(TextEditor *editor) const { diff --git a/src/ui/DiffView/HunkWidget.cpp b/src/ui/DiffView/HunkWidget.cpp index 8224004b2..40c4e5db1 100644 --- a/src/ui/DiffView/HunkWidget.cpp +++ b/src/ui/DiffView/HunkWidget.cpp @@ -31,8 +31,6 @@ namespace { bool disclosure = false; -const QString noNewLineAtEndOfFile = - HunkWidget::tr("No newline at end of file"); } // namespace _HunkWidget::HunkLabel::HunkLabel(const QString &name, bool submodule, @@ -1092,7 +1090,7 @@ void HunkWidget::createMarkersAndLineNumbers(const Line &line, int lidx, // Build annotations. QList annotations; if (!line.newline()) { - QString text = noNewLineAtEndOfFile; + QString text = HunkWidget::tr("No newline at end of file"); QByteArray styles = QByteArray(text.toUtf8().size(), TextEditor::EofNewline); annotations.append({text, styles}); @@ -1189,7 +1187,7 @@ QByteArray HunkWidget::hunk() const { } if (appended && mEditor->annotationLines(i) > 0 && - mEditor->annotationText(i) == noNewLineAtEndOfFile) { + mEditor->annotationText(i) == HunkWidget::tr("No newline at end of file")) { ar.remove(ar.length() - 1, 1); // remove new line } } @@ -1221,7 +1219,7 @@ QByteArray HunkWidget::apply() { } if (appended && mEditor->annotationLines(i) > 0 && - mEditor->annotationText(i) == noNewLineAtEndOfFile) { + mEditor->annotationText(i) == HunkWidget::tr("No newline at end of file")) { ar.remove(ar.length() - 1, 1); // remove new line } } diff --git a/src/ui/DoubleTreeWidget.h b/src/ui/DoubleTreeWidget.h index 57cc246d9..a386d552b 100644 --- a/src/ui/DoubleTreeWidget.h +++ b/src/ui/DoubleTreeWidget.h @@ -70,7 +70,7 @@ private slots: void loadEditorContent(const QModelIndexList &indexes); void toggleCollapseStagedFiles(); void toggleCollapseUnstagedFiles(); - QAction *setupAppearanceAction(const char *name, Setting::Id id, + QAction *setupAppearanceAction(const QString &text, Setting::Id id, bool defaultValue = false); DiffTreeModel *mDiffTreeModel{nullptr}; diff --git a/src/ui/RepoView.cpp b/src/ui/RepoView.cpp index 547ecddff..81d437cbb 100644 --- a/src/ui/RepoView.cpp +++ b/src/ui/RepoView.cpp @@ -56,7 +56,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -77,6 +79,14 @@ namespace { const QString kSplitterKey = "reposplitter"; const QString kMsgFmt = "%1 - %2"; +// Upper bounds for waiting on background work while shutting down. The GUI +// must never be allowed to block forever: worker threads talk back to the GUI +// thread over blocking queued connections (credential prompts and friends), +// so an unconditional wait deadlocks the whole process. +const int kRemoteCancelTimeoutMs = 5000; +const int kIndexerTerminateTimeoutMs = 1000; +const int kIndexerKillTimeoutMs = 3000; + QString msg(const git::Commit &commit) { QString summary = commit.summary(git::Commit::SubstituteEmoji); return kMsgFmt.arg(commit.link(), summary); @@ -546,9 +556,28 @@ void RepoView::cancelRemoteTransfer() { return; mCallbacks->setCanceled(true); - QCoreApplication::processEvents(); - if (mWatcher && mWatcher->isRunning()) - mWatcher->waitForFinished(); + + if (!mWatcher || !mWatcher->isRunning()) + return; + + // Wait for the transfer to notice the cancellation, but keep the event loop + // spinning instead of calling waitForFinished(). The worker thread reaches + // back into the GUI thread through blocking queued connections (credential + // and interactive auth prompts), so blocking here would deadlock both + // threads and the application could only be killed from the task manager. + // + // Give up after a timeout as well: a transfer stuck in a socket read only + // observes the cancel flag when libgit2 next invokes a progress callback. + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + connect(mWatcher, &QFutureWatcher::finished, &loop, + &QEventLoop::quit); + connect(mWatcher, &QObject::destroyed, &loop, &QEventLoop::quit); + + timeout.start(kRemoteCancelTimeoutMs); + loop.exec(QEventLoop::ExcludeUserInputEvents); } void RepoView::cancelBackgroundTasks() { @@ -856,14 +885,22 @@ void RepoView::cancelIndexing() { if (mIndexer.state() == QProcess::NotRunning) return; +#if defined(Q_OS_WIN) + // QProcess::terminate() posts WM_CLOSE to the target's windows. The indexer + // is a console process without a message loop, so asking politely only + // stalls the shutdown for the full timeout. Kill it right away. + mIndexer.kill(); + mIndexer.waitForFinished(kIndexerKillTimeoutMs); +#else mIndexer.terminate(); - mIndexer.waitForFinished(5000); + mIndexer.waitForFinished(kIndexerTerminateTimeoutMs); if (mIndexer.state() == QProcess::NotRunning) return; mIndexer.kill(); - mIndexer.waitForFinished(5000); + mIndexer.waitForFinished(kIndexerKillTimeoutMs); +#endif } bool RepoView::isLogVisible() const { return mIsLogVisible; } diff --git a/src/watcher/RepositoryWatcher_win.cpp b/src/watcher/RepositoryWatcher_win.cpp index 043965b16..bf4c8b857 100644 --- a/src/watcher/RepositoryWatcher_win.cpp +++ b/src/watcher/RepositoryWatcher_win.cpp @@ -20,6 +20,9 @@ const uint kFlags = FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_LAST_ACCESS | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_SECURITY); +// Never block the GUI thread indefinitely while shutting the watcher down. +const unsigned long kShutdownTimeoutMs = 2000; + } // namespace class RepositoryWatcherPrivate : public QThread { @@ -46,8 +49,10 @@ class RepositoryWatcherPrivate : public QThread { } ~RepositoryWatcherPrivate() { - CloseHandle(mHandle); - CloseHandle(mStop); + if (mHandle != INVALID_HANDLE_VALUE) + CloseHandle(mHandle); + if (mStop) + CloseHandle(mStop); } git::Repository repo() const { return mRepo; } @@ -74,15 +79,26 @@ class RepositoryWatcherPrivate : public QThread { } void watch() { + if (mHandle == INVALID_HANDLE_VALUE || mStopping.loadAcquire()) + return; + ReadDirectoryChangesW(mHandle, mBuffer.data(), mBuffer.size(), true, kFlags, nullptr, &mOverlapped, ¬ify); } void stop() { + // Don't queue any further reads and drop the pending one, so no completion + // routine can run while the thread is on its way out. + mStopping.storeRelease(1); + if (mHandle != INVALID_HANDLE_VALUE) + CancelIoEx(mHandle, &mOverlapped); + // Signal the thread to quit. SetEvent(mStop); } + bool isStopping() const { return mStopping.loadAcquire(); } + static void CALLBACK notify(DWORD errorCode, DWORD numBytes, LPOVERLAPPED overlapped) { if (errorCode || !numBytes) @@ -91,6 +107,9 @@ class RepositoryWatcherPrivate : public QThread { // Copy buffer and restart. RepositoryWatcherPrivate *watcher = static_cast(overlapped->hEvent); + if (watcher->isStopping()) + return; + QVector buffer = watcher->buffer(); watcher->watch(); @@ -125,6 +144,7 @@ class RepositoryWatcherPrivate : public QThread { HANDLE mHandle; QVector mBuffer; OVERLAPPED mOverlapped; + QAtomicInt mStopping{0}; }; RepositoryWatcher::RepositoryWatcher(const git::Repository &repo, @@ -139,7 +159,13 @@ RepositoryWatcher::RepositoryWatcher(const git::Repository &repo, RepositoryWatcher::~RepositoryWatcher() { d->stop(); - d->wait(); + + // Bounded wait: a completion routine that never returns must not be able to + // hang the whole application while the user is closing a window. + if (!d->wait(kShutdownTimeoutMs)) { + d->terminate(); + d->wait(kShutdownTimeoutMs); + } } #include "RepositoryWatcher_win.moc"