gitwebhooks 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitwebhooks/__init__.py +18 -0
- gitwebhooks/__main__.py +10 -0
- gitwebhooks/auth/__init__.py +20 -0
- gitwebhooks/auth/custom.py +40 -0
- gitwebhooks/auth/factory.py +73 -0
- gitwebhooks/auth/gitee.py +86 -0
- gitwebhooks/auth/github.py +52 -0
- gitwebhooks/auth/gitlab.py +29 -0
- gitwebhooks/auth/verifier.py +34 -0
- gitwebhooks/cli/__init__.py +107 -0
- gitwebhooks/cli/config.py +216 -0
- gitwebhooks/cli/init_wizard.py +701 -0
- gitwebhooks/cli/prompts.py +275 -0
- gitwebhooks/cli/service.py +374 -0
- gitwebhooks/config/__init__.py +17 -0
- gitwebhooks/config/loader.py +170 -0
- gitwebhooks/config/models.py +145 -0
- gitwebhooks/config/registry.py +104 -0
- gitwebhooks/config/server.py +85 -0
- gitwebhooks/handlers/__init__.py +22 -0
- gitwebhooks/handlers/base.py +107 -0
- gitwebhooks/handlers/custom.py +73 -0
- gitwebhooks/handlers/factory.py +83 -0
- gitwebhooks/handlers/gitee.py +113 -0
- gitwebhooks/handlers/github.py +43 -0
- gitwebhooks/handlers/gitlab.py +43 -0
- gitwebhooks/handlers/request.py +248 -0
- gitwebhooks/logging/__init__.py +10 -0
- gitwebhooks/logging/setup.py +57 -0
- gitwebhooks/main/__init__.py +117 -0
- gitwebhooks/main/__main__.py +12 -0
- gitwebhooks/models/__init__.py +14 -0
- gitwebhooks/models/provider.py +22 -0
- gitwebhooks/models/request.py +61 -0
- gitwebhooks/models/result.py +40 -0
- gitwebhooks/server.py +128 -0
- gitwebhooks/utils/__init__.py +14 -0
- gitwebhooks/utils/constants.py +55 -0
- gitwebhooks/utils/exceptions.py +34 -0
- gitwebhooks/utils/executor.py +34 -0
- gitwebhooks/utils/systemd.py +274 -0
- gitwebhooks-2.1.0.dist-info/METADATA +494 -0
- gitwebhooks-2.1.0.dist-info/RECORD +47 -0
- gitwebhooks-2.1.0.dist-info/WHEEL +5 -0
- gitwebhooks-2.1.0.dist-info/entry_points.txt +2 -0
- gitwebhooks-2.1.0.dist-info/licenses/LICENSE +21 -0
- gitwebhooks-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive configuration initialization wizard.
|
|
3
|
+
|
|
4
|
+
This module provides a user-friendly wizard for creating git-webhooks-server
|
|
5
|
+
configuration files through an interactive command-line interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Optional, List, Dict
|
|
12
|
+
import configparser
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# Module Constants
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
CONFIG_LEVELS = {
|
|
20
|
+
'system': {
|
|
21
|
+
'path': '/etc/gitwebhooks.ini',
|
|
22
|
+
'requires_root': True
|
|
23
|
+
},
|
|
24
|
+
'local': {
|
|
25
|
+
'path': '/usr/local/etc/gitwebhooks.ini',
|
|
26
|
+
'requires_root': True
|
|
27
|
+
},
|
|
28
|
+
'user': {
|
|
29
|
+
'path': '~/.gitwebhooks.ini',
|
|
30
|
+
'requires_root': False
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
PLATFORMS = {
|
|
35
|
+
'github': {
|
|
36
|
+
'events': ['push', 'release', 'pull_request', 'tag'],
|
|
37
|
+
'requires_secret': True
|
|
38
|
+
},
|
|
39
|
+
'gitee': {
|
|
40
|
+
'events': ['push', 'release', 'pull_request'],
|
|
41
|
+
'requires_secret': True
|
|
42
|
+
},
|
|
43
|
+
'gitlab': {
|
|
44
|
+
'events': ['push', 'release', 'tag'],
|
|
45
|
+
'requires_secret': True
|
|
46
|
+
},
|
|
47
|
+
'custom': {
|
|
48
|
+
'events': [],
|
|
49
|
+
'requires_secret': False,
|
|
50
|
+
'custom_fields': ['header_name', 'header_value', 'identifier_path', 'header_event']
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# =============================================================================
|
|
56
|
+
# Data Classes
|
|
57
|
+
# =============================================================================
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ConfigLevel:
|
|
61
|
+
"""Configuration level metadata."""
|
|
62
|
+
name: str
|
|
63
|
+
path: str
|
|
64
|
+
requires_root: bool
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class ServerConfig:
|
|
69
|
+
"""Server configuration parameters."""
|
|
70
|
+
address: str
|
|
71
|
+
port: int
|
|
72
|
+
log_file: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class PlatformConfig:
|
|
77
|
+
"""Platform configuration parameters."""
|
|
78
|
+
platform: str
|
|
79
|
+
handle_events: List[str]
|
|
80
|
+
verify: bool
|
|
81
|
+
secret: Optional[str] = None
|
|
82
|
+
custom_params: Optional[Dict[str, str]] = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class RepositoryConfig:
|
|
87
|
+
"""Repository configuration parameters."""
|
|
88
|
+
name: str
|
|
89
|
+
cwd: str
|
|
90
|
+
cmd: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# Input Validation Functions
|
|
95
|
+
# =============================================================================
|
|
96
|
+
|
|
97
|
+
def validate_repo_name(value: str) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Validate repository name format.
|
|
100
|
+
|
|
101
|
+
Supports:
|
|
102
|
+
- Two-level format: owner/repo (GitHub/Gitee)
|
|
103
|
+
- Multi-level format: group/subgroup/project (GitLab)
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
value: Repository name string
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if valid, False otherwise
|
|
110
|
+
"""
|
|
111
|
+
return bool(re.match(r'^[^/]+(/[^/]+)+$', value))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def validate_existing_path(value: str) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Validate that a path exists and is a directory.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
value: Path string to validate
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
True if path exists and is a directory, False otherwise
|
|
123
|
+
"""
|
|
124
|
+
return os.path.isdir(value)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def validate_non_empty(value: str) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Validate that a string is not empty or whitespace-only.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
value: String to validate
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
True if string has non-whitespace content, False otherwise
|
|
136
|
+
"""
|
|
137
|
+
return bool(value.strip())
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def validate_port(value: str) -> bool:
|
|
141
|
+
"""
|
|
142
|
+
Validate port number.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
value: Port number as string
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True if valid port (1-65535), False otherwise
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
port = int(value)
|
|
152
|
+
return 1 <= port <= 65535
|
|
153
|
+
except ValueError:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# =============================================================================
|
|
158
|
+
# INI Generation Functions
|
|
159
|
+
# =============================================================================
|
|
160
|
+
|
|
161
|
+
def _generate_config(
|
|
162
|
+
server: ServerConfig,
|
|
163
|
+
platform: PlatformConfig,
|
|
164
|
+
repo: RepositoryConfig
|
|
165
|
+
) -> configparser.ConfigParser:
|
|
166
|
+
"""
|
|
167
|
+
Generate configuration object from collected data.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
server: Server configuration
|
|
171
|
+
platform: Platform configuration
|
|
172
|
+
repo: Repository configuration
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
ConfigParser object with all sections
|
|
176
|
+
"""
|
|
177
|
+
config = configparser.ConfigParser()
|
|
178
|
+
|
|
179
|
+
# Server section
|
|
180
|
+
config['server'] = {
|
|
181
|
+
'address': server.address,
|
|
182
|
+
'port': str(server.port),
|
|
183
|
+
'log_file': server.log_file
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Platform section
|
|
187
|
+
platform_section = platform.platform
|
|
188
|
+
platform_config = {
|
|
189
|
+
'handle_events': ', '.join(platform.handle_events),
|
|
190
|
+
'verify': 'true' if platform.verify else 'false'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if platform.verify and platform.secret:
|
|
194
|
+
platform_config['secret'] = platform.secret
|
|
195
|
+
|
|
196
|
+
# Add custom platform parameters if present
|
|
197
|
+
if platform.custom_params:
|
|
198
|
+
for key, value in platform.custom_params.items():
|
|
199
|
+
if value: # Only add non-empty values
|
|
200
|
+
platform_config[key] = value
|
|
201
|
+
|
|
202
|
+
config[platform_section] = platform_config
|
|
203
|
+
|
|
204
|
+
# Repository section
|
|
205
|
+
repo_section = f'repo/{repo.name}'
|
|
206
|
+
config[repo_section] = {
|
|
207
|
+
'cwd': repo.cwd,
|
|
208
|
+
'cmd': repo.cmd
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return config
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# =============================================================================
|
|
215
|
+
# Wizard Class
|
|
216
|
+
# =============================================================================
|
|
217
|
+
|
|
218
|
+
class Wizard:
|
|
219
|
+
"""
|
|
220
|
+
Interactive configuration wizard.
|
|
221
|
+
|
|
222
|
+
Guides users through creating a git-webhooks-server configuration file
|
|
223
|
+
by collecting server, platform, and repository configuration parameters.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def __init__(self, level: Optional[str] = None):
|
|
227
|
+
"""
|
|
228
|
+
Initialize the wizard.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
level: Optional configuration level (system/local/user).
|
|
232
|
+
If None, user will be prompted to select.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
ValueError: If level is provided but invalid
|
|
236
|
+
"""
|
|
237
|
+
if level is not None and level not in CONFIG_LEVELS:
|
|
238
|
+
raise ValueError(f"Invalid configuration level: {level}. "
|
|
239
|
+
f"Must be one of: {', '.join(CONFIG_LEVELS.keys())}")
|
|
240
|
+
|
|
241
|
+
self.level = level
|
|
242
|
+
self.config_level: Optional[ConfigLevel] = None
|
|
243
|
+
|
|
244
|
+
def run(self) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Run the wizard and generate configuration file.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Absolute path to the generated configuration file
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
KeyboardInterrupt: User presses Ctrl+C
|
|
253
|
+
PermissionError: Cannot write to target directory
|
|
254
|
+
FileExistsError: User cancels overwrite
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
# Step 1: Determine configuration level
|
|
258
|
+
if self.level is None:
|
|
259
|
+
self._select_level()
|
|
260
|
+
else:
|
|
261
|
+
self._set_level(self.level)
|
|
262
|
+
|
|
263
|
+
# Step 2: Check permissions
|
|
264
|
+
self._check_permissions()
|
|
265
|
+
|
|
266
|
+
# Step 3: Collect server configuration
|
|
267
|
+
server_config = self._collect_server_config()
|
|
268
|
+
|
|
269
|
+
# Step 4: Select and configure platform
|
|
270
|
+
platform_name = self._select_platform()
|
|
271
|
+
platform_config = self._collect_platform_config(platform_name)
|
|
272
|
+
|
|
273
|
+
# Step 5: Collect repository configuration
|
|
274
|
+
repo_config = self._collect_repository_config()
|
|
275
|
+
|
|
276
|
+
# Step 6: Confirm overwrite if file exists
|
|
277
|
+
config_path = os.path.expanduser(self.config_level.path)
|
|
278
|
+
backup = False
|
|
279
|
+
if os.path.exists(config_path):
|
|
280
|
+
backup = self._confirm_overwrite(config_path)
|
|
281
|
+
|
|
282
|
+
# Step 7: Generate configuration
|
|
283
|
+
config = _generate_config(server_config, platform_config, repo_config)
|
|
284
|
+
|
|
285
|
+
# Step 8: Write configuration file
|
|
286
|
+
self._write_config(config, config_path, backup=backup)
|
|
287
|
+
|
|
288
|
+
# Step 9: Show completion message
|
|
289
|
+
self._show_completion(config_path)
|
|
290
|
+
|
|
291
|
+
return config_path
|
|
292
|
+
|
|
293
|
+
except KeyboardInterrupt:
|
|
294
|
+
print("\n操作已取消")
|
|
295
|
+
# Clean up partial files if any
|
|
296
|
+
self._cleanup()
|
|
297
|
+
raise
|
|
298
|
+
|
|
299
|
+
def _set_level(self, level_name: str) -> None:
|
|
300
|
+
"""
|
|
301
|
+
Set configuration level from name.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
level_name: Configuration level name (system/local/user)
|
|
305
|
+
"""
|
|
306
|
+
level_config = CONFIG_LEVELS[level_name]
|
|
307
|
+
self.config_level = ConfigLevel(
|
|
308
|
+
name=level_name,
|
|
309
|
+
path=level_config['path'],
|
|
310
|
+
requires_root=level_config['requires_root']
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def _select_level(self) -> None:
|
|
314
|
+
"""
|
|
315
|
+
Interactively select configuration level.
|
|
316
|
+
|
|
317
|
+
Updates self.config_level with selected level.
|
|
318
|
+
"""
|
|
319
|
+
print("选择配置级别:")
|
|
320
|
+
levels = list(CONFIG_LEVELS.items())
|
|
321
|
+
for idx, (name, config) in enumerate(levels, 1):
|
|
322
|
+
path = config['path']
|
|
323
|
+
print(f" {idx}. {name} ({path})")
|
|
324
|
+
|
|
325
|
+
while True:
|
|
326
|
+
try:
|
|
327
|
+
choice = input(f"输入选择 [1-{len(levels)}]: ").strip()
|
|
328
|
+
if not choice:
|
|
329
|
+
print("请选择一个配置级别")
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
index = int(choice) - 1
|
|
333
|
+
if 0 <= index < len(levels):
|
|
334
|
+
level_name, level_config = levels[index]
|
|
335
|
+
self._set_level(level_name)
|
|
336
|
+
return
|
|
337
|
+
else:
|
|
338
|
+
print(f"无效选择,请输入 1-{len(levels)} 之间的数字")
|
|
339
|
+
except ValueError:
|
|
340
|
+
print("请输入有效的数字")
|
|
341
|
+
except KeyboardInterrupt:
|
|
342
|
+
raise
|
|
343
|
+
|
|
344
|
+
def _check_permissions(self) -> None:
|
|
345
|
+
"""
|
|
346
|
+
Check if target directory is writable.
|
|
347
|
+
|
|
348
|
+
Raises:
|
|
349
|
+
PermissionError: If target directory is not writable
|
|
350
|
+
"""
|
|
351
|
+
config_path = os.path.expanduser(self.config_level.path)
|
|
352
|
+
target_dir = os.path.dirname(config_path)
|
|
353
|
+
|
|
354
|
+
# Create directory if it doesn't exist (for user level)
|
|
355
|
+
if not os.path.exists(target_dir):
|
|
356
|
+
try:
|
|
357
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
358
|
+
except OSError as e:
|
|
359
|
+
raise PermissionError(
|
|
360
|
+
f"无法创建目录 {target_dir}: {e}\n"
|
|
361
|
+
f"建议: 使用 sudo 或选择 user 级别"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Check write permission
|
|
365
|
+
if not os.access(target_dir, os.W_OK):
|
|
366
|
+
raise PermissionError(
|
|
367
|
+
f"无法写入 {self.config_level.path}(权限不足)\n"
|
|
368
|
+
f"建议: 使用 sudo 或选择 user 级别"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def _collect_server_config(self) -> ServerConfig:
|
|
372
|
+
"""
|
|
373
|
+
Collect server configuration parameters.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
ServerConfig object with collected parameters
|
|
377
|
+
"""
|
|
378
|
+
print("\n--- 服务器配置 ---")
|
|
379
|
+
|
|
380
|
+
# Address
|
|
381
|
+
default_address = "0.0.0.0"
|
|
382
|
+
address = input(f"服务器监听地址 [默认: {default_address}]: ").strip()
|
|
383
|
+
if not address:
|
|
384
|
+
address = default_address
|
|
385
|
+
|
|
386
|
+
# Port
|
|
387
|
+
default_port = "6789"
|
|
388
|
+
while True:
|
|
389
|
+
port_str = input(f"服务器端口 [默认: {default_port}]: ").strip()
|
|
390
|
+
if not port_str:
|
|
391
|
+
port = int(default_port)
|
|
392
|
+
break
|
|
393
|
+
if validate_port(port_str):
|
|
394
|
+
port = int(port_str)
|
|
395
|
+
break
|
|
396
|
+
print("端口号必须在 1-65535 之间")
|
|
397
|
+
|
|
398
|
+
# Log file
|
|
399
|
+
default_log = "/var/log/git-webhooks-server.log"
|
|
400
|
+
log_file = input(f"日志文件路径 [默认: {default_log}]: ").strip()
|
|
401
|
+
if not log_file:
|
|
402
|
+
log_file = default_log
|
|
403
|
+
|
|
404
|
+
return ServerConfig(
|
|
405
|
+
address=address,
|
|
406
|
+
port=port,
|
|
407
|
+
log_file=log_file
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def _select_platform(self) -> str:
|
|
411
|
+
"""
|
|
412
|
+
Interactively select Git platform.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Platform name (github/gitee/gitlab/custom)
|
|
416
|
+
"""
|
|
417
|
+
print("\n选择 Git 平台:")
|
|
418
|
+
platforms = list(PLATFORMS.keys())
|
|
419
|
+
for idx, platform in enumerate(platforms, 1):
|
|
420
|
+
print(f" {idx}. {platform}")
|
|
421
|
+
|
|
422
|
+
while True:
|
|
423
|
+
try:
|
|
424
|
+
choice = input(f"输入选择 [1-{len(platforms)}]: ").strip()
|
|
425
|
+
if not choice:
|
|
426
|
+
print("请选择一个平台")
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
index = int(choice) - 1
|
|
430
|
+
if 0 <= index < len(platforms):
|
|
431
|
+
return platforms[index]
|
|
432
|
+
else:
|
|
433
|
+
print(f"无效选择,请输入 1-{len(platforms)} 之间的数字")
|
|
434
|
+
except ValueError:
|
|
435
|
+
print("请输入有效的数字")
|
|
436
|
+
except KeyboardInterrupt:
|
|
437
|
+
raise
|
|
438
|
+
|
|
439
|
+
def _collect_platform_config(self, platform: str) -> PlatformConfig:
|
|
440
|
+
"""
|
|
441
|
+
Collect platform configuration parameters.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
platform: Platform name
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
PlatformConfig object with collected parameters
|
|
448
|
+
"""
|
|
449
|
+
print(f"\n--- {platform.upper()} 平台配置 ---")
|
|
450
|
+
|
|
451
|
+
platform_info = PLATFORMS[platform]
|
|
452
|
+
|
|
453
|
+
# Collect events
|
|
454
|
+
handle_events = self._collect_events(platform)
|
|
455
|
+
|
|
456
|
+
# Collect verification settings
|
|
457
|
+
verify = False
|
|
458
|
+
secret = None
|
|
459
|
+
|
|
460
|
+
if platform_info['requires_secret']:
|
|
461
|
+
while True:
|
|
462
|
+
verify_input = input("是否启用签名验证? [y/N]: ").strip().lower()
|
|
463
|
+
if verify_input in ('y', 'yes'):
|
|
464
|
+
verify = True
|
|
465
|
+
break
|
|
466
|
+
elif verify_input in ('n', 'no', ''):
|
|
467
|
+
verify = False
|
|
468
|
+
break
|
|
469
|
+
else:
|
|
470
|
+
print("请输入 y 或 n")
|
|
471
|
+
|
|
472
|
+
if verify:
|
|
473
|
+
while True:
|
|
474
|
+
secret = input("输入验证密钥: ").strip()
|
|
475
|
+
if validate_non_empty(secret):
|
|
476
|
+
break
|
|
477
|
+
print("验证密钥不能为空")
|
|
478
|
+
|
|
479
|
+
# Custom platform parameters
|
|
480
|
+
custom_params = None
|
|
481
|
+
if platform == 'custom':
|
|
482
|
+
custom_params = self._collect_custom_params()
|
|
483
|
+
|
|
484
|
+
return PlatformConfig(
|
|
485
|
+
platform=platform,
|
|
486
|
+
handle_events=handle_events,
|
|
487
|
+
verify=verify,
|
|
488
|
+
secret=secret,
|
|
489
|
+
custom_params=custom_params
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def _collect_events(self, platform: str) -> List[str]:
|
|
493
|
+
"""
|
|
494
|
+
Collect webhook events to handle.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
platform: Platform name
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
List of event names
|
|
501
|
+
"""
|
|
502
|
+
platform_info = PLATFORMS[platform]
|
|
503
|
+
|
|
504
|
+
if platform == 'custom':
|
|
505
|
+
# Custom platform: ask for event name
|
|
506
|
+
print("自定义平台事件处理")
|
|
507
|
+
print("输入要处理的事件名称(直接回车跳过):")
|
|
508
|
+
events_str = input().strip()
|
|
509
|
+
return [e.strip() for e in events_str.split(',') if e.strip()] if events_str else ['webhook']
|
|
510
|
+
|
|
511
|
+
# Standard platforms: show event list
|
|
512
|
+
events = platform_info['events']
|
|
513
|
+
print("选择要处理的 webhook 事件(多个用逗号分隔,直接回车默认选 1):")
|
|
514
|
+
for idx, event in enumerate(events, 1):
|
|
515
|
+
print(f" {idx}. {event}")
|
|
516
|
+
|
|
517
|
+
while True:
|
|
518
|
+
try:
|
|
519
|
+
choice = input("输入选择: ").strip()
|
|
520
|
+
if not choice:
|
|
521
|
+
# Default to first event (push)
|
|
522
|
+
return [events[0]]
|
|
523
|
+
|
|
524
|
+
# Parse comma-separated indices
|
|
525
|
+
indices = [int(x.strip()) - 1 for x in choice.split(',')]
|
|
526
|
+
|
|
527
|
+
# Validate indices
|
|
528
|
+
if all(0 <= i < len(events) for i in indices):
|
|
529
|
+
return [events[i] for i in indices]
|
|
530
|
+
else:
|
|
531
|
+
print(f"无效选择,请输入 1-{len(events)} 之间的数字,多个用逗号分隔")
|
|
532
|
+
|
|
533
|
+
except ValueError:
|
|
534
|
+
print("请输入有效的数字,多个用逗号分隔")
|
|
535
|
+
except KeyboardInterrupt:
|
|
536
|
+
raise
|
|
537
|
+
|
|
538
|
+
def _collect_custom_params(self) -> Dict[str, str]:
|
|
539
|
+
"""
|
|
540
|
+
Collect custom platform specific parameters.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
Dictionary of custom parameter names and values
|
|
544
|
+
"""
|
|
545
|
+
print("\n--- 自定义平台参数 ---")
|
|
546
|
+
|
|
547
|
+
params = {}
|
|
548
|
+
|
|
549
|
+
# header_name
|
|
550
|
+
default_header_name = "X-Webhook-Token"
|
|
551
|
+
header_name = input(f"输入识别 Header 名称 [默认: {default_header_name}]: ").strip()
|
|
552
|
+
params['header_name'] = header_name if header_name else default_header_name
|
|
553
|
+
|
|
554
|
+
# header_value
|
|
555
|
+
while True:
|
|
556
|
+
header_value = input("输入验证 Header 值: ").strip()
|
|
557
|
+
if validate_non_empty(header_value):
|
|
558
|
+
params['header_value'] = header_value
|
|
559
|
+
break
|
|
560
|
+
print("Header 值不能为空")
|
|
561
|
+
|
|
562
|
+
# identifier_path
|
|
563
|
+
default_identifier = "project.path_with_namespace"
|
|
564
|
+
identifier_path = input(f"输入仓库标识 JSON 路径 [默认: {default_identifier}]: ").strip()
|
|
565
|
+
params['identifier_path'] = identifier_path if identifier_path else default_identifier
|
|
566
|
+
|
|
567
|
+
# header_event (optional)
|
|
568
|
+
header_event = input("输入事件类型 Header 名称 [可选,直接回车跳过]: ").strip()
|
|
569
|
+
if header_event:
|
|
570
|
+
params['header_event'] = header_event
|
|
571
|
+
|
|
572
|
+
return params
|
|
573
|
+
|
|
574
|
+
def _collect_repository_config(self) -> RepositoryConfig:
|
|
575
|
+
"""
|
|
576
|
+
Collect repository configuration parameters.
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
RepositoryConfig object with collected parameters
|
|
580
|
+
"""
|
|
581
|
+
print("\n--- 仓库配置 ---")
|
|
582
|
+
|
|
583
|
+
# Repository name
|
|
584
|
+
while True:
|
|
585
|
+
repo_name = input("输入仓库名称 (格式: owner/repo): ").strip()
|
|
586
|
+
if validate_repo_name(repo_name):
|
|
587
|
+
break
|
|
588
|
+
print("仓库名称格式不正确,应为 owner/repo 格式")
|
|
589
|
+
|
|
590
|
+
# Working directory
|
|
591
|
+
while True:
|
|
592
|
+
cwd = input("输入工作目录路径 (必须已存在): ").strip()
|
|
593
|
+
if validate_existing_path(cwd):
|
|
594
|
+
break
|
|
595
|
+
print(f"错误: 目录不存在: {cwd}")
|
|
596
|
+
|
|
597
|
+
# Deploy command
|
|
598
|
+
while True:
|
|
599
|
+
cmd = input("输入部署命令: ").strip()
|
|
600
|
+
if validate_non_empty(cmd):
|
|
601
|
+
break
|
|
602
|
+
print("部署命令不能为空")
|
|
603
|
+
|
|
604
|
+
return RepositoryConfig(
|
|
605
|
+
name=repo_name,
|
|
606
|
+
cwd=cwd,
|
|
607
|
+
cmd=cmd
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
def _confirm_overwrite(self, path: str) -> bool:
|
|
611
|
+
"""
|
|
612
|
+
Confirm whether to overwrite existing configuration file.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
path: Path to existing file
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
True to proceed with overwrite, False to cancel
|
|
619
|
+
|
|
620
|
+
Raises:
|
|
621
|
+
FileExistsError: User chooses to cancel
|
|
622
|
+
"""
|
|
623
|
+
print(f"\n配置文件 {path} 已存在")
|
|
624
|
+
print("选择操作:")
|
|
625
|
+
print(" 1. 覆盖(会删除原文件)")
|
|
626
|
+
print(" 2. 备份后覆盖(保存为 .bak)")
|
|
627
|
+
print(" 3. 取消操作")
|
|
628
|
+
|
|
629
|
+
while True:
|
|
630
|
+
try:
|
|
631
|
+
choice = input("输入选择 [1-3]: ").strip()
|
|
632
|
+
if choice == '1':
|
|
633
|
+
return False # Overwrite without backup
|
|
634
|
+
elif choice == '2':
|
|
635
|
+
return True # Backup then overwrite
|
|
636
|
+
elif choice == '3':
|
|
637
|
+
raise FileExistsError("用户取消操作")
|
|
638
|
+
else:
|
|
639
|
+
print("请输入 1、2 或 3")
|
|
640
|
+
except KeyboardInterrupt:
|
|
641
|
+
raise
|
|
642
|
+
|
|
643
|
+
def _write_config(
|
|
644
|
+
self,
|
|
645
|
+
config: configparser.ConfigParser,
|
|
646
|
+
path: str,
|
|
647
|
+
backup: bool = False
|
|
648
|
+
) -> None:
|
|
649
|
+
"""
|
|
650
|
+
Write configuration file to disk.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
config: ConfigParser object
|
|
654
|
+
path: Target file path
|
|
655
|
+
backup: Whether to backup existing file
|
|
656
|
+
"""
|
|
657
|
+
try:
|
|
658
|
+
# Backup existing file if requested
|
|
659
|
+
if backup and os.path.exists(path):
|
|
660
|
+
backup_path = f"{path}.bak"
|
|
661
|
+
os.rename(path, backup_path)
|
|
662
|
+
print(f"已备份原文件到: {backup_path}")
|
|
663
|
+
|
|
664
|
+
# Write configuration
|
|
665
|
+
with open(path, 'w') as f:
|
|
666
|
+
config.write(f)
|
|
667
|
+
|
|
668
|
+
# Set secure permissions (user read/write only)
|
|
669
|
+
os.chmod(path, 0o600)
|
|
670
|
+
|
|
671
|
+
except OSError as e:
|
|
672
|
+
raise PermissionError(f"无法写入配置文件 {path}: {e}")
|
|
673
|
+
|
|
674
|
+
def _show_completion(self, path: str) -> None:
|
|
675
|
+
"""
|
|
676
|
+
Display completion message and next steps.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
path: Generated configuration file path
|
|
680
|
+
"""
|
|
681
|
+
print(f"\n配置文件已生成: {path}")
|
|
682
|
+
print("后续步骤:")
|
|
683
|
+
print(f" 1. 检查配置文件内容: gitwebhooks-cli config view {path}")
|
|
684
|
+
print(f" 2. 启动服务器: gitwebhooks-cli -c {path}")
|
|
685
|
+
print(" 3. 或安装为服务: sudo gitwebhooks-cli service install")
|
|
686
|
+
|
|
687
|
+
def _cleanup(self) -> None:
|
|
688
|
+
"""
|
|
689
|
+
Clean up partial files if wizard is interrupted.
|
|
690
|
+
|
|
691
|
+
This method is called when KeyboardInterrupt is caught.
|
|
692
|
+
"""
|
|
693
|
+
# Remove partial configuration file if it exists
|
|
694
|
+
if self.config_level:
|
|
695
|
+
config_path = os.path.expanduser(self.config_level.path)
|
|
696
|
+
if os.path.exists(config_path):
|
|
697
|
+
try:
|
|
698
|
+
os.remove(config_path)
|
|
699
|
+
print(f"已清理部分文件: {config_path}")
|
|
700
|
+
except OSError:
|
|
701
|
+
pass
|