• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rraval / git-nomad / 7074366391

03 Dec 2023 03:17AM UTC coverage: 98.531%. First build
7074366391

Pull #141

github

rraval
Explicit output control with Renderer

Gets rid of ad-hoc `println!` and `eprintln!` in favour of plumbing an
explicit `Renderer` that wraps a `Term`. This fixes some glitchy output
where progress bars were interfering with normal text (most noticeable
in `purge` since it's all progress bars).
Pull Request #141: Explicit output control with Renderer

374 of 406 new or added lines in 6 files covered. (92.12%)

2481 of 2518 relevant lines covered (98.53%)

22.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.91
/src/main.rs
1
use std::{
2
    borrow::Cow,
3
    collections::HashSet,
4
    env::{self, current_dir},
5
    ffi::OsString,
6
    path::Path,
7
};
8

9
use clap::{
10
    builder::PossibleValue, crate_authors, crate_description, crate_name, crate_version,
11
    parser::ValueSource, value_parser, Arg, ArgAction, ArgMatches, Command, ValueHint,
12
};
13
use git_version::git_version;
14
use renderer::{Renderer, TerminalRenderer};
15
use types::Branch;
16
use verbosity::Verbosity;
17

18
use crate::{
19
    git_binary::GitBinary,
20
    types::{Host, Remote, User},
21
    workflow::{Filter, LsPrinter, Workflow},
22
};
23

24
mod git_binary;
25
mod git_ref;
26
mod renderer;
27
mod snapshot;
28
mod types;
29
mod verbosity;
30
mod workflow;
31

32
#[cfg(test)]
33
mod git_testing;
34

35
const DEFAULT_REMOTE: Remote<'static> = Remote(Cow::Borrowed("origin"));
36
const ENV_USER: &str = "GIT_NOMAD_USER";
37
const ENV_HOST: &str = "GIT_NOMAD_HOST";
38
const ENV_REMOTE: &str = "GIT_NOMAD_REMOTE";
39
const CONFIG_USER: &str = "user";
40
const CONFIG_HOST: &str = "host";
41

42
const BUILD_VERSION: Option<&str> = option_env!("GIT_NOMAD_BUILD_VERSION");
43

44
// This value is only conditionally used if `git_version!` cannot find any other version.
45
const _CARGO_VERSION: &str = crate_version!();
46
const GIT_VERSION: &str = git_version!(
47
    prefix = "git:",
48
    args = ["--tags", "--always", "--dirty=-modified"],
49
    fallback = _CARGO_VERSION,
50
);
51

52
fn version() -> &'static str {
38✔
53
    BUILD_VERSION.unwrap_or(GIT_VERSION)
38✔
54
}
38✔
55

56
fn main() -> anyhow::Result<()> {
×
57
    nomad(
×
NEW
58
        &mut TerminalRenderer::stdout(),
×
59
        env::args_os(),
×
60
        current_dir()?.as_path(),
×
61
    )
62
}
×
63

64
fn nomad(
1✔
65
    renderer: &mut impl Renderer,
1✔
66
    args: impl IntoIterator<Item = impl Into<OsString> + Clone>,
1✔
67
    cwd: &Path,
1✔
68
) -> anyhow::Result<()> {
1✔
69
    let default_user = User::from(whoami::username());
1✔
70
    let default_host = Host::from(whoami::hostname());
1✔
71

1✔
72
    let mut matches = cli(default_user, default_host, args).unwrap_or_else(|e| e.exit());
1✔
73
    let verbosity = specified_verbosity(&mut matches);
1✔
74

1✔
75
    if verbosity.map_or(false, |v| v.display_version) {
1✔
NEW
76
        renderer.writer(|w| {
×
NEW
77
            writeln!(w)?;
×
NEW
78
            writeln!(w, "Version: {}", version())?;
×
NEW
79
            Ok(())
×
NEW
80
        })?;
×
81
    }
1✔
82

83
    let git = GitBinary::new(
1✔
84
        renderer,
1✔
85
        verbosity,
1✔
86
        Cow::from(specified_git(&mut matches)),
1✔
87
        cwd,
1✔
88
    )?;
1✔
89
    let workflow = specified_workflow(renderer, &mut matches, &git)?;
1✔
90

91
    if verbosity.map_or(false, |v| v.display_workflow) {
1✔
NEW
92
        renderer.writer(|w| {
×
NEW
93
            writeln!(w)?;
×
NEW
94
            writeln!(w, "Workflow: {:?}", workflow)?;
×
NEW
95
            Ok(())
×
NEW
96
        })?;
×
97
    }
1✔
98

99
    workflow.execute(renderer, &git)
1✔
100
}
1✔
101

