Skip to content

Commit 3e2c69f

Browse files
committed
stash apply: support reinstate_index and skip_conflicts
1 parent 8aabbf3 commit 3e2c69f

File tree

7 files changed

+117
-11
lines changed

7 files changed

+117
-11
lines changed

scmrepo/git/backend/base.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,13 @@ def _stash_push(
281281
"""
282282

283283
@abstractmethod
284-
def _stash_apply(self, rev: str):
284+
def _stash_apply(
285+
self,
286+
rev: str,
287+
reinstate_index: bool = False,
288+
skip_conflicts: bool = False,
289+
**kwargs,
290+
):
285291
"""Apply the specified stash revision."""
286292

287293
@abstractmethod

scmrepo/git/backend/dulwich/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,13 @@ def _stash_push(
695695
) from exc
696696
return os.fsdecode(rev), True
697697

698-
def _stash_apply(self, rev: str):
698+
def _stash_apply(
699+
self,
700+
rev: str,
701+
reinstate_index: bool = False,
702+
skip_conflicts: bool = False,
703+
**kwargs,
704+
):
699705
raise NotImplementedError
700706

701707
def _stash_drop(self, ref: str, index: int):

scmrepo/git/backend/gitpython.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -528,11 +528,23 @@ def _stash_push(
528528
self.git.stash("drop")
529529
return commit.hexsha, False
530530

531-
def _stash_apply(self, rev: str):
531+
def _stash_apply(
532+
self,
533+
rev: str,
534+
reinstate_index: bool = False,
535+
skip_conflicts: bool = False,
536+
**kwargs,
537+
):
532538
from git.exc import GitCommandError
533539

540+
if skip_conflicts:
541+
raise NotImplementedError
534542
try:
535-
self.git.stash("apply", rev)
543+
args = ["apply"]
544+
if reinstate_index:
545+
args.append("--index")
546+
args.append(rev)
547+
self.git.stash(args)
536548
except GitCommandError as exc:
537549
out = str(exc)
538550
if "CONFLICT" in out or "already exists" in out:

scmrepo/git/backend/pygit2.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -465,16 +465,25 @@ def _stash_push(
465465
self.repo.stash_drop()
466466
return str(oid), False
467467

468-
def _stash_apply(self, rev: str):
469-
from pygit2 import GitError
468+
def _stash_apply(
469+
self,
470+
rev: str,
471+
reinstate_index: bool = False,
472+
skip_conflicts: bool = False,
473+
**kwargs,
474+
):
475+
from pygit2 import GIT_CHECKOUT_ALLOW_CONFLICTS, GitError
470476

471477
from scmrepo.git import Stash
472478

473479
def _apply(index):
474480
try:
475481
self.repo.index.read(False)
482+
strategy = self._get_checkout_strategy()
483+
if skip_conflicts:
484+
strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS
476485
self.repo.stash_apply(
477-
index, strategy=self._get_checkout_strategy()
486+
index, strategy=strategy, reinstate_index=reinstate_index
478487
)
479488
except GitError as exc:
480489
raise MergeConflictError(

scmrepo/git/stash.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,40 @@ def push(
4444
self.scm.reset(hard=True)
4545
return rev
4646

47-
def pop(self):
47+
def pop(self, **kwargs):
48+
"""Pop the last stash commit.
49+
50+
Supports the same keyword arguments as apply().
51+
"""
4852
logger.debug("Popping from stash '%s'", self.ref)
4953
ref = f"{self.ref}@{{0}}"
5054
rev = self.scm.resolve_rev(ref)
5155
try:
52-
self.apply(rev)
56+
self.apply(rev, **kwargs)
5357
except Exception as exc:
5458
raise SCMError("Could not apply stash commit") from exc
5559
self.drop()
5660
return rev
5761

58-
def apply(self, rev):
62+
def apply(
63+
self,
64+
rev: str,
65+
reinstate_index: bool = False,
66+
skip_conflicts: bool = False,
67+
):
68+
"""Apply a stash commit.
69+
70+
Arguments:
71+
rev: Stash commit to apply.
72+
reinstate_index: If True, stashed index changes will be reapplied.
73+
skip_conflicts: If True, conflicting changes will be skipped and
74+
will not be applied from the stash. By default, apply will
75+
fail if any conflicts are found.
76+
"""
5977
logger.debug("Applying stash commit '%s'", rev)
60-
self.scm._stash_apply(rev) # pylint: disable=protected-access
78+
self.scm._stash_apply( # pylint: disable=protected-access
79+
rev, reinstate_index=reinstate_index, skip_conflicts=skip_conflicts
80+
)
6181

6282
def drop(self, index: int = 0):
6383
if index < 0 or index >= len(self):

tests/test_pygit2.py

+30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pygit2
22
import pytest
3+
from pytest_mock import MockerFixture
34
from pytest_test_utils import TmpDir
45

56
from scmrepo.git import Git
@@ -30,3 +31,32 @@ def test_pygit_resolve_refish(tmp_dir: TmpDir, scm: Git, use_sha: str):
3031
assert str(commit.id) == head
3132
if not use_sha:
3233
assert ref.name == f"refs/tags/{tag}"
34+
35+
36+
@pytest.mark.parametrize("skip_conflicts", [True, False])
37+
def test_pygit_stash_apply_conflicts(
38+
tmp_dir: TmpDir, scm: Git, skip_conflicts: bool, mocker: MockerFixture
39+
):
40+
from pygit2 import GIT_CHECKOUT_ALLOW_CONFLICTS
41+
42+
tmp_dir.gen("foo", "foo")
43+
scm.add_commit("foo", message="foo")
44+
tmp_dir.gen("foo", "bar")
45+
scm.stash.push()
46+
rev = scm.resolve_rev(r"stash@{0}")
47+
48+
backend = Pygit2Backend(tmp_dir)
49+
mock = mocker.patch.object(backend.repo, "stash_apply")
50+
backend._stash_apply( # pylint: disable=protected-access
51+
rev, skip_conflicts=skip_conflicts
52+
)
53+
expected_strategy = (
54+
backend._get_checkout_strategy() # pylint: disable=protected-access
55+
)
56+
if skip_conflicts:
57+
expected_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS
58+
mock.assert_called_once_with(
59+
0,
60+
strategy=expected_strategy,
61+
reinstate_index=False,
62+
)

tests/test_stash.py

+23
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,26 @@ def test_git_stash_clear(tmp_dir: TmpDir, scm: Git, ref: Optional[str]):
125125
# NOTE: some backends will completely remove reflog file on clear, some
126126
# will only truncate it, either case means an empty stash
127127
assert not reflog_file.exists() or not reflog_file.cat()
128+
129+
130+
@pytest.mark.skip_git_backend("dulwich")
131+
def test_git_stash_apply_index(
132+
tmp_dir: TmpDir,
133+
scm: Git,
134+
git: Git,
135+
):
136+
tmp_dir.gen("file", "0")
137+
scm.add_commit("file", message="init")
138+
tmp_dir.gen("file", "1")
139+
scm.add("file")
140+
scm.stash.push()
141+
rev = scm.resolve_rev(r"stash@{0}")
142+
143+
stash = Stash(git)
144+
stash.apply(rev, reinstate_index=True)
145+
146+
assert (tmp_dir / "file").read_text() == "1"
147+
staged, unstaged, untracked = scm.status()
148+
assert dict(staged) == {"modify": ["file"]}
149+
assert not dict(unstaged)
150+
assert not dict(untracked)

0 commit comments

Comments
 (0)