1、新增上传命令行参数:upload

2、上传参数关联函数调整与优化
This commit is contained in:
2026-06-26 17:16:44 +08:00
parent 46134343e3
commit 04904c2919
13 changed files with 22264 additions and 28 deletions

View File

@@ -14,6 +14,7 @@
#include "gitver_types.h" #include "gitver_types.h"
#include "gitver_version.h" #include "gitver_version.h"
#include "gitver_setup.h" #include "gitver_setup.h"
#include "gitver_upload.h"
#ifdef _DEBUG #ifdef _DEBUG
#define new DEBUG_NEW #define new DEBUG_NEW
@@ -355,6 +356,7 @@ void PrintFullUsageExamples()
_T(" gitver nuitkapydbuild=<pid> [repodir=<path>可选] [-test可选] [params=\"<modulePy> <nuitka参数>\"可选]\n") _T(" gitver nuitkapydbuild=<pid> [repodir=<path>可选] [-test可选] [params=\"<modulePy> <nuitka参数>\"可选]\n")
_T(" gitver pyinstaller=<pid> [repodir=<path>可选] [-test可选] [params=\"<mainPy|specFile> <pyinstaller参数>\"可选]\n") _T(" gitver pyinstaller=<pid> [repodir=<path>可选] [-test可选] [params=\"<mainPy|specFile> <pyinstaller参数>\"可选]\n")
_T(" gitver setup=n [pid=m可选] [repodir=<path>可选] [-test可选]\n") _T(" gitver setup=n [pid=m可选] [repodir=<path>可选] [-test可选]\n")
_T(" gitver upload username=<用户名> [file=<上传文件>] [info=<配置.txt>] [url=<PMC基址>]\n")
_T(" setup=0: 使用 Inno Setup 脚本 (setup.iss)\n") _T(" setup=0: 使用 Inno Setup 脚本 (setup.iss)\n")
_T(" setup=1: 使用 NSIS 脚本 (setup.nsi)\n") _T(" setup=1: 使用 NSIS 脚本 (setup.nsi)\n")
_T(" 不指定 pid 时自动从 .rc / AssemblyInfo.cs / version_info.txt 读取版本\n") _T(" 不指定 pid 时自动从 .rc / AssemblyInfo.cs / version_info.txt 读取版本\n")
@@ -381,6 +383,7 @@ void PrintFullUsageExamples()
_T(" gitver setup=0\n") _T(" gitver setup=0\n")
_T(" gitver setup=0 pid=5\n") _T(" gitver setup=0 pid=5\n")
_T(" gitver setup=1 pid=5 repodir=E:\\Code\\MyProj\n") _T(" gitver setup=1 pid=5 repodir=E:\\Code\\MyProj\n")
_T(" gitver upload username=admin\n")
_T("params: 传递给 Nuitka/PyInstaller 的参数(含入口文件),用双引号括起来,如 params=\"main.py --standalone --output-dir=dist\"\n") _T("params: 传递给 Nuitka/PyInstaller 的参数(含入口文件),用双引号括起来,如 params=\"main.py --standalone --output-dir=dist\"\n")
_T("-test: 将产品版本号的 major 和 minor 都置为 0用于测试版本构建\n") _T("-test: 将产品版本号的 major 和 minor 都置为 0用于测试版本构建\n")
_T("无参数时会读取当前分支最近三次 tag并提示选择 major 加 1 或 minor 加 1然后创建新 tag。\n") _T("无参数时会读取当前分支最近三次 tag并提示选择 major 加 1 或 minor 加 1然后创建新 tag。\n")
@@ -399,6 +402,7 @@ void PrintShortCommandUsage()
_tprintf(_T("gitver nuitkabuild=<pid> [repodir=<path>可选] [params=\"<mainPy> <nuitka参数>\"可选]\n")); _tprintf(_T("gitver nuitkabuild=<pid> [repodir=<path>可选] [params=\"<mainPy> <nuitka参数>\"可选]\n"));
_tprintf(_T("gitver nuitkapydbuild=<pid> [repodir=<path>可选] [params=\"<modulePy> <nuitka参数>\"可选]\n")); _tprintf(_T("gitver nuitkapydbuild=<pid> [repodir=<path>可选] [params=\"<modulePy> <nuitka参数>\"可选]\n"));
_tprintf(_T("gitver pyinstaller=<pid> [repodir=<path>可选] [params=\"<mainPy|specFile> <pyinstaller参数>\"可选]\n")); _tprintf(_T("gitver pyinstaller=<pid> [repodir=<path>可选] [params=\"<mainPy|specFile> <pyinstaller参数>\"可选]\n"));
_tprintf(_T("gitver upload username=<用户名> [file=<上传文件>] [info=<配置.txt>] [url=<PMC基址>]\n"));
} }
/// <summary> /// <summary>
@@ -507,6 +511,20 @@ LPCTSTR GetExitCodeHint(int nRetCode)
return _T("pyinstaller: write version_info.txt failed"); return _T("pyinstaller: write version_info.txt failed");
case 46: case 46:
return _T("pyinstaller: inject version into spec file failed"); return _T("pyinstaller: inject version into spec file failed");
case 47:
return _T("upload: missing required args");
case 48:
return _T("upload: file not found");
case 49:
return _T("upload: info invalid or env password missing");
case 50:
return _T("upload: login failed");
case 51:
return _T("upload: upload http failed");
case 52:
return _T("upload: version not found after upload");
case 53:
return _T("upload: network error");
default: default:
return _T(""); return _T("");
} }
@@ -605,6 +623,8 @@ int HandleSetVerCommand(int argc, TCHAR* argv[])
CString strProductVersion; CString strProductVersion;
CString strFileVersion; CString strFileVersion;
CString strBranch;
SYSTEMTIME stFileVersionDate = { 0 };
VersionBuildErrorCodes errorCodes = { 5, 6, 9 }; VersionBuildErrorCodes errorCodes = { 5, 6, 9 };
int nVersionRet = BuildVersionsFromRepo( int nVersionRet = BuildVersionsFromRepo(
lpRepoPath, lpRepoPath,
@@ -613,7 +633,9 @@ int HandleSetVerCommand(int argc, TCHAR* argv[])
strProductVersion, strProductVersion,
strFileVersion, strFileVersion,
DEFAULT_MAJOR_WHEN_NO_TAG, DEFAULT_MAJOR_WHEN_NO_TAG,
DEFAULT_MINOR_WHEN_NO_TAG); DEFAULT_MINOR_WHEN_NO_TAG,
&strBranch,
&stFileVersionDate);
if (nVersionRet != 0) if (nVersionRet != 0)
{ {
return nVersionRet; return nVersionRet;
@@ -633,6 +655,20 @@ int HandleSetVerCommand(int argc, TCHAR* argv[])
_tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString()); _tprintf(_T("ProductVersion=%s\n"), strProductVersion.GetString());
_tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString()); _tprintf(_T("FileVersion=%s\n"), strFileVersion.GetString());
CString strChangeLog;
if (!TryGetGitLogByBranchDate(lpRepoPath, strBranch, stFileVersionDate, strChangeLog))
{
strChangeLog = _T("无当日提交记录");
}
TCHAR szInfoPath[MAX_PATH] = { 0 };
_tcscpy_s(szInfoPath, lpRepoPath);
PathAppend(szInfoPath, _T("upload_info.txt"));
if (!WriteUploadInfoVersions(szInfoPath, strProductVersion, strFileVersion, strChangeLog))
{
_tprintf(_T("警告: 未能写入 upload_info.txt\n"));
}
return RewriteSetVerByProjectType(lpRepoPath, strProductVersion, strFileVersion); return RewriteSetVerByProjectType(lpRepoPath, strProductVersion, strFileVersion);
} }
@@ -909,6 +945,15 @@ int main(int argc, TCHAR* argv[], TCHAR* envp[])
return nCmdRet; return nCmdRet;
} }
if (argc >= 2 && _tcsicmp(argv[1], _T("upload")) == 0)
{
DWORD dwStartTick = LogCommandStart(_T("upload"));
int nCmdRet = HandleUploadCommand(argc, argv);
LogCommandEnd(_T("upload"), dwStartTick, nCmdRet);
PrintCommandFailedWithCode(_T("upload"), nCmdRet);
return nCmdRet;
}
_tprintf(_T("错误:未知命令:%s\n"), argv[1]); _tprintf(_T("错误:未知命令:%s\n"), argv[1]);
PrintShortCommandUsage(); PrintShortCommandUsage();

