Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pygit2/_pygit2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ class Repository:
def revparse_single(self, revision: str) -> Object: ...
def set_odb(self, odb: Odb) -> None: ...
def set_refdb(self, refdb: Refdb) -> None: ...
def status(self) -> dict[str,int]: ...
def status(self, untracked_files: str = "all", ignored: bool = True) -> dict[str,int]: ...
def status_file(self, path: str) -> int: ...
def walk(self, oid: _OidArg | None, sort_mode: int = GIT_SORT_NONE) -> Walker: ...

Expand Down
53 changes: 48 additions & 5 deletions src/repository.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
* Boston, MA 02110-1301, USA.
*/

#include <git2/status.h>
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "error.h"
Expand Down Expand Up @@ -1771,20 +1772,62 @@ Repository_compress_references(Repository *self)
}

PyDoc_STRVAR(Repository_status__doc__,
"status() -> dict[str, int]\n"
"status(untracked_files: str = \"all\", ignored: bool = False) -> dict[str, int]\n"
"\n"
"Reads the status of the repository and returns a dictionary with file\n"
"paths as keys and status flags as values. See pygit2.GIT_STATUS_*.");
"paths as keys and status flags as values. See pygit2.GIT_STATUS_*.\n"
"\n"
"Parameters:\n"
"\n"
"untracked_files\n"
" How to handle untracked files, defaults to \"all\"\n"
" \"no\": do not return untracked files\n"
" \"normal\": include untracked files/directories but no dot recurse subdirectories\n"
" \"all\": include all files in untracked directories\n"
" Using `untracked_files=\"no\"` or \"normal\"can be faster than \"all\" when the worktree\n"
"ignored\n"
" Whether to show ignored files with untracked files. Ignored when untracked_files == \"no\"\n"
" Defaults to False.\n"
"contains many untracked files/directories.");

PyObject *
Repository_status(Repository *self)
Repository_status(Repository *self, PyObject *args, PyObject *kw)
{
PyObject *dict;
int err;
size_t len, i;
git_status_list *list;

err = git_status_list_new(&list, self->repo, NULL);
char *untracked_files = "all";
static char *kwlist[] = {"untracked_files", "ignored", NULL};

PyObject* ignored = Py_False;

if (!PyArg_ParseTupleAndKeywords(args, kw, "|sO", kwlist,
&untracked_files, &ignored))
goto error;

git_status_options opts = GIT_STATUS_OPTIONS_INIT;
opts.flags = GIT_STATUS_OPT_DEFAULTS;

if (!strcmp(untracked_files, "no")) {
opts.flags &= ~(GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS);
} else if (!strcmp(untracked_files, "normal")){
opts.flags &= ~GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
} else if (strcmp(untracked_files, "all") ){
return PyErr_Format(
PyExc_ValueError,
"untracked_files must be one of \"all\", \"normal\" or \"one\"");
};

if (!PyBool_Check(ignored)) {
return PyErr_Format(PyExc_TypeError, "ignored must be True or False");
}
if (!PyObject_IsTrue(ignored)) {
opts.flags &= ~GIT_STATUS_OPT_INCLUDE_IGNORED;
}

err = git_status_list_new(&list, self->repo, &opts);
if (err < 0)
return Error_set(err);

Expand Down Expand Up @@ -2413,7 +2456,7 @@ PyMethodDef Repository_methods[] = {
METHOD(Repository, revparse_single, METH_O),
METHOD(Repository, revparse_ext, METH_O),
METHOD(Repository, revparse, METH_O),
METHOD(Repository, status, METH_NOARGS),
METHOD(Repository, status, METH_VARARGS | METH_KEYWORDS),
METHOD(Repository, status_file, METH_O),
METHOD(Repository, notes, METH_VARARGS),
METHOD(Repository, create_note, METH_VARARGS),
Expand Down
2 changes: 1 addition & 1 deletion src/repository.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ PyObject* Repository_create_reference_direct(Repository *self, PyObject *args, P
PyObject* Repository_create_reference_symbolic(Repository *self, PyObject *args, PyObject* kw);

PyObject* Repository_compress_references(Repository *self);
PyObject* Repository_status(Repository *self);
PyObject* Repository_status(Repository *self, PyObject *args, PyObject *kw);
PyObject* Repository_status_file(Repository *self, PyObject *value);
PyObject* Repository_TreeBuilder(Repository *self, PyObject *args);

Expand Down
Binary file modified test/data/dirtyrepo.zip
Binary file not shown.
2 changes: 2 additions & 0 deletions test/test_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
PATCHID = 'f31412498a17e6c3fbc635f2c5f9aa3ef4c1a9b7'

DIFF_HEAD_TO_INDEX_EXPECTED = [
'.gitignore',
'staged_changes',
'staged_changes_file_deleted',
'staged_changes_file_modified',
Expand All @@ -87,6 +88,7 @@
]

DIFF_INDEX_TO_WORK_EXPECTED = [
'.gitignore',
'file_deleted',
'modified_file',
'staged_changes_file_deleted',
Expand Down
48 changes: 48 additions & 0 deletions test/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
# along with this program; see the file COPYING. If not, write to
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.
import pygit2
import pytest


def test_status(dirtyrepo):
Expand All @@ -32,3 +34,49 @@ def test_status(dirtyrepo):
for filepath, status in git_status.items():
assert filepath in git_status
assert status == git_status[filepath]


def test_status_untracked_no(dirtyrepo):
git_status = dirtyrepo.status(untracked_files="no")
not any(status & pygit2.GIT_STATUS_WT_NEW for status in git_status.values())


@pytest.mark.parametrize(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests don't actually cover all of the possible scenarios, such as having tracked subdirectories with ignored files, tracked subdirectories with untracked files, untracked subdirectories with ignored files, etc, etc. Might improve these tests soon.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're just passing the options to libgit2, the purpose of the test suite is not to test libgit2, which already has its own test suite. So better to keep the tests simple, it's good the way they are already.

"untracked_files,expected",
[
("no", set()),
(
"normal",
{
"untracked_dir/",
"staged_delete_file_modified",
"subdir/new_file",
"new_file",
},
),
(
"all",
{
"new_file",
"subdir/new_file",
"staged_delete_file_modified",
"untracked_dir/untracked_file",
},
),
],
)
def test_status_untracked_normal(dirtyrepo, untracked_files, expected):
git_status = dirtyrepo.status(untracked_files=untracked_files)
assert {
file for file, status in git_status.items() if status & pygit2.GIT_STATUS_WT_NEW
} == expected


@pytest.mark.parametrize("ignored,expected", [(True, {"ignored"}), (False, set())])
def test_status_ignored(dirtyrepo, ignored, expected):
git_status = dirtyrepo.status(ignored=ignored)
assert {
file
for file, status in git_status.items()
if status & pygit2.GIT_STATUS_IGNORED
} == expected