1、加强注释

2、上传时,添加-f参数才会强制上传,否则需要根据当前Tag标签信息来判断是否需要上传。
This commit is contained in:
2026-06-28 20:56:56 +08:00
parent ec7b51873c
commit 6b0f602e57
24 changed files with 1248 additions and 527 deletions

View File

@@ -672,7 +672,6 @@ int HandleSetVerCommand(int argc, TCHAR* argv[])
return RewriteSetVerByProjectType(lpRepoPath, strProductVersion, strFileVersion);
}
/// <summary>
/// <summary>
/// 准备C++重写内容。
/// </summary>
@@ -854,6 +853,14 @@ int HandleRewriteCommand(int argc, TCHAR* argv[])
return 0;
}
/// <summary>
/// 程序入口:解析命令行参数并分发到各子命令处理函数。
/// 无参数时进入交互式 tag 创建模式。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="envp">环境变量数组。</param>
/// <returns>命令执行退出码0 表示成功。</returns>
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);

View File

@@ -5,11 +5,24 @@
extern TCHAR g_szCurModuleDir[MAX_PATH];
/// <summary>
/// 打印无效仓库路径的错误信息。
/// </summary>
/// <param name="lpRepoPath">无效的路径字符串。</param>
void PrintInvalidRepoPathError(LPCTSTR lpRepoPath)
{
_tprintf(_T("错误: 无效的仓库路径 %s\n"), lpRepoPath == NULL ? _T("<null>") : lpRepoPath);
}
/// <summary>
/// 解析 setver / nuitka / pyinstaller 等命令的 repodir= 与 -test 可选参数。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="nStartIndex">开始解析的下标。</param>
/// <param name="lpRepoPath">输出:仓库路径,未指定时为当前模块目录。</param>
/// <param name="bTestMode">输出:是否启用测试模式。</param>
/// <returns>0 成功3 多余参数23 无效 repodir。</returns>
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;
}
/// <summary>
/// 解析 rewrite 命令的可选 PE 类型1=exe2=dll与 -f 强制标志。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组argv[0] 为 "rewrite")。</param>
/// <param name="nPEType">输出PE 类型,默认 1。</param>
/// <param name="bForceRewrite">输出:是否忽略重写失败。</param>
/// <returns>0 成功22 PE 类型无效或重复。</returns>
int ParseRewriteOptions(int argc, TCHAR* argv[], int& nPEType, BOOL& bForceRewrite)
{
nPEType = 1;

View File

@@ -1,5 +1,16 @@
#pragma once
/// <summary>
/// 打印无效仓库路径的错误信息。
/// </summary>
void PrintInvalidRepoPathError(LPCTSTR lpRepoPath);
/// <summary>
/// 解析 setver 等命令的 repodir= 与 -test 可选参数。
/// </summary>
int ParseSetVerOptions(int argc, TCHAR* argv[], int nStartIndex, LPCTSTR& lpRepoPath, BOOL& bTestMode);
/// <summary>
/// 解析 rewrite 命令的 PE 类型与 -f 强制标志。
/// </summary>
int ParseRewriteOptions(int argc, TCHAR* argv[], int& nPEType, BOOL& bForceRewrite);

View File

@@ -2,6 +2,12 @@
#include "gitver.h"
#include "gitver_common.h"
/// <summary>
/// 将命令行参数拼接为便于日志输出的字符串(含引号转义)。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>空格分隔、已转义的参数字符串。</returns>
CString BuildArgsForLog(int argc, TCHAR* argv[])
{
CString strArgs;
@@ -16,6 +22,11 @@ CString BuildArgsForLog(int argc, TCHAR* argv[])
return strArgs;
}
/// <summary>
/// 记录命令开始时间并输出开始日志。
/// </summary>
/// <param name="lpCommandName">命令名称。</param>
/// <returns>当前 GetTickCount 值,供 LogCommandEnd 计算耗时。</returns>
DWORD LogCommandStart(LPCTSTR lpCommandName)
{
DWORD dwStartTick = GetTickCount();
@@ -23,6 +34,11 @@ DWORD LogCommandStart(LPCTSTR lpCommandName)
return dwStartTick;
}
/// <summary>
/// 为命令行参数添加引号转义(含空格、制表符或双引号时加引号)。
/// </summary>
/// <param name="strArg">原始参数字符串。</param>
/// <returns>可直接用于命令行的参数字符串。</returns>
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;
}
/// <summary>
/// 尝试将字符串解析为 0-65535 范围内的无符号整数。
/// </summary>
/// <param name="strValue">待解析字符串。</param>
/// <param name="nValue">输出:解析结果。</param>
/// <returns>解析成功返回 TRUE。</returns>
BOOL TryParseUInt16(const CString& strValue, UINT& nValue)
{
CString strTrimmed = strValue;
@@ -64,6 +86,15 @@ BOOL TryParseUInt16(const CString& strValue, UINT& nValue)
return TRUE;
}
/// <summary>
/// 解析 argv[nIndex] 为 UInt16失败时打印错误并返回指定错误码。
/// </summary>
/// <param name="argv">参数数组。</param>
/// <param name="nIndex">要解析的参数下标。</param>
/// <param name="lpArgName">参数名(用于错误提示)。</param>
/// <param name="nValue">输出:解析结果。</param>
/// <param name="nErrorCode">解析失败时的退出码。</param>
/// <returns>0 成功,否则返回 nErrorCode。</returns>
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;
}
/// <summary>
/// 判断参数字符串是否像路径(含 \、/、: 或以 . 开头)。
/// </summary>
/// <param name="lpArg">待判断的参数字符串。</param>
/// <returns>像路径则返回 TRUE。</returns>
BOOL IsLikelyPathArg(LPCTSTR lpArg)
{
if (lpArg == NULL)

View File

@@ -1,8 +1,31 @@
#pragma once
/// <summary>
/// 将命令行参数拼接为便于日志输出的字符串。
/// </summary>
CString BuildArgsForLog(int argc, TCHAR* argv[]);
/// <summary>
/// 记录命令开始时间并输出开始日志,返回 tick 供计算耗时。
/// </summary>
DWORD LogCommandStart(LPCTSTR lpCommandName);
/// <summary>
/// 为命令行参数添加引号转义。
/// </summary>
CString QuoteCmdArg(const CString& strArg);
/// <summary>
/// 尝试将字符串解析为 0-65535 范围内的无符号整数。
/// </summary>
BOOL TryParseUInt16(const CString& strValue, UINT& nValue);
/// <summary>
/// 解析 argv[nIndex] 为 UInt16失败时打印错误并返回指定错误码。
/// </summary>
int ParseUInt16ArgOrError(TCHAR* argv[], int nIndex, LPCTSTR lpArgName, UINT& nValue, int nErrorCode);
/// <summary>
/// 判断参数字符串是否像路径。
/// </summary>
BOOL IsLikelyPathArg(LPCTSTR lpArg);

View File

@@ -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;
/// <summary>
/// 解析 nuitkabuild / nuitkapydbuild 命令的 repodir=、params= 与 -test 参数。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="lpRepoPath">输出:仓库路径。</param>
/// <param name="strExtraArgs">输出:传给 Nuitka 的额外参数。</param>
/// <param name="bTestMode">输出:是否测试模式。</param>
/// <returns>0 成功3 未知/重复参数23 无效 repodir。</returns>
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;
}
/// <summary>
/// 执行 Nuitka 构建命令并输出版本信息与构建结果。
/// </summary>
/// <param name="lpStartMessage">构建开始提示语。</param>
/// <param name="strNuitkaCmd">完整 Nuitka 命令行。</param>
/// <param name="strProductVersion">产品版本号(仅日志)。</param>
/// <param name="strFileVersion">文件版本号(仅日志)。</param>
/// <param name="lpRepoPath">工作目录。</param>
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
}
}
/// <summary>
/// nuitkabuild / nuitkapydbuild 命令的公共实现:解析 pid、生成版本、调用 Nuitka。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="nCmdPrefixLen">argv[1] 中 "xxx=" 前缀长度。</param>
/// <param name="bBuildModule">TRUE 时使用 --module 模式。</param>
/// <param name="lpStartMessage">构建开始提示语。</param>
/// <param name="lpUsageName">命令名(用于错误提示)。</param>
/// <param name="nPidErrorCode">pid 无效时的退出码。</param>
/// <param name="nBidErrorCode">获取 bid 失败时的退出码。</param>
/// <param name="nTagErrorCode">获取 Tag 失败时的退出码。</param>
/// <param name="nCommitErrorCode">获取提交信息失败时的退出码。</param>
/// <returns>0 成功,否则为对应错误码。</returns>
static int HandleNuitkaBuildCommandCore(
int argc,
TCHAR* argv[],
@@ -165,6 +196,12 @@ static int HandleNuitkaBuildCommandCore(
return 0;
}
/// <summary>
/// 处理 nuitkabuild=pid 命令standalone 可执行文件打包。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>0 成功17-21 等为各类错误码。</returns>
int HandleNuitkaBuildCommand(int argc, TCHAR* argv[])
{
return HandleNuitkaBuildCommandCore(
@@ -180,6 +217,12 @@ int HandleNuitkaBuildCommand(int argc, TCHAR* argv[])
21);
}
/// <summary>
/// 处理 nuitkapydbuild=pid 命令Python 扩展模块(.pyd打包。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>0 成功24-28 等为各类错误码。</returns>
int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[])
{
return HandleNuitkaBuildCommandCore(

View File

@@ -1,4 +1,11 @@
#pragma once
/// <summary>
/// 处理 nuitkabuild=pid 命令standalone 可执行文件打包。
/// </summary>
int HandleNuitkaBuildCommand(int argc, TCHAR* argv[]);
/// <summary>
/// 处理 nuitkapydbuild=pid 命令Python 扩展模块(.pyd打包。
/// </summary>
int HandleNuitkaPydBuildCommand(int argc, TCHAR* argv[]);

View File

@@ -2,6 +2,13 @@
#include "gitver.h"
#include "gitver_process.h"
/// <summary>
/// 按顺序将文件中多处旧字符串替换为新字符串并写回。
/// </summary>
/// <param name="lpFile">目标文件路径。</param>
/// <param name="vtOldContent">待查找的旧内容列表。</param>
/// <param name="vtNewContent">对应的新内容列表。</param>
/// <returns>全部替换成功返回 TRUE。</returns>
BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent)
{
if (vtOldContent.size() != vtNewContent.size())
@@ -69,6 +76,12 @@ BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent,
return TRUE;
}
/// <summary>
/// 将子进程管道输出的原始字节按指定代码页解码为 CString。
/// </summary>
/// <param name="strRawOutput">管道原始输出字节。</param>
/// <param name="nOutputCodePage">解码代码页CP_UTF8 或 CP_ACP 等)。</param>
/// <returns>解码后的字符串。</returns>
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
}
/// <summary>
/// 启动子进程并捕获 stdout/stderr按代码页解码后返回输出。
/// </summary>
/// <param name="program">可执行程序路径NULL 时使用 args 中的命令。</param>
/// <param name="args">命令行参数。</param>
/// <param name="lpCurrentDirectory">工作目录。</param>
/// <param name="nOutputCodePage">输出解码代码页,默认 CP_ACP。</param>
/// <returns>子进程的标准输出/错误合并文本。</returns>
CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory, UINT nOutputCodePage)
{
std::string strRawOutput;

View File

@@ -1,6 +1,10 @@
#pragma once
/// <summary>
/// 按顺序将文件中多处旧字符串替换为新字符串并写回。
/// </summary>
BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent);
/// <summary>
/// 启动子进程并捕获 stdout/stderr。
/// nOutputCodePage 为管道原始字节的解码代码页,默认 CP_ACP按系统 ANSI/GBK 直接使用);