102
/// Use [`clap`] to implement the intended command line interface.
103
fn cli(
38✔
104
    default_user: User,
38✔
105
    default_host: Host,
38✔
106
    args: impl IntoIterator<Item = impl Into<OsString> + Clone>,
38✔
107
) -> clap::error::Result<ArgMatches> {
38✔
108
    Command::new(crate_name!())
38✔
109
        .arg_required_else_help(true)
38✔
110
        .version(version())
38✔
111
        .author(crate_authors!())
38✔
112
        .about(crate_description!())
38✔
113
        .arg(
38✔
114
            Arg::new("git")
38✔
115
                .global(true)
38✔
116
                .long("git")
38✔
117
                .help("Git binary to use")
38✔
118
                .value_parser(value_parser!(String))
38✔
119
                .value_hint(ValueHint::CommandName)
38✔
120
                .default_value("git"),
38✔
121
        )
38✔
122
        .arg(
38✔
123
            Arg::new("quiet")
38✔
124
                .global(true)
38✔
125
                .short('q')
38✔
126
                .long("quiet")
38✔
127
                .help("Suppress all output")
38✔
128
                .value_parser(value_parser!(bool))
38✔
129
                .action(ArgAction::SetTrue),
38✔
130
        )
38✔
131
        .arg(
38✔
132
            Arg::new("verbose")
38✔
133
                .global(true)
38✔
134
                .short('v')
38✔
135
                .long("verbose")
38✔
136
                .help("Verbose output, repeat up to 2 times for increasing verbosity")
38✔
137
                .value_parser(value_parser!(u8))
38✔
138
                .action(ArgAction::Count),
38✔
139
        )
38✔
140
        .arg(
38✔
141
            Arg::new("user")
38✔
142
                .global(true)
38✔
143
                .short('U')
38✔
144
                .long("user")
38✔
145
                .help("User name, shared by multiple clones, unique per remote")
38✔
146
                .value_parser(value_parser!(String))
38✔
147
                .value_hint(ValueHint::Username)
38✔
148
                .env(ENV_USER)
38✔
149
                .default_value(default_user.0.into_owned()),
38✔
150
        )
38✔
151
        .arg(
38✔
152
            Arg::new("host")
38✔
153
                .global(true)
38✔
154
                .short('H')
38✔
155
                .long("host")
38✔
156
                .value_parser(value_parser!(String))
38✔
157
                .value_hint(ValueHint::Hostname)
38✔
158
                .env(ENV_HOST)
38✔
159
                .default_value(default_host.0.into_owned())
38✔
160
                .help("Host name, unique per clone"),
38✔
161
        )
38✔
162
        .arg(
38✔
163
            Arg::new("remote")
38✔
164
                .global(true)
38✔
165
                .short('R')
38✔
166
                .long("remote")
38✔
167
                .help("Git remote to operate against")
38✔
168
                .value_parser(value_parser!(String))
38✔
169
                .value_hint(ValueHint::Other)
38✔
170
                .env(ENV_REMOTE)
38✔
171
                .default_value(DEFAULT_REMOTE.0.as_ref())
38✔
172
        )
38✔
173
        .subcommand(Command::new("sync").about("Sync local branches to remote"))
38✔
174
        .subcommand(
38✔
175
            Command::new("ls")
38✔
176
                .about("List nomad managed refs")
38✔
177
                .arg(
38✔
178
                    Arg::new("fetch")
38✔
179
                        .short('F')
38✔
180
                        .long("fetch")
38✔
181
                        .help("Fetch refs from remote before listing")
38✔
182
                        .value_parser(value_parser!(bool))
38✔
183
                        .action(ArgAction::SetTrue),
38✔
184
                )
38✔
185
                .arg(
38✔
186
                    Arg::new("print")
38✔
187
                        .long("print")
38✔
188
                        .help("Format for listing nomad managed refs")
38✔
189
                        .value_parser([
38✔
190
                            PossibleValue::new("grouped")
38✔
191
                                .help("Print ref name and commit ID grouped by host"),
38✔
192
                            PossibleValue::new("ref").help("Print only the ref name"),
38✔
193
                            PossibleValue::new("commit").help("Print only the commit ID"),
38✔
194
                        ])
38✔
195
                        .default_value("grouped"),
38✔
196
                )
38✔
197
                .arg(
38✔
198
                    Arg::new("head")
38✔
199
                    .long("head")
38✔
200
                    .help("Only display refs for the current branch")
38✔
201
                    .value_parser(value_parser!(bool))
38✔
202
                    .action(ArgAction::SetTrue),
38✔
203
                )
38✔
204
                .arg(
38✔
205
                    Arg::new("branch")
38✔
206
                    .short('b')
38✔
207
                    .long("branch")
38✔
208
                    .help("Only display refs for the named branch (can be specified multiple times)")
38✔
209
                    .value_parser(value_parser!(String))
38✔
210
                    .action(ArgAction::Append)
38✔
211
                )
38✔
212
                .arg(
38✔
213
                    Arg::new("print_self")
38✔
214
                    .long("print-self")
38✔
215
                    .help("Print refs for the current host")
38✔
216
                    .value_parser(value_parser!(bool))
38✔
217
                    .action(ArgAction::SetTrue)
38✔
218
                ),
38✔
219
        )
38✔
220
        .subcommand(
38✔
221
            Command::new("purge")
38✔
222
                .about("Delete nomad refs locally and on the remote")
38✔
223
                .arg(
38✔
224
                    Arg::new("all")
38✔
225
                        .long("all")
38✔
226
                        .help("Delete refs for all hosts")
38✔
227
                        .value_parser(value_parser!(bool))
38✔
228
                        .action(ArgAction::SetTrue),
38✔
229
                ),
38✔
230
        )
38✔
231
        .try_get_matches_from(args)
38✔
232
}
38✔
233

