pyoxidize 0.2.0__tar.gz → 0.2.3__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.
- pyoxidize-0.2.3/LICENSE +21 -0
- pyoxidize-0.2.3/PKG-INFO +204 -0
- pyoxidize-0.2.3/README.md +179 -0
- pyoxidize-0.2.3/oxidize/__init__.py +1 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/__init__.py +3 -3
- pyoxidize-0.2.3/oxidize/cli/commands/add.py +65 -0
- pyoxidize-0.2.3/oxidize/cli/commands/bisect.py +97 -0
- pyoxidize-0.2.3/oxidize/cli/commands/blame.py +55 -0
- pyoxidize-0.2.3/oxidize/cli/commands/branch.py +93 -0
- pyoxidize-0.2.3/oxidize/cli/commands/checkout.py +96 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/commit.py +5 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/diff.py +55 -9
- pyoxidize-0.2.3/oxidize/cli/commands/hooks.py +73 -0
- pyoxidize-0.2.3/oxidize/cli/commands/ignores.py +72 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/init.py +25 -18
- pyoxidize-0.2.3/oxidize/cli/commands/log.py +60 -0
- pyoxidize-0.2.3/oxidize/cli/commands/merge.py +105 -0
- pyoxidize-0.2.3/oxidize/cli/commands/notebook.py +112 -0
- pyoxidize-0.2.3/oxidize/cli/commands/remote.py +85 -0
- pyoxidize-0.2.3/oxidize/cli/commands/resolve.py +119 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/scan.py +14 -10
- pyoxidize-0.2.3/oxidize/cli/commands/show.py +37 -0
- pyoxidize-0.2.3/oxidize/cli/commands/stash.py +69 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/status.py +97 -75
- pyoxidize-0.2.3/oxidize/cli/commands/tag.py +69 -0
- pyoxidize-0.2.3/oxidize/cli/commands/undo.py +95 -0
- pyoxidize-0.2.3/oxidize/cli/main.py +52 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/repl.py +16 -7
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/core/__init__.py +5 -5
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/core/config.py +30 -30
- pyoxidize-0.2.3/oxidize/core/ignores.py +109 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/core/refs.py +70 -54
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/core/repository.py +70 -54
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/diff/__init__.py +3 -3
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/diff/engine.py +91 -91
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/index/__init__.py +3 -3
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/index/staging.py +73 -73
- pyoxidize-0.2.3/oxidize/merge/text.py +52 -0
- pyoxidize-0.2.3/oxidize/merge/treewalk.py +112 -0
- pyoxidize-0.2.3/oxidize/network/__init__.py +0 -0
- pyoxidize-0.2.3/oxidize/network/github_api.py +114 -0
- pyoxidize-0.2.3/oxidize/network/github_remote.py +190 -0
- pyoxidize-0.2.3/oxidize/network/remote.py +281 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/objects/__init__.py +12 -12
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/objects/types.py +164 -164
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/provenance/agent.py +0 -1
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/security/__init__.py +1 -1
- pyoxidize-0.2.3/oxidize/security/scanner.py +199 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/semantic/__init__.py +11 -4
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/semantic/differ.py +20 -20
- pyoxidize-0.2.3/oxidize/semantic/entities.py +253 -0
- pyoxidize-0.2.3/oxidize/semantic/parser.py +40 -0
- pyoxidize-0.2.3/oxidize/staging/__init__.py +1 -0
- pyoxidize-0.2.3/oxidize/staging/stash.py +84 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/storage/__init__.py +4 -4
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/storage/backend.py +71 -51
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/storage/database.py +45 -45
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/undo/__init__.py +1 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/undo/reverser.py +2 -2
- pyoxidize-0.2.3/oxidize/workflow/__init__.py +0 -0
- pyoxidize-0.2.3/oxidize/workflow/bisect.py +93 -0
- pyoxidize-0.2.3/oxidize/workflow/blame.py +71 -0
- pyoxidize-0.2.3/oxidize/workflow/hooks.py +123 -0
- pyoxidize-0.2.3/pyoxidize.egg-info/PKG-INFO +204 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/SOURCES.txt +36 -1
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/requires.txt +2 -1
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyproject.toml +58 -53
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/setup.cfg +4 -4
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_diff.py +28 -28
- pyoxidize-0.2.3/tests/test_github_remote.py +260 -0
- pyoxidize-0.2.3/tests/test_ignores.py +188 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_index.py +36 -35
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_merge.py +2 -2
- pyoxidize-0.2.3/tests/test_merge_conflict_blame_remote.py +213 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_notebook.py +3 -3
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_objects.py +48 -48
- pyoxidize-0.2.3/tests/test_refs_branches_tags.py +99 -0
- pyoxidize-0.2.3/tests/test_remote_fixes.py +232 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_repository.py +35 -33
- pyoxidize-0.2.3/tests/test_scanner.py +221 -0
- pyoxidize-0.2.3/tests/test_semantic.py +135 -0
- pyoxidize-0.2.3/tests/test_storage.py +86 -0
- pyoxidize-0.2.3/tests/test_stress.py +604 -0
- pyoxidize-0.2.3/tests/test_workflow.py +88 -0
- pyoxidize-0.2.0/PKG-INFO +0 -118
- pyoxidize-0.2.0/README.md +0 -96
- pyoxidize-0.2.0/oxidize/__init__.py +0 -1
- pyoxidize-0.2.0/oxidize/cli/commands/add.py +0 -28
- pyoxidize-0.2.0/oxidize/cli/commands/log.py +0 -40
- pyoxidize-0.2.0/oxidize/cli/commands/undo.py +0 -65
- pyoxidize-0.2.0/oxidize/cli/main.py +0 -26
- pyoxidize-0.2.0/oxidize/security/scanner.py +0 -65
- pyoxidize-0.2.0/oxidize/semantic/entities.py +0 -122
- pyoxidize-0.2.0/pyoxidize.egg-info/PKG-INFO +0 -118
- pyoxidize-0.2.0/tests/test_scanner.py +0 -65
- pyoxidize-0.2.0/tests/test_semantic.py +0 -73
- pyoxidize-0.2.0/tests/test_storage.py +0 -38
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/__init__.py +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/merge/__init__.py +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/merge/structured.py +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/notebook/__init__.py +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/notebook/differ.py +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/provenance/__init__.py +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/undo/journal.py +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/dependency_links.txt +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/entry_points.txt +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/top_level.txt +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_provenance.py +0 -0
- {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_undo.py +0 -0
pyoxidize-0.2.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Siddhant Bayas
|
|
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.
|
pyoxidize-0.2.3/PKG-INFO
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyoxidize
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: a version control system with semantic awareness built-in undo and interactive shell
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: click>=8.1
|
|
10
|
+
Requires-Dist: rich>=13.0
|
|
11
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
12
|
+
Requires-Dist: deepmerge>=1.1
|
|
13
|
+
Requires-Dist: pathspec>=0.11
|
|
14
|
+
Provides-Extra: semantic
|
|
15
|
+
Requires-Dist: tree-sitter>=0.21; extra == "semantic"
|
|
16
|
+
Requires-Dist: tree-sitter-python>=0.21; extra == "semantic"
|
|
17
|
+
Provides-Extra: notebook
|
|
18
|
+
Requires-Dist: nbformat>=5.9; extra == "notebook"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy>=1.5; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# oxidize
|
|
27
|
+
|
|
28
|
+
a content-addressable, semantic-aware version control system -- built from scratch in a weekend, kept (painfully) honest since.
|
|
29
|
+
|
|
30
|
+
[](https://github.com/siddhant-bayas/oxidize/actions/workflows/ci.yml)
|
|
31
|
+
[](https://pypi.org/project/pyoxidize/)
|
|
32
|
+
[](https://pypi.org/project/pyoxidize/)
|
|
33
|
+
[](#install--dependencies)
|
|
34
|
+
|
|
35
|
+
## install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install pyoxidize # core
|
|
39
|
+
pip install "pyoxidize[semantic]" # tree-sitter AST diffs
|
|
40
|
+
pip install "pyoxidize[notebook]" # jupyter integration
|
|
41
|
+
pip install "pyoxidize[dev]" # pytest, mypy, ruff
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
optional extras add `tree-sitter`, `nbformat`, etc. core itself pulls in only `click`, `rich`, `prompt_toolkit`, `deepmerge`, `pathspec` -- no native bindings, no large free-threaded runtime.
|
|
45
|
+
|
|
46
|
+
## why not just use git?
|
|
47
|
+
|
|
48
|
+
| need | git | oxidize |
|
|
49
|
+
|------|-----|---------|
|
|
50
|
+
| commit / branch / merge | ✅ | ✅ |
|
|
51
|
+
| real structured merge for json/yaml/toml/inipath | ❌ (line-level) | ✅ |
|
|
52
|
+
| cell-level notebook diffs | ❌ | ✅ |
|
|
53
|
+
| AST-aware diffs (function renames) | ❌ | ✅ (opt-in via the `semantic` extra) |
|
|
54
|
+
| secret scanning before commit | ❌ (out-of-tree tooling) | ✅ 21 patterns |
|
|
55
|
+
| ai-agent provenance per commit | ❌ | ✅ `oxidize log --agent ...` |
|
|
56
|
+
| interactive REPL with status bar | ❌ | ✅ |
|
|
57
|
+
| safe undo of every operation | ❌ | ✅ |
|
|
58
|
+
| local-self-contained footprint | ⚠ git + curl + ssh | ✅ one pip package |
|
|
59
|
+
| push to github | ⚠ requires git | ✅ via REST API |
|
|
60
|
+
| LFS over network | ✅ | ⏳ (filesystem remote only) |
|
|
61
|
+
|
|
62
|
+
oxidize is **not** a git replacement for big teams or monorepos. it's a focused, single-binary, Python-venv-friendly alternative for personal and small-team projects where the semantic and provenance extras matter more than SSH-authenticated push.
|
|
63
|
+
|
|
64
|
+
## feature status
|
|
65
|
+
|
|
66
|
+
| feature | status | since |
|
|
67
|
+
|---------|--------|-------|
|
|
68
|
+
| content-addressable store, blob/tree/commit | stable | v0.1.0 |
|
|
69
|
+
| recursive-tree directories | stable | v0.1.0 |
|
|
70
|
+
| `.oxignore` with full `.gitignore` syntax | stable | v0.1.0 |
|
|
71
|
+
| secret scanner (23 patterns) | stable | v0.1.0 |
|
|
72
|
+
| structured data merge | stable | v0.1.0 |
|
|
73
|
+
| notebook cell-level diff | stable | v0.1.0 |
|
|
74
|
+
| interactive REPL (`oxi`) | stable | v0.1.0 |
|
|
75
|
+
| safe undo (journaled) | stable | v0.1.0 |
|
|
76
|
+
| branches / tags / checkout / 3-way merge | stable | v0.1.0 |
|
|
77
|
+
| conflict resolution UI (`oxidize resolve`) | stable | v0.1.0 |
|
|
78
|
+
| ai-agent provenance + filtering | stable | v0.1.0 |
|
|
79
|
+
| ai-agent detection env-var coverage | stable | v0.1.0 |
|
|
80
|
+
| stash | beta | v0.1.0 |
|
|
81
|
+
| bisect | beta | v0.1.0 |
|
|
82
|
+
| hooks (`pre-commit`, etc) | beta | v0.1.0 |
|
|
83
|
+
| blame (line-level) | beta | v0.1.0 |
|
|
84
|
+
| remote sync (filesystem clone/push/pull) | stable | v0.1.0 |
|
|
85
|
+
| semantic diffs (AST-aware, `tree-sitter`) | alpha | v0.2.1 |
|
|
86
|
+
| ai-agent prose locking + qa | (planning) | -- |
|
|
87
|
+
| network remotes (github REST, push-only) | beta | v0.2.2 |
|
|
88
|
+
| LFS-style large-file extension | not yet | -- |
|
|
89
|
+
| submodule support | not yet | -- |
|
|
90
|
+
|
|
91
|
+
## quick start
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
oxidize init # create .oxidize/ and a starter .oxignore
|
|
95
|
+
oxi add main.py # stage a single file (or use the alias)
|
|
96
|
+
oxi add . # stage everything (respects .oxignore)
|
|
97
|
+
oxi add -f .env.local # force-add an ignored file
|
|
98
|
+
oxi status # working tree vs index vs HEAD
|
|
99
|
+
oxi commit -m "first commit" --agent claude-code
|
|
100
|
+
oxi branch create feature # branch off HEAD
|
|
101
|
+
oxi checkout feature # switch branches
|
|
102
|
+
oxi merge main # three-way merge
|
|
103
|
+
oxidize remote push file:///backup main # filesystem remote
|
|
104
|
+
oxi bisect start v1.0 broken # binary-search regressions
|
|
105
|
+
oxi # drop into the interactive REPL
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## the REPL
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
oxi> status
|
|
112
|
+
On branch main
|
|
113
|
+
staged:
|
|
114
|
+
hello.py
|
|
115
|
+
modified:
|
|
116
|
+
README.md
|
|
117
|
+
(main) main | staged:1 | HEAD:9ecfb22c | Ctrl-D exit | help for commands
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
launching `oxi` with no args opens a prompt_toolkit REPL with tab completion, history (`~/.oxidize_history`), and a live status bar showing the current branch, staged count, and short HEAD. every command above also works inside it.
|
|
121
|
+
|
|
122
|
+
## features (the headline ones)
|
|
123
|
+
|
|
124
|
+
* **content-addressable object store** -- SHA-256, zlib-compressed, deduplicated
|
|
125
|
+
* **recursive tree objects** -- proper nested directory tracking
|
|
126
|
+
* **interactive REPL** -- `oxi` launches a shell with tab completion, live status bar, command history
|
|
127
|
+
* **secret scanning** -- detects 21 types of API keys, tokens, and credentials before commit
|
|
128
|
+
* **structured data merge** -- JSON/YAML/TOML merge at the key level, not text lines
|
|
129
|
+
* **notebook-aware versioning** -- cell-level diffs for `.ipynb` files
|
|
130
|
+
* **AI agent provenance** -- track which agent/tool wrote what code
|
|
131
|
+
* **built-in undo** -- every operation is safely reversible
|
|
132
|
+
* **branches, tags, checkout, merge, resolve** -- full VCS primitives
|
|
133
|
+
* **stash / bisect / hooks** -- everyday workflow commands
|
|
134
|
+
* **filesystem remote sync** -- bare-repo clone/push/pull with atomic writes
|
|
135
|
+
* **github remote** -- push-only export to github repos via REST API (no git needed)
|
|
136
|
+
* **semantic diffs (alpha)** -- AST-aware diffs via the `semantic` extra
|
|
137
|
+
|
|
138
|
+
## docs
|
|
139
|
+
|
|
140
|
+
* [Getting Started](docs/getting-started.md) -- installation, quick start, basic workflow
|
|
141
|
+
* [CLI Reference](docs/cli-reference.md) -- every command, flag, and parameter
|
|
142
|
+
* [Configuration](docs/configuration.md) -- config files, environment variables
|
|
143
|
+
* [Interactive Shell](docs/repl.md) -- the `oxi` REPL
|
|
144
|
+
* [Ignores](docs/ignores.md) -- `.oxignore` patterns, builtins, force usage
|
|
145
|
+
* [Branching & Merging](docs/branching.md) -- branches, checkout, 3-way merge, conflicts
|
|
146
|
+
* [Workflow Commands](docs/workflow.md) -- stash, bisect, hooks, tags, blame, notebook-diff
|
|
147
|
+
* [Remote Sync](docs/remote.md) -- filesystem + github remote (beta)
|
|
148
|
+
* [Undo System](docs/undo.md) -- reversing operations
|
|
149
|
+
* [Semantic Diffs](docs/semantic-diffs.md) -- AST-aware diffs
|
|
150
|
+
* [Notebook Support](docs/notebook-support.md) -- Jupyter notebook versioning
|
|
151
|
+
* [Structured Merge](docs/merge.md) -- key-level data merging
|
|
152
|
+
* [Changelog](CHANGELOG.md)
|
|
153
|
+
* [Agent Provenance](docs/provenance.md) -- AI agent tracking
|
|
154
|
+
* [Object Storage](docs/storage.md) -- storage internals
|
|
155
|
+
* [Python API Reference](docs/api-reference.md) -- complete Python API
|
|
156
|
+
* [Architecture](docs/architecture.md) -- system design and internals
|
|
157
|
+
* [Contributing](docs/CONTRIBUTING.md) -- development setup and guidelines
|
|
158
|
+
|
|
159
|
+
## how it works
|
|
160
|
+
|
|
161
|
+
every file you add gets hashed using SHA-256 and stored as a blob inside `.oxidize/objects/`
|
|
162
|
+
|
|
163
|
+
a commit is just:
|
|
164
|
+
|
|
165
|
+
* a snapshot of the file tree (with proper subtrees for nested dirs)
|
|
166
|
+
* metadata (author, timestamp, agent provenance)
|
|
167
|
+
* a pointer to the previous commit(s)
|
|
168
|
+
|
|
169
|
+
```txt
|
|
170
|
+
files -> blobs -> tree -> commit -> commit -> commit
|
|
171
|
+
^
|
|
172
|
+
HEAD
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## structure
|
|
176
|
+
|
|
177
|
+
```txt
|
|
178
|
+
oxidize/
|
|
179
|
+
objects/ blob tree commit types and serialization
|
|
180
|
+
storage/ content-addressable object store
|
|
181
|
+
index/ staging area
|
|
182
|
+
core/ repo context refs config ignores
|
|
183
|
+
diff/ myers diff engine
|
|
184
|
+
merge/ structured + text merge
|
|
185
|
+
notebook/ ipynb cell-level support
|
|
186
|
+
semantic/ ast-aware diffs via tree-sitter
|
|
187
|
+
security/ secret scanning
|
|
188
|
+
provenance/ ai agent tracking
|
|
189
|
+
undo/ operation journal and reversal
|
|
190
|
+
workflow/ stash, bisect, hooks, blame
|
|
191
|
+
network/ filesystem + github remote sync
|
|
192
|
+
staging/ stash
|
|
193
|
+
cli/ click commands and prompt_toolkit repl
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## contributing
|
|
197
|
+
|
|
198
|
+
see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md)
|
|
199
|
+
|
|
200
|
+
## license
|
|
201
|
+
|
|
202
|
+
MIT
|
|
203
|
+
|
|
204
|
+
~ written by a guy who discovered hashing yesterday
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# oxidize
|
|
2
|
+
|
|
3
|
+
a content-addressable, semantic-aware version control system -- built from scratch in a weekend, kept (painfully) honest since.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/siddhant-bayas/oxidize/actions/workflows/ci.yml)
|
|
6
|
+
[](https://pypi.org/project/pyoxidize/)
|
|
7
|
+
[](https://pypi.org/project/pyoxidize/)
|
|
8
|
+
[](#install--dependencies)
|
|
9
|
+
|
|
10
|
+
## install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install pyoxidize # core
|
|
14
|
+
pip install "pyoxidize[semantic]" # tree-sitter AST diffs
|
|
15
|
+
pip install "pyoxidize[notebook]" # jupyter integration
|
|
16
|
+
pip install "pyoxidize[dev]" # pytest, mypy, ruff
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
optional extras add `tree-sitter`, `nbformat`, etc. core itself pulls in only `click`, `rich`, `prompt_toolkit`, `deepmerge`, `pathspec` -- no native bindings, no large free-threaded runtime.
|
|
20
|
+
|
|
21
|
+
## why not just use git?
|
|
22
|
+
|
|
23
|
+
| need | git | oxidize |
|
|
24
|
+
|------|-----|---------|
|
|
25
|
+
| commit / branch / merge | ✅ | ✅ |
|
|
26
|
+
| real structured merge for json/yaml/toml/inipath | ❌ (line-level) | ✅ |
|
|
27
|
+
| cell-level notebook diffs | ❌ | ✅ |
|
|
28
|
+
| AST-aware diffs (function renames) | ❌ | ✅ (opt-in via the `semantic` extra) |
|
|
29
|
+
| secret scanning before commit | ❌ (out-of-tree tooling) | ✅ 21 patterns |
|
|
30
|
+
| ai-agent provenance per commit | ❌ | ✅ `oxidize log --agent ...` |
|
|
31
|
+
| interactive REPL with status bar | ❌ | ✅ |
|
|
32
|
+
| safe undo of every operation | ❌ | ✅ |
|
|
33
|
+
| local-self-contained footprint | ⚠ git + curl + ssh | ✅ one pip package |
|
|
34
|
+
| push to github | ⚠ requires git | ✅ via REST API |
|
|
35
|
+
| LFS over network | ✅ | ⏳ (filesystem remote only) |
|
|
36
|
+
|
|
37
|
+
oxidize is **not** a git replacement for big teams or monorepos. it's a focused, single-binary, Python-venv-friendly alternative for personal and small-team projects where the semantic and provenance extras matter more than SSH-authenticated push.
|
|
38
|
+
|
|
39
|
+
## feature status
|
|
40
|
+
|
|
41
|
+
| feature | status | since |
|
|
42
|
+
|---------|--------|-------|
|
|
43
|
+
| content-addressable store, blob/tree/commit | stable | v0.1.0 |
|
|
44
|
+
| recursive-tree directories | stable | v0.1.0 |
|
|
45
|
+
| `.oxignore` with full `.gitignore` syntax | stable | v0.1.0 |
|
|
46
|
+
| secret scanner (23 patterns) | stable | v0.1.0 |
|
|
47
|
+
| structured data merge | stable | v0.1.0 |
|
|
48
|
+
| notebook cell-level diff | stable | v0.1.0 |
|
|
49
|
+
| interactive REPL (`oxi`) | stable | v0.1.0 |
|
|
50
|
+
| safe undo (journaled) | stable | v0.1.0 |
|
|
51
|
+
| branches / tags / checkout / 3-way merge | stable | v0.1.0 |
|
|
52
|
+
| conflict resolution UI (`oxidize resolve`) | stable | v0.1.0 |
|
|
53
|
+
| ai-agent provenance + filtering | stable | v0.1.0 |
|
|
54
|
+
| ai-agent detection env-var coverage | stable | v0.1.0 |
|
|
55
|
+
| stash | beta | v0.1.0 |
|
|
56
|
+
| bisect | beta | v0.1.0 |
|
|
57
|
+
| hooks (`pre-commit`, etc) | beta | v0.1.0 |
|
|
58
|
+
| blame (line-level) | beta | v0.1.0 |
|
|
59
|
+
| remote sync (filesystem clone/push/pull) | stable | v0.1.0 |
|
|
60
|
+
| semantic diffs (AST-aware, `tree-sitter`) | alpha | v0.2.1 |
|
|
61
|
+
| ai-agent prose locking + qa | (planning) | -- |
|
|
62
|
+
| network remotes (github REST, push-only) | beta | v0.2.2 |
|
|
63
|
+
| LFS-style large-file extension | not yet | -- |
|
|
64
|
+
| submodule support | not yet | -- |
|
|
65
|
+
|
|
66
|
+
## quick start
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
oxidize init # create .oxidize/ and a starter .oxignore
|
|
70
|
+
oxi add main.py # stage a single file (or use the alias)
|
|
71
|
+
oxi add . # stage everything (respects .oxignore)
|
|
72
|
+
oxi add -f .env.local # force-add an ignored file
|
|
73
|
+
oxi status # working tree vs index vs HEAD
|
|
74
|
+
oxi commit -m "first commit" --agent claude-code
|
|
75
|
+
oxi branch create feature # branch off HEAD
|
|
76
|
+
oxi checkout feature # switch branches
|
|
77
|
+
oxi merge main # three-way merge
|
|
78
|
+
oxidize remote push file:///backup main # filesystem remote
|
|
79
|
+
oxi bisect start v1.0 broken # binary-search regressions
|
|
80
|
+
oxi # drop into the interactive REPL
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## the REPL
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
oxi> status
|
|
87
|
+
On branch main
|
|
88
|
+
staged:
|
|
89
|
+
hello.py
|
|
90
|
+
modified:
|
|
91
|
+
README.md
|
|
92
|
+
(main) main | staged:1 | HEAD:9ecfb22c | Ctrl-D exit | help for commands
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
launching `oxi` with no args opens a prompt_toolkit REPL with tab completion, history (`~/.oxidize_history`), and a live status bar showing the current branch, staged count, and short HEAD. every command above also works inside it.
|
|
96
|
+
|
|
97
|
+
## features (the headline ones)
|
|
98
|
+
|
|
99
|
+
* **content-addressable object store** -- SHA-256, zlib-compressed, deduplicated
|
|
100
|
+
* **recursive tree objects** -- proper nested directory tracking
|
|
101
|
+
* **interactive REPL** -- `oxi` launches a shell with tab completion, live status bar, command history
|
|
102
|
+
* **secret scanning** -- detects 21 types of API keys, tokens, and credentials before commit
|
|
103
|
+
* **structured data merge** -- JSON/YAML/TOML merge at the key level, not text lines
|
|
104
|
+
* **notebook-aware versioning** -- cell-level diffs for `.ipynb` files
|
|
105
|
+
* **AI agent provenance** -- track which agent/tool wrote what code
|
|
106
|
+
* **built-in undo** -- every operation is safely reversible
|
|
107
|
+
* **branches, tags, checkout, merge, resolve** -- full VCS primitives
|
|
108
|
+
* **stash / bisect / hooks** -- everyday workflow commands
|
|
109
|
+
* **filesystem remote sync** -- bare-repo clone/push/pull with atomic writes
|
|
110
|
+
* **github remote** -- push-only export to github repos via REST API (no git needed)
|
|
111
|
+
* **semantic diffs (alpha)** -- AST-aware diffs via the `semantic` extra
|
|
112
|
+
|
|
113
|
+
## docs
|
|
114
|
+
|
|
115
|
+
* [Getting Started](docs/getting-started.md) -- installation, quick start, basic workflow
|
|
116
|
+
* [CLI Reference](docs/cli-reference.md) -- every command, flag, and parameter
|
|
117
|
+
* [Configuration](docs/configuration.md) -- config files, environment variables
|
|
118
|
+
* [Interactive Shell](docs/repl.md) -- the `oxi` REPL
|
|
119
|
+
* [Ignores](docs/ignores.md) -- `.oxignore` patterns, builtins, force usage
|
|
120
|
+
* [Branching & Merging](docs/branching.md) -- branches, checkout, 3-way merge, conflicts
|
|
121
|
+
* [Workflow Commands](docs/workflow.md) -- stash, bisect, hooks, tags, blame, notebook-diff
|
|
122
|
+
* [Remote Sync](docs/remote.md) -- filesystem + github remote (beta)
|
|
123
|
+
* [Undo System](docs/undo.md) -- reversing operations
|
|
124
|
+
* [Semantic Diffs](docs/semantic-diffs.md) -- AST-aware diffs
|
|
125
|
+
* [Notebook Support](docs/notebook-support.md) -- Jupyter notebook versioning
|
|
126
|
+
* [Structured Merge](docs/merge.md) -- key-level data merging
|
|
127
|
+
* [Changelog](CHANGELOG.md)
|
|
128
|
+
* [Agent Provenance](docs/provenance.md) -- AI agent tracking
|
|
129
|
+
* [Object Storage](docs/storage.md) -- storage internals
|
|
130
|
+
* [Python API Reference](docs/api-reference.md) -- complete Python API
|
|
131
|
+
* [Architecture](docs/architecture.md) -- system design and internals
|
|
132
|
+
* [Contributing](docs/CONTRIBUTING.md) -- development setup and guidelines
|
|
133
|
+
|
|
134
|
+
## how it works
|
|
135
|
+
|
|
136
|
+
every file you add gets hashed using SHA-256 and stored as a blob inside `.oxidize/objects/`
|
|
137
|
+
|
|
138
|
+
a commit is just:
|
|
139
|
+
|
|
140
|
+
* a snapshot of the file tree (with proper subtrees for nested dirs)
|
|
141
|
+
* metadata (author, timestamp, agent provenance)
|
|
142
|
+
* a pointer to the previous commit(s)
|
|
143
|
+
|
|
144
|
+
```txt
|
|
145
|
+
files -> blobs -> tree -> commit -> commit -> commit
|
|
146
|
+
^
|
|
147
|
+
HEAD
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## structure
|
|
151
|
+
|
|
152
|
+
```txt
|
|
153
|
+
oxidize/
|
|
154
|
+
objects/ blob tree commit types and serialization
|
|
155
|
+
storage/ content-addressable object store
|
|
156
|
+
index/ staging area
|
|
157
|
+
core/ repo context refs config ignores
|
|
158
|
+
diff/ myers diff engine
|
|
159
|
+
merge/ structured + text merge
|
|
160
|
+
notebook/ ipynb cell-level support
|
|
161
|
+
semantic/ ast-aware diffs via tree-sitter
|
|
162
|
+
security/ secret scanning
|
|
163
|
+
provenance/ ai agent tracking
|
|
164
|
+
undo/ operation journal and reversal
|
|
165
|
+
workflow/ stash, bisect, hooks, blame
|
|
166
|
+
network/ filesystem + github remote sync
|
|
167
|
+
staging/ stash
|
|
168
|
+
cli/ click commands and prompt_toolkit repl
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## contributing
|
|
172
|
+
|
|
173
|
+
see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md)
|
|
174
|
+
|
|
175
|
+
## license
|
|
176
|
+
|
|
177
|
+
MIT
|
|
178
|
+
|
|
179
|
+
~ written by a guy who discovered hashing yesterday
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.3"
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .main import cli
|
|
2
|
-
|
|
3
|
-
__all__ = ["cli"]
|
|
1
|
+
from .main import cli
|
|
2
|
+
|
|
3
|
+
__all__ = ["cli"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from oxidize.core.repository import Repository, RepositoryNotFound
|
|
9
|
+
from oxidize.undo.reverser import UndoManager
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("add")
|
|
15
|
+
@click.argument("paths", nargs=-1, required=True, type=click.Path(exists=True))
|
|
16
|
+
@click.option("-f", "--force", is_flag=True, help="Add ignored files (bypass .oxignore)")
|
|
17
|
+
def cmd_add(paths: tuple[str, ...], force: bool) -> None:
|
|
18
|
+
"""Stage files for the next commit."""
|
|
19
|
+
try:
|
|
20
|
+
repo = Repository.discover()
|
|
21
|
+
except RepositoryNotFound as e:
|
|
22
|
+
raise click.ClickException(str(e))
|
|
23
|
+
|
|
24
|
+
matcher = repo.ignore_matcher
|
|
25
|
+
|
|
26
|
+
undo_mgr = UndoManager(repo)
|
|
27
|
+
added_paths: list[str] = []
|
|
28
|
+
added_oids: list[str] = []
|
|
29
|
+
skipped_paths: list[str] = []
|
|
30
|
+
|
|
31
|
+
for raw_path in paths:
|
|
32
|
+
target = Path(raw_path).resolve()
|
|
33
|
+
if target.is_dir():
|
|
34
|
+
candidates = [f for f in target.rglob("*") if f.is_file()]
|
|
35
|
+
else:
|
|
36
|
+
candidates = [target]
|
|
37
|
+
|
|
38
|
+
if force:
|
|
39
|
+
files = candidates
|
|
40
|
+
else:
|
|
41
|
+
files = []
|
|
42
|
+
for f in candidates:
|
|
43
|
+
try:
|
|
44
|
+
rel = f.relative_to(repo.work_tree).as_posix()
|
|
45
|
+
except ValueError:
|
|
46
|
+
files.append(f)
|
|
47
|
+
continue
|
|
48
|
+
if matcher.is_ignored(rel):
|
|
49
|
+
skipped_paths.append(rel)
|
|
50
|
+
else:
|
|
51
|
+
files.append(f)
|
|
52
|
+
|
|
53
|
+
for file_path in files:
|
|
54
|
+
rel = file_path.relative_to(repo.work_tree).as_posix()
|
|
55
|
+
oid = repo.db.store_blob(file_path.read_bytes())
|
|
56
|
+
repo.index.add(rel, oid, file_path)
|
|
57
|
+
added_paths.append(rel)
|
|
58
|
+
added_oids.append(oid)
|
|
59
|
+
click.echo(f" staged: {rel}")
|
|
60
|
+
|
|
61
|
+
for skipped in skipped_paths:
|
|
62
|
+
console.print(f" [dim]ignored: {skipped}[/]")
|
|
63
|
+
|
|
64
|
+
if added_paths:
|
|
65
|
+
undo_mgr.record_add(added_paths, added_oids)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from oxidize.core.repository import Repository, RepositoryNotFound
|
|
7
|
+
from oxidize.workflow.bisect import BisectState, midpoint
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group(invoke_without_command=True)
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def cmd_bisect(ctx: click.Context) -> None:
|
|
15
|
+
"""Find a regression by binary search."""
|
|
16
|
+
if ctx.invoked_subcommand is None:
|
|
17
|
+
try:
|
|
18
|
+
repo = Repository.discover()
|
|
19
|
+
except RepositoryNotFound as e:
|
|
20
|
+
raise click.ClickException(str(e))
|
|
21
|
+
state = BisectState(repo.oxidize_dir / "bisect.json")
|
|
22
|
+
status = state.status()
|
|
23
|
+
if not status:
|
|
24
|
+
click.echo("no bisect session; try `oxidize bisect start`")
|
|
25
|
+
return
|
|
26
|
+
console.print("current session:")
|
|
27
|
+
for k, v in status.items():
|
|
28
|
+
console.print(f" [dim]{k}[/]: {v}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@cmd_bisect.command("start")
|
|
32
|
+
@click.argument("good", required=False, default="")
|
|
33
|
+
@click.argument("bad", required=False, default="")
|
|
34
|
+
def cmd_bisect_start(good: str, bad: str) -> None:
|
|
35
|
+
"""Start a bisect session. Optionally provide GOOD and BAD commits."""
|
|
36
|
+
try:
|
|
37
|
+
repo = Repository.discover()
|
|
38
|
+
except RepositoryNotFound as e:
|
|
39
|
+
raise click.ClickException(str(e))
|
|
40
|
+
|
|
41
|
+
state = BisectState(repo.oxidize_dir / "bisect.json")
|
|
42
|
+
state.reset()
|
|
43
|
+
|
|
44
|
+
if good and bad:
|
|
45
|
+
good_oid = repo.refs.read(good) or good
|
|
46
|
+
bad_oid = repo.refs.read(bad) or bad
|
|
47
|
+
state.start(good_oid, bad_oid)
|
|
48
|
+
click.echo(
|
|
49
|
+
f"check out: {midpoint(repo, good_oid, bad_oid)} then run `oxidize bisect good` or `oxidize bisect bad`"
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
click.echo("bisect armed; run `oxidize bisect bad HEAD`, then `oxidize bisect good <ref>`")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@cmd_bisect.command("good")
|
|
56
|
+
@click.argument("ref", required=False, default="HEAD")
|
|
57
|
+
def cmd_bisect_good(ref: str) -> None:
|
|
58
|
+
"""Mark the current commit (or ref) as good."""
|
|
59
|
+
try:
|
|
60
|
+
repo = Repository.discover()
|
|
61
|
+
except RepositoryNotFound as e:
|
|
62
|
+
raise click.ClickException(str(e))
|
|
63
|
+
state = BisectState(repo.oxidize_dir / "bisect.json")
|
|
64
|
+
oid = repo.refs.read(ref) or ref
|
|
65
|
+
result = state.mark("good", oid)
|
|
66
|
+
if result.get("done"):
|
|
67
|
+
click.echo(f"FOUND regression: {result['found']}")
|
|
68
|
+
else:
|
|
69
|
+
click.echo(f"check out: {result['next']}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@cmd_bisect.command("bad")
|
|
73
|
+
@click.argument("ref", required=False, default="HEAD")
|
|
74
|
+
def cmd_bisect_bad(ref: str) -> None:
|
|
75
|
+
"""Mark the current commit (or ref) as bad."""
|
|
76
|
+
try:
|
|
77
|
+
repo = Repository.discover()
|
|
78
|
+
except RepositoryNotFound as e:
|
|
79
|
+
raise click.ClickException(str(e))
|
|
80
|
+
state = BisectState(repo.oxidize_dir / "bisect.json")
|
|
81
|
+
oid = repo.refs.read(ref) or ref
|
|
82
|
+
result = state.mark("bad", oid)
|
|
83
|
+
if result.get("done"):
|
|
84
|
+
click.echo(f"FOUND regression: {result['found']}")
|
|
85
|
+
else:
|
|
86
|
+
click.echo(f"check out: {result['next']}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@cmd_bisect.command("reset")
|
|
90
|
+
def cmd_bisect_reset() -> None:
|
|
91
|
+
"""End the bisect session."""
|
|
92
|
+
try:
|
|
93
|
+
repo = Repository.discover()
|
|
94
|
+
except RepositoryNotFound as e:
|
|
95
|
+
raise click.ClickException(str(e))
|
|
96
|
+
BisectState(repo.oxidize_dir / "bisect.json").reset()
|
|
97
|
+
click.echo("bisect reset")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from oxidize.core.repository import Repository, RepositoryNotFound
|
|
10
|
+
from oxidize.workflow.blame import Blamer
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command("blame")
|
|
17
|
+
@click.argument("path")
|
|
18
|
+
def cmd_blame(path: str) -> None:
|
|
19
|
+
"""Show commit-by-commit annotation for a file."""
|
|
20
|
+
try:
|
|
21
|
+
repo = Repository.discover()
|
|
22
|
+
except RepositoryNotFound as e:
|
|
23
|
+
raise click.ClickException(str(e))
|
|
24
|
+
|
|
25
|
+
target = Path(path).resolve()
|
|
26
|
+
try:
|
|
27
|
+
rel = target.relative_to(repo.work_tree).as_posix()
|
|
28
|
+
except ValueError:
|
|
29
|
+
rel = path
|
|
30
|
+
|
|
31
|
+
annotations = Blamer(repo).annotate(rel)
|
|
32
|
+
if not annotations:
|
|
33
|
+
click.echo(f"no history for {rel}")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
history = {oid for oid, _ in annotations}
|
|
37
|
+
name = _author_lookup(repo, history)
|
|
38
|
+
|
|
39
|
+
for oid, line in annotations:
|
|
40
|
+
author = name.get(oid, "?")
|
|
41
|
+
when = "? "
|
|
42
|
+
console.print(f"{oid} {author:<10} {when} {line}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _author_lookup(repo: Repository, oids: set[str]) -> dict[str, str]:
|
|
46
|
+
out: dict[str, str] = {}
|
|
47
|
+
for oid in oids:
|
|
48
|
+
try:
|
|
49
|
+
commit = repo.db.load_commit(oid)
|
|
50
|
+
except KeyError:
|
|
51
|
+
logger.warning("missing commit %s, skipping author lookup", oid)
|
|
52
|
+
continue
|
|
53
|
+
full = oid
|
|
54
|
+
out[full[:8]] = commit.author.name.split()[0]
|
|
55
|
+
return out
|