gitpure 0.1.1__tar.gz → 0.1.2__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.
@@ -19,6 +19,25 @@ permissions:
19
19
  contents: read
20
20
 
21
21
  jobs:
22
+ tests:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
26
+ - name: Set up Python
27
+ uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
28
+ with:
29
+ python-version: '3.13'
30
+ - name: Set up Rust toolchain
31
+ uses: dtolnay/rust-toolchain@stable
32
+ - name: Install build tooling
33
+ run: |
34
+ python -m pip install --upgrade pip
35
+ pip install maturin
36
+ - name: Install project with test extras
37
+ run: pip install .[test]
38
+ - name: Run pytest
39
+ run: pytest -s
40
+
22
41
  linux:
23
42
  runs-on: ${{ matrix.platform.runner }}
24
43
  strategy:
@@ -37,12 +56,12 @@ jobs:
37
56
  - runner: ubuntu-22.04
38
57
  target: ppc64le
39
58
  steps:
40
- - uses: actions/checkout@v4
41
- - uses: actions/setup-python@v5
59
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
60
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
42
61
  with:
43
62
  python-version: 3.x
44
63
  - name: Build wheels
45
- uses: PyO3/maturin-action@v1
64
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1
46
65
  with:
47
66
  target: ${{ matrix.platform.target }}
48
67
  args: --release --out dist --find-interpreter
@@ -50,7 +69,7 @@ jobs:
50
69
  # ring requires a newer version of manylinux on aarch64
51
70
  manylinux: ${{ matrix.platform.target == 'aarch64' && '2_28' || 'auto' }}
52
71
  - name: Upload wheels
53
- uses: actions/upload-artifact@v4
72
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
54
73
  with:
55
74
  name: wheels-linux-${{ matrix.platform.target }}
56
75
  path: dist
@@ -69,19 +88,19 @@ jobs:
69
88
  - runner: ubuntu-22.04
70
89
  target: armv7
71
90
  steps:
72
- - uses: actions/checkout@v4
73
- - uses: actions/setup-python@v5
91
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
92
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
74
93
  with:
75
94
  python-version: 3.x
76
95
  - name: Build wheels
77
- uses: PyO3/maturin-action@v1
96
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1
78
97
  with:
79
98
  target: ${{ matrix.platform.target }}
80
99
  args: --release --out dist --find-interpreter
81
100
  sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
82
101
  manylinux: musllinux_1_2
83
102
  - name: Upload wheels
84
- uses: actions/upload-artifact@v4
103
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
85
104
  with:
86
105
  name: wheels-musllinux-${{ matrix.platform.target }}
87
106
  path: dist
@@ -96,19 +115,19 @@ jobs:
96
115
  - runner: windows-latest
97
116
  target: x86
98
117
  steps:
99
- - uses: actions/checkout@v4
100
- - uses: actions/setup-python@v5
118
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
119
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
101
120
  with:
102
121
  python-version: 3.x
103
122
  architecture: ${{ matrix.platform.target }}
104
123
  - name: Build wheels
105
- uses: PyO3/maturin-action@v1
124
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1
106
125
  with:
107
126
  target: ${{ matrix.platform.target }}
108
127
  args: --release --out dist --find-interpreter
109
128
  sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
110
129
  - name: Upload wheels
111
- uses: actions/upload-artifact@v4
130
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
112
131
  with:
113
132
  name: wheels-windows-${{ matrix.platform.target }}
114
133
  path: dist
@@ -123,33 +142,42 @@ jobs:
123
142
  - runner: macos-14
124
143
  target: aarch64
125
144
  steps:
126
- - uses: actions/checkout@v4
127
- - uses: actions/setup-python@v5
145
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
146
+ - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
128
147
  with:
129
148
  python-version: 3.x
130
149
  - name: Build wheels
131
- uses: PyO3/maturin-action@v1
150
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1
132
151
  with:
133
152
  target: ${{ matrix.platform.target }}
134
153
  args: --release --out dist --find-interpreter
135
154
  sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
136
155
  - name: Upload wheels
137
- uses: actions/upload-artifact@v4
156
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
138
157
  with:
139
158
  name: wheels-macos-${{ matrix.platform.target }}
140
159
  path: dist
141
160
 
161
+ success:
162
+ name: Success
163
+ runs-on: ubuntu-latest
164
+ needs: [tests, linux, musllinux, windows, macos, sdist]
165
+ if: ${{ !cancelled() && !failure() }}
166
+ steps:
167
+ - name: Mark success
168
+ run: echo "All required jobs completed successfully."
169
+
142
170
  sdist:
143
171
  runs-on: ubuntu-latest
144
172
  steps:
145
- - uses: actions/checkout@v4
173
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
146
174
  - name: Build sdist
147
- uses: PyO3/maturin-action@v1
175
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1
148
176
  with:
149
177
  command: sdist
150
178
  args: --out dist
151
179
  - name: Upload sdist
152
- uses: actions/upload-artifact@v4
180
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
153
181
  with:
154
182
  name: wheels-sdist
155
183
  path: dist
