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,275 @@
|
|
|
1
|
+
"""Interactive prompt utilities
|
|
2
|
+
|
|
3
|
+
Provides functions for interactive user input and validation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Callable, Optional, Dict, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Question template for configuration initialization
|
|
11
|
+
QUESTIONS = [
|
|
12
|
+
# Server configuration
|
|
13
|
+
{
|
|
14
|
+
'key': 'server_address',
|
|
15
|
+
'prompt': 'Server listen address [default: 0.0.0.0]: ',
|
|
16
|
+
'default': '0.0.0.0',
|
|
17
|
+
'validator': None,
|
|
18
|
+
'required': True
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
'key': 'server_port',
|
|
22
|
+
'prompt': 'Server port [default: 6789]: ',
|
|
23
|
+
'default': '6789',
|
|
24
|
+
'validator': 'validate_port',
|
|
25
|
+
'required': True
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
'key': 'log_file',
|
|
29
|
+
'prompt': 'Log file path [default: ~/.gitwebhook.log]: ',
|
|
30
|
+
'default': '~/.gitwebhook.log',
|
|
31
|
+
'validator': None,
|
|
32
|
+
'required': False
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Platform-specific questions
|
|
38
|
+
PLATFORM_QUESTIONS = {
|
|
39
|
+
'github': [
|
|
40
|
+
{
|
|
41
|
+
'section': 'github',
|
|
42
|
+
'key': 'verify',
|
|
43
|
+
'prompt': ' Enable signature verification? (Y/n) [default: Y]: ',
|
|
44
|
+
'default': 'true',
|
|
45
|
+
'validator': None,
|
|
46
|
+
'required': False
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
'section': 'github',
|
|
50
|
+
'key': 'secret',
|
|
51
|
+
'prompt': ' Webhook secret (leave empty to skip): ',
|
|
52
|
+
'default': '',
|
|
53
|
+
'validator': None,
|
|
54
|
+
'required': False
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
'gitee': [
|
|
58
|
+
{
|
|
59
|
+
'section': 'gitee',
|
|
60
|
+
'key': 'verify',
|
|
61
|
+
'prompt': ' Enable signature verification? (Y/n) [default: Y]: ',
|
|
62
|
+
'default': 'true',
|
|
63
|
+
'validator': None,
|
|
64
|
+
'required': False
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
'section': 'gitee',
|
|
68
|
+
'key': 'secret',
|
|
69
|
+
'prompt': ' Webhook secret (leave empty to skip): ',
|
|
70
|
+
'default': '',
|
|
71
|
+
'validator': None,
|
|
72
|
+
'required': False
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
'gitlab': [
|
|
76
|
+
{
|
|
77
|
+
'section': 'gitlab',
|
|
78
|
+
'key': 'verify',
|
|
79
|
+
'prompt': ' Enable token verification? (Y/n) [default: Y]: ',
|
|
80
|
+
'default': 'true',
|
|
81
|
+
'validator': None,
|
|
82
|
+
'required': False
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
'section': 'gitlab',
|
|
86
|
+
'key': 'secret',
|
|
87
|
+
'prompt': ' Webhook token (leave empty to skip): ',
|
|
88
|
+
'default': '',
|
|
89
|
+
'validator': None,
|
|
90
|
+
'required': False
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def ask_question(prompt: str,
|
|
97
|
+
validator: Optional[Callable[[str], Any]] = None,
|
|
98
|
+
default: Optional[Any] = None,
|
|
99
|
+
required: bool = True) -> Any:
|
|
100
|
+
"""Ask a question with optional validation and default value
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
prompt: Question prompt to display
|
|
104
|
+
validator: Optional validation function that raises ValueError on invalid input
|
|
105
|
+
default: Optional default value if user provides empty input
|
|
106
|
+
required: Whether a value is required (defaults to True)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Validated user input or default value
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
KeyboardInterrupt: If user confirms exit on Ctrl+C
|
|
113
|
+
"""
|
|
114
|
+
while True:
|
|
115
|
+
try:
|
|
116
|
+
response = input(prompt).strip()
|
|
117
|
+
|
|
118
|
+
# Use default if empty input provided
|
|
119
|
+
if not response and default is not None:
|
|
120
|
+
return default
|
|
121
|
+
|
|
122
|
+
# Validate if validator provided
|
|
123
|
+
if validator:
|
|
124
|
+
if isinstance(validator, str):
|
|
125
|
+
# Validator is a string name, look up function
|
|
126
|
+
validator_func = globals().get(validator)
|
|
127
|
+
if validator_func:
|
|
128
|
+
return validator_func(response)
|
|
129
|
+
else:
|
|
130
|
+
return validator(response)
|
|
131
|
+
|
|
132
|
+
return response
|
|
133
|
+
|
|
134
|
+
except KeyboardInterrupt:
|
|
135
|
+
# Ask for confirmation before exiting
|
|
136
|
+
if ask_yes_no('Confirm exit? Unsaved changes will be lost (y/N) ', default=False):
|
|
137
|
+
print('Configuration cancelled')
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
except ValueError as e:
|
|
140
|
+
print(f'Invalid input: {e}')
|
|
141
|
+
print('Please try again')
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def ask_yes_no(prompt: str, default: bool = False) -> bool:
|
|
145
|
+
"""Ask a yes/no question
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
prompt: Question prompt
|
|
149
|
+
default: Default answer if user just presses Enter
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True for yes, False for no
|
|
153
|
+
"""
|
|
154
|
+
# Set default indicator
|
|
155
|
+
default_indicator = 'Y' if default else 'N'
|
|
156
|
+
if not default:
|
|
157
|
+
prompt = prompt.replace('(y/N)', f'[{default_indicator}]')
|
|
158
|
+
else:
|
|
159
|
+
prompt = prompt.replace('(Y/n)', f'[{default_indicator}]')
|
|
160
|
+
|
|
161
|
+
while True:
|
|
162
|
+
try:
|
|
163
|
+
response = input(prompt).strip().lower()
|
|
164
|
+
|
|
165
|
+
# Use default if empty input
|
|
166
|
+
if not response:
|
|
167
|
+
return default
|
|
168
|
+
|
|
169
|
+
# Parse response
|
|
170
|
+
if response in ('y', 'yes'):
|
|
171
|
+
return True
|
|
172
|
+
elif response in ('n', 'no'):
|
|
173
|
+
return False
|
|
174
|
+
else:
|
|
175
|
+
print('Please enter y or n')
|
|
176
|
+
|
|
177
|
+
except KeyboardInterrupt:
|
|
178
|
+
if ask_yes_no('Confirm exit? Unsaved changes will be lost (y/N) ', default=False):
|
|
179
|
+
print('Configuration cancelled')
|
|
180
|
+
sys.exit(0)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def validate_port(value: str) -> int:
|
|
184
|
+
"""Validate port number
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
value: String input to validate
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Integer port number
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ValueError: If port is invalid
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
port = int(value)
|
|
197
|
+
if not 1 <= port <= 65535:
|
|
198
|
+
raise ValueError('Port must be between 1 and 65535')
|
|
199
|
+
return port
|
|
200
|
+
except ValueError as e:
|
|
201
|
+
if 'must be between' in str(e):
|
|
202
|
+
raise
|
|
203
|
+
raise ValueError('Port must be a number')
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def validate_address(value: str) -> str:
|
|
207
|
+
"""Validate server address
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
value: String input to validate
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Validated address string
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
ValueError: If address is empty
|
|
217
|
+
"""
|
|
218
|
+
if not value:
|
|
219
|
+
raise ValueError('Server address cannot be empty')
|
|
220
|
+
return value
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def validate_path(value: str) -> str:
|
|
224
|
+
"""Validate file path
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
value: String input to validate
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Validated path string
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
ValueError: If path is empty
|
|
234
|
+
"""
|
|
235
|
+
if not value:
|
|
236
|
+
raise ValueError('Path cannot be empty')
|
|
237
|
+
return value
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def validate_bool(value: str) -> bool:
|
|
241
|
+
"""Validate boolean input
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
value: String input to validate
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True or False
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
ValueError: If input is not a boolean value
|
|
251
|
+
"""
|
|
252
|
+
value_lower = value.lower()
|
|
253
|
+
if value_lower in ('y', 'yes', 'true', '1'):
|
|
254
|
+
return True
|
|
255
|
+
elif value_lower in ('n', 'no', 'false', '0'):
|
|
256
|
+
return False
|
|
257
|
+
else:
|
|
258
|
+
raise ValueError('Please enter yes/no or y/n')
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def validate_text(value: str) -> str:
|
|
262
|
+
"""Validate text input (non-empty)
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
value: String input to validate
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Validated text string
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
ValueError: If text is empty
|
|
272
|
+
"""
|
|
273
|
+
if not value:
|
|
274
|
+
raise ValueError('Input cannot be empty')
|
|
275
|
+
return value
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Service installation CLI commands
|
|
2
|
+
|
|
3
|
+
Provides systemd service installation and removal functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from gitwebhooks.utils.systemd import (
|
|
12
|
+
check_systemd,
|
|
13
|
+
check_root_permission,
|
|
14
|
+
generate_service_file,
|
|
15
|
+
get_service_path,
|
|
16
|
+
detect_installation_type,
|
|
17
|
+
InstallationType,
|
|
18
|
+
)
|
|
19
|
+
from gitwebhooks.cli.prompts import ask_yes_no
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Error codes
|
|
23
|
+
E_PERM = 1
|
|
24
|
+
E_SYSTEMD = 1
|
|
25
|
+
E_EXISTS = 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cmd_install(args: argparse.Namespace) -> int:
|
|
29
|
+
"""Install systemd service
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
args: Parsed command-line arguments
|
|
33
|
+
- force: Force overwrite existing service
|
|
34
|
+
- verbose: Verbosity level (0=normal, 1=verbose, 2=extra verbose)
|
|
35
|
+
- dry_run: Preview mode without actual installation
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Exit code (0 = success, 1 = error)
|
|
39
|
+
"""
|
|
40
|
+
# Get verbose and dry_run from args
|
|
41
|
+
verbose = getattr(args, 'verbose', 0)
|
|
42
|
+
dry_run = getattr(args, 'dry_run', False)
|
|
43
|
+
|
|
44
|
+
# For dry-run mode, skip root check
|
|
45
|
+
if not dry_run:
|
|
46
|
+
# Check root permission
|
|
47
|
+
if not check_root_permission():
|
|
48
|
+
print('Error: This operation requires root privileges', file=sys.stderr)
|
|
49
|
+
print('Please use: sudo gitwebhooks-cli service install', file=sys.stderr)
|
|
50
|
+
return E_PERM
|
|
51
|
+
|
|
52
|
+
# Check systemd availability
|
|
53
|
+
if not check_systemd():
|
|
54
|
+
print('Error: systemd is not supported on this system', file=sys.stderr)
|
|
55
|
+
return E_SYSTEMD
|
|
56
|
+
|
|
57
|
+
# Check if service already exists
|
|
58
|
+
service_path = get_service_path()
|
|
59
|
+
if service_path.exists():
|
|
60
|
+
if not args.force:
|
|
61
|
+
print(f'Error: Service already installed', file=sys.stderr)
|
|
62
|
+
print(f'Service file: {service_path}', file=sys.stderr)
|
|
63
|
+
print('Use --force to force overwrite', file=sys.stderr)
|
|
64
|
+
return E_EXISTS
|
|
65
|
+
|
|
66
|
+
# Confirm overwrite
|
|
67
|
+
if not confirm_overwrite():
|
|
68
|
+
print('Installation cancelled')
|
|
69
|
+
return 0
|
|
70
|
+
else:
|
|
71
|
+
# In dry-run mode, warn if service exists but continue
|
|
72
|
+
service_path = get_service_path()
|
|
73
|
+
if service_path.exists() and not args.force:
|
|
74
|
+
print(f'Notice: Service already exists at {service_path}', file=sys.stderr)
|
|
75
|
+
print('Previewing service file content (use --force to suppress this notice)...', file=sys.stderr)
|
|
76
|
+
print()
|
|
77
|
+
|
|
78
|
+
# Perform installation
|
|
79
|
+
return install_service(force=args.force, verbose=verbose, dry_run=dry_run)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def install_service(force: bool = False, verbose: int = 0, dry_run: bool = False) -> int:
|
|
83
|
+
"""Execute service installation logic
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
force: Force overwrite existing service
|
|
87
|
+
verbose: Verbosity level (0=normal, 1=verbose, 2=extra verbose)
|
|
88
|
+
dry_run: Preview mode without actual installation
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Exit code (0 = success, 1 = error)
|
|
92
|
+
"""
|
|
93
|
+
# Detect installation environment
|
|
94
|
+
env = detect_installation_type()
|
|
95
|
+
|
|
96
|
+
# Verbose output
|
|
97
|
+
if verbose >= 1:
|
|
98
|
+
print('Detecting Python environment...')
|
|
99
|
+
print(f' Installation type: {env.type.value}')
|
|
100
|
+
print(f' Python interpreter: {env.python_path}')
|
|
101
|
+
print(f' CLI path: {env.cli_path}')
|
|
102
|
+
if verbose >= 2 and env.detection_methods:
|
|
103
|
+
print(f' Detection methods: {", ".join(env.detection_methods)}')
|
|
104
|
+
if env.venv_root:
|
|
105
|
+
print(f' Virtual environment root: {env.venv_root}')
|
|
106
|
+
print()
|
|
107
|
+
|
|
108
|
+
# Check if environment is supported
|
|
109
|
+
if not env.is_supported:
|
|
110
|
+
print(f'Error: {env.error_message}', file=sys.stderr)
|
|
111
|
+
print()
|
|
112
|
+
print('For migration from conda:', file=sys.stderr)
|
|
113
|
+
print(' 1. Deactivate your conda environment: conda deactivate', file=sys.stderr)
|
|
114
|
+
print(' 2. Install using pipx: pipx install git-webhooks-server', file=sys.stderr)
|
|
115
|
+
print(' OR create a venv: python3 -m venv ~/venv/gitwebhooks', file=sys.stderr)
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
# Warnings for non-recommended installation types
|
|
119
|
+
if env.type == InstallationType.VIRTUALENV:
|
|
120
|
+
print('Warning: virtualenv detected. venv is recommended.', file=sys.stderr)
|
|
121
|
+
print()
|
|
122
|
+
elif env.type == InstallationType.SYSTEM_PIP:
|
|
123
|
+
print('Warning: System pip installation is not recommended.', file=sys.stderr)
|
|
124
|
+
print('Consider using pipx or venv for better isolation.', file=sys.stderr)
|
|
125
|
+
print()
|
|
126
|
+
|
|
127
|
+
# Get paths
|
|
128
|
+
service_path = get_service_path()
|
|
129
|
+
config_path = '~/.gitwebhook.ini'
|
|
130
|
+
|
|
131
|
+
# Generate service file based on installation type
|
|
132
|
+
# For pipx: use cli_path directly
|
|
133
|
+
# For others: use python -m gitwebhooks.cli
|
|
134
|
+
use_python_module = env.type != InstallationType.PIPX
|
|
135
|
+
|
|
136
|
+
service_content = generate_service_file(
|
|
137
|
+
cli_path=env.cli_path,
|
|
138
|
+
config_path=config_path,
|
|
139
|
+
python_path=env.python_path if use_python_module else None,
|
|
140
|
+
use_python_module=use_python_module
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Dry-run mode
|
|
144
|
+
if dry_run:
|
|
145
|
+
print('Dry-run mode: Preview of service file:')
|
|
146
|
+
print('---')
|
|
147
|
+
print(service_content)
|
|
148
|
+
print('---')
|
|
149
|
+
print()
|
|
150
|
+
print(f'Service file would be written to: {service_path}')
|
|
151
|
+
print('Dry-run complete: No changes made.')
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
# Actual installation
|
|
155
|
+
print('Installing systemd service...')
|
|
156
|
+
|
|
157
|
+
# Write service file
|
|
158
|
+
try:
|
|
159
|
+
service_path.write_text(service_content)
|
|
160
|
+
print(f'Service file: {service_path}')
|
|
161
|
+
except (IOError, OSError) as e:
|
|
162
|
+
print(f'Error: Cannot write service file: {e}', file=sys.stderr)
|
|
163
|
+
return 1
|
|
164
|
+
|
|
165
|
+
# Reload systemd daemon
|
|
166
|
+
print('Reloading systemd daemon...')
|
|
167
|
+
try:
|
|
168
|
+
subprocess.run(
|
|
169
|
+
['systemctl', 'daemon-reload'],
|
|
170
|
+
check=True,
|
|
171
|
+
capture_output=True
|
|
172
|
+
)
|
|
173
|
+
except subprocess.CalledProcessError as e:
|
|
174
|
+
print(f'Warning: systemctl daemon-reload failed: {e}', file=sys.stderr)
|
|
175
|
+
# Continue anyway
|
|
176
|
+
|
|
177
|
+
# Enable and start service
|
|
178
|
+
return enable_and_start_service()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def check_service_exists() -> bool:
|
|
182
|
+
"""Check if service is already installed
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
True if service file exists, False otherwise
|
|
186
|
+
"""
|
|
187
|
+
return get_service_path().exists()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def confirm_overwrite() -> bool:
|
|
191
|
+
"""Ask user to confirm overwrite
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if user confirms, False otherwise
|
|
195
|
+
"""
|
|
196
|
+
return ask_yes_no('Service already exists, overwrite? (y/N) ', default=False)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def enable_and_start_service() -> int:
|
|
200
|
+
"""Enable and start the service
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Exit code (0 = success, 1 = error)
|
|
204
|
+
"""
|
|
205
|
+
# Ask if user wants to enable and start
|
|
206
|
+
if not ask_yes_no('Enable and start service? (Y/n) ', default=True):
|
|
207
|
+
print('Service installed but not started')
|
|
208
|
+
print('Start manually: sudo systemctl start git-webhooks-server')
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
# Enable service
|
|
212
|
+
print('Enabling service...')
|
|
213
|
+
try:
|
|
214
|
+
subprocess.run(
|
|
215
|
+
['systemctl', 'enable', 'git-webhooks-server'],
|
|
216
|
+
check=True,
|
|
217
|
+
capture_output=True
|
|
218
|
+
)
|
|
219
|
+
except subprocess.CalledProcessError as e:
|
|
220
|
+
print(f'Warning: systemctl enable failed: {e}', file=sys.stderr)
|
|
221
|
+
# Continue anyway
|
|
222
|
+
|
|
223
|
+
# Start service
|
|
224
|
+
print('Starting service...')
|
|
225
|
+
try:
|
|
226
|
+
subprocess.run(
|
|
227
|
+
['systemctl', 'start', 'git-webhooks-server'],
|
|
228
|
+
check=True,
|
|
229
|
+
capture_output=True
|
|
230
|
+
)
|
|
231
|
+
except subprocess.CalledProcessError as e:
|
|
232
|
+
print(f'Warning: systemctl start failed: {e}', file=sys.stderr)
|
|
233
|
+
# Continue anyway
|
|
234
|
+
|
|
235
|
+
print()
|
|
236
|
+
print('Installation complete!')
|
|
237
|
+
print('Service status: systemctl status git-webhooks-server')
|
|
238
|
+
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def cmd_uninstall(args: argparse.Namespace) -> int:
|
|
243
|
+
"""Uninstall systemd service
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
args: Parsed command-line arguments
|
|
247
|
+
- purge: Also remove configuration file
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Exit code (0 = success, 1 = error)
|
|
251
|
+
"""
|
|
252
|
+
# Check root permission
|
|
253
|
+
if not check_root_permission():
|
|
254
|
+
print('Error: This operation requires root privileges', file=sys.stderr)
|
|
255
|
+
print('Please use: sudo gitwebhooks-cli service uninstall', file=sys.stderr)
|
|
256
|
+
return E_PERM
|
|
257
|
+
|
|
258
|
+
# Check if service is installed
|
|
259
|
+
if not check_service_exists():
|
|
260
|
+
print('Error: Service not installed', file=sys.stderr)
|
|
261
|
+
return 1
|
|
262
|
+
|
|
263
|
+
# Perform uninstallation
|
|
264
|
+
return uninstall_service(args.purge)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def uninstall_service(purge: bool = False) -> int:
|
|
268
|
+
"""Execute service uninstallation logic
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
purge: Also remove configuration file
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Exit code (0 = success, 1 = error)
|
|
275
|
+
"""
|
|
276
|
+
print('Uninstalling systemd service...')
|
|
277
|
+
|
|
278
|
+
# Stop and disable service
|
|
279
|
+
stop_and_disable_service()
|
|
280
|
+
|
|
281
|
+
# Remove service file
|
|
282
|
+
remove_service_file()
|
|
283
|
+
|
|
284
|
+
# Handle configuration cleanup
|
|
285
|
+
handle_config_cleanup(purge)
|
|
286
|
+
|
|
287
|
+
print()
|
|
288
|
+
print('Uninstallation complete!')
|
|
289
|
+
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def stop_and_disable_service() -> None:
|
|
294
|
+
"""Stop and disable the service
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
None
|
|
298
|
+
"""
|
|
299
|
+
service_path = get_service_path()
|
|
300
|
+
|
|
301
|
+
# Stop service if running
|
|
302
|
+
print('Stopping service...')
|
|
303
|
+
try:
|
|
304
|
+
subprocess.run(
|
|
305
|
+
['systemctl', 'stop', 'git-webhooks-server'],
|
|
306
|
+
check=False,
|
|
307
|
+
capture_output=True
|
|
308
|
+
)
|
|
309
|
+
except OSError as e:
|
|
310
|
+
print(f'Warning: systemctl stop failed: {e}', file=sys.stderr)
|
|
311
|
+
# Continue anyway
|
|
312
|
+
|
|
313
|
+
# Disable service
|
|
314
|
+
print('Disabling service...')
|
|
315
|
+
try:
|
|
316
|
+
subprocess.run(
|
|
317
|
+
['systemctl', 'disable', 'git-webhooks-server'],
|
|
318
|
+
check=False,
|
|
319
|
+
capture_output=True
|
|
320
|
+
)
|
|
321
|
+
except OSError as e:
|
|
322
|
+
print(f'Warning: systemctl disable failed: {e}', file=sys.stderr)
|
|
323
|
+
# Continue anyway
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def remove_service_file() -> None:
|
|
327
|
+
"""Remove the service file
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
None
|
|
331
|
+
"""
|
|
332
|
+
service_path = get_service_path()
|
|
333
|
+
|
|
334
|
+
print('Removing service file...')
|
|
335
|
+
try:
|
|
336
|
+
service_path.unlink()
|
|
337
|
+
except OSError as e:
|
|
338
|
+
print(f'Warning: Failed to remove service file: {e}', file=sys.stderr)
|
|
339
|
+
# Continue anyway
|
|
340
|
+
|
|
341
|
+
# Reload systemd daemon
|
|
342
|
+
print('Reloading systemd daemon...')
|
|
343
|
+
try:
|
|
344
|
+
subprocess.run(
|
|
345
|
+
['systemctl', 'daemon-reload'],
|
|
346
|
+
check=False,
|
|
347
|
+
capture_output=True
|
|
348
|
+
)
|
|
349
|
+
except OSError as e:
|
|
350
|
+
print(f'Warning: systemctl daemon-reload failed: {e}', file=sys.stderr)
|
|
351
|
+
# Continue anyway
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def handle_config_cleanup(purge: bool) -> None:
|
|
355
|
+
"""Handle configuration file cleanup
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
purge: If True, remove configuration file
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
None
|
|
362
|
+
"""
|
|
363
|
+
config_path = Path('~/.gitwebhook.ini').expanduser()
|
|
364
|
+
|
|
365
|
+
if not purge:
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
if config_path.exists():
|
|
369
|
+
print(f'Removing configuration file: {config_path}')
|
|
370
|
+
try:
|
|
371
|
+
config_path.unlink()
|
|
372
|
+
except OSError as e:
|
|
373
|
+
print(f'Warning: Failed to remove configuration file: {e}', file=sys.stderr)
|
|
374
|
+
# Continue anyway
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Configuration management module
|
|
2
|
+
|
|
3
|
+
Responsible for loading, parsing, and managing INI format configuration files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .loader import ConfigLoader
|
|
7
|
+
from .models import ProviderConfig, RepositoryConfig
|
|
8
|
+
from .registry import ConfigurationRegistry
|
|
9
|
+
from .server import ServerConfig
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'ConfigLoader',
|
|
13
|
+
'ProviderConfig',
|
|
14
|
+
'RepositoryConfig',
|
|
15
|
+
'ServerConfig',
|
|
16
|
+
'ConfigurationRegistry',
|
|
17
|
+
]
|