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
use std::path::PathBuf;

/// A repository path which either points to a work tree or the `.git` repository itself.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Path {
    /// The currently checked out linked worktree along with its connected and existing git directory, or the worktree checkout of a
    /// submodule.
    LinkedWorkTree {
        /// The base of the work tree.
        work_dir: PathBuf,
        /// The worktree-private git dir, located within the main git directory which holds most of the information.
        git_dir: PathBuf,
    },
    /// The currently checked out or nascent work tree of a git repository
    WorkTree(PathBuf),
    /// The git repository itself, typically bare and without known worktree.
    ///
    /// Note that it might still have linked work-trees which can be accessed later, weather bare or not, or it might be a
    /// submodule git directory in the `.git/modules/**/<name>` directory of the parent repository.
    Repository(PathBuf),
}

mod path {
    use std::path::PathBuf;

    use crate::{
        path::without_dot_git_dir,
        repository::{Kind, Path},
        DOT_GIT_DIR,
    };

    impl AsRef<std::path::Path> for Path {
        fn as_ref(&self) -> &std::path::Path {
            match self {
                Path::WorkTree(path)
                | Path::Repository(path)
                | Path::LinkedWorkTree {
                    work_dir: _,
                    git_dir: path,
                } => path,
            }
        }
    }

    impl Path {
        /// Instantiate a new path from `dir` which is expected to be the `.git` directory, with `kind` indicating
        /// whether it's a bare repository or not, with `current_dir` being used to normalize relative paths
        /// as needed.
        ///
        /// `None` is returned if `dir` could not be resolved due to being relative and trying to reach outside of the filesystem root.
        pub fn from_dot_git_dir(dir: PathBuf, kind: Kind, current_dir: &std::path::Path) -> Option<Self> {
            let cwd = current_dir;
            let normalize_on_trailing_dot_dot = |dir: PathBuf| -> Option<PathBuf> {
                if !matches!(dir.components().next_back(), Some(std::path::Component::ParentDir)) {
                    dir
                } else {
                    gix_path::normalize(dir.into(), cwd)?.into_owned()
                }
                .into()
            };

            match kind {
                Kind::Submodule { git_dir } => Path::LinkedWorkTree {
                    git_dir: gix_path::normalize(git_dir.into(), cwd)?.into_owned(),
                    work_dir: without_dot_git_dir(normalize_on_trailing_dot_dot(dir)?),
                },
                Kind::SubmoduleGitDir => Path::Repository(dir),
                Kind::WorkTreeGitDir { work_dir } => Path::LinkedWorkTree { git_dir: dir, work_dir },
                Kind::WorkTree { linked_git_dir } => match linked_git_dir {
                    Some(git_dir) => Path::LinkedWorkTree {
                        git_dir,
                        work_dir: without_dot_git_dir(normalize_on_trailing_dot_dot(dir)?),
                    },
                    None => {
                        let mut dir = normalize_on_trailing_dot_dot(dir)?;
                        dir.pop(); // ".git" suffix
                        let work_dir = dir.as_os_str().is_empty().then(|| PathBuf::from(".")).unwrap_or(dir);
                        Path::WorkTree(work_dir)
                    }
                },
                Kind::PossiblyBare => Path::Repository(dir),
            }
            .into()
        }
        /// Returns the [kind][Kind] of this repository path.
        pub fn kind(&self) -> Kind {
            match self {
                Path::LinkedWorkTree { work_dir: _, git_dir } => Kind::WorkTree {
                    linked_git_dir: Some(git_dir.to_owned()),
                },
                Path::WorkTree(_) => Kind::WorkTree { linked_git_dir: None },
                Path::Repository(_) => Kind::PossiblyBare,
            }
        }

        /// Consume and split this path into the location of the `.git` directory as well as an optional path to the work tree.
        pub fn into_repository_and_work_tree_directories(self) -> (PathBuf, Option<PathBuf>) {
            match self {
                Path::LinkedWorkTree { work_dir, git_dir } => (git_dir, Some(work_dir)),
                Path::WorkTree(working_tree) => (working_tree.join(DOT_GIT_DIR), Some(working_tree)),
                Path::Repository(repository) => (repository, None),
            }
        }
    }
}

/// The kind of repository path.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Kind {
    /// A bare repository does not have a work tree, that is files on disk beyond the `git` repository itself.
    ///
    /// Note that this is merely a guess at this point as we didn't read the configuration yet.
    ///
    /// Also note that due to optimizing for performance and *just* making an educated *guess in some situations*,
    /// we may consider a non-bare repository bare if it it doesn't have an index yet due to be freshly initialized.
    /// The caller is has to handle this, typically by reading the configuration.
    PossiblyBare,
    /// A `git` repository along with checked out files in a work tree.
    WorkTree {
        /// If set, this is the git dir associated with this _linked_ worktree.
        /// If `None`, the git_dir is the `.git` directory inside the _main_ worktree we represent.
        linked_git_dir: Option<PathBuf>,
    },
    /// A worktree's git directory in the common`.git` directory in `worktrees/<name>`.
    WorkTreeGitDir {
        /// Path to the worktree directory.
        work_dir: PathBuf,
    },
    /// The directory is a `.git` dir file of a submodule worktree.
    Submodule {
        /// The git repository itself that is referenced by the `.git` dir file, typically in the `.git/modules/**/<name>` directory of the parent
        /// repository.
        git_dir: PathBuf,
    },
    /// The git directory in the `.git/modules/**/<name>` directory tree of the parent repository
    SubmoduleGitDir,
}

impl Kind {
    /// Returns true if this is a bare repository, one without a work tree.
    pub fn is_bare(&self) -> bool {
        matches!(self, Kind::PossiblyBare)
    }
}