TortoiseSVN调用powershell命令及调试详解

本文最后更新于:7 小时后

引言

一直很喜欢自动化的东西,自己的项目都配置有CI/CD;但是公司的项目单独用的TortoiseSVNJenkins,运维也没有配置服务端的Piplile;因此自己在TortoiseSVN使用Hook Scripts的客户端触发方法,本机执行Jenkins构建,因为Jenkins直接支持API创建构建,所以整体流程还算顺利

脚本实现的功能:

在每个SVN项目根目录中添加Post-commit后,根据脚本中的配置,按需在指定路径代码更新后触发指定服务的构建;支持参数设置

本文将指导:

  • 任意脚本命令调试方法
  • Powershell调试方法
  • API调用Jenkins方法
  • TortoiseSVN触发Jenkins实用脚本

解决方案

API调用Jenkins

我的Jenkins版本为2.190.1 Jenkins的触发请查看Jenkins API;在网页的最下面有显示;地址为:

[Jenkins地址]/api/

认证方法为Basic认证,API Token请在用户设置中创建

jenkins_apitoken

新建构建在Create Job中有说明;

我的CURL命令是这样:

1
2
3
curl --location 'http://192.168.1.23/job/AAA.Web/buildWithParameters' \
--header 'Authorization: Basic XYZZZZXX=' \
--form 'REVISION="HEAD"'
  • AAA.Web为项目(Job)名称;
  • AuthorizationPostMan使用Basic Auth自动生成,用户名为账号,密码为APIKey
  • form为我的构建参数

TortoiseSVN提交代码后自动执行脚本

官方文档中有客户端触发事件的详细说明;我这里只需要代码提交后的事件:Post-commit

TortoiseSVN_hookscripts

其中Work Copy PathSVN本地根路径(每个CheckOut加一条);执行的命令为powershell SVN\AAA\autoJenkins.ps1

这个autoJenkins.ps1文件在后面;保存在SVN本地路径中;因此意思为使用powershell调用这个脚本

这里我先给出我的autoJenkins.ps1脚本文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
<#
.SYNOPSIS
Automated Jenkins Build Trigger for SVN Hooks.

.DESCRIPTION
This script is designed to be integrated with SVN (Subversion) post-commit hooks.
It analyzes the SVN revision, commit messages, and changed paths to automatically
trigger specific Jenkins jobs via the Jenkins Remote API.

Features:
- Trigger Jenkins jobs based on directory path mapping.
- Support for Commit Message commands: [ci skip], [build: projectName].
- Automatic detection of modified files using SVN XML logs.
- UTF-8 logging for both console and local log files.

.PARAMETER PATH
The execution path provided by the SVN hook.
.PARAMETER REVISION
The SVN revision number to analyze.
.PARAMETER RESULTPATH
The physical path of the committed files in the repository.
.PARAMETER projectName
(Optional) Force a specific Jenkins job name to build.

.EXAMPLE
.\autoJenkins.ps1 -REVISION "413" -RESULTPATH "G:\work\SVN\LifeHelper\Program\InterFace\WebService"
#>
# 执行命令
# powershell.exe -command "& {start-process powershell.exe (([system.environment]::GetCommandLineArgs()|ForEach-Object -process { '\"{0}\"' -f $_ })[3..([system.environment]::GetCommandLineArgs().Length-1)])}" "-file" "G:\work\SVN\LifeHelper\autoJenkins.ps1"

# powershell G:\work\SVN\LifeHelper\autoJenkins.ps1
# PATH DEPTH MESSAGEFILE REVISION ERROR

param(
[string]$PATH, # 命令执行路径
[string]$DEPTH, # 对应 arg 1
[string]$MESSAGEFILE, # 对应 arg 2
[string]$REVISION, # 对应 arg 2
[string]$SVN_ERROR, # 对应 arg 2
[string]$RESULTPATH, # svn文件提交路径
[string]$projectName # 发布的项目名称
)