234
/// The [`Verbosity`] intended by the user via the CLI.
235
fn specified_verbosity(matches: &mut ArgMatches) -> Option<Verbosity> {
14✔
236
    if matches.remove_one::<bool>("quiet").expect("has default") {
14✔
237
        None
4✔
238
    } else {
239
        match matches.remove_one::<u8>("verbose").expect("has default") {
10✔
240
            0 => Some(Verbosity::default()),
2✔
241
            1 => Some(Verbosity::verbose()),
4✔
242
            _ => Some(Verbosity::max()),
4✔
243
        }
244
    }
245
}
14✔
246

247
/// The [`GitBinary`] intended by the user via the CLI.
248
///
249
/// # Panics
250
///
251
/// If [`clap`] does not prevent certain assumed invalid states.
252
fn specified_git(matches: &mut ArgMatches) -> String {
3✔
253
    matches.remove_one("git").expect("default value")
3✔
254
}
3✔
255

256
/// The nomad workflow the user intends to execute via the CLI.
257
///
258
/// # Panics
259
///
260
/// If [`clap`] does not prevent certain assumed invalid states.
261
fn specified_workflow<'a>(
22✔
262
    renderer: &mut impl Renderer,
22✔
263
    matches: &'a mut ArgMatches,
22✔
264
    git: &GitBinary,
22✔
265
) -> anyhow::Result<Workflow<'a>> {
22✔
266
    let user = resolve(matches, "user", || {
22✔
267
        git.get_config(renderer, CONFIG_USER)
19✔
268
            .map(|opt| opt.map(User::from))
19✔
269
    })?;
22✔
270

271
    let host = resolve(matches, "host", || {
22✔
272
        git.get_config(renderer, CONFIG_HOST)
19✔
273
            .map(|opt| opt.map(Host::from))
19✔
274
    })?;
22✔
275

276
    let remote = Remote::from(
22✔
277
        matches
22✔
278
            .remove_one::<String>("remote")
22✔
279
            .expect("default value"),
22✔
280
    );
22✔
281

22✔
282
    let (subcommand, matches) = matches
22✔
283
        .remove_subcommand()
22✔
284
        .expect("subcommand is mandatory");
22✔
285

22✔
286
    return match (subcommand.as_str(), matches) {
22✔
287
        ("sync", _) => Ok(Workflow::Sync { user, host, remote }),
22✔
288

289
        ("ls", mut matches) => Ok(Workflow::Ls {
18✔
290
            printer: match matches
16✔
291
                .remove_one::<String>("print")
16✔
292
                .expect("has default")
16✔
293
                .as_str()
16✔
294
            {
16✔
295
                "grouped" => LsPrinter::Grouped,
16✔
296
                "ref" => LsPrinter::Ref,
4✔
297
                "commit" => LsPrinter::Commit,
2✔
298
                _ => unreachable!("has possible values"),
299
            },
300
            user,
16✔
301
            fetch_remote: if matches.remove_one::<bool>("fetch").expect("has default") {
16✔
302
                Some(remote)
3✔
303
            } else {
304
                None
13✔
305
            },
306
            host_filter: if matches
16✔
307
                .remove_one::<bool>("print_self")
16✔
308
                .expect("has default")
16✔
309
            {
310
                Filter::All
1✔
311
            } else {
312
                Filter::Deny([host].into())
15✔
313
            },
314
            branch_filter: {
315
                let mut branch_set = HashSet::<Branch>::new();
16✔
316

16✔
317
                if matches.remove_one::<bool>("head").expect("has default") {
16✔
318
                    branch_set.insert(git.current_branch(renderer)?);
1✔
319
                }
15✔
320

321
                if let Some(branches) = matches.remove_many::<String>("branch") {
16✔
322
                    branch_set.extend(branches.map(Branch::from));
1✔
323
                }
15✔
324

325
                if branch_set.is_empty() {
16✔
326
                    Filter::All
14✔
327
                } else {
328
                    Filter::Allow(branch_set)
2✔
329
                }
330
            },
331
        }),
332

333
        ("purge", mut matches) => {
2✔
334
            let remote = Remote::from(
2✔
335
                matches
2✔
336
                    .remove_one::<String>("remote")
2✔
337
                    .expect("<remote> is a required argument"),
2✔
338
            );
2✔
339
            let host_filter = if matches.remove_one::<bool>("all").expect("default value") {
2✔
340
                Filter::All
1✔
341
            } else {
342
                Filter::Allow(HashSet::from_iter([host]))
1✔
343
            };
344

345
            return Ok(Workflow::Purge {
2✔
346
                user,
2✔
347
                remote,
2✔
348
                host_filter,
2✔
349
            });
2✔
350
        }
351

352
        _ => unreachable!("unknown subcommand"),
353
    };
354
}
22✔
355

356
/// Extract user arguments in order of preference:
357
///
358
/// 1. Passed in as direct CLI options
359
/// 2. Specified as an environment variable
360
/// 3. Specified in `git config`
361
/// 4. A default from querying the operating system
362
fn resolve<T: Clone + From<String>>(
44✔
363
    matches: &mut ArgMatches,
44✔
364
    arg_name: &str,
44✔
365
    from_git_config: impl FnOnce() -> anyhow::Result<Option<T>>,
44✔
366
) -> anyhow::Result<T> {
44✔
367
    match (
44✔
368
        matches.value_source(arg_name).expect("default value"),
44✔
369
        matches
44✔
370
            .remove_one::<String>(arg_name)
44✔
371
            .expect("default value"),
44✔
372
    ) {
44✔
373
        (ValueSource::CommandLine | ValueSource::EnvVariable, value) => Ok(T::from(value)),
6✔
374
        (_, value) => match from_git_config()? {
38✔
375
            Some(git_value) => Ok(git_value),
3✔
376
            None => Ok(T::from(value)),
35✔
377
        },
378
    }
379
}
44✔
380

