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 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