dotman-git 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. dot_man/__init__.py +4 -0
  2. dot_man/backups.py +211 -0
  3. dot_man/branch_ops.py +347 -0
  4. dot_man/cli/__init__.py +113 -0
  5. dot_man/cli/add_cmd.py +167 -0
  6. dot_man/cli/audit_cmd.py +141 -0
  7. dot_man/cli/backup_cmd.py +105 -0
  8. dot_man/cli/branch_cmd.py +103 -0
  9. dot_man/cli/clean_cmd.py +97 -0
  10. dot_man/cli/common.py +548 -0
  11. dot_man/cli/completions_cmd.py +127 -0
  12. dot_man/cli/config_cmd.py +979 -0
  13. dot_man/cli/deploy_cmd.py +169 -0
  14. dot_man/cli/discover_cmd.py +105 -0
  15. dot_man/cli/doctor_cmd.py +229 -0
  16. dot_man/cli/edit_cmd.py +177 -0
  17. dot_man/cli/encrypt_cmd.py +205 -0
  18. dot_man/cli/export_cmd.py +146 -0
  19. dot_man/cli/import_cmd.py +315 -0
  20. dot_man/cli/init_cmd.py +532 -0
  21. dot_man/cli/interface.py +56 -0
  22. dot_man/cli/log_cmd.py +339 -0
  23. dot_man/cli/main.py +36 -0
  24. dot_man/cli/navigate_cmd.py +903 -0
  25. dot_man/cli/onboarding.py +546 -0
  26. dot_man/cli/profile_cmd.py +313 -0
  27. dot_man/cli/remote_cmd.py +454 -0
  28. dot_man/cli/restore_cmd.py +82 -0
  29. dot_man/cli/revert_cmd.py +86 -0
  30. dot_man/cli/show_cmd.py +29 -0
  31. dot_man/cli/status_cmd.py +185 -0
  32. dot_man/cli/switch_cmd.py +387 -0
  33. dot_man/cli/tag_cmd.py +164 -0
  34. dot_man/cli/template_cmd.py +244 -0
  35. dot_man/cli/tui_cmd.py +44 -0
  36. dot_man/cli/verify_cmd.py +156 -0
  37. dot_man/completions/_dot-man.zsh +28 -0
  38. dot_man/completions/dot-man.bash +15 -0
  39. dot_man/completions/dot-man.fish +58 -0
  40. dot_man/completions/install.sh +26 -0
  41. dot_man/config.py +23 -0
  42. dot_man/config_detector.py +426 -0
  43. dot_man/constants.py +109 -0
  44. dot_man/core.py +614 -0
  45. dot_man/dotman_config.py +516 -0
  46. dot_man/encryption.py +173 -0
  47. dot_man/exceptions.py +255 -0
  48. dot_man/files.py +443 -0
  49. dot_man/global_config.py +305 -0
  50. dot_man/hooks.py +232 -0
  51. dot_man/interactive.py +460 -0
  52. dot_man/lock.py +64 -0
  53. dot_man/merge.py +440 -0
  54. dot_man/operations.py +212 -0
  55. dot_man/py.typed +1 -0
  56. dot_man/save_deploy_ops.py +466 -0
  57. dot_man/secrets.py +473 -0
  58. dot_man/section.py +207 -0
  59. dot_man/status_ops.py +229 -0
  60. dot_man/tui_log.py +91 -0
  61. dot_man/ui.py +127 -0
  62. dot_man/utils.py +132 -0
  63. dot_man/vault.py +317 -0
  64. dotman_git-1.0.0.dist-info/METADATA +678 -0
  65. dotman_git-1.0.0.dist-info/RECORD +69 -0
  66. dotman_git-1.0.0.dist-info/WHEEL +5 -0
  67. dotman_git-1.0.0.dist-info/entry_points.txt +3 -0
  68. dotman_git-1.0.0.dist-info/licenses/LICENSE +21 -0
  69. dotman_git-1.0.0.dist-info/top_level.txt +1 -0
