versite 0.0.1__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.
- versite/__init__.py +3 -0
- versite/builders/__init__.py +1 -0
- versite/builders/command.py +66 -0
- versite/cli.py +182 -0
- versite/commands.py +406 -0
- versite/config.py +139 -0
- versite/git_utils.py +145 -0
- versite/jsonpath.py +81 -0
- versite/redirects.py +24 -0
- versite/serve.py +16 -0
- versite/templates/__init__.py +1 -0
- versite/templates/redirect.html +11 -0
- versite/versions.py +111 -0
- versite-0.0.1.dist-info/METADATA +222 -0
- versite-0.0.1.dist-info/RECORD +18 -0
- versite-0.0.1.dist-info/WHEEL +5 -0
- versite-0.0.1.dist-info/entry_points.txt +2 -0
- versite-0.0.1.dist-info/top_level.txt +1 -0
versite/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ["command"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BuilderError(RuntimeError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BuildResult:
|
|
15
|
+
site_dir: str
|
|
16
|
+
metadata: dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CommandBuilder:
|
|
21
|
+
name: str
|
|
22
|
+
command: list[str]
|
|
23
|
+
|
|
24
|
+
def build(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
version: str,
|
|
28
|
+
output_dir: str,
|
|
29
|
+
config: dict[str, Any],
|
|
30
|
+
quiet: bool = False,
|
|
31
|
+
) -> BuildResult:
|
|
32
|
+
variables = {
|
|
33
|
+
"version": version,
|
|
34
|
+
"output_dir": output_dir,
|
|
35
|
+
"source": config.get("source", "."),
|
|
36
|
+
"config_file": config.get("config_file", "mkdocs.yml"),
|
|
37
|
+
}
|
|
38
|
+
rendered = [part.format(**variables) for part in self.command]
|
|
39
|
+
env = os.environ.copy()
|
|
40
|
+
env["VERSITE_VERSION"] = version
|
|
41
|
+
if self.name == "mkdocs":
|
|
42
|
+
env["MIKE_DOCS_VERSION"] = version
|
|
43
|
+
kwargs: dict[str, Any] = {"check": True, "env": env}
|
|
44
|
+
if quiet:
|
|
45
|
+
kwargs["stdout"] = subprocess.DEVNULL
|
|
46
|
+
kwargs["stderr"] = subprocess.DEVNULL
|
|
47
|
+
try:
|
|
48
|
+
subprocess.run(rendered, **kwargs)
|
|
49
|
+
except FileNotFoundError as exc:
|
|
50
|
+
raise BuilderError(
|
|
51
|
+
f"builder command is unavailable: {rendered[0]}"
|
|
52
|
+
) from exc
|
|
53
|
+
except subprocess.CalledProcessError as exc:
|
|
54
|
+
raise BuilderError(f"builder command failed with exit code {exc.returncode}") from exc
|
|
55
|
+
return BuildResult(site_dir=output_dir, metadata={"command": rendered})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_builder(name: str, config: dict[str, Any]) -> CommandBuilder:
|
|
59
|
+
try:
|
|
60
|
+
builder_config = config["builders"][name]
|
|
61
|
+
except KeyError as exc:
|
|
62
|
+
raise BuilderError(f"unknown builder: {name}") from exc
|
|
63
|
+
command = builder_config.get("command")
|
|
64
|
+
if not command:
|
|
65
|
+
raise BuilderError(f"builder '{name}' has no command configured")
|
|
66
|
+
return CommandBuilder(name=name, command=list(command))
|
versite/cli.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from versite.commands import (
|
|
7
|
+
VersiteError,
|
|
8
|
+
alias_version,
|
|
9
|
+
delete_versions,
|
|
10
|
+
deploy_version,
|
|
11
|
+
list_versions,
|
|
12
|
+
props_version,
|
|
13
|
+
retitle_version,
|
|
14
|
+
serve_site,
|
|
15
|
+
set_default,
|
|
16
|
+
)
|
|
17
|
+
from versite.config import apply_cli_overrides, load_config
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
21
|
+
parser = argparse.ArgumentParser(prog="versite")
|
|
22
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
23
|
+
|
|
24
|
+
deploy = subparsers.add_parser("deploy")
|
|
25
|
+
deploy.add_argument("version")
|
|
26
|
+
deploy.add_argument("aliases", nargs="*")
|
|
27
|
+
_add_common_options(deploy, include_builder=True)
|
|
28
|
+
deploy.add_argument("--source")
|
|
29
|
+
deploy.add_argument("--output-dir")
|
|
30
|
+
deploy.add_argument("--build-command", nargs=argparse.REMAINDER)
|
|
31
|
+
deploy.add_argument("-q", "--quiet", action="store_true")
|
|
32
|
+
|
|
33
|
+
list_parser = subparsers.add_parser("list")
|
|
34
|
+
list_parser.add_argument("identifier", nargs="?")
|
|
35
|
+
_add_common_options(list_parser)
|
|
36
|
+
list_parser.add_argument("--json", action="store_true")
|
|
37
|
+
|
|
38
|
+
delete = subparsers.add_parser("delete")
|
|
39
|
+
delete.add_argument("identifiers", nargs="*")
|
|
40
|
+
delete.add_argument("--all", action="store_true")
|
|
41
|
+
_add_common_options(delete)
|
|
42
|
+
|
|
43
|
+
alias = subparsers.add_parser("alias")
|
|
44
|
+
alias.add_argument("identifier")
|
|
45
|
+
alias.add_argument("aliases", nargs="*")
|
|
46
|
+
_add_common_options(alias)
|
|
47
|
+
alias.add_argument("--alias-type", choices=["redirect", "copy", "symlink"])
|
|
48
|
+
|
|
49
|
+
retitle = subparsers.add_parser("retitle")
|
|
50
|
+
retitle.add_argument("identifier")
|
|
51
|
+
retitle.add_argument("title")
|
|
52
|
+
_add_common_options(retitle)
|
|
53
|
+
|
|
54
|
+
props = subparsers.add_parser("props")
|
|
55
|
+
props.add_argument("identifier")
|
|
56
|
+
props.add_argument("prop", nargs="?")
|
|
57
|
+
_add_common_options(props)
|
|
58
|
+
props.add_argument("--json", action="store_true")
|
|
59
|
+
|
|
60
|
+
default = subparsers.add_parser("set-default")
|
|
61
|
+
default.add_argument("identifier")
|
|
62
|
+
_add_common_options(default)
|
|
63
|
+
|
|
64
|
+
serve = subparsers.add_parser("serve")
|
|
65
|
+
_add_common_options(serve)
|
|
66
|
+
serve.add_argument("--host", default="127.0.0.1")
|
|
67
|
+
serve.add_argument("--port", type=int, default=8000)
|
|
68
|
+
|
|
69
|
+
return parser
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _add_common_options(parser: argparse.ArgumentParser, include_builder: bool = False) -> None:
|
|
73
|
+
parser.add_argument("--config-file")
|
|
74
|
+
if include_builder:
|
|
75
|
+
parser.add_argument("--builder")
|
|
76
|
+
parser.add_argument("-r", "--remote")
|
|
77
|
+
parser.add_argument("-b", "--branch")
|
|
78
|
+
parser.add_argument("-m", "--message")
|
|
79
|
+
parser.add_argument("-p", "--push", action="store_true", default=None)
|
|
80
|
+
parser.add_argument("--allow-empty", action="store_true")
|
|
81
|
+
parser.add_argument("--deploy-prefix")
|
|
82
|
+
parser.add_argument("-T", "--template", dest="redirect_template")
|
|
83
|
+
parser.add_argument("--ignore-remote-status", action="store_true")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _load_runtime_config(args: argparse.Namespace) -> dict:
|
|
87
|
+
config, _ = load_config(args.config_file)
|
|
88
|
+
return apply_cli_overrides(
|
|
89
|
+
config,
|
|
90
|
+
builder=getattr(args, "builder", None),
|
|
91
|
+
remote=args.remote,
|
|
92
|
+
branch=args.branch,
|
|
93
|
+
message=args.message,
|
|
94
|
+
push=args.push,
|
|
95
|
+
deploy_prefix=args.deploy_prefix,
|
|
96
|
+
alias_type=getattr(args, "alias_type", None),
|
|
97
|
+
redirect_template=args.redirect_template,
|
|
98
|
+
ignore_remote_status=args.ignore_remote_status,
|
|
99
|
+
source=getattr(args, "source", None),
|
|
100
|
+
output_dir=getattr(args, "output_dir", None),
|
|
101
|
+
build_command=getattr(args, "build_command", None),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main(argv: list[str] | None = None) -> int:
|
|
106
|
+
parser = build_parser()
|
|
107
|
+
args = parser.parse_args(argv)
|
|
108
|
+
try:
|
|
109
|
+
config = _load_runtime_config(args)
|
|
110
|
+
push = config.get("push", False)
|
|
111
|
+
if args.command == "deploy":
|
|
112
|
+
return deploy_version(
|
|
113
|
+
config,
|
|
114
|
+
args.version,
|
|
115
|
+
args.aliases,
|
|
116
|
+
message=args.message,
|
|
117
|
+
push=push,
|
|
118
|
+
allow_empty=args.allow_empty,
|
|
119
|
+
quiet=args.quiet,
|
|
120
|
+
)
|
|
121
|
+
if args.command == "list":
|
|
122
|
+
return list_versions(config, args.identifier, as_json=args.json)
|
|
123
|
+
if args.command == "delete":
|
|
124
|
+
return delete_versions(
|
|
125
|
+
config,
|
|
126
|
+
args.identifiers,
|
|
127
|
+
delete_all=args.all,
|
|
128
|
+
message=args.message,
|
|
129
|
+
push=push,
|
|
130
|
+
allow_empty=args.allow_empty,
|
|
131
|
+
)
|
|
132
|
+
if args.command == "alias":
|
|
133
|
+
return alias_version(
|
|
134
|
+
config,
|
|
135
|
+
args.identifier,
|
|
136
|
+
args.aliases,
|
|
137
|
+
alias_type=args.alias_type or config["alias_type"],
|
|
138
|
+
message=args.message,
|
|
139
|
+
push=push,
|
|
140
|
+
allow_empty=args.allow_empty,
|
|
141
|
+
)
|
|
142
|
+
if args.command == "retitle":
|
|
143
|
+
return retitle_version(
|
|
144
|
+
config,
|
|
145
|
+
args.identifier,
|
|
146
|
+
args.title,
|
|
147
|
+
message=args.message,
|
|
148
|
+
push=push,
|
|
149
|
+
allow_empty=args.allow_empty,
|
|
150
|
+
)
|
|
151
|
+
if args.command == "props":
|
|
152
|
+
return props_version(
|
|
153
|
+
config,
|
|
154
|
+
args.identifier,
|
|
155
|
+
args.prop,
|
|
156
|
+
message=args.message,
|
|
157
|
+
push=push,
|
|
158
|
+
allow_empty=args.allow_empty,
|
|
159
|
+
as_json=args.json,
|
|
160
|
+
)
|
|
161
|
+
if args.command == "set-default":
|
|
162
|
+
return set_default(
|
|
163
|
+
config,
|
|
164
|
+
args.identifier,
|
|
165
|
+
message=args.message,
|
|
166
|
+
push=push,
|
|
167
|
+
allow_empty=args.allow_empty,
|
|
168
|
+
)
|
|
169
|
+
if args.command == "serve":
|
|
170
|
+
return serve_site(config, host=args.host, port=args.port)
|
|
171
|
+
parser.error(f"unsupported command: {args.command}")
|
|
172
|
+
except (VersiteError, ValueError, KeyError) as exc:
|
|
173
|
+
print(f"versite: {exc}", file=sys.stderr)
|
|
174
|
+
return 1
|
|
175
|
+
except Exception as exc: # pragma: no cover
|
|
176
|
+
print(f"versite: {exc}", file=sys.stderr)
|
|
177
|
+
return 1
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
raise SystemExit(main())
|
versite/commands.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from versite.config import normalize_prefix
|
|
11
|
+
from versite.git_utils import GitError, branch_worktree, commit_all, git_root, push_branch
|
|
12
|
+
from versite.jsonpath import parse_assignment, parse_value
|
|
13
|
+
from versite.redirects import write_redirect
|
|
14
|
+
from versite.serve import serve_directory
|
|
15
|
+
from versite.versions import VersionStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VersiteError(RuntimeError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _site_root(worktree: Path, deploy_prefix: str) -> Path:
|
|
23
|
+
prefix = normalize_prefix(deploy_prefix)
|
|
24
|
+
return worktree / prefix if prefix else worktree
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _versions_file(worktree: Path, deploy_prefix: str) -> Path:
|
|
28
|
+
return _site_root(worktree, deploy_prefix) / "versions.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _identifier_path(worktree: Path, deploy_prefix: str, identifier: str) -> Path:
|
|
32
|
+
if identifier.startswith("/") or ".." in Path(identifier).parts:
|
|
33
|
+
raise VersiteError(f"unsafe identifier path: {identifier}")
|
|
34
|
+
return _site_root(worktree, deploy_prefix) / identifier
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _print(data: Any, as_json: bool = False) -> None:
|
|
38
|
+
if as_json:
|
|
39
|
+
print(json.dumps(data, indent=2, sort_keys=True))
|
|
40
|
+
elif isinstance(data, (dict, list)):
|
|
41
|
+
print(json.dumps(data, indent=2, sort_keys=True))
|
|
42
|
+
else:
|
|
43
|
+
print(data)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_store(worktree: Path, deploy_prefix: str) -> VersionStore:
|
|
47
|
+
return VersionStore.load(_versions_file(worktree, deploy_prefix))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _save_store(worktree: Path, deploy_prefix: str, store: VersionStore) -> None:
|
|
51
|
+
site_root = _site_root(worktree, deploy_prefix)
|
|
52
|
+
site_root.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
store.save(_versions_file(worktree, deploy_prefix))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _root_redirect_href(identifier: str, deploy_prefix: str) -> str:
|
|
57
|
+
prefix = normalize_prefix(deploy_prefix)
|
|
58
|
+
if prefix:
|
|
59
|
+
return f"./{prefix}/{identifier}/"
|
|
60
|
+
return f"./{identifier}/"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _alias_redirect_href(alias: str, version: str) -> str:
|
|
64
|
+
alias_parts = Path(alias).parts
|
|
65
|
+
upward = "../" * max(len(alias_parts), 1)
|
|
66
|
+
return f"{upward}{version}/"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _copy_contents(source: Path, destination: Path) -> None:
|
|
70
|
+
if destination.exists() or destination.is_symlink():
|
|
71
|
+
if destination.is_dir() and not destination.is_symlink():
|
|
72
|
+
shutil.rmtree(destination)
|
|
73
|
+
else:
|
|
74
|
+
destination.unlink()
|
|
75
|
+
shutil.copytree(source, destination, symlinks=True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _write_alias(
|
|
79
|
+
worktree: Path,
|
|
80
|
+
deploy_prefix: str,
|
|
81
|
+
alias: str,
|
|
82
|
+
version: str,
|
|
83
|
+
alias_type: str,
|
|
84
|
+
redirect_template: str | None,
|
|
85
|
+
) -> None:
|
|
86
|
+
alias_path = _identifier_path(worktree, deploy_prefix, alias)
|
|
87
|
+
version_path = _identifier_path(worktree, deploy_prefix, version)
|
|
88
|
+
if alias_path.exists() or alias_path.is_symlink():
|
|
89
|
+
if alias_path.is_dir() and not alias_path.is_symlink():
|
|
90
|
+
shutil.rmtree(alias_path)
|
|
91
|
+
else:
|
|
92
|
+
alias_path.unlink()
|
|
93
|
+
if alias_type == "redirect":
|
|
94
|
+
write_redirect(alias_path / "index.html", _alias_redirect_href(alias, version), redirect_template)
|
|
95
|
+
return
|
|
96
|
+
if alias_type == "copy":
|
|
97
|
+
_copy_contents(version_path, alias_path)
|
|
98
|
+
return
|
|
99
|
+
if alias_type == "symlink":
|
|
100
|
+
alias_path.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
target = os.path.relpath(version_path, alias_path.parent)
|
|
102
|
+
os.symlink(target, alias_path)
|
|
103
|
+
return
|
|
104
|
+
raise VersiteError(f"unsupported alias type: {alias_type}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _remove_path(path: Path) -> None:
|
|
108
|
+
if path.is_symlink() or path.is_file():
|
|
109
|
+
path.unlink(missing_ok=True)
|
|
110
|
+
elif path.is_dir():
|
|
111
|
+
shutil.rmtree(path)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def list_versions(config: dict[str, Any], identifier: str | None = None, as_json: bool = False) -> int:
|
|
115
|
+
repo = git_root()
|
|
116
|
+
with branch_worktree(
|
|
117
|
+
repo,
|
|
118
|
+
config["branch"],
|
|
119
|
+
remote=config["remote"],
|
|
120
|
+
ignore_remote_status=config.get("ignore_remote_status", False),
|
|
121
|
+
) as worktree:
|
|
122
|
+
store = _load_store(worktree, config["deploy_prefix"])
|
|
123
|
+
if identifier is None:
|
|
124
|
+
_print(store.as_list(), as_json)
|
|
125
|
+
return 0
|
|
126
|
+
record = store.get(identifier)
|
|
127
|
+
_print(
|
|
128
|
+
{
|
|
129
|
+
"version": record.version,
|
|
130
|
+
"title": record.title,
|
|
131
|
+
"aliases": record.aliases,
|
|
132
|
+
"properties": record.properties,
|
|
133
|
+
},
|
|
134
|
+
as_json,
|
|
135
|
+
)
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def delete_versions(
|
|
140
|
+
config: dict[str, Any],
|
|
141
|
+
identifiers: list[str],
|
|
142
|
+
*,
|
|
143
|
+
delete_all: bool = False,
|
|
144
|
+
message: str | None = None,
|
|
145
|
+
push: bool = False,
|
|
146
|
+
allow_empty: bool = False,
|
|
147
|
+
) -> int:
|
|
148
|
+
if not identifiers and not delete_all:
|
|
149
|
+
raise VersiteError("delete requires identifiers or --all")
|
|
150
|
+
repo = git_root()
|
|
151
|
+
with branch_worktree(
|
|
152
|
+
repo,
|
|
153
|
+
config["branch"],
|
|
154
|
+
remote=config["remote"],
|
|
155
|
+
ignore_remote_status=config.get("ignore_remote_status", False),
|
|
156
|
+
) as worktree:
|
|
157
|
+
store = _load_store(worktree, config["deploy_prefix"])
|
|
158
|
+
if delete_all:
|
|
159
|
+
targets = [record.version for record in list(store.records)]
|
|
160
|
+
else:
|
|
161
|
+
targets = identifiers
|
|
162
|
+
removed_any = False
|
|
163
|
+
for identifier in targets:
|
|
164
|
+
version, aliases = store.delete_identifier(identifier)
|
|
165
|
+
removed_any = True
|
|
166
|
+
if version is not None:
|
|
167
|
+
_remove_path(_identifier_path(worktree, config["deploy_prefix"], version))
|
|
168
|
+
for alias in aliases:
|
|
169
|
+
_remove_path(_identifier_path(worktree, config["deploy_prefix"], alias))
|
|
170
|
+
_save_store(worktree, config["deploy_prefix"], store)
|
|
171
|
+
committed = commit_all(
|
|
172
|
+
worktree,
|
|
173
|
+
message or ("Delete all deployed versions" if delete_all else f"Delete {' '.join(targets)}"),
|
|
174
|
+
allow_empty=allow_empty,
|
|
175
|
+
)
|
|
176
|
+
if push and committed:
|
|
177
|
+
push_branch(worktree, config["remote"], config["branch"])
|
|
178
|
+
return 0 if removed_any else 1
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def alias_version(
|
|
182
|
+
config: dict[str, Any],
|
|
183
|
+
identifier: str,
|
|
184
|
+
aliases: list[str],
|
|
185
|
+
*,
|
|
186
|
+
alias_type: str,
|
|
187
|
+
message: str | None = None,
|
|
188
|
+
push: bool = False,
|
|
189
|
+
allow_empty: bool = False,
|
|
190
|
+
) -> int:
|
|
191
|
+
if not aliases:
|
|
192
|
+
raise VersiteError("alias requires at least one alias")
|
|
193
|
+
repo = git_root()
|
|
194
|
+
with branch_worktree(
|
|
195
|
+
repo,
|
|
196
|
+
config["branch"],
|
|
197
|
+
remote=config["remote"],
|
|
198
|
+
ignore_remote_status=config.get("ignore_remote_status", False),
|
|
199
|
+
) as worktree:
|
|
200
|
+
store = _load_store(worktree, config["deploy_prefix"])
|
|
201
|
+
record = store.get(identifier)
|
|
202
|
+
store.upsert(record.version, aliases=aliases)
|
|
203
|
+
for alias in aliases:
|
|
204
|
+
_write_alias(
|
|
205
|
+
worktree,
|
|
206
|
+
config["deploy_prefix"],
|
|
207
|
+
alias,
|
|
208
|
+
record.version,
|
|
209
|
+
alias_type,
|
|
210
|
+
config.get("redirect_template"),
|
|
211
|
+
)
|
|
212
|
+
_save_store(worktree, config["deploy_prefix"], store)
|
|
213
|
+
committed = commit_all(
|
|
214
|
+
worktree,
|
|
215
|
+
message or f"Update aliases for {record.version}",
|
|
216
|
+
allow_empty=allow_empty,
|
|
217
|
+
)
|
|
218
|
+
if push and committed:
|
|
219
|
+
push_branch(worktree, config["remote"], config["branch"])
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def retitle_version(
|
|
224
|
+
config: dict[str, Any],
|
|
225
|
+
identifier: str,
|
|
226
|
+
title: str,
|
|
227
|
+
*,
|
|
228
|
+
message: str | None = None,
|
|
229
|
+
push: bool = False,
|
|
230
|
+
allow_empty: bool = False,
|
|
231
|
+
) -> int:
|
|
232
|
+
repo = git_root()
|
|
233
|
+
with branch_worktree(
|
|
234
|
+
repo,
|
|
235
|
+
config["branch"],
|
|
236
|
+
remote=config["remote"],
|
|
237
|
+
ignore_remote_status=config.get("ignore_remote_status", False),
|
|
238
|
+
) as worktree:
|
|
239
|
+
store = _load_store(worktree, config["deploy_prefix"])
|
|
240
|
+
store.retitle(identifier, title)
|
|
241
|
+
_save_store(worktree, config["deploy_prefix"], store)
|
|
242
|
+
committed = commit_all(worktree, message or f"Retitle {identifier}", allow_empty=allow_empty)
|
|
243
|
+
if push and committed:
|
|
244
|
+
push_branch(worktree, config["remote"], config["branch"])
|
|
245
|
+
return 0
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def props_version(
|
|
249
|
+
config: dict[str, Any],
|
|
250
|
+
identifier: str,
|
|
251
|
+
prop: str | None = None,
|
|
252
|
+
*,
|
|
253
|
+
message: str | None = None,
|
|
254
|
+
push: bool = False,
|
|
255
|
+
allow_empty: bool = False,
|
|
256
|
+
as_json: bool = False,
|
|
257
|
+
) -> int:
|
|
258
|
+
repo = git_root()
|
|
259
|
+
needs_write = prop is not None and ("=" in prop or prop.endswith("-"))
|
|
260
|
+
with branch_worktree(
|
|
261
|
+
repo,
|
|
262
|
+
config["branch"],
|
|
263
|
+
remote=config["remote"],
|
|
264
|
+
ignore_remote_status=config.get("ignore_remote_status", False),
|
|
265
|
+
) as worktree:
|
|
266
|
+
store = _load_store(worktree, config["deploy_prefix"])
|
|
267
|
+
if prop is None:
|
|
268
|
+
_print(store.get_properties(identifier), as_json)
|
|
269
|
+
return 0
|
|
270
|
+
if "=" in prop:
|
|
271
|
+
path, raw_value = parse_assignment(prop)
|
|
272
|
+
props = store.set_property(identifier, path, parse_value(raw_value))
|
|
273
|
+
_save_store(worktree, config["deploy_prefix"], store)
|
|
274
|
+
committed = commit_all(
|
|
275
|
+
worktree,
|
|
276
|
+
message or f"Update properties for {identifier}",
|
|
277
|
+
allow_empty=allow_empty,
|
|
278
|
+
)
|
|
279
|
+
if push and committed:
|
|
280
|
+
push_branch(worktree, config["remote"], config["branch"])
|
|
281
|
+
_print(props, as_json)
|
|
282
|
+
return 0
|
|
283
|
+
if prop.endswith("-"):
|
|
284
|
+
path = prop[:-1]
|
|
285
|
+
props = store.delete_property(identifier, path)
|
|
286
|
+
_save_store(worktree, config["deploy_prefix"], store)
|
|
287
|
+
committed = commit_all(
|
|
288
|
+
worktree,
|
|
289
|
+
message or f"Update properties for {identifier}",
|
|
290
|
+
allow_empty=allow_empty,
|
|
291
|
+
)
|
|
292
|
+
if push and committed:
|
|
293
|
+
push_branch(worktree, config["remote"], config["branch"])
|
|
294
|
+
_print(props, as_json)
|
|
295
|
+
return 0
|
|
296
|
+
_print(store.get_property(identifier, prop), as_json)
|
|
297
|
+
return 0
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def set_default(
|
|
301
|
+
config: dict[str, Any],
|
|
302
|
+
identifier: str,
|
|
303
|
+
*,
|
|
304
|
+
message: str | None = None,
|
|
305
|
+
push: bool = False,
|
|
306
|
+
allow_empty: bool = False,
|
|
307
|
+
) -> int:
|
|
308
|
+
repo = git_root()
|
|
309
|
+
with branch_worktree(
|
|
310
|
+
repo,
|
|
311
|
+
config["branch"],
|
|
312
|
+
remote=config["remote"],
|
|
313
|
+
ignore_remote_status=config.get("ignore_remote_status", False),
|
|
314
|
+
) as worktree:
|
|
315
|
+
store = _load_store(worktree, config["deploy_prefix"])
|
|
316
|
+
store.get(identifier)
|
|
317
|
+
write_redirect(
|
|
318
|
+
worktree / "index.html",
|
|
319
|
+
_root_redirect_href(identifier, config["deploy_prefix"]),
|
|
320
|
+
config.get("redirect_template"),
|
|
321
|
+
)
|
|
322
|
+
committed = commit_all(worktree, message or f"Set default site to {identifier}", allow_empty=allow_empty)
|
|
323
|
+
if push and committed:
|
|
324
|
+
push_branch(worktree, config["remote"], config["branch"])
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def serve_site(config: dict[str, Any], host: str = "127.0.0.1", port: int = 8000) -> int:
|
|
329
|
+
repo = git_root()
|
|
330
|
+
with branch_worktree(
|
|
331
|
+
repo,
|
|
332
|
+
config["branch"],
|
|
333
|
+
remote=config["remote"],
|
|
334
|
+
ignore_remote_status=True,
|
|
335
|
+
) as worktree:
|
|
336
|
+
serve_directory(worktree, host=host, port=port)
|
|
337
|
+
return 0
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def deploy_version(
|
|
341
|
+
config: dict[str, Any],
|
|
342
|
+
version: str,
|
|
343
|
+
aliases: list[str],
|
|
344
|
+
*,
|
|
345
|
+
message: str | None = None,
|
|
346
|
+
push: bool = False,
|
|
347
|
+
allow_empty: bool = False,
|
|
348
|
+
quiet: bool = False,
|
|
349
|
+
) -> int:
|
|
350
|
+
from versite.builders.command import BuilderError, load_builder
|
|
351
|
+
|
|
352
|
+
repo = git_root()
|
|
353
|
+
builder_name = config["builder"]
|
|
354
|
+
builder = load_builder(builder_name, config)
|
|
355
|
+
builder_config = config["builders"][builder_name]
|
|
356
|
+
output_dir = config.get("output_dir")
|
|
357
|
+
temp_output: tempfile.TemporaryDirectory[str] | None = None
|
|
358
|
+
if output_dir is None:
|
|
359
|
+
temp_output = tempfile.TemporaryDirectory(prefix="versite-build-")
|
|
360
|
+
output_dir = temp_output.name
|
|
361
|
+
try:
|
|
362
|
+
builder.build(
|
|
363
|
+
version=version,
|
|
364
|
+
output_dir=output_dir,
|
|
365
|
+
config=builder_config,
|
|
366
|
+
quiet=quiet,
|
|
367
|
+
)
|
|
368
|
+
with branch_worktree(
|
|
369
|
+
repo,
|
|
370
|
+
config["branch"],
|
|
371
|
+
remote=config["remote"],
|
|
372
|
+
ignore_remote_status=config.get("ignore_remote_status", False),
|
|
373
|
+
) as worktree:
|
|
374
|
+
site_root = _site_root(worktree, config["deploy_prefix"])
|
|
375
|
+
site_root.mkdir(parents=True, exist_ok=True)
|
|
376
|
+
version_path = _identifier_path(worktree, config["deploy_prefix"], version)
|
|
377
|
+
_copy_contents(Path(output_dir), version_path)
|
|
378
|
+
store = _load_store(worktree, config["deploy_prefix"])
|
|
379
|
+
title = version
|
|
380
|
+
existing = store.find(version)
|
|
381
|
+
if existing is not None:
|
|
382
|
+
title = existing.title
|
|
383
|
+
store.upsert(version, title=title, aliases=aliases)
|
|
384
|
+
for alias in aliases:
|
|
385
|
+
_write_alias(
|
|
386
|
+
worktree,
|
|
387
|
+
config["deploy_prefix"],
|
|
388
|
+
alias,
|
|
389
|
+
version,
|
|
390
|
+
config["alias_type"],
|
|
391
|
+
config.get("redirect_template"),
|
|
392
|
+
)
|
|
393
|
+
_save_store(worktree, config["deploy_prefix"], store)
|
|
394
|
+
committed = commit_all(
|
|
395
|
+
worktree,
|
|
396
|
+
message or f"Deploy {version}",
|
|
397
|
+
allow_empty=allow_empty,
|
|
398
|
+
)
|
|
399
|
+
if push and committed:
|
|
400
|
+
push_branch(worktree, config["remote"], config["branch"])
|
|
401
|
+
return 0
|
|
402
|
+
except BuilderError as exc:
|
|
403
|
+
raise VersiteError(str(exc)) from exc
|
|
404
|
+
finally:
|
|
405
|
+
if temp_output is not None:
|
|
406
|
+
temp_output.cleanup()
|