@@ -158,7 +186,7 @@ jobs:
158
186
  name: Release
159
187
  runs-on: ubuntu-latest
160
188
  if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
161
- needs: [linux, musllinux, windows, macos, sdist]
189
+ needs: [tests, linux, musllinux, windows, macos, sdist]
162
190
  permissions:
163
191
  # Use to sign the release artifacts
164
192
  id-token: write
@@ -170,14 +198,14 @@ jobs:
170
198
  name: pypi
171
199
  url: https://pypi.org/p/gitpure
172
200
  steps:
173
- - uses: actions/download-artifact@v4
201
+ - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
174
202
  - name: Generate artifact attestation
175
- uses: actions/attest-build-provenance@v2
203
+ uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
176
204
  with:
177
205
  subject-path: 'wheels-*/*'
178
206
  - name: Publish to PyPI
179
207
  if: ${{ startsWith(github.ref, 'refs/tags/') }}
180
- uses: PyO3/maturin-action@v1
208
+ uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1
181
209
  with:
182
210
  command: upload
183
211
  args: --non-interactive --skip-existing wheels-*/*
@@ -406,7 +406,7 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
406
406
 
407
407
  [[package]]
408
408
  name = "gitpure"
409
- version = "0.1.1"
409
+ version = "0.1.2"
410
410
  dependencies = [
411
411
  "gix",
412
412
  "pyo3",
@@ -1814,9 +1814,9 @@ dependencies = [
1814
1814
 
1815
1815
  [[package]]
1816
1816
  name = "pyo3"
1817
- version = "0.25.1"
1817
+ version = "0.26.0"
1818
1818
  source = "registry+https://github.com/rust-lang/crates.io-index"
1819
- checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a"
1819
+ checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383"
1820
1820
  dependencies = [
1821
1821
  "indoc",
1822
1822
  "libc",
@@ -1831,19 +1831,18 @@ dependencies = [
1831
1831
 
1832
1832
  [[package]]
1833
1833
  name = "pyo3-build-config"
1834
- version = "0.25.1"
1834
+ version = "0.26.0"
1835
1835
  source = "registry+https://github.com/rust-lang/crates.io-index"
1836
- checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598"
1836
+ checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f"
1837
1837
  dependencies = [
1838
- "once_cell",
1839
1838
  "target-lexicon",
1840
1839
  ]
1841
1840
 
1842
1841
  [[package]]
1843
1842
  name = "pyo3-ffi"
1844
- version = "0.25.1"
1843
+ version = "0.26.0"
1845
1844
  source = "registry+https://github.com/rust-lang/crates.io-index"
1846
- checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c"
1845
+ checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105"
1847
1846
  dependencies = [
1848
1847
  "libc",
1849
1848
  "pyo3-build-config",
@@ -1851,9 +1850,9 @@ dependencies = [
1851
1850
 
1852
1851
  [[package]]
1853
1852
  name = "pyo3-macros"
1854
- version = "0.25.1"
1853
+ version = "0.26.0"
1855
1854
  source = "registry+https://github.com/rust-lang/crates.io-index"
1856
- checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50"
1855
+ checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded"
1857
1856
  dependencies = [
1858
1857
  "proc-macro2",
1859
1858
  "pyo3-macros-backend",
@@ -1863,9 +1862,9 @@ dependencies = [
1863
1862
 
1864
1863
  [[package]]
1865
1864
  name = "pyo3-macros-backend"
1866
- version = "0.25.1"
1865
+ version = "0.26.0"
1867
1866
  source = "registry+https://github.com/rust-lang/crates.io-index"
1868
- checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc"
1867
+ checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf"
1869
1868
  dependencies = [
1870
1869
  "heck",
1871
1870
  "proc-macro2",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "gitpure"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  edition = "2024"
5
5
  authors = ["Max Dymond <cmeister2@gmail.com>"]
6
6
  license = "MIT"
@@ -17,7 +17,7 @@ name = "gitpure"
17
17
  crate-type = ["cdylib"]
18
18
 
19
19
  [dependencies]
20
- pyo3 = "0.25.0"
20
+ pyo3 = "0.26.0"
21
21
 
22
22
  [dependencies.gix]
23
23
  version = "0.73.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpure
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -20,6 +20,7 @@ Classifier: Topic :: Software Development :: Version Control
20
20
  Classifier: Topic :: Software Development :: Version Control :: Git
21
21
  Classifier: Typing :: Typed
22
22
  Requires-Dist: pytest ; extra == 'test'
23
+ Requires-Dist: gitpython>=3.1 ; extra == 'test'
23
24
  Provides-Extra: test
24
25
  License-File: LICENSE
25
26
  Summary: A Python git library implemented in Rust using gitoxide
@@ -0,0 +1,108 @@
1
+ """
2
+ Type stubs for gitpure - A pure git Python module implemented in Rust.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Type
7
+
8
+
9
+ class Commit:
10
+ """Lightweight representation of a git commit."""
11
+
12
+ @property
13
+ def hexsha(self) -> str:
14
+ """Hexadecimal object id of the commit."""
15
+ ...
16
+
17
+
18
+ class Head:
19
+ """A named reference to a commit, mirroring GitPython's Head."""
20
+
21
+ @property
22
+ def name(self) -> str:
23
+ """The short name of the head (branch)."""
24
+ ...
25
+
26
+ @property
27
+ def commit(self) -> Commit | None:
28
+ """The commit the head points to, if available."""
29
+ ...
30
+
31
+
32
+ class Tag:
33
+ """A tag reference with optional peeled commit."""
34
+
35
+ @property
36
+ def name(self) -> str:
37
+ """The short name of the tag."""
38
+ ...
39
+
40
+ @property
41
+ def commit(self) -> Commit | None:
42
+ """The commit the tag ultimately refers to, if available."""
43
+ ...
44
+
45
+ class Repo:
46
+ """A git repository wrapper using gix (gitoxide)."""
47
+
48
+ @property
49
+ def git_dir(self) -> Path:
50
+ """
51
+ Path to the .git directory of the repository.
52
+
53
+ Returns:
54
+ The absolute path to the .git directory as a pathlib.Path object
55
+ """
56
+ ...
57
+
58
+ def branches(self) -> list[str]:
59
+ """Return the names of all local branches in the repository."""
60
+ ...
61
+
62
+ @property
63
+ def working_tree_dir(self) -> Path | None:
64
+ """Return the working tree directory of the repository, if it exists."""
65
+ ...
66
+
67
+ @property
68
+ def is_bare(self) -> bool:
69
+ """Whether the repository is bare (has no working tree)."""
70
+ ...
71
+
72
+ @property
73
+ def active_branch(self) -> Head | None:
74
+ """Current active branch as a Head object, if available."""
75
+ ...
76
+
77
+ @property
78
+ def head(self) -> Head | None:
79
+ """HEAD reference as a Head object, if available."""
80
+ ...
81
+
82
+ @property
83
+ def heads(self) -> list[Head]:
84
+ """Return all local heads (branches) in the repository."""
85
+ ...
86
+
87
+ @property
88
+ def tags(self) -> list[Tag]:
89
+ """Return all tags in the repository."""
90
+ ...
91
+
92
+ @classmethod
93
+ def clone_from(cls: Type["Repo"], url: str, to_path: str, bare: bool = False) -> "Repo":
94
+ """
95
+ Clone a repository from a URL to a local path.
96
+
97
+ Args:
98
+ url: The URL of the repository to clone
99
+ to_path: The local path where the repository should be cloned
100
+ bare: Whether to create a bare repository (default: False)
101
+
102
+ Returns:
103
+ A new Repo instance representing the cloned repository
104
+
105
+ Raises:
106
+ RuntimeError: If the clone operation fails
107
+ """
108
+ ...
@@ -45,7 +45,10 @@ Repository = "https://github.com/cmeister2/gitpure"
45
45
  "Source Code" = "https://github.com/cmeister2/gitpure"
46
46
 
47
47
  [project.optional-dependencies]
48
- test = ["pytest"]
48
+ test = [
49
+ "pytest",
50
+ "GitPython>=3.1",
51
+ ]
49
52
 
50
53
  [tool.maturin]
51
54
  features = ["pyo3/extension-module"]
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:best-practices",
5
+ ":automergeAll",
6
+ ":assignAndReview(cmeister2)"
7
+ ]
8
+ }
@@ -0,0 +1,329 @@
1
+ use gix::bstr::ByteSlice;
2
+ use gix::Id;
3
+ use pyo3::exceptions::{PyRuntimeError, PyTypeError};
4
+ use pyo3::{prelude::*, types::PyType};
5
+ use std::collections::BTreeSet;
6
+ use std::path::Path;
7
+
8
+ #[pyclass]
9
+ #[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone)]
10
+ struct Commit {
11
+ hexsha: String,
12
+ }
13
+
14
+ #[pymethods]
15
+ impl Commit {
16
+ #[getter]
17
+ fn hexsha(&self) -> &str {
18
+ &self.hexsha
19
+ }
20
+
21
+ fn __repr__(&self) -> PyResult<String> {
22
+ Ok(format!("<gitpure.Commit \"{}\">", self.hexsha))
23
+ }
24
+
25
+ fn __str__(&self) -> PyResult<String> {
26
+ Ok(self.hexsha.clone())
27
+ }
28
+ }
29
+
30
+ impl Commit {
31
+ fn from_id(id_: Id) -> Self {
32
+ Self {
33
+ hexsha: id_.detach().to_string(),
34
+ }
35
+ }
36
+ }
37
+
38
+ #[pyclass]
39
+ #[derive(Hash, PartialEq, Eq, PartialOrd, Ord)]
40
+ struct Head {
41
+ name: String,
42
+ commit: Option<Commit>,
43
+ }
44
+
45
+ #[pymethods]
46
+ impl Head {
47
+ #[getter]
48
+ fn name(&self) -> &str {
49
+ &self.name
50
+ }
51
+
52
+ #[getter]
53
+ fn commit(&self) -> PyResult<Option<Commit>> {
54
+ Ok(self.commit.clone())
55
+ }
56
+
57
+ fn __repr__(&self) -> PyResult<String> {
58
+ Ok(format!("<gitpure.Head \"{}\">", self.name))
59
+ }
60
+
61
+ fn __str__(&self) -> PyResult<String> {
62
+ Ok(self.name.clone())
63
+ }
64
+ }
65
+
66
+ impl Head {
67
+ fn from_reference(reference: gix::Reference) -> Self {
68
+ let mut reference = reference;
69
+ let name = shorten_reference_name(&reference);
70
+ let commit = reference
71
+ .peel_to_id_in_place()
72
+ .ok()
73
+ .map(Commit::from_id);
74
+ Self { name, commit }
75
+ }
76
+ }
77
+
78
+ #[pyclass(module = "gitpure")]
79
+ #[derive(Hash, PartialEq, Eq, PartialOrd, Ord)]
80
+ struct Tag {
81
+ name: String,
82
+ commit: Option<Commit>,
83
+ }
84
+
85
+ #[pymethods]
86
+ impl Tag {
87
+ #[getter]
88
+ fn name(&self) -> &str {
89
+ &self.name
90
+ }
91
+
92
+ #[getter]
93
+ fn commit(&self) -> PyResult<Option<Commit>> {
94
+ Ok(self.commit.clone())
95
+ }
96
+
97
+ fn __repr__(&self) -> PyResult<String> {
98
+ Ok(format!("<gitpure.Tag \"{}\">", self.name))
99
+ }
100
+
101
+ fn __str__(&self) -> PyResult<String> {
102
+ Ok(self.name.clone())
103
+ }
104
+ }
105
+
106
+ impl Tag {
107
+ fn from_reference(reference: gix::Reference) -> Self {
108
+ let mut reference = reference;
109
+ let name = shorten_reference_name(&reference);
110
+ let commit = reference
111
+ .peel_to_id_in_place()
112
+ .ok()
113
+ .map(Commit::from_id);
114
+ Self { name, commit }
115
+ }
116
+ }
117
+
118
+ #[pyclass(unsendable)]
119
+ struct Repo {
120
+ inner: gix::Repository,
121
+ }
122
+
123
+ fn path_to_python(py: Python<'_>, path: &Path) -> PyResult<PyObject> {
124
+ let pathlib = py.import("pathlib")?;
125
+ let path_class = pathlib.getattr("Path")?;
126
+ Ok(path_class.call1((path.as_os_str(),))?.into())
127
+ }
128
+
129
+ fn shorten_reference_name(reference: &gix::Reference) -> String {
130
+ let short_name = reference.name().shorten();
131
+ match short_name.to_str() {
132
+ Ok(valid) => valid.to_owned(),
133
+ Err(_) => short_name.to_string(),
134
+ }
135
+ }
136
+
137
+ #[pymethods]
138
+ impl Repo {
139
+ /// The path to the `.git` directory of the repository.
140
+ #[getter]
141
+ fn git_dir(&self, py: Python) -> PyResult<PyObject> {
142
+ path_to_python(py, self.inner.git_dir())
143
+ }
144
+
145
+ /// The path to the working tree directory, if present.
146
+ #[getter]
147
+ fn working_tree_dir(&self, py: Python) -> PyResult<Option<PyObject>> {
148
+ match self.inner.workdir() {
149
+ Some(path) => Ok(Some(path_to_python(py, path)?)),
150
+ None => Ok(None),
151
+ }
152
+ }
153
+
154
+ /// Returns true if the repository is bare.
155
+ #[getter]
156
+ fn is_bare(&self) -> bool {
157
+ self.inner.workdir().is_none()
158
+ }
159
+
160
+ /// Clone a git repository from the given URL into the specified path.
161
+ #[classmethod]
162
+ #[pyo3(signature = (url, to_path, bare=false))]
163
+ fn clone_from(
164
+ _cls: &Bound<'_, PyType>,
165
+ url: &str,
166
+ to_path: &str,
167
+ bare: bool,
168
+ ) -> PyResult<Self> {
169
+ let target_path = Path::new(to_path);
170
+
171
+ // Configure the repository kind based on bare flag
172
+ let kind = if bare {
173
+ gix::create::Kind::Bare
174
+ } else {
175
+ gix::create::Kind::WithWorktree
176
+ };
177
+
178
+ let mut prepare_clone = gix::clone::PrepareFetch::new(
179
+ url,
180
+ target_path,
181
+ kind,
182
+ gix::create::Options::default(),
183
+ gix::open::Options::isolated(),
184
+ )
185
+ .map_err(|e| PyRuntimeError::new_err(format!("Failed to prepare clone: {}", e)))?;
186
+
187
+ let (mut prepare_checkout, _outcome) = prepare_clone
188
+ .fetch_then_checkout(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
189
+ .map_err(|e| PyRuntimeError::new_err(format!("Failed to fetch repository: {}", e)))?;
190
+
191
+ if bare {
192
+ let repo = prepare_checkout.persist();
193
+ Ok(Repo { inner: repo })
194
+ } else {
195
+ let (repo, _checkout_outcome) = prepare_checkout
196
+ .main_worktree(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
197
+ .map_err(|e| {
198
+ PyRuntimeError::new_err(format!("Failed to checkout worktree: {}", e))
199
+ })?;
200
+
201
+ Ok(Repo { inner: repo })
202
+ }
203
+ }
204
+
205
+ /// Return the names of all local branches in the repository.
206
+ fn branches(&self) -> PyResult<Vec<String>> {
207
+ let platform = self.inner.references().map_err(|err| {
208
+ PyRuntimeError::new_err(format!("Failed to access references: {err}"))
209
+ })?;
210
+
211
+ let iter = platform.local_branches().map_err(|err| {
212
+ PyRuntimeError::new_err(format!("Failed to list local branches: {err}"))
213
+ })?;
214
+
215
+ let branches: BTreeSet<String> = iter
216
+ .map(|reference_result| {
217
+ reference_result.map_err(|err| {
218
+ PyRuntimeError::new_err(format!(
219
+ "Failed to load branch reference: {err}"
220
+ ))
221
+ })
222
+ })
223
+ .collect::<Result<Vec<_>, _>>()?
224
+ .into_iter()
225
+ .map(|reference| shorten_reference_name(&reference))
226
+ .collect();
227
+
228
+ Ok(branches.into_iter().collect())
229
+ }
230
+
231
+ /// Return the path to the current active branch, if any.
232
+ #[getter]
233
+ fn active_branch(&self) -> PyResult<Option<Head>> {
234
+ if self.is_bare() {
235
+ return Ok(None);
236
+ }
237
+
238
+ let head = self
239
+ .inner
240
+ .head()
241
+ .map_err(|err| PyRuntimeError::new_err(format!("Failed to access HEAD: {err}")))?;
242
+
243
+ // If HEAD is detached then raise a TypeError like GitPython does
244
+ if head.is_detached() {
245
+ return Err(PyTypeError::new_err(
246
+ "HEAD is detached; no active branch".to_string(),
247
+ ));
248
+ }
249
+
250
+ if head.is_unborn() {
251
+ return Ok(None);
252
+ }
253
+
254
+ Ok(head.try_into_referent().map(Head::from_reference))
255
+ }
256
+
257
+ /// Return the commit id pointed to by HEAD, if available.
258
+ #[getter]
259
+ fn head(&self) -> PyResult<Option<Head>> {
260
+ let head = self
261
+ .inner
262
+ .head()
263
+ .map_err(|err| PyRuntimeError::new_err(format!("Failed to access HEAD: {err}")))?;
264
+
265
+ Ok(head.try_into_referent().map(Head::from_reference))
266
+ }
267
+
268
+ /// List all local heads (branches) in the repository.
269
+ #[getter]
270
+ fn heads(&self) -> PyResult<Vec<Head>> {
271
+ let platform = self.inner.references().map_err(|err| {
272
+ PyRuntimeError::new_err(format!("Failed to access references: {err}"))
273
+ })?;
274
+
275
+ let iter = platform.local_branches().map_err(|err| {
276
+ PyRuntimeError::new_err(format!("Failed to list local branches: {err}"))
277
+ })?;
278
+
279
+ let heads: BTreeSet<Head> = iter
280
+ .map(|reference_result| {
281
+ reference_result.map_err(|err| {
282
+ PyRuntimeError::new_err(format!(
283
+ "Failed to load branch reference: {err}"
284
+ ))
285
+ })
286
+ })
287
+ .collect::<Result<Vec<_>, _>>()?
288
+ .into_iter()
289
+ .map(Head::from_reference)
290
+ .collect::<BTreeSet<Head>>();
291
+
292
+ Ok(heads.into_iter().collect())
293
+ }
294
+
295
+ /// List all tags in the repository.
296
+ #[getter]
297
+ fn tags(&self) -> PyResult<Vec<Tag>> {
298
+ let platform = self.inner.references().map_err(|err| {
299
+ PyRuntimeError::new_err(format!("Failed to access references: {err}"))
300
+ })?;
301
+
302
+ let iter = platform.tags().map_err(|err| {
303
+ PyRuntimeError::new_err(format!("Failed to list tags: {err}"))
304
+ })?;
305
+
306
+ let tags: BTreeSet<Tag> = iter
307
+ .map(|reference_result| {
308
+ reference_result.map_err(|err| {
309
+ PyRuntimeError::new_err(format!("Failed to load tag reference: {err}"))
310
+ })
311
+ })
312
+ .collect::<Result<Vec<_>, _>>()?
313
+ .into_iter()
314
+ .map(Tag::from_reference)
315
+ .collect();
316
+
317
+ Ok(tags.into_iter().collect())
318
+ }
319
+ }
320
+
321
+ /// A pure git Python module implemented in Rust.
322
+ #[pymodule]
323
+ fn gitpure(m: &Bound<'_, PyModule>) -> PyResult<()> {
324
+ m.add_class::<Commit>()?;
325
+ m.add_class::<Head>()?;
326
+ m.add_class::<Tag>()?;
327
+ m.add_class::<Repo>()?;
328
+ Ok(())
329
+ }
@@ -0,0 +1,178 @@
1
+ import tempfile
2
+ from pathlib import Path
3
+
4
+ from git import Repo as GitPythonRepo
5
+
6
+ from gitpure import Repo
7
+
8
+ def test_clone_and_git_dir_worktree():
9
+ """Test cloning a worktree repository and git_dir property"""
10
+ with tempfile.TemporaryDirectory() as tmpdir:
11
+ repo_url = "https://github.com/cmeister2/gitpure"
12
+ repo_path = Path(tmpdir) / "gitpure"
13
+ repo = Repo.clone_from(repo_url, str(repo_path))
14
+
15
+ # Test basic clone functionality
16
+ assert (repo_path / ".git").exists()
17
+ assert (repo_path / "README.md").exists()
18
+
19
+ # Test git_dir property
20
+ git_dir = repo.git_dir
21
+ expected_git_dir = repo_path / ".git"
22
+
23
+ # Should return the correct path
24
+ assert Path(git_dir) == expected_git_dir
25
+
26
+ # The returned path should exist and be a directory
27
+ assert Path(git_dir).exists()
28
+ assert Path(git_dir).is_dir()
29
+
30
+ # Should contain typical git directory contents
31
+ git_dir_path = Path(git_dir)
32
+ assert (git_dir_path / "HEAD").exists()
33
+ assert (git_dir_path / "config").exists()
34
+ assert (git_dir_path / "objects").exists()
35
+ assert (git_dir_path / "refs").exists()
36
+
37
+ # Test git_dir property type and consistency
38
+ assert isinstance(git_dir, Path)
39
+ assert git_dir.is_absolute()
40
+
41
+ # Multiple calls should return the same result
42
+ git_dir2 = repo.git_dir
43
+ git_dir3 = repo.git_dir
44
+ assert git_dir == git_dir2 == git_dir3
45
+
46
+ # Metadata parity with GitPython
47
+ gitpython_repo = GitPythonRepo(str(repo_path))
48
+
49
+ gitpython_worktree = gitpython_repo.working_tree_dir
50
+ assert gitpython_worktree is not None
51
+ assert repo.working_tree_dir == Path(gitpython_worktree)
52
+ assert repo.is_bare is False
53
+ active_branch = repo.active_branch
54
+ assert active_branch is not None
55
+ assert active_branch.name == gitpython_repo.active_branch.name
56
+ assert active_branch.commit is not None
57
+ assert active_branch.commit.hexsha == gitpython_repo.active_branch.commit.hexsha
58
+
59
+ head = repo.head
60
+ assert head is not None
61
+ assert head.commit is not None
62
+ assert head.commit.hexsha == gitpython_repo.head.commit.hexsha
63
+ gitpure_heads = repo.heads
64
+ assert isinstance(gitpure_heads, list)
65
+ assert all(hasattr(head, "name") and hasattr(head, "commit") for head in gitpure_heads)
66
+
67
+ assert sorted(head.name for head in gitpure_heads) == sorted(
68
+ head.name for head in gitpython_repo.heads
69
+ )
70
+
71
+ assert sorted(head.commit.hexsha for head in gitpure_heads if head.commit) == sorted(
72
+ head.commit.hexsha for head in gitpython_repo.heads if head.commit
73
+ )
74
+
75
+ gitpure_tags = repo.tags
76
+ assert isinstance(gitpure_tags, list)
77
+ assert all(hasattr(tag, "name") and hasattr(tag, "commit") for tag in gitpure_tags)
78
+
79
+ assert sorted(tag.name for tag in gitpure_tags) == sorted(
80
+ tag.name for tag in gitpython_repo.tags
81
+ )
82
+
83
+ gitpure_tag_commits = {
84
+ tag.name: (tag.commit.hexsha if tag.commit else None)
85
+ for tag in gitpure_tags
86
+ }
87
+ gitpython_tag_commits = {
88
+ tag.name: (tag.commit.hexsha if tag.commit else None)
89
+ for tag in gitpython_repo.tags
90
+ }
91
+ assert gitpure_tag_commits == gitpython_tag_commits
92
+
93
+ def test_clone_and_git_dir_bare():
94
+ """Test cloning a bare repository and git_dir property"""
95
+ with tempfile.TemporaryDirectory() as tmpdir:
96
+ repo_url = "https://github.com/cmeister2/gitpure"
97
+ repo_path = Path(tmpdir) / "gitpure.git"
98
+ repo = Repo.clone_from(repo_url, str(repo_path), bare=True)
99
+
100
+ # Test bare clone functionality
101
+ # Should NOT have .git subdirectory (it IS the git directory)
102
+ assert not (repo_path / ".git").exists()
103
+
104
+ # Should have git files directly in the root
105
+ assert (repo_path / "HEAD").exists()
106
+ assert (repo_path / "config").exists()
107
+ assert (repo_path / "objects").exists()
108
+ assert (repo_path / "refs").exists()
109
+
110
+ # Should NOT have working tree files
111
+ assert not (repo_path / "README.md").exists()
112
+
113
+ # Test git_dir property for bare repo
114
+ git_dir = repo.git_dir
115
+
116
+ # For bare repos, git_dir should be the repo directory itself
117
+ assert Path(git_dir) == repo_path
118
+
119
+ # The returned path should exist and be a directory
120
+ assert Path(git_dir).exists()
121
+ assert Path(git_dir).is_dir()
122
+
123
+ # Test git_dir property type
124
+ assert isinstance(git_dir, Path)
125
+ assert git_dir.is_absolute()
126
+
127
+ # Metadata parity with GitPython
128
+ gitpython_repo = GitPythonRepo(str(repo_path))
129
+
130
+ assert gitpython_repo.working_tree_dir is None
131
+ assert repo.working_tree_dir is None
132
+ assert repo.is_bare is True
133
+ assert repo.active_branch is None
134
+
135
+ head = repo.head
136
+ assert head is not None
137
+ assert head.commit is not None
138
+ assert head.commit.hexsha == gitpython_repo.head.commit.hexsha
139
+ gitpure_heads = repo.heads
140
+ assert sorted(head.name for head in gitpure_heads) == sorted(
141
+ head.name for head in gitpython_repo.heads
142
+ )
143
+
144
+ gitpure_tags = repo.tags
145
+ assert sorted(tag.name for tag in gitpure_tags) == sorted(
146
+ tag.name for tag in gitpython_repo.tags
147
+ )
148
+
149
+ gitpure_tag_commits = {
150
+ tag.name: (tag.commit.hexsha if tag.commit else None)
151
+ for tag in gitpure_tags
152
+ }
153
+ gitpython_tag_commits = {
154
+ tag.name: (tag.commit.hexsha if tag.commit else None)
155
+ for tag in gitpython_repo.tags
156
+ }
157
+ assert gitpure_tag_commits == gitpython_tag_commits
158
+
159
+
160
+ def test_branch_listing_matches_gitpython():
161
+ """Ensure branch listings are aligned with GitPython."""
162
+ with tempfile.TemporaryDirectory() as tmpdir:
163
+ repo_url = "https://github.com/cmeister2/gitpure"
164
+ repo_path = Path(tmpdir) / "gitpure"
165
+ repo = Repo.clone_from(repo_url, str(repo_path))
166
+
167
+ gitpure_branches = repo.branches()
168
+ assert isinstance(gitpure_branches, list)
169
+ assert all(isinstance(branch, str) for branch in gitpure_branches)
170
+
171
+ gitpython_repo = GitPythonRepo(str(repo_path))
172
+ gitpython_branches = sorted(head.name for head in gitpython_repo.branches)
173
+
174
+ assert gitpure_branches == gitpython_branches
175
+
176
+ # heads property should mirror branches()
177
+ gitpure_heads = repo.heads
178
+ assert [head.name for head in gitpure_heads] == gitpure_branches
gitpure-0.1.1/gitpure.pyi DELETED
@@ -1,37 +0,0 @@
1
- """
2
- Type stubs for gitpure - A pure git Python module implemented in Rust.
3
- """
4
-
5
- from pathlib import Path
6
- from typing import Type
7
-
8
- class Repo:
9
- """A git repository wrapper using gix (gitoxide)."""
10
-
11
- @property
12
- def git_dir(self) -> Path:
13
- """
14
- Path to the .git directory of the repository.
15
-
16
- Returns:
17
- The absolute path to the .git directory as a pathlib.Path object
18
- """
19
- ...
20
-
21
- @classmethod
22
- def clone_from(cls: Type["Repo"], url: str, to_path: str, bare: bool = False) -> "Repo":
23
- """
24
- Clone a repository from a URL to a local path.
25
-
26
- Args:
27
- url: The URL of the repository to clone
28
- to_path: The local path where the repository should be cloned
29
- bare: Whether to create a bare repository (default: False)
30
-
31
- Returns:
32
- A new Repo instance representing the cloned repository
33
-
34
- Raises:
35
- RuntimeError: If the clone operation fails
36
- """
37
- ...
gitpure-0.1.1/src/lib.rs DELETED
@@ -1,73 +0,0 @@
1
- use pyo3::exceptions::PyRuntimeError;
2
- use pyo3::{prelude::*, types::PyType};
3
- use std::path::Path;
4
-
5
- #[pyclass(unsendable)]
6
- struct Repo {
7
- inner: gix::Repository,
8
- }
9
-
10
- #[pymethods]
11
- impl Repo {
12
- /// The path to the `.git` directory of the repository.
13
- #[getter]
14
- fn git_dir(&self, py: Python) -> PyResult<PyObject> {
15
- let git_dir_path = self.inner.git_dir();
16
- let pathlib = py.import("pathlib")?;
17
- let path_class = pathlib.getattr("Path")?;
18
- let path_obj = path_class.call1((git_dir_path.as_os_str(),))?;
19
- Ok(path_obj.into())
20
- }
21
-
22
- /// Clone a git repository from the given URL into the specified path.
23
- #[classmethod]
24
- #[pyo3(signature = (url, to_path, bare=false))]
25
- fn clone_from(
26
- _cls: &Bound<'_, PyType>,
27
- url: &str,
28
- to_path: &str,
29
- bare: bool,
30
- ) -> PyResult<Self> {
31
- let target_path = Path::new(to_path);
32
-
33
- // Configure the repository kind based on bare flag
34
- let kind = if bare {
35
- gix::create::Kind::Bare
36
- } else {
37
- gix::create::Kind::WithWorktree
38
- };
39
-
40
- let mut prepare_clone = gix::clone::PrepareFetch::new(
41
- url,
42
- target_path,
43
- kind,
44
- gix::create::Options::default(),
45
- gix::open::Options::isolated(),
46
- )
47
- .map_err(|e| PyRuntimeError::new_err(format!("Failed to prepare clone: {}", e)))?;
48
-
49
- let (mut prepare_checkout, _outcome) = prepare_clone
50
- .fetch_then_checkout(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
51
- .map_err(|e| PyRuntimeError::new_err(format!("Failed to fetch repository: {}", e)))?;
52
-
53
- if bare {
54
- let repo = prepare_checkout.persist();
55
- Ok(Repo { inner: repo })
56
- } else {
57
- let (repo, _checkout_outcome) = prepare_checkout
58
- .main_worktree(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
59
- .map_err(|e| {
60
- PyRuntimeError::new_err(format!("Failed to checkout worktree: {}", e))
61
- })?;
62
-
63
- Ok(Repo { inner: repo })
64
- }
65
- }
66
- }
67
-
68
- /// A pure git Python module implemented in Rust.
69
- #[pymodule]
70
- fn gitpure(m: &Bound<'_, PyModule>) -> PyResult<()> {
71
- m.add_class::<Repo>()?;
72
- Ok(())
73
- }
@@ -1,76 +0,0 @@
1
- import tempfile
2
- from pathlib import Path
3
-
4
- from gitpure import Repo
5
-
6
- def test_clone_and_git_dir_worktree():
7
- """Test cloning a worktree repository and git_dir property"""
8
- with tempfile.TemporaryDirectory() as tmpdir:
9
- repo_url = "https://github.com/curl/wcurl"
10
- repo_path = Path(tmpdir) / "wcurl"
11
- repo = Repo.clone_from(repo_url, str(repo_path))
12
-
13
- # Test basic clone functionality
14
- assert (repo_path / ".git").exists()
15
- assert (repo_path / "README.md").exists()
16
-
17
- # Test git_dir property
18
- git_dir = repo.git_dir
19
- expected_git_dir = repo_path / ".git"
20
-
21
- # Should return the correct path
22
- assert Path(git_dir) == expected_git_dir
23
-
24
- # The returned path should exist and be a directory
25
- assert Path(git_dir).exists()
26
- assert Path(git_dir).is_dir()
27
-
28
- # Should contain typical git directory contents
29
- git_dir_path = Path(git_dir)
30
- assert (git_dir_path / "HEAD").exists()
31
- assert (git_dir_path / "config").exists()
32
- assert (git_dir_path / "objects").exists()
33
- assert (git_dir_path / "refs").exists()
34
-
35
- # Test git_dir property type and consistency
36
- assert isinstance(git_dir, Path)
37
- assert git_dir.is_absolute()
38
-
39
- # Multiple calls should return the same result
40
- git_dir2 = repo.git_dir
41
- git_dir3 = repo.git_dir
42
- assert git_dir == git_dir2 == git_dir3
43
-
44
- def test_clone_and_git_dir_bare():
45
- """Test cloning a bare repository and git_dir property"""
46
- with tempfile.TemporaryDirectory() as tmpdir:
47
- repo_url = "https://github.com/curl/wcurl"
48
- repo_path = Path(tmpdir) / "wcurl.git"
49
- repo = Repo.clone_from(repo_url, str(repo_path), bare=True)
50
-
51
- # Test bare clone functionality
52
- # Should NOT have .git subdirectory (it IS the git directory)
53
- assert not (repo_path / ".git").exists()
54
-
55
- # Should have git files directly in the root
56
- assert (repo_path / "HEAD").exists()
57
- assert (repo_path / "config").exists()
58
- assert (repo_path / "objects").exists()
59
- assert (repo_path / "refs").exists()
60
-
61
- # Should NOT have working tree files
62
- assert not (repo_path / "README.md").exists()
63
-
64
- # Test git_dir property for bare repo
65
- git_dir = repo.git_dir
66
-
67
- # For bare repos, git_dir should be the repo directory itself
68
- assert Path(git_dir) == repo_path
69
-
70
- # The returned path should exist and be a directory
71
- assert Path(git_dir).exists()
72
- assert Path(git_dir).is_dir()
73
-
74
- # Test git_dir property type
75
- assert isinstance(git_dir, Path)
76
- assert git_dir.is_absolute()
File without changes
File without changes
File without changes