381
/// End-to-end workflow tests.
382
#[cfg(test)]
383
mod test_e2e {
384
    use std::{collections::HashSet, iter::FromIterator};
385

386
    use crate::{
387
        git_testing::{GitClone, GitRemote, INITIAL_BRANCH},
388
        nomad,
389
        renderer::test::{MemoryRenderer, NoRenderer},
390
        types::Branch,
391
        verbosity::Verbosity,
392
        workflow::{Filter, Workflow},
393
    };
394

395
    fn sync_host(clone: &GitClone) {
9✔
396
        Workflow::Sync {
9✔
397
            user: clone.user.always_borrow(),
9✔
398
            host: clone.host.always_borrow(),
9✔
399
            remote: clone.remote.always_borrow(),
9✔
400
        }
9✔
401
        .execute(&mut NoRenderer, &clone.git)
9✔
402
        .unwrap();
9✔
403
    }
9✔
404

405
    /// Invoking all the real logic in `nomad` should not panic.
406
    #[test]
1✔
407
    fn nomad_ls() {
1✔
408
        let origin = GitRemote::init(None);
1✔
409
        let mut renderer = MemoryRenderer::new();
1✔
410
        nomad(
1✔
411
            &mut renderer,
1✔
412
            ["git-nomad", "ls"],
1✔
413
            origin.working_directory(),
1✔
414
        )
1✔
415
        .unwrap();
1✔
416
        assert_eq!(renderer.as_str(), "");
1✔
417
    }
1✔
418

419
    /// Syncing should pick up nomad refs from other hosts.
420
    ///
421
    /// When the other host deletes their branch (and thus deletes their nomad ref on the remote),
422
    /// the equivalent local nomad ref for that host should also be deleted.
423
    ///
424
    /// See https://github.com/rraval/git-nomad/issues/1
425
    #[test]
1✔
426
    fn issue_1() {
1✔
427
        let origin = GitRemote::init(None);
1✔
428
        let feature = &Branch::from("feature");
1✔
429

1✔
430
        let host0 = origin.clone("user0", "host0");
1✔
431
        sync_host(&host0);
1✔
432

1✔
433
        let host1 = origin.clone("user0", "host1");
1✔
434
        host1
1✔
435
            .git
1✔
436
            .create_branch(&mut NoRenderer, "Start feature branch", feature)
1✔
437
            .unwrap();
1✔
438
        sync_host(&host1);
1✔
439

1✔
440
        // both hosts have synced, the origin should have refs from both (including the one for the
1✔
441
        // feature branch on host1)
1✔
442
        assert_eq!(
1✔
443
            origin.nomad_refs(),
1✔
444
            HashSet::from_iter([
1✔
445
                host0.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
446
                host1.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
447
                host1.get_nomad_ref("feature").unwrap(),
1✔
448
            ])
1✔
449
        );
1✔
450

451
        // host0 hasn't observed host1 yet
452
        assert_eq!(
1✔
453
            host0.nomad_refs(),
1✔
454
            HashSet::from_iter([host0.get_nomad_ref(INITIAL_BRANCH).unwrap(),])
1✔
455
        );
1✔
456

457
        // sync host0, which should observe host1 refs
458
        sync_host(&host0);
1✔
459
        assert_eq!(
1✔
460
            host0.nomad_refs(),
1✔
461
            HashSet::from_iter([
1✔
462
                host0.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
463
                host1.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
464
                host1.get_nomad_ref("feature").unwrap(),
1✔
465
            ])
1✔
466
        );
1✔
467

468
        // host1 deletes the branch and syncs, removing it from origin
469
        host1
1✔
470
            .git
1✔
471
            .delete_branch(&mut NoRenderer, "Abandon feature branch", feature)
1✔
472
            .unwrap();
1✔
473
        sync_host(&host1);
1✔
474

1✔
475
        assert_eq!(
1✔
476
            origin.nomad_refs(),
1✔
477
            HashSet::from_iter([
1✔
478
                host0.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
479
                host1.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
480
            ])
1✔
481
        );
1✔
482

483
        // host0 syncs and removes the ref for the deleted feature branch
484
        sync_host(&host0);
1✔
485
        assert_eq!(
1✔
486
            host0.nomad_refs(),
1✔
487
            HashSet::from_iter([
1✔
488
                host0.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
489
                host1.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
490
            ])
1✔
491
        );
1✔
492
    }
1✔
493

494
    /// Explicitly pruning other hosts should delete both local and remote nomad refs for that
495
    /// host.
496
    ///
497
    /// See https://github.com/rraval/git-nomad/issues/2
498
    #[test]
1✔
499
    fn issue_2_other_host() {
1✔
500
        let origin = GitRemote::init(None);
1✔
501

1✔
502
        let host0 = origin.clone("user0", "host0");
1✔
503
        sync_host(&host0);
1✔
504

1✔
505
        let host1 = origin.clone("user0", "host1");
1✔
506
        sync_host(&host1);
1✔
507

1✔
508
        // both hosts have synced, the origin should have both refs
1✔
509
        assert_eq!(
1✔
510
            origin.nomad_refs(),
1✔
511
            HashSet::from_iter([
1✔
512
                host0.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
513
                host1.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
514
            ])
1✔
515
        );
1✔
516

517
        // pruning refs for host0 from host1
518
        Workflow::Purge {
1✔
519
            user: host1.user.always_borrow(),
1✔
520
            remote: host1.remote.always_borrow(),
1✔
521
            host_filter: Filter::Allow(HashSet::from_iter([host0.host.always_borrow()])),
1✔
522
        }
1✔
523
        .execute(&mut NoRenderer, &host1.git)
1✔
524
        .unwrap();
1✔
525

1✔
526
        // the origin should only have refs for host1
1✔
527
        assert_eq!(
1✔
528
            origin.nomad_refs(),
1✔
529
            HashSet::from_iter([host1.get_nomad_ref(INITIAL_BRANCH).unwrap(),])
1✔
530
        );
1✔
531
    }
1✔
532

533
    /// Explicitly pruning everything should delete both local and remote refs for both the current
534
    /// and other host on the remote.
535
    ///
536
    /// See https://github.com/rraval/git-nomad/issues/2
537
    #[test]
1✔
538
    fn issue_2_all() {
1✔
539
        let origin = GitRemote::init(Some(Verbosity::max()));
1✔
540

1✔
541
        let host0 = origin.clone("user0", "host0");
1✔
542
        sync_host(&host0);
1✔
543

1✔
544
        let host1 = origin.clone("user0", "host1");
1✔
545
        sync_host(&host1);
1✔
546

1✔
547
        // both hosts have synced, the origin should have both refs
1✔
548
        assert_eq!(
1✔
549
            origin.nomad_refs(),
1✔
550
            HashSet::from_iter([
1✔
551
                host0.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
552
                host1.get_nomad_ref(INITIAL_BRANCH).unwrap(),
1✔
553
            ])
1✔
554
        );
1✔
555

556
        // pruning refs for all hosts from host1
557
        Workflow::Purge {
1✔
558
            user: host1.user.always_borrow(),
1✔
559
            remote: host1.remote,
1✔
560
            host_filter: Filter::All,
1✔
561
        }
1✔
562
        .execute(&mut NoRenderer, &host1.git)
1✔
563
        .unwrap();
1✔
564

1✔
565
        // the origin should have no refs
1✔
566
        assert_eq!(origin.nomad_refs(), HashSet::new(),);
1✔
567
    }
1✔
568
}
569