[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
$OutputEncoding = [System.Text.UTF8Encoding]::new()

Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') start script"
Add-Content svn-hook.log "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') start script"

# Read-Host "Press Enter to exit"

$svnPath = $env:svnPath;
if ([string]::IsNullOrWhiteSpace($svnPath)) {
$svnPath="D:\svn\repo"
}


$pathMap = @(
[PSCustomObject]@{
Path = "\Program\InterFace\WebService"
JobName = "WebService"
}
)


# Jenkins 服务器地址
$JENKINS_URL="http://192.168.1.100:8080"

$username = "007"
# 注意:如果是 Jenkins,这里通常填 API Token,而不是登录密码
$password = "xxx"

function Log {
param([string]$Message)
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message"
Add-Content svn-hook.log "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message"
}
Log "PATH: $PATH"
Log "DEPTH: $DEPTH"
Log "MESSAGEFILE: $MESSAGEFILE"
Log "REVISION: $REVISION"
Log "SVN_ERROR: $SVN_ERROR"
Log "RESULTPATH: $RESULTPATH"

$script:listProjectName = @()
# Read-Host "Press Enter to exit"
# 发送 Jenkins 构建请求
function BuildProject {
param(
[string]$projectName,
[string]$hookPath = ""
)

# 检查是否存在
if ($script:listProjectName -contains $ProjectName) {
Log "ignore project: $ProjectName"
return
}

# 添加到脚本作用域的变量中 (注意 script: 前缀)
$script:listProjectName += $ProjectName
# 脚本目录
# $ScriptDir = $PSScriptRoot
# $ParentDir = $ScriptDir
$ParentDir = $hookPath

$JOB_NAME=$projectName

# 不设置则取父级名称为项目名称
if ([string]::IsNullOrWhiteSpace($projectName)) {
$JOB_NAME=Split-Path $ParentDir -Leaf
}
Log " url host is :${JENKINS_URL} "

$uri = "$($JENKINS_URL.TrimEnd('/'))/job/$JOB_NAME/buildWithParameters"

Log "url: $uri"

$authPair = "$($username):$($password)"
$encodedCredentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($authPair))
$headers = @{
Authorization = "Basic $encodedCredentials"
}

Invoke-WebRequest -UseBasicParsing `
-Uri $uri `
-Method Post `
-Headers $headers `
-Body @{
REVISION="HEAD"
}
}
function CheckChildUpdate {
# ---------------------- 配置区域 ----------------------
param(
[string]$Revision = "413",
[string]$WatchDir = "/"
)

# ---------------------- 1. 获取 SVN 日志 ----------------------
Log "正在查询 SVN 版本: $Revision ..."
Log "WatchDir is: $WatchDir"

# 使用 --xml 参数,这样 PowerShell 可以直接转换成对象,不用去截取字符串
# 这里的 2>&1 是为了捕获可能的错误
try {
# 注意:如果是私有仓库,可能需要追加 --username "xxx" --password "xxx"
[xml]$svnLog = svn log . -r $Revision -v --xml
}
catch {
Log "SVN 命令执行失败,请检查 SVN 命令行工具是否安装,或 URL/权限是否正确。"
# Read-Host "Press Enter to exit"
exit 1
}
Log "get svn log done."

# ---------------------- 2. 提取变更文件列表 ----------------------
# 容错处理:确保 XML 结构存在
if ($null -eq $svnLog.log.logentry) {
Log "未找到版本信息 (可能版本号不存在)"
exit
}

# 获取所有变更路径的节点
# .InnerText 会获取形如 "/trunk/ProjectA/code.cs" 的字符串
$changedPaths = $svnLog.log.logentry.paths.path | ForEach-Object { $_.InnerText }

Log "该版本变更了 $($changedPaths.Count) 个文件。"

# ---------------------- 3. 匹配逻辑与执行 ----------------------

# 定义一个标志位
$isHit = $false

# 遍历所有变更文件
foreach ($pathTemp in $changedPaths) {

# Write-Host "变更文件: $path"
Log "pathTemp is: $pathTemp"

$targetPath=Join-Path (Split-Path $Path -Parent) $pathTemp
Log "targetpath: $targetPath"

if ($targetPath -like "$WatchDir*") {
Log " fiind target path: $pathTemp"
$isHit = $true
break
}
}
return $isHit
}
function BuildWithCommand {
# ---------------------- 配置区域 ----------------------
param(
[string]$Revision = "413",
[string]$WatchDir = "/"
)

# ---------------------- 1. 获取 SVN 日志 ----------------------
Log "for BuildWithCommand search svn version: $Revision ..."
Log "WatchDir is: $WatchDir"

# 使用 --xml 参数,这样 PowerShell 可以直接转换成对象,不用去截取字符串
# 这里的 2>&1 是为了捕获可能的错误
try {
# 注意:如果是私有仓库,可能需要追加 --username "xxx" --password "xxx"
[xml]$svnLog = svn log . -r $Revision -v --xml
}
catch {
Log "SVN 命令执行失败,请检查 SVN 命令行工具是否安装,或 URL/权限是否正确。"
# Read-Host "Press Enter to exit"
exit 1
}
Log "get svn log done."

# ---------------------- 2. 提取变更文件列表 ----------------------
# 容错处理:确保 XML 结构存在
if ($null -eq $svnLog.log.logentry) {
Log "未找到版本信息 (可能版本号不存在)"
exit
}

$commitMsg = $svnLog.log.logentry.msg.ToString()

Log "Commit Message: $commitMsg"

# ---------------------- 3. 匹配逻辑与执行 ----------------------

# 定义一个标志位
$isHit = $false

# ----------------------------------------------------
# 规则 A: 检查是否跳过 CI
# 使用正则匹配,(?i) 表示不区分大小写
# ----------------------------------------------------
if ($commitMsg -match "(?i)\[(ci skip|skip ci|no build)\]") {
Log "检测到跳过指令 [ci skip],停止执行 CI 流程。"
exit 0 # 正常退出,不报错
}

# ----------------------------------------------------
# 规则 B: 检查是否强制构建特定项目 (针对你的 LifeHelper)
# ----------------------------------------------------
if ($commitMsg -match "(?i)\[build:\s*web\]") {
Log "检测到 [build: web] 指令,强制触发 Web 构建..."
# 调用 Web 的构建函数
# Invoke-JenkinsJob "LifeHelper.Web"
}

$pattern = "build\s*:\s*([\w\.-]+)"

if ($commitMsg -match $pattern) {
# $matches[0] 是匹配到的完整字符串 (例如 "build: LifeHelper.Web")
# $matches[1] 是括号里捕获的内容 (即我们要的 "LifeHelper.Web")

$serviceName = $matches[1]

Log "match build server: [$serviceName]"

# 这里可以根据提取到的名称去执行逻辑
if ($serviceName -eq "LifeHelper.Web") {
# 触发 Web 构建
}
BuildProject $serviceName
exit 0 # 正常退出,不报错
}

return $isHit
}
# ([system.environment]::GetCommandLineArgs()|ForEach-Object -process { '\"{0}\"' -f $_ })
$myArgs = [System.Environment]::GetCommandLineArgs()

