git-branchless 0.3.4

Branchless workflow for Git
Documentation
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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
use std::convert::Infallible;
use std::mem::swap;
use std::rc::Rc;
use std::sync::{Arc, Mutex};

use crate::util::trim_lines;

use branchless::commands::undo::testing::{select_past_event, undo_events};
use branchless::core::eventlog::{EventCursor, EventLogDb, EventReplayer};
use branchless::core::formatting::Glyphs;
use branchless::core::mergebase::MergeBaseDb;
use branchless::git::{GitRunInfo, Repo};
use branchless::testing::{make_git, Git};
use branchless::tui::testing::{screen_to_string, CursiveTestingBackend, CursiveTestingEvent};
use branchless::tui::Effects;

use cursive::event::Key;
use cursive::CursiveRunnable;
use os_str_bytes::OsStrBytes;

fn run_select_past_event(
    repo: &Repo,
    events: Vec<CursiveTestingEvent>,
) -> eyre::Result<Option<EventCursor>> {
    let glyphs = Glyphs::text();
    let effects = Effects::new_suppress_for_test(glyphs);
    let conn = repo.get_db_conn()?;
    let merge_base_db = MergeBaseDb::new(&conn)?;
    let event_log_db: EventLogDb = EventLogDb::new(&conn)?;
    let mut event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?;
    let siv = CursiveRunnable::new::<Infallible, _>(move || {
        Ok(CursiveTestingBackend::init(events.clone()))
    });
    select_past_event(
        siv.into_runner(),
        &effects,
        repo,
        &merge_base_db,
        &mut event_replayer,
    )
}

fn run_undo_events(git: &Git, event_cursor: EventCursor) -> eyre::Result<String> {
    let glyphs = Glyphs::text();
    let effects = Effects::new_suppress_for_test(glyphs.clone());
    let repo = git.get_repo()?;
    let conn = repo.get_db_conn()?;
    let mut event_log_db: EventLogDb = EventLogDb::new(&conn)?;
    let event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?;
    let input = "y";
    let mut in_ = input.as_bytes();
    let out: Arc<Mutex<Vec<u8>>> = Default::default();

    let git_run_info = GitRunInfo {
        path_to_git: git.path_to_git.clone(),

        // Ensure that nested calls to `git` are run under the correct environment.
        // (Normally, the user will be running `git undo` from the correct directory
        // already.)
        working_directory: repo.get_working_copy_path().unwrap().to_path_buf(),

        // Normally, we want to inherit the user's environment when running external
        // Git commands. However, for testing, we may have inherited variables which
        // affect the execution of Git. In particular, `GIT_INDEX_FILE` is set to
        // `.git/index` normally (which works for the test), but can be set to an
        // absolute path when running `git commit -a`, and having these tests run as
        // part of a commit hook.
        env: std::env::vars_os()
            .filter(|(k, _v)| !k.to_raw_bytes().starts_with(b"GIT_"))
            .collect(),
    };

    let result = undo_events(
        &mut in_,
        &Effects::new_from_buffer_for_test(glyphs, &out),
        &repo,
        &git_run_info,
        &mut event_log_db,
        &event_replayer,
        event_cursor,
    )?;
    assert_eq!(result, 0);

    let out = {
        let mut buf = out.lock().unwrap();
        let mut result_buf = Vec::new();
        swap(&mut *buf, &mut result_buf);
        result_buf
    };
    let out = String::from_utf8(out)?;
    let out = git.preprocess_output(out)?;
    let out = trim_lines(out);
    Ok(out)
}

#[test]
fn test_undo_help() -> eyre::Result<()> {
    let git = make_git()?;

    git.init_repo()?;

    {
        let screenshot1 = Default::default();
        run_select_past_event(
            &git.get_repo()?,
            vec![
                CursiveTestingEvent::Event('h'.into()),
                CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
                CursiveTestingEvent::Event('q'.into()),
            ],
        )?;
        insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
            ┌───────────────────────────────────────────┤─How to use ├───────────────────────────────────────────┐
            │ Use `git undo` to view and revert to previous states of the repository.                            │
            │                                                                                                    │
            │ h/?: Show this help.                                                                               │
            │ q: Quit.                                                                                           │
            │ p/n or <left>/<right>: View next/previous state.                                                   │
            │ g: Go to a provided event ID.                                                                      │
            │ <enter>: Revert the repository to the given state (requires confirmation).                         │
            │                                                                                                    │
            │ You can also copy a commit hash from the past and manually run `git unhide` or `git rebase` on it. │
            │                                                                                                    │
            │                                                                                            <Close> │
            └────────────────────────────────────────────────────────────────────────────────────────────────────┘
            "###);
    }

    Ok(())
}