View File

@@ -77,7 +77,7 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>..\..\..\..\bin\$(ProjectName)\</OutDir> <OutDir>..\..\..\..\bin\$(ProjectName)\</OutDir>
<IntDir>$(OutDir)$(Configuration)\</IntDir> <IntDir>$(OutDir)$(Configuration)\</IntDir>
<TargetName>$(ProjectName)d</TargetName> <TargetName>$(ProjectName)</TargetName>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>..\..\..\..\bin\$(ProjectName)\</OutDir> <OutDir>..\..\..\..\bin\$(ProjectName)\</OutDir>
@@ -153,9 +153,11 @@
<ClInclude Include="GitVer_process.h" /> <ClInclude Include="GitVer_process.h" />
<ClInclude Include="GitVer_rewrite.h" /> <ClInclude Include="GitVer_rewrite.h" />
<ClInclude Include="GitVer_setup.h" /> <ClInclude Include="GitVer_setup.h" />
<ClInclude Include="GitVer_upload.h" />
<ClInclude Include="GitVer_tag.h" /> <ClInclude Include="GitVer_tag.h" />
<ClInclude Include="GitVer_types.h" /> <ClInclude Include="GitVer_types.h" />
<ClInclude Include="GitVer_version.h" /> <ClInclude Include="GitVer_version.h" />
<ClInclude Include="httplib.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<ClInclude Include="Resource.h" /> <ClInclude Include="Resource.h" />
<ClInclude Include="targetver.h" /> <ClInclude Include="targetver.h" />
@@ -169,6 +171,7 @@
<ClCompile Include="GitVer_process.cpp" /> <ClCompile Include="GitVer_process.cpp" />
<ClCompile Include="GitVer_rewrite.cpp" /> <ClCompile Include="GitVer_rewrite.cpp" />
<ClCompile Include="GitVer_setup.cpp" /> <ClCompile Include="GitVer_setup.cpp" />
<ClCompile Include="GitVer_upload.cpp" />
<ClCompile Include="GitVer_tag.cpp" /> <ClCompile Include="GitVer_tag.cpp" />
<ClCompile Include="GitVer_version.cpp" /> <ClCompile Include="GitVer_version.cpp" />
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">

View File

@@ -60,6 +60,12 @@
<ClInclude Include="GitVer_setup.h"> <ClInclude Include="GitVer_setup.h">
<Filter>头文件</Filter> <Filter>头文件</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="GitVer_upload.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="httplib.h">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="GitVer.cpp"> <ClCompile Include="GitVer.cpp">
@@ -95,6 +101,9 @@
<ClCompile Include="GitVer_setup.cpp"> <ClCompile Include="GitVer_setup.cpp">
<Filter>源文件</Filter> <Filter>源文件</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="GitVer_upload.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="GitVer.rc"> <ResourceCompile Include="GitVer.rc">

View File

@@ -69,9 +69,60 @@ BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent,
return TRUE; return TRUE;
} }
CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory) static CString DecodeProcessOutput(const std::string& strRawOutput, UINT nOutputCodePage)
{ {
CString strValue = _T(""); if (strRawOutput.empty())
{
return CString();
}
if (nOutputCodePage == CP_UTF8)
{
int nWideSize = MultiByteToWideChar(CP_UTF8, 0, strRawOutput.data(), (int)strRawOutput.size(), NULL, 0);
if (nWideSize <= 0)
{
return CString();
}
CStringW strWide;
MultiByteToWideChar(CP_UTF8, 0, strRawOutput.data(), (int)strRawOutput.size(), strWide.GetBuffer(nWideSize), nWideSize);
strWide.ReleaseBuffer(nWideSize);
#ifdef UNICODE
return CString(strWide);
#else
int nGbkSize = WideCharToMultiByte(CP_ACP, 0, strWide, strWide.GetLength(), NULL, 0, NULL, NULL);
if (nGbkSize <= 0)
{
return CString();
}
std::string strGbk;
strGbk.resize(nGbkSize);
WideCharToMultiByte(CP_ACP, 0, strWide, strWide.GetLength(), &strGbk[0], nGbkSize, NULL, NULL);
return CString(strGbk.c_str(), (int)strGbk.size());
#endif
}
#ifdef UNICODE
int nSize = MultiByteToWideChar(nOutputCodePage, 0, strRawOutput.data(), (int)strRawOutput.size(), NULL, 0);
if (nSize <= 0)
{
return CString();
}
CString strValue;
MultiByteToWideChar(nOutputCodePage, 0, strRawOutput.data(), (int)strRawOutput.size(), strValue.GetBuffer(nSize), nSize);
strValue.ReleaseBuffer(nSize);
return strValue;
#else
return CString(strRawOutput.c_str(), (int)strRawOutput.size());
#endif
}
CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory, UINT nOutputCodePage)
{
std::string strRawOutput;
const int MY_PIPE_BUFFER_SIZE = 8912; const int MY_PIPE_BUFFER_SIZE = 8912;
CString strMutableArgs = args == NULL ? _T("") : args; CString strMutableArgs = args == NULL ? _T("") : args;
LPTSTR lpArgs = strMutableArgs.IsEmpty() ? NULL : strMutableArgs.GetBuffer(); LPTSTR lpArgs = strMutableArgs.IsEmpty() ? NULL : strMutableArgs.GetBuffer();
@@ -128,12 +179,7 @@ CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory)
break; break;
} }
szPipeOut[dwStdLen] = '\0'; strRawOutput.append(szPipeOut, dwStdLen);
#ifdef UNICODE
strValue.AppendFormat(_T("%hs"), szPipeOut);
#else
strValue += szPipeOut;
#endif
} }
WaitForSingleObject(processInfo.hProcess, INFINITE); WaitForSingleObject(processInfo.hProcess, INFINITE);
@@ -160,6 +206,8 @@ CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory)
strMutableArgs.ReleaseBuffer(); strMutableArgs.ReleaseBuffer();
} }
CString strValue = DecodeProcessOutput(strRawOutput, nOutputCodePage);
_tprintf(_T("StartProcess program=%s args=%s cwd=%s output=%s\n"), _tprintf(_T("StartProcess program=%s args=%s cwd=%s output=%s\n"),
program == NULL ? _T("<null>") : program, program == NULL ? _T("<null>") : program,
args == NULL ? _T("<null>") : args, args == NULL ? _T("<null>") : args,

View File

@@ -1,4 +1,9 @@
#pragma once #pragma once
BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent); BOOL ReplaceFileContent(LPCTSTR lpFile, std::vector<std::string>& vtOldContent, std::vector<std::string>& vtNewContent);
CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory); /// <summary>
/// 启动子进程并捕获 stdout/stderr。
/// nOutputCodePage 为管道原始字节的解码代码页,默认 CP_ACP按系统 ANSI/GBK 直接使用);
/// CP_UTF8 时先按 UTF-8 解码为宽字符MultiByte 工程再转为 GBK 存入 CString。
/// </summary>
CString StartProcess(LPCTSTR program, LPCTSTR args, LPCTSTR lpCurrentDirectory, UINT nOutputCodePage = CP_ACP);

View File

