gitacc-switcher 1.0.1__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.
- gitacc_switcher/__init__.py +4 -0
- gitacc_switcher/account_manager.py +421 -0
- gitacc_switcher/cli.py +426 -0
- gitacc_switcher/completion.py +17 -0
- gitacc_switcher/config_manager.py +422 -0
- gitacc_switcher/hook_manager.py +230 -0
- gitacc_switcher/ssh_manager.py +237 -0
- gitacc_switcher/utils.py +82 -0
- gitacc_switcher-1.0.1.dist-info/METADATA +227 -0
- gitacc_switcher-1.0.1.dist-info/RECORD +22 -0
- gitacc_switcher-1.0.1.dist-info/WHEEL +5 -0
- gitacc_switcher-1.0.1.dist-info/entry_points.txt +2 -0
- gitacc_switcher-1.0.1.dist-info/licenses/LICENSE +21 -0
- gitacc_switcher-1.0.1.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_account_manager.py +257 -0
- tests/test_cli.py +242 -0
- tests/test_completion.py +30 -0
- tests/test_config_manager.py +231 -0
- tests/test_hook_manager.py +122 -0
- tests/test_ssh_manager.py +185 -0
- tests/test_utils.py +98 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Account management operations."""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Dict, List
|
|
7
|
+
from .config_manager import ConfigManager
|
|
8
|
+
from .ssh_manager import SSHManager
|
|
9
|
+
from .hook_manager import HookManager
|
|
10
|
+
from .utils import echo_color, ask_yes_no, validate_ssh_key_type
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AccountManager:
|
|
14
|
+
"""Manages Git account operations."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.config_manager = ConfigManager()
|
|
18
|
+
self.ssh_manager = SSHManager()
|
|
19
|
+
self.hook_manager = HookManager()
|
|
20
|
+
|
|
21
|
+
def add_account(self, key_type: str = "rsa") -> bool:
|
|
22
|
+
"""Add a new Git account.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
key_type: SSH key type (default: rsa)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
True if successful, False otherwise
|
|
29
|
+
"""
|
|
30
|
+
# Validate key type
|
|
31
|
+
if not validate_ssh_key_type(key_type):
|
|
32
|
+
echo_color("r", f"Invalid SSH key type: {key_type}")
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
# Get account info from user
|
|
36
|
+
account_name = input("Enter account name (identifier): ").strip()
|
|
37
|
+
if not account_name:
|
|
38
|
+
echo_color("r", "Account name cannot be empty!")
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
# Get Git name (defaults to account name)
|
|
42
|
+
git_name_prompt = (
|
|
43
|
+
f"Enter Git name (default: {account_name}, press Enter to use same): "
|
|
44
|
+
)
|
|
45
|
+
git_name = input(git_name_prompt).strip()
|
|
46
|
+
if not git_name:
|
|
47
|
+
git_name = account_name # Use account name as default
|
|
48
|
+
|
|
49
|
+
email = input("Enter your git user email: ").strip()
|
|
50
|
+
if not email:
|
|
51
|
+
echo_color("r", "Email cannot be empty!")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# Check if account already exists before prompting for passphrase
|
|
55
|
+
existing_account = self.config_manager.get_account(account_name)
|
|
56
|
+
overwrite = False
|
|
57
|
+
|
|
58
|
+
if existing_account:
|
|
59
|
+
echo_color("r", "Warning: Already have same account name.")
|
|
60
|
+
if ask_yes_no("Do you want to overwrite?"):
|
|
61
|
+
overwrite = True
|
|
62
|
+
else:
|
|
63
|
+
echo_color("y", "Please use another account name.")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
# Ask if user wants to add a passphrase to the SSH key
|
|
67
|
+
passphrase = None
|
|
68
|
+
if ask_yes_no("Do you want to add a passphrase to the SSH key?"):
|
|
69
|
+
passphrase = getpass.getpass("Enter passphrase (will be hidden): ")
|
|
70
|
+
if passphrase:
|
|
71
|
+
passphrase_confirm = getpass.getpass("Confirm passphrase: ")
|
|
72
|
+
if passphrase != passphrase_confirm:
|
|
73
|
+
echo_color("r", "Passphrases do not match!")
|
|
74
|
+
return False
|
|
75
|
+
else:
|
|
76
|
+
echo_color("y", "No passphrase set (empty passphrase).")
|
|
77
|
+
|
|
78
|
+
# Generate SSH keys
|
|
79
|
+
if overwrite:
|
|
80
|
+
# Remove old account entry first
|
|
81
|
+
self.config_manager.remove_account(account_name)
|
|
82
|
+
private_key, public_key = self.ssh_manager.overwrite_ssh_key(
|
|
83
|
+
key_type, account_name, email, passphrase
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
private_key, public_key = self.ssh_manager.generate_ssh_key(
|
|
87
|
+
key_type, account_name, email, passphrase
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not private_key or not public_key:
|
|
91
|
+
echo_color("r", "Failed to generate SSH keys!")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
# Add to .gitacc file
|
|
95
|
+
success = self.config_manager.add_account(
|
|
96
|
+
account_name, git_name, email, private_key, public_key
|
|
97
|
+
)
|
|
98
|
+
if not success:
|
|
99
|
+
echo_color("r", "Failed to save account info!")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# Show public key
|
|
103
|
+
public_key_content = self.ssh_manager.get_public_key_content(public_key)
|
|
104
|
+
if public_key_content:
|
|
105
|
+
echo_color("g", "Your SSH publish key is :")
|
|
106
|
+
print(public_key_content)
|
|
107
|
+
echo_color("g", "Paste it to your SSH keys in github or server.")
|
|
108
|
+
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
def remove_account(self, account_name: Optional[str] = None) -> bool:
|
|
112
|
+
"""Remove a Git account.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
account_name: Account name to remove (if None, prompt user)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if successful, False otherwise
|
|
119
|
+
"""
|
|
120
|
+
if not account_name:
|
|
121
|
+
account_name = input("Enter the git user name you want to remove: ").strip()
|
|
122
|
+
if not account_name:
|
|
123
|
+
echo_color("r", "Account name cannot be empty!")
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
# Get account info
|
|
127
|
+
account_info = self.config_manager.get_account(account_name)
|
|
128
|
+
if not account_info:
|
|
129
|
+
echo_color("r", "Wrong: account name!!")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
# Show account details and ask for confirmation
|
|
133
|
+
git_name = account_info.get("name", account_name)
|
|
134
|
+
email = account_info.get("email", "N/A")
|
|
135
|
+
echo_color("y", f"Account to remove: {account_name}")
|
|
136
|
+
echo_color("y", f" Git name: {git_name}")
|
|
137
|
+
echo_color("y", f" Email: {email}")
|
|
138
|
+
|
|
139
|
+
if not ask_yes_no(f"Are you sure you want to remove account '{account_name}'?"):
|
|
140
|
+
echo_color("y", "Removal cancelled.")
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
# Remove SSH keys
|
|
144
|
+
private_key = account_info.get("private_key")
|
|
145
|
+
public_key = account_info.get("public_key")
|
|
146
|
+
|
|
147
|
+
if private_key and public_key:
|
|
148
|
+
self.ssh_manager.delete_ssh_key(private_key, public_key)
|
|
149
|
+
|
|
150
|
+
# Remove from .gitacc file
|
|
151
|
+
success = self.config_manager.remove_account(account_name)
|
|
152
|
+
if not success:
|
|
153
|
+
echo_color("r", "Failed to remove account from config!")
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
echo_color("g", f'Account "{account_name}" removed successfully!')
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
def switch_account(self, account_name: str) -> bool:
|
|
160
|
+
"""Switch to a Git account.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
account_name: Account name to switch to
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if successful, False otherwise
|
|
167
|
+
"""
|
|
168
|
+
# Get account info
|
|
169
|
+
account_info = self.config_manager.get_account(account_name)
|
|
170
|
+
if not account_info:
|
|
171
|
+
echo_color("r", "Wrong: account name!!")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
# Check if SSH agent is already running
|
|
175
|
+
agent_running = self.ssh_manager.is_ssh_agent_running()
|
|
176
|
+
|
|
177
|
+
if not agent_running:
|
|
178
|
+
# Agent not running, need to start it
|
|
179
|
+
echo_color("y", "SSH agent is not running in your shell.")
|
|
180
|
+
echo_color("y", "Please run the following command in your shell:")
|
|
181
|
+
echo_color("b", " eval $(ssh-agent)")
|
|
182
|
+
echo_color("y", "Then run this command again:")
|
|
183
|
+
echo_color("b", f" gitacc switch {account_name}")
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
# Clear all existing keys from agent first
|
|
187
|
+
self.ssh_manager.clear_all_keys()
|
|
188
|
+
|
|
189
|
+
# Agent is running, add only the new account's key
|
|
190
|
+
private_key = account_info.get("private_key")
|
|
191
|
+
if not private_key or not Path(private_key).exists():
|
|
192
|
+
echo_color("r", f"SSH key not found: {private_key}")
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
success, error_msg = self.ssh_manager.add_key_to_agent(private_key)
|
|
196
|
+
if not success:
|
|
197
|
+
echo_color("r", f"Failed to add SSH key to agent: {error_msg}")
|
|
198
|
+
echo_color("y", "Make sure the SSH agent is running in your shell.")
|
|
199
|
+
echo_color("y", "You may need to run: eval $(ssh-agent)")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
# Verify key was added and is the only one
|
|
203
|
+
try:
|
|
204
|
+
result = subprocess.run(["ssh-add", "-l"], capture_output=True, text=True)
|
|
205
|
+
if result.returncode == 0:
|
|
206
|
+
key_count = len(
|
|
207
|
+
[line for line in result.stdout.strip().split("\n") if line.strip()]
|
|
208
|
+
)
|
|
209
|
+
echo_color(
|
|
210
|
+
"g",
|
|
211
|
+
f"SSH key added to agent successfully ({key_count} key(s) in agent)",
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
echo_color("y", "Warning: Could not verify key was added to agent")
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
# Set Git config
|
|
219
|
+
name = account_info.get("name")
|
|
220
|
+
email = account_info.get("email")
|
|
221
|
+
if not self.config_manager.set_git_config(name, email):
|
|
222
|
+
echo_color("r", "Failed to set Git config!")
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
echo_color("g", f'Switched to account "{account_name}" successfully!')
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
def logout(self) -> bool:
|
|
229
|
+
"""Logout current Git account.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if successful, False otherwise
|
|
233
|
+
"""
|
|
234
|
+
if self.ssh_manager.is_ssh_agent_running():
|
|
235
|
+
self.ssh_manager.kill_ssh_agent()
|
|
236
|
+
|
|
237
|
+
self.config_manager.unset_git_config()
|
|
238
|
+
echo_color("g", "Logged out successfully!")
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
def list_accounts(self) -> List[str]:
|
|
242
|
+
"""List all registered accounts.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
List of account names
|
|
246
|
+
"""
|
|
247
|
+
return self.config_manager.list_account_names()
|
|
248
|
+
|
|
249
|
+
def list_accounts_detailed(self) -> Dict[str, Dict[str, str]]:
|
|
250
|
+
"""List all registered accounts with details.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Dictionary mapping account names to their info
|
|
254
|
+
"""
|
|
255
|
+
return self.config_manager.read_accounts()
|
|
256
|
+
|
|
257
|
+
def account_exists(self, account_name: str) -> bool:
|
|
258
|
+
"""Check if an account exists.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
account_name: Account name to check
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
True if account exists, False otherwise
|
|
265
|
+
"""
|
|
266
|
+
return self.config_manager.get_account(account_name) is not None
|
|
267
|
+
|
|
268
|
+
def update_account(
|
|
269
|
+
self,
|
|
270
|
+
account_name: str,
|
|
271
|
+
new_git_name: Optional[str] = None,
|
|
272
|
+
new_email: Optional[str] = None,
|
|
273
|
+
) -> bool:
|
|
274
|
+
"""Update the Git name and/or email for an existing account.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
account_name: Account identifier to update
|
|
278
|
+
new_git_name: New Git name (if None, will prompt)
|
|
279
|
+
new_email: New email (if None, will prompt)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True if successful, False otherwise
|
|
283
|
+
"""
|
|
284
|
+
account_info = self.config_manager.get_account(account_name)
|
|
285
|
+
if not account_info:
|
|
286
|
+
echo_color("r", f'Account "{account_name}" not found!')
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
current_git_name = account_info.get("name", account_name)
|
|
290
|
+
current_email = account_info.get("email", "")
|
|
291
|
+
|
|
292
|
+
if new_git_name is None:
|
|
293
|
+
prompt = f"Enter new Git name (current: {current_git_name}, press Enter to keep): "
|
|
294
|
+
new_git_name = input(prompt).strip() or current_git_name
|
|
295
|
+
|
|
296
|
+
if new_email is None:
|
|
297
|
+
prompt = (
|
|
298
|
+
f"Enter new email (current: {current_email}, press Enter to keep): "
|
|
299
|
+
)
|
|
300
|
+
new_email = input(prompt).strip() or current_email
|
|
301
|
+
|
|
302
|
+
changed = False
|
|
303
|
+
|
|
304
|
+
if new_git_name != current_git_name:
|
|
305
|
+
if not self.config_manager.update_account_field(
|
|
306
|
+
account_name, "name", new_git_name
|
|
307
|
+
):
|
|
308
|
+
echo_color("r", "Failed to update Git name!")
|
|
309
|
+
return False
|
|
310
|
+
echo_color(
|
|
311
|
+
"g",
|
|
312
|
+
f'Updated Git name for "{account_name}": {current_git_name} → {new_git_name}',
|
|
313
|
+
)
|
|
314
|
+
changed = True
|
|
315
|
+
|
|
316
|
+
if new_email != current_email:
|
|
317
|
+
if not self.config_manager.update_account_field(
|
|
318
|
+
account_name, "email", new_email
|
|
319
|
+
):
|
|
320
|
+
echo_color("r", "Failed to update email!")
|
|
321
|
+
return False
|
|
322
|
+
echo_color(
|
|
323
|
+
"g",
|
|
324
|
+
f'Updated email for "{account_name}": {current_email} → {new_email}',
|
|
325
|
+
)
|
|
326
|
+
changed = True
|
|
327
|
+
|
|
328
|
+
if not changed:
|
|
329
|
+
echo_color("y", "No changes made.")
|
|
330
|
+
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
def init_repo(self, account_name: str, repo_path: Optional[Path] = None) -> bool:
|
|
334
|
+
"""Initialize repository with expected account and install pre-commit hook.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
account_name: Account name to set as expected for this repo
|
|
338
|
+
repo_path: Path to repository (default: current directory)
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
True if successful, False otherwise
|
|
342
|
+
"""
|
|
343
|
+
if not self.config_manager.is_git_repo(repo_path):
|
|
344
|
+
echo_color("r", "Not a Git repository!")
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
# Verify account exists
|
|
348
|
+
account_info = self.config_manager.get_account(account_name)
|
|
349
|
+
if not account_info:
|
|
350
|
+
echo_color("r", f'Account "{account_name}" not found!')
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
# Set expected account
|
|
354
|
+
if not self.config_manager.set_repo_expected_account(account_name, repo_path):
|
|
355
|
+
echo_color("r", "Failed to set expected account!")
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
# Install pre-commit hook
|
|
359
|
+
if not self.hook_manager.install_pre_commit_hook(repo_path):
|
|
360
|
+
echo_color("r", "Failed to install pre-commit hook!")
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
echo_color("g", f'Repository initialized with account "{account_name}"')
|
|
364
|
+
echo_color("g", "Pre-commit hook installed. Commits will be validated.")
|
|
365
|
+
return True
|
|
366
|
+
|
|
367
|
+
def verify_account(self, repo_path: Optional[Path] = None) -> bool:
|
|
368
|
+
"""Verify that the current Git account matches the expected account for the repository.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
repo_path: Path to repository (default: current directory)
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
True if account matches, False otherwise
|
|
375
|
+
"""
|
|
376
|
+
if not self.config_manager.is_git_repo(repo_path):
|
|
377
|
+
echo_color("r", "Not a Git repository!")
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
expected_account = self.config_manager.get_repo_expected_account(repo_path)
|
|
381
|
+
if not expected_account:
|
|
382
|
+
echo_color("y", "No expected account set for this repository.")
|
|
383
|
+
echo_color("y", 'Use "gitacc init <account>" to set one.')
|
|
384
|
+
return True # Not an error, just not configured
|
|
385
|
+
|
|
386
|
+
# Get expected account info
|
|
387
|
+
account_info = self.config_manager.get_account(expected_account)
|
|
388
|
+
if not account_info:
|
|
389
|
+
echo_color(
|
|
390
|
+
"r",
|
|
391
|
+
f'Expected account "{expected_account}" not found in registered accounts!',
|
|
392
|
+
)
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
# Get current config
|
|
396
|
+
current_config = self.config_manager.get_current_git_config_local(repo_path)
|
|
397
|
+
expected_name = account_info.get("name")
|
|
398
|
+
expected_email = account_info.get("email")
|
|
399
|
+
current_name = current_config.get("name")
|
|
400
|
+
current_email = current_config.get("email")
|
|
401
|
+
|
|
402
|
+
# Check if they match
|
|
403
|
+
if current_name == expected_name and current_email == expected_email:
|
|
404
|
+
echo_color("g", f'✓ Account verified: "{expected_account}"')
|
|
405
|
+
echo_color("g", f" Name: {current_name}")
|
|
406
|
+
echo_color("g", f" Email: {current_email}")
|
|
407
|
+
return True
|
|
408
|
+
else:
|
|
409
|
+
echo_color("r", f"✗ Account mismatch!")
|
|
410
|
+
echo_color("r", "")
|
|
411
|
+
echo_color("r", f'Expected account: "{expected_account}"')
|
|
412
|
+
echo_color("r", f" Name: {expected_name}")
|
|
413
|
+
echo_color("r", f" Email: {expected_email}")
|
|
414
|
+
echo_color("r", "")
|
|
415
|
+
echo_color("r", "Current Git config:")
|
|
416
|
+
echo_color("r", f" Name: {current_name}")
|
|
417
|
+
echo_color("r", f" Email: {current_email}")
|
|
418
|
+
echo_color("r", "")
|
|
419
|
+
echo_color("y", f"Please switch to the correct account:")
|
|
420
|
+
echo_color("y", f" gitacc switch {expected_account}")
|
|
421
|
+
return False
|