#[test]
fn test_undo_navigate() -> eyre::Result<()> {
    let git = make_git()?;

    if !git.supports_reference_transactions()? {
        return Ok(());
    }

    git.init_repo()?;
    git.commit_file("test1", 1)?;
    git.commit_file("test2", 2)?;

    {
        let screenshot1 = Default::default();
        let screenshot2 = Default::default();
        let event_cursor = run_select_past_event(
            &git.get_repo()?,
            vec![
                CursiveTestingEvent::Event('p'.into()),
                CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
                CursiveTestingEvent::Event('n'.into()),
                CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot2)),
                CursiveTestingEvent::Event(Key::Enter.into()),
            ],
        )?;
        insta::assert_debug_snapshot!(event_cursor, @r###"
            Some(
                EventCursor {
                    event_id: 6,
                },
            )
            "###);
        insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
            :
            @ 96d1c37a (master) create test2.txt
            Repo after transaction 3 (event 4). Press 'h' for help, 'q' to quit.
            1. Check out from 62fc20d2 create test1.txt
            to 96d1c37a create test2.txt
            2. Move branch master from 62fc20d2 create test1.txt
            to 96d1c37a create test2.txt
            "###);
        insta::assert_snapshot!(screen_to_string(&screenshot2), @r###"
            :
            @ 96d1c37a (master) create test2.txt
            Repo after transaction 4 (event 6). Press 'h' for help, 'q' to quit.
            1. Commit 96d1c37a create test2.txt
            "###);
    };

    Ok(())
}

#[test]
fn test_go_to_event() -> eyre::Result<()> {
    let git = make_git()?;

    if !git.supports_reference_transactions()? {
        return Ok(());
    }

    git.init_repo()?;
    git.commit_file("test1", 1)?;
    git.commit_file("test2", 2)?;

    let screenshot1 = Default::default();
    let screenshot2 = Default::default();
    run_select_past_event(
        &git.get_repo()?,
        vec![
            CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
            CursiveTestingEvent::Event('g'.into()),
            CursiveTestingEvent::Event('1'.into()),
            CursiveTestingEvent::Event(Key::Enter.into()),
            CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot2)),
            CursiveTestingEvent::Event('q'.into()),
        ],
    )?;

    insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
        :
        @ 96d1c37a (master) create test2.txt
        Repo after transaction 4 (event 6). Press 'h' for help, 'q' to quit.
        1. Commit 96d1c37a create test2.txt
        "###);
    insta::assert_snapshot!(screen_to_string(&screenshot2), @r###"
        :
        @ 62fc20d2 create test1.txt
        |
        O 96d1c37a (master) create test2.txt
        Repo after transaction 1 (event 1). Press 'h' for help, 'q' to quit.
        1. Check out from f777ecc9 create initial.txt
        to 62fc20d2 create test1.txt
        "###);

    Ok(())
}

