sandling 0.1.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.
- sandling-0.1.0/LICENSE +21 -0
- sandling-0.1.0/PKG-INFO +137 -0
- sandling-0.1.0/README.md +119 -0
- sandling-0.1.0/pyproject.toml +34 -0
- sandling-0.1.0/setup.cfg +4 -0
- sandling-0.1.0/src/sandling/__init__.py +3 -0
- sandling-0.1.0/src/sandling/__main__.py +3 -0
- sandling-0.1.0/src/sandling/cli.py +190 -0
- sandling-0.1.0/src/sandling/service.py +491 -0
- sandling-0.1.0/src/sandling.egg-info/PKG-INFO +137 -0
- sandling-0.1.0/src/sandling.egg-info/SOURCES.txt +16 -0
- sandling-0.1.0/src/sandling.egg-info/dependency_links.txt +1 -0
- sandling-0.1.0/src/sandling.egg-info/entry_points.txt +2 -0
- sandling-0.1.0/src/sandling.egg-info/requires.txt +5 -0
- sandling-0.1.0/src/sandling.egg-info/top_level.txt +1 -0
- sandling-0.1.0/tests/test_cli.py +17 -0
- sandling-0.1.0/tests/test_service.py +217 -0
- sandling-0.1.0/tests/test_shell_integration.py +10 -0
sandling-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Camilla Buys
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sandling-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sandling
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Safe, disposable Git workspaces with fewer abandoned clones.
|
|
5
|
+
Author-email: Camilla Buys <camilla.buys@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: Operating System :: POSIX
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff>=0.9; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Sandling
|
|
20
|
+
|
|
21
|
+
> **Small, temporary Git workspaces that know when to leave.** A Sandling is for
|
|
22
|
+
> debugging, building, and pushing—not becoming an archaeological layer in your home directory.
|
|
23
|
+
|
|
24
|
+
Sandling creates an isolated clone for a configured project, selects exactly the branch you ask
|
|
25
|
+
for, runs explicit bootstrap commands, and can remind you when the workspace is ready to retire.
|
|
26
|
+
It never deletes automatically. Sandboxes are disposable; your work is not.
|
|
27
|
+
|
|
28
|
+
## What it does
|
|
29
|
+
|
|
30
|
+
- Manages one or more explicit project profiles. A new install has no repository configured.
|
|
31
|
+
- Creates workspaces from a freshly fetched base branch or an existing remote branch unchanged.
|
|
32
|
+
- Runs profile bootstrap steps as argument arrays, never through a shell.
|
|
33
|
+
- Optionally launches your configured editor.
|
|
34
|
+
- Installs a local post-push reminder. It can mark a workspace ready; it cannot delete one.
|
|
35
|
+
- Refuses ordinary destruction when a workspace is dirty, contains untracked files, has unpushed
|
|
36
|
+
commits, or lacks a usable upstream.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
Sandling supports Linux and macOS with Python 3.11 or later.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pipx install sandling
|
|
44
|
+
sandling --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Upgrade a published installation with `pipx upgrade sandling`. Sandling does not try to update
|
|
48
|
+
itself from a random checkout; that is how tools acquire hobbies.
|
|
49
|
+
|
|
50
|
+
For local development:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python -m pip install -e '.[dev]'
|
|
54
|
+
python -m pytest
|
|
55
|
+
python -m ruff check src tests
|
|
56
|
+
python -m ruff format --check src tests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configure a project
|
|
60
|
+
|
|
61
|
+
Create a profile for any Git repository:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
sandling profile init demo --repository-url https://example.test/demo.git --base-branch main
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This creates `~/.config/sandling/config.toml` (or `$XDG_CONFIG_HOME/sandling/config.toml`). Add
|
|
68
|
+
optional bootstrap and editor commands as shell-free argument arrays:
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[profiles.demo]
|
|
72
|
+
repository_url = "https://example.test/demo.git"
|
|
73
|
+
base_branch = "main"
|
|
74
|
+
editor_command = ["code", "."]
|
|
75
|
+
bootstrap = [
|
|
76
|
+
["python3", "-m", "venv", ".venv"],
|
|
77
|
+
["{sandbox}/.venv/bin/python", "-m", "pip", "install", "-r", "requirements.txt"],
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Recognised placeholders are `{sandbox}`, `{profile}`, and `{branch}`. The profile is your
|
|
82
|
+
configuration; review it with the same calm suspicion you reserve for shell aliases copied from
|
|
83
|
+
the internet.
|
|
84
|
+
|
|
85
|
+
## Start work
|
|
86
|
+
|
|
87
|
+
Create a new branch from the profile's freshly fetched base branch:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
sandling start demo --new-branch fix/parse-dates
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or check out an existing remote branch without merging or rebasing it:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
sandling start demo fix/parse-dates
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
List known workspaces and mark one after its push is accepted:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
sandling list
|
|
103
|
+
sandling flag fix_parse_dates-20261110-090000
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
In an interactive Bash shell, source `scripts/sandling-shell.bash` if you want successful
|
|
107
|
+
`sandling start` calls to enter the newly created workspace. A Python process cannot move its
|
|
108
|
+
parent shell; physics is rude but consistent.
|
|
109
|
+
|
|
110
|
+
## Safe cleanup
|
|
111
|
+
|
|
112
|
+
Destroying requires an exact-name confirmation:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
sandling destroy fix_parse_dates-20261110-090000
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Normal destruction refuses dirty, untracked, unpushed, and upstream-less workspaces. `--force`
|
|
119
|
+
bypasses those Git-state checks but still requires the exact workspace name. Use it slowly, after
|
|
120
|
+
checking the path, and ideally after coffee.
|
|
121
|
+
|
|
122
|
+
Destroyed-workspace metadata remains for seven days. `sandling prune-metadata` removes only those
|
|
123
|
+
expired records; it never removes a workspace directory. There is deliberately no clean-all and no
|
|
124
|
+
remote-branch deletion command.
|
|
125
|
+
|
|
126
|
+
## Security and scope
|
|
127
|
+
|
|
128
|
+
Sandling runs only the commands you place in a profile and never invokes a shell for bootstrap
|
|
129
|
+
steps. It does not manage credentials, install system dependencies, configure project services, or
|
|
130
|
+
make remote Git changes other than normal user-initiated pushes from a workspace.
|
|
131
|
+
|
|
132
|
+
Report vulnerabilities through the process in [SECURITY.md](SECURITY.md). Contributions are
|
|
133
|
+
welcome; see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
Created by Camilla Buys. Build fast. Push clean. Leave no clones behind.
|
sandling-0.1.0/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Sandling
|
|
2
|
+
|
|
3
|
+
> **Small, temporary Git workspaces that know when to leave.** A Sandling is for
|
|
4
|
+
> debugging, building, and pushing—not becoming an archaeological layer in your home directory.
|
|
5
|
+
|
|
6
|
+
Sandling creates an isolated clone for a configured project, selects exactly the branch you ask
|
|
7
|
+
for, runs explicit bootstrap commands, and can remind you when the workspace is ready to retire.
|
|
8
|
+
It never deletes automatically. Sandboxes are disposable; your work is not.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
- Manages one or more explicit project profiles. A new install has no repository configured.
|
|
13
|
+
- Creates workspaces from a freshly fetched base branch or an existing remote branch unchanged.
|
|
14
|
+
- Runs profile bootstrap steps as argument arrays, never through a shell.
|
|
15
|
+
- Optionally launches your configured editor.
|
|
16
|
+
- Installs a local post-push reminder. It can mark a workspace ready; it cannot delete one.
|
|
17
|
+
- Refuses ordinary destruction when a workspace is dirty, contains untracked files, has unpushed
|
|
18
|
+
commits, or lacks a usable upstream.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Sandling supports Linux and macOS with Python 3.11 or later.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pipx install sandling
|
|
26
|
+
sandling --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Upgrade a published installation with `pipx upgrade sandling`. Sandling does not try to update
|
|
30
|
+
itself from a random checkout; that is how tools acquire hobbies.
|
|
31
|
+
|
|
32
|
+
For local development:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m pip install -e '.[dev]'
|
|
36
|
+
python -m pytest
|
|
37
|
+
python -m ruff check src tests
|
|
38
|
+
python -m ruff format --check src tests
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Configure a project
|
|
42
|
+
|
|
43
|
+
Create a profile for any Git repository:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
sandling profile init demo --repository-url https://example.test/demo.git --base-branch main
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This creates `~/.config/sandling/config.toml` (or `$XDG_CONFIG_HOME/sandling/config.toml`). Add
|
|
50
|
+
optional bootstrap and editor commands as shell-free argument arrays:
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
[profiles.demo]
|
|
54
|
+
repository_url = "https://example.test/demo.git"
|
|
55
|
+
base_branch = "main"
|
|
56
|
+
editor_command = ["code", "."]
|
|
57
|
+
bootstrap = [
|
|
58
|
+
["python3", "-m", "venv", ".venv"],
|
|
59
|
+
["{sandbox}/.venv/bin/python", "-m", "pip", "install", "-r", "requirements.txt"],
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Recognised placeholders are `{sandbox}`, `{profile}`, and `{branch}`. The profile is your
|
|
64
|
+
configuration; review it with the same calm suspicion you reserve for shell aliases copied from
|
|
65
|
+
the internet.
|
|
66
|
+
|
|
67
|
+
## Start work
|
|
68
|
+
|
|
69
|
+
Create a new branch from the profile's freshly fetched base branch:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
sandling start demo --new-branch fix/parse-dates
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or check out an existing remote branch without merging or rebasing it:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
sandling start demo fix/parse-dates
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
List known workspaces and mark one after its push is accepted:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
sandling list
|
|
85
|
+
sandling flag fix_parse_dates-20261110-090000
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
In an interactive Bash shell, source `scripts/sandling-shell.bash` if you want successful
|
|
89
|
+
`sandling start` calls to enter the newly created workspace. A Python process cannot move its
|
|
90
|
+
parent shell; physics is rude but consistent.
|
|
91
|
+
|
|
92
|
+
## Safe cleanup
|
|
93
|
+
|
|
94
|
+
Destroying requires an exact-name confirmation:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
sandling destroy fix_parse_dates-20261110-090000
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Normal destruction refuses dirty, untracked, unpushed, and upstream-less workspaces. `--force`
|
|
101
|
+
bypasses those Git-state checks but still requires the exact workspace name. Use it slowly, after
|
|
102
|
+
checking the path, and ideally after coffee.
|
|
103
|
+
|
|
104
|
+
Destroyed-workspace metadata remains for seven days. `sandling prune-metadata` removes only those
|
|
105
|
+
expired records; it never removes a workspace directory. There is deliberately no clean-all and no
|
|
106
|
+
remote-branch deletion command.
|
|
107
|
+
|
|
108
|
+
## Security and scope
|
|
109
|
+
|
|
110
|
+
Sandling runs only the commands you place in a profile and never invokes a shell for bootstrap
|
|
111
|
+
steps. It does not manage credentials, install system dependencies, configure project services, or
|
|
112
|
+
make remote Git changes other than normal user-initiated pushes from a workspace.
|
|
113
|
+
|
|
114
|
+
Report vulnerabilities through the process in [SECURITY.md](SECURITY.md). Contributions are
|
|
115
|
+
welcome; see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
Created by Camilla Buys. Build fast. Push clean. Leave no clones behind.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sandling"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Safe, disposable Git workspaces with fewer abandoned clones."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Camilla Buys", email = "camilla.buys@gmail.com" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Operating System :: POSIX",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
sandling = "sandling.cli:main"
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = ["build>=1.2", "pytest>=8.0", "ruff>=0.9"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
27
|
+
addopts = "-q"
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
target-version = "py311"
|
|
31
|
+
line-length = 100
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint]
|
|
34
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
sandling-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from sandling.service import (
|
|
8
|
+
BootstrapError,
|
|
9
|
+
SandlingError,
|
|
10
|
+
destroy_workspace,
|
|
11
|
+
get_profile,
|
|
12
|
+
init_profile,
|
|
13
|
+
load_all_records,
|
|
14
|
+
load_config,
|
|
15
|
+
mark_ready,
|
|
16
|
+
post_push,
|
|
17
|
+
prune_metadata,
|
|
18
|
+
record_git_summary,
|
|
19
|
+
start_workspace,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
prog="sandling",
|
|
26
|
+
description="Safe, disposable Git workspaces. Temporary by design, not by neglect.",
|
|
27
|
+
)
|
|
28
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
29
|
+
|
|
30
|
+
profile = commands.add_parser("profile", help="create a configured project profile")
|
|
31
|
+
profile_commands = profile.add_subparsers(dest="profile_command", required=True)
|
|
32
|
+
profile_init = profile_commands.add_parser("init", help="add a minimal project profile")
|
|
33
|
+
profile_init.add_argument("name")
|
|
34
|
+
profile_init.add_argument("--repository-url", required=True)
|
|
35
|
+
profile_init.add_argument("--base-branch", default="main")
|
|
36
|
+
|
|
37
|
+
start = commands.add_parser("start", help="clone, select a branch, and bootstrap a workspace")
|
|
38
|
+
start.add_argument("profile", help="configured profile name")
|
|
39
|
+
start.add_argument("branch", nargs="?", help="existing remote branch to check out unchanged")
|
|
40
|
+
start.add_argument(
|
|
41
|
+
"--new-branch", metavar="BRANCH", help="create BRANCH from the configured base branch"
|
|
42
|
+
)
|
|
43
|
+
start.add_argument(
|
|
44
|
+
"--no-editor", action="store_true", help="do not launch the profile editor command"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
listed = commands.add_parser("list", help="list known workspaces and their safety state")
|
|
48
|
+
listed.add_argument("profile", nargs="?", help="show only one profile")
|
|
49
|
+
|
|
50
|
+
flag = commands.add_parser("flag", help="mark a workspace ready for later destruction")
|
|
51
|
+
flag.add_argument("name", nargs="?", help="workspace name; defaults to the current workspace")
|
|
52
|
+
|
|
53
|
+
destroy = commands.add_parser("destroy", help="delete one safe, explicitly confirmed workspace")
|
|
54
|
+
destroy.add_argument("name", help="workspace name from 'sandling list'")
|
|
55
|
+
destroy.add_argument(
|
|
56
|
+
"--force", action="store_true", help="bypass Git-state safeguards; still types name"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
commands.add_parser(
|
|
60
|
+
"prune-metadata", help="remove destroyed-workspace records older than seven days"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
hook = commands.add_parser("_post-push", help=argparse.SUPPRESS)
|
|
64
|
+
hook.add_argument("--workspace", required=True)
|
|
65
|
+
hook.add_argument("--remote", required=True)
|
|
66
|
+
return parser
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def prompt_after_push() -> bool:
|
|
70
|
+
message = "\nSandling: push accepted. Mark this workspace ready for destruction? [y/N] "
|
|
71
|
+
try:
|
|
72
|
+
with Path("/dev/tty").open("r+", encoding="utf-8") as tty:
|
|
73
|
+
tty.write(message)
|
|
74
|
+
tty.flush()
|
|
75
|
+
return tty.readline().strip().lower() in {"y", "yes"}
|
|
76
|
+
except OSError:
|
|
77
|
+
print(
|
|
78
|
+
"Sandling: push accepted. Run 'sandling flag' when the workspace is expendable.",
|
|
79
|
+
file=sys.stderr,
|
|
80
|
+
)
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def print_records(profile_name: str | None) -> None:
|
|
85
|
+
config = load_config()
|
|
86
|
+
records = [
|
|
87
|
+
record
|
|
88
|
+
for record in load_all_records(config)
|
|
89
|
+
if not profile_name or record.profile == profile_name
|
|
90
|
+
]
|
|
91
|
+
if not records:
|
|
92
|
+
print("No Sandling workspaces recorded. An empty beach is not a crisis.")
|
|
93
|
+
return
|
|
94
|
+
print(f"{'NAME':<42} {'PROFILE':<18} {'STATE':<12} {'BRANCH':<30} PATH")
|
|
95
|
+
for record in records:
|
|
96
|
+
state, _ = record_git_summary(record)
|
|
97
|
+
ready = " ready" if record.ready_for_destruction_at else ""
|
|
98
|
+
row = f"{record.name:<42} {record.profile:<18} {(state + ready):<12}"
|
|
99
|
+
print(f"{row} {record.branch:<30} {record.path}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def confirm_destruction(name: str) -> bool:
|
|
103
|
+
return input(f"Type '{name}' to destroy that workspace directory: ").strip() == name
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def handle_start(args: argparse.Namespace) -> None:
|
|
107
|
+
if bool(args.branch) == bool(args.new_branch):
|
|
108
|
+
raise SandlingError(
|
|
109
|
+
"Use exactly one: 'sandling start PROFILE BRANCH' or '--new-branch BRANCH'."
|
|
110
|
+
)
|
|
111
|
+
config = load_config()
|
|
112
|
+
profile = get_profile(config, args.profile)
|
|
113
|
+
branch = args.new_branch or args.branch
|
|
114
|
+
record = start_workspace(
|
|
115
|
+
config,
|
|
116
|
+
profile,
|
|
117
|
+
branch=branch,
|
|
118
|
+
create_new_branch=bool(args.new_branch),
|
|
119
|
+
open_editor=not args.no_editor,
|
|
120
|
+
)
|
|
121
|
+
source = (
|
|
122
|
+
f"{profile.push_remote}/{profile.base_branch}"
|
|
123
|
+
if args.new_branch
|
|
124
|
+
else f"{profile.push_remote}/{branch} (unchanged)"
|
|
125
|
+
)
|
|
126
|
+
print(f"Sandling workspace ready: {record.path}\nBranch: {branch}\nSource: {source}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def handle_post_push(args: argparse.Namespace) -> None:
|
|
130
|
+
config = load_config()
|
|
131
|
+
record = post_push(
|
|
132
|
+
config,
|
|
133
|
+
workspace=Path(args.workspace).resolve(),
|
|
134
|
+
remote=args.remote,
|
|
135
|
+
pushed_refs=sys.stdin.read(),
|
|
136
|
+
)
|
|
137
|
+
if record and prompt_after_push():
|
|
138
|
+
mark_ready(config, record.name)
|
|
139
|
+
print(
|
|
140
|
+
f"Sandling: {record.name} is marked ready for destruction. "
|
|
141
|
+
"It will not wander off by itself."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main(argv: list[str] | None = None) -> None:
|
|
146
|
+
args = build_parser().parse_args(argv)
|
|
147
|
+
try:
|
|
148
|
+
if args.command == "profile":
|
|
149
|
+
path = init_profile(args.name, args.repository_url, args.base_branch)
|
|
150
|
+
print(f"Sandling profile {args.name!r} added to {path}.")
|
|
151
|
+
else:
|
|
152
|
+
config = load_config()
|
|
153
|
+
if args.command == "start":
|
|
154
|
+
prune_metadata(config)
|
|
155
|
+
handle_start(args)
|
|
156
|
+
elif args.command == "list":
|
|
157
|
+
prune_metadata(config)
|
|
158
|
+
print_records(args.profile)
|
|
159
|
+
elif args.command == "flag":
|
|
160
|
+
prune_metadata(config)
|
|
161
|
+
record = mark_ready(config, args.name)
|
|
162
|
+
print(
|
|
163
|
+
f"Sandling: {record.name} is marked ready for destruction. "
|
|
164
|
+
"Nothing has been deleted."
|
|
165
|
+
)
|
|
166
|
+
elif args.command == "destroy":
|
|
167
|
+
prune_metadata(config)
|
|
168
|
+
if not confirm_destruction(args.name):
|
|
169
|
+
raise SandlingError("Destruction cancelled: workspace name did not match.")
|
|
170
|
+
record = destroy_workspace(config, args.name, force=args.force)
|
|
171
|
+
print(f"Sandling: destroyed {record.path}. Metadata is retained for seven days.")
|
|
172
|
+
elif args.command == "prune-metadata":
|
|
173
|
+
removed = prune_metadata(config)
|
|
174
|
+
print(
|
|
175
|
+
f"Sandling: removed {removed} expired destroyed-workspace metadata record(s)."
|
|
176
|
+
)
|
|
177
|
+
elif args.command == "_post-push":
|
|
178
|
+
prune_metadata(config)
|
|
179
|
+
handle_post_push(args)
|
|
180
|
+
except BootstrapError as error:
|
|
181
|
+
print(f"Sandling workspace bootstrap failed: {error.workspace}")
|
|
182
|
+
print(f"Sandling: {error}", file=sys.stderr)
|
|
183
|
+
raise SystemExit(2) from error
|
|
184
|
+
except SandlingError as error:
|
|
185
|
+
print(f"Sandling: {error}", file=sys.stderr)
|
|
186
|
+
raise SystemExit(2) from error
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
main()
|