workfold 0.1.0a1__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.
Files changed (62) hide show
  1. workfold-0.1.0a1/LICENSE +21 -0
  2. workfold-0.1.0a1/MANIFEST.in +2 -0
  3. workfold-0.1.0a1/PKG-INFO +204 -0
  4. workfold-0.1.0a1/README.md +165 -0
  5. workfold-0.1.0a1/THIRD_PARTY_NOTICES.md +19 -0
  6. workfold-0.1.0a1/pyproject.toml +107 -0
  7. workfold-0.1.0a1/setup.cfg +4 -0
  8. workfold-0.1.0a1/tests/support/__init__.py +1 -0
  9. workfold-0.1.0a1/tests/support/git_repo.py +130 -0
  10. workfold-0.1.0a1/tests/test_aggregation.py +274 -0
  11. workfold-0.1.0a1/tests/test_application.py +409 -0
  12. workfold-0.1.0a1/tests/test_cli.py +210 -0
  13. workfold-0.1.0a1/tests/test_config.py +323 -0
  14. workfold-0.1.0a1/tests/test_coverage.py +195 -0
  15. workfold-0.1.0a1/tests/test_end_to_end.py +439 -0
  16. workfold-0.1.0a1/tests/test_filesystem_collector.py +719 -0
  17. workfold-0.1.0a1/tests/test_filesystem_ignores.py +384 -0
  18. workfold-0.1.0a1/tests/test_filesystem_times.py +275 -0
  19. workfold-0.1.0a1/tests/test_git_changes.py +415 -0
  20. workfold-0.1.0a1/tests/test_git_collector.py +633 -0
  21. workfold-0.1.0a1/tests/test_git_objects.py +142 -0
  22. workfold-0.1.0a1/tests/test_git_reflogs.py +629 -0
  23. workfold-0.1.0a1/tests/test_git_tags.py +437 -0
  24. workfold-0.1.0a1/tests/test_linux_statx.py +70 -0
  25. workfold-0.1.0a1/tests/test_models.py +241 -0
  26. workfold-0.1.0a1/tests/test_package.py +14 -0
  27. workfold-0.1.0a1/tests/test_schedule.py +152 -0
  28. workfold-0.1.0a1/tests/test_terminal_renderer.py +382 -0
  29. workfold-0.1.0a1/tests/test_time_ranges.py +251 -0
  30. workfold-0.1.0a1/workfold/__init__.py +10 -0
  31. workfold-0.1.0a1/workfold/__main__.py +5 -0
  32. workfold-0.1.0a1/workfold/aggregation.py +394 -0
  33. workfold-0.1.0a1/workfold/application.py +1022 -0
  34. workfold-0.1.0a1/workfold/cli.py +204 -0
  35. workfold-0.1.0a1/workfold/collectors/__init__.py +54 -0
  36. workfold-0.1.0a1/workfold/collectors/base.py +41 -0
  37. workfold-0.1.0a1/workfold/collectors/filesystem.py +858 -0
  38. workfold-0.1.0a1/workfold/collectors/filesystem_times.py +379 -0
  39. workfold-0.1.0a1/workfold/collectors/git.py +788 -0
  40. workfold-0.1.0a1/workfold/collectors/git_changes.py +495 -0
  41. workfold-0.1.0a1/workfold/collectors/git_objects.py +370 -0
  42. workfold-0.1.0a1/workfold/collectors/git_reflogs.py +649 -0
  43. workfold-0.1.0a1/workfold/collectors/git_tags.py +549 -0
  44. workfold-0.1.0a1/workfold/collectors/ignores.py +621 -0
  45. workfold-0.1.0a1/workfold/collectors/linux_statx.py +145 -0
  46. workfold-0.1.0a1/workfold/config.py +505 -0
  47. workfold-0.1.0a1/workfold/coverage.py +553 -0
  48. workfold-0.1.0a1/workfold/models.py +363 -0
  49. workfold-0.1.0a1/workfold/provenance.py +187 -0
  50. workfold-0.1.0a1/workfold/py.typed +1 -0
  51. workfold-0.1.0a1/workfold/renderers/__init__.py +5 -0
  52. workfold-0.1.0a1/workfold/renderers/terminal.py +621 -0
  53. workfold-0.1.0a1/workfold/reports.py +101 -0
  54. workfold-0.1.0a1/workfold/sanitization.py +143 -0
  55. workfold-0.1.0a1/workfold/schedule.py +211 -0
  56. workfold-0.1.0a1/workfold/time_ranges.py +316 -0
  57. workfold-0.1.0a1/workfold.egg-info/PKG-INFO +204 -0
  58. workfold-0.1.0a1/workfold.egg-info/SOURCES.txt +60 -0
  59. workfold-0.1.0a1/workfold.egg-info/dependency_links.txt +1 -0
  60. workfold-0.1.0a1/workfold.egg-info/entry_points.txt +2 -0
  61. workfold-0.1.0a1/workfold.egg-info/requires.txt +18 -0
  62. workfold-0.1.0a1/workfold.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jan T. Müller
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.
@@ -0,0 +1,2 @@
1
+ recursive-include tests *.py
2
+ include THIRD_PARTY_NOTICES.md
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: workfold
3
+ Version: 0.1.0a1
4
+ Summary: Fold local Git and filesystem timestamp activity onto a representative week.
5
+ Author-email: "Jan T. Müller" <mail@jantmueller.com>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://github.com/janthmueller/workfold
8
+ Project-URL: documentation, https://janthmueller.github.io/workfold/
9
+ Project-URL: source, https://github.com/janthmueller/workfold
10
+ Project-URL: tracker, https://github.com/janthmueller/workfold/issues
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: pathspec<2,>=1
25
+ Requires-Dist: rich<15,>=14
26
+ Requires-Dist: tzdata>=2025.2; sys_platform == "win32"
27
+ Requires-Dist: tzlocal<6,>=5
28
+ Provides-Extra: dev
29
+ Requires-Dist: pyright>=1.1; extra == "dev"
30
+ Requires-Dist: pytest>=8; extra == "dev"
31
+ Requires-Dist: pytest-cov>=6; extra == "dev"
32
+ Requires-Dist: ruff>=0.8; extra == "dev"
33
+ Provides-Extra: release
34
+ Requires-Dist: build>=1; extra == "release"
35
+ Requires-Dist: pyinstaller>=6; extra == "release"
36
+ Requires-Dist: python-semantic-release<11,>=10; extra == "release"
37
+ Requires-Dist: twine>=5; extra == "release"
38
+ Dynamic: license-file
39
+
40
+ # Workfold
41
+
42
+ Workfold is a private, local CLI that folds timestamped activity from Git and
43
+ filesystem metadata onto one representative Monday-to-Sunday week. It makes the
44
+ shape of recorded activity easy to scan and marks events outside a configured
45
+ working schedule without pretending to measure hours worked.
46
+
47
+ ```text
48
+ Time Mon Tue Wed Thu Fri Sat Sun
49
+ 07:48–08:12 ○●■ ●
50
+ ⋮ 1h 8m
51
+ 09:20–09:29 ■●
52
+ ```
53
+
54
+ Terminal output starts directly with this matrix, followed by a content-aware
55
+ symbol key and three direct statistic rows: total events, the inside/outside
56
+ schedule split, and the weekday/weekend calendar split. There is no summary
57
+ heading. The key has no heading and names only categories actually
58
+ visible in the matrix; its `×N` explanation appears only when a cell uses exact
59
+ count notation. The configured working hours follow on their own left-aligned
60
+ line. Schedule and calendar percentages each use the total event count as their
61
+ denominator; weekend activity may also be outside the configured schedule. A
62
+ normal complete run stops there.
63
+ `--verbose` adds scope, period, the full successful coverage status, operational
64
+ metadata, and the detailed coverage ledger. Use `--coverage` when the ledger is
65
+ wanted without those verbose details. Partial collection, explicit narrowing,
66
+ unsupported capabilities, and nonzero display cropping remain visible by
67
+ default as exception notices; compact output never hides those limitations.
68
+
69
+ Each ordinary symbol is one activity marker. Circles are Git and squares are
70
+ filesystem evidence; filled shapes are inside working hours and hollow red
71
+ shapes are outside. Git-inside markers are green and filesystem-inside markers
72
+ are blue. Shapes preserve the same meaning with `--no-color`. Rows are greedy,
73
+ globally aligned time clusters (one hour by default), so empty time disappears
74
+ without losing the order expressed by the labels.
75
+
76
+ ## Status
77
+
78
+ Workfold is an alpha, terminal-only CLI with its sparse terminal renderer and
79
+ local collection pipeline implemented end to end. Git commits, per-file
80
+ changes, annotated tags, local
81
+ reflogs, filesystem metadata, combined-source views, the `full` profile, strict
82
+ coverage accounting, and outside-event listing share the same normalized
83
+ pipeline.
84
+
85
+ The [documentation](https://janthmueller.github.io/workfold/) covers CLI usage,
86
+ accuracy, and development. The Astro site in `docs/` documents the terminal
87
+ product; it is not an HTML reporting feature.
88
+
89
+ ## Install
90
+
91
+ Workfold requires Python 3.10 or newer. Git collection uses the installed
92
+ `git` executable and never contacts a remote.
93
+
94
+ Install Workfold from PyPI:
95
+
96
+ ```bash
97
+ pip install workfold
98
+ workfold --help
99
+ ```
100
+
101
+ For an isolated CLI environment, `uv tool install workfold` and
102
+ `pipx install workfold` are supported alternatives. Standalone archives for
103
+ supported platforms are attached to
104
+ [GitHub releases](https://github.com/janthmueller/workfold/releases).
105
+
106
+ ## Quick Git view
107
+
108
+ Run inside a repository to fold the current ISO week's author timestamps from
109
+ commits reachable through all local refs:
110
+
111
+ ```bash
112
+ workfold
113
+ ```
114
+
115
+ This uses the default selectors `-t this-week -m git -p standard`.
116
+ `-t/--time` also accepts an ISO week, repeated ISO weeks, an inclusive
117
+ `DATE..DATE` range with either endpoint open, or `all`. `-m/--mode` accepts
118
+ `git`, `fs`, or `all`; changing time never changes mode and vice versa.
119
+
120
+ Common selections include:
121
+
122
+ ```bash
123
+ workfold . -t 2026-W31
124
+ workfold . -t 2026-W30 -t 2026-W31
125
+ workfold . -t 2026-07-01..2026-07-31
126
+ workfold . -t 2026-01-01..
127
+ workfold . -t all -m git --git-commit-times author,committer --coverage
128
+ workfold . --hours 'Mo-Thu 08:00-16:30; Fr 08:00-14:00'
129
+ workfold . --cluster-window 30s
130
+ workfold . --cluster-window 10m
131
+ workfold . --cluster-window 1h5m
132
+ workfold . --cluster-window '1h 5m'
133
+ workfold . --timezone Europe/Berlin --list-outside --limit 50
134
+ ```
135
+
136
+ Chart band labels always use `HH:MM`, even for second-level cluster windows and
137
+ events. Exact seconds and nanoseconds remain in normalized provenance and the
138
+ outside-hours event list.
139
+
140
+ For portable Git-object evidence across all available history, use:
141
+
142
+ ```bash
143
+ workfold . -t all -m git -p portable
144
+ ```
145
+
146
+ The portable profile includes commit author and committer timestamps plus
147
+ annotated-tag tagger timestamps. It does not use filesystem metadata or local
148
+ reflogs, contact a remote, or claim to show when an object was published
149
+ remotely.
150
+
151
+ The exhaustive local command is:
152
+
153
+ ```bash
154
+ workfold . -t all -m all -p full
155
+ ```
156
+
157
+ `-p/--profile` defaults to `standard`. The `full` profile broadens timestamp
158
+ and record collection only inside the selected time and mode. It never changes
159
+ either selector. With `-m all`, it requests all
160
+ supported Git and filesystem evidence, ignored entries, and all local refs. It
161
+ changes collection scope only: detailed ledger output still requires
162
+ `--coverage` or `--verbose`. It does not parse date-like text inside files.
163
+
164
+ ## Accuracy and privacy
165
+
166
+ A Workfold event is a discrete timestamp observation, not a work session. Git
167
+ file changes are derived from first-parent tree differences, not stored human
168
+ actions. Commits can be rewritten; annotated tags have independent tagger dates
169
+ while lightweight tags do not; and reflogs can expire. Filesystem metadata is a
170
+ mutable current snapshot: Linux birth time is collected through `statx` when
171
+ the filesystem returns it, birth time remains platform-dependent elsewhere,
172
+ ctime is metadata-change time, and atime may be unreliable. Workfold cannot
173
+ recover unsaved edit sessions, deleted untracked files, or earlier metadata
174
+ values.
175
+
176
+ Collection remains local. No Git hosting API, telemetry service, web server, or
177
+ network collector is part of the MVP. Coverage reports account for captured,
178
+ filtered, unavailable, unsupported, and unreadable timestamps within the exact
179
+ requested scope instead of claiming unqualified completeness.
180
+
181
+ ## Development
182
+
183
+ The project intentionally uses a flat Python package: the implementation is in
184
+ `workfold/`, with no `src/` wrapper.
185
+
186
+ ```bash
187
+ nix develop
188
+ uv sync --extra dev
189
+ uv run pytest
190
+ ruff check .
191
+ ruff format --check .
192
+ uv run pyright
193
+ uv run workfold --help
194
+ ```
195
+
196
+ Build the Python distributions and validate the Astro documentation with:
197
+
198
+ ```bash
199
+ uv build
200
+ nix run .#docs-check
201
+ nix run .#docs-build
202
+ ```
203
+
204
+ Start the documentation site locally with `nix run .#docs-dev`.
@@ -0,0 +1,165 @@
1
+ # Workfold
2
+
3
+ Workfold is a private, local CLI that folds timestamped activity from Git and
4
+ filesystem metadata onto one representative Monday-to-Sunday week. It makes the
5
+ shape of recorded activity easy to scan and marks events outside a configured
6
+ working schedule without pretending to measure hours worked.
7
+
8
+ ```text
9
+ Time Mon Tue Wed Thu Fri Sat Sun
10
+ 07:48–08:12 ○●■ ●
11
+ ⋮ 1h 8m
12
+ 09:20–09:29 ■●
13
+ ```
14
+
15
+ Terminal output starts directly with this matrix, followed by a content-aware
16
+ symbol key and three direct statistic rows: total events, the inside/outside
17
+ schedule split, and the weekday/weekend calendar split. There is no summary
18
+ heading. The key has no heading and names only categories actually
19
+ visible in the matrix; its `×N` explanation appears only when a cell uses exact
20
+ count notation. The configured working hours follow on their own left-aligned
21
+ line. Schedule and calendar percentages each use the total event count as their
22
+ denominator; weekend activity may also be outside the configured schedule. A
23
+ normal complete run stops there.
24
+ `--verbose` adds scope, period, the full successful coverage status, operational
25
+ metadata, and the detailed coverage ledger. Use `--coverage` when the ledger is
26
+ wanted without those verbose details. Partial collection, explicit narrowing,
27
+ unsupported capabilities, and nonzero display cropping remain visible by
28
+ default as exception notices; compact output never hides those limitations.
29
+
30
+ Each ordinary symbol is one activity marker. Circles are Git and squares are
31
+ filesystem evidence; filled shapes are inside working hours and hollow red
32
+ shapes are outside. Git-inside markers are green and filesystem-inside markers
33
+ are blue. Shapes preserve the same meaning with `--no-color`. Rows are greedy,
34
+ globally aligned time clusters (one hour by default), so empty time disappears
35
+ without losing the order expressed by the labels.
36
+
37
+ ## Status
38
+
39
+ Workfold is an alpha, terminal-only CLI with its sparse terminal renderer and
40
+ local collection pipeline implemented end to end. Git commits, per-file
41
+ changes, annotated tags, local
42
+ reflogs, filesystem metadata, combined-source views, the `full` profile, strict
43
+ coverage accounting, and outside-event listing share the same normalized
44
+ pipeline.
45
+
46
+ The [documentation](https://janthmueller.github.io/workfold/) covers CLI usage,
47
+ accuracy, and development. The Astro site in `docs/` documents the terminal
48
+ product; it is not an HTML reporting feature.
49
+
50
+ ## Install
51
+
52
+ Workfold requires Python 3.10 or newer. Git collection uses the installed
53
+ `git` executable and never contacts a remote.
54
+
55
+ Install Workfold from PyPI:
56
+
57
+ ```bash
58
+ pip install workfold
59
+ workfold --help
60
+ ```
61
+
62
+ For an isolated CLI environment, `uv tool install workfold` and
63
+ `pipx install workfold` are supported alternatives. Standalone archives for
64
+ supported platforms are attached to
65
+ [GitHub releases](https://github.com/janthmueller/workfold/releases).
66
+
67
+ ## Quick Git view
68
+
69
+ Run inside a repository to fold the current ISO week's author timestamps from
70
+ commits reachable through all local refs:
71
+
72
+ ```bash
73
+ workfold
74
+ ```
75
+
76
+ This uses the default selectors `-t this-week -m git -p standard`.
77
+ `-t/--time` also accepts an ISO week, repeated ISO weeks, an inclusive
78
+ `DATE..DATE` range with either endpoint open, or `all`. `-m/--mode` accepts
79
+ `git`, `fs`, or `all`; changing time never changes mode and vice versa.
80
+
81
+ Common selections include:
82
+
83
+ ```bash
84
+ workfold . -t 2026-W31
85
+ workfold . -t 2026-W30 -t 2026-W31
86
+ workfold . -t 2026-07-01..2026-07-31
87
+ workfold . -t 2026-01-01..
88
+ workfold . -t all -m git --git-commit-times author,committer --coverage
89
+ workfold . --hours 'Mo-Thu 08:00-16:30; Fr 08:00-14:00'
90
+ workfold . --cluster-window 30s
91
+ workfold . --cluster-window 10m
92
+ workfold . --cluster-window 1h5m
93
+ workfold . --cluster-window '1h 5m'
94
+ workfold . --timezone Europe/Berlin --list-outside --limit 50
95
+ ```
96
+
97
+ Chart band labels always use `HH:MM`, even for second-level cluster windows and
98
+ events. Exact seconds and nanoseconds remain in normalized provenance and the
99
+ outside-hours event list.
100
+
101
+ For portable Git-object evidence across all available history, use:
102
+
103
+ ```bash
104
+ workfold . -t all -m git -p portable
105
+ ```
106
+
107
+ The portable profile includes commit author and committer timestamps plus
108
+ annotated-tag tagger timestamps. It does not use filesystem metadata or local
109
+ reflogs, contact a remote, or claim to show when an object was published
110
+ remotely.
111
+
112
+ The exhaustive local command is:
113
+
114
+ ```bash
115
+ workfold . -t all -m all -p full
116
+ ```
117
+
118
+ `-p/--profile` defaults to `standard`. The `full` profile broadens timestamp
119
+ and record collection only inside the selected time and mode. It never changes
120
+ either selector. With `-m all`, it requests all
121
+ supported Git and filesystem evidence, ignored entries, and all local refs. It
122
+ changes collection scope only: detailed ledger output still requires
123
+ `--coverage` or `--verbose`. It does not parse date-like text inside files.
124
+
125
+ ## Accuracy and privacy
126
+
127
+ A Workfold event is a discrete timestamp observation, not a work session. Git
128
+ file changes are derived from first-parent tree differences, not stored human
129
+ actions. Commits can be rewritten; annotated tags have independent tagger dates
130
+ while lightweight tags do not; and reflogs can expire. Filesystem metadata is a
131
+ mutable current snapshot: Linux birth time is collected through `statx` when
132
+ the filesystem returns it, birth time remains platform-dependent elsewhere,
133
+ ctime is metadata-change time, and atime may be unreliable. Workfold cannot
134
+ recover unsaved edit sessions, deleted untracked files, or earlier metadata
135
+ values.
136
+
137
+ Collection remains local. No Git hosting API, telemetry service, web server, or
138
+ network collector is part of the MVP. Coverage reports account for captured,
139
+ filtered, unavailable, unsupported, and unreadable timestamps within the exact
140
+ requested scope instead of claiming unqualified completeness.
141
+
142
+ ## Development
143
+
144
+ The project intentionally uses a flat Python package: the implementation is in
145
+ `workfold/`, with no `src/` wrapper.
146
+
147
+ ```bash
148
+ nix develop
149
+ uv sync --extra dev
150
+ uv run pytest
151
+ ruff check .
152
+ ruff format --check .
153
+ uv run pyright
154
+ uv run workfold --help
155
+ ```
156
+
157
+ Build the Python distributions and validate the Astro documentation with:
158
+
159
+ ```bash
160
+ uv build
161
+ nix run .#docs-check
162
+ nix run .#docs-build
163
+ ```
164
+
165
+ Start the documentation site locally with `nix run .#docs-dev`.
@@ -0,0 +1,19 @@
1
+ # Third-party notices for standalone Workfold bundles
2
+
3
+ The standalone archives are produced with PyInstaller and include portions of
4
+ the Python runtime plus Workfold's runtime dependencies. Their exact license
5
+ files are copied into the archive's `licenses/` directory at build time.
6
+
7
+ - Python and its standard library: Python Software Foundation License.
8
+ - pathspec: Mozilla Public License 2.0.
9
+ - Rich: MIT License.
10
+ - markdown-it-py and mdurl: MIT License.
11
+ - Pygments: BSD 2-Clause License.
12
+ - tzlocal: MIT License.
13
+ - tzdata, when bundled on Windows: licenses shipped by the Python tzdata
14
+ distribution and the underlying IANA time-zone database.
15
+ - PyInstaller bootloader: GNU General Public License with PyInstaller's
16
+ bootloader exception; its shipped notices are included.
17
+
18
+ This notice is informational. The complete license texts included beside the
19
+ binary control the corresponding components.
@@ -0,0 +1,107 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "workfold"
7
+ version = "0.1.0-alpha.1"
8
+ description = "Fold local Git and filesystem timestamp activity onto a representative week."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "pathspec>=1,<2",
14
+ "rich>=14,<15",
15
+ "tzdata>=2025.2; sys_platform == 'win32'",
16
+ "tzlocal>=5,<6",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 2 - Pre-Alpha",
20
+ "Environment :: Console",
21
+ "Intended Audience :: Developers",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ ]
30
+
31
+ [[project.authors]]
32
+ name = "Jan T. Müller"
33
+ email = "mail@jantmueller.com"
34
+
35
+ [project.urls]
36
+ homepage = "https://github.com/janthmueller/workfold"
37
+ documentation = "https://janthmueller.github.io/workfold/"
38
+ source = "https://github.com/janthmueller/workfold"
39
+ tracker = "https://github.com/janthmueller/workfold/issues"
40
+
41
+ [project.scripts]
42
+ workfold = "workfold.cli:main"
43
+
44
+ [project.optional-dependencies]
45
+ dev = [
46
+ "pyright>=1.1",
47
+ "pytest>=8",
48
+ "pytest-cov>=6",
49
+ "ruff>=0.8",
50
+ ]
51
+ release = [
52
+ "build>=1",
53
+ "pyinstaller>=6",
54
+ "python-semantic-release>=10,<11",
55
+ "twine>=5",
56
+ ]
57
+
58
+ [tool.setuptools.packages.find]
59
+ where = ["."]
60
+ include = ["workfold*"]
61
+ exclude = ["tests*"]
62
+
63
+ [tool.setuptools.package-data]
64
+ workfold = ["py.typed"]
65
+
66
+ [tool.ruff]
67
+ line-length = 120
68
+ target-version = "py310"
69
+ src = ["workfold", "tests"]
70
+
71
+ [tool.ruff.lint]
72
+ select = ["E4", "E7", "E9", "F", "I"]
73
+
74
+ [tool.pyright]
75
+ pythonVersion = "3.10"
76
+ typeCheckingMode = "strict"
77
+ include = ["workfold", "tests"]
78
+ venvPath = "."
79
+ venv = ".venv"
80
+
81
+ [tool.pytest.ini_options]
82
+ addopts = ["-ra", "--strict-config", "--strict-markers"]
83
+ testpaths = ["tests"]
84
+
85
+ [tool.coverage.run]
86
+ branch = true
87
+ source = ["workfold"]
88
+ omit = ["workfold/__main__.py"]
89
+
90
+ [tool.coverage.report]
91
+ fail_under = 90
92
+ show_missing = true
93
+
94
+ [tool.semantic_release]
95
+ version_toml = ["pyproject.toml:project.version"]
96
+ commit_parser = "conventional"
97
+ allow_zero_version = true
98
+ major_on_zero = false
99
+ commit_message = "chore(release): {version} [skip ci]\n\nAutomatically generated by python-semantic-release"
100
+ build_command = "python -m build"
101
+
102
+ [tool.semantic_release.branches.main]
103
+ match = "main"
104
+ prerelease = false
105
+
106
+ [tool.semantic_release.publish]
107
+ upload_to_vcs_release = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """Deterministic test support for Workfold."""
@@ -0,0 +1,130 @@
1
+ """Isolated local Git repositories with exact timestamp fixtures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Mapping
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class GitRepo:
14
+ """Small plumbing-based repository fixture independent of user config."""
15
+
16
+ path: Path
17
+ branch: str = "main"
18
+
19
+ @classmethod
20
+ def create(cls, path: Path) -> GitRepo:
21
+ path.mkdir(parents=True)
22
+ repo = cls(path=path)
23
+ repo.run("init", "--initial-branch=main")
24
+ return repo
25
+
26
+ def _environment(self, additions: Mapping[str, str] | None = None) -> dict[str, str]:
27
+ environment = dict(os.environ)
28
+ environment.update(
29
+ {
30
+ "GIT_CONFIG_GLOBAL": os.devnull,
31
+ "GIT_CONFIG_NOSYSTEM": "1",
32
+ "GIT_TERMINAL_PROMPT": "0",
33
+ "LANG": "C",
34
+ "LC_ALL": "C",
35
+ }
36
+ )
37
+ if additions is not None:
38
+ environment.update(additions)
39
+ return environment
40
+
41
+ def run(
42
+ self,
43
+ *arguments: str,
44
+ input_data: bytes | None = None,
45
+ environment: Mapping[str, str] | None = None,
46
+ ) -> bytes:
47
+ completed = subprocess.run(
48
+ (
49
+ "git",
50
+ "--no-pager",
51
+ "-c",
52
+ "commit.gpgSign=false",
53
+ "-c",
54
+ "core.hooksPath=/dev/null",
55
+ *arguments,
56
+ ),
57
+ cwd=self.path,
58
+ env=self._environment(environment),
59
+ input=input_data,
60
+ stdout=subprocess.PIPE,
61
+ stderr=subprocess.PIPE,
62
+ shell=False,
63
+ check=False,
64
+ )
65
+ if completed.returncode != 0:
66
+ stderr = completed.stderr.decode("utf-8", errors="replace")
67
+ raise AssertionError(f"fixture Git command failed ({completed.returncode}): {stderr}")
68
+ return completed.stdout
69
+
70
+ def commit(
71
+ self,
72
+ filename: str,
73
+ content: str,
74
+ subject: str,
75
+ *,
76
+ author_date: str,
77
+ committer_date: str,
78
+ author_name: str = "Fixture Author",
79
+ author_email: str = "author@example.test",
80
+ committer_name: str = "Fixture Committer",
81
+ committer_email: str = "committer@example.test",
82
+ parent: str | None = None,
83
+ update_ref: str | None = None,
84
+ ) -> str:
85
+ target = self.path / filename
86
+ target.parent.mkdir(parents=True, exist_ok=True)
87
+ target.write_text(content, encoding="utf-8")
88
+ self.run("add", "--", filename)
89
+ tree = self.run("write-tree").decode("ascii").strip()
90
+ if parent is None:
91
+ parent_result = subprocess.run(
92
+ ("git", "rev-parse", "--verify", "HEAD^{commit}"),
93
+ cwd=self.path,
94
+ env=self._environment(),
95
+ stdout=subprocess.PIPE,
96
+ stderr=subprocess.DEVNULL,
97
+ shell=False,
98
+ check=False,
99
+ )
100
+ parent = parent_result.stdout.decode("ascii").strip() if parent_result.returncode == 0 else None
101
+
102
+ arguments = ["commit-tree", tree]
103
+ if parent is not None:
104
+ arguments.extend(("-p", parent))
105
+ identity_environment = {
106
+ "GIT_AUTHOR_DATE": author_date,
107
+ "GIT_AUTHOR_EMAIL": author_email,
108
+ "GIT_AUTHOR_NAME": author_name,
109
+ "GIT_COMMITTER_DATE": committer_date,
110
+ "GIT_COMMITTER_EMAIL": committer_email,
111
+ "GIT_COMMITTER_NAME": committer_name,
112
+ }
113
+ commit_id = (
114
+ self.run(
115
+ *arguments,
116
+ input_data=subject.encode("utf-8") + b"\n",
117
+ environment=identity_environment,
118
+ )
119
+ .decode("ascii")
120
+ .strip()
121
+ )
122
+ ref_name = update_ref or f"refs/heads/{self.branch}"
123
+ self.run("update-ref", ref_name, commit_id)
124
+ return commit_id
125
+
126
+ def point_ref(self, ref_name: str, object_id: str) -> None:
127
+ self.run("update-ref", ref_name, object_id)
128
+
129
+ def detach(self, object_id: str) -> None:
130
+ self.run("update-ref", "--no-deref", "HEAD", object_id)