View File

@@ -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;
/// <summary>
/// 解析 pyinstaller 命令的 repodir=、params= 与 -test 参数。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <param name="lpRepoPath">输出:仓库路径。</param>
/// <param name="strExtraArgs">输出:传给 PyInstaller 的参数。</param>
/// <param name="bTestMode">输出:是否测试模式。</param>
/// <returns>0 成功3 未知/重复参数23 无效 repodir。</returns>
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" → 四个分量
/// <summary>
/// 解析版本字符串 "a.b.c.d" 为四个无符号整数分量。
/// </summary>
/// <param name="lpVer">版本字符串。</param>
/// <param name="v1">输出:第 1 段。</param>
/// <param name="v2">输出:第 2 段。</param>
/// <param name="v3">输出:第 3 段。</param>
/// <param name="v4">输出:第 4 段。</param>
/// <returns>格式正确返回 TRUE。</returns>
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支持双引号括起的路径
/// <summary>
/// 提取 params 字符串中第一个 token支持双引号括起的路径
/// </summary>
/// <param name="strParams">完整 params 参数字符串。</param>
/// <returns>第一个 token空输入返回空字符串。</returns>
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 格式)
/// <summary>
/// 在指定目录生成 PyInstaller VSVersionInfo 格式的 version_info.txt。
/// </summary>
/// <param name="lpDir">输出目录。</param>
/// <param name="lpProductVersion">产品版本号。</param>
/// <param name="lpFileVersion">文件版本号。</param>
/// <param name="strOutPath">输出:生成的文件完整路径。</param>
/// <returns>成功返回 TRUE。</returns>
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= 参数后新增一行
/// <summary>
/// 向 .spec 文件注入或更新 version='version_info.txt' 参数。
/// 已有 version= 则更新整行,否则在 name= 行后插入。
/// </summary>
/// <param name="lpSpecPath">.spec 文件路径。</param>
/// <returns>成功返回 TRUE。</returns>
static BOOL InjectVersionIntoSpec(LPCTSTR lpSpecPath)
{
if (!PathFileExists(lpSpecPath))
@@ -260,8 +292,12 @@ static BOOL InjectVersionIntoSpec(LPCTSTR lpSpecPath)
return TRUE;
}
// 从 .spec 文件读取 version= 参数的值(单/双引号括起的路径)
// 返回 FALSE 表示未找到、值为 None 或值为空
/// <summary>
/// 从 .spec 文件读取 version= 参数引号内的外部版本信息文件路径。
/// </summary>
/// <param name="lpSpecPath">.spec 文件路径。</param>
/// <param name="strVersionPath">输出version= 指向的路径。</param>
/// <returns>找到有效引号路径返回 TRUENone 或空值返回 FALSE。</returns>
static BOOL ReadVersionPathFromSpec(LPCTSTR lpSpecPath, CString& strVersionPath)
{
strVersionPath.Empty();
@@ -310,7 +346,13 @@ static BOOL ReadVersionPathFromSpec(LPCTSTR lpSpecPath, CString& strVersionPath)
return FALSE;
}
// 更新已有 version_info 文件的版本字段;若文件不存在则创建
/// <summary>
/// 更新已有 version_info 文件的版本字段;文件不存在时按模板创建。
/// </summary>
/// <param name="lpPath">version_info.txt 或内联 spec 路径。</param>
/// <param name="lpProductVersion">产品版本号。</param>
/// <param name="lpFileVersion">文件版本号。</param>
/// <returns>成功返回 TRUE。</returns>
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 对象)
/// <summary>
/// 检查 .spec 中 version= 是否为无引号的 Python 标识符(内联 VSVersionInfo 对象名)。
/// </summary>
/// <param name="lpSpecPath">.spec 文件路径。</param>
/// <returns>是内联版本块引用返回 TRUE。</returns>
static BOOL HasInlineVersionInfoBlock(LPCTSTR lpSpecPath)
{
if (!PathFileExists(lpSpecPath)) return FALSE;
@@ -504,8 +550,12 @@ static BOOL HasInlineVersionInfoBlock(LPCTSTR lpSpecPath)
return FALSE;
}
// 修改 spec 文件中 version= 的引号值(内联版本号字符串场景)
// 当 version= 后是非外部文件路径的字符串字面量时,直接替换该值为新版本号
/// <summary>
/// 替换 .spec 文件中 version= 引号内的内联版本号字符串。
/// </summary>
/// <param name="lpSpecPath">.spec 文件路径。</param>
/// <param name="lpNewValue">新版本号字符串。</param>
/// <returns>找到并替换返回 TRUE。</returns>
static BOOL UpdateVersionStringInSpec(LPCTSTR lpSpecPath, LPCTSTR lpNewValue)
{
if (!PathFileExists(lpSpecPath))
@@ -581,6 +631,12 @@ static BOOL UpdateVersionStringInSpec(LPCTSTR lpSpecPath, LPCTSTR lpNewValue)
return TRUE;
}
/// <summary>
/// 处理 pyinstaller=pid 命令:生成版本、处理 spec/version_info、调用 PyInstaller 打包。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>0 成功40-46 等为各类错误码。</returns>
int HandlePyinstallerBuildCommand(int argc, TCHAR* argv[])
{
#ifdef _DEBUG

View File

@@ -1,3 +1,6 @@
#pragma once
/// <summary>
/// 处理 pyinstaller=pid 命令:生成版本信息并调用 PyInstaller 打包。
/// </summary>
int HandlePyinstallerBuildCommand(int argc, TCHAR* argv[]);

View File

@@ -14,6 +14,12 @@ LPCTSTR GetProjectCodeTypeName(int nCodeType);
/// <summary>
/// 替换指定范围内包含指定标记的整行内容。
/// </summary>
/// <param name="strContent">文件内容(原地修改)。</param>
/// <param name="strToken">行内查找标记。</param>
/// <param name="strNewLine">替换后的整行文本。</param>
/// <param name="nRangeStart">搜索范围起始偏移。</param>
/// <param name="nRangeEnd">搜索范围结束偏移。</param>
/// <returns>找到并替换返回 TRUE。</returns>
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;
}
/// <summary>
/// 在全文件范围内替换包含指定标记的整行。
/// </summary>
BOOL ReplaceWholeLineByToken(std::string& strContent, const std::string& strToken, const std::string& strNewLine)
{
return ReplaceWholeLineByTokenInRange(strContent, strToken, strNewLine, 0, strContent.size());
}
/// <summary>
/// 替换首个匹配行,并删除后续所有含同一标记的重复行。
/// </summary>
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;
}
/// <summary>
/// 若存在含标记的行则替换并去重,否则在文件末尾追加新行。
/// </summary>
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;
}
/// <summary>
/// 定位 .rc 文件中 VS_VERSION_INFO VERSIONINFO 块的 BEGIN…END 字节范围。
/// </summary>
/// <param name="strContent">.rc 文件内容。</param>
/// <param name="nBlockStart">输出:块起始偏移。</param>
/// <param name="nBlockEnd">输出:块结束偏移(不含末尾换行之后)。</param>
/// <returns>找到完整块返回 TRUE。</returns>
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;
}
/// <summary>
/// 检测文本文件 BOM 类型UTF-16 BOM 视为不支持。
/// </summary>
/// <param name="strData">文件原始字节。</param>
/// <param name="nBomLength">输出BOM 长度(无 BOM 时为 0。</param>
/// <returns>支持的编码返回 TRUEUTF-16 返回 FALSE。</returns>
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;
}
/// <summary>
/// 以 ANSI/UTF-8带 BOM 时保留前缀)读取文本文件。
/// </summary>
/// <param name="lpFile">文件路径。</param>
/// <param name="strContent">输出:不含 BOM 的正文。</param>
/// <param name="strBomPrefix">输出:原始 BOM 前缀,写回时需保留。</param>
/// <returns>读取成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 将文本内容与 BOM 前缀写回文件。
/// </summary>
/// <param name="lpFile">文件路径。</param>
/// <param name="strContent">正文内容。</param>
/// <param name="strBomPrefix">BOM 前缀(可为空)。</param>
/// <returns>写入成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 将 ProductVersion / FileVersion 写入 C++ .rc 文件的 VERSIONINFO 块。
/// </summary>
/// <param name="strRcFile">.rc 文件路径。</param>
/// <param name="strProductVersion">产品版本号。</param>
/// <param name="strFileVersion">文件版本号。</param>
/// <returns>成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 将 ProductVersion / FileVersion 写入 C# AssemblyInfo.cs。
/// </summary>
/// <param name="strAssemblyInfoFile">AssemblyInfo.cs 路径。</param>
/// <param name="strProductVersion">产品版本号AssemblyVersion。</param>
/// <param name="strFileVersion">文件版本号AssemblyFileVersion。</param>
/// <returns>成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 按项目类型C++ / C#)将版本号写入对应资源文件。
/// </summary>
/// <param name="lpRepoPath">项目根目录NULL 时使用当前模块目录。</param>
/// <param name="strProductVersion">产品版本号。</param>
/// <param name="strFileVersion">文件版本号。</param>
/// <returns>0 成功15 不支持的项目类型35 写版本失败。</returns>
int RewriteSetVerByProjectType(LPCTSTR lpRepoPath, const CString& strProductVersion, const CString& strFileVersion)
{
LPCTSTR lpBaseDir = lpRepoPath == NULL ? g_szCurModuleDir : lpRepoPath;

View File

@@ -1,3 +1,6 @@
#pragma once
/// <summary>
/// 按项目类型C++ / C#)将版本号写入对应资源文件。
/// </summary>
int RewriteSetVerByProjectType(LPCTSTR lpRepoPath, const CString& strProductVersion, const CString& strFileVersion);

File diff suppressed because it is too large Load Diff

View File

@@ -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;
/// <summary>
/// 获取当前分支下最近 nMaxCount 个符合 major.minor 格式的 Tag。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">输出:当前分支名。</param>
/// <param name="vtTags">输出Tag 列表(按版本升序,取最后 nMaxCount 个)。</param>
/// <param name="nMaxCount">最多返回的 Tag 数量。</param>
/// <returns>成功返回 TRUE无 Tag 时 vtTags 为空亦视为成功)。</returns>
BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector<BranchTagInfo>& vtTags, int nMaxCount)
{
vtTags.clear();
@@ -73,6 +81,10 @@ BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector
return TRUE;
}
/// <summary>
/// 交互式 Tag 创建流程:展示最近 Tag、选择 major+1 或 minor+1、创建并推送。
/// </summary>
/// <returns>0 成功30-34 交互/创建/验证失败36 推送失败。</returns>
int HandleCreateTagInteractive()
{
CString strBranch;

View File

@@ -1,5 +1,8 @@
#pragma once
/// <summary>
/// 分支 Tag 的 major/minor 解析结果。
/// </summary>
struct BranchTagInfo
{
CString strTag;
@@ -7,5 +10,12 @@ struct BranchTagInfo
UINT nMinor;
};
/// <summary>
/// 获取当前分支下最近 nMaxCount 个符合 major.minor 格式的 Tag。
/// </summary>
BOOL GetRecentMajorMinorTags(LPCTSTR lpRepoPath, CString& strBranch, std::vector<BranchTagInfo>& vtTags, int nMaxCount);
/// <summary>
/// 交互式 Tag 创建流程:展示最近 Tag、选择递增方式、创建并推送。
/// </summary>
int HandleCreateTagInteractive();

View File

@@ -1,5 +1,8 @@
#pragma once
/// <summary>
/// 项目源码类型枚举。
/// </summary>
enum ProjectCodeType
{
PROJECT_UNKNOWN = 0,

View File

@@ -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
// 字符串 / 编码工具
// ─────────────────────────────────────────────
/// <summary>
/// 将宽字符(或 ANSI字符串转换为 UTF-8 编码的 std::string。
/// </summary>
/// <param name="lpText">输入字符串。</param>
/// <returns>UTF-8 编码结果,空输入返回空字符串。</returns>
static std::string ToUtf8String(LPCTSTR lpText)
{
if (lpText == NULL || lpText[0] == _T('\0'))
@@ -74,6 +81,11 @@ static std::string ToUtf8String(LPCTSTR lpText)
#endif
}
/// <summary>
/// 将 UTF-8 编码的 std::string 转换为 CString。
/// </summary>
/// <param name="strUtf8">UTF-8 字符串。</param>
/// <returns>转换后的 CString。</returns>
static CString Utf8ToCString(const std::string& strUtf8)
{
if (strUtf8.empty())
@@ -116,6 +128,11 @@ static CString Utf8ToCString(const std::string& strUtf8)
#endif
}
/// <summary>
/// 将 ANSI 编码的 std::string 转换为 CString。
/// </summary>
/// <param name="strAnsi">ANSI 字符串。</param>
/// <returns>转换后的 CString。</returns>
static CString AnsiToCString(const std::string& strAnsi)
{
if (strAnsi.empty())
@@ -139,6 +156,11 @@ static CString AnsiToCString(const std::string& strAnsi)
#endif
}
/// <summary>
/// 粗略检测字节序列是否为合法 UTF-8 编码。
/// </summary>
/// <param name="strValue">待检测的字节串。</param>
/// <returns>看起来像合法 UTF-8 则返回 TRUE。</returns>
static BOOL IsLikelyValidUtf8(const std::string& strValue)
{
size_t i = 0;
@@ -188,6 +210,11 @@ static BOOL IsLikelyValidUtf8(const std::string& strValue)
return TRUE;
}
/// <summary>
/// 将 info 文件中的原始字节值转为 CString自动区分 UTF-8 与 ANSI。
/// </summary>
/// <param name="strValue">原始字节值。</param>
/// <returns>解码后的 CString。</returns>
static CString InfoValueToCString(const std::string& strValue)
{
if (IsLikelyValidUtf8(strValue))
@@ -198,6 +225,11 @@ static CString InfoValueToCString(const std::string& strValue)
return AnsiToCString(strValue);
}
/// <summary>
/// 对 UTF-8 字符串进行 application/x-www-form-urlencoded 风格 URL 编码。
/// </summary>
/// <param name="strValue">待编码的 UTF-8 字符串。</param>
/// <returns>编码后的字符串。</returns>
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;
}
/// <summary>
/// 反转义 info 文件值中的 \n 为换行符。
/// </summary>
/// <param name="strValue">含转义序列的原始值。</param>
/// <returns>反转义后的字符串。</returns>
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;
}
/// <summary>
/// 将字符串转义后写入 info 文件(换行 → \n反斜杠 → \\)。
/// </summary>
/// <param name="strValue">原始字符串。</param>
/// <returns>适合写入 info 文件的转义字符串。</returns>
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;
}
/// <summary>
/// 去除 ANSI 字符串首尾的空格、制表符和回车。
/// </summary>
/// <param name="strValue">待修剪的字符串(原地修改)。</param>
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)
}
}
/// <summary>
/// 读取二进制文件的全部内容到字节向量。
/// </summary>
/// <param name="lpFile">文件路径。</param>
/// <param name="data">输出:文件字节内容。</param>
/// <returns>读取成功返回 TRUE。</returns>
static BOOL ReadBinaryFile(LPCTSTR lpFile, std::vector<BYTE>& data)
{
data.clear();
@@ -312,6 +364,11 @@ static BOOL ReadBinaryFile(LPCTSTR lpFile, std::vector<BYTE>& data)
return TRUE;
}
/// <summary>
/// 将 ASCII 字母转为小写(仅 A-Z
/// </summary>
/// <param name="strValue">输入字符串。</param>
/// <returns>小写化后的新字符串。</returns>
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
// ─────────────────────────────────────────────
/// <summary>
/// 去除 std::string 首尾的空格和制表符。
/// </summary>
/// <param name="strValue">待修剪的字符串(原地修改)。</param>
static void TrimStdStringInPlace(std::string& strValue)
{
while (!strValue.empty() && (strValue.front() == ' ' || strValue.front() == '\t'))
@@ -340,6 +401,11 @@ static void TrimStdStringInPlace(std::string& strValue)
}
}
/// <summary>
/// 将 Set-Cookie 响应头中的 name=value 合并到 Cookie 字符串,同名则覆盖。
/// </summary>
/// <param name="strHeaderValue">Set-Cookie 头的值部分。</param>
/// <param name="strCookies">输入/输出:累积的 Cookie 字符串。</param>
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;
}
/// <summary>
/// 从 HTTP 响应的所有 Set-Cookie 头中收集并合并 Cookie。
/// </summary>
/// <param name="response">HTTP 响应对象。</param>
/// <param name="strCookies">输出:合并后的 Cookie 字符串。</param>
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
}
}
/// <summary>
/// 根据 Cookie 字符串构造 httplib 请求头。
/// </summary>
/// <param name="strCookies">Cookie 字符串。</param>
/// <returns>含 Cookie 头的 Headers 对象。</returns>
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;
}
/// <summary>
/// 创建指向 PMC 服务的 httplib 客户端,配置超时且不自动跟随重定向。
/// </summary>
/// <param name="lpBaseUrl">PMC 基址 URL。</param>
/// <returns>有效客户端指针,无效 URL 时返回 nullptr。</returns>
static std::unique_ptr<httplib::Client> CreatePmcClient(LPCTSTR lpBaseUrl)
{
std::string strBaseUrl = ToUtf8String(lpBaseUrl);
@@ -472,6 +553,12 @@ static std::unique_ptr<httplib::Client> CreatePmcClient(LPCTSTR lpBaseUrl)
// info 文件解析
// ─────────────────────────────────────────────
/// <summary>
/// 根据键名将 info 文件中的一行键值对写入 UploadInfoConfig 结构体。
/// </summary>
/// <param name="config">输出配置对象。</param>
/// <param name="strKey">键名。</param>
/// <param name="strRawValue">原始值(含转义序列)。</param>
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
}
}
/// <summary>
/// 从 upload_info.txt 等 info 配置文件加载上传参数。
/// </summary>
/// <param name="lpInfoFile">info 文件路径。</param>
/// <param name="config">输出:解析后的配置。</param>
/// <returns>文件读取成功返回 TRUE。</returns>
static BOOL LoadUploadInfoFile(LPCTSTR lpInfoFile, UploadInfoConfig& config)
{
std::string strContent;
@@ -571,6 +664,11 @@ static BOOL LoadUploadInfoFile(LPCTSTR lpInfoFile, UploadInfoConfig& config)
return TRUE;
}
/// <summary>
/// 校验上传配置是否包含必填字段。
/// </summary>
/// <param name="config">待校验的配置。</param>
/// <returns>必填字段齐全返回 TRUE缺失时打印错误并返回 FALSE。</returns>
static BOOL ValidateUploadConfig(const UploadInfoConfig& config)
{
if (config.strPackageName.IsEmpty())
@@ -601,6 +699,12 @@ static BOOL ValidateUploadConfig(const UploadInfoConfig& config)
return TRUE;
}
/// <summary>
/// 从与用户名同名的环境变量中读取登录密码。
/// </summary>
/// <param name="lpUsername">PMC 登录用户名(亦作环境变量名)。</param>
/// <param name="strPassword">输出:读取到的密码。</param>
/// <returns>环境变量存在且非空返回 TRUE。</returns>
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;
}
/// <summary>
/// 更新 upload_info.txt 中指定键的值,保留其他行和 BOM 前缀。
/// 文件中不存在的键会追加到末尾。
/// </summary>
/// <param name="lpInfoFile">info 文件路径。</param>
/// <param name="updates">要更新的键值对映射。</param>
/// <returns>写入成功返回 TRUE。</returns>
static BOOL UpdateUploadInfoKeys(LPCTSTR lpInfoFile, const std::map<std::string, std::string>& updates)
{
std::vector<std::string> outLines;
@@ -729,19 +840,21 @@ static BOOL UpdateUploadInfoKeys(LPCTSTR lpInfoFile, const std::map<std::string,
return TRUE;
}
BOOL WriteUploadInfoVersions(
LPCTSTR lpInfoFile,
LPCTSTR lpProductVersion,
LPCTSTR lpFileVersion,
LPCTSTR lpFileChangeLog)
/// <summary>
/// 更新 upload_info.txt 中的版本号与变更日志字段。
/// </summary>
BOOL WriteUploadInfoVersions(LPCTSTR lpInfoFile, LPCTSTR lpProductVersion, LPCTSTR lpFileVersion, LPCTSTR lpFileChangeLog)
{
std::map<std::string, std::string> 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);
}
/// <summary>
/// 更新 upload_info.txt 中的 file 字段(安装包路径)。
/// </summary>
BOOL WriteUploadInfoPackageFile(LPCTSTR lpInfoFile, LPCTSTR lpPackageFile)
{
std::map<std::string, std::string> updates;
@@ -749,18 +862,64 @@ BOOL WriteUploadInfoPackageFile(LPCTSTR lpInfoFile, LPCTSTR lpPackageFile)
return UpdateUploadInfoKeys(lpInfoFile, updates);
}
/// <summary>
/// 打印 upload 命令的用法说明。
/// </summary>
static void PrintUploadUsage()
{
_tprintf(_T("用法gitver upload username=<用户名> [file=<上传的文件>] [info=<配置.txt>] [url=<PMC基址>]\n"));
_tprintf(_T("用法gitver upload username=<用户名> [file=<上传的文件>] [info=<配置.txt>] [url=<PMC基址>] [-f]\n"));
_tprintf(_T("说明info= 缺省为 upload_info.txtfile= 可省略,从 info 的 file 字段读取\n"));
_tprintf(_T("说明:登录密码从与 username 同名的环境变量读取\n"));
_tprintf(_T("说明:默认要求当前在分支上且 HEAD 指向 Tag加 -f 可忽略 Tag 校验\n"));
_tprintf(_T("示例gitver upload username=admin\n"));
}
/// <summary>
/// 校验当前 Git 仓库是否处于可上传状态(在分支上且 HEAD 指向 Tag
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <returns>0 表示通过54 表示 HEAD 游离或未指向 Tag。</returns>
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
// ─────────────────────────────────────────────
/// <summary>
/// 向 PMC 发送登录请求,获取 Session Cookie。
/// </summary>
/// <param name="cli">httplib 客户端。</param>
/// <param name="lpUsername">登录用户名。</param>
/// <param name="lpPassword">登录密码。</param>
/// <param name="strCookies">输出:登录成功后的 Cookie 字符串。</param>
/// <returns>0 成功50 登录失败53 网络错误。</returns>
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;
}
/// <summary>
/// 以 multipart/form-data 方式上传安装包到 PMC。
/// </summary>
/// <param name="cli">httplib 客户端。</param>
/// <param name="strCookies">登录 Session Cookie。</param>
/// <param name="config">上传配置(包名、版本、变更日志等)。</param>
/// <param name="lpFilePath">待上传的文件路径。</param>
/// <returns>0 成功48 文件读取失败51 上传 HTTP 失败53 网络错误。</returns>
static int PmcUploadPackage(httplib::Client& cli, const std::string& strCookies, const UploadInfoConfig& config, LPCTSTR lpFilePath)
{
std::vector<BYTE> fileData;
@@ -856,6 +1023,13 @@ static int PmcUploadPackage(httplib::Client& cli, const std::string& strCookies,
return 0;
}
/// <summary>
/// 在 JSON 响应体中查找 "field":"value" 形式的字段值(简单字符串匹配)。
/// </summary>
/// <param name="strBody">JSON 响应正文。</param>
/// <param name="lpField">字段名。</param>
/// <param name="lpValue">期望的字段值。</param>
/// <returns>找到匹配则返回 TRUE。</returns>
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;
}
/// <summary>
/// 调用 PMC API 验证上传后是否存在对应包名和版本记录。
/// </summary>
/// <param name="cli">httplib 客户端。</param>
/// <param name="strCookies">登录 Session Cookie。</param>
/// <param name="config">含包名和版本信息的配置。</param>
/// <returns>0 验证通过52 未找到版本53 网络错误。</returns>
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
// 命令入口
// ─────────────────────────────────────────────
/// <summary>
/// 处理 upload 命令:解析参数、登录 PMC、上传包并验证结果。
/// </summary>
/// <param name="argc">参数个数。</param>
/// <param name="argv">参数数组。</param>
/// <returns>0 成功47-54 各类错误,详见头文件说明。</returns>
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))
{

View File

@@ -3,7 +3,7 @@
/// <summary>
/// 处理 upload 命令:登录 PMC 并上传包文件。
/// 退出码0 成功47 缺少参数48 文件不存在49 info 无效或未设置同名环境变量密码,
/// 50 登录失败51 上传 HTTP 失败52 验证未找到版本53 网络错误。
/// 50 登录失败51 上传 HTTP 失败52 验证未找到版本53 网络错误54 Tag/分支校验失败
/// </summary>
int HandleUploadCommand(int argc, TCHAR* argv[]);

View File

@@ -4,6 +4,12 @@
#include "gitver_process.h"
#include "gitver_version.h"
/// <summary>
/// 检测 Git 仓库是否处于正常分支状态(非 HEAD 游离)。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strStatus">输出symbolic-ref HEAD 结果。</param>
/// <returns>在分支上返回 TRUE游离状态返回 FALSE。</returns>
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;
}
/// <summary>
/// 获取当前检出的本地分支名称。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">输出:分支名。</param>
/// <returns>成功返回 TRUE。</returns>
BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch)
{
_tprintf(_T("获取当前分支名称: \n"));
@@ -35,6 +47,12 @@ BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch)
return TRUE;
}
/// <summary>
/// 在 HEAD 游离时通过远程分支推断当前所属分支名origin/ 前缀已去除)。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">输出:推断的分支名。</param>
/// <returns>找到远程分支返回 TRUE。</returns>
BOOL TryGetDetectedBranch(LPCTSTR lpRepoPath, CString& strBranch)
{
// 远程仓库名必须以 origin 开头;
@@ -54,6 +72,12 @@ BOOL TryGetDetectedBranch(LPCTSTR lpRepoPath, CString& strBranch)
return TRUE;
}
/// <summary>
/// 获取指向当前 HEAD 的 Tag 名称。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strTag">输出Tag 名(多个时取 git 输出首行)。</param>
/// <returns>存在 Tag 返回 TRUE。</returns>
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;
}
/// <summary>
/// 从分支名解析 bid分支 IDmain/master=0release=1dev=2其余取末段数字。
/// </summary>
/// <param name="strBranch">分支名称。</param>
/// <param name="nBid">输出:分支 bid。</param>
/// <returns>解析成功返回 TRUE。</returns>
BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid)
{
nBid = 0;
@@ -106,6 +136,14 @@ BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid)
return TryParseUInt16(strNum, nBid);
}
/// <summary>
/// 解析符合「分支名.major.minor」格式的 Tag提取 major 与 minor。
/// </summary>
/// <param name="strBranch">分支名称。</param>
/// <param name="strTag">Tag 名称。</param>
/// <param name="nMajor">输出:主版本号。</param>
/// <param name="nMinor">输出:次版本号。</param>
/// <returns>格式匹配且解析成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 获取当前分支下版本号最大的 major.minor Tag按 v:refname 排序遍历)。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="strTag">输出:最新 Tag 全名。</param>
/// <param name="nMajor">输出:主版本号。</param>
/// <param name="nMinor">输出:次版本号。</param>
/// <returns>找到匹配 Tag 返回 TRUE。</returns>
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();
}
/// <summary>
/// 将 SYSTEMTIME 转换为 FileVersion 中的 yy 与 mmdd 分量。
/// 月份 1-12 映射为 11-22mmdd = (month+10)*100 + day
/// </summary>
/// <param name="stLocal">本地时间。</param>
/// <param name="nYY">输出:年份后两位。</param>
/// <param name="nMMDD">输出:编码后的月日。</param>
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;
}
/// <summary>
/// 统计指定日期内 origin/分支 的提交次数(用于 FileVersion 末段 id
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="stDate">目标日期。</param>
/// <param name="nId">输出:当日提交数。</param>
/// <returns>成功返回 TRUE。</returns>
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);
}
/// <summary>
/// 获取分支最近一次提交的日期git log -1 --date=short
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="stDate">输出:提交日期。</param>
/// <returns>成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 获取 Tag 对应提交的日期git log -1 --date=short
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strTag">Tag 名称。</param>
/// <param name="stDate">输出:提交日期。</param>
/// <returns>成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 基于 Tag 提交日期解析 FileVersion 的 yy、mmdd 与 id当日提交数
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="strTag">Tag 名称。</param>
/// <param name="nYY">输出:年份后两位。</param>
/// <param name="nMMDD">输出:编码月日。</param>
/// <param name="nId">输出:提交序号。</param>
/// <param name="pOutDate">可选输出Tag 提交日期。</param>
/// <returns>成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 基于当前日期(或最近提交日)解析 FileVersion 的 yy、mmdd 与 id。
/// 当天提交数为 0 时回退到最近一次提交日期。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="nYY">输出:年份后两位。</param>
/// <param name="nMMDD">输出:编码月日。</param>
/// <param name="nId">输出:提交序号。</param>
/// <param name="pOutDate">可选输出:采用的日期。</param>
/// <returns>成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 获取指定日期内 origin/分支 的提交摘要列表UTF-8 输出,一行一条)。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="strBranch">分支名称。</param>
/// <param name="stDate">目标日期。</param>
/// <param name="strLog">输出:提交摘要,多行以换行分隔。</param>
/// <returns>成功返回 TRUE。</returns>
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;
}
/// <summary>
/// 从 Git 仓库信息构建 ProductVersion 与 FileVersion 字符串。
/// 正常分支ProductVersion = pid.bid.major.minorFileVersion = pid.yy.mmdd.id。
/// HEAD 游离时改用远程分支与 Tag 解析。
/// </summary>
/// <param name="lpRepoPath">仓库根目录。</param>
/// <param name="nPid">产品 ID。</param>
/// <param name="errorCodes">各步骤失败时的退出码映射。</param>
/// <param name="strProductVersion">输出:产品版本号。</param>
/// <param name="strFileVersion">输出:文件版本号。</param>
/// <param name="nDefaultMajor">无 Tag 时的默认 major。</param>
/// <param name="nDefaultMinor">无 Tag 时的默认 minor。</param>
/// <param name="pOutBranch">可选输出:使用的分支名。</param>
/// <param name="pOutFileVersionDate">可选输出FileVersion 采用的日期。</param>
/// <returns>0 成功,否则返回 errorCodes 中对应错误码。</returns>
int BuildVersionsFromRepo(
LPCTSTR lpRepoPath,
UINT nPid,

View File

@@ -1,5 +1,8 @@
#pragma once
/// <summary>
/// BuildVersionsFromRepo 各步骤失败时使用的退出码映射。
/// </summary>
struct VersionBuildErrorCodes
{
int nBidErrorCode;
@@ -8,13 +11,44 @@ struct VersionBuildErrorCodes
int nStatusErrorCode;
};
/// <summary>
/// 检测 Git 仓库是否处于正常分支状态(非 HEAD 游离)。
/// </summary>
BOOL TryGetGitStatus(LPCTSTR lpRepoPath, CString& strStatus);
/// <summary>
/// 获取当前检出的本地分支名称。
/// </summary>
BOOL TryGetCurrentBranch(LPCTSTR lpRepoPath, CString& strBranch);
/// <summary>
/// 在 HEAD 游离时推断当前所属远程分支名。
/// </summary>
BOOL TryGetDetectedBranch(LPCTSTR lpRepoPath, CString& strBranch);
/// <summary>
/// 获取指向当前 HEAD 的 Tag 名称。
/// </summary>
BOOL TryGetDetectedTag(LPCTSTR lpRepoPath, CString& strTag);
/// <summary>
/// 从分支名解析 bid分支 ID
/// </summary>
BOOL TryGetBidFromBranch(const CString& strBranch, UINT& nBid);
/// <summary>
/// 解析符合「分支名.major.minor」格式的 Tag。
/// </summary>
BOOL TryParseTagMajorMinor(const CString& strBranch, const CString& strTag, UINT& nMajor, UINT& nMinor);
/// <summary>
/// 获取当前分支下版本号最大的 major.minor Tag。
/// </summary>
BOOL GetLastMajorMinorTag(LPCTSTR lpRepoPath, const CString& strBranch, CString& strTag, UINT& nMajor, UINT& nMinor);
/// <summary>
/// 基于 Tag 提交日期解析 FileVersion 的 yy、mmdd 与 id。
/// </summary>
BOOL ResolveTagFileVersionDateAndCommitCount(
LPCTSTR lpRepoPath,
const CString& strBranch,
@@ -23,6 +57,10 @@ BOOL ResolveTagFileVersionDateAndCommitCount(
UINT& nMMDD,
UINT& nId,
SYSTEMTIME* pOutDate = NULL);
/// <summary>
/// 基于当前或最近提交日解析 FileVersion 的 yy、mmdd 与 id。
/// </summary>
BOOL ResolveFileVersionDateAndCommitCount(
LPCTSTR lpRepoPath,
const CString& strBranch,
@@ -30,11 +68,19 @@ BOOL ResolveFileVersionDateAndCommitCount(
UINT& nMMDD,
UINT& nId,
SYSTEMTIME* pOutDate = NULL);
/// <summary>
/// 获取指定日期内 origin/分支 的提交摘要列表。
/// </summary>
BOOL TryGetGitLogByBranchDate(
LPCTSTR lpRepoPath,
const CString& strBranch,
const SYSTEMTIME& stDate,
CString& strLog);
/// <summary>
/// 从 Git 仓库信息构建 ProductVersion 与 FileVersion 字符串。
/// </summary>
int BuildVersionsFromRepo(
LPCTSTR lpRepoPath,
UINT nPid,

View File

@@ -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

View File

@@ -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 <SDKDDKVer.h>

View File

@@ -298,6 +298,9 @@ gitver setup=1 pid=5 repodir=E:\Code\MyProj
```
gitver upload username=<用户名> [file=<上传文件>] [info=<配置.txt>] [url=<PMC基址>]
gitver upload username=<用户名> # 需在分支上且 HEAD 指向 Tag
gitver upload username=<用户名> -f # 强制上传,忽略 Tag 校验
gitver upload -f username=<用户名> [file=<上传文件>] # -f 位置任意
```
| 参数 | 说明 |