dotman-git 1.2.2__tar.gz → 1.2.3__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.
- {dotman_git-1.2.2 → dotman_git-1.2.3}/CHANGELOG.md +6 -2
- {dotman_git-1.2.2/dotman_git.egg-info → dotman_git-1.2.3}/PKG-INFO +7 -7
- {dotman_git-1.2.2 → dotman_git-1.2.3}/README.md +6 -6
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/__init__.py +1 -1
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/branch_ops.py +11 -2
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/add_cmd.py +12 -1
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/navigate_cmd.py +37 -3
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/watch_cmd.py +14 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/interactive.py +42 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/save_deploy_ops.py +23 -3
- {dotman_git-1.2.2 → dotman_git-1.2.3/dotman_git.egg-info}/PKG-INFO +7 -7
- {dotman_git-1.2.2 → dotman_git-1.2.3}/pyproject.toml +1 -1
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_add_cmd.py +65 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_branch_ops.py +1 -1
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_save_deploy_ops.py +152 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/CONTRIBUTING.md +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/LICENSE +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/MANIFEST.in +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/backups.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/__init__.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/audit_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/backup_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/branch_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/clean_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/common.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/completions_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/config_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/deploy_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/discover_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/doctor_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/edit_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/encrypt_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/export_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/import_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/init_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/interface.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/log_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/main.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/onboarding.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/profile_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/remote_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/restore_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/revert_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/rollback_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/show_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/status_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/switch_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/tag_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/template_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/tui_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/cli/verify_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/completions/_dot-man.zsh +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/completions/dot-man.bash +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/completions/dot-man.fish +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/completions/install.sh +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/config.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/config_detector.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/constants.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/core.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/dotman_config.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/encryption.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/exceptions.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/files.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/global_config.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/hooks.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/lock.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/merge.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/operations.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/py.typed +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/secrets.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/section.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/status_ops.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/tui_log.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/ui.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/utils.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dot_man/vault.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dotman_git.egg-info/SOURCES.txt +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dotman_git.egg-info/dependency_links.txt +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dotman_git.egg-info/entry_points.txt +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dotman_git.egg-info/requires.txt +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/dotman_git.egg-info/top_level.txt +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/setup.cfg +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_audit_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_backup_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_backups.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_branch_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_clean.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_cli_commands.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_cli_extra.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_cli_help.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_cli_revert.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_cli_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_common_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_completion.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_completions_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_comprehensive.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_config.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_config_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_config_cmd_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_config_ops_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_config_preservation.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_config_section.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_core.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_core_extended.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_core_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_custom_secrets.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_deploy_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_discover_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_doctor_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_dotman_config.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_edit_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_encrypt_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_encryption.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_exceptions.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_export_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_files_atomic.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_files_comprehensive.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_files_extended.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_final_push.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_fixtures.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_git_manager.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_git_manager_extended.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_global_config.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_hooks.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_hooks_new.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_import_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_init_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_integration.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_interactive.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_lock.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_log_checkout_tag.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_log_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_log_cmd_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_merge.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_more_integration.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_navigate_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_navigate_switch_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_onboarding.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_operations.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_ops_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_performance_logic.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_profile_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_profile_cmd_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_profile_extra.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_remote_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_restore_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_rollback_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_secrets.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_section.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_shell_completions.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_show_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_status_ops.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_switch_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_switch_enhancements.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_symlink_deploy.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_tag_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_template_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_template_cmd_v1.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_template_extra.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_ui.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_utils.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_vault.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_verify_cmd.py +0 -0
- {dotman_git-1.2.2 → dotman_git-1.2.3}/tests/test_watch_cmd.py +0 -0
|
@@ -7,11 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [1.2.
|
|
10
|
+
## [1.2.3] - 2026-06-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Interactive symlink handling** — `dot-man add`, `navigate`, and `watch` now detect symlinked config paths and prompt the user with three choices: follow the link (save target content), ignore (skip this path), or ignore all. Deploying also warns when overwriting a symlinked local path.
|
|
11
15
|
|
|
12
16
|
### Fixed
|
|
13
17
|
|
|
14
|
-
- **Python 3.10 CI compatibility** — `dot_man/cli/__init__.py` no longer shadows module names with same-named function imports
|
|
18
|
+
- **Python 3.10 CI compatibility** — `dot_man/cli/__init__.py` no longer shadows module names with same-named function imports.
|
|
15
19
|
|
|
16
20
|
## [1.2.1] - 2026-06-10
|
|
17
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotman-git
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Dotfile manager with git-powered branching
|
|
5
5
|
Author-email: Bishoy Ehab <beshoyehabhhmm@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -69,7 +69,7 @@ Dynamic: license-file
|
|
|
69
69
|
[](https://github.com/psf/black)
|
|
70
70
|
[](https://github.com/astral-sh/ruff)
|
|
71
71
|
[](https://mypy-lang.org/)
|
|
72
|
-
[](https://github.com/BeshoyEhab/dot-man)
|
|
73
73
|
[](https://github.com/BeshoyEhab/dot-man/pulls)
|
|
74
74
|
|
|
75
75
|
---
|
|
@@ -480,10 +480,10 @@ System variables (`{{HOSTNAME}}`, `{{USER}}`, `{{SHELL}}`, etc.) are auto-popula
|
|
|
480
480
|
|
|
481
481
|
| Metric | Value |
|
|
482
482
|
|--------|-------|
|
|
483
|
-
| Version | `1.
|
|
484
|
-
| Python | `3.
|
|
483
|
+
| Version | `1.2.3` |
|
|
484
|
+
| Python | `3.10 · 3.11 · 3.12 · 3.13` |
|
|
485
485
|
| Platforms | Linux, macOS |
|
|
486
|
-
| Test Coverage |
|
|
486
|
+
| Test Coverage | 83% (1519 tests) |
|
|
487
487
|
| Commands | 30+ |
|
|
488
488
|
| PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
|
|
489
489
|
|
|
@@ -512,9 +512,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [DEVELOPMENT.md](DEVELOPMENT.md) for
|
|
|
512
512
|
|
|
513
513
|
## Roadmap
|
|
514
514
|
|
|
515
|
-
- [
|
|
515
|
+
- [x] 80%+ test coverage
|
|
516
516
|
- [ ] Full documentation site (mkdocs)
|
|
517
|
-
- [
|
|
517
|
+
- [x] Symlink mode
|
|
518
518
|
- [x] `dot-man watch` — auto-sync on file change
|
|
519
519
|
- [x] Deploy rollback (transaction-style)
|
|
520
520
|
- [ ] Plugin system
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://github.com/psf/black)
|
|
13
13
|
[](https://github.com/astral-sh/ruff)
|
|
14
14
|
[](https://mypy-lang.org/)
|
|
15
|
-
[](https://github.com/BeshoyEhab/dot-man)
|
|
16
16
|
[](https://github.com/BeshoyEhab/dot-man/pulls)
|
|
17
17
|
|
|
18
18
|
---
|
|
@@ -423,10 +423,10 @@ System variables (`{{HOSTNAME}}`, `{{USER}}`, `{{SHELL}}`, etc.) are auto-popula
|
|
|
423
423
|
|
|
424
424
|
| Metric | Value |
|
|
425
425
|
|--------|-------|
|
|
426
|
-
| Version | `1.
|
|
427
|
-
| Python | `3.
|
|
426
|
+
| Version | `1.2.3` |
|
|
427
|
+
| Python | `3.10 · 3.11 · 3.12 · 3.13` |
|
|
428
428
|
| Platforms | Linux, macOS |
|
|
429
|
-
| Test Coverage |
|
|
429
|
+
| Test Coverage | 83% (1519 tests) |
|
|
430
430
|
| Commands | 30+ |
|
|
431
431
|
| PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
|
|
432
432
|
|
|
@@ -455,9 +455,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [DEVELOPMENT.md](DEVELOPMENT.md) for
|
|
|
455
455
|
|
|
456
456
|
## Roadmap
|
|
457
457
|
|
|
458
|
-
- [
|
|
458
|
+
- [x] 80%+ test coverage
|
|
459
459
|
- [ ] Full documentation site (mkdocs)
|
|
460
|
-
- [
|
|
460
|
+
- [x] Symlink mode
|
|
461
461
|
- [x] `dot-man watch` — auto-sync on file change
|
|
462
462
|
- [x] Deploy rollback (transaction-style)
|
|
463
463
|
- [ ] Plugin system
|
|
@@ -54,7 +54,9 @@ class BranchMixin:
|
|
|
54
54
|
|
|
55
55
|
@abstractmethod
|
|
56
56
|
def save_all(
|
|
57
|
-
self,
|
|
57
|
+
self,
|
|
58
|
+
secret_handler: Optional[Callable[[SecretMatch], str]] = None,
|
|
59
|
+
symlink_ignore: Optional[set[Path]] = None,
|
|
58
60
|
) -> dict[str, Any]: ...
|
|
59
61
|
|
|
60
62
|
@property
|
|
@@ -93,10 +95,17 @@ class BranchMixin:
|
|
|
93
95
|
target_branch: str,
|
|
94
96
|
dry_run: bool = False,
|
|
95
97
|
secret_handler: Optional[Callable[[SecretMatch], str]] = None,
|
|
98
|
+
symlink_ignore: Optional[set[Path]] = None,
|
|
96
99
|
) -> dict:
|
|
97
100
|
"""
|
|
98
101
|
Switch to a different branch.
|
|
99
102
|
|
|
103
|
+
Args:
|
|
104
|
+
target_branch: Branch to switch to.
|
|
105
|
+
dry_run: If True, don't make changes.
|
|
106
|
+
secret_handler: Optional callback for secret handling.
|
|
107
|
+
symlink_ignore: Set of symlinked paths to skip when saving.
|
|
108
|
+
|
|
100
109
|
Returns dict with:
|
|
101
110
|
- saved_count: files saved from current branch
|
|
102
111
|
- deployed_count: files deployed from target branch
|
|
@@ -127,7 +136,7 @@ class BranchMixin:
|
|
|
127
136
|
with lock_context:
|
|
128
137
|
# Phase 1: Save current branch
|
|
129
138
|
if not dry_run:
|
|
130
|
-
save_result = self.save_all(secret_handler)
|
|
139
|
+
save_result = self.save_all(secret_handler, symlink_ignore)
|
|
131
140
|
result["saved_count"] = save_result["saved"]
|
|
132
141
|
result["secrets_redacted"] = len(save_result["secrets"])
|
|
133
142
|
result["errors"].extend(save_result["errors"])
|
|
@@ -70,7 +70,18 @@ def add(
|
|
|
70
70
|
dot-man add ~/.config/hypr --inherits linux-gui --post-deploy "hyprctl reload"
|
|
71
71
|
"""
|
|
72
72
|
try:
|
|
73
|
-
|
|
73
|
+
raw_path = Path(path).expanduser()
|
|
74
|
+
|
|
75
|
+
# Warn if path is a symlink
|
|
76
|
+
if raw_path.is_symlink():
|
|
77
|
+
from ..interactive import prompt_symlink_action
|
|
78
|
+
|
|
79
|
+
action = prompt_symlink_action(raw_path)
|
|
80
|
+
if action in ("ignore", "all_ignore"):
|
|
81
|
+
warn(f"Skipped symlink: {raw_path}")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
local_path = raw_path.resolve()
|
|
74
85
|
|
|
75
86
|
# Auto-generate section name if not provided
|
|
76
87
|
if not section:
|
|
@@ -128,6 +128,37 @@ def _warn_symlinks(save_result: dict) -> None:
|
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def _prompt_for_symlinks(ops) -> set[Path]:
|
|
132
|
+
"""Scan sections for symlinks and prompt user how to handle each.
|
|
133
|
+
|
|
134
|
+
Returns a set of symlinked paths the user chose to ignore.
|
|
135
|
+
"""
|
|
136
|
+
from ..interactive import prompt_symlink_action
|
|
137
|
+
|
|
138
|
+
symlink_ignore: set[Path] = set()
|
|
139
|
+
ignore_all = False
|
|
140
|
+
|
|
141
|
+
for section_name in ops.get_sections():
|
|
142
|
+
section = ops.get_section(section_name)
|
|
143
|
+
for path in section.paths:
|
|
144
|
+
if not path.is_symlink():
|
|
145
|
+
continue
|
|
146
|
+
if ignore_all:
|
|
147
|
+
symlink_ignore.add(path)
|
|
148
|
+
continue
|
|
149
|
+
if path in symlink_ignore:
|
|
150
|
+
continue
|
|
151
|
+
action = prompt_symlink_action(path)
|
|
152
|
+
if action == "ignore":
|
|
153
|
+
symlink_ignore.add(path)
|
|
154
|
+
elif action == "all_ignore":
|
|
155
|
+
symlink_ignore.add(path)
|
|
156
|
+
ignore_all = True
|
|
157
|
+
# "follow" — no-op, proceed with default behavior
|
|
158
|
+
|
|
159
|
+
return symlink_ignore
|
|
160
|
+
|
|
161
|
+
|
|
131
162
|
def run_branch_hooks(ops, hook_type: str) -> None:
|
|
132
163
|
"""Run on_activate or on_deactivate hooks from sections.
|
|
133
164
|
|
|
@@ -527,7 +558,8 @@ def _handle_commit_navigate(
|
|
|
527
558
|
if save_mode == "save":
|
|
528
559
|
ui.console.print(f"[bold]Saving current branch '{current_branch}'...[/bold]")
|
|
529
560
|
secret_handler = get_secret_handler()
|
|
530
|
-
|
|
561
|
+
symlink_ignore = _prompt_for_symlinks(ops)
|
|
562
|
+
save_result = ops.save_all(secret_handler, symlink_ignore=symlink_ignore)
|
|
531
563
|
_warn_symlinks(save_result)
|
|
532
564
|
saved_count = save_result["saved"]
|
|
533
565
|
sections = get_changed_sections(ops)
|
|
@@ -592,7 +624,8 @@ def _handle_tag_navigate(
|
|
|
592
624
|
if save_mode == "save":
|
|
593
625
|
ui.console.print(f"[bold]Saving current branch '{current_branch}'...[/bold]")
|
|
594
626
|
secret_handler = get_secret_handler()
|
|
595
|
-
|
|
627
|
+
symlink_ignore = _prompt_for_symlinks(ops)
|
|
628
|
+
save_result = ops.save_all(secret_handler, symlink_ignore=symlink_ignore)
|
|
596
629
|
_warn_symlinks(save_result)
|
|
597
630
|
saved_count = save_result["saved"]
|
|
598
631
|
sections = get_changed_sections(ops)
|
|
@@ -670,7 +703,8 @@ def _handle_branch_navigate(
|
|
|
670
703
|
|
|
671
704
|
if save_mode == "save":
|
|
672
705
|
secret_handler = get_secret_handler()
|
|
673
|
-
|
|
706
|
+
symlink_ignore = _prompt_for_symlinks(ops)
|
|
707
|
+
save_result = ops.save_all(secret_handler, symlink_ignore=symlink_ignore)
|
|
674
708
|
_warn_symlinks(save_result)
|
|
675
709
|
saved_count = save_result["saved"]
|
|
676
710
|
secrets = save_result["secrets"]
|
|
@@ -212,6 +212,20 @@ def watch(interval: float, commit: bool, message: str, dry_run: bool):
|
|
|
212
212
|
warn("No paths to watch.")
|
|
213
213
|
return
|
|
214
214
|
|
|
215
|
+
# Scan for symlinks
|
|
216
|
+
symlink_paths = [p for p in tracked_paths if p.is_symlink()]
|
|
217
|
+
if symlink_paths:
|
|
218
|
+
ui.console.print()
|
|
219
|
+
for sym_path in symlink_paths:
|
|
220
|
+
target = sym_path.resolve()
|
|
221
|
+
ui.console.print(
|
|
222
|
+
f" [yellow]⚠ {sym_path} is a symlink → {target}[/yellow]"
|
|
223
|
+
)
|
|
224
|
+
ui.console.print(
|
|
225
|
+
" [dim] Edits affect the symlink target, not the dot-man repo.[/dim]"
|
|
226
|
+
)
|
|
227
|
+
ui.console.print()
|
|
228
|
+
|
|
215
229
|
backend = "watchdog" if _WATCHDOG_AVAILABLE else "polling"
|
|
216
230
|
ui.console.print(
|
|
217
231
|
f"[bold]👁 dot-man watch[/bold] [dim]({backend} backend)[/dim]"
|
|
@@ -5,9 +5,11 @@ __all__ = [
|
|
|
5
5
|
"run_global_wizard",
|
|
6
6
|
"run_templates_wizard",
|
|
7
7
|
"edit_template",
|
|
8
|
+
"prompt_symlink_action",
|
|
8
9
|
]
|
|
9
10
|
|
|
10
11
|
from pathlib import Path
|
|
12
|
+
from typing import cast
|
|
11
13
|
|
|
12
14
|
import questionary
|
|
13
15
|
from questionary import Style, ValidationError, Validator
|
|
@@ -458,3 +460,43 @@ def edit_template(config: DotManConfig, name: str):
|
|
|
458
460
|
).ask()
|
|
459
461
|
if val:
|
|
460
462
|
template["update_strategy"] = val
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def prompt_symlink_action(path: Path) -> str:
|
|
466
|
+
"""Ask the user how to handle a symlinked config path.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
path: The symlinked path.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
"follow" to save the symlink target content,
|
|
473
|
+
"ignore" to skip this path,
|
|
474
|
+
"all_ignore" to skip this and all remaining symlinks.
|
|
475
|
+
"""
|
|
476
|
+
target = path.resolve()
|
|
477
|
+
warn(
|
|
478
|
+
f"{path} is a symlink to {target}. "
|
|
479
|
+
"Changes to the repo may not affect the original file."
|
|
480
|
+
)
|
|
481
|
+
choice = questionary.select(
|
|
482
|
+
"How would you like to handle this symlink?",
|
|
483
|
+
choices=[
|
|
484
|
+
questionary.Choice(
|
|
485
|
+
title="Follow the link — save the target file's content into the repo",
|
|
486
|
+
value="follow",
|
|
487
|
+
),
|
|
488
|
+
questionary.Choice(
|
|
489
|
+
title="Ignore — don't track this path",
|
|
490
|
+
value="ignore",
|
|
491
|
+
),
|
|
492
|
+
questionary.Choice(
|
|
493
|
+
title="Ignore all — skip all symlinked paths in this operation",
|
|
494
|
+
value="all_ignore",
|
|
495
|
+
),
|
|
496
|
+
],
|
|
497
|
+
default="follow",
|
|
498
|
+
style=custom_style,
|
|
499
|
+
).ask()
|
|
500
|
+
if choice is None:
|
|
501
|
+
return "follow"
|
|
502
|
+
return cast(str, choice)
|
|
@@ -216,10 +216,16 @@ class SaveDeployMixin:
|
|
|
216
216
|
self,
|
|
217
217
|
section: Section,
|
|
218
218
|
secret_handler: Optional[Callable[[SecretMatch], str]] = None,
|
|
219
|
+
symlink_ignore: Optional[set[Path]] = None,
|
|
219
220
|
) -> tuple[int, list[SecretMatch], list[str], list[Path]]:
|
|
220
221
|
"""
|
|
221
222
|
Save a section from local to repo.
|
|
222
223
|
|
|
224
|
+
Args:
|
|
225
|
+
section: Section to save.
|
|
226
|
+
secret_handler: Optional callback for secret handling.
|
|
227
|
+
symlink_ignore: Set of symlinked paths to skip (ignore).
|
|
228
|
+
|
|
223
229
|
Returns:
|
|
224
230
|
(files_saved, secrets_detected, errors, symlink_paths)
|
|
225
231
|
"""
|
|
@@ -258,13 +264,17 @@ class SaveDeployMixin:
|
|
|
258
264
|
# Merge exclude patterns with ignored directories
|
|
259
265
|
final_excludes = self._build_final_excludes(section)
|
|
260
266
|
|
|
267
|
+
symlink_ignore = symlink_ignore or set()
|
|
268
|
+
|
|
261
269
|
for local_path in section.paths:
|
|
262
270
|
if not local_path.exists():
|
|
263
271
|
continue
|
|
264
272
|
|
|
265
|
-
#
|
|
273
|
+
# If path is a symlink, check if user chose to ignore it
|
|
266
274
|
if local_path.is_symlink():
|
|
267
275
|
symlink_paths.append(local_path)
|
|
276
|
+
if local_path in symlink_ignore:
|
|
277
|
+
continue
|
|
268
278
|
|
|
269
279
|
repo_path = section.get_repo_path(local_path, REPO_DIR)
|
|
270
280
|
|
|
@@ -316,6 +326,12 @@ class SaveDeployMixin:
|
|
|
316
326
|
for local_path in section.paths:
|
|
317
327
|
repo_path = section.get_repo_path(local_path, REPO_DIR)
|
|
318
328
|
|
|
329
|
+
if local_path.is_symlink():
|
|
330
|
+
errors.append(
|
|
331
|
+
f"⚠ {local_path} is a symlink → {local_path.resolve()}. "
|
|
332
|
+
f"Deploy will overwrite it with a regular file."
|
|
333
|
+
)
|
|
334
|
+
|
|
319
335
|
if section.update_strategy == "ignore" and local_path.exists():
|
|
320
336
|
continue
|
|
321
337
|
|
|
@@ -431,7 +447,9 @@ class SaveDeployMixin:
|
|
|
431
447
|
}
|
|
432
448
|
|
|
433
449
|
def save_all(
|
|
434
|
-
self,
|
|
450
|
+
self,
|
|
451
|
+
secret_handler: Optional[Callable[[SecretMatch], str]] = None,
|
|
452
|
+
symlink_ignore: Optional[set[Path]] = None,
|
|
435
453
|
) -> dict:
|
|
436
454
|
"""
|
|
437
455
|
Save all sections from local to repo.
|
|
@@ -447,7 +465,9 @@ class SaveDeployMixin:
|
|
|
447
465
|
|
|
448
466
|
with self.vault.batch(), ThreadPoolExecutor() as executor:
|
|
449
467
|
future_to_section = {
|
|
450
|
-
executor.submit(
|
|
468
|
+
executor.submit(
|
|
469
|
+
self.save_section, section, secret_handler, symlink_ignore
|
|
470
|
+
): section
|
|
451
471
|
for section in sections
|
|
452
472
|
}
|
|
453
473
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotman-git
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Dotfile manager with git-powered branching
|
|
5
5
|
Author-email: Bishoy Ehab <beshoyehabhhmm@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -69,7 +69,7 @@ Dynamic: license-file
|
|
|
69
69
|
[](https://github.com/psf/black)
|
|
70
70
|
[](https://github.com/astral-sh/ruff)
|
|
71
71
|
[](https://mypy-lang.org/)
|
|
72
|
-
[](https://github.com/BeshoyEhab/dot-man)
|
|
73
73
|
[](https://github.com/BeshoyEhab/dot-man/pulls)
|
|
74
74
|
|
|
75
75
|
---
|
|
@@ -480,10 +480,10 @@ System variables (`{{HOSTNAME}}`, `{{USER}}`, `{{SHELL}}`, etc.) are auto-popula
|
|
|
480
480
|
|
|
481
481
|
| Metric | Value |
|
|
482
482
|
|--------|-------|
|
|
483
|
-
| Version | `1.
|
|
484
|
-
| Python | `3.
|
|
483
|
+
| Version | `1.2.3` |
|
|
484
|
+
| Python | `3.10 · 3.11 · 3.12 · 3.13` |
|
|
485
485
|
| Platforms | Linux, macOS |
|
|
486
|
-
| Test Coverage |
|
|
486
|
+
| Test Coverage | 83% (1519 tests) |
|
|
487
487
|
| Commands | 30+ |
|
|
488
488
|
| PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
|
|
489
489
|
|
|
@@ -512,9 +512,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) and [DEVELOPMENT.md](DEVELOPMENT.md) for
|
|
|
512
512
|
|
|
513
513
|
## Roadmap
|
|
514
514
|
|
|
515
|
-
- [
|
|
515
|
+
- [x] 80%+ test coverage
|
|
516
516
|
- [ ] Full documentation site (mkdocs)
|
|
517
|
-
- [
|
|
517
|
+
- [x] Symlink mode
|
|
518
518
|
- [x] `dot-man watch` — auto-sync on file change
|
|
519
519
|
- [x] Deploy rollback (transaction-style)
|
|
520
520
|
- [ ] Plugin system
|
|
@@ -94,6 +94,71 @@ class TestAddWithInit:
|
|
|
94
94
|
|
|
95
95
|
assert result.exit_code in [0, 1, 7]
|
|
96
96
|
|
|
97
|
+
def test_add_symlink_follows(self, clean_env):
|
|
98
|
+
"""Add follows a symlink when prompt returns 'follow'."""
|
|
99
|
+
runner, dot_man_dir, repo_dir, global_toml, home = clean_env
|
|
100
|
+
|
|
101
|
+
# Create minimal global config
|
|
102
|
+
if not global_toml.parent.exists():
|
|
103
|
+
global_toml.parent.mkdir(parents=True)
|
|
104
|
+
global_toml.write_text("[defaults]\n")
|
|
105
|
+
|
|
106
|
+
from git import Repo
|
|
107
|
+
|
|
108
|
+
repo = Repo.init(repo_dir)
|
|
109
|
+
config_writer = repo.config_writer()
|
|
110
|
+
config_writer.set_value("user", "name", "Test")
|
|
111
|
+
config_writer.set_value("user", "email", "test@test.com")
|
|
112
|
+
config_writer.release()
|
|
113
|
+
|
|
114
|
+
(repo_dir / "test.txt").write_text("test")
|
|
115
|
+
repo.index.add(["test.txt"])
|
|
116
|
+
repo.index.commit("Initial")
|
|
117
|
+
|
|
118
|
+
target = home / "real_config.txt"
|
|
119
|
+
target.write_text("real content")
|
|
120
|
+
|
|
121
|
+
link = home / ".linked_config"
|
|
122
|
+
link.symlink_to(target)
|
|
123
|
+
|
|
124
|
+
with patch("dot_man.interactive.prompt_symlink_action", return_value="follow"):
|
|
125
|
+
result = runner.invoke(cli, ["add", str(link)])
|
|
126
|
+
|
|
127
|
+
assert result.exit_code in [0, 1, 7]
|
|
128
|
+
|
|
129
|
+
def test_add_symlink_ignore(self, clean_env):
|
|
130
|
+
"""Add skips a symlink when prompt returns 'ignore'."""
|
|
131
|
+
runner, dot_man_dir, repo_dir, global_toml, home = clean_env
|
|
132
|
+
|
|
133
|
+
# Create minimal global config
|
|
134
|
+
if not global_toml.parent.exists():
|
|
135
|
+
global_toml.parent.mkdir(parents=True)
|
|
136
|
+
global_toml.write_text("[defaults]\n")
|
|
137
|
+
|
|
138
|
+
from git import Repo
|
|
139
|
+
|
|
140
|
+
repo = Repo.init(repo_dir)
|
|
141
|
+
config_writer = repo.config_writer()
|
|
142
|
+
config_writer.set_value("user", "name", "Test")
|
|
143
|
+
config_writer.set_value("user", "email", "test@test.com")
|
|
144
|
+
config_writer.release()
|
|
145
|
+
|
|
146
|
+
(repo_dir / "test.txt").write_text("test")
|
|
147
|
+
repo.index.add(["test.txt"])
|
|
148
|
+
repo.index.commit("Initial")
|
|
149
|
+
|
|
150
|
+
target = home / "real_config.txt"
|
|
151
|
+
target.write_text("real content")
|
|
152
|
+
|
|
153
|
+
link = home / ".linked_config"
|
|
154
|
+
link.symlink_to(target)
|
|
155
|
+
|
|
156
|
+
with patch("dot_man.interactive.prompt_symlink_action", return_value="ignore"):
|
|
157
|
+
result = runner.invoke(cli, ["add", str(link)])
|
|
158
|
+
|
|
159
|
+
assert result.exit_code == 0
|
|
160
|
+
assert "Skipped symlink" in result.output
|
|
161
|
+
|
|
97
162
|
def test_add_directory(self, clean_env):
|
|
98
163
|
"""Add a directory to tracking."""
|
|
99
164
|
runner, dot_man_dir, repo_dir, global_toml, home = clean_env
|