Skip to main content

cargo/ops/
cargo_new.rs

1use crate::core::{Shell, Workspace};
2use crate::util::errors::{CargoResult, CargoResultExt};
3use crate::util::{existing_vcs_repo, FossilRepo, GitRepo, HgRepo, PijulRepo};
4use crate::util::{paths, restricted_names, Config};
5use git2::Config as GitConfig;
6use git2::Repository as GitRepository;
7use serde::de;
8use serde::Deserialize;
9use std::collections::BTreeMap;
10use std::env;
11use std::fmt;
12use std::fs;
13use std::io::{BufRead, BufReader, ErrorKind};
14use std::path::{Path, PathBuf};
15use std::process::Command;
16use std::str::{from_utf8, FromStr};
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub enum VersionControl {
20    Git,
21    Hg,
22    Pijul,
23    Fossil,
24    NoVcs,
25}
26
27impl FromStr for VersionControl {
28    type Err = anyhow::Error;
29
30    fn from_str(s: &str) -> Result<Self, anyhow::Error> {
31        match s {
32            "git" => Ok(VersionControl::Git),
33            "hg" => Ok(VersionControl::Hg),
34            "pijul" => Ok(VersionControl::Pijul),
35            "fossil" => Ok(VersionControl::Fossil),
36            "none" => Ok(VersionControl::NoVcs),
37            other => anyhow::bail!("unknown vcs specification: `{}`", other),
38        }
39    }
40}
41
42impl<'de> de::Deserialize<'de> for VersionControl {
43    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
44    where
45        D: de::Deserializer<'de>,
46    {
47        let s = String::deserialize(deserializer)?;
48        FromStr::from_str(&s).map_err(de::Error::custom)
49    }
50}
51
52#[derive(Debug)]
53pub struct NewOptions {
54    pub version_control: Option<VersionControl>,
55    pub kind: NewProjectKind,
56    /// Absolute path to the directory for the new package
57    pub path: PathBuf,
58    pub name: Option<String>,
59    pub edition: Option<String>,
60    pub registry: Option<String>,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum NewProjectKind {
65    Bin,
66    Lib,
67}
68
69impl NewProjectKind {
70    fn is_bin(self) -> bool {
71        self == NewProjectKind::Bin
72    }
73}
74
75impl fmt::Display for NewProjectKind {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match *self {
78            NewProjectKind::Bin => "binary (application)",
79            NewProjectKind::Lib => "library",
80        }
81        .fmt(f)
82    }
83}
84
85struct SourceFileInformation {
86    relative_path: String,
87    target_name: String,
88    bin: bool,
89}
90
91struct MkOptions<'a> {
92    version_control: Option<VersionControl>,
93    path: &'a Path,
94    name: &'a str,
95    source_files: Vec<SourceFileInformation>,
96    bin: bool,
97    edition: Option<&'a str>,
98    registry: Option<&'a str>,
99}
100
101impl NewOptions {
102    pub fn new(
103        version_control: Option<VersionControl>,
104        bin: bool,
105        lib: bool,
106        path: PathBuf,
107        name: Option<String>,
108        edition: Option<String>,
109        registry: Option<String>,
110    ) -> CargoResult<NewOptions> {
111        let kind = match (bin, lib) {
112            (true, true) => anyhow::bail!("can't specify both lib and binary outputs"),
113            (false, true) => NewProjectKind::Lib,
114            // default to bin
115            (_, false) => NewProjectKind::Bin,
116        };
117
118        let opts = NewOptions {
119            version_control,
120            kind,
121            path,
122            name,
123            edition,
124            registry,
125        };
126        Ok(opts)
127    }
128}
129
130#[derive(Deserialize)]
131struct CargoNewConfig {
132    name: Option<String>,
133    email: Option<String>,
134    #[serde(rename = "vcs")]
135    version_control: Option<VersionControl>,
136}
137
138fn get_name<'a>(path: &'a Path, opts: &'a NewOptions) -> CargoResult<&'a str> {
139    if let Some(ref name) = opts.name {
140        return Ok(name);
141    }
142
143    let file_name = path.file_name().ok_or_else(|| {
144        anyhow::format_err!(
145            "cannot auto-detect package name from path {:?} ; use --name to override",
146            path.as_os_str()
147        )
148    })?;
149
150    file_name.to_str().ok_or_else(|| {
151        anyhow::format_err!(
152            "cannot create package with a non-unicode name: {:?}",
153            file_name
154        )
155    })
156}
157
158fn check_name(name: &str, name_help: &str, has_bin: bool, shell: &mut Shell) -> CargoResult<()> {
159    restricted_names::validate_package_name(name, "crate name", name_help)?;
160
161    if restricted_names::is_keyword(name) {
162        anyhow::bail!(
163            "the name `{}` cannot be used as a crate name, it is a Rust keyword{}",
164            name,
165            name_help
166        );
167    }
168    if restricted_names::is_conflicting_artifact_name(name) {
169        if has_bin {
170            anyhow::bail!(
171                "the name `{}` cannot be used as a crate name, \
172                it conflicts with cargo's build directory names{}",
173                name,
174                name_help
175            );
176        } else {
177            shell.warn(format!(
178                "the name `{}` will not support binary \
179                executables with that name, \
180                it conflicts with cargo's build directory names",
181                name
182            ))?;
183        }
184    }
185    if name == "test" {
186        anyhow::bail!(
187            "the name `test` cannot be used as a crate name, \
188            it conflicts with Rust's built-in test library{}",
189            name_help
190        );
191    }
192    if ["core", "std", "alloc", "proc_macro", "proc-macro"].contains(&name) {
193        shell.warn(format!(
194            "the name `{}` is part of Rust's standard library\n\
195            It is recommended to use a different name to avoid problems.",
196            name
197        ))?;
198    }
199    if restricted_names::is_windows_reserved(name) {
200        if cfg!(windows) {
201            anyhow::bail!(
202                "cannot use name `{}`, it is a reserved Windows filename{}",
203                name,
204                name_help
205            );
206        } else {
207            shell.warn(format!(
208                "the name `{}` is a reserved Windows filename\n\
209                This package will not work on Windows platforms.",
210                name
211            ))?;
212        }
213    }
214    if restricted_names::is_non_ascii_name(name) {
215        shell.warn(format!(
216            "the name `{}` contains non-ASCII characters\n\
217            Support for non-ASCII crate names is experimental and only valid \
218            on the nightly toolchain.",
219            name
220        ))?;
221    }
222
223    Ok(())
224}
225
226fn detect_source_paths_and_types(
227    package_path: &Path,
228    package_name: &str,
229    detected_files: &mut Vec<SourceFileInformation>,
230) -> CargoResult<()> {
231    let path = package_path;
232    let name = package_name;
233
234    enum H {
235        Bin,
236        Lib,
237        Detect,
238    }
239
240    struct Test {
241        proposed_path: String,
242        handling: H,
243    }
244
245    let tests = vec![
246        Test {
247            proposed_path: "src/main.rs".to_string(),
248            handling: H::Bin,
249        },
250        Test {
251            proposed_path: "main.rs".to_string(),
252            handling: H::Bin,
253        },
254        Test {
255            proposed_path: format!("src/{}.rs", name),
256            handling: H::Detect,
257        },
258        Test {
259            proposed_path: format!("{}.rs", name),
260            handling: H::Detect,
261        },
262        Test {
263            proposed_path: "src/lib.rs".to_string(),
264            handling: H::Lib,
265        },
266        Test {
267            proposed_path: "lib.rs".to_string(),
268            handling: H::Lib,
269        },
270    ];
271
272    for i in tests {
273        let pp = i.proposed_path;
274
275        // path/pp does not exist or is not a file
276        if !fs::metadata(&path.join(&pp))
277            .map(|x| x.is_file())
278            .unwrap_or(false)
279        {
280            continue;
281        }
282
283        let sfi = match i.handling {
284            H::Bin => SourceFileInformation {
285                relative_path: pp,
286                target_name: package_name.to_string(),
287                bin: true,
288            },
289            H::Lib => SourceFileInformation {
290                relative_path: pp,
291                target_name: package_name.to_string(),
292                bin: false,
293            },
294            H::Detect => {
295                let content = paths::read(&path.join(pp.clone()))?;
296                let isbin = content.contains("fn main");
297                SourceFileInformation {
298                    relative_path: pp,
299                    target_name: package_name.to_string(),
300                    bin: isbin,
301                }
302            }
303        };
304        detected_files.push(sfi);
305    }
306
307    // Check for duplicate lib attempt
308
309    let mut previous_lib_relpath: Option<&str> = None;
310    let mut duplicates_checker: BTreeMap<&str, &SourceFileInformation> = BTreeMap::new();
311
312    for i in detected_files {
313        if i.bin {
314            if let Some(x) = BTreeMap::get::<str>(&duplicates_checker, i.target_name.as_ref()) {
315                anyhow::bail!(
316                    "\
317multiple possible binary sources found:
318  {}
319  {}
320cannot automatically generate Cargo.toml as the main target would be ambiguous",
321                    &x.relative_path,
322                    &i.relative_path
323                );
324            }
325            duplicates_checker.insert(i.target_name.as_ref(), i);
326        } else {
327            if let Some(plp) = previous_lib_relpath {
328                anyhow::bail!(
329                    "cannot have a package with \
330                     multiple libraries, \
331                     found both `{}` and `{}`",
332                    plp,
333                    i.relative_path
334                )
335            }
336            previous_lib_relpath = Some(&i.relative_path);
337        }
338    }
339
340    Ok(())
341}
342
343fn plan_new_source_file(bin: bool, package_name: String) -> SourceFileInformation {
344    if bin {
345        SourceFileInformation {
346            relative_path: "src/main.rs".to_string(),
347            target_name: package_name,
348            bin: true,
349        }
350    } else {
351        SourceFileInformation {
352            relative_path: "src/lib.rs".to_string(),
353            target_name: package_name,
354            bin: false,
355        }
356    }
357}
358
359pub fn new(opts: &NewOptions, config: &Config) -> CargoResult<()> {
360    let path = &opts.path;
361    if fs::metadata(path).is_ok() {
362        anyhow::bail!(
363            "destination `{}` already exists\n\n\
364             Use `cargo init` to initialize the directory",
365            path.display()
366        )
367    }
368
369    let name = get_name(path, opts)?;
370    check_name(name, "", opts.kind.is_bin(), &mut config.shell())?;
371
372    let mkopts = MkOptions {
373        version_control: opts.version_control,
374        path,
375        name,
376        source_files: vec![plan_new_source_file(opts.kind.is_bin(), name.to_string())],
377        bin: opts.kind.is_bin(),
378        edition: opts.edition.as_deref(),
379        registry: opts.registry.as_deref(),
380    };
381
382    mk(config, &mkopts).chain_err(|| {
383        anyhow::format_err!(
384            "Failed to create package `{}` at `{}`",
385            name,
386            path.display()
387        )
388    })?;
389    Ok(())
390}
391
392pub fn init(opts: &NewOptions, config: &Config) -> CargoResult<()> {
393    // This is here just as a random location to exercise the internal error handling.
394    if std::env::var_os("__CARGO_TEST_INTERNAL_ERROR").is_some() {
395        return Err(crate::util::internal("internal error test"));
396    }
397
398    let path = &opts.path;
399
400    if fs::metadata(&path.join("Cargo.toml")).is_ok() {
401        anyhow::bail!("`cargo init` cannot be run on existing Cargo packages")
402    }
403
404    let name = get_name(path, opts)?;
405
406    let mut src_paths_types = vec![];
407
408    detect_source_paths_and_types(path, name, &mut src_paths_types)?;
409
410    if src_paths_types.is_empty() {
411        src_paths_types.push(plan_new_source_file(opts.kind.is_bin(), name.to_string()));
412    } else {
413        // --bin option may be ignored if lib.rs or src/lib.rs present
414        // Maybe when doing `cargo init --bin` inside a library package stub,
415        // user may mean "initialize for library, but also add binary target"
416    }
417    let has_bin = src_paths_types.iter().any(|x| x.bin);
418    // If --name is already used to override, no point in suggesting it
419    // again as a fix.
420    let name_help = match opts.name {
421        Some(_) => "",
422        None => "\nuse --name to override crate name",
423    };
424    check_name(name, name_help, has_bin, &mut config.shell())?;
425
426    let mut version_control = opts.version_control;
427
428    if version_control == None {
429        let mut num_detected_vsces = 0;
430
431        if fs::metadata(&path.join(".git")).is_ok() {
432            version_control = Some(VersionControl::Git);
433            num_detected_vsces += 1;
434        }
435
436        if fs::metadata(&path.join(".hg")).is_ok() {
437            version_control = Some(VersionControl::Hg);
438            num_detected_vsces += 1;
439        }
440
441        if fs::metadata(&path.join(".pijul")).is_ok() {
442            version_control = Some(VersionControl::Pijul);
443            num_detected_vsces += 1;
444        }
445
446        if fs::metadata(&path.join(".fossil")).is_ok() {
447            version_control = Some(VersionControl::Fossil);
448            num_detected_vsces += 1;
449        }
450
451        // if none exists, maybe create git, like in `cargo new`
452
453        if num_detected_vsces > 1 {
454            anyhow::bail!(
455                "more than one of .hg, .git, .pijul, .fossil configurations \
456                 found and the ignore file can't be filled in as \
457                 a result. specify --vcs to override detection"
458            );
459        }
460    }
461
462    let mkopts = MkOptions {
463        version_control,
464        path,
465        name,
466        bin: has_bin,
467        source_files: src_paths_types,
468        edition: opts.edition.as_deref(),
469        registry: opts.registry.as_deref(),
470    };
471
472    mk(config, &mkopts).chain_err(|| {
473        anyhow::format_err!(
474            "Failed to create package `{}` at `{}`",
475            name,
476            path.display()
477        )
478    })?;
479    Ok(())
480}
481
482/// IgnoreList
483struct IgnoreList {
484    /// git like formatted entries
485    ignore: Vec<String>,
486    /// mercurial formatted entries
487    hg_ignore: Vec<String>,
488}
489
490impl IgnoreList {
491    /// constructor to build a new ignore file
492    fn new() -> IgnoreList {
493        IgnoreList {
494            ignore: Vec::new(),
495            hg_ignore: Vec::new(),
496        }
497    }
498
499    /// add a new entry to the ignore list. Requires two arguments with the
500    /// entry in two different formats. One for "git style" entries and one for
501    /// "mercurial like" entries.
502    fn push(&mut self, ignore: &str, hg_ignore: &str) {
503        self.ignore.push(ignore.to_string());
504        self.hg_ignore.push(hg_ignore.to_string());
505    }
506
507    /// Return the correctly formatted content of the ignore file for the given
508    /// version control system as `String`.
509    fn format_new(&self, vcs: VersionControl) -> String {
510        let ignore_items = match vcs {
511            VersionControl::Hg => &self.hg_ignore,
512            _ => &self.ignore,
513        };
514
515        ignore_items.join("\n") + "\n"
516    }
517
518    /// format_existing is used to format the IgnoreList when the ignore file
519    /// already exists. It reads the contents of the given `BufRead` and
520    /// checks if the contents of the ignore list are already existing in the
521    /// file.
522    fn format_existing<T: BufRead>(&self, existing: T, vcs: VersionControl) -> String {
523        // TODO: is unwrap safe?
524        let existing_items = existing.lines().collect::<Result<Vec<_>, _>>().unwrap();
525
526        let ignore_items = match vcs {
527            VersionControl::Hg => &self.hg_ignore,
528            _ => &self.ignore,
529        };
530
531        let mut out = "\n\n#Added by cargo\n".to_string();
532        if ignore_items
533            .iter()
534            .any(|item| existing_items.contains(item))
535        {
536            out.push_str("#\n#already existing elements were commented out\n");
537        }
538        out.push('\n');
539
540        for item in ignore_items {
541            if existing_items.contains(item) {
542                out.push('#');
543            }
544            out.push_str(item);
545            out.push('\n');
546        }
547
548        out
549    }
550}
551
552/// Writes the ignore file to the given directory. If the ignore file for the
553/// given vcs system already exists, its content is read and duplicate ignore
554/// file entries are filtered out.
555fn write_ignore_file(
556    base_path: &Path,
557    list: &IgnoreList,
558    vcs: VersionControl,
559) -> CargoResult<String> {
560    let fp_ignore = match vcs {
561        VersionControl::Git => base_path.join(".gitignore"),
562        VersionControl::Hg => base_path.join(".hgignore"),
563        VersionControl::Pijul => base_path.join(".ignore"),
564        VersionControl::Fossil => return Ok("".to_string()),
565        VersionControl::NoVcs => return Ok("".to_string()),
566    };
567
568    let ignore: String = match fs::File::open(&fp_ignore) {
569        Err(why) => match why.kind() {
570            ErrorKind::NotFound => list.format_new(vcs),
571            _ => return Err(anyhow::format_err!("{}", why)),
572        },
573        Ok(file) => list.format_existing(BufReader::new(file), vcs),
574    };
575
576    paths::append(&fp_ignore, ignore.as_bytes())?;
577
578    Ok(ignore)
579}
580
581/// Initializes the correct VCS system based on the provided config.
582fn init_vcs(path: &Path, vcs: VersionControl, config: &Config) -> CargoResult<()> {
583    match vcs {
584        VersionControl::Git => {
585            if !path.join(".git").exists() {
586                // Temporary fix to work around bug in libgit2 when creating a
587                // directory in the root of a posix filesystem.
588                // See: https://github.com/libgit2/libgit2/issues/5130
589                paths::create_dir_all(path)?;
590                GitRepo::init(path, config.cwd())?;
591            }
592        }
593        VersionControl::Hg => {
594            if !path.join(".hg").exists() {
595                HgRepo::init(path, config.cwd())?;
596            }
597        }
598        VersionControl::Pijul => {
599            if !path.join(".pijul").exists() {
600                PijulRepo::init(path, config.cwd())?;
601            }
602        }
603        VersionControl::Fossil => {
604            if !path.join(".fossil").exists() {
605                FossilRepo::init(path, config.cwd())?;
606            }
607        }
608        VersionControl::NoVcs => {
609            paths::create_dir_all(path)?;
610        }
611    };
612
613    Ok(())
614}
615
616fn mk(config: &Config, opts: &MkOptions<'_>) -> CargoResult<()> {
617    let path = opts.path;
618    let name = opts.name;
619    let cfg = config.get::<CargoNewConfig>("cargo-new")?;
620
621    // Using the push method with two arguments ensures that the entries for
622    // both `ignore` and `hgignore` are in sync.
623    let mut ignore = IgnoreList::new();
624    ignore.push("/target", "^target/");
625    if !opts.bin {
626        ignore.push("Cargo.lock", "glob:Cargo.lock");
627    }
628
629    let vcs = opts.version_control.unwrap_or_else(|| {
630        let in_existing_vcs = existing_vcs_repo(path.parent().unwrap_or(path), config.cwd());
631        match (cfg.version_control, in_existing_vcs) {
632            (None, false) => VersionControl::Git,
633            (Some(opt), false) => opt,
634            (_, true) => VersionControl::NoVcs,
635        }
636    });
637
638    init_vcs(path, vcs, config)?;
639    write_ignore_file(path, &ignore, vcs)?;
640
641    let (author_name, email) = discover_author()?;
642    let author = match (cfg.name, cfg.email, author_name, email) {
643        (Some(name), Some(email), _, _)
644        | (Some(name), None, _, Some(email))
645        | (None, Some(email), name, _)
646        | (None, None, name, Some(email)) => {
647            if email.is_empty() {
648                name
649            } else {
650                format!("{} <{}>", name, email)
651            }
652        }
653        (Some(name), None, _, None) | (None, None, name, None) => name,
654    };
655
656    let mut cargotoml_path_specifier = String::new();
657
658    // Calculate what `[lib]` and `[[bin]]`s we need to append to `Cargo.toml`.
659
660    for i in &opts.source_files {
661        if i.bin {
662            if i.relative_path != "src/main.rs" {
663                cargotoml_path_specifier.push_str(&format!(
664                    r#"
665[[bin]]
666name = "{}"
667path = {}
668"#,
669                    i.target_name,
670                    toml::Value::String(i.relative_path.clone())
671                ));
672            }
673        } else if i.relative_path != "src/lib.rs" {
674            cargotoml_path_specifier.push_str(&format!(
675                r#"
676[lib]
677name = "{}"
678path = {}
679"#,
680                i.target_name,
681                toml::Value::String(i.relative_path.clone())
682            ));
683        }
684    }
685
686    // Create `Cargo.toml` file with necessary `[lib]` and `[[bin]]` sections, if needed.
687
688    paths::write(
689        &path.join("Cargo.toml"),
690        format!(
691            r#"[package]
692name = "{}"
693version = "0.1.0"
694authors = [{}]
695edition = {}
696{}
697# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
698
699[dependencies]
700{}"#,
701            name,
702            toml::Value::String(author),
703            match opts.edition {
704                Some(edition) => toml::Value::String(edition.to_string()),
705                None => toml::Value::String("2018".to_string()),
706            },
707            match opts.registry {
708                Some(registry) => format!(
709                    "publish = {}\n",
710                    toml::Value::Array(vec!(toml::Value::String(registry.to_string())))
711                ),
712                None => "".to_string(),
713            },
714            cargotoml_path_specifier
715        )
716        .as_bytes(),
717    )?;
718
719    // Create all specified source files (with respective parent directories) if they don't exist.
720
721    for i in &opts.source_files {
722        let path_of_source_file = path.join(i.relative_path.clone());
723
724        if let Some(src_dir) = path_of_source_file.parent() {
725            paths::create_dir_all(src_dir)?;
726        }
727
728        let default_file_content: &[u8] = if i.bin {
729            b"\
730fn main() {
731    println!(\"Hello, world!\");
732}
733"
734        } else {
735            b"\
736#[cfg(test)]
737mod tests {
738    #[test]
739    fn it_works() {
740        assert_eq!(2 + 2, 4);
741    }
742}
743"
744        };
745
746        if !fs::metadata(&path_of_source_file)
747            .map(|x| x.is_file())
748            .unwrap_or(false)
749        {
750            paths::write(&path_of_source_file, default_file_content)?;
751
752            // Format the newly created source file
753            match Command::new("rustfmt").arg(&path_of_source_file).output() {
754                Err(e) => log::warn!("failed to call rustfmt: {}", e),
755                Ok(output) => {
756                    if !output.status.success() {
757                        log::warn!("rustfmt failed: {:?}", from_utf8(&output.stdout));
758                    }
759                }
760            };
761        }
762    }
763
764    if let Err(e) = Workspace::new(&path.join("Cargo.toml"), config) {
765        let msg = format!(
766            "compiling this new crate may not work due to invalid \
767             workspace configuration\n\n{:?}",
768            e,
769        );
770        config.shell().warn(msg)?;
771    }
772
773    Ok(())
774}
775
776fn get_environment_variable(variables: &[&str]) -> Option<String> {
777    variables.iter().filter_map(|var| env::var(var).ok()).next()
778}
779
780fn discover_author() -> CargoResult<(String, Option<String>)> {
781    let git_config = find_git_config();
782    let git_config = git_config.as_ref();
783
784    let name_variables = [
785        "CARGO_NAME",
786        "GIT_AUTHOR_NAME",
787        "GIT_COMMITTER_NAME",
788        "USER",
789        "USERNAME",
790        "NAME",
791    ];
792    let name = get_environment_variable(&name_variables[0..3])
793        .or_else(|| git_config.and_then(|g| g.get_string("user.name").ok()))
794        .or_else(|| get_environment_variable(&name_variables[3..]));
795
796    let name = match name {
797        Some(name) => name,
798        None => {
799            let username_var = if cfg!(windows) { "USERNAME" } else { "USER" };
800            anyhow::bail!(
801                "could not determine the current user, please set ${}",
802                username_var
803            )
804        }
805    };
806    let email_variables = [
807        "CARGO_EMAIL",
808        "GIT_AUTHOR_EMAIL",
809        "GIT_COMMITTER_EMAIL",
810        "EMAIL",
811    ];
812    let email = get_environment_variable(&email_variables[0..3])
813        .or_else(|| git_config.and_then(|g| g.get_string("user.email").ok()))
814        .or_else(|| get_environment_variable(&email_variables[3..]));
815
816    let name = name.trim().to_string();
817    let email = email.map(|s| {
818        let mut s = s.trim();
819
820        // In some cases emails will already have <> remove them since they
821        // are already added when needed.
822        if s.starts_with('<') && s.ends_with('>') {
823            s = &s[1..s.len() - 1];
824        }
825
826        s.to_string()
827    });
828
829    Ok((name, email))
830}
831
832fn find_git_config() -> Option<GitConfig> {
833    match env::var("__CARGO_TEST_ROOT") {
834        Ok(test_root) => find_tests_git_config(test_root),
835        Err(_) => find_real_git_config(),
836    }
837}
838
839fn find_tests_git_config(cargo_test_root: String) -> Option<GitConfig> {
840    // Path where 'git config --local' puts variables when run from inside a test
841    let test_git_config = PathBuf::from(cargo_test_root).join(".git").join("config");
842
843    if test_git_config.exists() {
844        GitConfig::open(&test_git_config).ok()
845    } else {
846        GitConfig::open_default().ok()
847    }
848}
849
850fn find_real_git_config() -> Option<GitConfig> {
851    match env::current_dir() {
852        Ok(cwd) => GitRepository::discover(cwd)
853            .and_then(|repo| repo.config())
854            .or_else(|_| GitConfig::open_default())
855            .ok(),
856        Err(_) => GitConfig::open_default().ok(),
857    }
858}