livery-git 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.
- livery/git/__init__.py +276 -0
- livery/git/_docs/index.md +94 -0
- livery/git/_docs/nav.toml +12 -0
- livery/git/_environment.py +50 -0
- livery/git/_marks.py +34 -0
- livery/git/_objects.py +416 -0
- livery/git/_ops.py +988 -0
- livery/git/_plugin.py +72 -0
- livery/git/_ssh.py +186 -0
- livery/git/_store.py +1506 -0
- livery/git/py.typed +0 -0
- livery_git-0.1.0.dist-info/METADATA +28 -0
- livery_git-0.1.0.dist-info/RECORD +16 -0
- livery_git-0.1.0.dist-info/WHEEL +4 -0
- livery_git-0.1.0.dist-info/entry_points.txt +6 -0
- livery_git-0.1.0.dist-info/licenses/LICENSE +21 -0
livery/git/__init__.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Git access for the livery ecosystem, every call a function of the repository's root.
|
|
2
|
+
|
|
3
|
+
Reach for the named reader or mover first: [livery.git.position][]
|
|
4
|
+
for where a checkout stands, [livery.git.fetch][] and
|
|
5
|
+
[livery.git.push_force][] for the network, [livery.git.commit_all][]
|
|
6
|
+
for a commit. [livery.git.run][] and [livery.git.call][] are the door
|
|
7
|
+
for a subcommand no function names yet: `run` raises
|
|
8
|
+
[livery.git.GitError][] on a non-zero exit, `call` returns the result
|
|
9
|
+
whatever the code.
|
|
10
|
+
|
|
11
|
+
Nothing is held between calls, so a function may be called from any
|
|
12
|
+
process at any time and answers what git holds at that moment.
|
|
13
|
+
|
|
14
|
+
The package is also a footman plugin, `plugin("livery.git")`: its hook
|
|
15
|
+
gives every task's git the ssh command that shares one connection per
|
|
16
|
+
host ([livery.git.ssh_environment][]), the tools' identity when the
|
|
17
|
+
person has none ([livery.git.tool_identity][]), an editor that never
|
|
18
|
+
opens, and what a task marks for itself with [livery.git.identity][]
|
|
19
|
+
and [livery.git.unsigned][].
|
|
20
|
+
|
|
21
|
+
The state store, JSON rows on git refs, is here too: a
|
|
22
|
+
[livery.git.StateStore][] built with a caller's policy, the
|
|
23
|
+
[livery.git.Series][] and [livery.git.Keyed][] declarations that read
|
|
24
|
+
and write through it, and the snapshots that make a run's reads one
|
|
25
|
+
listing and one fetch.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from livery.git._environment import (
|
|
31
|
+
config_environment,
|
|
32
|
+
identity_environment,
|
|
33
|
+
tool_identity,
|
|
34
|
+
)
|
|
35
|
+
from livery.git._marks import identity, unsigned
|
|
36
|
+
from livery.git._ops import (
|
|
37
|
+
GitError,
|
|
38
|
+
add,
|
|
39
|
+
add_remote,
|
|
40
|
+
amend_all,
|
|
41
|
+
any_head,
|
|
42
|
+
author_email,
|
|
43
|
+
behind_base,
|
|
44
|
+
call,
|
|
45
|
+
changed_between,
|
|
46
|
+
changed_paths,
|
|
47
|
+
clean,
|
|
48
|
+
commit_all,
|
|
49
|
+
commit_message,
|
|
50
|
+
commits_since,
|
|
51
|
+
config_get,
|
|
52
|
+
config_regexp,
|
|
53
|
+
config_set,
|
|
54
|
+
config_unset,
|
|
55
|
+
conflicts_with_base,
|
|
56
|
+
create_branch,
|
|
57
|
+
current_branch,
|
|
58
|
+
delete_branch,
|
|
59
|
+
detach,
|
|
60
|
+
dirty_paths,
|
|
61
|
+
fetch,
|
|
62
|
+
fetch_tag,
|
|
63
|
+
fetch_tags,
|
|
64
|
+
file_at,
|
|
65
|
+
files_in_commit,
|
|
66
|
+
first_parent_trees,
|
|
67
|
+
has_commits,
|
|
68
|
+
has_remote,
|
|
69
|
+
head_body,
|
|
70
|
+
head_message,
|
|
71
|
+
head_sha,
|
|
72
|
+
head_subject,
|
|
73
|
+
integrate,
|
|
74
|
+
is_ancestor,
|
|
75
|
+
is_clean,
|
|
76
|
+
last_commit_touching,
|
|
77
|
+
local_branch_exists,
|
|
78
|
+
local_branches,
|
|
79
|
+
log_paths,
|
|
80
|
+
ls_files,
|
|
81
|
+
ls_remote,
|
|
82
|
+
main_checkout,
|
|
83
|
+
merge_base,
|
|
84
|
+
merge_parents,
|
|
85
|
+
merge_tree,
|
|
86
|
+
object_id,
|
|
87
|
+
object_ids,
|
|
88
|
+
oneline,
|
|
89
|
+
position,
|
|
90
|
+
push,
|
|
91
|
+
push_force,
|
|
92
|
+
push_refspec,
|
|
93
|
+
recent_commits,
|
|
94
|
+
remote_branches,
|
|
95
|
+
remote_head,
|
|
96
|
+
remote_tags,
|
|
97
|
+
remote_url,
|
|
98
|
+
remotes,
|
|
99
|
+
reset_hard,
|
|
100
|
+
restore,
|
|
101
|
+
run,
|
|
102
|
+
set_remote_url,
|
|
103
|
+
sha_of,
|
|
104
|
+
staged_text,
|
|
105
|
+
subjects_ahead,
|
|
106
|
+
switch,
|
|
107
|
+
switch_new,
|
|
108
|
+
tag_dates,
|
|
109
|
+
tags,
|
|
110
|
+
tracking_branches,
|
|
111
|
+
tracks,
|
|
112
|
+
tree_diff,
|
|
113
|
+
unmerged_paths,
|
|
114
|
+
unpushed,
|
|
115
|
+
unstage,
|
|
116
|
+
working_tree_id,
|
|
117
|
+
worktree_add,
|
|
118
|
+
worktree_remove,
|
|
119
|
+
)
|
|
120
|
+
from livery.git._ssh import (
|
|
121
|
+
master_up,
|
|
122
|
+
record_unsupported,
|
|
123
|
+
remote_host,
|
|
124
|
+
ssh_command,
|
|
125
|
+
ssh_environment,
|
|
126
|
+
ssh_sockets,
|
|
127
|
+
)
|
|
128
|
+
from livery.git._store import (
|
|
129
|
+
FETCHED,
|
|
130
|
+
FETCHED_NAMESPACE,
|
|
131
|
+
LOCAL_NAMESPACE,
|
|
132
|
+
NAMESPACE,
|
|
133
|
+
RUN_PREFIX,
|
|
134
|
+
TRACE_NAMESPACE,
|
|
135
|
+
WHOLE,
|
|
136
|
+
Fetched,
|
|
137
|
+
Keyed,
|
|
138
|
+
Read,
|
|
139
|
+
Row,
|
|
140
|
+
Rows,
|
|
141
|
+
Series,
|
|
142
|
+
Skipped,
|
|
143
|
+
StateStore,
|
|
144
|
+
drop,
|
|
145
|
+
fetch_store,
|
|
146
|
+
fetched_snapshot,
|
|
147
|
+
list_refs,
|
|
148
|
+
put,
|
|
149
|
+
read,
|
|
150
|
+
remote_snapshot,
|
|
151
|
+
slug,
|
|
152
|
+
sweep,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
__all__ = [
|
|
156
|
+
"FETCHED",
|
|
157
|
+
"FETCHED_NAMESPACE",
|
|
158
|
+
"LOCAL_NAMESPACE",
|
|
159
|
+
"NAMESPACE",
|
|
160
|
+
"RUN_PREFIX",
|
|
161
|
+
"TRACE_NAMESPACE",
|
|
162
|
+
"WHOLE",
|
|
163
|
+
"Fetched",
|
|
164
|
+
"GitError",
|
|
165
|
+
"Keyed",
|
|
166
|
+
"Read",
|
|
167
|
+
"Row",
|
|
168
|
+
"Rows",
|
|
169
|
+
"Series",
|
|
170
|
+
"Skipped",
|
|
171
|
+
"StateStore",
|
|
172
|
+
"__version__",
|
|
173
|
+
"add",
|
|
174
|
+
"add_remote",
|
|
175
|
+
"amend_all",
|
|
176
|
+
"any_head",
|
|
177
|
+
"author_email",
|
|
178
|
+
"behind_base",
|
|
179
|
+
"call",
|
|
180
|
+
"changed_between",
|
|
181
|
+
"changed_paths",
|
|
182
|
+
"clean",
|
|
183
|
+
"commit_all",
|
|
184
|
+
"commit_message",
|
|
185
|
+
"commits_since",
|
|
186
|
+
"config_environment",
|
|
187
|
+
"config_get",
|
|
188
|
+
"config_regexp",
|
|
189
|
+
"config_set",
|
|
190
|
+
"config_unset",
|
|
191
|
+
"conflicts_with_base",
|
|
192
|
+
"create_branch",
|
|
193
|
+
"current_branch",
|
|
194
|
+
"delete_branch",
|
|
195
|
+
"detach",
|
|
196
|
+
"dirty_paths",
|
|
197
|
+
"drop",
|
|
198
|
+
"fetch",
|
|
199
|
+
"fetch_store",
|
|
200
|
+
"fetch_tag",
|
|
201
|
+
"fetch_tags",
|
|
202
|
+
"fetched_snapshot",
|
|
203
|
+
"file_at",
|
|
204
|
+
"files_in_commit",
|
|
205
|
+
"first_parent_trees",
|
|
206
|
+
"has_commits",
|
|
207
|
+
"has_remote",
|
|
208
|
+
"head_body",
|
|
209
|
+
"head_message",
|
|
210
|
+
"head_sha",
|
|
211
|
+
"head_subject",
|
|
212
|
+
"identity",
|
|
213
|
+
"identity_environment",
|
|
214
|
+
"integrate",
|
|
215
|
+
"is_ancestor",
|
|
216
|
+
"is_clean",
|
|
217
|
+
"last_commit_touching",
|
|
218
|
+
"list_refs",
|
|
219
|
+
"local_branch_exists",
|
|
220
|
+
"local_branches",
|
|
221
|
+
"log_paths",
|
|
222
|
+
"ls_files",
|
|
223
|
+
"ls_remote",
|
|
224
|
+
"main_checkout",
|
|
225
|
+
"master_up",
|
|
226
|
+
"merge_base",
|
|
227
|
+
"merge_parents",
|
|
228
|
+
"merge_tree",
|
|
229
|
+
"object_id",
|
|
230
|
+
"object_ids",
|
|
231
|
+
"oneline",
|
|
232
|
+
"position",
|
|
233
|
+
"push",
|
|
234
|
+
"push_force",
|
|
235
|
+
"push_refspec",
|
|
236
|
+
"put",
|
|
237
|
+
"read",
|
|
238
|
+
"recent_commits",
|
|
239
|
+
"record_unsupported",
|
|
240
|
+
"remote_branches",
|
|
241
|
+
"remote_head",
|
|
242
|
+
"remote_host",
|
|
243
|
+
"remote_snapshot",
|
|
244
|
+
"remote_tags",
|
|
245
|
+
"remote_url",
|
|
246
|
+
"remotes",
|
|
247
|
+
"reset_hard",
|
|
248
|
+
"restore",
|
|
249
|
+
"run",
|
|
250
|
+
"set_remote_url",
|
|
251
|
+
"sha_of",
|
|
252
|
+
"slug",
|
|
253
|
+
"ssh_command",
|
|
254
|
+
"ssh_environment",
|
|
255
|
+
"ssh_sockets",
|
|
256
|
+
"staged_text",
|
|
257
|
+
"subjects_ahead",
|
|
258
|
+
"sweep",
|
|
259
|
+
"switch",
|
|
260
|
+
"switch_new",
|
|
261
|
+
"tag_dates",
|
|
262
|
+
"tags",
|
|
263
|
+
"tool_identity",
|
|
264
|
+
"tracking_branches",
|
|
265
|
+
"tracks",
|
|
266
|
+
"tree_diff",
|
|
267
|
+
"unmerged_paths",
|
|
268
|
+
"unpushed",
|
|
269
|
+
"unsigned",
|
|
270
|
+
"unstage",
|
|
271
|
+
"working_tree_id",
|
|
272
|
+
"worktree_add",
|
|
273
|
+
"worktree_remove",
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# livery-git
|
|
2
|
+
|
|
3
|
+
Git access for the livery ecosystem. Every call is a function of the
|
|
4
|
+
repository's root: `git.position(root)` says where a checkout stands,
|
|
5
|
+
`git.fetch(root)` and `git.push_force(root, branch)` reach the network,
|
|
6
|
+
`git.commit_all(root, message)` commits. A reader of refs, objects,
|
|
7
|
+
history or config opens the repository's own files in-process, through
|
|
8
|
+
dulwich, and starts no git; a mover, and a reader of the index, the
|
|
9
|
+
working tree or the remote, runs the git the tool store pins, never one
|
|
10
|
+
found on the path. Every function returns strings or raises `GitError`,
|
|
11
|
+
with git's own words where git ran.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from livery import git
|
|
17
|
+
|
|
18
|
+
root = Path(".")
|
|
19
|
+
branch, head = git.position(root)
|
|
20
|
+
if git.conflicts_with_base(root, "main"):
|
|
21
|
+
raise SystemExit("resolve the conflict first")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Nothing is held between calls. A function answers what git holds at the
|
|
25
|
+
moment it is called, so a flow that needs one fact twice reads it twice,
|
|
26
|
+
and a function that moves the repository says so in its name: `fetch`,
|
|
27
|
+
`push`, `commit_all`, `switch`, `integrate`.
|
|
28
|
+
|
|
29
|
+
`git.run(root, *args)` and `git.call(root, *args)` are the door for a
|
|
30
|
+
subcommand no function names: `run` raises on a non-zero exit, `call`
|
|
31
|
+
returns the result whatever the code, for a caller that reads the exit
|
|
32
|
+
code as an answer.
|
|
33
|
+
|
|
34
|
+
## The plugin
|
|
35
|
+
|
|
36
|
+
`plugin("livery.git")` in a project's `tasks.py` mounts the package as a
|
|
37
|
+
footman plugin; a project that depends on `livery-git` directly gets it
|
|
38
|
+
through footman's project rung, and every workshop project runs with it.
|
|
39
|
+
Its one hook writes into each task's own environment, which the task's
|
|
40
|
+
subprocesses see:
|
|
41
|
+
|
|
42
|
+
- `GIT_SSH_COMMAND`, so every network call shares one ssh connection per
|
|
43
|
+
remote host: a master opens on the first call and lingers ten minutes
|
|
44
|
+
after the last, with its socket under the runner's data directory. A
|
|
45
|
+
`GIT_SSH_COMMAND` or `core.sshCommand` of your own wins, and a file named
|
|
46
|
+
`unsupported` in the socket directory turns it off with the words it
|
|
47
|
+
holds; delete the file to try again. `fm doctor` says which it is.
|
|
48
|
+
- The tools' identity, `<prog>` at `<prog>@<prog>.invalid`, as author and
|
|
49
|
+
committer, when git's config names no `user.email`; a person's own
|
|
50
|
+
identity stands.
|
|
51
|
+
- `GIT_EDITOR=true`, unless one is set already: no verb reads a message
|
|
52
|
+
from an editor.
|
|
53
|
+
|
|
54
|
+
A task marks itself for the cases it knows better: `@git.identity(name,
|
|
55
|
+
email)` commits as that identity whatever the config says, and
|
|
56
|
+
`@git.unsigned` turns commit and tag signing off for the task's git. The
|
|
57
|
+
plugin never turns signing off on its own.
|
|
58
|
+
|
|
59
|
+
Any process of the same user may reuse the open connection for its
|
|
60
|
+
window, without the agent's consent: the same trust boundary as
|
|
61
|
+
`SSH_AUTH_SOCK`.
|
|
62
|
+
|
|
63
|
+
## The state store
|
|
64
|
+
|
|
65
|
+
JSON rows on git refs, in a namespace a default clone or fetch never
|
|
66
|
+
downloads: `refs/workshop/*` on the remote and `refs/workshop-local/*`
|
|
67
|
+
in the checkout's git directory. A `StateStore` is a checkout and the
|
|
68
|
+
policy its builder gave it: whether this process is a CI run, the
|
|
69
|
+
identity every commit carries, and the variable a published listing
|
|
70
|
+
travels in. A `Series` declares one ref's rows and reads and writes
|
|
71
|
+
through the store; a `Keyed` family declares one series per key.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from livery import git
|
|
75
|
+
|
|
76
|
+
store = git.StateStore(
|
|
77
|
+
root, in_ci=False, identity=("fm", "fm@fm.invalid"), snapshot_variable="MY_SNAPSHOT"
|
|
78
|
+
)
|
|
79
|
+
rows = git.Series("metrics", window=300)
|
|
80
|
+
print(rows.rows(store).rows[:3])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A write is a compare-and-swap on the sha the read returned. Only the
|
|
84
|
+
wire is a git process: a remote read is one fetch, a remote write one
|
|
85
|
+
fetch and one push, a local read or write none, whatever the series
|
|
86
|
+
holds, since every tree is read and every commit written in the
|
|
87
|
+
checkout's own object store. A `remote_snapshot` block makes every read
|
|
88
|
+
inside one listing and one fetch. `sweep` is the janitor. Every failure
|
|
89
|
+
is a printed reason, never a boolean.
|
|
90
|
+
|
|
91
|
+
## What is not here
|
|
92
|
+
|
|
93
|
+
Porcelain that waits on a person, a signer, is the caller's to arrange:
|
|
94
|
+
the functions pass git's own words back and never answer a prompt.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# This package's section of the workspace site, hand-authored.
|
|
2
|
+
# Labels map to pages relative to this docs/ tree, or to nested
|
|
3
|
+
# lists. Every authored page must appear here, and every entry must
|
|
4
|
+
# name a real page. A generator owns a block between `# nav:begin
|
|
5
|
+
# <name>` and `# nav:end <name>` markers (tasks, tools); the emitter's
|
|
6
|
+
# own sections, changelog, coverage and api, are blocks too: place a
|
|
7
|
+
# pair where one belongs, and a section without one lands after the
|
|
8
|
+
# authored entries, changelog and coverage ahead of the tasks block,
|
|
9
|
+
# api last.
|
|
10
|
+
nav = [
|
|
11
|
+
{ "Index" = "index.md" },
|
|
12
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""The tools' identity, and git settings that ride the environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def tool_identity(prog: str) -> tuple[str, str]:
|
|
9
|
+
"""The name and address a tool called *prog* commits under.
|
|
10
|
+
|
|
11
|
+
For every commit a verb makes when the person has no identity of
|
|
12
|
+
their own: a release, a loop's scratch, the state store's rows.
|
|
13
|
+
The domain is `.invalid`, reserved for names that resolve nowhere.
|
|
14
|
+
"""
|
|
15
|
+
return prog, f"{prog}@{prog}.invalid"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def identity_environment(name: str, email: str) -> dict[str, str]:
|
|
19
|
+
"""The variables that make every commit and tag by *name* and *email*.
|
|
20
|
+
|
|
21
|
+
Author and committer both: git reads the four variables ahead of
|
|
22
|
+
any config, so a checkout's own identity is left as it is.
|
|
23
|
+
"""
|
|
24
|
+
return {
|
|
25
|
+
"GIT_AUTHOR_NAME": name,
|
|
26
|
+
"GIT_AUTHOR_EMAIL": email,
|
|
27
|
+
"GIT_COMMITTER_NAME": name,
|
|
28
|
+
"GIT_COMMITTER_EMAIL": email,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def config_environment(
|
|
33
|
+
environ: Mapping[str, str], pairs: Sequence[tuple[str, str]]
|
|
34
|
+
) -> dict[str, str]:
|
|
35
|
+
"""*pairs* as git configuration riding the environment, after what it carries.
|
|
36
|
+
|
|
37
|
+
git reads `GIT_CONFIG_COUNT` with a key and a value per index, so an
|
|
38
|
+
entry the outer environment already carries keeps its index and
|
|
39
|
+
these follow it. The mapping returned holds the new entries and the
|
|
40
|
+
count; merge it over *environ*.
|
|
41
|
+
"""
|
|
42
|
+
raw = environ.get("GIT_CONFIG_COUNT", "")
|
|
43
|
+
count = int(raw) if raw.isdigit() else 0
|
|
44
|
+
made: dict[str, str] = {}
|
|
45
|
+
for offset, (key, value) in enumerate(pairs):
|
|
46
|
+
index = count + offset
|
|
47
|
+
made[f"GIT_CONFIG_KEY_{index}"] = key
|
|
48
|
+
made[f"GIT_CONFIG_VALUE_{index}"] = value
|
|
49
|
+
made["GIT_CONFIG_COUNT"] = str(count + len(pairs))
|
|
50
|
+
return made
|
livery/git/_marks.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Marks a task sets on itself for the git plugin: an identity, or unsigned commits.
|
|
2
|
+
|
|
3
|
+
A mark is an attribute on the task's function, written by a decorator
|
|
4
|
+
below or above `@task` and read by the plugin's hook through the
|
|
5
|
+
handle's `task.mark(...)`. The decorators import nothing of footman,
|
|
6
|
+
so a module that only marks its tasks stays importable without it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import TypeVar
|
|
13
|
+
|
|
14
|
+
_F = TypeVar("_F", bound=Callable[..., object])
|
|
15
|
+
|
|
16
|
+
#: The attribute names the plugin's hook reads off a task.
|
|
17
|
+
IDENTITY_MARK = "livery_git_identity"
|
|
18
|
+
UNSIGNED_MARK = "livery_git_unsigned"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def identity(name: str, email: str) -> Callable[[_F], _F]:
|
|
22
|
+
"""Make the decorated task's commits carry *name* and *email*, whatever git says."""
|
|
23
|
+
|
|
24
|
+
def mark(fn: _F) -> _F:
|
|
25
|
+
setattr(fn, IDENTITY_MARK, (name, email))
|
|
26
|
+
return fn
|
|
27
|
+
|
|
28
|
+
return mark
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def unsigned(fn: _F) -> _F:
|
|
32
|
+
"""Turn commit and tag signing off for the decorated task's git."""
|
|
33
|
+
setattr(fn, UNSIGNED_MARK, True)
|
|
34
|
+
return fn
|