$target = $RESULTPATH

for ($i = 0; $i -lt $myArgs.Length; $i++) {
Log ("arg[{0}] = `"{1}`"" -f $i, $myArgs[$i])
# if($i -eq 7){
# $target=$myArgs[$i];
# }
}
Log "target is: $target"
$PATH=$myArgs[1]
Log "new PATH: $PATH"

if (![string]::IsNullOrWhiteSpace($projectName)) {
BuildProject $projectName
}

BuildWithCommand -Revision $REVISION -WatchDir $target

$fullPathMap = [System.Collections.Generic.List[PSCustomObject]]::new()

foreach ($kv in $pathMap) {
$key=$kv.Path;
Log "key is $key "
$fullPath = Join-Path $svnPath $key
Log "full path ${fullPath}"
Log "kv is $kv "
# Log "kvvalue is $(pathMap[$key])"
$obj = [PSCustomObject]@{
Path = $fullPath
JobName = $kv.JobName
}
$fullPathMap.Add($obj);
# $fullPathMap.Add($fullPath,$(pathMap[$key]))
}


Log "target is: $targetNorm"

# 规范化路径(非常重要)
# $targetNorm = [System.IO.Path]::GetFullPath($target).TrimEnd('\')
$targetNorm = $target
Log "targetNorm is: $targetNorm"
# 匹配并取 Value(核心逻辑)
$matchedValue = $null

foreach ($entry in $fullPathMap) {
$keyNorm = [System.IO.Path]::GetFullPath($entry.Path).TrimEnd('\')
Log "keyNorm is: $keyNorm"

if ($targetNorm.StartsWith($keyNorm, [System.StringComparison]::OrdinalIgnoreCase)) {
$matchedValue = $entry.JobName
Log "Matched path: $keyNorm => value: $matchedValue"
BuildProject $matchedValue $keyNorm
}
else {
Log "Not matched path: $keyNorm try find by parent"
if($keyNorm.StartsWith($targetNorm, [System.StringComparison]::OrdinalIgnoreCase)){
$isHit = CheckChildUpdate -Revision $REVISION -WatchDir $keyNorm
if($isHit){
$matchedValue = $entry.JobName
Log "Matched by child path: $keyNorm => value: $matchedValue"
BuildProject $matchedValue $keyNorm
}
}
}
}

if ($null -eq $matchedValue) {
Log "No path matched: $targetNorm"
# Read-Host "Press Enter to exit"
return
}

Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') script end"
Add-Content svn-hook.log "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') commit done"
# Read-Host "Press Enter to exit"

配置说明

配置项 说明
svnPath SVN根目录环境变量
pathMap SVN监听目录及其对应的Jenkins名称字典;此为SVN目录中的相对路径;
JENKINS_URL Jenkins服务地址
username Jenkins服务用户
password Jenkins服务API Token

因为脚本用到了SVN获取提交记录的方法,因此需要安装SVN CLI;在安装TortoiseSVN时需选择command line client tools

TortoiseSVN

用法

本来是先写这个文章的,结果发现脚本有些问题;经常会报错;于是只能先解决报错;就有了前一篇文章:解决powershell表达式或语句中包含意外的标记问题 ;又经过几次测试使用;改良了脚本;

当有多个不同目录的文件提交到SVN时,它的RESULTPATH会是所有目录的最小父级,因此要能根据SVN提交的目录来触发对应的Job还要从REVISION中遍历所有文件;找到对应的配置匹配的目录触发多个Job

此脚本功能:

  • 根据配置($pathMap)在指定svn目录commit后触发对应的Jenkins Job
  • 使用命令直接触发Jenkins Job,命令:.\autoJenkins.ps1 -projectName "JobName"
  • 从历史版本号触发对应的Jenkins Job,命令:.\autoJenkins.ps1 -REVISION "500" -RESULTPATH "G:\work\SVN\LifeHelper\Program\InterFace\WebService"
  • SVN Commit中编写[build: MySpecialJob] 直接触发构建MySpecialJob

Powershell调试方法

VS Code中一般调试方法

1.安装 PowerShell 扩展

2.打开 .ps1

3.左侧点红点打断点

4.F5 启动调试

假如需要参数可以在PowerShell Extension的控制台中直接输入带参数的命令触发

vscode_powershell_extension

TERMINAL中直接输入脚本带参数命令powershell.exe -File "G:\work\SVN\LifeHelper\autoJenkins.ps1" -Revision 537就可以直接中断调试

调试Post-commit命令的方法

如果直接使用powershell SVN\AAA\autoJenkins.ps1这样的触发命令;因为命令后台执行;假如有意外的错误完全摸不着头脑

那有什么可以实时看到控制台输出及结果的方法吗?

我在这位老哥那里找到了:Using a TortoiseSVN Client Hook Script to Run a Visible PowerShell Script

powershell SVN\AAA\autoJenkins.ps1命令替换为以下内容

1
powershell.exe -command "& {start-process powershell.exe (([system.environment]::GetCommandLineArgs()|ForEach-Object -process { '\"{0}\"' -f $_ })[3..([system.environment]::GetCommandLineArgs().Length-1)])}" "-file" "G:\work\SVN\LifeHelper\autoJenkins.ps1"

它可以将启动参数传递给一个新的有前台界面的powershell进程;这样就可以直观看到命令结果了

假如命令太快弹窗一下就消失,可以在代码中添加Read-Host "Press Enter to exit"这样的暂停代码;这样只有在回车确认之后才回继续执行

如果还有一些异常情况;可以本地开启录屏;在录像中逐帧分析报错

结语

实测Post-commit触发器只在svn的命令执行时才有效;比如文件管理器集成的TortoiseSVN GUI操作,SVN Commit命令以及visual studioVisualSVN插件操作;

第三方集成的svn客户端无法触发;比如IDEASVN VCS代码工具无法触发

参考资料


TortoiseSVN调用powershell命令及调试详解
http://blog.wangshuai.app/2025-12-14-TortoiseSVN客户端触发Jenkins/
作者
王帅
发布于
2025年12月14日
许可协议