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 浣嶇疆浠绘剰
```
| 鍙傛暟 | 璇存槑 |