@@ -5,6 +5,7 @@
#include "gitver_cli.h" #include "gitver_cli.h"
#include "gitver_version.h" #include "gitver_version.h"
#include "gitver_process.h" #include "gitver_process.h"
#include "gitver_upload.h"
extern TCHAR g_szCurModuleDir[MAX_PATH]; extern TCHAR g_szCurModuleDir[MAX_PATH];
@@ -344,13 +345,143 @@ static BOOL ReadVersionFromProjectFiles(LPCTSTR lpBaseDir, CString& strVersion)
return FALSE; return FALSE;
} }
// ─────────────────────────────────────────────
// 解析编译输出中的安装包路径
// ─────────────────────────────────────────────
static CString ExtractQuotedPath(const CString& strText)
{
int nStart = strText.Find(_T('"'));
if (nStart < 0)
{
return strText;
}
int nEnd = strText.Find(_T('"'), nStart + 1);
if (nEnd <= nStart)
{
return strText;
}
return strText.Mid(nStart + 1, nEnd - nStart - 1);
}
static BOOL FindNewestExeInDirectory(LPCTSTR lpDir, CString& strExePath)
{
strExePath.Empty();
if (lpDir == NULL || lpDir[0] == _T('\0') || !PathIsDirectory(lpDir))
{
return FALSE;
}
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;
}
FILETIME ftNewest = { 0 };
BOOL bFound = FALSE;
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));
::FindClose(hFind);
return bFound && PathFileExists(strExePath);
}
static BOOL ParseInstallerPathFromBuildOutput(int nSetupType, const CString& strOutput, const CString& strScriptDir, CString& strInstallerPath)
{
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();
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;
}
}
CString strOutputDir;
strOutputDir.Format(_T("%s\\Output"), strScriptDir.GetString());
if (FindNewestExeInDirectory(strOutputDir, strInstallerPath))
{
return TRUE;
}
return FindNewestExeInDirectory(strScriptDir, strInstallerPath);
}
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
// 公共执行函数 // 公共执行函数
// 不回写版本信息到项目文件,只修改安装脚本并调用编译器打包。 // 不回写版本信息到项目文件,只修改安装脚本并调用编译器打包。
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion) BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion, CString& strInstallerPath)
{ {
strInstallerPath.Empty();
#ifdef _DEBUG #ifdef _DEBUG
Sleep(1500); Sleep(1500);
#endif #endif
@@ -412,6 +543,14 @@ BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const C
{ {
_tprintf(_T("警告: 编译器无输出,可能执行失败。\n")); _tprintf(_T("警告: 编译器无输出,可能执行失败。\n"));
} }
else if (!ParseInstallerPathFromBuildOutput(nSetupType, strOutput, strScriptDir, strInstallerPath))
{
_tprintf(_T("警告: 未能从编译输出中识别安装包路径。\n"));
}
else
{
_tprintf(_T("安装包路径: %s\n"), strInstallerPath.GetString());
}
return TRUE; return TRUE;
} }
@@ -538,10 +677,22 @@ int HandleSetupCommand(int argc, TCHAR* argv[])
} }
// 修改安装脚本并编译打包 // 修改安装脚本并编译打包
if (!ExecuteSetupBuild((int)nSetupType, strProductVersion, strFileVersion)) CString strInstallerPath;
if (!ExecuteSetupBuild((int)nSetupType, strProductVersion, strFileVersion, strInstallerPath))
{ {
return 38; 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"));
}
}
return 0; return 0;
} }

View File

@@ -1,16 +1,16 @@
#pragma once #pragma once
/// <summary> /// <summary>
/// 查找安装脚本、修改版本号并调用编译器打包。 /// 查找安装脚本、修改版本号并调用编译器打包。
/// nSetupType=0: Inno Setup (.iss)nSetupType=1: NSIS (.nsi)。 /// nSetupType=0: Inno Setup (.iss)nSetupType=1: NSIS (.nsi)。
/// 不写回项目版本文件(.rc / AssemblyInfo.cs /// 不写回项目版本文件(.rc / AssemblyInfo.cs
/// </summary> /// </summary>
/// <returns>TRUE 成功FALSE 失败(错误已打印)。</returns> /// <returns>TRUE 成功FALSE 失败(错误已打印)。</returns>
BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion); BOOL ExecuteSetupBuild(int nSetupType, const CString& strProductVersion, const CString& strFileVersion, CString& strInstallerPath);
/// <summary> /// <summary>
/// 处理独立的 setup=n 命令行入口。 /// 处理独立的 setup=n 命令行入口。
/// 退出码0 成功36 setup 值不支持37 未找到脚本, /// 退出码0 成功36 setup 值不支持37 未找到脚本,
/// 38 修改脚本失败39 未找到编译器,其他同通用错误码。 /// 38 修改脚本失败39 未找到编译器,其他同通用错误码。
/// </summary> /// </summary>
int HandleSetupCommand(int argc, TCHAR* argv[]); int HandleSetupCommand(int argc, TCHAR* argv[]);

929
GitVer/GitVer_upload.cpp Normal file
View File

