dotman-git 1.1.1__tar.gz → 1.2.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.
- {dotman_git-1.1.1 → dotman_git-1.2.0}/CHANGELOG.md +60 -4
- {dotman_git-1.1.1 → dotman_git-1.2.0}/PKG-INFO +32 -4
- {dotman_git-1.1.1 → dotman_git-1.2.0}/README.md +28 -3
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/__init__.py +1 -1
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/branch_ops.py +19 -4
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/__init__.py +4 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/audit_cmd.py +2 -2
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/common.py +57 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/encrypt_cmd.py +27 -7
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/export_cmd.py +8 -1
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/import_cmd.py +12 -6
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/init_cmd.py +4 -2
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/interface.py +10 -1
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/log_cmd.py +4 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/navigate_cmd.py +33 -37
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/profile_cmd.py +9 -3
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/rollback_cmd.py +15 -4
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/status_cmd.py +2 -2
- dotman_git-1.2.0/dot_man/cli/switch_cmd.py +68 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/watch_cmd.py +4 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/constants.py +37 -9
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/core.py +5 -3
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/dotman_config.py +49 -76
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/files.py +133 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/global_config.py +59 -21
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/merge.py +4 -30
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/save_deploy_ops.py +105 -48
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/secrets.py +98 -1
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/section.py +46 -20
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/status_ops.py +3 -3
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/PKG-INFO +32 -4
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/SOURCES.txt +10 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/requires.txt +3 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/pyproject.toml +4 -1
- dotman_git-1.2.0/tests/test_branch_ops.py +577 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_commands.py +1 -1
- dotman_git-1.2.0/tests/test_completions_cmd.py +197 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_comprehensive.py +2 -2
- dotman_git-1.2.0/tests/test_config_preservation.py +134 -0
- dotman_git-1.2.0/tests/test_core_extended.py +485 -0
- dotman_git-1.2.0/tests/test_custom_secrets.py +166 -0
- dotman_git-1.2.0/tests/test_encryption.py +338 -0
- dotman_git-1.2.0/tests/test_files_comprehensive.py +682 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_hooks.py +1 -1
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_init_cmd.py +1 -1
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_integration.py +4 -4
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_navigate_cmd.py +1 -1
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_profile_cmd_v1.py +82 -0
- dotman_git-1.2.0/tests/test_save_deploy_ops.py +562 -0
- dotman_git-1.2.0/tests/test_section.py +504 -0
- dotman_git-1.2.0/tests/test_shell_completions.py +136 -0
- dotman_git-1.2.0/tests/test_status_ops.py +356 -0
- dotman_git-1.2.0/tests/test_symlink_deploy.py +173 -0
- dotman_git-1.1.1/dot_man/cli/switch_cmd.py +0 -387
- dotman_git-1.1.1/tests/test_encryption.py +0 -96
- dotman_git-1.1.1/tests/test_section.py +0 -66
- {dotman_git-1.1.1 → dotman_git-1.2.0}/CONTRIBUTING.md +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/LICENSE +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/MANIFEST.in +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/backups.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/add_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/backup_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/branch_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/clean_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/completions_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/config_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/deploy_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/discover_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/doctor_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/edit_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/main.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/onboarding.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/remote_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/restore_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/revert_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/show_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/tag_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/template_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/tui_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/verify_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/completions/_dot-man.zsh +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/completions/dot-man.bash +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/completions/dot-man.fish +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/completions/install.sh +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/config.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/config_detector.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/encryption.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/exceptions.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/hooks.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/interactive.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/lock.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/operations.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/py.typed +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/tui_log.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/ui.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/utils.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/vault.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/dependency_links.txt +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/entry_points.txt +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/top_level.txt +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/setup.cfg +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_add_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_audit_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_backup_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_backups.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_branch_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_clean.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_extra.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_help.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_revert.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_v1.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_common_v1.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_completion.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config_cmd_v1.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config_ops_v1.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config_section.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_core.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_core_v1.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_deploy_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_discover_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_doctor_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_dotman_config.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_edit_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_encrypt_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_exceptions.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_export_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_files_atomic.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_files_extended.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_final_push.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_fixtures.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_git_manager.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_git_manager_extended.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_global_config.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_hooks_new.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_import_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_interactive.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_lock.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_log_checkout_tag.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_log_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_log_cmd_v1.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_merge.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_more_integration.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_navigate_switch_v1.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_onboarding.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_operations.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_ops_v1.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_performance_logic.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_profile_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_profile_extra.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_remote_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_restore_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_secrets.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_show_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_switch_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_switch_enhancements.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_tag_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_template_cmd.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_template_cmd_v1.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_template_extra.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_ui.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_utils.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_vault.py +0 -0
- {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_verify_cmd.py +0 -0
|
@@ -110,11 +110,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
110
110
|
- `checkout` → Use `navigate` instead
|
|
111
111
|
- `tag switch` → Use `navigate` instead
|
|
112
112
|
|
|
113
|
-
## [
|
|
113
|
+
## [1.2.0] - 2026-06-09
|
|
114
114
|
|
|
115
115
|
### Added
|
|
116
|
-
-
|
|
117
|
-
-
|
|
116
|
+
- **Generalized placeholder system in hooks** — `{qs_config}` replaced with `{config_name}`, `{config_root}`, `{section_name}`, `{paths}`, `{branch}`
|
|
117
|
+
- **Shell completions** — `encrypt <section>`, `export --branch`, `rollback <target>`
|
|
118
|
+
- **Custom Secret Patterns** — Support defining custom regex patterns for secret detection in config
|
|
119
|
+
- **Secret Scanner Customization** — `use_default_patterns = false` to disable built-in patterns
|
|
120
|
+
- **Symlink Deploy Mode** — per-section `deploy_method = "symlink"` for edit-in-place workflows
|
|
121
|
+
- **Quickshell Hook Aliases** — `quickshell_reload`, `quickshell_restart`, `quickshell_validate` with `{qs_config}` → `{config_name}` placeholder
|
|
122
|
+
- **`complete_sections()`** — shell completion callback for section names
|
|
118
123
|
|
|
119
124
|
### Changed
|
|
120
|
-
-
|
|
125
|
+
- **`switch_cmd` consolidated → thin wrapper** — `switch` is now a 17-line wrapper calling `_navigate_impl()` (~80% code reduction)
|
|
126
|
+
- **`BranchParamType` deduplicated** — moved from `switch_cmd.py`/`navigate_cmd.py` to `common.py`
|
|
127
|
+
- **Schema keys deduplicated** — `VALID_SECTION_KEYS` defined once in `dotman_config.py`
|
|
128
|
+
- **`HOOK_ALIASES` unified** — `constants.py` is canonical; `merge.py` imports from it
|
|
129
|
+
- **Silent `except: pass` cleanup** — `logging.debug()`/`logging.warning()` added to 11+ bare exceptions across `common.py`, `navigate_cmd.py`, `rollback_cmd.py`, `core.py`, `files.py`, `init_cmd.py`
|
|
130
|
+
- **Production `assert` statements removed** — all replaced with `if/error()` pattern in `navigate_cmd.py`, `encrypt_cmd.py`, `import_cmd.py`, `rollback_cmd.py`
|
|
131
|
+
- **Coverage improved from 60% → 66%** — 1146 tests (up from 1021), 1 skipped
|
|
132
|
+
|
|
133
|
+
### Fixed
|
|
134
|
+
- **Weak `>=` assertions** in `test_section.py`, `test_save_deploy_ops.py`, `test_status_ops.py`, `test_core_extended.py`, and 4 new test files
|
|
135
|
+
- **Missing f-string prefix** in `dotman_config.py:96`
|
|
136
|
+
- **Stale `dot-man.ini` reference** in `status_ops.py:158`
|
|
137
|
+
|
|
138
|
+
### New Tests
|
|
139
|
+
| File | Tests | Coverage Impact |
|
|
140
|
+
|------|-------|-----------------|
|
|
141
|
+
| `test_files_comprehensive.py` | 55 | `files.py` 73% → 94% |
|
|
142
|
+
| `test_branch_ops.py` | 39 | `branch_ops.py` 40% → 97% |
|
|
143
|
+
| `test_encryption.py` | 32 | `encryption.py` 33% → 100% |
|
|
144
|
+
| `test_completions_cmd.py` | 10 | `completions_cmd.py` 12% → 94% |
|
|
145
|
+
| `test_shell_completions.py` | 7 | Shell completions verified |
|
|
146
|
+
| `test_section.py` | 39 (+36) | `section.py` 70% → 95% |
|
|
147
|
+
| `test_save_deploy_ops.py` | 26 | `save_deploy_ops.py` 52% → 75% |
|
|
148
|
+
| `test_status_ops.py` | 17 | `status_ops.py` 59% → 89% |
|
|
149
|
+
| `test_core_extended.py` | 46 | `core.py` 58% → 66% |
|
|
150
|
+
|
|
151
|
+
## [1.1.1] - 2026-05-20
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
- **PyPI Publication** - Official release published to PyPI as `dotman-git`.
|
|
155
|
+
- **Command Aliases** - Short 3-letter command aliases added for all major operations:
|
|
156
|
+
- `nav` (navigate), `doc` (doctor), `dep` (deploy), `enc` (encrypt)
|
|
157
|
+
- `exp` (export), `imp` (import), `dis` (discover), `aud` (audit)
|
|
158
|
+
- `cln` (clean), `ver` (verify), `rev` (revert), `rol` (rollback)
|
|
159
|
+
- `wat` (watch), `cpl` (completions), `rst` (restore), `edt` (edit)
|
|
160
|
+
- `sta` (status), `ini` (init), `syn` (sync), `log` (log), `dif` (diff)
|
|
161
|
+
- `hks` (hooks)
|
|
162
|
+
- **`dot-man watch`** — Auto-save tracked dotfiles on change (supporting both watchdog and polling backends, debounced commits, and `--no-commit`/`--dry-run` flags).
|
|
163
|
+
- **`dot-man rollback`** — Transaction-style rollback to any previous commit, tag, or `HEAD~N`, with colored diffs and automatic pre-rollback backups.
|
|
164
|
+
- **YAML Comment Preservation** - Added round-trip YAML configuration comment and layout preservation using `ruamel.yaml`.
|
|
165
|
+
|
|
166
|
+
### Changed
|
|
167
|
+
- **README Overhaul** - Redesigned README with project badges, architecture diagrams, config examples (TOML/YAML), and command reference tables.
|
|
168
|
+
- **License & Copyright** - Updated license with additional copyright holder (ZVAXEROWS) and copyright years to 2025, 2026.
|
|
169
|
+
|
|
170
|
+
### Fixed
|
|
171
|
+
- **Profile Switch & Serialization** - Resolved a traceback in `profile switch` by properly parsing branch parameters for `ctx.invoke`.
|
|
172
|
+
- **Global Config Save** - Defaulted `GlobalConfig.save()` to write to disk (`force=True`) to prevent silent save failures across CLI commands.
|
|
173
|
+
- **TOML Serialization of None** - Prevented `tomlkit` ConvertError by omitting the `inherits` key if its value is `None` during profile creation.
|
|
174
|
+
- **YAML Configuration Overwrites** - Resolved a bug where editing configuration fields would silently convert YAML files back to TOML on save.
|
|
175
|
+
- **TOML/YAML Comment Preservation & Key Deletions** - Fixed a bug in document updates where deleted configuration keys were not removed from the saved file, and added value equality checks to prevent losing comments on sequence fields.
|
|
176
|
+
- **Integration Tests** - Added robust test cases covering the complete `profile switch` flow, validating branch switching, warnings, inheritance, and error handling.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotman-git
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Dotfile manager with git-powered branching
|
|
5
5
|
Author-email: Bishoy Ehab <beshoyehabhhmm@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -38,6 +38,7 @@ Requires-Dist: tomlkit>=0.12.0
|
|
|
38
38
|
Requires-Dist: questionary>=2.0.0
|
|
39
39
|
Provides-Extra: yaml
|
|
40
40
|
Requires-Dist: pyyaml>=6.0; extra == "yaml"
|
|
41
|
+
Requires-Dist: ruamel.yaml>=0.17.0; extra == "yaml"
|
|
41
42
|
Provides-Extra: tui
|
|
42
43
|
Requires-Dist: textual>=0.73.0; extra == "tui"
|
|
43
44
|
Provides-Extra: watch
|
|
@@ -45,11 +46,13 @@ Requires-Dist: watchdog>=4.0; extra == "watch"
|
|
|
45
46
|
Provides-Extra: dev
|
|
46
47
|
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
|
47
48
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
49
|
+
Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
|
|
48
50
|
Requires-Dist: black>=24.8.0; extra == "dev"
|
|
49
51
|
Requires-Dist: mypy>=1.14.1; extra == "dev"
|
|
50
52
|
Requires-Dist: textual>=0.73.0; extra == "dev"
|
|
51
53
|
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
52
54
|
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
55
|
+
Requires-Dist: ruamel.yaml>=0.17.0; extra == "dev"
|
|
53
56
|
Dynamic: license-file
|
|
54
57
|
|
|
55
58
|
<div align="center">
|
|
@@ -66,7 +69,7 @@ Dynamic: license-file
|
|
|
66
69
|
[](https://github.com/psf/black)
|
|
67
70
|
[](https://github.com/astral-sh/ruff)
|
|
68
71
|
[](https://mypy-lang.org/)
|
|
69
|
-
[](https://github.com/BeshoyEhab/dot-man)
|
|
70
73
|
[](https://github.com/BeshoyEhab/dot-man/pulls)
|
|
71
74
|
|
|
72
75
|
---
|
|
@@ -414,6 +417,31 @@ system file: api_key = "sk-abc123..." ← restored on deploy
|
|
|
414
417
|
|
|
415
418
|
Run `dot-man audit` to scan at any time. Use `dot-man audit --strict` in CI/CD pipelines.
|
|
416
419
|
|
|
420
|
+
### Custom Secret Patterns
|
|
421
|
+
|
|
422
|
+
You can define custom regular expressions to match sensitive patterns, either globally in `~/.config/dot-man/global.toml` or for a specific repository in `dot-man.toml`.
|
|
423
|
+
|
|
424
|
+
#### Global custom secrets (`global.toml`)
|
|
425
|
+
```toml
|
|
426
|
+
[security]
|
|
427
|
+
use_default_patterns = true # Set to false to disable built-in patterns
|
|
428
|
+
|
|
429
|
+
[[security.patterns]]
|
|
430
|
+
name = "My Custom API Token"
|
|
431
|
+
pattern = 'my_api_key_[a-zA-Z0-9]{32}'
|
|
432
|
+
severity = "HIGH"
|
|
433
|
+
description = "Internal service API key"
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
#### Repository custom secrets (`dot-man.toml`)
|
|
437
|
+
```toml
|
|
438
|
+
[secrets]
|
|
439
|
+
use_default_patterns = true # Set to false to disable built-in patterns
|
|
440
|
+
patterns = [
|
|
441
|
+
{ name = "Special Repo Key", pattern = 'special_[0-9]+', severity = "CRITICAL" }
|
|
442
|
+
]
|
|
443
|
+
```
|
|
444
|
+
|
|
417
445
|
---
|
|
418
446
|
|
|
419
447
|
## Multi-Machine Profiles
|
|
@@ -452,10 +480,10 @@ System variables (`{{HOSTNAME}}`, `{{USER}}`, `{{SHELL}}`, etc.) are auto-popula
|
|
|
452
480
|
|
|
453
481
|
| Metric | Value |
|
|
454
482
|
|--------|-------|
|
|
455
|
-
| Version | `1.1.
|
|
483
|
+
| Version | `1.1.1` |
|
|
456
484
|
| Python | `3.9+` |
|
|
457
485
|
| Platforms | Linux, macOS |
|
|
458
|
-
| Test Coverage |
|
|
486
|
+
| Test Coverage | 59% (869 tests) |
|
|
459
487
|
| Commands | 30+ |
|
|
460
488
|
| PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
|
|
461
489
|
|
|
@@ -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
|
---
|
|
@@ -360,6 +360,31 @@ system file: api_key = "sk-abc123..." ← restored on deploy
|
|
|
360
360
|
|
|
361
361
|
Run `dot-man audit` to scan at any time. Use `dot-man audit --strict` in CI/CD pipelines.
|
|
362
362
|
|
|
363
|
+
### Custom Secret Patterns
|
|
364
|
+
|
|
365
|
+
You can define custom regular expressions to match sensitive patterns, either globally in `~/.config/dot-man/global.toml` or for a specific repository in `dot-man.toml`.
|
|
366
|
+
|
|
367
|
+
#### Global custom secrets (`global.toml`)
|
|
368
|
+
```toml
|
|
369
|
+
[security]
|
|
370
|
+
use_default_patterns = true # Set to false to disable built-in patterns
|
|
371
|
+
|
|
372
|
+
[[security.patterns]]
|
|
373
|
+
name = "My Custom API Token"
|
|
374
|
+
pattern = 'my_api_key_[a-zA-Z0-9]{32}'
|
|
375
|
+
severity = "HIGH"
|
|
376
|
+
description = "Internal service API key"
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
#### Repository custom secrets (`dot-man.toml`)
|
|
380
|
+
```toml
|
|
381
|
+
[secrets]
|
|
382
|
+
use_default_patterns = true # Set to false to disable built-in patterns
|
|
383
|
+
patterns = [
|
|
384
|
+
{ name = "Special Repo Key", pattern = 'special_[0-9]+', severity = "CRITICAL" }
|
|
385
|
+
]
|
|
386
|
+
```
|
|
387
|
+
|
|
363
388
|
---
|
|
364
389
|
|
|
365
390
|
## Multi-Machine Profiles
|
|
@@ -398,10 +423,10 @@ System variables (`{{HOSTNAME}}`, `{{USER}}`, `{{SHELL}}`, etc.) are auto-popula
|
|
|
398
423
|
|
|
399
424
|
| Metric | Value |
|
|
400
425
|
|--------|-------|
|
|
401
|
-
| Version | `1.1.
|
|
426
|
+
| Version | `1.1.1` |
|
|
402
427
|
| Python | `3.9+` |
|
|
403
428
|
| Platforms | Linux, macOS |
|
|
404
|
-
| Test Coverage |
|
|
429
|
+
| Test Coverage | 59% (869 tests) |
|
|
405
430
|
| Commands | 30+ |
|
|
406
431
|
| PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
|
|
407
432
|
|
|
@@ -131,6 +131,13 @@ class BranchMixin:
|
|
|
131
131
|
result["saved_count"] = save_result["saved"]
|
|
132
132
|
result["secrets_redacted"] = len(save_result["secrets"])
|
|
133
133
|
result["errors"].extend(save_result["errors"])
|
|
134
|
+
# Warn about symlinked paths
|
|
135
|
+
symlinks: list[Path] = save_result.get("symlinks", [])
|
|
136
|
+
for sym_path in symlinks:
|
|
137
|
+
result["errors"].append(
|
|
138
|
+
f"⚠ {sym_path} is a symlink → {sym_path.resolve()}. "
|
|
139
|
+
f"Edits affect the symlink target, not the config folder."
|
|
140
|
+
)
|
|
134
141
|
|
|
135
142
|
commit_msg = f"Auto-save from '{current_branch}' before switch to '{target_branch}'"
|
|
136
143
|
self.git.commit(commit_msg)
|
|
@@ -195,7 +202,7 @@ class BranchMixin:
|
|
|
195
202
|
self.global_config.current_branch = target_branch
|
|
196
203
|
self.global_config.save()
|
|
197
204
|
|
|
198
|
-
|
|
205
|
+
# Clear file comparison cache since files have likely changed
|
|
199
206
|
clear_comparison_cache()
|
|
200
207
|
|
|
201
208
|
return result
|
|
@@ -299,9 +306,17 @@ class BranchMixin:
|
|
|
299
306
|
|
|
300
307
|
for pattern, hook_name in FILE_TO_HOOK_MAP.items():
|
|
301
308
|
if pattern in file_str or file_str.endswith(pattern):
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
309
|
+
# Try to resolve the hook alias first
|
|
310
|
+
from .constants import HOOK_ALIASES
|
|
311
|
+
|
|
312
|
+
if hook_name in HOOK_ALIASES:
|
|
313
|
+
resolved_cmd: str = HOOK_ALIASES[hook_name]
|
|
314
|
+
hooks_to_run.add(resolved_cmd)
|
|
315
|
+
else:
|
|
316
|
+
# Fallback to get_hook_for_config
|
|
317
|
+
fallback_cmd = get_hook_for_config(pattern)
|
|
318
|
+
if fallback_cmd:
|
|
319
|
+
hooks_to_run.add(fallback_cmd)
|
|
305
320
|
break
|
|
306
321
|
|
|
307
322
|
section_name = self._find_section_for_file(file_path)
|
|
@@ -10,6 +10,7 @@ from .backup_cmd import backup
|
|
|
10
10
|
from .branch_cmd import branch
|
|
11
11
|
from .clean_cmd import clean
|
|
12
12
|
from .common import (
|
|
13
|
+
BRANCH,
|
|
13
14
|
DotManGroup,
|
|
14
15
|
_clear_completion_cache,
|
|
15
16
|
_set_git_runner,
|
|
@@ -17,6 +18,7 @@ from .common import (
|
|
|
17
18
|
complete_commits,
|
|
18
19
|
complete_config_keys,
|
|
19
20
|
complete_profiles,
|
|
21
|
+
complete_sections,
|
|
20
22
|
complete_switch_args,
|
|
21
23
|
complete_tags,
|
|
22
24
|
complete_template_keys,
|
|
@@ -72,7 +74,9 @@ __all__ = [
|
|
|
72
74
|
"complete_template_keys",
|
|
73
75
|
"complete_config_keys",
|
|
74
76
|
"complete_profiles",
|
|
77
|
+
"complete_sections",
|
|
75
78
|
"complete_switch_args",
|
|
79
|
+
"BRANCH",
|
|
76
80
|
"parse_branch_arg",
|
|
77
81
|
"get_secret_handler",
|
|
78
82
|
"_set_git_runner",
|
|
@@ -6,7 +6,7 @@ from .. import ui
|
|
|
6
6
|
from ..constants import REPO_DIR
|
|
7
7
|
from ..core import GitManager
|
|
8
8
|
from ..exceptions import DotManError
|
|
9
|
-
from ..secrets import PermanentRedactGuard, SecretGuard, SecretMatch,
|
|
9
|
+
from ..secrets import PermanentRedactGuard, SecretGuard, SecretMatch, get_custom_scanner
|
|
10
10
|
from .common import AliasedCommand, error, handle_exception, require_init, success
|
|
11
11
|
from .interface import cli as main
|
|
12
12
|
|
|
@@ -26,7 +26,7 @@ def audit(strict: bool, fix: bool):
|
|
|
26
26
|
Use --strict in CI/CD pipelines to fail builds if secrets are found.
|
|
27
27
|
"""
|
|
28
28
|
try:
|
|
29
|
-
scanner =
|
|
29
|
+
scanner = get_custom_scanner()
|
|
30
30
|
guard = SecretGuard()
|
|
31
31
|
permanent_guard = PermanentRedactGuard()
|
|
32
32
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Common utilities for dot-man CLI commands."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
6
7
|
import subprocess
|
|
@@ -127,6 +128,20 @@ def require_init(func):
|
|
|
127
128
|
ui.console.print()
|
|
128
129
|
ui.console.print("[bold]The Dotfile Manager for Professionals[/bold]")
|
|
129
130
|
ui.console.print()
|
|
131
|
+
ui.console.print("[bold cyan]Coming from another tool?[/bold cyan]")
|
|
132
|
+
ui.console.print(
|
|
133
|
+
" [cyan]dot-man import chezmoi[/cyan] - Import from chezmoi"
|
|
134
|
+
)
|
|
135
|
+
ui.console.print(
|
|
136
|
+
" [cyan]dot-man import yadm[/cyan] - Import from yadm"
|
|
137
|
+
)
|
|
138
|
+
ui.console.print(
|
|
139
|
+
" [cyan]dot-man import stow[/cyan] - Import from GNU Stow"
|
|
140
|
+
)
|
|
141
|
+
ui.console.print(
|
|
142
|
+
" [cyan]dot-man import all[/cyan] - Auto-detect and import"
|
|
143
|
+
)
|
|
144
|
+
ui.console.print()
|
|
130
145
|
ui.console.print("[bold cyan]Get started:[/bold cyan]")
|
|
131
146
|
ui.console.print(
|
|
132
147
|
" [cyan]dot-man init[/cyan] - Initialize your dotfiles repository"
|
|
@@ -214,6 +229,7 @@ def _get_completion_cache() -> dict:
|
|
|
214
229
|
_memory_cache_time = time.time()
|
|
215
230
|
return _memory_cache
|
|
216
231
|
except Exception:
|
|
232
|
+
logging.debug("Failed to load completion cache from file")
|
|
217
233
|
pass
|
|
218
234
|
|
|
219
235
|
_memory_cache = {}
|
|
@@ -233,6 +249,7 @@ def _save_completion_cache(data: dict) -> None:
|
|
|
233
249
|
_COMPLETION_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
234
250
|
_COMPLETION_CACHE_FILE.write_text(json.dumps(data))
|
|
235
251
|
except Exception:
|
|
252
|
+
logging.debug("Failed to save completion cache to file")
|
|
236
253
|
pass
|
|
237
254
|
|
|
238
255
|
|
|
@@ -246,6 +263,9 @@ def _clear_completion_cache() -> None:
|
|
|
246
263
|
if _COMPLETION_CACHE_FILE.exists():
|
|
247
264
|
_COMPLETION_CACHE_FILE.unlink()
|
|
248
265
|
except Exception:
|
|
266
|
+
logging.debug(
|
|
267
|
+
"Failed to unlink completion cache file in _clear_completion_cache"
|
|
268
|
+
)
|
|
249
269
|
pass
|
|
250
270
|
|
|
251
271
|
|
|
@@ -264,6 +284,7 @@ def _clear_all_caches() -> None:
|
|
|
264
284
|
if _COMPLETION_CACHE_FILE.exists():
|
|
265
285
|
_COMPLETION_CACHE_FILE.unlink()
|
|
266
286
|
except Exception:
|
|
287
|
+
logging.debug("Failed to unlink completion cache file in _clear_all_caches")
|
|
267
288
|
pass
|
|
268
289
|
|
|
269
290
|
|
|
@@ -304,6 +325,20 @@ def parse_branch_arg(arg: str) -> dict:
|
|
|
304
325
|
return {"type": "branch", "base": arg, "target": arg}
|
|
305
326
|
|
|
306
327
|
|
|
328
|
+
class BranchParamType(click.ParamType):
|
|
329
|
+
"""Parameter type that accepts branch, branch@tag, or commit SHA."""
|
|
330
|
+
|
|
331
|
+
name = "branch"
|
|
332
|
+
|
|
333
|
+
def convert(self, value, param, ctx):
|
|
334
|
+
if not value:
|
|
335
|
+
return None
|
|
336
|
+
return parse_branch_arg(value)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
BRANCH = BranchParamType()
|
|
340
|
+
|
|
341
|
+
|
|
307
342
|
def complete_switch_args(ctx, param, incomplete):
|
|
308
343
|
"""Shell completion callback for switch (branches, tags, commits).
|
|
309
344
|
|
|
@@ -419,6 +454,7 @@ def _complete_navigate_items(
|
|
|
419
454
|
_save_completion_cache(cache)
|
|
420
455
|
return items
|
|
421
456
|
except Exception:
|
|
457
|
+
logging.debug("Failed to complete navigate items")
|
|
422
458
|
return []
|
|
423
459
|
|
|
424
460
|
|
|
@@ -437,6 +473,7 @@ def complete_branches(ctx, param, incomplete):
|
|
|
437
473
|
branches = cache["branches"]
|
|
438
474
|
return [b for b in branches if b.startswith(incomplete)]
|
|
439
475
|
except Exception:
|
|
476
|
+
logging.debug("Failed to complete branches")
|
|
440
477
|
return []
|
|
441
478
|
|
|
442
479
|
|
|
@@ -453,6 +490,7 @@ def complete_tags(ctx, param, incomplete):
|
|
|
453
490
|
tags = cache["tags"]
|
|
454
491
|
return [t for t in tags if t.startswith(incomplete)]
|
|
455
492
|
except Exception:
|
|
493
|
+
logging.debug("Failed to complete tags")
|
|
456
494
|
return []
|
|
457
495
|
|
|
458
496
|
|
|
@@ -473,6 +511,7 @@ def complete_commits(ctx, param, incomplete):
|
|
|
473
511
|
commits = cache["commits_all"]
|
|
474
512
|
return [c for c in commits if c.startswith(incomplete)]
|
|
475
513
|
except Exception:
|
|
514
|
+
logging.debug("Failed to complete commits")
|
|
476
515
|
return []
|
|
477
516
|
|
|
478
517
|
|
|
@@ -491,6 +530,7 @@ def complete_template_keys(ctx, param, incomplete):
|
|
|
491
530
|
_template_cache = list(templates.keys())
|
|
492
531
|
return [k for k in _template_cache if k.startswith(incomplete)]
|
|
493
532
|
except Exception:
|
|
533
|
+
logging.debug("Failed to complete template keys")
|
|
494
534
|
return []
|
|
495
535
|
|
|
496
536
|
|
|
@@ -512,6 +552,7 @@ def complete_config_keys(ctx, param, incomplete):
|
|
|
512
552
|
_config_keys_cache = keys
|
|
513
553
|
return [k for k in keys if k.startswith(incomplete)]
|
|
514
554
|
except Exception:
|
|
555
|
+
logging.debug("Failed to complete config keys")
|
|
515
556
|
return []
|
|
516
557
|
|
|
517
558
|
|
|
@@ -530,6 +571,22 @@ def complete_profiles(ctx, param, incomplete):
|
|
|
530
571
|
_profiles_cache = list(profiles.keys())
|
|
531
572
|
return [k for k in _profiles_cache if k.startswith(incomplete)]
|
|
532
573
|
except Exception:
|
|
574
|
+
logging.debug("Failed to complete profiles")
|
|
575
|
+
return []
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def complete_sections(ctx, param, incomplete):
|
|
579
|
+
"""Shell completion callback for section names."""
|
|
580
|
+
try:
|
|
581
|
+
from ..dotman_config import DotManConfig
|
|
582
|
+
|
|
583
|
+
config = DotManConfig()
|
|
584
|
+
config.load()
|
|
585
|
+
return [
|
|
586
|
+
name for name in config.get_section_names() if name.startswith(incomplete)
|
|
587
|
+
]
|
|
588
|
+
except Exception:
|
|
589
|
+
logging.debug("Failed to complete sections")
|
|
533
590
|
return []
|
|
534
591
|
|
|
535
592
|
|
|
@@ -11,13 +11,20 @@ from ..encryption import (
|
|
|
11
11
|
EncryptionManager,
|
|
12
12
|
detect_available_encryption,
|
|
13
13
|
)
|
|
14
|
-
from .common import
|
|
14
|
+
from .common import (
|
|
15
|
+
AliasedCommand,
|
|
16
|
+
complete_sections,
|
|
17
|
+
error,
|
|
18
|
+
require_init,
|
|
19
|
+
success,
|
|
20
|
+
warn,
|
|
21
|
+
)
|
|
15
22
|
from .interface import cli as main
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
@main.command("encrypt", cls=AliasedCommand, aliases=["enc"])
|
|
19
26
|
@click.argument("action", type=click.Choice(["encrypt", "decrypt", "status"]))
|
|
20
|
-
@click.argument("section", required=False)
|
|
27
|
+
@click.argument("section", required=False, shell_complete=complete_sections)
|
|
21
28
|
@click.option(
|
|
22
29
|
"--method",
|
|
23
30
|
"-m",
|
|
@@ -111,10 +118,13 @@ def _encrypt_section(
|
|
|
111
118
|
ops = get_operations()
|
|
112
119
|
config = DotManConfig()
|
|
113
120
|
|
|
114
|
-
|
|
121
|
+
if section_name is None:
|
|
122
|
+
error("Section name is required", exit_code=1)
|
|
123
|
+
return
|
|
115
124
|
section = ops.get_section(section_name)
|
|
116
125
|
if not section:
|
|
117
126
|
error(f"Section not found: {section_name}", exit_code=1)
|
|
127
|
+
return
|
|
118
128
|
|
|
119
129
|
if recipient is None:
|
|
120
130
|
if section.encryption_recipient:
|
|
@@ -124,6 +134,7 @@ def _encrypt_section(
|
|
|
124
134
|
"No recipient specified. Use --recipient or set encryption_recipient in config",
|
|
125
135
|
exit_code=1,
|
|
126
136
|
)
|
|
137
|
+
return
|
|
127
138
|
|
|
128
139
|
ui.console.print(f"[dim]Encrypting section: {section_name}[/dim]")
|
|
129
140
|
|
|
@@ -132,7 +143,9 @@ def _encrypt_section(
|
|
|
132
143
|
for path_str in section.paths:
|
|
133
144
|
local_path = Path(path_str).expanduser()
|
|
134
145
|
repo_dir_str = ops.git.repo.working_dir
|
|
135
|
-
|
|
146
|
+
if repo_dir_str is None:
|
|
147
|
+
error("Failed to determine repo working directory", exit_code=1)
|
|
148
|
+
return
|
|
136
149
|
repo_path = section.get_repo_path(local_path, Path(repo_dir_str))
|
|
137
150
|
|
|
138
151
|
if not local_path.exists():
|
|
@@ -171,10 +184,13 @@ def _decrypt_section(
|
|
|
171
184
|
ops = get_operations()
|
|
172
185
|
config = DotManConfig()
|
|
173
186
|
|
|
174
|
-
|
|
187
|
+
if section_name is None:
|
|
188
|
+
error("Section name is required", exit_code=1)
|
|
189
|
+
return
|
|
175
190
|
section = ops.get_section(section_name)
|
|
176
191
|
if not section:
|
|
177
192
|
error(f"Section not found: {section_name}", exit_code=1)
|
|
193
|
+
return
|
|
178
194
|
|
|
179
195
|
ui.console.print(f"[dim]Decrypting section: {section_name}[/dim]")
|
|
180
196
|
|
|
@@ -183,7 +199,9 @@ def _decrypt_section(
|
|
|
183
199
|
for path_str in section.paths:
|
|
184
200
|
local_path = Path(path_str).expanduser()
|
|
185
201
|
repo_dir_str = ops.git.repo.working_dir
|
|
186
|
-
|
|
202
|
+
if repo_dir_str is None:
|
|
203
|
+
error("Failed to determine repo working directory", exit_code=1)
|
|
204
|
+
return
|
|
187
205
|
repo_path = section.get_repo_path(local_path, Path(repo_dir_str))
|
|
188
206
|
|
|
189
207
|
encrypted_path = repo_path.with_suffix(repo_path.suffix + ".gpg")
|
|
@@ -198,7 +216,9 @@ def _decrypt_section(
|
|
|
198
216
|
except EncryptionError as e:
|
|
199
217
|
warn(f"Failed to decrypt {local_path.name}: {e}")
|
|
200
218
|
|
|
201
|
-
|
|
219
|
+
if section_name is None:
|
|
220
|
+
error("Section name is required", exit_code=1)
|
|
221
|
+
return
|
|
202
222
|
config.update_section(section_name, encrypted=False)
|
|
203
223
|
config.save()
|
|
204
224
|
|
|
@@ -10,7 +10,13 @@ import click
|
|
|
10
10
|
|
|
11
11
|
from .. import ui
|
|
12
12
|
from ..constants import REPO_DIR
|
|
13
|
-
from .common import
|
|
13
|
+
from .common import (
|
|
14
|
+
AliasedCommand,
|
|
15
|
+
complete_branches,
|
|
16
|
+
error,
|
|
17
|
+
require_init,
|
|
18
|
+
success,
|
|
19
|
+
)
|
|
14
20
|
from .interface import cli as main
|
|
15
21
|
|
|
16
22
|
|
|
@@ -21,6 +27,7 @@ from .interface import cli as main
|
|
|
21
27
|
"--branch",
|
|
22
28
|
"-b",
|
|
23
29
|
default=None,
|
|
30
|
+
shell_complete=complete_branches,
|
|
24
31
|
help="Export specific branch (default: current branch)",
|
|
25
32
|
)
|
|
26
33
|
@click.option(
|
|
@@ -157,8 +157,10 @@ def _import_chezmoi(source_path: str | None, dry_run: bool, git: GitManager):
|
|
|
157
157
|
"chezmoi source not found. Install chezmoi first or use --path.",
|
|
158
158
|
exit_code=1,
|
|
159
159
|
)
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
return
|
|
161
|
+
if source_path is None:
|
|
162
|
+
error("Source path is required", exit_code=1)
|
|
163
|
+
return
|
|
162
164
|
source = Path(source_path).expanduser().resolve()
|
|
163
165
|
|
|
164
166
|
if not source.exists():
|
|
@@ -210,8 +212,10 @@ def _import_yadm(source_path: str | None, dry_run: bool, git: GitManager):
|
|
|
210
212
|
error(
|
|
211
213
|
"yadm source not found. Install yadm first or use --path.", exit_code=1
|
|
212
214
|
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
return
|
|
216
|
+
if source_path is None:
|
|
217
|
+
error("Source path is required", exit_code=1)
|
|
218
|
+
return
|
|
215
219
|
source = Path(source_path).expanduser().resolve()
|
|
216
220
|
|
|
217
221
|
if not source.exists():
|
|
@@ -255,8 +259,10 @@ def _import_stow(source_path: str | None, dry_run: bool, git: GitManager):
|
|
|
255
259
|
source_path = _detect_stow()
|
|
256
260
|
if source_path is None:
|
|
257
261
|
error("Stow packages not found. Use --path to specify.", exit_code=1)
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
return
|
|
263
|
+
if source_path is None:
|
|
264
|
+
error("Source path is required", exit_code=1)
|
|
265
|
+
return
|
|
260
266
|
source = Path(source_path).expanduser().resolve()
|
|
261
267
|
|
|
262
268
|
if not source.exists():
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Init command for dot-man CLI."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import shutil
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
@@ -102,7 +103,7 @@ def init(
|
|
|
102
103
|
if result.returncode == 0:
|
|
103
104
|
current_branch = result.stdout.strip()
|
|
104
105
|
except Exception:
|
|
105
|
-
|
|
106
|
+
logging.debug("Failed to get current branch from source repo")
|
|
106
107
|
|
|
107
108
|
# Copy entire repo including .git
|
|
108
109
|
shutil.copytree(source_path, REPO_DIR, dirs_exist_ok=True)
|
|
@@ -115,7 +116,7 @@ def init(
|
|
|
115
116
|
if current_branch in git.list_branches():
|
|
116
117
|
git.checkout(current_branch)
|
|
117
118
|
except Exception:
|
|
118
|
-
|
|
119
|
+
logging.debug("Failed to checkout branch after import")
|
|
119
120
|
|
|
120
121
|
ui.success(f"Imported dotfiles from {source_path}")
|
|
121
122
|
else:
|
|
@@ -128,6 +129,7 @@ def init(
|
|
|
128
129
|
git.repo.config_reader().get_value("user", "name")
|
|
129
130
|
git.repo.config_reader().get_value("user", "email")
|
|
130
131
|
except Exception:
|
|
132
|
+
logging.debug("Git user config not found, will set defaults")
|
|
131
133
|
ui.console.print()
|
|
132
134
|
ui.warn("Git user configuration not found. Setting defaults...")
|
|
133
135
|
with git.repo.config_writer() as config:
|
|
@@ -19,7 +19,16 @@ from .common import DotManGroup
|
|
|
19
19
|
@click.option("--debug", is_flag=True, help="Enable debug logging to file")
|
|
20
20
|
@click.pass_context
|
|
21
21
|
def cli(ctx, verbose: bool, debug: bool):
|
|
22
|
-
"""dot-man: The Dotfile Manager for Professionals.
|
|
22
|
+
"""dot-man: The Dotfile Manager for Professionals.
|
|
23
|
+
|
|
24
|
+
Git-powered branching for your dotfiles. Switch between configurations
|
|
25
|
+
(work, personal, minimal, server) on the fly — each branch is a
|
|
26
|
+
complete dotfile environment.
|
|
27
|
+
|
|
28
|
+
Supports deploy_method = "symlink" for edit-in-place workflows,
|
|
29
|
+
auto-detected post-deploy hooks, and import from chezmoi, yadm,
|
|
30
|
+
and GNU Stow.
|
|
31
|
+
"""
|
|
23
32
|
# Ensure config dir exists for logs
|
|
24
33
|
if not DOT_MAN_DIR.exists():
|
|
25
34
|
try:
|