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.
Files changed (165) hide show
  1. {dotman_git-1.1.1 → dotman_git-1.2.0}/CHANGELOG.md +60 -4
  2. {dotman_git-1.1.1 → dotman_git-1.2.0}/PKG-INFO +32 -4
  3. {dotman_git-1.1.1 → dotman_git-1.2.0}/README.md +28 -3
  4. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/__init__.py +1 -1
  5. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/branch_ops.py +19 -4
  6. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/__init__.py +4 -0
  7. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/audit_cmd.py +2 -2
  8. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/common.py +57 -0
  9. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/encrypt_cmd.py +27 -7
  10. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/export_cmd.py +8 -1
  11. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/import_cmd.py +12 -6
  12. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/init_cmd.py +4 -2
  13. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/interface.py +10 -1
  14. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/log_cmd.py +4 -0
  15. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/navigate_cmd.py +33 -37
  16. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/profile_cmd.py +9 -3
  17. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/rollback_cmd.py +15 -4
  18. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/status_cmd.py +2 -2
  19. dotman_git-1.2.0/dot_man/cli/switch_cmd.py +68 -0
  20. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/watch_cmd.py +4 -0
  21. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/constants.py +37 -9
  22. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/core.py +5 -3
  23. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/dotman_config.py +49 -76
  24. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/files.py +133 -0
  25. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/global_config.py +59 -21
  26. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/merge.py +4 -30
  27. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/save_deploy_ops.py +105 -48
  28. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/secrets.py +98 -1
  29. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/section.py +46 -20
  30. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/status_ops.py +3 -3
  31. {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/PKG-INFO +32 -4
  32. {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/SOURCES.txt +10 -0
  33. {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/requires.txt +3 -0
  34. {dotman_git-1.1.1 → dotman_git-1.2.0}/pyproject.toml +4 -1
  35. dotman_git-1.2.0/tests/test_branch_ops.py +577 -0
  36. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_commands.py +1 -1
  37. dotman_git-1.2.0/tests/test_completions_cmd.py +197 -0
  38. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_comprehensive.py +2 -2
  39. dotman_git-1.2.0/tests/test_config_preservation.py +134 -0
  40. dotman_git-1.2.0/tests/test_core_extended.py +485 -0
  41. dotman_git-1.2.0/tests/test_custom_secrets.py +166 -0
  42. dotman_git-1.2.0/tests/test_encryption.py +338 -0
  43. dotman_git-1.2.0/tests/test_files_comprehensive.py +682 -0
  44. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_hooks.py +1 -1
  45. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_init_cmd.py +1 -1
  46. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_integration.py +4 -4
  47. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_navigate_cmd.py +1 -1
  48. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_profile_cmd_v1.py +82 -0
  49. dotman_git-1.2.0/tests/test_save_deploy_ops.py +562 -0
  50. dotman_git-1.2.0/tests/test_section.py +504 -0
  51. dotman_git-1.2.0/tests/test_shell_completions.py +136 -0
  52. dotman_git-1.2.0/tests/test_status_ops.py +356 -0
  53. dotman_git-1.2.0/tests/test_symlink_deploy.py +173 -0
  54. dotman_git-1.1.1/dot_man/cli/switch_cmd.py +0 -387
  55. dotman_git-1.1.1/tests/test_encryption.py +0 -96
  56. dotman_git-1.1.1/tests/test_section.py +0 -66
  57. {dotman_git-1.1.1 → dotman_git-1.2.0}/CONTRIBUTING.md +0 -0
  58. {dotman_git-1.1.1 → dotman_git-1.2.0}/LICENSE +0 -0
  59. {dotman_git-1.1.1 → dotman_git-1.2.0}/MANIFEST.in +0 -0
  60. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/backups.py +0 -0
  61. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/add_cmd.py +0 -0
  62. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/backup_cmd.py +0 -0
  63. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/branch_cmd.py +0 -0
  64. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/clean_cmd.py +0 -0
  65. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/completions_cmd.py +0 -0
  66. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/config_cmd.py +0 -0
  67. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/deploy_cmd.py +0 -0
  68. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/discover_cmd.py +0 -0
  69. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/doctor_cmd.py +0 -0
  70. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/edit_cmd.py +0 -0
  71. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/main.py +0 -0
  72. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/onboarding.py +0 -0
  73. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/remote_cmd.py +0 -0
  74. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/restore_cmd.py +0 -0
  75. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/revert_cmd.py +0 -0
  76. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/show_cmd.py +0 -0
  77. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/tag_cmd.py +0 -0
  78. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/template_cmd.py +0 -0
  79. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/tui_cmd.py +0 -0
  80. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/cli/verify_cmd.py +0 -0
  81. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/completions/_dot-man.zsh +0 -0
  82. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/completions/dot-man.bash +0 -0
  83. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/completions/dot-man.fish +0 -0
  84. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/completions/install.sh +0 -0
  85. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/config.py +0 -0
  86. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/config_detector.py +0 -0
  87. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/encryption.py +0 -0
  88. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/exceptions.py +0 -0
  89. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/hooks.py +0 -0
  90. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/interactive.py +0 -0
  91. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/lock.py +0 -0
  92. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/operations.py +0 -0
  93. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/py.typed +0 -0
  94. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/tui_log.py +0 -0
  95. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/ui.py +0 -0
  96. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/utils.py +0 -0
  97. {dotman_git-1.1.1 → dotman_git-1.2.0}/dot_man/vault.py +0 -0
  98. {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/dependency_links.txt +0 -0
  99. {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/entry_points.txt +0 -0
  100. {dotman_git-1.1.1 → dotman_git-1.2.0}/dotman_git.egg-info/top_level.txt +0 -0
  101. {dotman_git-1.1.1 → dotman_git-1.2.0}/setup.cfg +0 -0
  102. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_add_cmd.py +0 -0
  103. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_audit_cmd.py +0 -0
  104. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_backup_cmd.py +0 -0
  105. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_backups.py +0 -0
  106. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_branch_cmd.py +0 -0
  107. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_clean.py +0 -0
  108. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_extra.py +0 -0
  109. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_help.py +0 -0
  110. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_revert.py +0 -0
  111. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_cli_v1.py +0 -0
  112. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_common_v1.py +0 -0
  113. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_completion.py +0 -0
  114. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config.py +0 -0
  115. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config_cmd.py +0 -0
  116. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config_cmd_v1.py +0 -0
  117. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config_ops_v1.py +0 -0
  118. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_config_section.py +0 -0
  119. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_core.py +0 -0
  120. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_core_v1.py +0 -0
  121. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_deploy_cmd.py +0 -0
  122. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_discover_cmd.py +0 -0
  123. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_doctor_cmd.py +0 -0
  124. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_dotman_config.py +0 -0
  125. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_edit_cmd.py +0 -0
  126. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_encrypt_cmd.py +0 -0
  127. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_exceptions.py +0 -0
  128. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_export_cmd.py +0 -0
  129. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_files_atomic.py +0 -0
  130. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_files_extended.py +0 -0
  131. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_final_push.py +0 -0
  132. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_fixtures.py +0 -0
  133. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_git_manager.py +0 -0
  134. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_git_manager_extended.py +0 -0
  135. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_global_config.py +0 -0
  136. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_hooks_new.py +0 -0
  137. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_import_cmd.py +0 -0
  138. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_interactive.py +0 -0
  139. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_lock.py +0 -0
  140. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_log_checkout_tag.py +0 -0
  141. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_log_cmd.py +0 -0
  142. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_log_cmd_v1.py +0 -0
  143. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_merge.py +0 -0
  144. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_more_integration.py +0 -0
  145. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_navigate_switch_v1.py +0 -0
  146. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_onboarding.py +0 -0
  147. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_operations.py +0 -0
  148. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_ops_v1.py +0 -0
  149. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_performance_logic.py +0 -0
  150. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_profile_cmd.py +0 -0
  151. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_profile_extra.py +0 -0
  152. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_remote_cmd.py +0 -0
  153. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_restore_cmd.py +0 -0
  154. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_secrets.py +0 -0
  155. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_show_cmd.py +0 -0
  156. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_switch_cmd.py +0 -0
  157. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_switch_enhancements.py +0 -0
  158. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_tag_cmd.py +0 -0
  159. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_template_cmd.py +0 -0
  160. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_template_cmd_v1.py +0 -0
  161. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_template_extra.py +0 -0
  162. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_ui.py +0 -0
  163. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_utils.py +0 -0
  164. {dotman_git-1.1.1 → dotman_git-1.2.0}/tests/test_vault.py +0 -0
  165. {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
- ## [Unreleased]
113
+ ## [1.2.0] - 2026-06-09
114
114
 
115
115
  ### Added
116
- - **`dot-man watch`** auto-save tracked dotfiles on change (watchdog or polling backend, debounced commits, `--no-commit` / `--dry-run` flags)
117
- - **`dot-man rollback`**transaction-style rollback to any previous commit, tag, or `HEAD~N`; shows coloured diff, auto-backs up before rolling back
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
- - README completely overhauled: badges, shields, architecture diagram, full command reference tables, TOML/YAML config examples
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.1.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
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
67
70
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
68
71
  [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
69
- [![Coverage](https://img.shields.io/badge/coverage-61%25-yellow)](https://github.com/BeshoyEhab/dot-man)
72
+ [![Coverage](https://img.shields.io/badge/coverage-60%25-green)](https://github.com/BeshoyEhab/dot-man)
70
73
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](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.0` |
483
+ | Version | `1.1.1` |
456
484
  | Python | `3.9+` |
457
485
  | Platforms | Linux, macOS |
458
- | Test Coverage | 61% |
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
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
13
13
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
14
14
  [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
15
- [![Coverage](https://img.shields.io/badge/coverage-61%25-yellow)](https://github.com/BeshoyEhab/dot-man)
15
+ [![Coverage](https://img.shields.io/badge/coverage-60%25-green)](https://github.com/BeshoyEhab/dot-man)
16
16
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](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.0` |
426
+ | Version | `1.1.1` |
402
427
  | Python | `3.9+` |
403
428
  | Platforms | Linux, macOS |
404
- | Test Coverage | 61% |
429
+ | Test Coverage | 59% (869 tests) |
405
430
  | Commands | 30+ |
406
431
  | PyPI | [`dotman-git`](https://pypi.org/project/dotman-git/) |
407
432
 
@@ -1,4 +1,4 @@
1
1
  """dot-man: Dotfile manager with git-powered branching."""
2
2
 
3
- __version__ = "1.1.1"
3
+ __version__ = "1.2.0"
4
4
  __author__ = "Bishoy Ehab"
@@ -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
- # Clear file comparison cache since files have likely changed
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
- hook_cmd = get_hook_for_config(pattern)
303
- if hook_cmd:
304
- hooks_to_run.add(hook_cmd)
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, SecretScanner
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 = SecretScanner()
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 AliasedCommand, error, require_init, success, warn
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
- assert section_name is not None
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
- assert repo_dir_str is not None
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
- assert section_name is not None
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
- assert repo_dir_str is not None
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
- assert section_name is not None
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 AliasedCommand, error, require_init, success
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
- assert source_path is not None
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
- assert source_path is not None
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
- assert source_path is not None
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
- pass
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
- pass
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: