File: test_rerere.py

package info (click to toggle)
git-revise 0.7.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 344 kB
  • sloc: python: 2,505; makefile: 16
file content (370 lines) | stat: -rw-r--r-- 10,225 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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# pylint: skip-file

import textwrap

from conftest import *
from gitrevise.merge import normalize_conflicted_file


def history_with_two_conflicting_commits(autoUpdate: bool = False):
    bash(
        f"""
        git config rerere.enabled true
        git config rerere.autoUpdate {"true" if autoUpdate else "false"}
        echo > file; git add file; git commit -m 'initial commit'
        echo one > file; git commit -am 'commit one'
        echo two > file; git commit -am 'commit two'
        """
    )


def test_reuse_recorded_resolution(repo):
    history_with_two_conflicting_commits(autoUpdate=True)

    with editor_main(("-i", "HEAD~~"), input=b"y\ny\ny\ny\n") as ed:
        flip_last_two_commits(repo, ed)
        with ed.next_file() as f:
            f.replace_dedent("resolved two\n")
        with ed.next_file() as f:
            f.replace_dedent("resolved one\n")

    tree_after_resolving_conflicts = repo.get_commit("HEAD").tree()
    bash("git reset --hard HEAD@{1}")

    # Now we can change the order of the two commits and reuse the recorded conflict resolution.
    with editor_main(("-i", "HEAD~~")) as ed:
        flip_last_two_commits(repo, ed)

    assert tree_after_resolving_conflicts == repo.get_commit("HEAD").tree()
    leftover_index = hunks(repo.git("diff", "-U0", "HEAD"))
    assert leftover_index == dedent(
        """\
        @@ -1 +1 @@
        -resolved one
        +two"""
    )

    # When we fail to read conflict data from the cache, we fall back to
    # letting the user resolve the conflict.
    bash("git reset --hard HEAD@{1}")
    bash("rm .git/rr-cache/*/preimage")
    with editor_main(("-i", "HEAD~~"), input=b"y\ny\ny\ny\n") as ed:
        flip_last_two_commits(repo, ed)
        with ed.next_file() as f:
            f.replace_dedent("resolved two\n")
        with ed.next_file() as f:
            f.replace_dedent("resolved one\n")


def test_rerere_no_autoupdate(repo):
    history_with_two_conflicting_commits(autoUpdate=False)

    with editor_main(("-i", "HEAD~~"), input=b"y\ny\ny\ny\n") as ed:
        flip_last_two_commits(repo, ed)
        with ed.next_file() as f:
            f.replace_dedent("resolved two\n")
        with ed.next_file() as f:
            f.replace_dedent("resolved one\n")

    tree_after_resolving_conflicts = repo.get_commit("HEAD").tree()
    bash("git reset --hard HEAD@{1}")

    # Use the recorded resolution by confirming both times.
    with editor_main(("-i", "HEAD~~"), input=b"y\ny\n") as ed:
        flip_last_two_commits(repo, ed)
    assert tree_after_resolving_conflicts == repo.get_commit("HEAD").tree()
    leftover_index = hunks(repo.git("diff", "-U0", "HEAD"))
    assert leftover_index == dedent(
        """\
        @@ -1 +1 @@
        -resolved one
        +two"""
    )
    bash("git reset --hard HEAD@{1}")

    # Do not use the recorded resolution for the second commit.
    with editor_main(("-i", "HEAD~~"), input=b"y\nn\ny\ny\n") as ed:
        flip_last_two_commits(repo, ed)
        with ed.next_file() as f:
            f.replace_dedent("resolved differently\n")
    leftover_index = hunks(repo.git("diff", "-U0", "HEAD"))
    assert leftover_index == dedent(
        """\
        @@ -1 +1 @@
        -resolved differently
        +two"""
    )


def test_rerere_merge(repo):
    (repo.workdir / "file").write_bytes(10 * b"x\n")
    bash(
        """
        git config rerere.enabled true
        git config rerere.autoUpdate true
        git add file; git commit -m 'initial commit'
        """
    )
    changeline("file", 0, b"original1\n")
    bash("git commit -am 'commit 1'")
    changeline("file", 0, b"original2\n")
    bash("git commit -am 'commit 2'")

    # Record a resolution for changing the order of two commits.
    with editor_main(("-i", "HEAD~~"), input=b"y\ny\ny\ny\n") as ed:
        flip_last_two_commits(repo, ed)
        with ed.next_file() as f:
            f.replace_dedent(b"resolved1\n" + 9 * b"x\n")
        with ed.next_file() as f:
            f.replace_dedent(b"resolved2\n" + 9 * b"x\n")
    # Go back to the old history so we can try replaying the resolution.
    bash("git reset --hard HEAD@{1}")

    # Introduce an unrelated change that will not conflict to check that we can
    # merge the file contents, and not just use the recorded postimage as is.
    changeline("file", 9, b"unrelated change, present in all commits\n")
    bash("git add file")
    main(["HEAD~2"])

    with editor_main(("-i", "HEAD~~")) as ed:
        flip_last_two_commits(repo, ed)

    assert hunks(repo.git("show", "-U0", "HEAD~")) == dedent(
        """\
            @@ -1 +1 @@
            -x
            +resolved1"""
    )
    assert hunks(repo.git("show", "-U0", "HEAD")) == dedent(
        """\
            @@ -1 +1 @@
            -resolved1
            +resolved2"""
    )
    leftover_index = hunks(repo.git("diff", "-U0", "HEAD"))
    assert leftover_index == dedent(
        """\
        @@ -1 +1 @@
        -resolved2
        +original2"""
    )


def test_replay_resolution_recorded_by_git(repo):
    history_with_two_conflicting_commits(autoUpdate=True)
    # Switch the order of the last two commits, recording the conflict
    # resolution with Git itself.
    bash(
        r"""
        one=$(git rev-parse HEAD~)
        two=$(git rev-parse HEAD)
        git reset --hard HEAD~~
        git cherry-pick "$two" 2>&1 | grep 'could not apply'
        echo resolved two > file; git add file; GIT_EDITOR=: git cherry-pick --continue
        git cherry-pick "$one" 2>&1 | grep 'could not apply'
        echo resolved one > file; git add file; GIT_EDITOR=: git cherry-pick --continue --no-edit
        git reset --hard "$two"
        """
    )

    # Now let's try to do the same thing with git-revise, reusing the recorded resolution.
    with editor_main(("-i", "HEAD~~")) as ed:
        flip_last_two_commits(repo, ed)

    assert repo.git("log", "-p", trim_newline=False) == dedent(
        """\
        commit dc50430ecbd2d0697ee9266ba6057e0e0b511d7f
        Author: Bash Author <bash_author@example.com>
        Date:   Thu Jul 13 21:40:00 2017 -0500

            commit one

        diff --git a/file b/file
        index 474b904..936bcfd 100644
        --- a/file
        +++ b/file
        @@ -1 +1 @@
        -resolved two
        +resolved one

        commit e51ab202e87f0557df78e5273dcedf51f408a468
        Author: Bash Author <bash_author@example.com>
        Date:   Thu Jul 13 21:40:00 2017 -0500

            commit two

        diff --git a/file b/file
        index 8b13789..474b904 100644
        --- a/file
        +++ b/file
        @@ -1 +1 @@
        -
        +resolved two

        commit d72132e74176624d6c3e5b6b4f5ef774ff23a1b3
        Author: Bash Author <bash_author@example.com>
        Date:   Thu Jul 13 21:40:00 2017 -0500

            initial commit

        diff --git a/file b/file
        new file mode 100644
        index 0000000..8b13789
        --- /dev/null
        +++ b/file
        @@ -0,0 +1 @@
        +
        """
    )


def test_normalize_conflicted_file():
    # Normalize conflict markers and labels.
    assert normalize_conflicted_file(
        dedent(
            """\
            <<<<<<< HEAD
            a
            =======
            b
            >>>>>>> original thingamabob

            unrelated line

            <<<<<<<<<< HEAD
            c
            ==========
            d
            >>>>>>>>>> longer conflict marker, to be trimmed
            """
        )
    ) == (
        dedent(
            """\
            <<<<<<<
            a
            =======
            b
            >>>>>>>

            unrelated line

            <<<<<<<
            c
            =======
            d
            >>>>>>>
            """
        ),
        "3d7cdc2948951408412cc64f3816558407f77e18",
    )

    # Discard original-text-marker from merge.conflictStyle diff3.
    assert (
        normalize_conflicted_file(
            dedent(
                """\
            <<<<<<< theirs
            a
            ||||||| common origin
            b
            =======
            c
            >>>>>>> ours
            """
            )
        )[0]
        == dedent(
            """\
            <<<<<<<
            a
            =======
            c
            >>>>>>>
            """
        )
    )

    # The two sides of the conflict are ordered.
    assert (
        normalize_conflicted_file(
            dedent(
                """\
                <<<<<<< this way round
                b
                =======
                a
                >>>>>>> (unsorted)
                """
            )
        )[0]
        == dedent(
            """\
            <<<<<<<
            a
            =======
            b
            >>>>>>>
            """
        )
    )

    # Nested conflict markers.
    assert (
        normalize_conflicted_file(
            dedent(
                """\
            <<<<<<<
            outer left
            <<<<<<<<<<<
            inner left
            |||||||||||
            inner diff3 original section
            ===========
            inner right
            >>>>>>>>>>>
            =======
            outer right
            >>>>>>>
            """
            )
        )[0]
        == dedent(
            """\
            <<<<<<<
            outer left
            <<<<<<<
            inner left
            =======
            inner right
            >>>>>>>
            =======
            outer right
            >>>>>>>
            """
        )
    )


def flip_last_two_commits(repo: Repository, ed: Editor):
    head = repo.get_commit("HEAD")
    with ed.next_file() as f:
        lines = f.indata.splitlines()
        assert lines[0].startswith(b"pick " + head.parent().oid.short().encode())
        assert lines[1].startswith(b"pick " + head.oid.short().encode())
        assert not lines[2], "expect todo list with exactly two items"

        f.replace_dedent(
            f"""\
            pick {head.oid.short()}
            pick {head.parent().oid.short()}
            """
        )


def dedent(text: str) -> bytes:
    return textwrap.dedent(text).encode()


def hunks(diff: bytes) -> bytes:
    return diff[diff.index(b"@@") :]