570
/// CLI invocation tests
571
#[cfg(test)]
572
mod test_cli {
573
    use std::{collections::HashSet, iter::FromIterator};
574

575
    use clap::{error::ErrorKind, ArgMatches};
576

577
    use crate::{
578
        cli,
579
        git_testing::GitRemote,
580
        renderer::test::NoRenderer,
581
        specified_git, specified_verbosity, specified_workflow,
582
        types::{Branch, Host, Remote, User},
583
        verbosity::Verbosity,
584
        workflow::{Filter, LsPrinter, Workflow},
585
        CONFIG_HOST, CONFIG_USER, DEFAULT_REMOTE,
586
    };
587

588
    struct CliTest {
589
        default_user: User<'static>,
590
        default_host: Host<'static>,
591
    }
592

593
    impl CliTest {
594
        fn default_host_filter(&self) -> Filter<Host> {
14✔
595
            Filter::Deny([self.default_host.always_borrow()].into())
14✔
596
        }
14✔
597

598
        fn matches(&self, args: &[&str]) -> clap::error::Result<ArgMatches> {
37✔
599
            let mut vec = vec!["git-nomad"];
37✔
600
            vec.extend_from_slice(args);
37✔
601
            cli(self.default_user.clone(), self.default_host.clone(), &vec)
37✔
602
        }
37✔
603

604
        fn remote(&self, args: &[&str]) -> CliTestRemote {
21✔
605
            CliTestRemote {
21✔
606
                matches: self.matches(args).unwrap(),
21✔
607
                remote: GitRemote::init(Some(Verbosity::max())),
21✔
608
            }
21✔
609
        }
21✔
610
    }
611

612
    struct CliTestRemote {
613
        matches: ArgMatches,
614
        remote: GitRemote,
615
    }
616

617
    impl CliTestRemote {
618
        fn set_config(&mut self, key: &str, value: &str) -> &mut Self {
3✔
619
            self.remote
3✔
620
                .git
3✔
621
                .set_config(&mut NoRenderer, key, value)
3✔
622
                .unwrap();
3✔
623
            self
3✔
624
        }
3✔
625

626
        fn workflow(&mut self) -> Workflow<'_> {
21✔
627
            specified_workflow(&mut NoRenderer, &mut self.matches, &self.remote.git).unwrap()
21✔
628
        }
21✔
629
    }
630

631
    impl Default for CliTest {
632
        fn default() -> Self {
37✔
633
            Self {
37✔
634
                default_user: User::from("default_user"),
37✔
635
                default_host: Host::from("default_host"),
37✔
636
            }
37✔
637
        }
37✔
638
    }
