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.
- dot_man/__init__.py +4 -0
- dot_man/backups.py +211 -0
- dot_man/branch_ops.py +347 -0
- dot_man/cli/__init__.py +113 -0
- dot_man/cli/add_cmd.py +167 -0
- dot_man/cli/audit_cmd.py +141 -0
- dot_man/cli/backup_cmd.py +105 -0
- dot_man/cli/branch_cmd.py +103 -0
- dot_man/cli/clean_cmd.py +97 -0
- dot_man/cli/common.py +548 -0
- dot_man/cli/completions_cmd.py +127 -0
- dot_man/cli/config_cmd.py +979 -0
- dot_man/cli/deploy_cmd.py +169 -0
- dot_man/cli/discover_cmd.py +105 -0
- dot_man/cli/doctor_cmd.py +229 -0
- dot_man/cli/edit_cmd.py +177 -0
- dot_man/cli/encrypt_cmd.py +205 -0
- dot_man/cli/export_cmd.py +146 -0
- dot_man/cli/import_cmd.py +315 -0
- dot_man/cli/init_cmd.py +532 -0
- dot_man/cli/interface.py +56 -0
- dot_man/cli/log_cmd.py +339 -0
- dot_man/cli/main.py +36 -0
- dot_man/cli/navigate_cmd.py +903 -0
- dot_man/cli/onboarding.py +546 -0
- dot_man/cli/profile_cmd.py +313 -0
- dot_man/cli/remote_cmd.py +454 -0
- dot_man/cli/restore_cmd.py +82 -0
- dot_man/cli/revert_cmd.py +86 -0
- dot_man/cli/show_cmd.py +29 -0
- dot_man/cli/status_cmd.py +185 -0
- dot_man/cli/switch_cmd.py +387 -0
- dot_man/cli/tag_cmd.py +164 -0
- dot_man/cli/template_cmd.py +244 -0
- dot_man/cli/tui_cmd.py +44 -0
- dot_man/cli/verify_cmd.py +156 -0
- dot_man/completions/_dot-man.zsh +28 -0
- dot_man/completions/dot-man.bash +15 -0
- dot_man/completions/dot-man.fish +58 -0
- dot_man/completions/install.sh +26 -0
- dot_man/config.py +23 -0
- dot_man/config_detector.py +426 -0
- dot_man/constants.py +109 -0
- dot_man/core.py +614 -0
- dot_man/dotman_config.py +516 -0
- dot_man/encryption.py +173 -0
- dot_man/exceptions.py +255 -0
- dot_man/files.py +443 -0
- dot_man/global_config.py +305 -0
- dot_man/hooks.py +232 -0
- dot_man/interactive.py +460 -0
- dot_man/lock.py +64 -0
- dot_man/merge.py +440 -0
- dot_man/operations.py +212 -0
- dot_man/py.typed +1 -0
- dot_man/save_deploy_ops.py +466 -0
- dot_man/secrets.py +473 -0
- dot_man/section.py +207 -0
- dot_man/status_ops.py +229 -0
- dot_man/tui_log.py +91 -0
- dot_man/ui.py +127 -0
- dot_man/utils.py +132 -0
- dot_man/vault.py +317 -0
- dotman_git-1.0.0.dist-info/METADATA +678 -0
- dotman_git-1.0.0.dist-info/RECORD +69 -0
- dotman_git-1.0.0.dist-info/WHEEL +5 -0
- dotman_git-1.0.0.dist-info/entry_points.txt +3 -0
- dotman_git-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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()
|