treestamp 0.1.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.
- treestamp/__init__.py +0 -0
- treestamp/__main__.py +10 -0
- treestamp/cli.py +197 -0
- treestamp/codec.py +515 -0
- treestamp/document.py +476 -0
- treestamp/editor.py +202 -0
- treestamp/errors.py +41 -0
- treestamp/gitio.py +337 -0
- treestamp/maintenance.py +115 -0
- treestamp/model.py +266 -0
- treestamp/py.typed +0 -0
- treestamp/session.py +327 -0
- treestamp/sources.py +103 -0
- treestamp/tui.py +688 -0
- treestamp/tui_dialogs.py +187 -0
- treestamp-0.1.0.dist-info/METADATA +205 -0
- treestamp-0.1.0.dist-info/RECORD +20 -0
- treestamp-0.1.0.dist-info/WHEEL +4 -0
- treestamp-0.1.0.dist-info/entry_points.txt +2 -0
- treestamp-0.1.0.dist-info/licenses/LICENSE +21 -0
treestamp/__init__.py
ADDED
|
File without changes
|
treestamp/__main__.py
ADDED
treestamp/cli.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Headless maintenance and lazy dispatch to the interactive editor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import difflib
|
|
7
|
+
import sys
|
|
8
|
+
from importlib.metadata import version
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from treestamp.document import (
|
|
13
|
+
Document,
|
|
14
|
+
load_document,
|
|
15
|
+
prepare_write,
|
|
16
|
+
publish_write,
|
|
17
|
+
validate_source_tree,
|
|
18
|
+
)
|
|
19
|
+
from treestamp.errors import ExecutionError, MarkerError, TreestampError
|
|
20
|
+
from treestamp.gitio import (
|
|
21
|
+
Snapshot,
|
|
22
|
+
capture_snapshot,
|
|
23
|
+
changes,
|
|
24
|
+
document_path,
|
|
25
|
+
repo_root,
|
|
26
|
+
)
|
|
27
|
+
from treestamp.maintenance import update
|
|
28
|
+
from treestamp.model import Settings, build_tree, tree_path
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Sequence
|
|
32
|
+
|
|
33
|
+
from treestamp.editor import EditorController
|
|
34
|
+
|
|
35
|
+
COMMANDS = ("tui", "update", "check")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parser() -> argparse.ArgumentParser:
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
prog="treestamp",
|
|
41
|
+
description=(
|
|
42
|
+
"Curate Git directory trees. With no command, open the editor. "
|
|
43
|
+
"Use --print to print the complete treestamp without opening the editor."
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--version", action="version", version=f"treestamp {version('treestamp')}"
|
|
48
|
+
)
|
|
49
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
50
|
+
editor = commands.add_parser("tui", help="open the editor or print a treestamp")
|
|
51
|
+
editor.add_argument("path", nargs="?", default=".", help="repository location")
|
|
52
|
+
editor.add_argument(
|
|
53
|
+
"--document",
|
|
54
|
+
type=Path,
|
|
55
|
+
help="repository-root-relative Load document path (open it with --print)",
|
|
56
|
+
)
|
|
57
|
+
editor.add_argument(
|
|
58
|
+
"--print",
|
|
59
|
+
dest="print_output",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="print the complete treestamp and exit without writing a document",
|
|
62
|
+
)
|
|
63
|
+
for command in ("check", "update"):
|
|
64
|
+
child = commands.add_parser(
|
|
65
|
+
command,
|
|
66
|
+
help=(
|
|
67
|
+
"report whether Update would change a document"
|
|
68
|
+
if command == "check"
|
|
69
|
+
else "update a treestamped document"
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
child.add_argument(
|
|
73
|
+
"document",
|
|
74
|
+
nargs="?",
|
|
75
|
+
type=Path,
|
|
76
|
+
default=Path("README.md"),
|
|
77
|
+
help="document path relative to the current repository root",
|
|
78
|
+
)
|
|
79
|
+
child.add_argument(
|
|
80
|
+
"--target", metavar="REF", help="target Git ref (default: captured index)"
|
|
81
|
+
)
|
|
82
|
+
child.add_argument(
|
|
83
|
+
"--compare", metavar="REF", help="reference for detected moves"
|
|
84
|
+
)
|
|
85
|
+
return parser
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _maintain(args: argparse.Namespace) -> int:
|
|
89
|
+
repository = repo_root(Path.cwd())
|
|
90
|
+
path = document_path(repository, args.document)
|
|
91
|
+
document, previous = load_document(path)
|
|
92
|
+
assert document is not None and previous is not None
|
|
93
|
+
root = repository / document.settings.root
|
|
94
|
+
target = capture_snapshot(root, args.target, repository=repository)
|
|
95
|
+
validate_source_tree(target.tree, document.settings)
|
|
96
|
+
if args.compare == "":
|
|
97
|
+
raise MarkerError("--compare requires a nonempty ref")
|
|
98
|
+
moves = changes(target, args.compare).moves if args.compare is not None else None
|
|
99
|
+
hidden = frozenset(
|
|
100
|
+
tree_path(path, document.settings.root) for path in document.hidden
|
|
101
|
+
)
|
|
102
|
+
tree = update(
|
|
103
|
+
document.tree, target.tree, document.settings, hidden=hidden, moves=moves
|
|
104
|
+
)
|
|
105
|
+
maintained = Document(tree, document.settings, document.hidden)
|
|
106
|
+
prepared = prepare_write(
|
|
107
|
+
maintained, previous.path, expected=previous, repository=repository
|
|
108
|
+
)
|
|
109
|
+
if prepared.content == previous.content:
|
|
110
|
+
_emit(f"Current: {previous.path}\n")
|
|
111
|
+
return 0
|
|
112
|
+
if args.command == "check":
|
|
113
|
+
before = previous.content.decode("utf-8", "replace").splitlines(keepends=True)
|
|
114
|
+
after = prepared.content.decode("utf-8", "replace").splitlines(keepends=True)
|
|
115
|
+
_emit(
|
|
116
|
+
"".join(
|
|
117
|
+
difflib.unified_diff(
|
|
118
|
+
before,
|
|
119
|
+
after,
|
|
120
|
+
fromfile=str(previous.path),
|
|
121
|
+
tofile=str(previous.path),
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
return 1
|
|
126
|
+
publish_write(prepared)
|
|
127
|
+
_emit(f"Updated: {previous.path}\n")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _editor_inputs(args: argparse.Namespace) -> EditorController:
|
|
132
|
+
from treestamp.editor import EditorController
|
|
133
|
+
|
|
134
|
+
repository = repo_root(Path(args.path))
|
|
135
|
+
path = document_path(repository, args.document or "README.md")
|
|
136
|
+
document, previous = load_document(path, allow_absent=args.document is None)
|
|
137
|
+
settings = document.settings if document else Settings()
|
|
138
|
+
target = capture_snapshot(repository / settings.root, repository=repository)
|
|
139
|
+
return EditorController(
|
|
140
|
+
target,
|
|
141
|
+
settings,
|
|
142
|
+
tree=document.tree if document else None,
|
|
143
|
+
hidden=document.hidden if document else frozenset(),
|
|
144
|
+
document_path=path,
|
|
145
|
+
document_snapshot=previous,
|
|
146
|
+
loaded_document=document,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _explore(args: argparse.Namespace) -> int:
|
|
151
|
+
if args.print_output:
|
|
152
|
+
_emit(_editor_inputs(args).region_text())
|
|
153
|
+
return 0
|
|
154
|
+
from treestamp.editor import EditorController
|
|
155
|
+
from treestamp.tui import explore
|
|
156
|
+
|
|
157
|
+
repository = repo_root(Path(args.path))
|
|
158
|
+
path = document_path(repository, args.document or "README.md").relative_to(
|
|
159
|
+
repository
|
|
160
|
+
)
|
|
161
|
+
# Startup has no accepted source yet; New/Load captures its selected subtree.
|
|
162
|
+
editor = EditorController(
|
|
163
|
+
Snapshot(repository, repository, build_tree([]), ""), Settings()
|
|
164
|
+
)
|
|
165
|
+
if explore(editor, startup_document=path):
|
|
166
|
+
_emit(editor.region_text())
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _emit(text: str) -> None:
|
|
171
|
+
"""Preserve Git bytes and newlines even through a Windows output pipe."""
|
|
172
|
+
stream = getattr(sys.stdout, "buffer", None)
|
|
173
|
+
if stream is None:
|
|
174
|
+
sys.stdout.write(text)
|
|
175
|
+
return
|
|
176
|
+
sys.stdout.flush()
|
|
177
|
+
stream.write(text.encode("utf-8", "surrogateescape"))
|
|
178
|
+
stream.flush()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
182
|
+
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
183
|
+
if not arguments or arguments[0] not in (*COMMANDS, "--help", "-h", "--version"):
|
|
184
|
+
arguments.insert(0, "tui")
|
|
185
|
+
try:
|
|
186
|
+
args = _parser().parse_args(arguments)
|
|
187
|
+
if args.command == "tui":
|
|
188
|
+
return _explore(args)
|
|
189
|
+
return _maintain(args)
|
|
190
|
+
except TreestampError as exc:
|
|
191
|
+
print(f"treestamp: {exc}", file=sys.stderr)
|
|
192
|
+
return exc.exit_code
|
|
193
|
+
except (OSError, UnicodeError) as exc:
|
|
194
|
+
print(f"treestamp: {exc}", file=sys.stderr)
|
|
195
|
+
return ExecutionError.exit_code
|
|
196
|
+
except KeyboardInterrupt:
|
|
197
|
+
return 130
|