@@ -0,0 +1,929 @@
#include "pch.h"
#include "GitVer_upload.h"
#include "httplib.h"
#include <memory>
#include <string>
#include <vector>
#include <map>
extern std::string ToAnsiString(LPCTSTR lpText);
extern BOOL ReadTextFileAsAnsi(LPCTSTR lpFile, std::string& strContent, std::string& strBomPrefix);
static const LPCTSTR DEFAULT_BASE_URL = _T("http://tools-center.mokadisplay.com/");
struct UploadInfoConfig
{
CString strBaseUrl;
CString strPackageFile;
CString strPackageName;
CString strPackageTypeId;
CString strProductVersion;
CString strFileVersion;
CString strFileChangeLog;
CString strProductChangeLog;
CString strDescription;
CString strWikiUrl;
};
// ─────────────────────────────────────────────
// 字符串 / 编码工具
// ─────────────────────────────────────────────
static std::string ToUtf8String(LPCTSTR lpText)
{
if (lpText == NULL || lpText[0] == _T('\0'))
{
return std::string();
}
#ifdef UNICODE
int nSize = WideCharToMultiByte(CP_UTF8, 0, lpText, -1, NULL, 0, NULL, NULL);
if (nSize <= 1)
{
return std::string();
}
std::string strValue;
strValue.resize(nSize - 1);
WideCharToMultiByte(CP_UTF8, 0, lpText, -1, &strValue[0], nSize, NULL, NULL);
return strValue;
#else
return std::string(lpText);
#endif
}
static CString Utf8ToCString(const std::string& strUtf8)
{
if (strUtf8.empty())
{
return CString();
}
#ifdef UNICODE
int nSize = MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), (int)strUtf8.size(), NULL, 0);
if (nSize <= 0)
{
return CString();
}
CString strValue;
MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), (int)strUtf8.size(), strValue.GetBuffer(nSize), nSize);
strValue.ReleaseBuffer(nSize);
return strValue;
#else
int nSize = MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), (int)strUtf8.size(), NULL, 0);
if (nSize <= 0)
{
return CString();
}
CStringW strWide;
MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), (int)strUtf8.size(), strWide.GetBuffer(nSize), nSize);
strWide.ReleaseBuffer(nSize);
nSize = WideCharToMultiByte(CP_ACP, 0, strWide, -1, NULL, 0, NULL, NULL);
if (nSize <= 1)
{
return CString();
}
CString strValue;
WideCharToMultiByte(CP_ACP, 0, strWide, -1, strValue.GetBuffer(nSize), nSize, NULL, NULL);
strValue.ReleaseBuffer();
return strValue;
#endif
}
static std::string UrlEncodeUtf8(const std::string& strValue)
{
static const char* kHex = "0123456789ABCDEF";
std::string strEncoded;
strEncoded.reserve(strValue.size() * 3);
for (unsigned char ch : strValue)
{
if ((ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '_' || ch == '.' || ch == '~')
{
strEncoded.push_back((char)ch);
}
else if (ch == ' ')
{
strEncoded.push_back('+');
}
else
{
strEncoded.push_back('%');
strEncoded.push_back(kHex[ch >> 4]);
strEncoded.push_back(kHex[ch & 0x0F]);
}
}
return strEncoded;
}
static std::string UnescapeInfoValue(const std::string& strValue)
{
std::string strResult;
strResult.reserve(strValue.size());
for (size_t i = 0; i < strValue.size(); ++i)
{
if (strValue[i] == '\\' && i + 1 < strValue.size() && strValue[i + 1] == 'n')
{
strResult.push_back('\n');
++i;
}
else
{
strResult.push_back(strValue[i]);
}
}
return strResult;
}
static std::string EscapeInfoValueForFile(const std::string& strValue)
{
std::string strResult;
strResult.reserve(strValue.size());
for (size_t i = 0; i < strValue.size(); ++i)
{
char ch = strValue[i];
if (ch == '\r')
{
continue;
}
if (ch == '\n')
{
strResult += "\\n";
continue;
}
if (ch == '\\')
{
strResult += "\\\\";
continue;
}
strResult.push_back(ch);
}
return strResult;
}
static void TrimAnsiInPlace(std::string& strValue)
{
while (!strValue.empty() && (strValue.front() == ' ' || strValue.front() == '\t' || strValue.front() == '\r'))
{
strValue.erase(strValue.begin());
}
while (!strValue.empty() && (strValue.back() == ' ' || strValue.back() == '\t' || strValue.back() == '\r'))
{
strValue.pop_back();
}
}
static BOOL ReadBinaryFile(LPCTSTR lpFile, std::vector<BYTE>& data)
{
data.clear();
CFile myFile;
CFileException fileExp;
if (!myFile.Open(lpFile, CFile::modeRead | CFile::shareDenyWrite, &fileExp))
{
return FALSE;
}
ULONGLONG ullLength = myFile.GetLength();
if (ullLength == 0)
{
myFile.Close();
return TRUE;
}
data.resize((size_t)ullLength);
myFile.Read(data.data(), (UINT)ullLength);
myFile.Close();
return TRUE;
}
static std::string ToLowerAscii(std::string strValue)
{
for (char& ch : strValue)
{
if (ch >= 'A' && ch <= 'Z')
{
ch = (char)(ch - 'A' + 'a');
}
}
return strValue;
}
// ─────────────────────────────────────────────
// Cookie 管理httplib 不自动维护 Session
// ─────────────────────────────────────────────
static void TrimStdStringInPlace(std::string& strValue)
{
while (!strValue.empty() && (strValue.front() == ' ' || strValue.front() == '\t'))
{
strValue.erase(strValue.begin());
}
while (!strValue.empty() && (strValue.back() == ' ' || strValue.back() == '\t'))
{
strValue.pop_back();
}
}
static void MergeSetCookieValue(const std::string& strHeaderValue, std::string& strCookies)
{
std::string strLine = strHeaderValue;
TrimStdStringInPlace(strLine);
if (strLine.empty())
{
return;
}
size_t nSemi = strLine.find(';');
std::string strPair = (nSemi != std::string::npos) ? strLine.substr(0, nSemi) : strLine;
TrimStdStringInPlace(strPair);
if (strPair.empty())
{
return;
}
size_t nEq = strPair.find('=');
if (nEq == std::string::npos || nEq == 0)
{
return;
}
std::string strName = strPair.substr(0, nEq);
std::string strValue = strPair.substr(nEq + 1);
TrimStdStringInPlace(strName);
TrimStdStringInPlace(strValue);
if (strName.empty())
{
return;
}
std::string strNewPair = strName + "=" + strValue;
if (strCookies.empty())
{
strCookies = strNewPair;
return;
}
std::string strResult;
BOOL bReplaced = FALSE;
size_t nStart = 0;
while (nStart < strCookies.size())
{
size_t nSemiPos = strCookies.find(';', nStart);
std::string strItem = (nSemiPos != std::string::npos)
? strCookies.substr(nStart, nSemiPos - nStart)
: strCookies.substr(nStart);
TrimStdStringInPlace(strItem);
if (!strItem.empty())
{
size_t nItemEq = strItem.find('=');
std::string strItemName = (nItemEq != std::string::npos) ? strItem.substr(0, nItemEq) : strItem;
TrimStdStringInPlace(strItemName);
if (!strResult.empty())
{
strResult += "; ";
}
if (ToLowerAscii(strItemName) == ToLowerAscii(strName))
{
strResult += strNewPair;
bReplaced = TRUE;
}
else
{
strResult += strItem;
}
}
if (nSemiPos == std::string::npos)
{
break;
}
nStart = nSemiPos + 1;
}
if (!bReplaced)
{
if (!strResult.empty())
{
strResult += "; ";
}
strResult += strNewPair;
}
strCookies = strResult;
}
static void CollectResponseCookies(const httplib::Response& response, std::string& strCookies)
{
for (const auto& header : response.headers)
{
if (ToLowerAscii(header.first) == "set-cookie")
{
MergeSetCookieValue(header.second, strCookies);
}
}
}
static httplib::Headers MakeCookieHeaders(const std::string& strCookies)
{
httplib::Headers headers;
if (!strCookies.empty())
{
headers.emplace("Cookie", strCookies);
}
return headers;
}
static std::unique_ptr<httplib::Client> CreatePmcClient(LPCTSTR lpBaseUrl)
{
std::string strBaseUrl = ToUtf8String(lpBaseUrl);
auto cli = std::make_unique<httplib::Client>(strBaseUrl);
if (!cli->is_valid())
{
return nullptr;
}
cli->set_follow_location(false);
cli->set_connection_timeout(30, 0);
cli->set_read_timeout(300, 0);
cli->set_write_timeout(300, 0);
return cli;
}
// ─────────────────────────────────────────────
// info 文件解析
// ─────────────────────────────────────────────
static void ApplyInfoKeyValue(UploadInfoConfig& config, const std::string& strKey, const std::string& strRawValue)
{
std::string strValue = UnescapeInfoValue(strRawValue);
CString strCStringValue = Utf8ToCString(strValue);
if (strKey == "url")
{
config.strBaseUrl = strCStringValue;
}
else if (strKey == "file")
{
config.strPackageFile = strCStringValue;
}
else if (strKey == "package_name")
{
config.strPackageName = strCStringValue;
}
else if (strKey == "package_type_id")
{
config.strPackageTypeId = strCStringValue;
}
else if (strKey == "product_version")
{
config.strProductVersion = strCStringValue;
}
else if (strKey == "file_version")
{
config.strFileVersion = strCStringValue;
}
else if (strKey == "file_change_log")
{
config.strFileChangeLog = strCStringValue;
}
else if (strKey == "product_change_log")
{
config.strProductChangeLog = strCStringValue;
}
else if (strKey == "description")
{
config.strDescription = strCStringValue;
}
else if (strKey == "wiki_url")
{
config.strWikiUrl = strCStringValue;
}
}
static BOOL LoadUploadInfoFile(LPCTSTR lpInfoFile, UploadInfoConfig& config)
{
std::string strContent;
std::string strBomPrefix;
if (!ReadTextFileAsAnsi(lpInfoFile, strContent, strBomPrefix))
{
_tprintf(_T("错误: 无法读取 info 配置文件 %s\n"), lpInfoFile);
return FALSE;
}
size_t nLineStart = 0;
while (nLineStart <= strContent.size())
{
size_t nLineEnd = strContent.find('\n', nLineStart);
if (nLineEnd == std::string::npos)
{
nLineEnd = strContent.size();
}
std::string strLine = strContent.substr(nLineStart, nLineEnd - nLineStart);
if (!strLine.empty() && strLine.back() == '\r')
{
strLine.pop_back();
}
TrimAnsiInPlace(strLine);
if (!strLine.empty() && strLine[0] != '#')
{
size_t nEq = strLine.find('=');
if (nEq != std::string::npos)
{
std::string strKey = strLine.substr(0, nEq);
std::string strValue = strLine.substr(nEq + 1);
TrimAnsiInPlace(strKey);
TrimAnsiInPlace(strValue);
if (!strKey.empty())
{
ApplyInfoKeyValue(config, strKey, strValue);
}
}
}
if (nLineEnd >= strContent.size())
{
break;
}
nLineStart = nLineEnd + 1;
}
return TRUE;
}
static BOOL ValidateUploadConfig(const UploadInfoConfig& config)
{
if (config.strPackageName.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 package_name\n"));
return FALSE;
}
if (config.strPackageTypeId.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 package_type_id\n"));
return FALSE;
}
if (config.strProductVersion.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 product_version\n"));
return FALSE;
}
if (config.strFileVersion.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 file_version\n"));
return FALSE;
}
if (config.strFileChangeLog.IsEmpty())
{
_tprintf(_T("错误: info 文件缺少 file_change_log\n"));
return FALSE;
}
return TRUE;
}
static BOOL ResolvePasswordFromEnv(LPCTSTR lpUsername, CString& strPassword)
{
if (lpUsername == NULL || lpUsername[0] == _T('\0'))
{
return FALSE;
}
#ifdef UNICODE
LPWSTR lpEnvValue = _wgetenv(lpUsername);
if (lpEnvValue == NULL || lpEnvValue[0] == L'\0')
{
_tprintf(_T("错误: 未设置环境变量 %s用于读取登录密码\n"), lpUsername);
return FALSE;
}
strPassword = lpEnvValue;
#else
std::string strEnvName = ToAnsiString(lpUsername);
char* lpBuffer = NULL;
size_t nLen = 0;
if (_dupenv_s(&lpBuffer, &nLen, strEnvName.c_str()) != 0 || lpBuffer == NULL || lpBuffer[0] == '\0')
{
if (lpBuffer != NULL)
{
free(lpBuffer);
}
_tprintf(_T("错误: 未设置环境变量 %s用于读取登录密码\n"), lpUsername);
return FALSE;
}
strPassword = lpBuffer;
free(lpBuffer);
#endif
return TRUE;
}
static BOOL UpdateUploadInfoKeys(LPCTSTR lpInfoFile, const std::map<std::string, std::string>& updates)
{
std::vector<std::string> outLines;
std::string strBomPrefix;
std::map<std::string, BOOL> foundKeys;
for (const auto& item : updates)
{
foundKeys[item.first] = FALSE;
}
if (PathFileExists(lpInfoFile))
{
std::string strContent;
if (!ReadTextFileAsAnsi(lpInfoFile, strContent, strBomPrefix))
{
_tprintf(_T("警告: 无法读取 upload_info.txt将创建新文件\n"));
}
else
{
size_t nLineStart = 0;
while (nLineStart <= strContent.size())
{
size_t nLineEnd = strContent.find('\n', nLineStart);
if (nLineEnd == std::string::npos)
{
nLineEnd = strContent.size();
}
std::string strLine = strContent.substr(nLineStart, nLineEnd - nLineStart);
if (!strLine.empty() && strLine.back() == '\r')
{
strLine.pop_back();
}
std::string strOutLine = strLine;
if (!strLine.empty() && strLine[0] != '#')
{
size_t nEq = strLine.find('=');
if (nEq != std::string::npos)
{
std::string strKey = strLine.substr(0, nEq);
TrimAnsiInPlace(strKey);
std::map<std::string, std::string>::const_iterator it = updates.find(strKey);
if (it != updates.end())
{
strOutLine = strKey + "=" + it->second;
foundKeys[strKey] = TRUE;
}
}
}
outLines.push_back(strOutLine);
if (nLineEnd >= strContent.size())
{
break;
}
nLineStart = nLineEnd + 1;
}
}
}
for (const auto& item : updates)
{
if (!foundKeys[item.first])
{
outLines.push_back(item.first + "=" + item.second);
}
}
CFile myFile;
CFileException fileExp;
if (!myFile.Open(lpInfoFile, CFile::modeCreate | CFile::modeWrite, &fileExp))
{
_tprintf(_T("错误: 无法写入 upload_info.txt %s\n"), lpInfoFile);
return FALSE;
}
if (!strBomPrefix.empty())
{
myFile.Write(strBomPrefix.c_str(), (UINT)strBomPrefix.size());
}
for (size_t i = 0; i < outLines.size(); ++i)
{
myFile.Write(outLines[i].c_str(), (UINT)outLines[i].size());
myFile.Write("\n", 1);
}
myFile.Close();
_tprintf(_T("已更新 upload_info.txt: %s\n"), lpInfoFile);
return TRUE;
}
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));
return UpdateUploadInfoKeys(lpInfoFile, updates);
}
BOOL WriteUploadInfoPackageFile(LPCTSTR lpInfoFile, LPCTSTR lpPackageFile)
{
std::map<std::string, std::string> updates;
updates["file"] = ToUtf8String(lpPackageFile);
return UpdateUploadInfoKeys(lpInfoFile, updates);
}
static void PrintUploadUsage()
{
_tprintf(_T("用法gitver upload username=<用户名> [file=<上传的文件>] [info=<配置.txt>] [url=<PMC基址>]\n"));
_tprintf(_T("说明info= 缺省为 upload_info.txtfile= 可省略,从 info 的 file 字段读取\n"));
_tprintf(_T("说明:登录密码从与 username 同名的环境变量读取\n"));
_tprintf(_T("示例gitver upload username=admin\n"));
}
// ─────────────────────────────────────────────
// PMC 交互cpp-httplib
// ─────────────────────────────────────────────
static int PmcLogin(httplib::Client& cli, LPCTSTR lpUsername, LPCTSTR lpPassword, std::string& strCookies)
{
strCookies.clear();
httplib::Params params;
params.emplace("username", ToUtf8String(lpUsername));
params.emplace("password", ToUtf8String(lpPassword));
httplib::Result result = cli.Post("/auth/login", params);
if (!result)
{
_tprintf(_T("错误: 登录请求失败(网络错误)\n"));
return 53;
}
if (result->status == 401 || result->status == 403)
{
_tprintf(_T("错误: 登录被拒绝HTTP %d\n"), result->status);
return 50;
}
if (result->status != 303)
{
_tprintf(_T("错误: 登录返回意外状态码 HTTP %d\n"), result->status);
return 50;
}
std::string strLocation = ToLowerAscii(result->get_header_value("Location"));
if (strLocation.find("/login") != std::string::npos)
{
_tprintf(_T("错误: 登录失败,用户名或密码错误\n"));
return 50;
}
CollectResponseCookies(*result, strCookies);
if (strCookies.empty())
{
_tprintf(_T("错误: 登录成功但未收到 Session Cookie\n"));
return 50;
}
_tprintf(_T("已登录 PMC 账号 %s\n"), lpUsername);
return 0;
}
static int PmcUploadPackage(httplib::Client& cli, const std::string& strCookies, const UploadInfoConfig& config, LPCTSTR lpFilePath)
{
std::vector<BYTE> fileData;
if (!ReadBinaryFile(lpFilePath, fileData))
{
_tprintf(_T("错误: 读取上传文件失败 %s\n"), lpFilePath);
return 48;
}
std::string strFileContent((const char*)fileData.data(), fileData.size());
std::string strFileName = ToUtf8String(PathFindFileName(lpFilePath));
httplib::UploadFormDataItems items;
items.push_back({ "package_name", ToUtf8String(config.strPackageName), "", "" });
items.push_back({ "package_type_id", ToUtf8String(config.strPackageTypeId), "", "" });
items.push_back({ "description", ToUtf8String(config.strDescription), "", "" });
items.push_back({ "wiki_url", ToUtf8String(config.strWikiUrl), "", "" });
items.push_back({ "product_version", ToUtf8String(config.strProductVersion), "", "" });
items.push_back({ "file_version", ToUtf8String(config.strFileVersion), "", "" });
items.push_back({ "product_change_log", ToUtf8String(config.strProductChangeLog), "", "" });
items.push_back({ "file_change_log", ToUtf8String(config.strFileChangeLog), "", "" });
items.push_back({ "package_file", strFileContent, strFileName, "application/octet-stream" });
httplib::Result result = cli.Post("/admin/packages/upload", MakeCookieHeaders(strCookies), items);
if (!result)
{
_tprintf(_T("错误: 上传请求失败(网络错误)\n"));
return 53;
}
if (result->status == 401)
{
_tprintf(_T("错误: 上传时未登录HTTP 401\n"));
return 51;
}
if (result->status == 403)
{
_tprintf(_T("错误: 上传需要管理员权限HTTP 403\n"));
return 51;
}
if (result->status != 303)
{
_tprintf(_T("错误: 上传返回意外状态码 HTTP %d\n"), result->status);
return 51;
}
CString strLocation = Utf8ToCString(result->get_header_value("Location"));
_tprintf(_T("已提交上传请求HTTP 303 -> %s\n"), strLocation.GetString());
return 0;
}
static BOOL JsonContainsFieldValue(const std::string& strBody, LPCSTR lpField, LPCSTR lpValue)
{
if (lpField == NULL || lpValue == NULL)
{
return FALSE;
}
std::string strNeedle = std::string("\"") + lpField + "\":\"" + lpValue + "\"";
return strBody.find(strNeedle) != std::string::npos;
}
static int VerifyUploadResult(httplib::Client& cli, const std::string& strCookies, const UploadInfoConfig& config)
{
std::string strPath = "/api/packages/latest?keyword=" +
UrlEncodeUtf8(ToUtf8String(config.strPackageName)) + "&page_size=50";
httplib::Result result = cli.Get(strPath, MakeCookieHeaders(strCookies));
if (!result)
{
_tprintf(_T("错误: 验证上传结果时网络请求失败\n"));
return 53;
}
if (result->status != 200)
{
_tprintf(_T("错误: 验证上传结果返回 HTTP %d\n"), result->status);
return 52;
}
std::string strPackageName = ToUtf8String(config.strPackageName);
std::string strProductVersion = ToUtf8String(config.strProductVersion);
std::string strFileVersion = ToUtf8String(config.strFileVersion);
if (JsonContainsFieldValue(result->body, "package_name", strPackageName.c_str()) &&
JsonContainsFieldValue(result->body, "product_version", strProductVersion.c_str()) &&
JsonContainsFieldValue(result->body, "file_version", strFileVersion.c_str()))
{
_tprintf(_T("upload ok: %s product_version="), config.strPackageName.GetString());
_tprintf(_T("%s file_version="), config.strProductVersion.GetString());
_tprintf(_T("%s\n"), config.strFileVersion.GetString());
return 0;
}
_tprintf(_T("错误: 上传后未在 PMC 中找到对应版本,可能表单校验失败或版本冲突\n"));
return 52;
}
// ─────────────────────────────────────────────
// 命令入口
// ─────────────────────────────────────────────
int HandleUploadCommand(int argc, TCHAR* argv[])
{
CString strUsername;
CString strFilePath;
CString strInfoPath;
CString strUrlOverride;
for (int i = 2; i < argc; ++i)
{
CString strArg = argv[i];
if (strArg.GetLength() > 9 && strArg.Left(9).CompareNoCase(_T("username=")) == 0)
{
strUsername = strArg.Mid(9);
continue;
}
if (strArg.GetLength() > 5 && strArg.Left(5).CompareNoCase(_T("file=")) == 0)
{
strFilePath = strArg.Mid(5);
continue;
}
if (strArg.GetLength() > 5 && strArg.Left(5).CompareNoCase(_T("info=")) == 0)
{
strInfoPath = strArg.Mid(5);
continue;
}
if (strArg.GetLength() > 4 && strArg.Left(4).CompareNoCase(_T("url=")) == 0)
{
strUrlOverride = strArg.Mid(4);
continue;
}
_tprintf(_T("错误: 多余的参数 %s\n"), argv[i]);
PrintUploadUsage();
return 3;
}
if (strUsername.IsEmpty())
{
_tprintf(_T("错误: upload 缺少必填参数 username=\n"));
PrintUploadUsage();
return 47;
}
if (strInfoPath.IsEmpty())
{
strInfoPath = _T("upload_info.txt");
}
if (!PathFileExists(strInfoPath))
{
_tprintf(_T("错误: info 配置文件不存在 %s\n"), strInfoPath.GetString());
return 49;
}
UploadInfoConfig config;
config.strBaseUrl = DEFAULT_BASE_URL;
if (!LoadUploadInfoFile(strInfoPath, config))
{
return 49;
}
if (strFilePath.IsEmpty())
{
strFilePath = config.strPackageFile;
}
if (strFilePath.IsEmpty())
{
_tprintf(_T("错误: 未指定 file=,且 info 文件中缺少 file 字段\n"));
PrintUploadUsage();
return 47;
}
if (!PathFileExists(strFilePath))
{
_tprintf(_T("错误: 上传文件不存在 %s\n"), strFilePath.GetString());
return 48;
}
if (!strUrlOverride.IsEmpty())
{
config.strBaseUrl = strUrlOverride;
}
if (config.strBaseUrl.IsEmpty())
{
config.strBaseUrl = DEFAULT_BASE_URL;
}
if (!ValidateUploadConfig(config))
{
return 49;
}
CString strPassword;
if (!ResolvePasswordFromEnv(strUsername, strPassword))
{
return 49;
}
std::unique_ptr<httplib::Client> cli = CreatePmcClient(config.strBaseUrl);
if (!cli)
{
_tprintf(_T("错误: 无法连接 PMC 服务 %s\n"), config.strBaseUrl.GetString());
return 53;
}
std::string strCookies;
int nLoginRet = PmcLogin(*cli, strUsername, strPassword, strCookies);
if (nLoginRet != 0)
{
return nLoginRet;
}
int nUploadRet = PmcUploadPackage(*cli, strCookies, config, strFilePath);
if (nUploadRet != 0)
{
return nUploadRet;
}
return VerifyUploadResult(*cli, strCookies, config);
}