#[test]
fn test_undo_hide() -> eyre::Result<()> {
    let git = make_git()?;

    if !git.supports_reference_transactions()? {
        return Ok(());
    }

    git.init_repo()?;
    git.run(&["checkout", "-b", "test1"])?;
    git.commit_file("test1", 1)?;
    git.run(&["checkout", "HEAD^"])?;
    git.commit_file("test2", 2)?;
    git.run(&["hide", "test1"])?;
    git.run(&["branch", "-D", "test1"])?;

    {
        let (stdout, stderr) = git.run(&["smartlog"])?;
        insta::assert_snapshot!(stderr, @"");
        insta::assert_snapshot!(stdout, @r###"
            O f777ecc9 (master) create initial.txt
            |
            @ fe65c1fe create test2.txt
            "###);
    }

    let event_cursor = run_select_past_event(
        &git.get_repo()?,
        vec![
            CursiveTestingEvent::Event('p'.into()),
            CursiveTestingEvent::Event('p'.into()),
            CursiveTestingEvent::Event(Key::Enter.into()),
            CursiveTestingEvent::Event('y'.into()),
        ],
    )?;
    insta::assert_debug_snapshot!(event_cursor, @r###"
        Some(
            EventCursor {
                event_id: 9,
            },
        )
        "###);
    let event_cursor = event_cursor.unwrap();

    {
        let stdout = run_undo_events(&git, event_cursor)?;
        insta::assert_snapshot!(stdout, @r###"
            Will apply these actions:
            1. Create branch test1 at 62fc20d2 create test1.txt

            2. Unhide commit 62fc20d2 create test1.txt

            Confirm? [yN] Applied 2 inverse events.
            "###);
    }

    {
        let (stdout, _stderr) = git.run(&["smartlog"])?;
        insta::assert_snapshot!(stdout, @r###"
            O f777ecc9 (master) create initial.txt
            |\
            | o 62fc20d2 (test1) create test1.txt
            |
            @ fe65c1fe create test2.txt
            "###);
    }

    Ok(())
}

#[test]
fn test_undo_move_refs() -> eyre::Result<()> {
    let git = make_git()?;

    if !git.supports_reference_transactions()? {
        return Ok(());
    }

    git.init_repo()?;
    git.commit_file("test1", 1)?;
    git.commit_file("test2", 2)?;

    let event_cursor = run_select_past_event(
        &git.get_repo()?,
        vec![
            CursiveTestingEvent::Event('p'.into()),
            CursiveTestingEvent::Event('p'.into()),
            CursiveTestingEvent::Event(Key::Enter.into()),
            CursiveTestingEvent::Event('y'.into()),
        ],
    )?;
    insta::assert_debug_snapshot!(event_cursor, @r###"
        Some(
            EventCursor {
                event_id: 3,
            },
        )
        "###);
    let event_cursor = event_cursor.unwrap();

    {
        let stdout = run_undo_events(&git, event_cursor)?;
        insta::assert_snapshot!(stdout, @r###"
        Will apply these actions:
        1. Check out from 96d1c37a create test2.txt
                       to 62fc20d2 create test1.txt
        2. Hide commit 96d1c37a create test2.txt

        3. Move branch master from 96d1c37a create test2.txt
                                to 62fc20d2 create test1.txt
        Confirm? [yN] branchless: <git-executable> checkout --detach 62fc20d2a290daea0d52bdc2ed2ad4be6491010e
        Applied 3 inverse events.
        "###);
    }

    {
        let (stdout, _stderr) = git.run(&["smartlog"])?;
        insta::assert_snapshot!(stdout, @r###"
            :
            @ 62fc20d2 (master) create test1.txt
            "###);
    }

    Ok(())
}

#[test]
fn test_historical_smartlog_visibility() -> eyre::Result<()> {
    let git = make_git()?;

    git.init_repo()?;
    git.commit_file("test1", 1)?;
    git.run(&["hide", "HEAD"])?;

    let screenshot1 = Default::default();
    let screenshot2 = Default::default();
    run_select_past_event(
        &git.get_repo()?,
        vec![
            CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
            CursiveTestingEvent::Event('p'.into()),
            CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot2)),
            CursiveTestingEvent::Event('q'.into()),
        ],
    )?;

    if git.supports_reference_transactions()? {
        insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
        :
        % 62fc20d2 (manually hidden) (master) create test1.txt
        Repo after transaction 3 (event 4). Press 'h' for help, 'q' to quit.
        1. Hide commit 62fc20d2 create test1.txt
        "###);
        insta::assert_snapshot!(screen_to_string(&screenshot2), @r###"
        :
        @ 62fc20d2 (master) create test1.txt
        Repo after transaction 2 (event 3). Press 'h' for help, 'q' to quit.
        1. Commit 62fc20d2 create test1.txt
        "###);
    } else {
        insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
            :
            % 62fc20d2 (manually hidden) (master) create test1.txt
            Repo after transaction 2 (event 2). Press 'h' for help, 'q' to quit.
            1. Hide commit 62fc20d2 create test1.txt
            "###);
        insta::assert_snapshot!(screen_to_string(&screenshot2), @r###"
            :
            @ 62fc20d2 (master) create test1.txt
            Repo after transaction 1 (event 1). Press 'h' for help, 'q' to quit.
            1. Commit 62fc20d2 create test1.txt
            "###);
    }

    Ok(())
}

#[test]
fn test_undo_doesnt_make_working_dir_dirty() -> eyre::Result<()> {
    let git = make_git()?;

    if !git.supports_reference_transactions()? {
        return Ok(());
    }

    git.init_repo()?;

    // Modify a reference.
    git.run(&["branch", "foo"])?;
    // Make a change that causes a checkout.
    git.commit_file("test1", 1)?;
    // Modify a reference.
    git.run(&["branch", "bar"])?;

    let screenshot1 = Default::default();
    let event_cursor = run_select_past_event(
        &git.get_repo()?,
        vec![
            CursiveTestingEvent::Event('p'.into()),
            CursiveTestingEvent::Event('p'.into()),
            CursiveTestingEvent::Event('p'.into()),
            CursiveTestingEvent::Event('p'.into()),
            CursiveTestingEvent::TakeScreenshot(Rc::clone(&screenshot1)),
            CursiveTestingEvent::Event(Key::Enter.into()),
        ],
    )?;
    insta::assert_snapshot!(screen_to_string(&screenshot1), @r###"
        :
        O 62fc20d2 (master) create test1.txt
        There are no previous available events.
        "###);

    // If there are no dirty files in the repository prior to the `undo`,
    // then there should still be no dirty files after the `undo`.
    let event_cursor = event_cursor.expect("Should have an event cursor to undo");
    {
        let (stdout, _stderr) = git.run(&["status", "--porcelain"])?;
        assert_eq!(stdout, "");
    }
    {
        let stdout = run_undo_events(&git, event_cursor)?;
        insta::assert_snapshot!(stdout, @r###"
        Will apply these actions:
        1. Check out from 62fc20d2 create test1.txt
                       to f777ecc9 create initial.txt
        2. Delete branch bar at 62fc20d2 create test1.txt

        3. Hide commit 62fc20d2 create test1.txt

        4. Move branch master from 62fc20d2 create test1.txt
                                to f777ecc9 create initial.txt
        5. Delete branch foo at f777ecc9 create initial.txt

        Confirm? [yN] branchless: <git-executable> checkout --detach f777ecc9b0db5ed372b2615695191a8a17f79f24
        Applied 5 inverse events.
        "###);
    }
    {
        let (stdout, _stderr) = git.run(&["status", "--porcelain"])?;
        assert_eq!(stdout, "");
    }

    Ok(())
}