639

640
    /// Should print help and stop processing if no subcommand is specified.
641
    #[test]
1✔
642
    fn subcommand_is_required() {
1✔
643
        let cli_test = CliTest::default();
1✔
644
        let matches = cli_test.matches(&[]);
1✔
645
        assert!(matches.is_err());
1✔
646
        assert_eq!(
1✔
647
            match matches {
1✔
648
                Ok(_) => unreachable!(),
649
                Err(e) => e.kind(),
1✔
650
            },
651
            ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand,
652
        );
653
    }
1✔
654

655
    /// `--git` before/after the subcommand.
656
    #[test]
1✔
657
    fn git_option() {
1✔
658
        for args in &[&["--git", "foo", "ls"], &["ls", "--git", "foo"]] {
3✔
659
            println!("{:?}", args);
2✔
660
            let cli_test = CliTest::default();
2✔
661
            let mut matches = cli_test.matches(*args).unwrap();
2✔
662
            assert_eq!(specified_git(&mut matches), "foo");
2✔
663
        }
664
    }
1✔
665

666
    #[test]
1✔
667
    fn quiet_verbosity() {
1✔
668
        for args in &[
5✔
669
            &["--quiet", "ls"],
5✔
670
            &["-q", "ls"],
5✔
671
            &["ls", "--quiet"],
5✔
672
            &["ls", "-q"],
5✔
673
        ] {
5✔
674
            println!("{:?}", args);
4✔
675
            let cli_test = CliTest::default();
4✔
676
            let mut matches = cli_test.matches(*args).unwrap();
4✔
677
            assert_eq!(specified_verbosity(&mut matches), None);
4✔
678
        }
679
    }
1✔
680

681
    #[test]
1✔
682
    fn default_verbosity() {
1✔
683
        let cli_test = CliTest::default();
1✔
684
        let mut matches = cli_test.matches(&["ls"]).unwrap();
1✔
685
        assert_eq!(
1✔
686
            specified_verbosity(&mut matches),
1✔
687
            Some(Verbosity::default())
1✔
688
        );
1✔
689
    }
1✔
690

691
    #[test]
1✔
692
    fn verbose_verbosity() {
1✔
693
        for args in &[
5✔
694
            &["--verbose", "ls"],
5✔
695
            &["-v", "ls"],
5✔
696
            &["ls", "--verbose"],
5✔
697
            &["ls", "-v"],
5✔
698
        ] {
5✔
699
            println!("{:?}", args);
4✔
700
            let cli_test = CliTest::default();
4✔
701
            let mut matches = cli_test.matches(*args).unwrap();
4✔
702
            assert_eq!(
4✔
703
                specified_verbosity(&mut matches),
4✔
704
                Some(Verbosity::verbose())
4✔
705
            );
4✔
706
        }
707
    }
1✔
708

709
    #[test]
1✔
710
    fn max_verbosity() {
1✔
711
        for args in &[
4✔
712
            &["--verbose", "--verbose", "ls"] as &[&str],
1✔
713
            &["ls", "-vv"],
714
            &["ls", "-v", "--verbose"],
715
            &["ls", "-vv", "-vv"],
716
        ] {
717
            println!("{:?}", args);
4✔
718
            let cli_test = CliTest::default();
4✔
719
            let mut matches = cli_test.matches(*args).unwrap();
4✔
720
            assert_eq!(specified_verbosity(&mut matches), Some(Verbosity::max()));
4✔
721
        }
722
    }
1✔
723

724
    #[test]
1✔
725
    fn ls() {
1✔
726
        let cli_test = CliTest::default();
1✔
727
        assert_eq!(
1✔
728
            cli_test.remote(&["ls"]).workflow(),
1✔
729
            Workflow::Ls {
1✔
730
                printer: LsPrinter::Grouped,
1✔
731
                user: cli_test.default_user.always_borrow(),
1✔
732
                fetch_remote: None,
1✔
733
                host_filter: cli_test.default_host_filter(),
1✔
734
                branch_filter: Filter::All,
1✔
735
            },
1✔
736
        );
1✔
737
    }
1✔
738

739
    #[test]
1✔
740
    fn ls_fetch_remote_default() {
1✔
741
        let cli_test = CliTest::default();
1✔
742
        assert_eq!(
1✔
743
            cli_test.remote(&["ls", "--fetch"]).workflow(),
1✔
744
            Workflow::Ls {
1✔
745
                printer: LsPrinter::Grouped,
1✔
746
                user: cli_test.default_user.always_borrow(),
1✔
747
                fetch_remote: Some(DEFAULT_REMOTE),
1✔
748
                host_filter: cli_test.default_host_filter(),
1✔
749
                branch_filter: Filter::All,
1✔
750
            },
1✔
751
        );
1✔
752
    }
1✔
753

754
    #[test]
1✔
755
    fn ls_fetch_remote_global() {
1✔
756
        let cli_test = CliTest::default();
1✔
757
        assert_eq!(
1✔
758
            cli_test
1✔
759
                .remote(&["--remote", "foo", "ls", "--fetch"])
1✔
760
                .workflow(),
1✔
761
            Workflow::Ls {
1✔
762
                printer: LsPrinter::Grouped,
1✔
763
                user: cli_test.default_user.always_borrow(),
1✔
764
                fetch_remote: Some(Remote::from("foo")),
1✔
765
                host_filter: cli_test.default_host_filter(),
1✔
766
                branch_filter: Filter::All,
1✔
767
            },
1✔
768
        );
1✔
769
    }