22
GitVer/GitVer_upload.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
/// <summary>
/// 处理 upload 命令:登录 PMC 并上传包文件。
/// 退出码0 成功47 缺少参数48 文件不存在49 info 无效或未设置同名环境变量密码,
/// 50 登录失败51 上传 HTTP 失败52 验证未找到版本53 网络错误。
/// </summary>
int HandleUploadCommand(int argc, TCHAR* argv[]);
/// <summary>
/// 更新 upload_info.txt 中的 product_version、file_version、file_change_log保留其他字段。
/// </summary>
BOOL WriteUploadInfoVersions(
LPCTSTR lpInfoFile,
LPCTSTR lpProductVersion,
LPCTSTR lpFileVersion,
LPCTSTR lpFileChangeLog);
/// <summary>
/// 更新 upload_info.txt 中的 file安装包路径保留其他字段。
/// </summary>
BOOL WriteUploadInfoPackageFile(LPCTSTR lpInfoFile, LPCTSTR lpPackageFile);

View File

@@ -313,7 +313,7 @@ BOOL TryGetTagCommitDate(LPCTSTR lpRepoPath, const CString& strTag, SYSTEMTIME&
return TRUE; return TRUE;
} }
BOOL ResolveTagFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& strBranch, const CString& strTag, UINT& nYY, UINT& nMMDD, UINT& nId) BOOL ResolveTagFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& strBranch, const CString& strTag, UINT& nYY, UINT& nMMDD, UINT& nId, SYSTEMTIME* pOutDate)
{ {
nYY = 0; nYY = 0;
nMMDD = 0; nMMDD = 0;
@@ -351,10 +351,14 @@ BOOL ResolveTagFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString&
GetFileVersionDateFromSystemTime(stTargetDate, nYY, nMMDD); GetFileVersionDateFromSystemTime(stTargetDate, nYY, nMMDD);
nId = nCommitCount; nId = nCommitCount;
if (pOutDate != NULL)
{
*pOutDate = stTargetDate;
}
return TRUE; return TRUE;
} }
BOOL ResolveFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& strBranch, UINT& nYY, UINT& nMMDD, UINT& nId) BOOL ResolveFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& strBranch, UINT& nYY, UINT& nMMDD, UINT& nId, SYSTEMTIME* pOutDate)
{ {
nYY = 0; nYY = 0;
nMMDD = 0; nMMDD = 0;
@@ -396,6 +400,47 @@ BOOL ResolveFileVersionDateAndCommitCount(LPCTSTR lpRepoPath, const CString& str
GetFileVersionDateFromSystemTime(stTargetDate, nYY, nMMDD); GetFileVersionDateFromSystemTime(stTargetDate, nYY, nMMDD);
nId = nTodayCommitCount; nId = nTodayCommitCount;
if (pOutDate != NULL)
{
*pOutDate = stTargetDate;
}
return TRUE;
}
BOOL TryGetGitLogByBranchDate(LPCTSTR lpRepoPath, const CString& strBranch, const SYSTEMTIME& stDate, CString& strLog)
{
strLog.Empty();
if (strBranch.IsEmpty())
{
return FALSE;
}
CString strCmd;
strCmd.Format(
_T("cmd /c git log --reverse --pretty=format:%%s --since=\"%04u-%02u-%02u 00:00:00\" --until=\"%04u-%02u-%02u 23:59:59\" \"origin/%s\""),
stDate.wYear,
stDate.wMonth,
stDate.wDay,
stDate.wYear,
stDate.wMonth,
stDate.wDay,
strBranch.GetString());
CString strOutput = StartProcess(NULL, strCmd.GetBuffer(), lpRepoPath, CP_UTF8);
strCmd.ReleaseBuffer();
if (strOutput.IsEmpty())
{
return FALSE;
}
strOutput.Replace(_T("\r"), _T(""));
strOutput.Trim();
if (strOutput.IsEmpty() || strOutput.Left(6).CompareNoCase(_T("fatal:")) == 0)
{
return FALSE;
}
strLog = strOutput;
return TRUE; return TRUE;
} }
@@ -406,7 +451,9 @@ int BuildVersionsFromRepo(
CString& strProductVersion, CString& strProductVersion,
CString& strFileVersion, CString& strFileVersion,
UINT nDefaultMajor, UINT nDefaultMajor,
UINT nDefaultMinor) UINT nDefaultMinor,
CString* pOutBranch,
SYSTEMTIME* pOutFileVersionDate)
{ {
_tprintf(_T("开始构建版本: 仓库路径=%s pid=%u 默认Tag(%u.%u)\n"), _tprintf(_T("开始构建版本: 仓库路径=%s pid=%u 默认Tag(%u.%u)\n"),
lpRepoPath == NULL ? _T("<null>") : lpRepoPath, lpRepoPath == NULL ? _T("<null>") : lpRepoPath,
@@ -441,12 +488,17 @@ int BuildVersionsFromRepo(
_tprintf(_T("游离标签版本: %s (major=%u minor=%u)\n"), strTag.GetString(), nMajor, nMinor); _tprintf(_T("游离标签版本: %s (major=%u minor=%u)\n"), strTag.GetString(), nMajor, nMinor);
} }
if (!ResolveTagFileVersionDateAndCommitCount(lpRepoPath, strBranch, strTag, nYY, nMMDD, nId)) if (!ResolveTagFileVersionDateAndCommitCount(lpRepoPath, strBranch, strTag, nYY, nMMDD, nId, pOutFileVersionDate))
{ {
_tprintf(_T("错误: 获取游离标签提交信息失败\n")); _tprintf(_T("错误: 获取游离标签提交信息失败\n"));
return errorCodes.nCommitErrorCode; return errorCodes.nCommitErrorCode;
} }
if (pOutBranch != NULL)
{
*pOutBranch = strBranch;
}
_tprintf(_T("游离标签提交信息: yy=%u mmdd=%u id=%u\n"), nYY, nMMDD, nId); _tprintf(_T("游离标签提交信息: yy=%u mmdd=%u id=%u\n"), nYY, nMMDD, nId);
} }
else else
@@ -477,11 +529,17 @@ int BuildVersionsFromRepo(
_tprintf(_T("最近标签: %s (major=%u minor=%u)\n"), strLastTag.GetString(), nMajor, nMinor); _tprintf(_T("最近标签: %s (major=%u minor=%u)\n"), strLastTag.GetString(), nMajor, nMinor);
} }
if (!ResolveFileVersionDateAndCommitCount(lpRepoPath, strBranch, nYY, nMMDD, nId)) if (!ResolveFileVersionDateAndCommitCount(lpRepoPath, strBranch, nYY, nMMDD, nId, pOutFileVersionDate))
{ {
_tprintf(_T("错误: 获取分支提交信息失败\n")); _tprintf(_T("错误: 获取分支提交信息失败\n"));
return errorCodes.nCommitErrorCode; return errorCodes.nCommitErrorCode;
} }
if (pOutBranch != NULL)
{
*pOutBranch = strBranch;
}
_tprintf(_T("分支提交信息: yy=%u mmdd=%u id=%u\n"), nYY, nMMDD, nId); _tprintf(_T("分支提交信息: yy=%u mmdd=%u id=%u\n"), nYY, nMMDD, nId);
} }

