File: test_stash.py

package info (click to toggle)
dulwich 1.0.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 7,388 kB
  • sloc: python: 99,991; makefile: 163; sh: 67
file content (233 lines) | stat: -rw-r--r-- 8,141 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# test_stash.py -- tests for stash
# Copyright (C) 2018 Jelmer Vernooij <jelmer@jelmer.uk>
#
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
# General Public License as published by the Free Software Foundation; version 2.0
# or (at your option) any later version. You can redistribute it and/or
# modify it under the terms of either of these two licenses.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# You should have received a copy of the licenses; if not, see
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
# License, Version 2.0.
#

"""Tests for stashes."""

import os
import shutil
import tempfile

from dulwich.objects import Blob, Tree
from dulwich.repo import Repo
from dulwich.stash import DEFAULT_STASH_REF, Stash
from dulwich.worktree import add_worktree

from . import TestCase


class StashTests(TestCase):
    """Tests for stash."""

    def setUp(self):
        self.test_dir = tempfile.mkdtemp()
        self.repo_dir = os.path.join(self.test_dir, "repo")
        os.makedirs(self.repo_dir)
        self.repo = Repo.init(self.repo_dir)

        # Create initial commit so we can have a HEAD
        blob = Blob()
        blob.data = b"initial data"
        self.repo.object_store.add_object(blob)

        tree = Tree()
        tree.add(b"initial.txt", 0o100644, blob.id)
        tree_id = self.repo.object_store.add_object(tree)

        self.commit_id = self.repo.get_worktree().commit(
            message=b"Initial commit",
            tree=tree_id,
        )

    def tearDown(self):
        shutil.rmtree(self.test_dir)

    def test_obtain(self) -> None:
        stash = Stash.from_repo(self.repo)
        self.assertIsInstance(stash, Stash)

    def test_empty_stash(self) -> None:
        stash = Stash.from_repo(self.repo)
        # Make sure logs directory exists for reflog
        os.makedirs(os.path.join(self.repo.commondir(), "logs"), exist_ok=True)

        self.assertEqual(0, len(stash))
        self.assertEqual([], list(stash.stashes()))

    def test_push_stash(self) -> None:
        stash = Stash.from_repo(self.repo)

        # Make sure logs directory exists for reflog
        os.makedirs(os.path.join(self.repo.commondir(), "logs"), exist_ok=True)

        # Create a file and add it to the index
        file_path = os.path.join(self.repo_dir, "testfile.txt")
        with open(file_path, "wb") as f:
            f.write(b"test data")
        self.repo.get_worktree().stage(["testfile.txt"])

        # Push to stash
        commit_id = stash.push(message=b"Test stash message")
        self.assertIsNotNone(commit_id)

        # Verify stash was created
        self.assertEqual(1, len(stash))

        # Verify stash entry
        entry = stash[0]
        self.assertEqual(commit_id, entry.new_sha)
        self.assertTrue(b"Test stash message" in entry.message)

    def test_drop_stash(self) -> None:
        stash = Stash.from_repo(self.repo)

        # Make sure logs directory exists for reflog
        logs_dir = os.path.join(self.repo.commondir(), "logs")
        os.makedirs(logs_dir, exist_ok=True)

        # Create a couple of files and stash them
        file1_path = os.path.join(self.repo_dir, "testfile1.txt")
        with open(file1_path, "wb") as f:
            f.write(b"test data 1")
        self.repo.get_worktree().stage(["testfile1.txt"])
        commit_id1 = stash.push(message=b"Test stash 1")

        file2_path = os.path.join(self.repo_dir, "testfile2.txt")
        with open(file2_path, "wb") as f:
            f.write(b"test data 2")
        self.repo.get_worktree().stage(["testfile2.txt"])
        stash.push(message=b"Test stash 2")

        self.assertEqual(2, len(stash))

        # Drop the newest stash
        stash.drop(0)
        self.assertEqual(1, len(stash))
        self.assertEqual(commit_id1, stash[0].new_sha)

        # Drop the remaining stash
        stash.drop(0)
        self.assertEqual(0, len(stash))
        self.assertNotIn(DEFAULT_STASH_REF, self.repo.refs)

    def test_custom_ref(self) -> None:
        custom_ref = b"refs/custom_stash"
        stash = Stash(self.repo, ref=custom_ref)
        self.assertEqual(custom_ref, stash._ref)

    def test_pop_stash(self) -> None:
        stash = Stash.from_repo(self.repo)

        # Make sure logs directory exists for reflog
        os.makedirs(os.path.join(self.repo.commondir(), "logs"), exist_ok=True)

        # Create a file and add it to the index
        file_path = os.path.join(self.repo_dir, "testfile.txt")
        with open(file_path, "wb") as f:
            f.write(b"test data")
        self.repo.get_worktree().stage(["testfile.txt"])

        # Push to stash
        stash.push(message=b"Test stash message")
        self.assertEqual(1, len(stash))

        # After stash push, the file should be removed from working tree
        # (matching git's behavior)
        self.assertFalse(os.path.exists(file_path))

        # Pop the stash
        stash.pop(0)

        # Verify file is restored
        self.assertTrue(os.path.exists(file_path))
        with open(file_path, "rb") as f:
            self.assertEqual(b"test data", f.read())

        # Verify stash is empty
        self.assertEqual(0, len(stash))

        # Verify the file is in the index
        index = self.repo.open_index()
        self.assertIn(b"testfile.txt", index)

    def test_pop_stash_with_index_changes(self) -> None:
        stash = Stash.from_repo(self.repo)

        # Make sure logs directory exists for reflog
        os.makedirs(os.path.join(self.repo.commondir(), "logs"), exist_ok=True)

        # First commit a file so we have tracked files
        tracked_path = os.path.join(self.repo_dir, "tracked.txt")
        with open(tracked_path, "wb") as f:
            f.write(b"original content")
        self.repo.get_worktree().stage(["tracked.txt"])
        self.repo.get_worktree().commit(
            message=b"Add tracked file",
        )

        # Modify the tracked file and stage it
        with open(tracked_path, "wb") as f:
            f.write(b"staged changes")
        self.repo.get_worktree().stage(["tracked.txt"])

        # Modify it again but don't stage
        with open(tracked_path, "wb") as f:
            f.write(b"working tree changes")

        # Create a new file and stage it
        new_file_path = os.path.join(self.repo_dir, "new.txt")
        with open(new_file_path, "wb") as f:
            f.write(b"new file content")
        self.repo.get_worktree().stage(["new.txt"])

        # Push to stash
        stash.push(message=b"Test stash with index")
        self.assertEqual(1, len(stash))

        # After stash push, new file should be removed and tracked file reset
        self.assertFalse(os.path.exists(new_file_path))
        with open(tracked_path, "rb") as f:
            self.assertEqual(b"original content", f.read())

        # Pop the stash
        stash.pop(0)

        # Verify tracked file has working tree changes
        self.assertTrue(os.path.exists(tracked_path))
        with open(tracked_path, "rb") as f:
            self.assertEqual(b"working tree changes", f.read())

        # Verify new file is restored
        self.assertTrue(os.path.exists(new_file_path))
        with open(new_file_path, "rb") as f:
            self.assertEqual(b"new file content", f.read())

        # Verify index has the staged changes
        index = self.repo.open_index()
        self.assertIn(b"new.txt", index)


class StashInWorktreeTest(StashTests):
    """Tests for stash in a worktree."""

    def setUp(self) -> None:
        super().setUp()
        self.repo_dir = os.path.join(self.test_dir, "wt")
        self.repo = add_worktree(self.repo, self.repo_dir, "worktree")