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.
Files changed (109) hide show
  1. pyoxidize-0.2.3/LICENSE +21 -0
  2. pyoxidize-0.2.3/PKG-INFO +204 -0
  3. pyoxidize-0.2.3/README.md +179 -0
  4. pyoxidize-0.2.3/oxidize/__init__.py +1 -0
  5. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/__init__.py +3 -3
  6. pyoxidize-0.2.3/oxidize/cli/commands/add.py +65 -0
  7. pyoxidize-0.2.3/oxidize/cli/commands/bisect.py +97 -0
  8. pyoxidize-0.2.3/oxidize/cli/commands/blame.py +55 -0
  9. pyoxidize-0.2.3/oxidize/cli/commands/branch.py +93 -0
  10. pyoxidize-0.2.3/oxidize/cli/commands/checkout.py +96 -0
  11. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/commit.py +5 -0
  12. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/diff.py +55 -9
  13. pyoxidize-0.2.3/oxidize/cli/commands/hooks.py +73 -0
  14. pyoxidize-0.2.3/oxidize/cli/commands/ignores.py +72 -0
  15. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/init.py +25 -18
  16. pyoxidize-0.2.3/oxidize/cli/commands/log.py +60 -0
  17. pyoxidize-0.2.3/oxidize/cli/commands/merge.py +105 -0
  18. pyoxidize-0.2.3/oxidize/cli/commands/notebook.py +112 -0
  19. pyoxidize-0.2.3/oxidize/cli/commands/remote.py +85 -0
  20. pyoxidize-0.2.3/oxidize/cli/commands/resolve.py +119 -0
  21. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/scan.py +14 -10
  22. pyoxidize-0.2.3/oxidize/cli/commands/show.py +37 -0
  23. pyoxidize-0.2.3/oxidize/cli/commands/stash.py +69 -0
  24. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/status.py +97 -75
  25. pyoxidize-0.2.3/oxidize/cli/commands/tag.py +69 -0
  26. pyoxidize-0.2.3/oxidize/cli/commands/undo.py +95 -0
  27. pyoxidize-0.2.3/oxidize/cli/main.py +52 -0
  28. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/repl.py +16 -7
  29. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/core/__init__.py +5 -5
  30. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/core/config.py +30 -30
  31. pyoxidize-0.2.3/oxidize/core/ignores.py +109 -0
  32. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/core/refs.py +70 -54
  33. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/core/repository.py +70 -54
  34. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/diff/__init__.py +3 -3
  35. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/diff/engine.py +91 -91
  36. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/index/__init__.py +3 -3
  37. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/index/staging.py +73 -73
  38. pyoxidize-0.2.3/oxidize/merge/text.py +52 -0
  39. pyoxidize-0.2.3/oxidize/merge/treewalk.py +112 -0
  40. pyoxidize-0.2.3/oxidize/network/__init__.py +0 -0
  41. pyoxidize-0.2.3/oxidize/network/github_api.py +114 -0
  42. pyoxidize-0.2.3/oxidize/network/github_remote.py +190 -0
  43. pyoxidize-0.2.3/oxidize/network/remote.py +281 -0
  44. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/objects/__init__.py +12 -12
  45. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/objects/types.py +164 -164
  46. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/provenance/agent.py +0 -1
  47. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/security/__init__.py +1 -1
  48. pyoxidize-0.2.3/oxidize/security/scanner.py +199 -0
  49. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/semantic/__init__.py +11 -4
  50. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/semantic/differ.py +20 -20
  51. pyoxidize-0.2.3/oxidize/semantic/entities.py +253 -0
  52. pyoxidize-0.2.3/oxidize/semantic/parser.py +40 -0
  53. pyoxidize-0.2.3/oxidize/staging/__init__.py +1 -0
  54. pyoxidize-0.2.3/oxidize/staging/stash.py +84 -0
  55. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/storage/__init__.py +4 -4
  56. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/storage/backend.py +71 -51
  57. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/storage/database.py +45 -45
  58. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/undo/__init__.py +1 -0
  59. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/undo/reverser.py +2 -2
  60. pyoxidize-0.2.3/oxidize/workflow/__init__.py +0 -0
  61. pyoxidize-0.2.3/oxidize/workflow/bisect.py +93 -0
  62. pyoxidize-0.2.3/oxidize/workflow/blame.py +71 -0
  63. pyoxidize-0.2.3/oxidize/workflow/hooks.py +123 -0
  64. pyoxidize-0.2.3/pyoxidize.egg-info/PKG-INFO +204 -0
  65. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/SOURCES.txt +36 -1
  66. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/requires.txt +2 -1
  67. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyproject.toml +58 -53
  68. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/setup.cfg +4 -4
  69. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_diff.py +28 -28
  70. pyoxidize-0.2.3/tests/test_github_remote.py +260 -0
  71. pyoxidize-0.2.3/tests/test_ignores.py +188 -0
  72. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_index.py +36 -35
  73. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_merge.py +2 -2
  74. pyoxidize-0.2.3/tests/test_merge_conflict_blame_remote.py +213 -0
  75. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_notebook.py +3 -3
  76. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_objects.py +48 -48
  77. pyoxidize-0.2.3/tests/test_refs_branches_tags.py +99 -0
  78. pyoxidize-0.2.3/tests/test_remote_fixes.py +232 -0
  79. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_repository.py +35 -33
  80. pyoxidize-0.2.3/tests/test_scanner.py +221 -0
  81. pyoxidize-0.2.3/tests/test_semantic.py +135 -0
  82. pyoxidize-0.2.3/tests/test_storage.py +86 -0
  83. pyoxidize-0.2.3/tests/test_stress.py +604 -0
  84. pyoxidize-0.2.3/tests/test_workflow.py +88 -0
  85. pyoxidize-0.2.0/PKG-INFO +0 -118
  86. pyoxidize-0.2.0/README.md +0 -96
  87. pyoxidize-0.2.0/oxidize/__init__.py +0 -1
  88. pyoxidize-0.2.0/oxidize/cli/commands/add.py +0 -28
  89. pyoxidize-0.2.0/oxidize/cli/commands/log.py +0 -40
  90. pyoxidize-0.2.0/oxidize/cli/commands/undo.py +0 -65
  91. pyoxidize-0.2.0/oxidize/cli/main.py +0 -26
  92. pyoxidize-0.2.0/oxidize/security/scanner.py +0 -65
  93. pyoxidize-0.2.0/oxidize/semantic/entities.py +0 -122
  94. pyoxidize-0.2.0/pyoxidize.egg-info/PKG-INFO +0 -118
  95. pyoxidize-0.2.0/tests/test_scanner.py +0 -65
  96. pyoxidize-0.2.0/tests/test_semantic.py +0 -73
  97. pyoxidize-0.2.0/tests/test_storage.py +0 -38
  98. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/cli/commands/__init__.py +0 -0
  99. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/merge/__init__.py +0 -0
  100. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/merge/structured.py +0 -0
  101. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/notebook/__init__.py +0 -0
  102. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/notebook/differ.py +0 -0
  103. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/provenance/__init__.py +0 -0
  104. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/oxidize/undo/journal.py +0 -0
  105. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/dependency_links.txt +0 -0
  106. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/entry_points.txt +0 -0
  107. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/pyoxidize.egg-info/top_level.txt +0 -0
  108. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_provenance.py +0 -0
  109. {pyoxidize-0.2.0 → pyoxidize-0.2.3}/tests/test_undo.py +0 -0
@@ -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.
@@ -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
+ [![ci](https://github.com/siddhant-bayas/oxidize/actions/workflows/ci.yml/badge.svg)](https://github.com/siddhant-bayas/oxidize/actions/workflows/ci.yml)
31
+ [![pypi](https://img.shields.io/pypi/v/pyoxidize)](https://pypi.org/project/pyoxidize/)
32
+ [![python](https://img.shields.io/badge/python-3.11%2B-blue)](https://pypi.org/project/pyoxidize/)
33
+ [![size](https://img.shields.io/badge/pip_install_size-tiny-blue)](#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
+ [![ci](https://github.com/siddhant-bayas/oxidize/actions/workflows/ci.yml/badge.svg)](https://github.com/siddhant-bayas/oxidize/actions/workflows/ci.yml)
6
+ [![pypi](https://img.shields.io/pypi/v/pyoxidize)](https://pypi.org/project/pyoxidize/)
7
+ [![python](https://img.shields.io/badge/python-3.11%2B-blue)](https://pypi.org/project/pyoxidize/)
8
+ [![size](https://img.shields.io/badge/pip_install_size-tiny-blue)](#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