1✔
770

771
    #[test]
1✔
772
    fn ls_fetch_remote_local() {
1✔
773
        let cli_test = CliTest::default();
1✔
774
        assert_eq!(
1✔
775
            cli_test
1✔
776
                .remote(&["ls", "--fetch", "--remote", "foo"])
1✔
777
                .workflow(),
1✔
778
            Workflow::Ls {
1✔
779
                printer: LsPrinter::Grouped,
1✔
780
                user: cli_test.default_user.always_borrow(),
1✔
781
                fetch_remote: Some(Remote::from("foo")),
1✔
782
                host_filter: cli_test.default_host_filter(),
1✔
783
                branch_filter: Filter::All,
1✔
784
            },
1✔
785
        );
1✔
786
    }
1✔
787

788
    #[test]
1✔
789
    fn ls_print_grouped() {
1✔
790
        for args in &[
2✔
791
            &["ls", "--print", "grouped"] as &[&str],
1✔
792
            &["ls", "--print=grouped"],
793
        ] {
794
            println!("{:?}", args);
2✔
795

2✔
796
            let cli_test = CliTest::default();
2✔
797
            assert_eq!(
2✔
798
                cli_test.remote(args).workflow(),
2✔
799
                Workflow::Ls {
2✔
800
                    printer: LsPrinter::Grouped,
2✔
801
                    user: cli_test.default_user.always_borrow(),
2✔
802
                    fetch_remote: None,
2✔
803
                    host_filter: cli_test.default_host_filter(),
2✔
804
                    branch_filter: Filter::All,
2✔
805
                },
2✔
806
            );
2✔
807
        }
808
    }
1✔
809

810
    #[test]
1✔
811
    fn ls_print_ref() {
1✔
812
        for args in &[&["ls", "--print", "ref"] as &[&str], &["ls", "--print=ref"]] {
2✔
813
            println!("{:?}", args);
2✔
814

2✔
815
            let cli_test = CliTest::default();
2✔
816
            assert_eq!(
2✔
817
                cli_test.remote(args).workflow(),
2✔
818
                Workflow::Ls {
2✔
819
                    printer: LsPrinter::Ref,
2✔
820
                    user: cli_test.default_user.always_borrow(),
2✔
821
                    fetch_remote: None,
2✔
822
                    host_filter: cli_test.default_host_filter(),
2✔
823
                    branch_filter: Filter::All,
2✔
824
                },
2✔
825
            );
2✔
826
        }
827
    }
1✔
828

829
    #[test]
1✔
830
    fn ls_print_commit() {
1✔
831
        for args in &[
2✔
832
            &["ls", "--print", "commit"] as &[&str],
1✔
833
            &["ls", "--print=commit"],
834
        ] {
835
            println!("{:?}", args);
2✔
836

2✔
837
            let cli_test = CliTest::default();
2✔
838
            assert_eq!(
2✔
839
                cli_test.remote(args).workflow(),
2✔
840
                Workflow::Ls {
2✔
841
                    printer: LsPrinter::Commit,
2✔
842
                    user: cli_test.default_user.always_borrow(),
2✔
843
                    fetch_remote: None,
2✔
844
                    host_filter: cli_test.default_host_filter(),
2✔
845
                    branch_filter: Filter::All,
2✔
846
                },
2✔
847
            );
2✔
848
        }
849
    }
1✔
850

851
    #[test]
1✔
852
    fn ls_explicit() {
1✔
853
        let cli_test = CliTest::default();
1✔
854
        assert_eq!(
1✔
855
            cli_test.remote(&["ls", "-U", "explicit_user"]).workflow(),
1✔
856
            Workflow::Ls {
1✔
857
                printer: LsPrinter::Grouped,
1✔
858
                user: User::from("explicit_user"),
1✔
859
                fetch_remote: None,
1✔
860
                host_filter: cli_test.default_host_filter(),
1✔
861
                branch_filter: Filter::All,
1✔
862
            },
1✔
863
        );
1✔
864
    }
1✔
865

866
    #[test]
1✔
867
    fn ls_config_beats_default() {
1✔
868
        let cli_test = CliTest::default();
1✔
869
        assert_eq!(
1✔
870
            cli_test
1✔
871
                .remote(&["ls"])
1✔
872
                .set_config(CONFIG_USER, "config_user")
1✔
873
                .workflow(),
1✔
874
            Workflow::Ls {
1✔
875
                printer: LsPrinter::Grouped,
1✔
876
                user: User::from("config_user"),
1✔
877
                fetch_remote: None,
1✔
878
                host_filter: cli_test.default_host_filter(),
1✔
879
                branch_filter: Filter::All,
1✔
880
            },
1✔
881
        );
1✔
882
    }
1✔
883

884
    #[test]
