git-worklog 1.6.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.
- git_worklog/__init__.py +58 -0
- git_worklog/__main__.py +10 -0
- git_worklog/analysis/__init__.py +48 -0
- git_worklog/analysis/commits.py +118 -0
- git_worklog/analysis/coverage.py +149 -0
- git_worklog/analysis/history.py +339 -0
- git_worklog/analysis/manifest.py +432 -0
- git_worklog/analysis/reconcile.py +211 -0
- git_worklog/analysis/refs.py +178 -0
- git_worklog/analysis/results.py +871 -0
- git_worklog/analysis/worktree.py +114 -0
- git_worklog/cli/__init__.py +414 -0
- git_worklog/cli/analyze.py +340 -0
- git_worklog/cli/apply.py +61 -0
- git_worklog/cli/coverage.py +62 -0
- git_worklog/cli/doctor.py +348 -0
- git_worklog/cli/migrate.py +43 -0
- git_worklog/cli/models.py +112 -0
- git_worklog/cli/preview.py +124 -0
- git_worklog/cli/refs.py +55 -0
- git_worklog/cli/reindex.py +72 -0
- git_worklog/cli/report.py +227 -0
- git_worklog/cli/validate.py +252 -0
- git_worklog/cli/version.py +47 -0
- git_worklog/cli/view.py +218 -0
- git_worklog/config.py +79 -0
- git_worklog/data/provider_models.json +34 -0
- git_worklog/dates.py +237 -0
- git_worklog/language.py +295 -0
- git_worklog/markers.py +765 -0
- git_worklog/mdhtml.py +266 -0
- git_worklog/migrate.py +305 -0
- git_worklog/paths.py +85 -0
- git_worklog/preview.py +776 -0
- git_worklog/providers.py +337 -0
- git_worklog/viewer.py +1198 -0
- git_worklog/viewserver.py +120 -0
- git_worklog/writer.py +420 -0
- git_worklog-1.6.0.dist-info/METADATA +352 -0
- git_worklog-1.6.0.dist-info/RECORD +44 -0
- git_worklog-1.6.0.dist-info/WHEEL +5 -0
- git_worklog-1.6.0.dist-info/entry_points.txt +2 -0
- git_worklog-1.6.0.dist-info/licenses/LICENSE +21 -0
- git_worklog-1.6.0.dist-info/top_level.txt +1 -0
git_worklog/language.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""BCP 47 language tags, and how a run decides which language to write in.
|
|
2
|
+
|
|
3
|
+
Roadmap §6.2. The rule this module exists to enforce: *source data never
|
|
4
|
+
chooses the output language*. A repository full of English commit messages,
|
|
5
|
+
English identifiers, English comments and an English README still produces a
|
|
6
|
+
``zh-TW`` worklog when the person asking wants ``zh-TW``. Nothing here reads
|
|
7
|
+
repository content, and nothing downstream may infer a language from it.
|
|
8
|
+
|
|
9
|
+
Resolution order (§6.2.1), highest priority first::
|
|
10
|
+
|
|
11
|
+
user request → CLI argument → project config → agent host
|
|
12
|
+
→ conversation → environment → system locale → en
|
|
13
|
+
|
|
14
|
+
The user-request, agent-host and conversation tiers are not observable from a
|
|
15
|
+
process: only the agent hosting the run knows what was asked for, or what
|
|
16
|
+
language the conversation is in. So the agent resolves those tiers itself and
|
|
17
|
+
passes the answer down with ``--language``, declaring where it came from with
|
|
18
|
+
``--language-source`` (§6.2.5). This module resolves only the tiers a process
|
|
19
|
+
can actually see — config, environment, locale — and records the source
|
|
20
|
+
faithfully either way, so the manifest says *why* a language was chosen and not
|
|
21
|
+
merely which one.
|
|
22
|
+
|
|
23
|
+
Deliberately not implemented: RFC 5646 extensions (``-u-``/``-t-``) and
|
|
24
|
+
private-use subtags. The accepted grammar is language[-script][-region][-variant],
|
|
25
|
+
which covers every tag §6.2.4 contemplates. An extension-bearing tag is
|
|
26
|
+
rejected rather than silently truncated.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
|
|
35
|
+
# The language of last resort (§6.2.1, §6.2.14). Reaching it is a warning, not
|
|
36
|
+
# a silent default: an agent that lands here is expected to re-run with an
|
|
37
|
+
# explicit --language rather than accept English.
|
|
38
|
+
FALLBACK = "en"
|
|
39
|
+
|
|
40
|
+
# Where a resolved language came from (§6.2.8). The first three are only ever
|
|
41
|
+
# reported by an agent passing --language-source; the rest this module sets.
|
|
42
|
+
SOURCES = (
|
|
43
|
+
"user-request",
|
|
44
|
+
"cli-argument",
|
|
45
|
+
"project-config",
|
|
46
|
+
"environment",
|
|
47
|
+
"agent-host",
|
|
48
|
+
"conversation",
|
|
49
|
+
"system-locale",
|
|
50
|
+
"fallback",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# The environment variable for standalone CLI use (§6.2.6). New in the language
|
|
54
|
+
# contract, so unlike GIT_WORKLOG_<HOST>_MODEL there is no legacy name to fall
|
|
55
|
+
# back to.
|
|
56
|
+
LANGUAGE_ENV = "GIT_WORKLOG_LANGUAGE"
|
|
57
|
+
|
|
58
|
+
# Consulted in order for the system-locale tier. Read directly rather than via
|
|
59
|
+
# locale.getdefaultlocale(), which is deprecated from Python 3.11 and reads
|
|
60
|
+
# these same variables anyway.
|
|
61
|
+
_LOCALE_ENV = ("LC_ALL", "LC_MESSAGES", "LANG")
|
|
62
|
+
|
|
63
|
+
# Locale values that carry no language intent — the default in containers and
|
|
64
|
+
# CI (§6.2.5). Treated as absent, not as English.
|
|
65
|
+
_EMPTY_LOCALES = {"C", "POSIX", ""}
|
|
66
|
+
|
|
67
|
+
# "auto" means "nobody has decided yet", in both config and --language. It is
|
|
68
|
+
# the shipped config default (markers.render_config), so on disk it is
|
|
69
|
+
# indistinguishable from a config the user never touched — which is exactly how
|
|
70
|
+
# it must be read.
|
|
71
|
+
AUTO = "auto"
|
|
72
|
+
|
|
73
|
+
# Syntactically valid BCP 47, but too coarse to write in: zh alone does not say
|
|
74
|
+
# Hant or Hans (§6.2.4). Rejected with a message naming the alternatives rather
|
|
75
|
+
# than guessed at.
|
|
76
|
+
_AMBIGUOUS = {
|
|
77
|
+
"zh": ("zh-TW", "zh-CN"),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# The primary subtag is capped at 3 letters: every ISO 639 code is 2 (639-1) or
|
|
81
|
+
# 3 (639-2/3) letters. RFC 5646 also reserves 4-letter and registers 5-8-letter
|
|
82
|
+
# primary subtags, but none are in use, and allowing them would make "chinese"
|
|
83
|
+
# and "garbage" parse as languages — turning a typo or a junk locale into a
|
|
84
|
+
# confidently wrong output language instead of an error.
|
|
85
|
+
_TAG_RE = re.compile(
|
|
86
|
+
r"^([A-Za-z]{2,3})" # language
|
|
87
|
+
r"(?:-([A-Za-z]{4}))?" # script
|
|
88
|
+
r"(?:-([A-Za-z]{2}|[0-9]{3}))?" # region
|
|
89
|
+
r"((?:-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*)$" # variants
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LanguageError(ValueError):
|
|
94
|
+
"""A language value that cannot be honoured, carrying the wire code."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, code: str, message: str, **extra):
|
|
97
|
+
self.code = code
|
|
98
|
+
self.message = message
|
|
99
|
+
self.extra = extra
|
|
100
|
+
super().__init__(message)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def normalize(tag: str) -> str:
|
|
104
|
+
"""Return ``tag`` in canonical BCP 47 casing, or raise LanguageError.
|
|
105
|
+
|
|
106
|
+
Casing is normalised (``ZH-tw`` → ``zh-TW``) because tags are compared for
|
|
107
|
+
equality across a run — manifest against result, preview against apply —
|
|
108
|
+
and a case difference is not a language difference. What is *not* normalised
|
|
109
|
+
away is the distinction between ``zh-TW`` and ``zh-CN``: they are separate
|
|
110
|
+
languages here and comparing equal would be a bug (§21.4).
|
|
111
|
+
"""
|
|
112
|
+
if not isinstance(tag, str):
|
|
113
|
+
raise LanguageError("LANGUAGE_INVALID",
|
|
114
|
+
f"Language must be a string, got {type(tag).__name__}.")
|
|
115
|
+
raw = tag.strip()
|
|
116
|
+
if not raw:
|
|
117
|
+
raise LanguageError("LANGUAGE_INVALID", "Language must not be empty.")
|
|
118
|
+
|
|
119
|
+
m = _TAG_RE.match(raw)
|
|
120
|
+
if not m:
|
|
121
|
+
raise LanguageError(
|
|
122
|
+
"LANGUAGE_INVALID",
|
|
123
|
+
f"{raw!r} is not a BCP 47 language tag. Use a tag like 'zh-TW', "
|
|
124
|
+
f"'en' or 'ja' — not a language name.",
|
|
125
|
+
language=raw,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
lang, script, region, variants = m.groups()
|
|
129
|
+
out = lang.lower()
|
|
130
|
+
if script:
|
|
131
|
+
out += "-" + script.title()
|
|
132
|
+
if region:
|
|
133
|
+
out += "-" + region.upper()
|
|
134
|
+
if variants:
|
|
135
|
+
out += "".join("-" + v.lower() for v in variants.split("-") if v)
|
|
136
|
+
|
|
137
|
+
if out in _AMBIGUOUS:
|
|
138
|
+
options = " or ".join(_AMBIGUOUS[out])
|
|
139
|
+
raise LanguageError(
|
|
140
|
+
"LANGUAGE_AMBIGUOUS",
|
|
141
|
+
f"{out!r} does not identify a written language; use {options}.",
|
|
142
|
+
language=out,
|
|
143
|
+
)
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_valid(tag: str) -> bool:
|
|
148
|
+
"""True if ``tag`` is a language this tool will write in."""
|
|
149
|
+
try:
|
|
150
|
+
normalize(tag)
|
|
151
|
+
except LanguageError:
|
|
152
|
+
return False
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _from_locale() -> "str | None":
|
|
157
|
+
"""The system locale as a BCP 47 tag, or None if it carries no intent."""
|
|
158
|
+
for var in _LOCALE_ENV:
|
|
159
|
+
raw = os.environ.get(var)
|
|
160
|
+
if not raw:
|
|
161
|
+
continue
|
|
162
|
+
value = raw.split(".")[0].split("@")[0].strip()
|
|
163
|
+
if value in _EMPTY_LOCALES:
|
|
164
|
+
continue
|
|
165
|
+
try:
|
|
166
|
+
return normalize(value.replace("_", "-"))
|
|
167
|
+
except LanguageError:
|
|
168
|
+
# A locale we cannot read is not an error the user caused; fall
|
|
169
|
+
# through to the next variable, and ultimately to English.
|
|
170
|
+
continue
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class Resolution:
|
|
176
|
+
"""The language decision for one run, and the story of how it was made."""
|
|
177
|
+
|
|
178
|
+
requested: "str | None"
|
|
179
|
+
resolved: str
|
|
180
|
+
source: str
|
|
181
|
+
fallback: str = FALLBACK
|
|
182
|
+
warnings: list = field(default_factory=list)
|
|
183
|
+
|
|
184
|
+
def as_manifest(self) -> dict:
|
|
185
|
+
"""The ``language`` block for a manifest or a payload (§6.2.8)."""
|
|
186
|
+
return {
|
|
187
|
+
"requested": self.requested,
|
|
188
|
+
"resolved": self.resolved,
|
|
189
|
+
"source": self.source,
|
|
190
|
+
"fallback": self.fallback,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def resolve(
|
|
195
|
+
explicit: "str | None" = None,
|
|
196
|
+
source: "str | None" = None,
|
|
197
|
+
config_value: "str | None" = None,
|
|
198
|
+
allow_locale: bool = True,
|
|
199
|
+
) -> Resolution:
|
|
200
|
+
"""Decide the output language for a run (§6.2.1).
|
|
201
|
+
|
|
202
|
+
``explicit`` is ``--language``: either a tag, or ``auto`` / None meaning the
|
|
203
|
+
caller has nothing to say. ``source`` is ``--language-source``, letting an
|
|
204
|
+
agent declare that its tag came from the user rather than from a flag — the
|
|
205
|
+
tiers above ``cli-argument`` exist only in the agent, so without this the
|
|
206
|
+
manifest could not tell a user's explicit request apart from a default.
|
|
207
|
+
|
|
208
|
+
``allow_locale`` is False for agent-hosted runs, where guessing from the
|
|
209
|
+
host OS is wrong (§6.2.5): a remote dev container is fixed to en_US and says
|
|
210
|
+
nothing about what the user wants.
|
|
211
|
+
"""
|
|
212
|
+
if source is not None and source not in SOURCES:
|
|
213
|
+
raise LanguageError(
|
|
214
|
+
"LANGUAGE_SOURCE_INVALID",
|
|
215
|
+
f"{source!r} is not a language source. Expected one of: "
|
|
216
|
+
f"{', '.join(SOURCES)}.",
|
|
217
|
+
source=source,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
requested = explicit if explicit else None
|
|
221
|
+
|
|
222
|
+
if explicit and explicit.strip().lower() != AUTO:
|
|
223
|
+
return Resolution(requested=requested,
|
|
224
|
+
resolved=normalize(explicit),
|
|
225
|
+
source=source or "cli-argument")
|
|
226
|
+
|
|
227
|
+
# An explicit "auto" and an omitted flag mean the same thing — keep looking
|
|
228
|
+
# — but "auto" is worth echoing back as what was requested.
|
|
229
|
+
if config_value and config_value.strip().lower() != AUTO:
|
|
230
|
+
return Resolution(requested=requested or config_value,
|
|
231
|
+
resolved=normalize(config_value),
|
|
232
|
+
source=source or "project-config")
|
|
233
|
+
|
|
234
|
+
env_value = os.environ.get(LANGUAGE_ENV)
|
|
235
|
+
if env_value and env_value.strip():
|
|
236
|
+
return Resolution(requested=requested or env_value,
|
|
237
|
+
resolved=normalize(env_value),
|
|
238
|
+
source=source or "environment")
|
|
239
|
+
|
|
240
|
+
if allow_locale:
|
|
241
|
+
from_locale = _from_locale()
|
|
242
|
+
if from_locale:
|
|
243
|
+
return Resolution(requested=requested,
|
|
244
|
+
resolved=from_locale,
|
|
245
|
+
source=source or "system-locale")
|
|
246
|
+
|
|
247
|
+
return Resolution(
|
|
248
|
+
requested=requested,
|
|
249
|
+
resolved=FALLBACK,
|
|
250
|
+
source="fallback",
|
|
251
|
+
warnings=[{
|
|
252
|
+
"code": "LANGUAGE_NOT_RESOLVED",
|
|
253
|
+
"message": "No output language was provided; falling back to English.",
|
|
254
|
+
"fallback_language": FALLBACK,
|
|
255
|
+
}],
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# --- interface language ------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
# CLI status, error and prompt text (§6.2.13). Phase one ships English only —
|
|
262
|
+
# and that is explicitly allowed — but the flag is honoured rather than ignored:
|
|
263
|
+
# asking for an interface language we do not have gets a warning saying so, not
|
|
264
|
+
# silence that looks like success.
|
|
265
|
+
INTERFACE_SUPPORTED = ("en",)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def resolve_interface(explicit: "str | None" = None) -> Resolution:
|
|
269
|
+
"""Decide the language for the CLI's own messages (§6.2.13).
|
|
270
|
+
|
|
271
|
+
Independent of the content language on purpose: ``--language zh-TW
|
|
272
|
+
--interface-language en`` is a supported combination, and the worklog being
|
|
273
|
+
zh-TW must never drag the CLI's diagnostics along with it, nor the reverse.
|
|
274
|
+
"""
|
|
275
|
+
if not explicit or explicit.strip().lower() == AUTO:
|
|
276
|
+
return Resolution(requested=explicit or None, resolved=FALLBACK,
|
|
277
|
+
source="fallback")
|
|
278
|
+
|
|
279
|
+
tag = normalize(explicit)
|
|
280
|
+
if tag in INTERFACE_SUPPORTED:
|
|
281
|
+
return Resolution(requested=explicit, resolved=tag, source="cli-argument")
|
|
282
|
+
|
|
283
|
+
return Resolution(
|
|
284
|
+
requested=explicit,
|
|
285
|
+
resolved=FALLBACK,
|
|
286
|
+
source="fallback",
|
|
287
|
+
warnings=[{
|
|
288
|
+
"code": "INTERFACE_LANGUAGE_NOT_SUPPORTED",
|
|
289
|
+
"message": (f"Interface messages are not available in {tag!r} yet; "
|
|
290
|
+
f"using {FALLBACK}. This does not affect worklog "
|
|
291
|
+
f"content language."),
|
|
292
|
+
"requested_language": tag,
|
|
293
|
+
"fallback_language": FALLBACK,
|
|
294
|
+
}],
|
|
295
|
+
)
|