View File

@@ -21,7 +21,20 @@ BOOL ResolveTagFileVersionDateAndCommitCount(
const CString& strTag, const CString& strTag,
UINT& nYY, UINT& nYY,
UINT& nMMDD, UINT& nMMDD,
UINT& nId); UINT& nId,
SYSTEMTIME* pOutDate = NULL);
BOOL ResolveFileVersionDateAndCommitCount(
LPCTSTR lpRepoPath,
const CString& strBranch,
UINT& nYY,
UINT& nMMDD,
UINT& nId,
SYSTEMTIME* pOutDate = NULL);
BOOL TryGetGitLogByBranchDate(
LPCTSTR lpRepoPath,
const CString& strBranch,
const SYSTEMTIME& stDate,
CString& strLog);
int BuildVersionsFromRepo( int BuildVersionsFromRepo(
LPCTSTR lpRepoPath, LPCTSTR lpRepoPath,
UINT nPid, UINT nPid,
@@ -29,4 +42,6 @@ int BuildVersionsFromRepo(
CString& strProductVersion, CString& strProductVersion,
CString& strFileVersion, CString& strFileVersion,
UINT nDefaultMajor, UINT nDefaultMajor,
UINT nDefaultMinor); UINT nDefaultMinor,
CString* pOutBranch = NULL,
SYSTEMTIME* pOutFileVersionDate = NULL);

20834
GitVer/httplib.h Normal file

File diff suppressed because it is too large Load Diff

117
README.md
View File

@@ -15,6 +15,7 @@ gitver nuitkabuild=<pid> [repodir=<path>] [-test] [params="<mainPy> <nuitka参
gitver nuitkapydbuild=<pid> [repodir=<path>] [-test] [params="<modulePy> <nuitka参数>"] gitver nuitkapydbuild=<pid> [repodir=<path>] [-test] [params="<modulePy> <nuitka参数>"]
gitver pyinstaller=<pid> [repodir=<path>] [-test] [params="<mainPy|specFile> <pyinstaller参数>"] gitver pyinstaller=<pid> [repodir=<path>] [-test] [params="<mainPy|specFile> <pyinstaller参数>"]
gitver setup=n [pid=m] [repodir=<path>] [-test] gitver setup=n [pid=m] [repodir=<path>] [-test]
gitver upload username=<用户名> [file=<上传文件>] [info=<配置.txt>] [url=<PMC基址>]
``` ```
**通用参数说明:** **通用参数说明:**
@@ -116,8 +117,21 @@ gitver setver=5 -test
``` ```
ProductVersion=5.0.1920.11 ProductVersion=5.0.1920.11
FileVersion=5.26.0519.3 FileVersion=5.26.0519.3
已更新 upload_info.txt: E:\Code\MyProj\upload_info.txt
``` ```
**upload_info.txt 自动更新:**
`setver` 成功生成版本号后,会在仓库目录(`repodir`,缺省为当前目录)写入或更新 `upload_info.txt`
| 字段 | 来源 |
|---|---|
| `product_version` | 本次生成的 ProductVersion |
| `file_version` | 本次生成的 FileVersion |
| `file_change_log` | 文件版本对应日期的 git 提交说明(`origin/<分支>` 当天 log多条以换行分隔 |
`setup` 成功编译安装包后,还会写入 `file`(安装包路径)。文件中已有的其他字段(如 `package_name``package_type_id``url` 等)会保留不变。
**回写说明:** **回写说明:**
- **C++**:回写 `.rc` 文件中的 FILEVERSION、PRODUCTVERSION 及对应字符串字段 - **C++**:回写 `.rc` 文件中的 FILEVERSION、PRODUCTVERSION 及对应字符串字段
@@ -265,6 +279,81 @@ gitver setup=0 pid=5
gitver setup=1 pid=5 repodir=E:\Code\MyProj gitver setup=1 pid=5 repodir=E:\Code\MyProj
``` ```
**upload_info.txt 自动更新:**
`setup` 编译成功后,会在仓库目录(`repodir`,缺省为当前目录)写入或更新 `upload_info.txt``file` 字段(安装包完整路径)。路径从 ISCC/NSIS 编译输出中解析;若无法解析,则尝试脚本目录下 `Output\*.exe` 或脚本目录内最新 `.exe`
**输出示例:**
```
安装包路径: E:\Code\MyProj\Output\MyApp_Setup_5.26.0624.3.exe
已更新 upload_info.txt: E:\Code\MyProj\upload_info.txt
```
---
### `gitver upload`(上传包到 PMC
将构建产物上传到 PMC 包管理中心。需 PMC 服务已运行,且账号具有管理员权限。
```
gitver upload username=<用户名> [file=<上传文件>] [info=<配置.txt>] [url=<PMC基址>]
```
| 参数 | 说明 |
|---|---|
| `username=` | PMC 登录账号(必填);登录密码从**同名环境变量**读取,如 `username=admin` 读取 `%admin%` |
| `file=` | 可选,要上传的包文件路径;省略时从 info 文件的 `file` 字段读取(通常由 `setup` 写入) |
| `info=` | 可选,上传配置信息文件路径,默认 `upload_info.txt`(见下方格式) |
| `url=` | 可选PMC 服务基址,默认 `http://127.0.0.1:8000` |
**info 配置文件格式**UTF-8 或 ANSI每行 `key=value``#` 开头为注释):
```ini
url=http://127.0.0.1:8000
package_name=daily-report
package_type_id=1
file=E:\dist\MyApp_Setup_5.26.0624.3.exe
product_version=5.0.1920.11
file_version=5.26.0624.3
file_change_log=修复导出 bug
description=日报工具
wiki_url=https://example.com/wiki
product_change_log=首次发布
```
| info 字段 | 必填 | 说明 |
|---|---|---|
| `package_name` | 是 | 包名称 |
| `package_type_id` | 是 | 包类型 ID整数 |
| `file` | 否* | 安装包路径;`upload` 省略 `file=` 时必填,通常由 `setup` 自动写入 |
| `product_version` | 是 | 产品版本号 |
| `file_version` | 是 | 文件版本号 |
| `file_change_log` | 是 | 文件版本更新说明 |
| `description` | 否 | 包描述 |
| `wiki_url` | 否 | Wiki 链接 |
| `product_change_log` | 否 | 产品版本更新说明 |
| `url` | 否 | PMC 基址,默认 `http://127.0.0.1:8000` |
上传流程:先 `POST /auth/login` 获取 Session`POST /admin/packages/upload`multipart/form-data最后通过 `/api/packages/latest` 验证版本是否写入。
HTTP 实现基于 [cpp-httplib](https://github.com/yhirose/cpp-httplib)[`GitVer/httplib.h`](GitVer/httplib.h)),默认支持 `http://`;若需 `https://` 须另行启用 OpenSSL 支持。
**示例:**
```powershell
# 先设置与 username 同名的环境变量作为登录密码
$env:admin = "your_admin_password"
```
```
gitver upload username=admin
gitver upload username=admin info=E:\upload_info.txt
gitver upload username=admin file=E:\dist\app.zip info=E:\upload_info.txt
gitver upload username=admin url=http://192.168.1.10:8000
```
--- ---
## Nuitka 与 PyInstaller 常用参数速查 ## Nuitka 与 PyInstaller 常用参数速查
@@ -505,6 +594,13 @@ VSVersionInfo(
| 44 | `pyinstaller=` 获取当天分支提交次数失败 | | 44 | `pyinstaller=` 获取当天分支提交次数失败 |
| 45 | `pyinstaller=` 版本信息文件无法生成或更新 | | 45 | `pyinstaller=` 版本信息文件无法生成或更新 |
| 46 | `pyinstaller=` spec 文件无法注入或修改版本信息 | | 46 | `pyinstaller=` spec 文件无法注入或修改版本信息 |
| 47 | `upload` 缺少必填参数username或 file 未指定且 info 中无 file 字段 |
| 48 | `upload` 上传文件不存在 |
| 49 | `upload` info 文件无效、缺少必填字段,或未设置与 username 同名的环境变量 |
| 50 | `upload` PMC 登录失败 |
| 51 | `upload` HTTP 上传请求失败 |
| 52 | `upload` 上传后未找到对应版本(校验失败或版本冲突) |
| 53 | `upload` 网络/HTTP 错误 |
--- ---
@@ -592,6 +688,16 @@ gitver setup=0 pid=5
# 预期: 使用 pid=5 生成版本号,修改 setup.iss 并调用 ISCC.exe 编译 # 预期: 使用 pid=5 生成版本号,修改 setup.iss 并调用 ISCC.exe 编译
``` ```
### 验证 upload
```powershell
# 前提PMC 运行于 http://127.0.0.1:8000且已有 admin 账号
$env:admin = "your_admin_password"
# 准备 upload_info.txt含 package_name、file 等字段,不含 password
gitver upload username=admin
# 预期: 读取当前目录 upload_info.txt返回码 0PMC 管理页可见新版本
```
--- ---
## 回归清单 ## 回归清单
@@ -658,6 +764,17 @@ gitver setup=0 pid=5
| `gitver setup=9` | 36 | | `gitver setup=9` | 36 |
| `gitver setup=0 pid=abc` | 4 | | `gitver setup=0 pid=abc` | 4 |
### upload 组
| 命令 | 预期返回码 |
|---|---|
| `gitver upload username=admin` | 0 / 47 / 48 / 49 / 50 / 52 / 53 |
| `gitver upload username=admin info=E:\upload_info.txt` | 0 / 50 / 52 / 53 |
| `gitver upload username=admin file=E:\dist\app.zip info=E:\upload_info.txt` | 0 / 50 / 52 / 53 |
| `gitver upload username=admin file=E:\NotExists info=E:\upload_info.txt` | 48 |
| `gitver upload username=admin info=E:\NotExists` | 49 |
| `gitver upload` | 47 |
> **注意**如果打包时提示“未找到安装编译器ISCC.exe / makensis.exe请手动下载安装对应工具并将其可执行文件路径如 `ISCC.exe` 或 `makensis.exe` 所在目录)添加到系统环境变量 `PATH`。常见下载地址: > **注意**如果打包时提示“未找到安装编译器ISCC.exe / makensis.exe请手动下载安装对应工具并将其可执行文件路径如 `ISCC.exe` 或 `makensis.exe` 所在目录)添加到系统环境变量 `PATH`。常见下载地址:
> >
> - Inno Setup: https://jrsoftware.org/isinfo.php > - Inno Setup: https://jrsoftware.org/isinfo.php