From 6b0f602e57f45a73e74692663355c5aa8b9ddc6f Mon Sep 17 00:00:00 2001 From: Jeff <632006142@qq.com> Date: Sun, 28 Jun 2026 20:56:56 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E5=8A=A0=E5=BC=BA=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=202=E3=80=81=E4=B8=8A=E4=BC=A0=E6=97=B6=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0-f=E5=8F=82=E6=95=B0=E6=89=8D=E4=BC=9A=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E4=B8=8A=E4=BC=A0=EF=BC=8C=E5=90=A6=E5=88=99=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E6=A0=B9=E6=8D=AE=E5=BD=93=E5=89=8DTag=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E4=BF=A1=E6=81=AF=E6=9D=A5=E5=88=A4=E6=96=AD=E6=98=AF?= =?UTF-8?q?=E5=90=A6=E9=9C=80=E8=A6=81=E4=B8=8A=E4=BC=A0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GitVer/GitVer.cpp | 16 +- GitVer/GitVer_cli.cpp | 21 + GitVer/GitVer_cli.h | 11 + GitVer/GitVer_common.cpp | 36 ++ GitVer/GitVer_common.h | 23 + GitVer/GitVer_nuitka.cpp | 43 ++ GitVer/GitVer_nuitka.h | 7 + GitVer/GitVer_process.cpp | 21 + GitVer/GitVer_process.h | 4 + GitVer/GitVer_pyinstaller.cpp | 78 ++- GitVer/GitVer_pyinstaller.h | 3 + GitVer/GitVer_rewrite.cpp | 63 ++ GitVer/GitVer_rewrite.h | 3 + GitVer/GitVer_setup.cpp | 1015 +++++++++++++++++---------------- GitVer/GitVer_tag.cpp | 12 + GitVer/GitVer_tag.h | 10 + GitVer/GitVer_types.h | 3 + GitVer/GitVer_upload.cpp | 220 ++++++- GitVer/GitVer_upload.h | 2 +- GitVer/GitVer_version.cpp | 121 ++++ GitVer/GitVer_version.h | 46 ++ GitVer/resource.h | 6 +- GitVer/targetver.h | 8 +- README.md | 3 + 24 files changed, 1248 insertions(+), 527 deletions(-) diff --git a/GitVer/GitVer.cpp b/GitVer/GitVer.cpp index a0250ef..125a33f 100644 --- a/GitVer/GitVer.cpp +++ b/GitVer/GitVer.cpp @@ -672,7 +672,6 @@ int HandleSetVerCommand(int argc, TCHAR* argv[]) return RewriteSetVerByProjectType(lpRepoPath, strProductVersion, strFileVersion); } -/// /// /// 准备C++重写内容。 /// @@ -854,6 +853,14 @@ int HandleRewriteCommand(int argc, TCHAR* argv[]) return 0; } +/// +/// 程序入口:解析命令行参数并分发到各子命令处理函数。 +/// 无参数时进入交互式 tag 创建模式。 +/// +/// 参数个数。 +/// 参数数组。 +/// 环境变量数组。 +/// 命令执行退出码,0 表示成功。 int main(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0; @@ -863,10 +870,6 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[]) SendMessage(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)LoadIcon(NULL, MAKEINTRESOURCE(IDI_ICON_APP))); HMODULE hModule = ::GetModuleHandle(NULL); -#ifdef _DEBUG - Sleep(15000); -#endif - GetDirInfo(); if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0)) { @@ -911,6 +914,9 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[]) if (argc >= 2 && _tcsnicmp(argv[1], _T("setver="), 7) == 0) { +#ifdef _DEBUG + Sleep(10000); +#endif DWORD dwStartTick = LogCommandStart(_T("setver")); int nCmdRet = HandleSetVerCommand(argc, argv); LogCommandEnd(_T("setver"), dwStartTick, nCmdRet); diff --git a/GitVer/GitVer_cli.cpp b/GitVer/GitVer_cli.cpp index 1373b77..d8026d6 100644 --- a/GitVer/GitVer_cli.cpp +++ b/GitVer/GitVer_cli.cpp @@ -5,11 +5,24 @@ extern TCHAR g_szCurModuleDir[MAX_PATH]; +/// +/// 打印无效仓库路径的错误信息。 +/// +/// 无效的路径字符串。 void PrintInvalidRepoPathError(LPCTSTR lpRepoPath) { _tprintf(_T("错误: 无效的仓库路径 %s\n"), lpRepoPath == NULL ? _T("") : lpRepoPath); } +/// +/// 解析 setver / nuitka / pyinstaller 等命令的 repodir= 与 -test 可选参数。 +/// +/// 参数个数。 +/// 参数数组。 +/// 开始解析的下标。 +/// 输出:仓库路径,未指定时为当前模块目录。 +/// 输出:是否启用测试模式。 +/// 0 成功;3 多余参数;23 无效 repodir。 int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode) { lpRepoPath = NULL; @@ -53,6 +66,14 @@ int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepo return 0; } +/// +/// 解析 rewrite 命令的可选 PE 类型(1=exe,2=dll)与 -f 强制标志。 +/// +/// 参数个数。 +/// 参数数组(argv[0] 为 "rewrite")。 +/// 输出:PE 类型,默认 1。 +/// 输出:是否忽略重写失败。 +/// 0 成功;22 PE 类型无效或重复。 int ParseRewriteOptions(int argc, TCHAR* argv[], int& nPEType, BOOL& bForceRewrite) { nPEType = 1; diff --git a/GitVer/GitVer_cli.h b/GitVer/GitVer_cli.h index 88f7ddd..e0181ad 100644 --- a/GitVer/GitVer_cli.h +++ b/GitVer/GitVer_cli.h @@ -1,5 +1,16 @@ #pragma once +/// +/// 打印无效仓库路径的错误信息。 +/// void PrintInvalidRepoPathError(LPCTSTR lpRepoPath); + +/// +/// 解析 setver 等命令的 repodir= 与 -test 可选参数。 +/// int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode); + +/// +/// 解析 rewrite 命令的 PE 类型与 -f 强制标志。 +/// int ParseRewriteOptions(int argc, TCHAR* argv[], int& nPEType, BOOL& bForceRewrite); diff --git a/GitVer/GitVer_common.cpp b/GitVer/GitVer_common.cpp index cdd288e..861ca07 100644 --- a/GitVer/GitVer_common.cpp +++ b/GitVer/GitVer_common.cpp @@ -2,6 +2,12 @@ #include "gitver.h" #include "gitver_common.h" +/// +/// 将命令行参数拼接为便于日志输出的字符串(含引号转义)。 +/// +/// 参数个数。 +/// 参数数组。 +/// 空格分隔、已转义的参数字符串。 CString BuildArgsForLog(int argc, TCHAR* argv[]) { CString strArgs; @@ -16,6 +22,11 @@ CString BuildArgsForLog(int argc, TCHAR* argv[]) return strArgs; } +/// +/// 记录命令开始时间并输出开始日志。 +/// +/// 命令名称。 +/// 当前 GetTickCount 值,供 LogCommandEnd 计算耗时。 DWORD LogCommandStart(LPCTSTR lpCommandName) { DWORD dwStartTick = GetTickCount(); @@ -23,6 +34,11 @@ DWORD LogCommandStart(LPCTSTR lpCommandName) return dwStartTick; } +/// +/// 为命令行参数添加引号转义(含空格、制表符或双引号时加引号)。 +/// +/// 原始参数字符串。 +/// 可直接用于命令行的参数字符串。 CString QuoteCmdArg(const CString& strArg) { if (strArg.Find(_T(' ')) == -1 && strArg.Find(_T('\t')) == -1 && strArg.Find(_T('"')) == -1) @@ -37,6 +53,12 @@ CString QuoteCmdArg(const CString& strArg) return strQuoted; } +/// +/// 尝试将字符串解析为 0-65535 范围内的无符号整数。 +/// +/// 待解析字符串。 +/// 输出:解析结果。 +/// 解析成功返回 TRUE。 BOOL TryParseUInt16(const CString& strValue, UINT& nValue) { CString strTrimmed = strValue; @@ -64,6 +86,15 @@ BOOL TryParseUInt16(const CString& strValue, UINT& nValue) return TRUE; } +/// +/// 解析 argv[nIndex] 为 UInt16,失败时打印错误并返回指定错误码。 +/// +/// 参数数组。 +/// 要解析的参数下标。 +/// 参数名(用于错误提示)。 +/// 输出:解析结果。 +/// 解析失败时的退出码。 +/// 0 成功,否则返回 nErrorCode。 int ParseUInt16ArgOrError(TCHAR* argv[], int nIndex, LPCTSTR lpArgName, UINT& nValue, int nErrorCode) { if (!TryParseUInt16(argv[nIndex], nValue)) @@ -75,6 +106,11 @@ int ParseUInt16ArgOrError(TCHAR* argv[], int nIndex, LPCTSTR lpArgName, UINT& nV return 0; } +/// +/// 判断参数字符串是否像路径(含 \、/、: 或以 . 开头)。 +/// +/// 待判断的参数字符串。 +/// 像路径则返回 TRUE。 BOOL IsLikelyPathArg(LPCTSTR lpArg) { if (lpArg == NULL) diff --git a/GitVer/GitVer_common.h b/GitVer/GitVer_common.h index f25b306..344ba9b 100644 --- a/GitVer/GitVer_common.h +++ b/GitVer/GitVer_common.h @@ -1,8 +1,31 @@ #pragma once +/// +/// 将命令行参数拼接为便于日志输出的字符串。 +/// CString BuildArgsForLog(int argc, TCHAR* argv[]); + +/// +/// 记录命令开始时间并输出开始日志,返回 tick 供计算耗时。 +/// DWORD LogCommandStart(LPCTSTR lpCommandName); + +/// +/// 为命令行参数添加引号转义。 +/// CString QuoteCmdArg(const CString& strArg); + +/// +/// 尝试将字符串解析为 0-65535 范围内的无符号整数。 +/// BOOL TryParseUInt16(const CString& strValue, UINT& nValue); + +/// +/// 解析 argv[nIndex] 为 UInt16,失败时打印错误并返回指定错误码。 +/// int ParseUInt16ArgOrError(TCHAR* argv[], int nIndex, LPCTSTR lpArgName, UINT& nValue, int nErrorCode); + +/// +/// 判断参数字符串是否像路径。 +/// BOOL IsLikelyPathArg(LPCTSTR lpArg); diff --git a/GitVer/GitVer_nuitka.cpp b/GitVer/GitVer_nuitka.cpp index 02fa5e9..7e16bf4 100644 --- a/GitVer/GitVer_nuitka.cpp +++ b/GitVer/GitVer_nuitka.cpp @@ -11,6 +11,15 @@ extern TCHAR g_szCurModuleDir[MAX_PATH]; const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_NUITKA = 1; const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_NUITKA = 0; +/// +/// 解析 nuitkabuild / nuitkapydbuild 命令的 repodir=、params= 与 -test 参数。 +/// +/// 参数个数。 +/// 参数数组。 +/// 输出:仓库路径。 +/// 输出:传给 Nuitka 的额外参数。 +/// 输出:是否测试模式。 +/// 0 成功;3 未知/重复参数;23 无效 repodir。 static int ParseNuitkaOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, CString& strExtraArgs, BOOL& bTestMode) { lpRepoPath = NULL; @@ -71,6 +80,14 @@ static int ParseNuitkaOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, CStr return 0; } +/// +/// 执行 Nuitka 构建命令并输出版本信息与构建结果。 +/// +/// 构建开始提示语。 +/// 完整 Nuitka 命令行。 +/// 产品版本号(仅日志)。 +/// 文件版本号(仅日志)。 +/// 工作目录。 static void RunNuitkaBuild(LPCTSTR lpStartMessage, CString& strNuitkaCmd, const CString& strProductVersion, const CString& strFileVersion, LPCTSTR lpRepoPath) { _tprintf(_T("%s\n"), lpStartMessage); @@ -86,6 +103,20 @@ static void RunNuitkaBuild(LPCTSTR lpStartMessage, CString& strNuitkaCmd, const } } +/// +/// nuitkabuild / nuitkapydbuild 命令的公共实现:解析 pid、生成版本、调用 Nuitka。 +/// +/// 参数个数。 +/// 参数数组。 +/// argv[1] 中 "xxx=" 前缀长度。 +/// TRUE 时使用 --module 模式。 +/// 构建开始提示语。 +/// 命令名(用于错误提示)。 +/// pid 无效时的退出码。 +/// 获取 bid 失败时的退出码。 +/// 获取 Tag 失败时的退出码。 +/// 获取提交信息失败时的退出码。 +/// 0 成功,否则为对应错误码。 static int HandleNuitkaBuildCommandCore( int argc, TCHAR* argv[], @@ -165,6 +196,12 @@ static int HandleNuitkaBuildCommandCore( return 0; } +/// +/// 处理 nuitkabuild=pid 命令:standalone 可执行文件打包。 +/// +/// 参数个数。 +/// 参数数组。 +/// 0 成功,17-21 等为各类错误码。 int HandleNuitkaBuildCommand(int argc, TCHAR* argv[]) { return HandleNuitkaBuildCommandCore( @@ -180,6 +217,12 @@ int HandleNuitkaBuildCommand(int argc, TCHAR* argv[]) 21); } +/// +/// 处理 nuitkapydbuild=pid 命令:Python 扩展模块(.pyd)打包。 +/// +/// 参数个数。 +/// 参数数组。 +/// 0 成功,24-28 等为各类错误码。 int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[]) { return HandleNuitkaBuildCommandCore( diff --git a/GitVer/GitVer_nuitka.h b/GitVer/GitVer_nuitka.h index dd880d2..eb6446f 100644 --- a/GitVer/GitVer_nuitka.h +++ b/GitVer/GitVer_nuitka.h @@ -1,4 +1,11 @@ #pragma once +/// +/// 处理 nuitkabuild=pid 命令:standalone 可执行文件打包。 +/// int HandleNuitkaBuildCommand(int argc, TCHAR* argv[]); + +/// +/// 处理 nuitkapydbuild=pid 命令:Python 扩展模块(.pyd)打包。 +/// int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[]); diff --git a/GitVer/GitVer_process.cpp b/GitVer/GitVer_process.cpp index 8aee189..27783e3 100644 --- a/GitVer/GitVer_process.cpp +++ b/GitVer/GitVer_process.cpp @@ -2,6 +2,13 @@ #include "gitver.h" #include "gitver_process.h" +/// +/// 按顺序将文件中多处旧字符串替换为新字符串并写回。 +/// +/// 目标文件路径。 +/// 待查找的旧内容列表。 +/// 对应的新内容列表。 +/// 全部替换成功返回 TRUE。 BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector& vtOldContent, std::vector& vtNewContent) { if (vtOldContent.size() != vtNewContent.size()) @@ -69,6 +76,12 @@ BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector& vtOldContent, return TRUE; } +/// +/// 将子进程管道输出的原始字节按指定代码页解码为 CString。 +/// +/// 管道原始输出字节。 +/// 解码代码页(CP_UTF8 或 CP_ACP 等)。 +/// 解码后的字符串。 static CString DecodeProcessOutput(const std::string& strRawOutput, UINT nOutputCodePage) { if (strRawOutput.empty()) @@ -120,6 +133,14 @@ static CString DecodeProcessOutput(const std::string& strRawOutput, UINT nOutput #endif } +/// +/// 启动子进程并捕获 stdout/stderr,按代码页解码后返回输出。 +/// +/// 可执行程序路径,NULL 时使用 args 中的命令。 +/// 命令行参数。 +/// 工作目录。 +/// 输出解码代码页,默认 CP_ACP。 +/// 子进程的标准输出/错误合并文本。 CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory, UINT nOutputCodePage) { std::string strRawOutput; diff --git a/GitVer/GitVer_process.h b/GitVer/GitVer_process.h index 2463d70..51183ea 100644 --- a/GitVer/GitVer_process.h +++ b/GitVer/GitVer_process.h @@ -1,6 +1,10 @@ #pragma once +/// +/// 按顺序将文件中多处旧字符串替换为新字符串并写回。 +/// BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector& vtOldContent, std::vector& vtNewContent); + /// /// 启动子进程并捕获 stdout/stderr。 /// nOutputCodePage 为管道原始字节的解码代码页,默认 CP_ACP(按系统 ANSI/GBK 直接使用); diff --git a/GitVer/GitVer_pyinstaller.cpp b/GitVer/GitVer_pyinstaller.cpp index 4827268..4580cb1 100644 --- a/GitVer/GitVer_pyinstaller.cpp +++ b/GitVer/GitVer_pyinstaller.cpp @@ -11,6 +11,15 @@ extern TCHAR g_szCurModuleDir[MAX_PATH]; const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_PYINSTALLER = 1; const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_PYINSTALLER = 0; +/// +/// 解析 pyinstaller 命令的 repodir=、params= 与 -test 参数。 +/// +/// 参数个数。 +/// 参数数组。 +/// 输出:仓库路径。 +/// 输出:传给 PyInstaller 的参数。 +/// 输出:是否测试模式。 +/// 0 成功;3 未知/重复参数;23 无效 repodir。 static int ParsePyinstallerOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, CString& strExtraArgs, BOOL& bTestMode) { lpRepoPath = NULL; @@ -71,7 +80,15 @@ static int ParsePyinstallerOptions(int argc, TCHAR* argv[], LPCTSTR& lpRepoPath, return 0; } -// 解析版本字符串 "a.b.c.d" → 四个分量 +/// +/// 解析版本字符串 "a.b.c.d" 为四个无符号整数分量。 +/// +/// 版本字符串。 +/// 输出:第 1 段。 +/// 输出:第 2 段。 +/// 输出:第 3 段。 +/// 输出:第 4 段。 +/// 格式正确返回 TRUE。 static BOOL ParseVersionTuple(LPCTSTR lpVer, UINT& v1, UINT& v2, UINT& v3, UINT& v4) { CString str(lpVer); @@ -88,7 +105,11 @@ static BOOL ParseVersionTuple(LPCTSTR lpVer, UINT& v1, UINT& v2, UINT& v3, UINT& return TRUE; } -// 提取 params 中第一个空格分隔的 token(支持双引号括起的路径) +/// +/// 提取 params 字符串中第一个 token(支持双引号括起的路径)。 +/// +/// 完整 params 参数字符串。 +/// 第一个 token,空输入返回空字符串。 static CString ExtractFirstToken(const CString& strParams) { CString str = strParams; @@ -105,7 +126,14 @@ static CString ExtractFirstToken(const CString& strParams) return str.Left(nSpace); } -// 生成 version_info.txt 到 lpDir 目录(PyInstaller VSVersionInfo 格式) +/// +/// 在指定目录生成 PyInstaller VSVersionInfo 格式的 version_info.txt。 +/// +/// 输出目录。 +/// 产品版本号。 +/// 文件版本号。 +/// 输出:生成的文件完整路径。 +/// 成功返回 TRUE。 static BOOL WriteVersionInfoFile(LPCTSTR lpDir, LPCTSTR lpProductVersion, LPCTSTR lpFileVersion, CString& strOutPath) { UINT pv1 = 0, pv2 = 0, pv3 = 0, pv4 = 0; @@ -165,8 +193,12 @@ static BOOL WriteVersionInfoFile(LPCTSTR lpDir, LPCTSTR lpProductVersion, LPCTST return TRUE; } -// 向 .spec 文件注入或更新 version= 参数 -// 若已有 version= 则更新其值,否则在 name= 参数后新增一行 +/// +/// 向 .spec 文件注入或更新 version='version_info.txt' 参数。 +/// 已有 version= 则更新整行,否则在 name= 行后插入。 +/// +/// .spec 文件路径。 +/// 成功返回 TRUE。 static BOOL InjectVersionIntoSpec(LPCTSTR lpSpecPath) { if (!PathFileExists(lpSpecPath)) @@ -260,8 +292,12 @@ static BOOL InjectVersionIntoSpec(LPCTSTR lpSpecPath) return TRUE; } -// 从 .spec 文件读取 version= 参数的值(单/双引号括起的路径) -// 返回 FALSE 表示未找到、值为 None 或值为空 +/// +/// 从 .spec 文件读取 version= 参数引号内的外部版本信息文件路径。 +/// +/// .spec 文件路径。 +/// 输出:version= 指向的路径。 +/// 找到有效引号路径返回 TRUE;None 或空值返回 FALSE。 static BOOL ReadVersionPathFromSpec(LPCTSTR lpSpecPath, CString& strVersionPath) { strVersionPath.Empty(); @@ -310,7 +346,13 @@ static BOOL ReadVersionPathFromSpec(LPCTSTR lpSpecPath, CString& strVersionPath) return FALSE; } -// 更新已有 version_info 文件的版本字段;若文件不存在则创建 +/// +/// 更新已有 version_info 文件的版本字段;文件不存在时按模板创建。 +/// +/// version_info.txt 或内联 spec 路径。 +/// 产品版本号。 +/// 文件版本号。 +/// 成功返回 TRUE。 static BOOL UpdateVersionInfoFile(LPCTSTR lpPath, LPCTSTR lpProductVersion, LPCTSTR lpFileVersion) { UINT pv1 = 0, pv2 = 0, pv3 = 0, pv4 = 0; @@ -462,7 +504,11 @@ static BOOL UpdateVersionInfoFile(LPCTSTR lpPath, LPCTSTR lpProductVersion, LPCT return TRUE; } -// 检查 spec 文件中 version= 是否为无引号标识符(表示 spec 内内联 VSVersionInfo 对象) +/// +/// 检查 .spec 中 version= 是否为无引号的 Python 标识符(内联 VSVersionInfo 对象名)。 +/// +/// .spec 文件路径。 +/// 是内联版本块引用返回 TRUE。 static BOOL HasInlineVersionInfoBlock(LPCTSTR lpSpecPath) { if (!PathFileExists(lpSpecPath)) return FALSE; @@ -504,8 +550,12 @@ static BOOL HasInlineVersionInfoBlock(LPCTSTR lpSpecPath) return FALSE; } -// 修改 spec 文件中 version= 的引号值(内联版本号字符串场景) -// 当 version= 后是非外部文件路径的字符串字面量时,直接替换该值为新版本号 +/// +/// 替换 .spec 文件中 version= 引号内的内联版本号字符串。 +/// +/// .spec 文件路径。 +/// 新版本号字符串。 +/// 找到并替换返回 TRUE。 static BOOL UpdateVersionStringInSpec(LPCTSTR lpSpecPath, LPCTSTR lpNewValue) { if (!PathFileExists(lpSpecPath)) @@ -581,6 +631,12 @@ static BOOL UpdateVersionStringInSpec(LPCTSTR lpSpecPath, LPCTSTR lpNewValue) return TRUE; } +/// +/// 处理 pyinstaller=pid 命令:生成版本、处理 spec/version_info、调用 PyInstaller 打包。 +/// +/// 参数个数。 +/// 参数数组。 +/// 0 成功;40-46 等为各类错误码。 int HandlePyinstallerBuildCommand(int argc, TCHAR* argv[]) { #ifdef _DEBUG diff --git a/GitVer/GitVer_pyinstaller.h b/GitVer/GitVer_pyinstaller.h index dc792a1..fdd9f06 100644 --- a/GitVer/GitVer_pyinstaller.h +++ b/GitVer/GitVer_pyinstaller.h @@ -1,3 +1,6 @@ #pragma once +/// +/// 处理 pyinstaller=pid 命令:生成版本信息并调用 PyInstaller 打包。 +/// int HandlePyinstallerBuildCommand(int argc, TCHAR* argv[]); diff --git a/GitVer/GitVer_rewrite.cpp b/GitVer/GitVer_rewrite.cpp index 45e66ab..4267e73 100644 --- a/GitVer/GitVer_rewrite.cpp +++ b/GitVer/GitVer_rewrite.cpp @@ -14,6 +14,12 @@ LPCTSTR GetProjectCodeTypeName(int nCodeType); /// /// 替换指定范围内包含指定标记的整行内容。 /// +/// 文件内容(原地修改)。 +/// 行内查找标记。 +/// 替换后的整行文本。 +/// 搜索范围起始偏移。 +/// 搜索范围结束偏移。 +/// 找到并替换返回 TRUE。 BOOL ReplaceWholeLineByTokenInRange(std::string& strContent, const std::string& strToken, const std::string& strNewLine, size_t nRangeStart, size_t nRangeEnd) { if (nRangeStart == std::string::npos || nRangeEnd == std::string::npos || nRangeStart >= nRangeEnd) @@ -54,11 +60,17 @@ BOOL ReplaceWholeLineByTokenInRange(std::string& strContent, const std::string& return TRUE; } +/// +/// 在全文件范围内替换包含指定标记的整行。 +/// BOOL ReplaceWholeLineByToken(std::string& strContent, const std::string& strToken, const std::string& strNewLine) { return ReplaceWholeLineByTokenInRange(strContent, strToken, strNewLine, 0, strContent.size()); } +/// +/// 替换首个匹配行,并删除后续所有含同一标记的重复行。 +/// BOOL EnsureSingleLineByToken(std::string& strContent, const std::string& strToken, const std::string& strNewLine) { std::string::size_type nPos = strContent.find(strToken); @@ -127,6 +139,9 @@ BOOL EnsureSingleLineByToken(std::string& strContent, const std::string& strToke return TRUE; } +/// +/// 若存在含标记的行则替换并去重,否则在文件末尾追加新行。 +/// BOOL EnsureSingleLineByTokenOrAppend(std::string& strContent, const std::string& strToken, const std::string& strNewLine) { if (strContent.find(strToken) != std::string::npos) @@ -147,6 +162,13 @@ BOOL EnsureSingleLineByTokenOrAppend(std::string& strContent, const std::string& return TRUE; } +/// +/// 定位 .rc 文件中 VS_VERSION_INFO VERSIONINFO 块的 BEGIN…END 字节范围。 +/// +/// .rc 文件内容。 +/// 输出:块起始偏移。 +/// 输出:块结束偏移(不含末尾换行之后)。 +/// 找到完整块返回 TRUE。 BOOL FindVersionInfoBlockRange(const std::string& strContent, size_t& nBlockStart, size_t& nBlockEnd) { nBlockStart = std::string::npos; @@ -210,6 +232,12 @@ BOOL FindVersionInfoBlockRange(const std::string& strContent, size_t& nBlockStar return FALSE; } +/// +/// 检测文本文件 BOM 类型;UTF-16 BOM 视为不支持。 +/// +/// 文件原始字节。 +/// 输出:BOM 长度(无 BOM 时为 0)。 +/// 支持的编码返回 TRUE,UTF-16 返回 FALSE。 BOOL DetectSupportedTextBom(const std::string& strData, size_t& nBomLength) { nBomLength = 0; @@ -236,6 +264,13 @@ BOOL DetectSupportedTextBom(const std::string& strData, size_t& nBomLength) return TRUE; } +/// +/// 以 ANSI/UTF-8(带 BOM 时保留前缀)读取文本文件。 +/// +/// 文件路径。 +/// 输出:不含 BOM 的正文。 +/// 输出:原始 BOM 前缀,写回时需保留。 +/// 读取成功返回 TRUE。 BOOL ReadTextFileAsAnsi(LPCTSTR lpFile, std::string& strContent, std::string& strBomPrefix) { strContent.clear(); @@ -284,6 +319,13 @@ BOOL ReadTextFileAsAnsi(LPCTSTR lpFile, std::string& strContent, std::string& st return TRUE; } +/// +/// 将文本内容与 BOM 前缀写回文件。 +/// +/// 文件路径。 +/// 正文内容。 +/// BOM 前缀(可为空)。 +/// 写入成功返回 TRUE。 BOOL WriteTextFileAsAnsi(LPCTSTR lpFile, const std::string& strContent, const std::string& strBomPrefix) { CFile myFile; @@ -303,6 +345,13 @@ BOOL WriteTextFileAsAnsi(LPCTSTR lpFile, const std::string& strContent, const st return TRUE; } +/// +/// 将 ProductVersion / FileVersion 写入 C++ .rc 文件的 VERSIONINFO 块。 +/// +/// .rc 文件路径。 +/// 产品版本号。 +/// 文件版本号。 +/// 成功返回 TRUE。 BOOL RewriteSetVerToCppRc(const CString& strRcFile, const CString& strProductVersion, const CString& strFileVersion) { std::string strContent; @@ -356,6 +405,13 @@ BOOL RewriteSetVerToCppRc(const CString& strRcFile, const CString& strProductVer return TRUE; } +/// +/// 将 ProductVersion / FileVersion 写入 C# AssemblyInfo.cs。 +/// +/// AssemblyInfo.cs 路径。 +/// 产品版本号(AssemblyVersion)。 +/// 文件版本号(AssemblyFileVersion)。 +/// 成功返回 TRUE。 BOOL RewriteSetVerToAssemblyInfo(const CString& strAssemblyInfoFile, const CString& strProductVersion, const CString& strFileVersion) { std::string strContent; @@ -392,6 +448,13 @@ BOOL RewriteSetVerToAssemblyInfo(const CString& strAssemblyInfoFile, const CStri return TRUE; } +/// +/// 按项目类型(C++ / C#)将版本号写入对应资源文件。 +/// +/// 项目根目录,NULL 时使用当前模块目录。 +/// 产品版本号。 +/// 文件版本号。 +/// 0 成功;15 不支持的项目类型;35 写版本失败。 int RewriteSetVerByProjectType(LPCTSTR lpRepoPath, const CString& strProductVersion, const CString& strFileVersion) { LPCTSTR lpBaseDir = lpRepoPath == NULL ? g_szCurModuleDir : lpRepoPath; diff --git a/GitVer/GitVer_rewrite.h b/GitVer/GitVer_rewrite.h index a6a0fde..b3ec9bc 100644 --- a/GitVer/GitVer_rewrite.h +++ b/GitVer/GitVer_rewrite.h @@ -1,3 +1,6 @@ #pragma once +/// +/// 按项目类型(C++ / C#)将版本号写入对应资源文件。 +/// int RewriteSetVerByProjectType(LPCTSTR lpRepoPath, const CString& strProductVersion, const CString& strFileVersion); diff --git a/GitVer/GitVer_setup.cpp b/GitVer/GitVer_setup.cpp index c5cf0a6..ff78448 100644 --- a/GitVer/GitVer_setup.cpp +++ b/GitVer/GitVer_setup.cpp @@ -33,30 +33,30 @@ static const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP = 0; /// static BOOL ReplaceLineByToken(std::string& strContent, const std::string& strToken, const std::string& strNewLine) { - std::string::size_type nPos = strContent.find(strToken); - if (nPos == std::string::npos) - { - return FALSE; - } + std::string::size_type nPos = strContent.find(strToken); + if (nPos == std::string::npos) + { + return FALSE; + } - std::string::size_type nLineStart = strContent.rfind('\n', nPos); - nLineStart = (nLineStart == std::string::npos) ? 0 : (nLineStart + 1); + std::string::size_type nLineStart = strContent.rfind('\n', nPos); + nLineStart = (nLineStart == std::string::npos) ? 0 : (nLineStart + 1); - std::string::size_type nLineEnd = strContent.find('\n', nPos); - if (nLineEnd == std::string::npos) - { - nLineEnd = strContent.size(); - } + std::string::size_type nLineEnd = strContent.find('\n', nPos); + if (nLineEnd == std::string::npos) + { + nLineEnd = strContent.size(); + } - // 去掉可能存在的 \r - std::string::size_type nReplEnd = nLineEnd; - if (nReplEnd > nLineStart && strContent[nReplEnd - 1] == '\r') - { - --nReplEnd; - } + // 去掉可能存在的 \r + std::string::size_type nReplEnd = nLineEnd; + if (nReplEnd > nLineStart && strContent[nReplEnd - 1] == '\r') + { + --nReplEnd; + } - strContent.replace(nLineStart, nReplEnd - nLineStart, strNewLine); - return TRUE; + strContent.replace(nLineStart, nReplEnd - nLineStart, strNewLine); + return TRUE; } // ───────────────────────────────────────────── @@ -69,36 +69,36 @@ static BOOL ReplaceLineByToken(std::string& strContent, const std::string& strTo /// static BOOL FindSetupScript(int nSetupType, CString& strScriptPath) { - strScriptPath.Empty(); + strScriptPath.Empty(); - LPCTSTR lpFileName = (nSetupType == 0) ? _T("setup.iss") : _T("setup.nsi"); + LPCTSTR lpFileName = (nSetupType == 0) ? _T("setup.iss") : _T("setup.nsi"); - // 当前 exe 目录(无末尾反斜杠) - CString strExeDir = g_szCurModuleDir; + // 当前 exe 目录(无末尾反斜杠) + CString strExeDir = g_szCurModuleDir; - // 尝试 exe 目录 - CString strCandidate; - strCandidate.Format(_T("%s\\%s"), strExeDir.GetString(), lpFileName); - if (PathFileExists(strCandidate)) - { - strScriptPath = strCandidate; - return TRUE; - } + // 尝试 exe 目录 + CString strCandidate; + strCandidate.Format(_T("%s\\%s"), strExeDir.GetString(), lpFileName); + if (PathFileExists(strCandidate)) + { + strScriptPath = strCandidate; + return TRUE; + } - // 尝试上级目录 - int nSlash = strExeDir.ReverseFind(_T('\\')); - if (nSlash > 0) - { - CString strParentDir = strExeDir.Left(nSlash); - strCandidate.Format(_T("%s\\%s"), strParentDir.GetString(), lpFileName); - if (PathFileExists(strCandidate)) - { - strScriptPath = strCandidate; - return TRUE; - } - } + // 尝试上级目录 + int nSlash = strExeDir.ReverseFind(_T('\\')); + if (nSlash > 0) + { + CString strParentDir = strExeDir.Left(nSlash); + strCandidate.Format(_T("%s\\%s"), strParentDir.GetString(), lpFileName); + if (PathFileExists(strCandidate)) + { + strScriptPath = strCandidate; + return TRUE; + } + } - return FALSE; + return FALSE; } // ───────────────────────────────────────────── @@ -110,36 +110,36 @@ static BOOL FindSetupScript(int nSetupType, CString& strScriptPath) /// static BOOL ModifyIssScript(const CString& strScriptPath, const CString& strFileVersion) { - std::string strContent; - std::string strBomPrefix; - if (!ReadTextFileAsAnsi(strScriptPath, strContent, strBomPrefix)) - { - _tprintf(_T("错误: 读取 .iss 文件失败: %s\n"), strScriptPath.GetString()); - return FALSE; - } + std::string strContent; + std::string strBomPrefix; + if (!ReadTextFileAsAnsi(strScriptPath, strContent, strBomPrefix)) + { + _tprintf(_T("错误: 读取 .iss 文件失败: %s\n"), strScriptPath.GetString()); + return FALSE; + } - const std::string strToken = "#define MyAppVersion "; - if (strContent.find(strToken) == std::string::npos) - { - _tprintf(_T("错误: .iss 文件中未找到 #define MyAppVersion 行: %s\n"), strScriptPath.GetString()); - return FALSE; - } + const std::string strToken = "#define MyAppVersion "; + if (strContent.find(strToken) == std::string::npos) + { + _tprintf(_T("错误: .iss 文件中未找到 #define MyAppVersion 行: %s\n"), strScriptPath.GetString()); + return FALSE; + } - std::string strNewLine = "#define MyAppVersion \"" + ToAnsiString(strFileVersion.GetString()) + "\""; - if (!ReplaceLineByToken(strContent, strToken, strNewLine)) - { - _tprintf(_T("错误: 替换 .iss #define MyAppVersion 行失败。\n")); - return FALSE; - } + std::string strNewLine = "#define MyAppVersion \"" + ToAnsiString(strFileVersion.GetString()) + "\""; + if (!ReplaceLineByToken(strContent, strToken, strNewLine)) + { + _tprintf(_T("错误: 替换 .iss #define MyAppVersion 行失败。\n")); + return FALSE; + } - if (!WriteTextFileAsAnsi(strScriptPath, strContent, strBomPrefix)) - { - _tprintf(_T("错误: 写入 .iss 文件失败: %s\n"), strScriptPath.GetString()); - return FALSE; - } + if (!WriteTextFileAsAnsi(strScriptPath, strContent, strBomPrefix)) + { + _tprintf(_T("错误: 写入 .iss 文件失败: %s\n"), strScriptPath.GetString()); + return FALSE; + } - _tprintf(_T("成功: 已更新 .iss #define MyAppVersion \"%s\"\n"), strFileVersion.GetString()); - return TRUE; + _tprintf(_T("成功: 已更新 .iss #define MyAppVersion \"%s\"\n"), strFileVersion.GetString()); + return TRUE; } // ───────────────────────────────────────────── @@ -151,36 +151,36 @@ static BOOL ModifyIssScript(const CString& strScriptPath, const CString& strFile /// static BOOL ModifyNshScript(const CString& strScriptPath, const CString& strFileVersion) { - std::string strContent; - std::string strBomPrefix; - if (!ReadTextFileAsAnsi(strScriptPath, strContent, strBomPrefix)) - { - _tprintf(_T("错误: 读取 .nsh 文件失败: %s\n"), strScriptPath.GetString()); - return FALSE; - } + std::string strContent; + std::string strBomPrefix; + if (!ReadTextFileAsAnsi(strScriptPath, strContent, strBomPrefix)) + { + _tprintf(_T("错误: 读取 .nsh 文件失败: %s\n"), strScriptPath.GetString()); + return FALSE; + } - const std::string strToken = "!define PRODUCT_VERSION"; - if (strContent.find(strToken) == std::string::npos) - { - _tprintf(_T("错误: .nsh 文件中未找到 !define PRODUCT_VERSION 行: %s\n"), strScriptPath.GetString()); - return FALSE; - } + const std::string strToken = "!define PRODUCT_VERSION"; + if (strContent.find(strToken) == std::string::npos) + { + _tprintf(_T("错误: .nsh 文件中未找到 !define PRODUCT_VERSION 行: %s\n"), strScriptPath.GetString()); + return FALSE; + } - std::string strNewLine = "!define PRODUCT_VERSION \"" + ToAnsiString(strFileVersion.GetString()) + "\""; - if (!ReplaceLineByToken(strContent, strToken, strNewLine)) - { - _tprintf(_T("错误: 替换 .nsh PRODUCT_VERSION 行失败。\n")); - return FALSE; - } + std::string strNewLine = "!define PRODUCT_VERSION \"" + ToAnsiString(strFileVersion.GetString()) + "\""; + if (!ReplaceLineByToken(strContent, strToken, strNewLine)) + { + _tprintf(_T("错误: 替换 .nsh PRODUCT_VERSION 行失败。\n")); + return FALSE; + } - if (!WriteTextFileAsAnsi(strScriptPath, strContent, strBomPrefix)) - { - _tprintf(_T("错误: 写入 .nsh 文件失败: %s\n"), strScriptPath.GetString()); - return FALSE; - } + if (!WriteTextFileAsAnsi(strScriptPath, strContent, strBomPrefix)) + { + _tprintf(_T("错误: 写入 .nsh 文件失败: %s\n"), strScriptPath.GetString()); + return FALSE; + } - _tprintf(_T("成功: 已更新 .nsh PRODUCT_VERSION=%s\n"), strFileVersion.GetString()); - return TRUE; + _tprintf(_T("成功: 已更新 .nsh PRODUCT_VERSION=%s\n"), strFileVersion.GetString()); + return TRUE; } // ───────────────────────────────────────────── @@ -193,29 +193,29 @@ static BOOL ModifyNshScript(const CString& strScriptPath, const CString& strFile /// static CString FindInnoSetupCompiler() { - TCHAR szPath[MAX_PATH] = { 0 }; - if (::SearchPath(NULL, _T("ISCC.exe"), NULL, MAX_PATH, szPath, NULL) > 0) - { - return CString(szPath); - } + TCHAR szPath[MAX_PATH] = { 0 }; + if (::SearchPath(NULL, _T("ISCC.exe"), NULL, MAX_PATH, szPath, NULL) > 0) + { + return CString(szPath); + } - static LPCTSTR lpCandidates[] = - { - _T("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"), - _T("C:\\Program Files\\Inno Setup 6\\ISCC.exe"), - _T("C:\\Program Files (x86)\\Inno Setup 5\\ISCC.exe"), - _T("C:\\Program Files\\Inno Setup 5\\ISCC.exe"), - }; + static LPCTSTR lpCandidates[] = + { + _T("C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe"), + _T("C:\\Program Files\\Inno Setup 6\\ISCC.exe"), + _T("C:\\Program Files (x86)\\Inno Setup 5\\ISCC.exe"), + _T("C:\\Program Files\\Inno Setup 5\\ISCC.exe"), + }; - for (int i = 0; i < _countof(lpCandidates); ++i) - { - if (PathFileExists(lpCandidates[i])) - { - return CString(lpCandidates[i]); - } - } + for (int i = 0; i < _countof(lpCandidates); ++i) + { + if (PathFileExists(lpCandidates[i])) + { + return CString(lpCandidates[i]); + } + } - return CString(); + return CString(); } /// @@ -224,27 +224,27 @@ static CString FindInnoSetupCompiler() /// static CString FindNsisCompiler() { - TCHAR szPath[MAX_PATH] = { 0 }; - if (::SearchPath(NULL, _T("makensis.exe"), NULL, MAX_PATH, szPath, NULL) > 0) - { - return CString(szPath); - } + TCHAR szPath[MAX_PATH] = { 0 }; + if (::SearchPath(NULL, _T("makensis.exe"), NULL, MAX_PATH, szPath, NULL) > 0) + { + return CString(szPath); + } - static LPCTSTR lpCandidates[] = - { - _T("C:\\Program Files (x86)\\NSIS\\makensis.exe"), - _T("C:\\Program Files\\NSIS\\makensis.exe"), - }; + static LPCTSTR lpCandidates[] = + { + _T("C:\\Program Files (x86)\\NSIS\\makensis.exe"), + _T("C:\\Program Files\\NSIS\\makensis.exe"), + }; - for (int i = 0; i < _countof(lpCandidates); ++i) - { - if (PathFileExists(lpCandidates[i])) - { - return CString(lpCandidates[i]); - } - } + for (int i = 0; i < _countof(lpCandidates); ++i) + { + if (PathFileExists(lpCandidates[i])) + { + return CString(lpCandidates[i]); + } + } - return CString(); + return CString(); } // ───────────────────────────────────────────── @@ -256,20 +256,20 @@ static CString FindNsisCompiler() /// static CString ExtractQuotedValueAfterToken(const std::string& strContent, const std::string& strToken) { - std::string::size_type nPos = strContent.find(strToken); - if (nPos == std::string::npos) - return CString(); + std::string::size_type nPos = strContent.find(strToken); + if (nPos == std::string::npos) + return CString(); - std::string::size_type nStart = strContent.find('"', nPos + strToken.size()); - if (nStart == std::string::npos) - return CString(); - ++nStart; + std::string::size_type nStart = strContent.find('"', nPos + strToken.size()); + if (nStart == std::string::npos) + return CString(); + ++nStart; - std::string::size_type nEnd = strContent.find('"', nStart); - if (nEnd == std::string::npos) - return CString(); + std::string::size_type nEnd = strContent.find('"', nStart); + if (nEnd == std::string::npos) + return CString(); - return CString(strContent.substr(nStart, nEnd - nStart).c_str()); + return CString(strContent.substr(nStart, nEnd - nStart).c_str()); } /// @@ -278,200 +278,220 @@ static CString ExtractQuotedValueAfterToken(const std::string& strContent, const /// static BOOL ReadVersionFromProjectFiles(LPCTSTR lpBaseDir, CString& strVersion) { - strVersion.Empty(); - std::string strContent, strBomPrefix; + strVersion.Empty(); + std::string strContent, strBomPrefix; - // 1. .rc 文件 -> VALUE "FileVersion", "x.x.x.x" - CString strRcFile; - if (FindPreferredCppRcFileAt(lpBaseDir, strRcFile)) - { - if (ReadTextFileAsAnsi(strRcFile, strContent, strBomPrefix)) - { - CString strVal = ExtractQuotedValueAfterToken(strContent, "VALUE \"FileVersion\","); - if (!strVal.IsEmpty()) - { - strVersion = strVal; - _tprintf(_T("从 .rc 文件读取版本号: %s\n"), strVersion.GetString()); - return TRUE; - } - } - } + // 1. .rc 文件 -> VALUE "FileVersion", "x.x.x.x" + CString strRcFile; + if (FindPreferredCppRcFileAt(lpBaseDir, strRcFile)) + { + if (ReadTextFileAsAnsi(strRcFile, strContent, strBomPrefix)) + { + CString strVal = ExtractQuotedValueAfterToken(strContent, "VALUE \"FileVersion\","); + if (!strVal.IsEmpty()) + { + strVersion = strVal; + _tprintf(_T("从 .rc 文件读取版本号: %s\n"), strVersion.GetString()); + return TRUE; + } + } + } - // 2. AssemblyInfo.cs -> AssemblyFileVersion("x.x.x.x") - CString strAiFile; - if (FindPreferredAssemblyInfoFileAt(lpBaseDir, strAiFile)) - { - if (ReadTextFileAsAnsi(strAiFile, strContent, strBomPrefix)) - { - CString strVal = ExtractQuotedValueAfterToken(strContent, "AssemblyFileVersion("); - if (!strVal.IsEmpty()) - { - strVersion = strVal; - _tprintf(_T("从 AssemblyInfo.cs 读取版本号: %s\n"), strVersion.GetString()); - return TRUE; - } - } - } + // 2. AssemblyInfo.cs -> AssemblyFileVersion("x.x.x.x") + CString strAiFile; + if (FindPreferredAssemblyInfoFileAt(lpBaseDir, strAiFile)) + { + if (ReadTextFileAsAnsi(strAiFile, strContent, strBomPrefix)) + { + CString strVal = ExtractQuotedValueAfterToken(strContent, "AssemblyFileVersion("); + if (!strVal.IsEmpty()) + { + strVersion = strVal; + _tprintf(_T("从 AssemblyInfo.cs 读取版本号: %s\n"), strVersion.GetString()); + return TRUE; + } + } + } - // 3. version_info.txt / .spec -> filevers=(x, x, x, x) - CString strSpecFile; - CString strBaseWithSlash = lpBaseDir; - if (!strBaseWithSlash.IsEmpty() && strBaseWithSlash.Right(1) != _T("\\")) - strBaseWithSlash += _T("\\"); - if (FindFirstFileByPatternRecursive(strBaseWithSlash, _T("version_info.txt"), strSpecFile) || - FindFirstFileByPatternRecursive(strBaseWithSlash, _T("*.spec"), strSpecFile)) - { - if (ReadTextFileAsAnsi(strSpecFile, strContent, strBomPrefix)) - { - std::string::size_type nPos = strContent.find("filevers=("); - if (nPos != std::string::npos) - { - nPos += 10; - std::string::size_type nEnd = strContent.find(')', nPos); - if (nEnd != std::string::npos) - { - std::string strTuple = strContent.substr(nPos, nEnd - nPos); - std::string strClean; - for (char c : strTuple) - if (c != ' ') strClean += (c == ',' ? '.' : c); - strVersion = CString(strClean.c_str()); - _tprintf(_T("从 version_info.txt/.spec 读取版本号: %s\n"), strVersion.GetString()); - return TRUE; - } - } - } - } + // 3. version_info.txt / .spec -> filevers=(x, x, x, x) + CString strSpecFile; + CString strBaseWithSlash = lpBaseDir; + if (!strBaseWithSlash.IsEmpty() && strBaseWithSlash.Right(1) != _T("\\")) + strBaseWithSlash += _T("\\"); + if (FindFirstFileByPatternRecursive(strBaseWithSlash, _T("version_info.txt"), strSpecFile) || + FindFirstFileByPatternRecursive(strBaseWithSlash, _T("*.spec"), strSpecFile)) + { + if (ReadTextFileAsAnsi(strSpecFile, strContent, strBomPrefix)) + { + std::string::size_type nPos = strContent.find("filevers=("); + if (nPos != std::string::npos) + { + nPos += 10; + std::string::size_type nEnd = strContent.find(')', nPos); + if (nEnd != std::string::npos) + { + std::string strTuple = strContent.substr(nPos, nEnd - nPos); + std::string strClean; + for (char c : strTuple) + if (c != ' ') strClean += (c == ',' ? '.' : c); + strVersion = CString(strClean.c_str()); + _tprintf(_T("从 version_info.txt/.spec 读取版本号: %s\n"), strVersion.GetString()); + return TRUE; + } + } + } + } - return FALSE; + return FALSE; } // ───────────────────────────────────────────── // 解析编译输出中的安装包路径 // ───────────────────────────────────────────── +/// +/// 从文本中提取首个双引号括起来的路径;若无引号则返回原文本。 +/// +/// 待解析的文本行。 +/// 提取出的路径字符串。 static CString ExtractQuotedPath(const CString& strText) { - int nStart = strText.Find(_T('"')); - if (nStart < 0) - { - return strText; - } + int nStart = strText.Find(_T('"')); + if (nStart < 0) + { + return strText; + } - int nEnd = strText.Find(_T('"'), nStart + 1); - if (nEnd <= nStart) - { - return strText; - } + int nEnd = strText.Find(_T('"'), nStart + 1); + if (nEnd <= nStart) + { + return strText; + } - return strText.Mid(nStart + 1, nEnd - nStart - 1); + return strText.Mid(nStart + 1, nEnd - nStart - 1); } +/// +/// 在指定目录中查找修改时间最新的 .exe 文件。 +/// +/// 要搜索的目录路径。 +/// 输出:找到的最新 exe 完整路径。 +/// 找到则返回 TRUE,否则返回 FALSE。 static BOOL FindNewestExeInDirectory(LPCTSTR lpDir, CString& strExePath) { - strExePath.Empty(); - if (lpDir == NULL || lpDir[0] == _T('\0') || !PathIsDirectory(lpDir)) - { - return FALSE; - } + strExePath.Empty(); + if (lpDir == NULL || lpDir[0] == _T('\0') || !PathIsDirectory(lpDir)) + { + return FALSE; + } - CString strPattern; - strPattern.Format(_T("%s\\*.exe"), lpDir); + CString strPattern; + strPattern.Format(_T("%s\\*.exe"), lpDir); - WIN32_FIND_DATA findData = { 0 }; - HANDLE hFind = ::FindFirstFile(strPattern, &findData); - if (hFind == INVALID_HANDLE_VALUE) - { - return FALSE; - } + WIN32_FIND_DATA findData = { 0 }; + HANDLE hFind = ::FindFirstFile(strPattern, &findData); + if (hFind == INVALID_HANDLE_VALUE) + { + return FALSE; + } - FILETIME ftNewest = { 0 }; - BOOL bFound = FALSE; + FILETIME ftNewest = { 0 }; + BOOL bFound = FALSE; - do - { - if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - continue; - } + do + { + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + continue; + } - if (!bFound || CompareFileTime(&findData.ftLastWriteTime, &ftNewest) > 0) - { - ftNewest = findData.ftLastWriteTime; - strExePath.Format(_T("%s\\%s"), lpDir, findData.cFileName); - bFound = TRUE; - } - } while (::FindNextFile(hFind, &findData)); + if (!bFound || CompareFileTime(&findData.ftLastWriteTime, &ftNewest) > 0) + { + ftNewest = findData.ftLastWriteTime; + strExePath.Format(_T("%s\\%s"), lpDir, findData.cFileName); + bFound = TRUE; + } + } while (::FindNextFile(hFind, &findData)); - ::FindClose(hFind); - return bFound && PathFileExists(strExePath); + ::FindClose(hFind); + return bFound && PathFileExists(strExePath); } +/// +/// 从 Inno Setup / NSIS 编译输出中解析安装包路径。 +/// 优先匹配编译器输出的路径行,失败则回退到 Output 目录或脚本目录中最新的 exe。 +/// +/// 0=Inno Setup,1=NSIS。 +/// 编译器标准输出。 +/// 安装脚本所在目录。 +/// 输出:解析到的安装包路径。 +/// 成功解析到有效路径则返回 TRUE。 static BOOL ParseInstallerPathFromBuildOutput(int nSetupType, const CString& strOutput, const CString& strScriptDir, CString& strInstallerPath) { - strInstallerPath.Empty(); + strInstallerPath.Empty(); - if (nSetupType == 0) - { - LPCTSTR lpMarker = _T("Resulting Setup program filename is:"); - int nPos = strOutput.Find(lpMarker); - if (nPos >= 0) - { - CString strRest = strOutput.Mid(nPos + (int)_tcslen(lpMarker)); - strRest.Trim(); + if (nSetupType == 0) + { + LPCTSTR lpMarker = _T("Resulting Setup program filename is:"); + int nPos = strOutput.Find(lpMarker); + if (nPos >= 0) + { + CString strRest = strOutput.Mid(nPos + (int)_tcslen(lpMarker)); + strRest.Trim(); - int nLineBreak = strRest.Find(_T('\n')); - CString strFirst = (nLineBreak >= 0) ? strRest.Left(nLineBreak) : strRest; - strFirst.Trim(); - if (!strFirst.IsEmpty()) - { - strFirst = ExtractQuotedPath(strFirst); - if (PathFileExists(strFirst)) - { - strInstallerPath = strFirst; - return TRUE; - } - } + int nLineBreak = strRest.Find(_T('\n')); + CString strFirst = (nLineBreak >= 0) ? strRest.Left(nLineBreak) : strRest; + strFirst.Trim(); + if (!strFirst.IsEmpty()) + { + strFirst = ExtractQuotedPath(strFirst); + if (PathFileExists(strFirst)) + { + strInstallerPath = strFirst; + return TRUE; + } + } - if (nLineBreak >= 0) - { - CString strSecond = strRest.Mid(nLineBreak + 1); - strSecond.Trim(); - strSecond = ExtractQuotedPath(strSecond); - if (PathFileExists(strSecond)) - { - strInstallerPath = strSecond; - return TRUE; - } - } - } - } - else - { - int nPos = 0; - while ((nPos = strOutput.Find(_T("Output:"), nPos)) >= 0) - { - CString strRest = strOutput.Mid(nPos + 7); - int nLineBreak = strRest.Find(_T('\n')); - CString strLine = (nLineBreak >= 0) ? strRest.Left(nLineBreak) : strRest; - strLine.Trim(); - strLine = ExtractQuotedPath(strLine); - if (!strLine.IsEmpty() && PathFileExists(strLine)) - { - strInstallerPath = strLine; - return TRUE; - } - nPos += 7; - } - } + if (nLineBreak >= 0) + { + CString strSecond = strRest.Mid(nLineBreak + 1); + strSecond.Trim(); + strSecond = ExtractQuotedPath(strSecond); + if (PathFileExists(strSecond)) + { + strInstallerPath = strSecond; + return TRUE; + } + } + } + } + else + { + int nPos = 0; + while ((nPos = strOutput.Find(_T("Output:"), nPos)) >= 0) + { + CString strRest = strOutput.Mid(nPos + 7); + int nLineBreak = strRest.Find(_T('\n')); + CString strLine = (nLineBreak >= 0) ? strRest.Left(nLineBreak) : strRest; + strLine.Trim(); + strLine = ExtractQuotedPath(strLine); + if (!strLine.IsEmpty() && PathFileExists(strLine)) + { + strInstallerPath = strLine; + return TRUE; + } + nPos += 7; + } + } - CString strOutputDir; - strOutputDir.Format(_T("%s\\Output"), strScriptDir.GetString()); - if (FindNewestExeInDirectory(strOutputDir, strInstallerPath)) - { - return TRUE; - } + CString strOutputDir; + strOutputDir.Format(_T("%s\\Output"), strScriptDir.GetString()); + if (FindNewestExeInDirectory(strOutputDir, strInstallerPath)) + { + return TRUE; + } - return FindNewestExeInDirectory(strScriptDir, strInstallerPath); + return FindNewestExeInDirectory(strScriptDir, strInstallerPath); } // ───────────────────────────────────────────── @@ -479,220 +499,227 @@ static BOOL ParseInstallerPathFromBuildOutput(int nSetupType, const CString& str // 不回写版本信息到项目文件,只修改安装脚本并调用编译器打包。 // ───────────────────────────────────────────── +/// +/// 修改安装脚本版本号并调用 Inno Setup / NSIS 编译器打包。 +/// 不写回 .rc / AssemblyInfo.cs 等项目文件。 +/// +/// 0=Inno Setup (.iss),1=NSIS (.nsi)。 +/// 产品版本号(仅日志用途)。 +/// 写入脚本的文件版本号。 +/// 输出:编译生成的安装包路径(可能为空)。 +/// 编译流程执行成功返回 TRUE,脚本/编译器缺失或修改失败返回 FALSE。 BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion, CString& strInstallerPath) { - strInstallerPath.Empty(); -#ifdef _DEBUG - Sleep(1500); -#endif - // 查找安装脚本 - CString strScriptPath; - if (!FindSetupScript(nSetupType, strScriptPath)) - { - LPCTSTR lpScriptName = (nSetupType == 0) ? _T("setup.iss") : _T("setup.nsi"); - _tprintf(_T("错误: 未在 exe 目录或上级目录中找到 %s。\n"), lpScriptName); - return FALSE; - } - _tprintf(_T("找到安装脚本: %s\n"), strScriptPath.GetString()); + strInstallerPath.Empty(); + // 查找安装脚本 + CString strScriptPath; + if (!FindSetupScript(nSetupType, strScriptPath)) + { + LPCTSTR lpScriptName = (nSetupType == 0) ? _T("setup.iss") : _T("setup.nsi"); + _tprintf(_T("错误: 未在 exe 目录或上级目录中找到 %s。\n"), lpScriptName); + return FALSE; + } + _tprintf(_T("找到安装脚本: %s\n"), strScriptPath.GetString()); - // 修改脚本中的版本号(不写回 .rc / AssemblyInfo.cs) - BOOL bModifyOk = (nSetupType == 0) - ? ModifyIssScript(strScriptPath, strFileVersion) - : ModifyNshScript(strScriptPath, strFileVersion); + // 修改脚本中的版本号(不写回 .rc / AssemblyInfo.cs) + BOOL bModifyOk = (nSetupType == 0) + ? ModifyIssScript(strScriptPath, strFileVersion) + : ModifyNshScript(strScriptPath, strFileVersion); - if (!bModifyOk) - { - _tprintf(_T("错误: 修改安装脚本版本信息失败。\n")); - return FALSE; - } + if (!bModifyOk) + { + _tprintf(_T("错误: 修改安装脚本版本信息失败。\n")); + return FALSE; + } - // 查找编译器 - CString strCompiler = (nSetupType == 0) - ? FindInnoSetupCompiler() - : FindNsisCompiler(); + // 查找编译器 + CString strCompiler = (nSetupType == 0) + ? FindInnoSetupCompiler() + : FindNsisCompiler(); - if (strCompiler.IsEmpty()) - { - LPCTSTR lpCompilerName = (nSetupType == 0) - ? _T("ISCC.exe (Inno Setup)") - : _T("makensis.exe (NSIS)"); - _tprintf(_T("错误: 未找到安装包编译器 %s,请确认已安装并加入 PATH。\n"), lpCompilerName); - return FALSE; - } - _tprintf(_T("找到编译器: %s\n"), strCompiler.GetString()); + if (strCompiler.IsEmpty()) + { + LPCTSTR lpCompilerName = (nSetupType == 0) + ? _T("ISCC.exe (Inno Setup)") + : _T("makensis.exe (NSIS)"); + _tprintf(_T("错误: 未找到安装包编译器 %s,请确认已安装并加入 PATH。\n"), lpCompilerName); + return FALSE; + } + _tprintf(_T("找到编译器: %s\n"), strCompiler.GetString()); - // 取脚本所在目录作为工作目录 - CString strScriptDir = strScriptPath; - int nSlash = strScriptDir.ReverseFind(_T('\\')); - if (nSlash >= 0) - { - strScriptDir = strScriptDir.Left(nSlash); - } + // 取脚本所在目录作为工作目录 + CString strScriptDir = strScriptPath; + int nSlash = strScriptDir.ReverseFind(_T('\\')); + if (nSlash >= 0) + { + strScriptDir = strScriptDir.Left(nSlash); + } - // 构建并执行编译命令 - CString strCmd; - strCmd.Format(_T("cmd /c %s %s"), - QuoteCmdArg(strCompiler).GetString(), - QuoteCmdArg(strScriptPath).GetString()); - _tprintf(_T("执行编译: %s\n"), strCmd.GetString()); + // 构建并执行编译命令 + CString strCmd; + strCmd.Format(_T("cmd /c %s %s"), QuoteCmdArg(strCompiler).GetString(), QuoteCmdArg(strScriptPath).GetString()); + _tprintf(_T("执行编译: %s\n"), strCmd.GetString()); - CString strOutput = StartProcess(NULL, strCmd.GetBuffer(), strScriptDir.GetString()); - strCmd.ReleaseBuffer(); + CString strOutput = StartProcess(NULL, strCmd.GetBuffer(), strScriptDir.GetString()); + strCmd.ReleaseBuffer(); - if (strOutput.IsEmpty()) - { - _tprintf(_T("警告: 编译器无输出,可能执行失败。\n")); - } - else if (!ParseInstallerPathFromBuildOutput(nSetupType, strOutput, strScriptDir, strInstallerPath)) - { - _tprintf(_T("警告: 未能从编译输出中识别安装包路径。\n")); - } - else - { - _tprintf(_T("安装包路径: %s\n"), strInstallerPath.GetString()); - } + if (strOutput.IsEmpty()) + { + _tprintf(_T("警告: 编译器无输出,可能执行失败。\n")); + } + else if (!ParseInstallerPathFromBuildOutput(nSetupType, strOutput, strScriptDir, strInstallerPath)) + { + _tprintf(_T("警告: 未能从编译输出中识别安装包路径。\n")); + } + else + { + _tprintf(_T("安装包路径: %s\n"), strInstallerPath.GetString()); + } - return TRUE; + return TRUE; } // ───────────────────────────────────────────── // 独立命令入口(argv[1] 形如 "setup=0") // ───────────────────────────────────────────── +/// +/// 处理 setup=n 命令:解析参数、确定版本号、编译安装包并更新 upload_info.txt。 +/// +/// 参数个数。 +/// 参数数组,argv[1] 形如 setup=0 或 setup=1。 +/// 0 成功;36 setup 值无效;38 脚本修改/编译失败;其他见 setver 相关退出码。 int HandleSetupCommand(int argc, TCHAR* argv[]) { -#ifdef _DEBUG - Sleep(15000); -#endif - // 解析 setup=n 中的 n - CString strSetupArg = argv[1]; - CString strN = strSetupArg.Mid(6); // 跳过 "setup=" - UINT nSetupType = UINT(-1); - if (!TryParseUInt16(strN, nSetupType) || (nSetupType != 0 && nSetupType != 1)) - { - _tprintf(_T("错误: setup 参数值 \"%s\" 不支持,仅支持 0(Inno Setup)或 1(NSIS)。\n"), - strN.GetString()); - return 36; - } + // 解析 setup=n 中的 n + CString strSetupArg = argv[1]; + CString strN = strSetupArg.Mid(6); // 跳过 "setup=" + UINT nSetupType = UINT(-1); + if (!TryParseUInt16(strN, nSetupType) || (nSetupType != 0 && nSetupType != 1)) + { + _tprintf(_T("错误: setup 参数值 \"%s\" 不支持,仅支持 0(Inno Setup)或 1(NSIS)。\n"), + strN.GetString()); + return 36; + } - // 解析可选的 pid=m、repodir=、-test - LPCTSTR lpRepoPath = NULL; - BOOL bTestMode = FALSE; - BOOL bHasPid = FALSE; - UINT nPid = 0; + // 解析可选的 pid=m、repodir=、-test + LPCTSTR lpRepoPath = NULL; + BOOL bTestMode = FALSE; + BOOL bHasPid = FALSE; + UINT nPid = 0; - for (int i = 2; i < argc; ++i) - { - CString strArg = argv[i]; - if (strArg.CompareNoCase(_T("-test")) == 0) - { - bTestMode = TRUE; - continue; - } - if (strArg.GetLength() > 8 && strArg.Left(8).CompareNoCase(_T("repodir=")) == 0) - { - if (lpRepoPath != NULL) - { - _tprintf(_T("错误: repodir 参数重复: %s\n"), argv[i]); - return 23; - } - LPCTSTR lpPath = argv[i] + 8; - if (!PathIsDirectory(lpPath)) - { - PrintInvalidRepoPathError(lpPath); - return 23; - } - lpRepoPath = lpPath; - continue; - } - if (strArg.GetLength() > 4 && strArg.Left(4).CompareNoCase(_T("pid=")) == 0) - { - if (bHasPid) - { - _tprintf(_T("错误: pid 参数重复: %s\n"), argv[i]); - return 3; - } - CString strPid = strArg.Mid(4); - if (!TryParseUInt16(strPid, nPid)) - { - _tprintf(_T("错误: pid 参数值 \"%s\" 无效,应为 0-65535 范围整数。\n"), strPid.GetString()); - return 4; - } - bHasPid = TRUE; - continue; - } - _tprintf(_T("错误: 多余的参数 %s\n"), argv[i]); - return 3; - } + for (int i = 2; i < argc; ++i) + { + CString strArg = argv[i]; + if (strArg.CompareNoCase(_T("-test")) == 0) + { + bTestMode = TRUE; + continue; + } + if (strArg.GetLength() > 8 && strArg.Left(8).CompareNoCase(_T("repodir=")) == 0) + { + if (lpRepoPath != NULL) + { + _tprintf(_T("错误: repodir 参数重复: %s\n"), argv[i]); + return 23; + } + LPCTSTR lpPath = argv[i] + 8; + if (!PathIsDirectory(lpPath)) + { + PrintInvalidRepoPathError(lpPath); + return 23; + } + lpRepoPath = lpPath; + continue; + } + if (strArg.GetLength() > 4 && strArg.Left(4).CompareNoCase(_T("pid=")) == 0) + { + if (bHasPid) + { + _tprintf(_T("错误: pid 参数重复: %s\n"), argv[i]); + return 3; + } + CString strPid = strArg.Mid(4); + if (!TryParseUInt16(strPid, nPid)) + { + _tprintf(_T("错误: pid 参数值 \"%s\" 无效,应为 0-65535 范围整数。\n"), strPid.GetString()); + return 4; + } + bHasPid = TRUE; + continue; + } + _tprintf(_T("错误: 多余的参数 %s\n"), argv[i]); + return 3; + } - if (lpRepoPath == NULL) - { - lpRepoPath = g_szCurModuleDir; - } + if (lpRepoPath == NULL) + { + lpRepoPath = g_szCurModuleDir; + } - CString strFileVersion; - CString strProductVersion; + CString strFileVersion; + CString strProductVersion; - if (bHasPid) - { - // 从 git 仓库生成版本号 - VersionBuildErrorCodes errorCodes = { 5, 6, 9 }; - int nVersionRet = BuildVersionsFromRepo( - lpRepoPath, - nPid, - errorCodes, - strProductVersion, - strFileVersion, - DEFAULT_MAJOR_WHEN_NO_TAG_FOR_SETUP, - DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP); - if (nVersionRet != 0) - { - return nVersionRet; - } + if (bHasPid) + { + // 从 git 仓库生成版本号 + VersionBuildErrorCodes errorCodes = { 5, 6, 9 }; + int nVersionRet = BuildVersionsFromRepo( + lpRepoPath, + nPid, + errorCodes, + strProductVersion, + strFileVersion, + DEFAULT_MAJOR_WHEN_NO_TAG_FOR_SETUP, + DEFAULT_MINOR_WHEN_NO_TAG_FOR_SETUP); + if (nVersionRet != 0) + { + return nVersionRet; + } - if (bTestMode) - { - int nDot1 = strProductVersion.Find(_T('.')); - int nDot2 = (nDot1 >= 0) ? strProductVersion.Find(_T('.'), nDot1 + 1) : -1; - if (nDot2 > nDot1) - { - strProductVersion = strProductVersion.Left(nDot2 + 1) + _T("0.0"); - } - _tprintf(_T("[测试版本] 已将 major/minor 置零: ProductVersion=%s\n"), strProductVersion.GetString()); - } + if (bTestMode) + { + int nDot1 = strProductVersion.Find(_T('.')); + int nDot2 = (nDot1 >= 0) ? strProductVersion.Find(_T('.'), nDot1 + 1) : -1; + if (nDot2 > nDot1) + { + strProductVersion = strProductVersion.Left(nDot2 + 1) + _T("0.0"); + } + _tprintf(_T("[测试版本] 已将 major/minor 置零: ProductVersion=%s\n"), strProductVersion.GetString()); + } - _tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString()); - _tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString()); - } - else - { - // 从项目文件读取版本号 - if (!ReadVersionFromProjectFiles(lpRepoPath, strFileVersion)) - { - _tprintf(_T("错误: 未找到 .rc、AssemblyInfo.cs 或 version_info.txt/.spec 文件,无法读取版本号。\n")); - _tprintf(_T("请使用 pid=m 参数指定产品 ID 以从 git 仓库生成版本号。\n")); - return 35; - } - strProductVersion = strFileVersion; - } + _tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString()); + _tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString()); + } + else + { + // 从项目文件读取版本号 + if (!ReadVersionFromProjectFiles(lpRepoPath, strFileVersion)) + { + _tprintf(_T("错误: 未找到 .rc、AssemblyInfo.cs 或 version_info.txt/.spec 文件,无法读取版本号。\n")); + _tprintf(_T("请使用 pid=m 参数指定产品 ID 以从 git 仓库生成版本号。\n")); + return 35; + } + strProductVersion = strFileVersion; + } - // 修改安装脚本并编译打包 - CString strInstallerPath; - if (!ExecuteSetupBuild((int)nSetupType, strProductVersion, strFileVersion, strInstallerPath)) - { - return 38; - } + // 修改安装脚本并编译打包 + CString strInstallerPath; + if (!ExecuteSetupBuild((int)nSetupType, strProductVersion, strFileVersion, strInstallerPath)) + { + return 38; + } - if (!strInstallerPath.IsEmpty()) - { - TCHAR szInfoPath[MAX_PATH] = { 0 }; - _tcscpy_s(szInfoPath, lpRepoPath); - PathAppend(szInfoPath, _T("upload_info.txt")); - if (!WriteUploadInfoPackageFile(szInfoPath, strInstallerPath)) - { - _tprintf(_T("警告: 未能写入 upload_info.txt\n")); - } - } + if (!strInstallerPath.IsEmpty()) + { + TCHAR szInfoPath[MAX_PATH] = { 0 }; + _tcscpy_s(szInfoPath, lpRepoPath); + PathAppend(szInfoPath, _T("upload_info.txt")); + if (!WriteUploadInfoPackageFile(szInfoPath, strInstallerPath)) + { + _tprintf(_T("警告: 未能写入 upload_info.txt\n")); + } + } - return 0; + return 0; } diff --git a/GitVer/GitVer_tag.cpp b/GitVer/GitVer_tag.cpp index a5572b6..822eaf5 100644 --- a/GitVer/GitVer_tag.cpp +++ b/GitVer/GitVer_tag.cpp @@ -10,6 +10,14 @@ extern TCHAR g_szCurModuleDir[MAX_PATH]; const UINT DEFAULT_MAJOR_WHEN_NO_TAG_FOR_TAG = 1; const UINT DEFAULT_MINOR_WHEN_NO_TAG_FOR_TAG = 0; +/// +/// 获取当前分支下最近 nMaxCount 个符合 major.minor 格式的 Tag。 +/// +/// 仓库根目录。 +/// 输出:当前分支名。 +/// 输出:Tag 列表(按版本升序,取最后 nMaxCount 个)。 +/// 最多返回的 Tag 数量。 +/// 成功返回 TRUE(无 Tag 时 vtTags 为空亦视为成功)。 BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector& vtTags, int nMaxCount) { vtTags.clear(); @@ -73,6 +81,10 @@ BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector return TRUE; } +/// +/// 交互式 Tag 创建流程:展示最近 Tag、选择 major+1 或 minor+1、创建并推送。 +/// +/// 0 成功;30-34 交互/创建/验证失败;36 推送失败。 int HandleCreateTagInteractive() { CString strBranch; diff --git a/GitVer/GitVer_tag.h b/GitVer/GitVer_tag.h index 3cf9e0d..04514b1 100644 --- a/GitVer/GitVer_tag.h +++ b/GitVer/GitVer_tag.h @@ -1,5 +1,8 @@ #pragma once +/// +/// 分支 Tag 的 major/minor 解析结果。 +/// struct BranchTagInfo { CString strTag; @@ -7,5 +10,12 @@ struct BranchTagInfo UINT nMinor; }; +/// +/// 获取当前分支下最近 nMaxCount 个符合 major.minor 格式的 Tag。 +/// BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector& vtTags, int nMaxCount); + +/// +/// 交互式 Tag 创建流程:展示最近 Tag、选择递增方式、创建并推送。 +/// int HandleCreateTagInteractive(); diff --git a/GitVer/GitVer_types.h b/GitVer/GitVer_types.h index 60b39ea..5405a94 100644 --- a/GitVer/GitVer_types.h +++ b/GitVer/GitVer_types.h @@ -1,5 +1,8 @@ #pragma once +/// +/// 项目源码类型枚举。 +/// enum ProjectCodeType { PROJECT_UNKNOWN = 0, diff --git a/GitVer/GitVer_upload.cpp b/GitVer/GitVer_upload.cpp index cc623d0..1cde4ad 100644 --- a/GitVer/GitVer_upload.cpp +++ b/GitVer/GitVer_upload.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "GitVer_upload.h" +#include "gitver_version.h" #include "httplib.h" @@ -10,6 +11,7 @@ extern std::string ToAnsiString(LPCTSTR lpText); extern BOOL ReadTextFileAsAnsi(LPCTSTR lpFile, std::string& strContent, std::string& strBomPrefix); +extern TCHAR g_szCurModuleDir[MAX_PATH]; static const LPCTSTR DEFAULT_BASE_URL = _T("http://tools-center.mokadisplay.com/"); @@ -31,6 +33,11 @@ struct UploadInfoConfig // 字符串 / 编码工具 // ───────────────────────────────────────────── +/// +/// 将宽字符(或 ANSI)字符串转换为 UTF-8 编码的 std::string。 +/// +/// 输入字符串。 +/// UTF-8 编码结果,空输入返回空字符串。 static std::string ToUtf8String(LPCTSTR lpText) { if (lpText == NULL || lpText[0] == _T('\0')) @@ -74,6 +81,11 @@ static std::string ToUtf8String(LPCTSTR lpText) #endif } +/// +/// 将 UTF-8 编码的 std::string 转换为 CString。 +/// +/// UTF-8 字符串。 +/// 转换后的 CString。 static CString Utf8ToCString(const std::string& strUtf8) { if (strUtf8.empty()) @@ -116,6 +128,11 @@ static CString Utf8ToCString(const std::string& strUtf8) #endif } +/// +/// 将 ANSI 编码的 std::string 转换为 CString。 +/// +/// ANSI 字符串。 +/// 转换后的 CString。 static CString AnsiToCString(const std::string& strAnsi) { if (strAnsi.empty()) @@ -139,6 +156,11 @@ static CString AnsiToCString(const std::string& strAnsi) #endif } +/// +/// 粗略检测字节序列是否为合法 UTF-8 编码。 +/// +/// 待检测的字节串。 +/// 看起来像合法 UTF-8 则返回 TRUE。 static BOOL IsLikelyValidUtf8(const std::string& strValue) { size_t i = 0; @@ -188,6 +210,11 @@ static BOOL IsLikelyValidUtf8(const std::string& strValue) return TRUE; } +/// +/// 将 info 文件中的原始字节值转为 CString,自动区分 UTF-8 与 ANSI。 +/// +/// 原始字节值。 +/// 解码后的 CString。 static CString InfoValueToCString(const std::string& strValue) { if (IsLikelyValidUtf8(strValue)) @@ -198,6 +225,11 @@ static CString InfoValueToCString(const std::string& strValue) return AnsiToCString(strValue); } +/// +/// 对 UTF-8 字符串进行 application/x-www-form-urlencoded 风格 URL 编码。 +/// +/// 待编码的 UTF-8 字符串。 +/// 编码后的字符串。 static std::string UrlEncodeUtf8(const std::string& strValue) { static const char* kHex = "0123456789ABCDEF"; @@ -228,6 +260,11 @@ static std::string UrlEncodeUtf8(const std::string& strValue) return strEncoded; } +/// +/// 反转义 info 文件值中的 \n 为换行符。 +/// +/// 含转义序列的原始值。 +/// 反转义后的字符串。 static std::string UnescapeInfoValue(const std::string& strValue) { std::string strResult; @@ -249,6 +286,11 @@ static std::string UnescapeInfoValue(const std::string& strValue) return strResult; } +/// +/// 将字符串转义后写入 info 文件(换行 → \n,反斜杠 → \\)。 +/// +/// 原始字符串。 +/// 适合写入 info 文件的转义字符串。 static std::string EscapeInfoValueForFile(const std::string& strValue) { std::string strResult; @@ -277,6 +319,10 @@ static std::string EscapeInfoValueForFile(const std::string& strValue) return strResult; } +/// +/// 去除 ANSI 字符串首尾的空格、制表符和回车。 +/// +/// 待修剪的字符串(原地修改)。 static void TrimAnsiInPlace(std::string& strValue) { while (!strValue.empty() && (strValue.front() == ' ' || strValue.front() == '\t' || strValue.front() == '\r')) @@ -289,6 +335,12 @@ static void TrimAnsiInPlace(std::string& strValue) } } +/// +/// 读取二进制文件的全部内容到字节向量。 +/// +/// 文件路径。 +/// 输出:文件字节内容。 +/// 读取成功返回 TRUE。 static BOOL ReadBinaryFile(LPCTSTR lpFile, std::vector& data) { data.clear(); @@ -312,6 +364,11 @@ static BOOL ReadBinaryFile(LPCTSTR lpFile, std::vector& data) return TRUE; } +/// +/// 将 ASCII 字母转为小写(仅 A-Z)。 +/// +/// 输入字符串。 +/// 小写化后的新字符串。 static std::string ToLowerAscii(std::string strValue) { for (char& ch : strValue) @@ -328,6 +385,10 @@ static std::string ToLowerAscii(std::string strValue) // Cookie 管理(httplib 不自动维护 Session) // ───────────────────────────────────────────── +/// +/// 去除 std::string 首尾的空格和制表符。 +/// +/// 待修剪的字符串(原地修改)。 static void TrimStdStringInPlace(std::string& strValue) { while (!strValue.empty() && (strValue.front() == ' ' || strValue.front() == '\t')) @@ -340,6 +401,11 @@ static void TrimStdStringInPlace(std::string& strValue) } } +/// +/// 将 Set-Cookie 响应头中的 name=value 合并到 Cookie 字符串,同名则覆盖。 +/// +/// Set-Cookie 头的值部分。 +/// 输入/输出:累积的 Cookie 字符串。 static void MergeSetCookieValue(const std::string& strHeaderValue, std::string& strCookies) { std::string strLine = strHeaderValue; @@ -431,6 +497,11 @@ static void MergeSetCookieValue(const std::string& strHeaderValue, std::string& strCookies = strResult; } +/// +/// 从 HTTP 响应的所有 Set-Cookie 头中收集并合并 Cookie。 +/// +/// HTTP 响应对象。 +/// 输出:合并后的 Cookie 字符串。 static void CollectResponseCookies(const httplib::Response& response, std::string& strCookies) { for (const auto& header : response.headers) @@ -442,6 +513,11 @@ static void CollectResponseCookies(const httplib::Response& response, std::strin } } +/// +/// 根据 Cookie 字符串构造 httplib 请求头。 +/// +/// Cookie 字符串。 +/// 含 Cookie 头的 Headers 对象。 static httplib::Headers MakeCookieHeaders(const std::string& strCookies) { httplib::Headers headers; @@ -452,6 +528,11 @@ static httplib::Headers MakeCookieHeaders(const std::string& strCookies) return headers; } +/// +/// 创建指向 PMC 服务的 httplib 客户端,配置超时且不自动跟随重定向。 +/// +/// PMC 基址 URL。 +/// 有效客户端指针,无效 URL 时返回 nullptr。 static std::unique_ptr CreatePmcClient(LPCTSTR lpBaseUrl) { std::string strBaseUrl = ToUtf8String(lpBaseUrl); @@ -472,6 +553,12 @@ static std::unique_ptr CreatePmcClient(LPCTSTR lpBaseUrl) // info 文件解析 // ───────────────────────────────────────────── +/// +/// 根据键名将 info 文件中的一行键值对写入 UploadInfoConfig 结构体。 +/// +/// 输出配置对象。 +/// 键名。 +/// 原始值(含转义序列)。 static void ApplyInfoKeyValue(UploadInfoConfig& config, const std::string& strKey, const std::string& strRawValue) { std::string strValue = UnescapeInfoValue(strRawValue); @@ -519,6 +606,12 @@ static void ApplyInfoKeyValue(UploadInfoConfig& config, const std::string& strKe } } +/// +/// 从 upload_info.txt 等 info 配置文件加载上传参数。 +/// +/// info 文件路径。 +/// 输出:解析后的配置。 +/// 文件读取成功返回 TRUE。 static BOOL LoadUploadInfoFile(LPCTSTR lpInfoFile, UploadInfoConfig& config) { std::string strContent; @@ -571,6 +664,11 @@ static BOOL LoadUploadInfoFile(LPCTSTR lpInfoFile, UploadInfoConfig& config) return TRUE; } +/// +/// 校验上传配置是否包含必填字段。 +/// +/// 待校验的配置。 +/// 必填字段齐全返回 TRUE,缺失时打印错误并返回 FALSE。 static BOOL ValidateUploadConfig(const UploadInfoConfig& config) { if (config.strPackageName.IsEmpty()) @@ -601,6 +699,12 @@ static BOOL ValidateUploadConfig(const UploadInfoConfig& config) return TRUE; } +/// +/// 从与用户名同名的环境变量中读取登录密码。 +/// +/// PMC 登录用户名(亦作环境变量名)。 +/// 输出:读取到的密码。 +/// 环境变量存在且非空返回 TRUE。 static BOOL ResolvePasswordFromEnv(LPCTSTR lpUsername, CString& strPassword) { if (lpUsername == NULL || lpUsername[0] == _T('\0')) @@ -635,6 +739,13 @@ static BOOL ResolvePasswordFromEnv(LPCTSTR lpUsername, CString& strPassword) return TRUE; } +/// +/// 更新 upload_info.txt 中指定键的值,保留其他行和 BOM 前缀。 +/// 文件中不存在的键会追加到末尾。 +/// +/// info 文件路径。 +/// 要更新的键值对映射。 +/// 写入成功返回 TRUE。 static BOOL UpdateUploadInfoKeys(LPCTSTR lpInfoFile, const std::map& updates) { std::vector outLines; @@ -729,19 +840,21 @@ static BOOL UpdateUploadInfoKeys(LPCTSTR lpInfoFile, const std::map +/// 更新 upload_info.txt 中的版本号与变更日志字段。 +/// +BOOL WriteUploadInfoVersions(LPCTSTR lpInfoFile, LPCTSTR lpProductVersion, LPCTSTR lpFileVersion, LPCTSTR lpFileChangeLog) { std::map updates; - updates["product_version"] = ToUtf8String(lpProductVersion); - updates["file_version"] = ToUtf8String(lpFileVersion); - updates["file_change_log"] = EscapeInfoValueForFile(ToUtf8String(lpFileChangeLog)); + updates["product_version"] = lpProductVersion; + updates["file_version"] = lpFileVersion; + updates["file_change_log"] = EscapeInfoValueForFile(lpFileChangeLog); return UpdateUploadInfoKeys(lpInfoFile, updates); } +/// +/// 更新 upload_info.txt 中的 file 字段(安装包路径)。 +/// BOOL WriteUploadInfoPackageFile(LPCTSTR lpInfoFile, LPCTSTR lpPackageFile) { std::map updates; @@ -749,18 +862,64 @@ BOOL WriteUploadInfoPackageFile(LPCTSTR lpInfoFile, LPCTSTR lpPackageFile) return UpdateUploadInfoKeys(lpInfoFile, updates); } +/// +/// 打印 upload 命令的用法说明。 +/// static void PrintUploadUsage() { - _tprintf(_T("用法:gitver upload username=<用户名> [file=<上传的文件>] [info=<配置.txt>] [url=]\n")); + _tprintf(_T("用法:gitver upload username=<用户名> [file=<上传的文件>] [info=<配置.txt>] [url=] [-f]\n")); _tprintf(_T("说明:info= 缺省为 upload_info.txt;file= 可省略,从 info 的 file 字段读取\n")); _tprintf(_T("说明:登录密码从与 username 同名的环境变量读取\n")); + _tprintf(_T("说明:默认要求当前在分支上且 HEAD 指向 Tag;加 -f 可忽略 Tag 校验\n")); _tprintf(_T("示例:gitver upload username=admin\n")); } +/// +/// 校验当前 Git 仓库是否处于可上传状态(在分支上且 HEAD 指向 Tag)。 +/// +/// 仓库根目录。 +/// 0 表示通过;54 表示 HEAD 游离或未指向 Tag。 +static int ValidateUploadGitTagState(LPCTSTR lpRepoPath) +{ + CString strStatus; + if (!TryGetGitStatus(lpRepoPath, strStatus)) + { + CString strTag; + if (TryGetDetectedTag(lpRepoPath, strTag)) + { + _tprintf(_T("错误: 当前处于 HEAD 游离状态(Tag: %s),不允许上传\n"), strTag.GetString()); + } + else + { + _tprintf(_T("错误: 当前处于 HEAD 游离状态,不允许上传\n")); + } + _tprintf(_T("提示: 使用 upload ... -f 可强制上传\n")); + return 54; + } + + CString strTag; + if (!TryGetDetectedTag(lpRepoPath, strTag)) + { + _tprintf(_T("错误: 当前 commit 未指向 Tag 标签,不允许上传\n")); + _tprintf(_T("提示: 使用 upload ... -f 可强制上传\n")); + return 54; + } + + return 0; +} + // ───────────────────────────────────────────── // PMC 交互(cpp-httplib) // ───────────────────────────────────────────── +/// +/// 向 PMC 发送登录请求,获取 Session Cookie。 +/// +/// httplib 客户端。 +/// 登录用户名。 +/// 登录密码。 +/// 输出:登录成功后的 Cookie 字符串。 +/// 0 成功;50 登录失败;53 网络错误。 static int PmcLogin(httplib::Client& cli, LPCTSTR lpUsername, LPCTSTR lpPassword, std::string& strCookies) { strCookies.clear(); @@ -805,6 +964,14 @@ static int PmcLogin(httplib::Client& cli, LPCTSTR lpUsername, LPCTSTR lpPassword return 0; } +/// +/// 以 multipart/form-data 方式上传安装包到 PMC。 +/// +/// httplib 客户端。 +/// 登录 Session Cookie。 +/// 上传配置(包名、版本、变更日志等)。 +/// 待上传的文件路径。 +/// 0 成功;48 文件读取失败;51 上传 HTTP 失败;53 网络错误。 static int PmcUploadPackage(httplib::Client& cli, const std::string& strCookies, const UploadInfoConfig& config, LPCTSTR lpFilePath) { std::vector fileData; @@ -856,6 +1023,13 @@ static int PmcUploadPackage(httplib::Client& cli, const std::string& strCookies, return 0; } +/// +/// 在 JSON 响应体中查找 "field":"value" 形式的字段值(简单字符串匹配)。 +/// +/// JSON 响应正文。 +/// 字段名。 +/// 期望的字段值。 +/// 找到匹配则返回 TRUE。 static BOOL JsonContainsFieldValue(const std::string& strBody, LPCSTR lpField, LPCSTR lpValue) { if (lpField == NULL || lpValue == NULL) @@ -867,6 +1041,13 @@ static BOOL JsonContainsFieldValue(const std::string& strBody, LPCSTR lpField, L return strBody.find(strNeedle) != std::string::npos; } +/// +/// 调用 PMC API 验证上传后是否存在对应包名和版本记录。 +/// +/// httplib 客户端。 +/// 登录 Session Cookie。 +/// 含包名和版本信息的配置。 +/// 0 验证通过;52 未找到版本;53 网络错误。 static int VerifyUploadResult(httplib::Client& cli, const std::string& strCookies, const UploadInfoConfig& config) { std::string strPath = "/api/packages/latest?keyword=" + @@ -907,16 +1088,28 @@ static int VerifyUploadResult(httplib::Client& cli, const std::string& strCookie // 命令入口 // ───────────────────────────────────────────── +/// +/// 处理 upload 命令:解析参数、登录 PMC、上传包并验证结果。 +/// +/// 参数个数。 +/// 参数数组。 +/// 0 成功;47-54 各类错误,详见头文件说明。 int HandleUploadCommand(int argc, TCHAR* argv[]) { CString strUsername; CString strFilePath; CString strInfoPath; CString strUrlOverride; + BOOL bForceUpload = FALSE; for (int i = 2; i < argc; ++i) { CString strArg = argv[i]; + if (strArg.CompareNoCase(_T("-f")) == 0) + { + bForceUpload = TRUE; + continue; + } if (strArg.GetLength() > 9 && strArg.Left(9).CompareNoCase(_T("username=")) == 0) { strUsername = strArg.Mid(9); @@ -1001,6 +1194,15 @@ int HandleUploadCommand(int argc, TCHAR* argv[]) return 49; } + if (!bForceUpload) + { + int nTagRet = ValidateUploadGitTagState(g_szCurModuleDir); + if (nTagRet != 0) + { + return nTagRet; + } + } + CString strPassword; if (!ResolvePasswordFromEnv(strUsername, strPassword)) { diff --git a/GitVer/GitVer_upload.h b/GitVer/GitVer_upload.h index 5765d25..24d6536 100644 --- a/GitVer/GitVer_upload.h +++ b/GitVer/GitVer_upload.h @@ -3,7 +3,7 @@ /// /// 处理 upload 命令:登录 PMC 并上传包文件。 /// 退出码:0 成功,47 缺少参数,48 文件不存在,49 info 无效或未设置同名环境变量密码, -/// 50 登录失败,51 上传 HTTP 失败,52 验证未找到版本,53 网络错误。 +/// 50 登录失败,51 上传 HTTP 失败,52 验证未找到版本,53 网络错误,54 Tag/分支校验失败。 /// int HandleUploadCommand(int argc, TCHAR* argv[]); diff --git a/GitVer/GitVer_version.cpp b/GitVer/GitVer_version.cpp index 1fdb1ba..977ea1b 100644 --- a/GitVer/GitVer_version.cpp +++ b/GitVer/GitVer_version.cpp @@ -4,6 +4,12 @@ #include "gitver_process.h" #include "gitver_version.h" +/// +/// 检测 Git 仓库是否处于正常分支状态(非 HEAD 游离)。 +/// +/// 仓库根目录。 +/// 输出:symbolic-ref HEAD 结果。 +/// 在分支上返回 TRUE,游离状态返回 FALSE。 BOOL TryGetGitStatus(LPCTSTR lpRepoPath, CString& strStatus) { strStatus = StartProcess(NULL, _T("cmd /c git symbolic-ref --quiet HEAD"), lpRepoPath); @@ -19,6 +25,12 @@ BOOL TryGetGitStatus(LPCTSTR lpRepoPath, CString& strStatus) return TRUE; } +/// +/// 获取当前检出的本地分支名称。 +/// +/// 仓库根目录。 +/// 输出:分支名。 +/// 成功返回 TRUE。 BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch) { _tprintf(_T("获取当前分支名称: \n")); @@ -35,6 +47,12 @@ BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch) return TRUE; } +/// +/// 在 HEAD 游离时,通过远程分支推断当前所属分支名(origin/ 前缀已去除)。 +/// +/// 仓库根目录。 +/// 输出:推断的分支名。 +/// 找到远程分支返回 TRUE。 BOOL TryGetDetectedBranch(LPCTSTR lpRepoPath, CString& strBranch) { // 远程仓库名必须以 origin 开头; @@ -54,6 +72,12 @@ BOOL TryGetDetectedBranch(LPCTSTR lpRepoPath, CString& strBranch) return TRUE; } +/// +/// 获取指向当前 HEAD 的 Tag 名称。 +/// +/// 仓库根目录。 +/// 输出:Tag 名(多个时取 git 输出首行)。 +/// 存在 Tag 返回 TRUE。 BOOL TryGetDetectedTag(LPCTSTR lpRepoPath, CString& strTag) { strTag = StartProcess(NULL, _T("cmd /c git tag --points-at HEAD"), lpRepoPath); @@ -70,6 +94,12 @@ BOOL TryGetDetectedTag(LPCTSTR lpRepoPath, CString& strTag) return TRUE; } +/// +/// 从分支名解析 bid(分支 ID):main/master=0,release=1,dev=2,其余取末段数字。 +/// +/// 分支名称。 +/// 输出:分支 bid。 +/// 解析成功返回 TRUE。 BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid) { nBid = 0; @@ -106,6 +136,14 @@ BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid) return TryParseUInt16(strNum, nBid); } +/// +/// 解析符合「分支名.major.minor」格式的 Tag,提取 major 与 minor。 +/// +/// 分支名称。 +/// Tag 名称。 +/// 输出:主版本号。 +/// 输出:次版本号。 +/// 格式匹配且解析成功返回 TRUE。 BOOL TryParseTagMajorMinor(const CString& strBranch, const CString& strTag, UINT& nMajor, UINT& nMinor) { if (strBranch.IsEmpty()) @@ -141,6 +179,15 @@ BOOL TryParseTagMajorMinor(const CString& strBranch, const CString& strTag, UINT return TRUE; } +/// +/// 获取当前分支下版本号最大的 major.minor Tag(按 v:refname 排序遍历)。 +/// +/// 仓库根目录。 +/// 分支名称。 +/// 输出:最新 Tag 全名。 +/// 输出:主版本号。 +/// 输出:次版本号。 +/// 找到匹配 Tag 返回 TRUE。 BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString& strTag, UINT& nMajor, UINT& nMinor) { strTag.Empty(); @@ -184,6 +231,13 @@ BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString& return !strTag.IsEmpty(); } +/// +/// 将 SYSTEMTIME 转换为 FileVersion 中的 yy 与 mmdd 分量。 +/// 月份 1-12 映射为 11-22(mmdd = (month+10)*100 + day)。 +/// +/// 本地时间。 +/// 输出:年份后两位。 +/// 输出:编码后的月日。 void GetFileVersionDateFromSystemTime(const SYSTEMTIME& stLocal, UINT& nYY, UINT& nMMDD) { nYY = stLocal.wYear % 100; @@ -191,6 +245,14 @@ void GetFileVersionDateFromSystemTime(const SYSTEMTIME& stLocal, UINT& nYY, UINT nMMDD = (stLocal.wMonth + 10) * 100 + stLocal.wDay; } +/// +/// 统计指定日期内 origin/分支 的提交次数(用于 FileVersion 末段 id)。 +/// +/// 仓库根目录。 +/// 分支名称。 +/// 目标日期。 +/// 输出:当日提交数。 +/// 成功返回 TRUE。 BOOL TryGetBranchCommitCountByDate(LPCTSTR lpRepoPath, const CString& strBranch, const SYSTEMTIME& stDate, UINT& nId) { nId = 0; @@ -229,6 +291,13 @@ BOOL TryGetBranchCommitCountByDate(LPCTSTR lpRepoPath, const CString& strBranch, return TryParseUInt16(strCount, nId); } +/// +/// 获取分支最近一次提交的日期(git log -1 --date=short)。 +/// +/// 仓库根目录。 +/// 分支名称。 +/// 输出:提交日期。 +/// 成功返回 TRUE。 BOOL TryGetLastBranchCommitDate(LPCTSTR lpRepoPath, const CString& strBranch, SYSTEMTIME& stDate) { ::ZeroMemory(&stDate, sizeof(stDate)); @@ -271,6 +340,13 @@ BOOL TryGetLastBranchCommitDate(LPCTSTR lpRepoPath, const CString& strBranch, SY return TRUE; } +/// +/// 获取 Tag 对应提交的日期(git log -1 --date=short)。 +/// +/// 仓库根目录。 +/// Tag 名称。 +/// 输出:提交日期。 +/// 成功返回 TRUE。 BOOL TryGetTagCommitDate(LPCTSTR lpRepoPath, const CString& strTag, SYSTEMTIME& stDate) { ::ZeroMemory(&stDate, sizeof(stDate)); @@ -313,6 +389,17 @@ BOOL TryGetTagCommitDate(LPCTSTR lpRepoPath, const CString& strTag, SYSTEMTIME& return TRUE; } +/// +/// 基于 Tag 提交日期解析 FileVersion 的 yy、mmdd 与 id(当日提交数)。 +/// +/// 仓库根目录。 +/// 分支名称。 +/// Tag 名称。 +/// 输出:年份后两位。 +/// 输出:编码月日。 +/// 输出:提交序号。 +/// 可选输出:Tag 提交日期。 +/// 成功返回 TRUE。 BOOL ResolveTagFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& strBranch, const CString& strTag, UINT& nYY, UINT& nMMDD, UINT& nId, SYSTEMTIME* pOutDate) { nYY = 0; @@ -358,6 +445,17 @@ BOOL ResolveTagFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& return TRUE; } +/// +/// 基于当前日期(或最近提交日)解析 FileVersion 的 yy、mmdd 与 id。 +/// 当天提交数为 0 时回退到最近一次提交日期。 +/// +/// 仓库根目录。 +/// 分支名称。 +/// 输出:年份后两位。 +/// 输出:编码月日。 +/// 输出:提交序号。 +/// 可选输出:采用的日期。 +/// 成功返回 TRUE。 BOOL ResolveFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& strBranch, UINT& nYY, UINT& nMMDD, UINT& nId, SYSTEMTIME* pOutDate) { nYY = 0; @@ -407,6 +505,14 @@ BOOL ResolveFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& str return TRUE; } +/// +/// 获取指定日期内 origin/分支 的提交摘要列表(UTF-8 输出,一行一条)。 +/// +/// 仓库根目录。 +/// 分支名称。 +/// 目标日期。 +/// 输出:提交摘要,多行以换行分隔。 +/// 成功返回 TRUE。 BOOL TryGetGitLogByBranchDate(LPCTSTR lpRepoPath, const CString& strBranch, const SYSTEMTIME& stDate, CString& strLog) { strLog.Empty(); @@ -444,6 +550,21 @@ BOOL TryGetGitLogByBranchDate(LPCTSTR lpRepoPath, const CString& strBranch, cons return TRUE; } +/// +/// 从 Git 仓库信息构建 ProductVersion 与 FileVersion 字符串。 +/// 正常分支:ProductVersion = pid.bid.major.minor,FileVersion = pid.yy.mmdd.id。 +/// HEAD 游离时改用远程分支与 Tag 解析。 +/// +/// 仓库根目录。 +/// 产品 ID。 +/// 各步骤失败时的退出码映射。 +/// 输出:产品版本号。 +/// 输出:文件版本号。 +/// 无 Tag 时的默认 major。 +/// 无 Tag 时的默认 minor。 +/// 可选输出:使用的分支名。 +/// 可选输出:FileVersion 采用的日期。 +/// 0 成功,否则返回 errorCodes 中对应错误码。 int BuildVersionsFromRepo( LPCTSTR lpRepoPath, UINT nPid, diff --git a/GitVer/GitVer_version.h b/GitVer/GitVer_version.h index 359a31e..837eb5f 100644 --- a/GitVer/GitVer_version.h +++ b/GitVer/GitVer_version.h @@ -1,5 +1,8 @@ #pragma once +/// +/// BuildVersionsFromRepo 各步骤失败时使用的退出码映射。 +/// struct VersionBuildErrorCodes { int nBidErrorCode; @@ -8,13 +11,44 @@ struct VersionBuildErrorCodes int nStatusErrorCode; }; +/// +/// 检测 Git 仓库是否处于正常分支状态(非 HEAD 游离)。 +/// BOOL TryGetGitStatus(LPCTSTR lpRepoPath, CString& strStatus); + +/// +/// 获取当前检出的本地分支名称。 +/// BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch); + +/// +/// 在 HEAD 游离时推断当前所属远程分支名。 +/// BOOL TryGetDetectedBranch(LPCTSTR lpRepoPath, CString& strBranch); + +/// +/// 获取指向当前 HEAD 的 Tag 名称。 +/// BOOL TryGetDetectedTag(LPCTSTR lpRepoPath, CString& strTag); + +/// +/// 从分支名解析 bid(分支 ID)。 +/// BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid); + +/// +/// 解析符合「分支名.major.minor」格式的 Tag。 +/// BOOL TryParseTagMajorMinor(const CString& strBranch, const CString& strTag, UINT& nMajor, UINT& nMinor); + +/// +/// 获取当前分支下版本号最大的 major.minor Tag。 +/// BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString& strTag, UINT& nMajor, UINT& nMinor); + +/// +/// 基于 Tag 提交日期解析 FileVersion 的 yy、mmdd 与 id。 +/// BOOL ResolveTagFileVersionDateAndCommitCount( LPCTSTR lpRepoPath, const CString& strBranch, @@ -23,6 +57,10 @@ BOOL ResolveTagFileVersionDateAndCommitCount( UINT& nMMDD, UINT& nId, SYSTEMTIME* pOutDate = NULL); + +/// +/// 基于当前或最近提交日解析 FileVersion 的 yy、mmdd 与 id。 +/// BOOL ResolveFileVersionDateAndCommitCount( LPCTSTR lpRepoPath, const CString& strBranch, @@ -30,11 +68,19 @@ BOOL ResolveFileVersionDateAndCommitCount( UINT& nMMDD, UINT& nId, SYSTEMTIME* pOutDate = NULL); + +/// +/// 获取指定日期内 origin/分支 的提交摘要列表。 +/// BOOL TryGetGitLogByBranchDate( LPCTSTR lpRepoPath, const CString& strBranch, const SYSTEMTIME& stDate, CString& strLog); + +/// +/// 从 Git 仓库信息构建 ProductVersion 与 FileVersion 字符串。 +/// int BuildVersionsFromRepo( LPCTSTR lpRepoPath, UINT nPid, diff --git a/GitVer/resource.h b/GitVer/resource.h index d83cd2b..7e00215 100644 --- a/GitVer/resource.h +++ b/GitVer/resource.h @@ -1,6 +1,6 @@ -锘//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ 鐢熸垚鐨勫寘鍚枃浠躲 -// 渚 GitVer.rc 浣跨敤 +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ 生成的包含文件。 +// 供 GitVer.rc 使用 // #define IDI_ICON_APP 100 #define IDI_ICON1 101 diff --git a/GitVer/targetver.h b/GitVer/targetver.h index a186354..e8c3701 100644 --- a/GitVer/targetver.h +++ b/GitVer/targetver.h @@ -1,6 +1,6 @@ -锘#pragma once +#pragma once -// // 鍖呭惈 SDKDDKVer.h 鍙畾涔夊彲鐢ㄧ殑鏈楂樼増鏈殑 Windows 骞冲彴銆 -// 濡傛灉甯屾湜涓轰箣鍓嶇殑 Windows 骞冲彴鏋勫缓搴旂敤绋嬪簭锛屽湪鍖呭惈 SDKDDKVer.h 涔嬪墠璇峰厛鍖呭惈 WinSDKVer.h 骞 -// 灏 _WIN32_WINNT 瀹忚缃负鎯宠鏀寔鐨勫钩鍙般 +// // 包含 SDKDDKVer.h 可定义可用的最高版本的 Windows 平台。 +// 如果希望为之前的 Windows 平台构建应用程序,在包含 SDKDDKVer.h 之前请先包含 WinSDKVer.h 并 +// 将 _WIN32_WINNT 宏设置为想要支持的平台。 #include diff --git a/README.md b/README.md index c116498..32ec2ae 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,9 @@ gitver setup=1 pid=5 repodir=E:\Code\MyProj ``` gitver upload username=<鐢ㄦ埛鍚> [file=<涓婁紶鏂囦欢>] [info=<閰嶇疆.txt>] [url=] +gitver upload username=<鐢ㄦ埛鍚> # 闇鍦ㄥ垎鏀笂涓 HEAD 鎸囧悜 Tag +gitver upload username=<鐢ㄦ埛鍚> -f # 寮哄埗涓婁紶锛屽拷鐣 Tag 鏍¢獙 +gitver upload -f username=<鐢ㄦ埛鍚> [file=<涓婁紶鏂囦欢>] # -f 浣嶇疆浠绘剰 ``` | 鍙傛暟 | 璇存槑 |