dot_man/cli/log_cmd.py ADDED
@@ -0,0 +1,339 @@
1
+ """Log command for dot-man CLI."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from .. import ui
8
+ from .common import complete_branches, complete_tags, error, require_init, success
9
+ from .interface import cli as main
10
+
11
+
12
+ @main.command()
13
+ @click.argument("file", required=False, type=click.Path(path_type=Path))
14
+ @click.option("-n", "--count", type=int, help="Number of commits to show")
15
+ @click.option(
16
+ "--diff", "-d", "show_diff", is_flag=True, help="Show diff for each commit"
17
+ )
18
+ @click.option("--stat", is_flag=True, help="Show file change statistics")
19
+ @click.option("--interactive", "-i", is_flag=True, help="Interactive log browser")
20
+ @require_init
21
+ def log(
22
+ file: Path | None, count: int | None, show_diff: bool, stat: bool, interactive: bool
23
+ ):
24
+ """Show commit history.
25
+
26
+ Examples:
27
+ dot-man log
28
+ dot-man log .bashrc
29
+ dot-man log -n 20
30
+ dot-man log --diff
31
+ dot-man log --interactive
32
+ """
33
+ try:
34
+ import subprocess
35
+
36
+ from ..constants import REPO_DIR
37
+
38
+ if interactive:
39
+ from ..tui_log import LogViewerApp
40
+
41
+ app = LogViewerApp()
42
+ app.run()
43
+ return
44
+
45
+ git_args = ["git", "log", "--color=always"]
46
+ if count:
47
+ git_args.append(f"-n{count}")
48
+ if show_diff:
49
+ git_args.append("-p")
50
+ if stat:
51
+ git_args.append("--stat")
52
+
53
+ if file:
54
+ from ..operations import get_operations
55
+
56
+ ops = get_operations()
57
+ target_file = file.expanduser().resolve()
58
+ found = False
59
+ for section_name in ops.get_sections():
60
+ section = ops.get_section(section_name)
61
+ for tracked_path in section.paths:
62
+ if tracked_path.resolve() == target_file:
63
+ repo_path = section.get_repo_path(tracked_path, REPO_DIR)
64
+ git_args.extend(["--", str(repo_path.relative_to(REPO_DIR))])
65
+ found = True
66
+ break
67
+ if found:
68
+ break
69
+
70
+ if not found:
71
+ error(f"File not tracked: {target_file}")
72
+ return
73
+
74
+ # Let git handle the pager and standard output natively
75
+ subprocess.run(git_args, cwd=REPO_DIR)
76
+
77
+ except Exception as e:
78
+ error(str(e))
79
+
80
+
81
+ @main.command("diff")
82
+ @click.argument("file", required=False, type=click.Path(path_type=Path))
83
+ @click.option(
84
+ "--branch",
85
+ "-b",
86
+ help="Compare with another branch",
87
+ shell_complete=complete_branches,
88
+ )
89
+ @click.option("--staged", is_flag=True, help="Show staged changes")
90
+ @click.option(
91
+ "--rich/--no-rich",
92
+ default=True,
93
+ help="Use rich for syntax-highlighted diff (default: enabled)",
94
+ )
95
+ @require_init
96
+ def diff(file: Path | None, branch: str | None, staged: bool, rich: bool):
97
+ """Show changes between branches or files.
98
+
99
+ Examples:
100
+ dot-man diff # Show uncommitted changes
101
+ dot-man diff --branch main # Compare current branch with main
102
+ dot-man diff .bashrc # Show changes for specific file
103
+ dot-man diff --no-rich # Use plain git diff
104
+ """
105
+ try:
106
+ import subprocess
107
+
108
+ from ..constants import REPO_DIR
109
+
110
+ if rich:
111
+ _show_rich_diff(file, branch, staged)
112
+ else:
113
+ git_args = ["git", "diff", "--color=always"]
114
+
115
+ if staged:
116
+ git_args.append("--staged")
117
+
118
+ if branch:
119
+ from ..operations import get_operations
120
+
121
+ ops = get_operations()
122
+ current = ops.current_branch
123
+ git_args.append(f"{branch}...{current}")
124
+
125
+ if file:
126
+ from ..operations import get_operations
127
+
128
+ ops = get_operations()
129
+ target_file = file.expanduser().resolve()
130
+ found = False
131
+ for section_name in ops.get_sections():
132
+ section = ops.get_section(section_name)
133
+ for tracked_path in section.paths:
134
+ if tracked_path.resolve() == target_file:
135
+ repo_path = section.get_repo_path(tracked_path, REPO_DIR)
136
+ git_args.extend(
137
+ ["--no-index", str(repo_path), str(target_file)]
138
+ )
139
+ found = True
140
+ break
141
+ if found:
142
+ break
143
+
144
+ if not found:
145
+ error(f"File not tracked: {target_file}")
146
+ return
147
+
148
+ subprocess.run(git_args, cwd=REPO_DIR)
149
+
150
+ except Exception as e:
151
+ error(str(e))
152
+
153
+
154
+ @main.command(deprecated=True, help="⚠️ DEPRECATED: Use 'dot-man navigate' instead")
155
+ @click.argument("target", shell_complete=complete_tags)
156
+ @require_init
157
+ def checkout(target: str):
158
+ """Checkout a specific commit or tag (creates detached HEAD).
159
+
160
+ This allows you to view the state of your dotfiles at a specific
161
+ commit or tag without switching branches.
162
+
163
+ ⚠️ DEPRECATED: Use 'dot-man navigate' instead.
164
+
165
+ To return to a branch, use:
166
+ dot-man navigate <branch-name>
167
+
168
+ Examples:
169
+ dot-man checkout abc1234
170
+ dot-man checkout my-tag
171
+ """
172
+ ui.console.print(
173
+ "[yellow bold]⚠️ WARNING:[/yellow bold] [yellow]'checkout' is deprecated.[/yellow]\n"
174
+ " Use [cyan]dot-man navigate[/cyan] instead.\n"
175
+ " Run [cyan]dot-man navigate --help[/cyan] to see the new command.\n"
176
+ )
177
+ try:
178
+ from ..operations import get_operations
179
+
180
+ ops = get_operations()
181
+ current_branch = ops.current_branch
182
+
183
+ # Try to determine if target is a tag or commit
184
+ from .common import parse_branch_arg
185
+
186
+ parsed = parse_branch_arg(target)
187
+
188
+ if parsed["type"] == "tag":
189
+ _checkout_tag(ops, current_branch, parsed["target"])
190
+ elif parsed["type"] == "commit":
191
+ _checkout_commit(ops, current_branch, parsed["target"])
192
+ else:
193
+ # Check if it's a tag or commit by checking git
194
+ # First try as commit
195
+ try:
196
+ ops.git.repo.commit(target)
197
+ _checkout_commit(ops, current_branch, target)
198
+ except Exception:
199
+ # Try as tag
200
+ tag_commit = ops.git.get_tag_commit(target)
201
+ if tag_commit:
202
+ _checkout_tag(ops, current_branch, target)
203
+ else:
204
+ error(f"Unknown commit or tag: {target}", exit_code=1)
205
+
206
+ except Exception as e:
207
+ error(str(e))
208
+
209
+
210
+ def _checkout_commit(ops, current_branch: str, commit_sha: str):
211
+ """Checkout a specific commit."""
212
+
213
+ # Check if it's a valid commit
214
+ try:
215
+ commit_obj = ops.git.repo.commit(commit_sha)
216
+ except Exception:
217
+ error(f"Invalid commit SHA: {commit_sha}", exit_code=1)
218
+
219
+ # Save current changes if dirty
220
+ if ops.git.is_dirty():
221
+ ui.console.print(
222
+ f"[yellow]Warning:[/yellow] You have uncommitted changes on branch "
223
+ f"[bold]{current_branch}[/bold]"
224
+ )
225
+ ui.console.print(" These changes will NOT be saved.")
226
+ ui.console.print()
227
+
228
+ # Checkout the commit
229
+ ops.git.checkout_commit(commit_sha)
230
+
231
+ ui.console.print("Note: You are in [bold]detached HEAD[/bold] state")
232
+ ui.console.print(f" Commit: [cyan]{commit_sha}[/cyan]")
233
+ ui.console.print(f" Message: {commit_obj.message.strip().split(chr(10))[0][:60]}")
234
+ ui.console.print()
235
+ ui.console.print("To return to a branch, run:")
236
+ ui.console.print(" [cyan]dot-man switch <branch-name>[/cyan]")
237
+
238
+
239
+ def _checkout_tag(ops, current_branch: str, tag_name: str):
240
+ """Checkout a specific tag."""
241
+
242
+ tag_commit = ops.git.get_tag_commit(tag_name)
243
+ if not tag_commit:
244
+ error(f"Tag not found: {tag_name}", exit_code=1)
245
+
246
+ # Get tag info
247
+ try:
248
+ tag_obj = ops.git.repo.tags[tag_name]
249
+ message = ""
250
+ if tag_obj.tag:
251
+ message = tag_obj.tag.message.strip().split("\n")[0]
252
+ except Exception:
253
+ message = ""
254
+
255
+ # Checkout the tag
256
+ ops.git.checkout(tag_name)
257
+
258
+ success(f"Checked out tag '{tag_name}'")
259
+ if message:
260
+ ui.console.print(f" Tag message: {message}")
261
+ ui.console.print(f" Commit: [cyan]{tag_commit}[/cyan]")
262
+ ui.console.print()
263
+ ui.console.print("To return to a branch, run:")
264
+ ui.console.print(" [cyan]dot-man switch <branch-name>[/cyan]")
265
+
266
+
267
+ def _show_rich_diff(file: Path | None, branch: str | None, staged: bool):
268
+ """Show syntax-highlighted diff using rich."""
269
+ try:
270
+ import subprocess
271
+
272
+ from rich.console import Console
273
+ from rich.syntax import Syntax
274
+
275
+ from ..constants import REPO_DIR
276
+
277
+ console = Console()
278
+
279
+ git_args = ["git", "diff", "--no-color"]
280
+
281
+ if staged:
282
+ git_args.append("--staged")
283
+
284
+ if branch:
285
+ from ..operations import get_operations
286
+
287
+ ops = get_operations()
288
+ current = ops.current_branch
289
+ git_args.append(f"{branch}...{current}")
290
+
291
+ if file:
292
+ from ..operations import get_operations
293
+
294
+ ops = get_operations()
295
+ target_file = file.expanduser().resolve()
296
+ found = False
297
+ for section_name in ops.get_sections():
298
+ section = ops.get_section(section_name)
299
+ for tracked_path in section.paths:
300
+ if tracked_path.resolve() == target_file:
301
+ repo_path = section.get_repo_path(tracked_path, REPO_DIR)
302
+ git_args.extend(
303
+ ["--no-index", str(repo_path), str(target_file)]
304
+ )
305
+ found = True
306
+ break
307
+ if found:
308
+ break
309
+
310
+ if not found:
311
+ error(f"File not tracked: {target_file}")
312
+ return
313
+
314
+ result = subprocess.run(git_args, cwd=REPO_DIR, capture_output=True, text=True)
315
+
316
+ if result.stdout:
317
+ syntax = Syntax(result.stdout, "diff", theme="monokai", line_numbers=True)
318
+ console.print(syntax)
319
+ else:
320
+ ui.console.print("[dim]No changes found[/dim]")
321
+
322
+ if result.returncode == 1:
323
+ pass
324
+ elif result.returncode > 1:
325
+ error(f"Git diff failed: {result.stderr}")
326
+
327
+ except ImportError:
328
+ ui.console.print(
329
+ "[yellow]Rich not installed, falling back to git diff[/yellow]"
330
+ )
331
+ git_args = ["git", "diff", "--color=always"]
332
+ if staged:
333
+ git_args.append("--staged")
334
+ if branch:
335
+ from ..operations import get_operations
336
+
337
+ ops = get_operations()
338
+ git_args.append(f"{branch}...{ops.current_branch}")
339
+ subprocess.run(git_args, cwd=REPO_DIR)
dot_man/cli/main.py ADDED
@@ -0,0 +1,36 @@
1
+ """Main entry point for dot-man CLI.
2
+
3
+ This module aggregates all subcommands and exposes the main entry point.
4
+ """
5
+
6
+ # Import the shared CLI group (interface)
7
+ from .interface import cli
8
+
9
+ # Import all subcommands to register them with the CLI group
10
+
11
+
12
+ def main() -> None:
13
+ """Main entry point for the CLI.
14
+
15
+ On the very first launch (no ~/.config/dot-man/ or sentinel missing),
16
+ the onboarding tutorial runs automatically before anything else.
17
+ """
18
+ from .onboarding import is_first_run, run_onboarding
19
+
20
+ if is_first_run():
21
+ run_onboarding()
22
+ return
23
+
24
+ # Try to install completions if not already installed
25
+ try:
26
+ from .completions_cmd import run_install
27
+
28
+ run_install()
29
+ except Exception:
30
+ pass
31
+
32
+ cli()
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()