libscilo/version_control/
mod.rs

1//! This module contains functions and structures related to different version control systems.
2//! See [`SupportedVCS`] for which systems are supported.
3
4use std::{
5    env::current_dir,
6    fs, io,
7    ops::Not,
8    path::{Path, PathBuf},
9};
10use strum::VariantArray;
11
12/// Which version control systems are supported.
13#[derive(VariantArray)]
14pub enum SupportedVCS {
15    /// [Git](https://git-scm.com) repositories
16    Git,
17
18    ///[Jujutsu](https://www.jj-vcs.dev/latest/) repositories
19    Jujutsu,
20
21    /// [Mercurial](https://mercurial-scm.org/) repositories
22    Mercurial,
23
24    /// [Subversion](https://subversion.apache.org/) repositories
25    Subversion,
26}
27
28impl SupportedVCS {
29    /// The name of the directory containing repository information for different version control systems.
30    fn repo_dirname(&self) -> &str {
31        match self {
32            Self::Git => ".git",
33            Self::Jujutsu => ".jj",
34            Self::Mercurial => ".hg",
35            Self::Subversion => ".svn",
36        }
37    }
38}
39
40/// Determine the project root folder.
41///
42/// This currently supports `git` as the version control that defines the root of a project.
43pub fn project_root() -> PathBuf {
44    match current_dir() {
45        Ok(current_dir) => match determine_vcs_root(&current_dir) {
46            Ok(dir) => dir,
47            Err(_) => current_dir,
48        },
49        Err(_) => panic!(
50            "Error getting the current working directory. This directory might have been deleted by another process. Please double check this is the correct directory."
51        ),
52    }
53}
54
55/// Determine the directory that is the repository root.
56///
57/// This function works by ascending up the file hierarchy and looking for the version control repository folder for each of [`SupportedVCS`].
58fn determine_vcs_root(start_path: &Path) -> io::Result<PathBuf> {
59    let mut path = start_path;
60    let mut errors = Vec::new();
61
62    loop {
63        let mut dir_contents = fs::read_dir(path)?;
64
65        let contains_vcs_info = dir_contents.any(|entry| match entry {
66            Ok(entry) => match entry.file_name().to_str() {
67                // check if the file name matches any of the string variants for a supported VCS
68                Some(name) => SupportedVCS::VARIANTS
69                    .iter()
70                    .any(|vcs| vcs.repo_dirname() == name),
71                None => {
72                    errors.push(format!(
73                        "Error when reading entry name {0} inside directory {1}.",
74                        entry.file_name().display(),
75                        path.display()
76                    ));
77                    false
78                }
79            },
80            Err(cause) => {
81                errors.push(format!(
82                    "Error when reading entry inside directory {0}: {1}",
83                    path.display(),
84                    cause,
85                ));
86                false
87            }
88        });
89
90        if contains_vcs_info {
91            return Ok(path.to_path_buf());
92        }
93
94        if let Some(parent) = path.parent() {
95            path = parent;
96        } else {
97            let mut message = format!(
98                "No parent of {0} contains version control information. Are you sure you're in a repository?",
99                start_path.display(),
100            );
101
102            //only report errors, if we don't find a .git directory, to avoid noisy logging and the need for a logging dependency
103            if errors.is_empty().not() {
104                message.push_str("\n\nThe following errors have occurred while looking for version control information:\n");
105                for error in errors {
106                    message.push_str(&format!("  - {error}\n"));
107                }
108            }
109
110            return Err(io::Error::other(message));
111        }
112    }
113}