1✔
885
    fn ls_head() {
1✔
886
        let cli_test = CliTest::default();
1✔
887
        assert_eq!(
1✔
888
            cli_test.remote(&["ls", "--head"]).workflow(),
1✔
889
            Workflow::Ls {
1✔
890
                printer: LsPrinter::Grouped,
1✔
891
                user: cli_test.default_user.always_borrow(),
1✔
892
                fetch_remote: None,
1✔
893
                host_filter: cli_test.default_host_filter(),
1✔
894
                branch_filter: Filter::Allow(["master"].map(Branch::from).into()),
1✔
895
            },
1✔
896
        );
1✔
897
    }
1✔
898

899
    #[test]
1✔
900
    fn ls_branches() {
1✔
901
        let cli_test = CliTest::default();
1✔
902
        assert_eq!(
1✔
903
            cli_test
1✔
904
                .remote(&["ls", "-b", "foo", "--branch", "bar", "--branch=baz"])
1✔
905
                .workflow(),
1✔
906
            Workflow::Ls {
1✔
907
                printer: LsPrinter::Grouped,
1✔
908
                user: cli_test.default_user.always_borrow(),
1✔
909
                fetch_remote: None,
1✔
910
                host_filter: cli_test.default_host_filter(),
1✔
911
                branch_filter: Filter::Allow(["foo", "bar", "baz"].map(Branch::from).into()),
1✔
912
            },
1✔
913
        );
1✔
914
    }
1✔
915

916
    #[test]
1✔
917
    fn ls_print_self() {
1✔
918
        let cli_test = CliTest::default();
1✔
919
        assert_eq!(
1✔
920
            cli_test.remote(&["ls", "--print-self"]).workflow(),
1✔
921
            Workflow::Ls {
1✔
922
                printer: LsPrinter::Grouped,
1✔
923
                user: cli_test.default_user.always_borrow(),
1✔
924
                fetch_remote: None,
1✔
925
                host_filter: Filter::All,
1✔
926
                branch_filter: Filter::All,
1✔
927
            },
1✔
928
        );
1✔
929
    }
1✔
930

931
    /// Invoke `sync` with explicit `user` and `host`
932
    #[test]
1✔
933
    fn sync_explicit() {
1✔
934
        for args in &[
2✔
935
            &[
1✔
936
                "--user", "user0", "sync", "--host", "host0", "--remote", "remote",
1✔
937
            ] as &[&str],
1✔
938
            &["sync", "-U", "user0", "-H", "host0", "-R", "remote"],
939
        ] {
940
            println!("{:?}", args);
2✔
941
            let cli_test = CliTest::default();
2✔
942
            assert_eq!(
2✔
943
                cli_test.remote(args).workflow(),
2✔
944
                Workflow::Sync {
2✔
945
                    user: User::from("user0"),
2✔
946
                    host: Host::from("host0"),
2✔
947
                    remote: Remote::from("remote"),
2✔
948
                },
2✔
949
            );
2✔
950
        }
951
    }
1✔
952

953
    /// Invoke `sync` with `user` and `host` coming from `git config`.
954
    #[test]
1✔
955
    fn sync_config() {
1✔
956
        let cli_test = CliTest::default();
1✔
957
        assert_eq!(
1✔
958
            cli_test
1✔
959
                .remote(&["sync"])
1✔
960
                .set_config(CONFIG_USER, "user0")
1✔
961
                .set_config(CONFIG_HOST, "host0")
1✔
962
                .workflow(),
1✔
963
            Workflow::Sync {
1✔
964
                user: User::from("user0"),
1✔
965
                host: Host::from("host0"),
1✔
966
                remote: DEFAULT_REMOTE.clone(),
1✔
967
            }
1✔
968
        );
1✔
969
    }
1✔
970

971
    /// Invoke `sync` with defaults.
972
    #[test]
1✔
973
    fn sync_default() {
1✔
974
        let cli_test = CliTest::default();
1✔
975
        assert_eq!(
1✔
976
            cli_test.remote(&["sync"]).workflow(),
1✔
977
            Workflow::Sync {
1✔
978
                user: cli_test.default_user.always_borrow(),
1✔
979
                host: cli_test.default_host.always_borrow(),
1✔
980
                remote: DEFAULT_REMOTE.clone(),
1✔
981
            }
1✔
982
        );
1✔
983
    }
1✔
984

985
    #[test]
1✔
986
    fn purge_all() {
1✔
987
        let cli_test = CliTest::default();
1✔
988
        assert_eq!(
1✔
989
            cli_test.remote(&["purge", "--all"]).workflow(),
1✔
990
            Workflow::Purge {
1✔
991
                user: cli_test.default_user.always_borrow(),
1✔
992
                remote: DEFAULT_REMOTE.clone(),
1✔
993
                host_filter: Filter::All,
1✔
994
            }
1✔
995
        );
1✔
996
    }
1✔
997

998
    #[test]
1✔
999
    fn purge_hosts() {
1✔
1000
        let cli_test = CliTest::default();
1✔
1001
        assert_eq!(
1✔
1002
            cli_test
1✔
1003
                .remote(&["--host=host0", "purge", "-R", "remote"])
1✔
1004
                .workflow(),
1✔
1005
            Workflow::Purge {
1✔
1006
                user: cli_test.default_user.always_borrow(),
1✔
1007
                remote: Remote::from("remote"),
1✔
1008
                host_filter: Filter::Allow(HashSet::from_iter(["host0"].map(Host::from))),
1✔
1009
            }
1✔
1010
        );
1✔
1011
    }
1✔
1012
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc