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.
Files changed (44) hide show
  1. git_worklog/__init__.py +58 -0
  2. git_worklog/__main__.py +10 -0
  3. git_worklog/analysis/__init__.py +48 -0
  4. git_worklog/analysis/commits.py +118 -0
  5. git_worklog/analysis/coverage.py +149 -0
  6. git_worklog/analysis/history.py +339 -0
  7. git_worklog/analysis/manifest.py +432 -0
  8. git_worklog/analysis/reconcile.py +211 -0
  9. git_worklog/analysis/refs.py +178 -0
  10. git_worklog/analysis/results.py +871 -0
  11. git_worklog/analysis/worktree.py +114 -0
  12. git_worklog/cli/__init__.py +414 -0
  13. git_worklog/cli/analyze.py +340 -0
  14. git_worklog/cli/apply.py +61 -0
  15. git_worklog/cli/coverage.py +62 -0
  16. git_worklog/cli/doctor.py +348 -0
  17. git_worklog/cli/migrate.py +43 -0
  18. git_worklog/cli/models.py +112 -0
  19. git_worklog/cli/preview.py +124 -0
  20. git_worklog/cli/refs.py +55 -0
  21. git_worklog/cli/reindex.py +72 -0
  22. git_worklog/cli/report.py +227 -0
  23. git_worklog/cli/validate.py +252 -0
  24. git_worklog/cli/version.py +47 -0
  25. git_worklog/cli/view.py +218 -0
  26. git_worklog/config.py +79 -0
  27. git_worklog/data/provider_models.json +34 -0
  28. git_worklog/dates.py +237 -0
  29. git_worklog/language.py +295 -0
  30. git_worklog/markers.py +765 -0
  31. git_worklog/mdhtml.py +266 -0
  32. git_worklog/migrate.py +305 -0
  33. git_worklog/paths.py +85 -0
  34. git_worklog/preview.py +776 -0
  35. git_worklog/providers.py +337 -0
  36. git_worklog/viewer.py +1198 -0
  37. git_worklog/viewserver.py +120 -0
  38. git_worklog/writer.py +420 -0
  39. git_worklog-1.6.0.dist-info/METADATA +352 -0
  40. git_worklog-1.6.0.dist-info/RECORD +44 -0
  41. git_worklog-1.6.0.dist-info/WHEEL +5 -0
  42. git_worklog-1.6.0.dist-info/entry_points.txt +2 -0
  43. git_worklog-1.6.0.dist-info/licenses/LICENSE +21 -0
  44. git_worklog-1.6.0.dist-info/top_level.txt +1 -0
@@ -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
+ )