gitacc-switcher 1.2.0__tar.gz → 1.3.0__tar.gz
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-1.2.0/gitacc_switcher.egg-info → gitacc_switcher-1.3.0}/PKG-INFO +1 -1
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher/__init__.py +1 -1
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher/account_manager.py +61 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher/cli.py +41 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher/config_manager.py +15 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher/ssh_manager.py +49 -1
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0/gitacc_switcher.egg-info}/PKG-INFO +1 -1
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/pyproject.toml +1 -1
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/setup.py +1 -1
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/tests/test_account_manager.py +105 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/tests/test_cli.py +59 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/tests/test_config_manager.py +21 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/tests/test_ssh_manager.py +48 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/LICENSE +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/MANIFEST.in +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/README.md +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher/completion.py +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher/hook_manager.py +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher/utils.py +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/SOURCES.txt +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/dependency_links.txt +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/entry_points.txt +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/requires.txt +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/top_level.txt +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/requirements.txt +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/setup.cfg +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/tests/__init__.py +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/tests/test_completion.py +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/tests/test_hook_manager.py +0 -0
- {gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/tests/test_utils.py +0 -0
|
@@ -382,6 +382,67 @@ class AccountManager:
|
|
|
382
382
|
|
|
383
383
|
return True
|
|
384
384
|
|
|
385
|
+
def get_status(self, repo_path: Optional[Path] = None) -> Dict[str, object]:
|
|
386
|
+
"""Report the currently active Git identity and SSH agent key, and
|
|
387
|
+
which registered account (if any) they match.
|
|
388
|
+
|
|
389
|
+
Read-only: makes no changes to Git config, the SSH agent, or any
|
|
390
|
+
managed files.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
repo_path: Path to repository (default: current directory). Used
|
|
394
|
+
to resolve local Git config when run inside a repo.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Dictionary with the current name/email, the matched account name
|
|
398
|
+
(or None), whether the SSH agent is running, and which account
|
|
399
|
+
(if any) the currently loaded agent key belongs to.
|
|
400
|
+
"""
|
|
401
|
+
if self.config_manager.is_git_repo(repo_path):
|
|
402
|
+
current_config = self.config_manager.get_current_git_config_local(repo_path)
|
|
403
|
+
else:
|
|
404
|
+
current_config = self.config_manager.get_current_git_config()
|
|
405
|
+
|
|
406
|
+
current_name = current_config.get("name")
|
|
407
|
+
current_email = current_config.get("email")
|
|
408
|
+
|
|
409
|
+
accounts = self.config_manager.read_accounts()
|
|
410
|
+
|
|
411
|
+
matched_account = None
|
|
412
|
+
for account_name, account_info in accounts.items():
|
|
413
|
+
if (
|
|
414
|
+
current_name
|
|
415
|
+
and current_email
|
|
416
|
+
and account_info.get("name") == current_name
|
|
417
|
+
and account_info.get("email") == current_email
|
|
418
|
+
):
|
|
419
|
+
matched_account = account_name
|
|
420
|
+
break
|
|
421
|
+
|
|
422
|
+
agent_running = self.ssh_manager.is_ssh_agent_running()
|
|
423
|
+
agent_fingerprints = (
|
|
424
|
+
self.ssh_manager.get_agent_fingerprints() if agent_running else []
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
matched_key_account = None
|
|
428
|
+
if agent_fingerprints:
|
|
429
|
+
for account_name, account_info in accounts.items():
|
|
430
|
+
public_key = account_info.get("public_key")
|
|
431
|
+
if not public_key:
|
|
432
|
+
continue
|
|
433
|
+
fingerprint = self.ssh_manager.get_key_fingerprint(public_key)
|
|
434
|
+
if fingerprint and fingerprint in agent_fingerprints:
|
|
435
|
+
matched_key_account = account_name
|
|
436
|
+
break
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
"name": current_name,
|
|
440
|
+
"email": current_email,
|
|
441
|
+
"matched_account": matched_account,
|
|
442
|
+
"agent_running": agent_running,
|
|
443
|
+
"matched_key_account": matched_key_account,
|
|
444
|
+
}
|
|
445
|
+
|
|
385
446
|
def init_repo(self, account_name: str, repo_path: Optional[Path] = None) -> bool:
|
|
386
447
|
"""Initialize repository with expected account and install pre-commit hook.
|
|
387
448
|
|
|
@@ -45,6 +45,7 @@ class CLI:
|
|
|
45
45
|
self._add_switch_command(subparsers)
|
|
46
46
|
self._add_list_command(subparsers)
|
|
47
47
|
self._add_logout_command(subparsers)
|
|
48
|
+
self._add_status_command(subparsers)
|
|
48
49
|
self._add_init_command(subparsers)
|
|
49
50
|
self._add_verify_command(subparsers)
|
|
50
51
|
self._add_update_command(subparsers)
|
|
@@ -74,6 +75,7 @@ Examples:
|
|
|
74
75
|
gitacc remove myaccount Remove an account
|
|
75
76
|
gitacc list List all registered accounts
|
|
76
77
|
gitacc logout Logout current account
|
|
78
|
+
gitacc status Show the currently active account
|
|
77
79
|
gitacc init myaccount Initialize repo with account validation
|
|
78
80
|
gitacc verify Verify current account matches repo
|
|
79
81
|
gitacc update myaccount Update Git name for an account
|
|
@@ -158,6 +160,16 @@ Examples:
|
|
|
158
160
|
)
|
|
159
161
|
parser.set_defaults(func=self._handle_logout)
|
|
160
162
|
|
|
163
|
+
def _add_status_command(self, subparsers: argparse._SubParsersAction) -> None:
|
|
164
|
+
"""Add the 'status' command parser."""
|
|
165
|
+
parser = subparsers.add_parser(
|
|
166
|
+
"status",
|
|
167
|
+
help="Show the currently active account",
|
|
168
|
+
description="Show the current Git identity and SSH agent key, "
|
|
169
|
+
"and which managed account (if any) matches",
|
|
170
|
+
)
|
|
171
|
+
parser.set_defaults(func=self._handle_status)
|
|
172
|
+
|
|
161
173
|
def _add_init_command(self, subparsers: argparse._SubParsersAction) -> None:
|
|
162
174
|
"""Add the 'init' command parser."""
|
|
163
175
|
parser = subparsers.add_parser(
|
|
@@ -396,6 +408,35 @@ Examples:
|
|
|
396
408
|
success = self.account_manager.logout()
|
|
397
409
|
return 0 if success else 1
|
|
398
410
|
|
|
411
|
+
def _handle_status(self, args: argparse.Namespace) -> int:
|
|
412
|
+
"""Handle the 'status' command."""
|
|
413
|
+
status = self.account_manager.get_status()
|
|
414
|
+
name = status.get("name")
|
|
415
|
+
email = status.get("email")
|
|
416
|
+
|
|
417
|
+
echo_color("g", "Current Git identity:")
|
|
418
|
+
echo_color("g", f" Name: {name or '(not set)'}")
|
|
419
|
+
echo_color("g", f" Email: {email or '(not set)'}")
|
|
420
|
+
|
|
421
|
+
matched_account = status.get("matched_account")
|
|
422
|
+
if matched_account:
|
|
423
|
+
echo_color("g", f'Matched account: "{matched_account}"')
|
|
424
|
+
else:
|
|
425
|
+
echo_color("y", "No managed account matches the current Git identity.")
|
|
426
|
+
|
|
427
|
+
if status.get("agent_running"):
|
|
428
|
+
matched_key_account = status.get("matched_key_account")
|
|
429
|
+
if matched_key_account:
|
|
430
|
+
echo_color(
|
|
431
|
+
"g", f'SSH agent key matches account: "{matched_key_account}"'
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
echo_color("y", "SSH agent key does not match any managed account.")
|
|
435
|
+
else:
|
|
436
|
+
echo_color("y", "SSH agent is not running.")
|
|
437
|
+
|
|
438
|
+
return 0
|
|
439
|
+
|
|
399
440
|
def _handle_init(self, args: argparse.Namespace) -> int:
|
|
400
441
|
"""Handle the 'init' command."""
|
|
401
442
|
success = self.account_manager.init_repo(args.account_name)
|
|
@@ -359,22 +359,37 @@ class ConfigManager:
|
|
|
359
359
|
line = lines[i]
|
|
360
360
|
if re.match(rf"^\s*\[{re.escape(account_name)}\]\s*$", line):
|
|
361
361
|
in_target_section = True
|
|
362
|
+
field_written = False
|
|
362
363
|
new_lines.append(line)
|
|
363
364
|
i += 1
|
|
364
365
|
while i < len(lines) and in_target_section:
|
|
365
366
|
if re.match(r"^\s*\[", lines[i]) and not re.match(
|
|
366
367
|
rf"^\s*\[{re.escape(account_name)}\]\s*$", lines[i]
|
|
367
368
|
):
|
|
369
|
+
if not field_written:
|
|
370
|
+
# Field never appeared in this section (e.g. an
|
|
371
|
+
# account written before this field existed) —
|
|
372
|
+
# add it rather than silently leaving it unset.
|
|
373
|
+
new_lines.append(f"\t{field} = {new_value}\n")
|
|
374
|
+
field_written = True
|
|
368
375
|
in_target_section = False
|
|
369
376
|
new_lines.append(lines[i])
|
|
370
377
|
i += 1
|
|
371
378
|
break
|
|
372
379
|
elif re.match(rf"^\s*{re.escape(field)}\s*=", lines[i]):
|
|
373
380
|
new_lines.append(f"\t{field} = {new_value}\n")
|
|
381
|
+
field_written = True
|
|
374
382
|
i += 1
|
|
375
383
|
else:
|
|
376
384
|
new_lines.append(lines[i])
|
|
377
385
|
i += 1
|
|
386
|
+
else:
|
|
387
|
+
# Loop ended because we reached end of file, not a break —
|
|
388
|
+
# the target section is the last one, so append here too.
|
|
389
|
+
if in_target_section and not field_written:
|
|
390
|
+
new_lines.append(f"\t{field} = {new_value}\n")
|
|
391
|
+
field_written = True
|
|
392
|
+
in_target_section = False
|
|
378
393
|
else:
|
|
379
394
|
new_lines.append(line)
|
|
380
395
|
i += 1
|
|
@@ -6,7 +6,7 @@ import subprocess
|
|
|
6
6
|
import tempfile
|
|
7
7
|
import re
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Optional, Tuple
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class SSHManager:
|
|
@@ -221,6 +221,54 @@ class SSHManager:
|
|
|
221
221
|
"ssh-add command not found. Please ensure SSH tools are installed.",
|
|
222
222
|
)
|
|
223
223
|
|
|
224
|
+
def get_agent_fingerprints(self) -> List[str]:
|
|
225
|
+
"""Get fingerprints of keys currently loaded in the SSH agent.
|
|
226
|
+
|
|
227
|
+
Read-only; does not modify the agent.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of fingerprint strings (e.g. "SHA256:abc..."). Empty if the
|
|
231
|
+
agent is not running, has no keys loaded, or can't be reached.
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
result = subprocess.run(["ssh-add", "-l"], capture_output=True, text=True)
|
|
235
|
+
except FileNotFoundError:
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
if result.returncode != 0:
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
fingerprints = []
|
|
242
|
+
for line in result.stdout.strip().split("\n"):
|
|
243
|
+
parts = line.strip().split()
|
|
244
|
+
if len(parts) >= 2:
|
|
245
|
+
fingerprints.append(parts[1])
|
|
246
|
+
return fingerprints
|
|
247
|
+
|
|
248
|
+
def get_key_fingerprint(self, public_key_path: str) -> Optional[str]:
|
|
249
|
+
"""Get the fingerprint of a public key file.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
public_key_path: Path to the public key file
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Fingerprint string (e.g. "SHA256:abc...") or None on error
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
result = subprocess.run(
|
|
259
|
+
["ssh-keygen", "-lf", str(public_key_path)],
|
|
260
|
+
capture_output=True,
|
|
261
|
+
text=True,
|
|
262
|
+
)
|
|
263
|
+
except FileNotFoundError:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
if result.returncode != 0:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
parts = result.stdout.strip().split()
|
|
270
|
+
return parts[1] if len(parts) >= 2 else None
|
|
271
|
+
|
|
224
272
|
def get_public_key_content(self, public_key_path: str) -> Optional[str]:
|
|
225
273
|
"""Read public key content.
|
|
226
274
|
|
|
@@ -9,7 +9,7 @@ long_description = readme_file.read_text() if readme_file.exists() else ""
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="gitacc-switcher",
|
|
12
|
-
version="1.
|
|
12
|
+
version="1.3.0",
|
|
13
13
|
description="Git Account Switcher - Manage multiple Git SSH accounts easily",
|
|
14
14
|
long_description=long_description,
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
@@ -426,3 +426,108 @@ class TestAddAccount:
|
|
|
426
426
|
assert result is True
|
|
427
427
|
_, kwargs = manager.config_manager.add_account.call_args
|
|
428
428
|
assert kwargs["sign"] is False
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class TestGetStatus:
|
|
432
|
+
def test_matches_account_by_identity(self, manager):
|
|
433
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
434
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
435
|
+
"name": "Work User",
|
|
436
|
+
"email": "work@company.com",
|
|
437
|
+
}
|
|
438
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
439
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
440
|
+
|
|
441
|
+
status = manager.get_status()
|
|
442
|
+
|
|
443
|
+
assert status["name"] == "Work User"
|
|
444
|
+
assert status["email"] == "work@company.com"
|
|
445
|
+
assert status["matched_account"] == "work"
|
|
446
|
+
assert status["agent_running"] is False
|
|
447
|
+
assert status["matched_key_account"] is None
|
|
448
|
+
|
|
449
|
+
def test_no_match_when_identity_unknown(self, manager):
|
|
450
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
451
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
452
|
+
"name": "Someone Else",
|
|
453
|
+
"email": "someone@else.com",
|
|
454
|
+
}
|
|
455
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
456
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
457
|
+
|
|
458
|
+
status = manager.get_status()
|
|
459
|
+
|
|
460
|
+
assert status["matched_account"] is None
|
|
461
|
+
|
|
462
|
+
def test_no_match_when_identity_unset(self, manager):
|
|
463
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
464
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
465
|
+
"name": None,
|
|
466
|
+
"email": None,
|
|
467
|
+
}
|
|
468
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
469
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
470
|
+
|
|
471
|
+
status = manager.get_status()
|
|
472
|
+
|
|
473
|
+
assert status["matched_account"] is None
|
|
474
|
+
|
|
475
|
+
def test_uses_local_git_config_inside_repo(self, manager):
|
|
476
|
+
manager.config_manager.is_git_repo.return_value = True
|
|
477
|
+
manager.config_manager.get_current_git_config_local.return_value = {
|
|
478
|
+
"name": "Work User",
|
|
479
|
+
"email": "work@company.com",
|
|
480
|
+
}
|
|
481
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
482
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
483
|
+
|
|
484
|
+
status = manager.get_status()
|
|
485
|
+
|
|
486
|
+
manager.config_manager.get_current_git_config_local.assert_called_once()
|
|
487
|
+
manager.config_manager.get_current_git_config.assert_not_called()
|
|
488
|
+
assert status["matched_account"] == "work"
|
|
489
|
+
|
|
490
|
+
def test_matches_account_by_agent_key_fingerprint(self, manager):
|
|
491
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
492
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
493
|
+
"name": None,
|
|
494
|
+
"email": None,
|
|
495
|
+
}
|
|
496
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
497
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = True
|
|
498
|
+
manager.ssh_manager.get_agent_fingerprints.return_value = ["SHA256:abc123"]
|
|
499
|
+
manager.ssh_manager.get_key_fingerprint.return_value = "SHA256:abc123"
|
|
500
|
+
|
|
501
|
+
status = manager.get_status()
|
|
502
|
+
|
|
503
|
+
assert status["agent_running"] is True
|
|
504
|
+
assert status["matched_key_account"] == "work"
|
|
505
|
+
|
|
506
|
+
def test_no_key_match_when_fingerprints_differ(self, manager):
|
|
507
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
508
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
509
|
+
"name": None,
|
|
510
|
+
"email": None,
|
|
511
|
+
}
|
|
512
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
513
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = True
|
|
514
|
+
manager.ssh_manager.get_agent_fingerprints.return_value = ["SHA256:other"]
|
|
515
|
+
manager.ssh_manager.get_key_fingerprint.return_value = "SHA256:abc123"
|
|
516
|
+
|
|
517
|
+
status = manager.get_status()
|
|
518
|
+
|
|
519
|
+
assert status["matched_key_account"] is None
|
|
520
|
+
|
|
521
|
+
def test_no_key_lookup_when_agent_not_running(self, manager):
|
|
522
|
+
manager.config_manager.is_git_repo.return_value = False
|
|
523
|
+
manager.config_manager.get_current_git_config.return_value = {
|
|
524
|
+
"name": None,
|
|
525
|
+
"email": None,
|
|
526
|
+
}
|
|
527
|
+
manager.config_manager.read_accounts.return_value = {"work": WORK_ACCOUNT}
|
|
528
|
+
manager.ssh_manager.is_ssh_agent_running.return_value = False
|
|
529
|
+
|
|
530
|
+
status = manager.get_status()
|
|
531
|
+
|
|
532
|
+
manager.ssh_manager.get_agent_fingerprints.assert_not_called()
|
|
533
|
+
assert status["matched_key_account"] is None
|
|
@@ -69,6 +69,9 @@ class TestParserCommands:
|
|
|
69
69
|
def test_logout_command(self, cli):
|
|
70
70
|
assert cli.parser.parse_args(["logout"]).command == "logout"
|
|
71
71
|
|
|
72
|
+
def test_status_command(self, cli):
|
|
73
|
+
assert cli.parser.parse_args(["status"]).command == "status"
|
|
74
|
+
|
|
72
75
|
def test_init_parses_account(self, cli):
|
|
73
76
|
args = cli.parser.parse_args(["init", "mywork"])
|
|
74
77
|
assert args.account_name == "mywork"
|
|
@@ -170,6 +173,50 @@ class TestHandlers:
|
|
|
170
173
|
cli.account_manager.verify_account.return_value = False
|
|
171
174
|
assert cli._handle_verify(argparse.Namespace()) == 1
|
|
172
175
|
|
|
176
|
+
def test_handle_status_matched(self, cli, capsys):
|
|
177
|
+
cli.account_manager.get_status.return_value = {
|
|
178
|
+
"name": "Work User",
|
|
179
|
+
"email": "work@co.com",
|
|
180
|
+
"matched_account": "work",
|
|
181
|
+
"agent_running": True,
|
|
182
|
+
"matched_key_account": "work",
|
|
183
|
+
}
|
|
184
|
+
result = cli._handle_status(argparse.Namespace())
|
|
185
|
+
out = capsys.readouterr().out
|
|
186
|
+
assert result == 0
|
|
187
|
+
assert "Work User" in out
|
|
188
|
+
assert "work@co.com" in out
|
|
189
|
+
assert 'Matched account: "work"' in out
|
|
190
|
+
assert 'SSH agent key matches account: "work"' in out
|
|
191
|
+
|
|
192
|
+
def test_handle_status_no_match(self, cli, capsys):
|
|
193
|
+
cli.account_manager.get_status.return_value = {
|
|
194
|
+
"name": "Someone",
|
|
195
|
+
"email": "someone@else.com",
|
|
196
|
+
"matched_account": None,
|
|
197
|
+
"agent_running": True,
|
|
198
|
+
"matched_key_account": None,
|
|
199
|
+
}
|
|
200
|
+
result = cli._handle_status(argparse.Namespace())
|
|
201
|
+
out = capsys.readouterr().out
|
|
202
|
+
assert result == 0
|
|
203
|
+
assert "No managed account matches" in out
|
|
204
|
+
assert "does not match any managed account" in out
|
|
205
|
+
|
|
206
|
+
def test_handle_status_no_identity_set(self, cli, capsys):
|
|
207
|
+
cli.account_manager.get_status.return_value = {
|
|
208
|
+
"name": None,
|
|
209
|
+
"email": None,
|
|
210
|
+
"matched_account": None,
|
|
211
|
+
"agent_running": False,
|
|
212
|
+
"matched_key_account": None,
|
|
213
|
+
}
|
|
214
|
+
result = cli._handle_status(argparse.Namespace())
|
|
215
|
+
out = capsys.readouterr().out
|
|
216
|
+
assert result == 0
|
|
217
|
+
assert "(not set)" in out
|
|
218
|
+
assert "SSH agent is not running." in out
|
|
219
|
+
|
|
173
220
|
def test_handle_init_success(self, cli):
|
|
174
221
|
cli.account_manager.init_repo.return_value = True
|
|
175
222
|
assert cli._handle_init(argparse.Namespace(account_name="work")) == 0
|
|
@@ -295,3 +342,15 @@ class TestRun:
|
|
|
295
342
|
with patch.object(sys, "argv", ["gitacc", "logout"]):
|
|
296
343
|
result = cli.run()
|
|
297
344
|
assert result == 0
|
|
345
|
+
|
|
346
|
+
def test_status_command_runs(self, cli):
|
|
347
|
+
cli.account_manager.get_status.return_value = {
|
|
348
|
+
"name": None,
|
|
349
|
+
"email": None,
|
|
350
|
+
"matched_account": None,
|
|
351
|
+
"agent_running": False,
|
|
352
|
+
"matched_key_account": None,
|
|
353
|
+
}
|
|
354
|
+
with patch.object(sys, "argv", ["gitacc", "status"]):
|
|
355
|
+
result = cli.run()
|
|
356
|
+
assert result == 0
|
|
@@ -164,6 +164,27 @@ class TestUpdateAccountField:
|
|
|
164
164
|
def test_returns_false_for_missing_account(self, config_with_accounts):
|
|
165
165
|
assert config_with_accounts.update_account_field("nobody", "name", "X") is False
|
|
166
166
|
|
|
167
|
+
def test_appends_field_missing_from_section(self, config_with_accounts):
|
|
168
|
+
# SAMPLE_GITACC accounts have no "sign" field — this reproduces a pre-1.2.0
|
|
169
|
+
# account whose section was written before that field existed.
|
|
170
|
+
assert "sign" not in config_with_accounts.get_account("work")
|
|
171
|
+
assert config_with_accounts.update_account_field("work", "sign", "true") is True
|
|
172
|
+
assert config_with_accounts.get_account("work")["sign"] == "true"
|
|
173
|
+
|
|
174
|
+
def test_appending_missing_field_preserves_other_fields(self, config_with_accounts):
|
|
175
|
+
config_with_accounts.update_account_field("work", "sign", "true")
|
|
176
|
+
account = config_with_accounts.get_account("work")
|
|
177
|
+
assert account["name"] == "Work User"
|
|
178
|
+
assert account["email"] == "work@company.com"
|
|
179
|
+
assert account["private_key"] == "/home/user/.ssh/id_rsa_work"
|
|
180
|
+
assert account["public_key"] == "/home/user/.ssh/id_rsa_work.pub"
|
|
181
|
+
|
|
182
|
+
def test_appending_missing_field_does_not_affect_other_account(
|
|
183
|
+
self, config_with_accounts
|
|
184
|
+
):
|
|
185
|
+
config_with_accounts.update_account_field("work", "sign", "true")
|
|
186
|
+
assert "sign" not in config_with_accounts.get_account("personal")
|
|
187
|
+
|
|
167
188
|
|
|
168
189
|
class TestListAccountNames:
|
|
169
190
|
def test_empty(self, config):
|
|
@@ -183,3 +183,51 @@ class TestGetPublicKeyContent:
|
|
|
183
183
|
|
|
184
184
|
def test_returns_none_for_missing_file(self, ssh, tmp_path):
|
|
185
185
|
assert ssh.get_public_key_content(str(tmp_path / "nonexistent.pub")) is None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestGetAgentFingerprints:
|
|
189
|
+
def test_returns_fingerprints_for_loaded_keys(self, ssh):
|
|
190
|
+
output = (
|
|
191
|
+
"2048 SHA256:abc123 work@co.com (RSA)\n"
|
|
192
|
+
"256 SHA256:def456 personal@co.com (ED25519)\n"
|
|
193
|
+
)
|
|
194
|
+
with patch(
|
|
195
|
+
"subprocess.run",
|
|
196
|
+
return_value=MagicMock(returncode=0, stdout=output),
|
|
197
|
+
):
|
|
198
|
+
fingerprints = ssh.get_agent_fingerprints()
|
|
199
|
+
assert fingerprints == ["SHA256:abc123", "SHA256:def456"]
|
|
200
|
+
|
|
201
|
+
def test_returns_empty_when_agent_has_no_keys(self, ssh):
|
|
202
|
+
with patch(
|
|
203
|
+
"subprocess.run",
|
|
204
|
+
return_value=MagicMock(returncode=1, stdout=""),
|
|
205
|
+
):
|
|
206
|
+
assert ssh.get_agent_fingerprints() == []
|
|
207
|
+
|
|
208
|
+
def test_returns_empty_when_ssh_add_missing(self, ssh):
|
|
209
|
+
with patch("subprocess.run", side_effect=FileNotFoundError):
|
|
210
|
+
assert ssh.get_agent_fingerprints() == []
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class TestGetKeyFingerprint:
|
|
214
|
+
def test_returns_fingerprint(self, ssh, tmp_path):
|
|
215
|
+
pub_key = tmp_path / "id_rsa_test.pub"
|
|
216
|
+
pub_key.write_text("ssh-rsa AAAA test@test.com")
|
|
217
|
+
with patch(
|
|
218
|
+
"subprocess.run",
|
|
219
|
+
return_value=MagicMock(
|
|
220
|
+
returncode=0, stdout="2048 SHA256:abc123 test@test.com (RSA)\n"
|
|
221
|
+
),
|
|
222
|
+
):
|
|
223
|
+
assert ssh.get_key_fingerprint(str(pub_key)) == "SHA256:abc123"
|
|
224
|
+
|
|
225
|
+
def test_returns_none_on_failure(self, ssh, tmp_path):
|
|
226
|
+
pub_key = tmp_path / "id_rsa_test.pub"
|
|
227
|
+
with patch("subprocess.run", return_value=MagicMock(returncode=1, stdout="")):
|
|
228
|
+
assert ssh.get_key_fingerprint(str(pub_key)) is None
|
|
229
|
+
|
|
230
|
+
def test_returns_none_when_ssh_keygen_missing(self, ssh, tmp_path):
|
|
231
|
+
pub_key = tmp_path / "id_rsa_test.pub"
|
|
232
|
+
with patch("subprocess.run", side_effect=FileNotFoundError):
|
|
233
|
+
assert ssh.get_key_fingerprint(str(pub_key)) is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gitacc_switcher-1.2.0 → gitacc_switcher-1.3.0}/gitacc_switcher.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|