summaryrefslogtreecommitdiff
path: root/src/parser.rs
blob: 5806cec3b53289ac4d4db5d8414ef43eacd54263 (plain)
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
use clap::Parser;
use std::{
    path::{Path, PathBuf},
    str::FromStr,
};
use thiserror::Error;

#[derive(Parser, Clone, Debug, PartialEq, Eq)]
#[command(name = "")]
pub enum ShackleCommand {
    /// Create a new repository
    Init(InitArgs),
    /// List all repositories available
    List(ListArgs),
    /// Sets the description of a repository, as shown in the CLI listing and web interfaces
    SetDescription(SetDescriptionArgs),
    /// Sets the main branch of the repository
    SetBranch(SetBranchArgs),
    /// Deletes a repository
    Delete(DeleteArgs),
    /// Does any housekeeping, like deleting unreachable objects and repacking more efficiently
    Housekeeping(HousekeepingArgs),
    /// Quit the shell
    Exit,
    /// Server side command required to git fetch from the server
    GitUploadPack(GitUploadPackArgs),
    /// Server side command required by git push to the server
    GitReceivePack(GitReceivePackArgs),
}

#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct InitArgs {
    /// Share repository ownership with the specified group (user must be a member of the group)
    #[arg(long)]
    pub group: Option<String>,
    /// Sets the description of the repository, as shown in the CLI listing and web interfaces
    #[arg(long)]
    pub description: Option<String>,
    /// Sets the main branch of the repository
    #[arg(long, default_value = "main")]
    pub branch: String,
    /// Name of the new repository
    pub repo_name: String,
}

#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct ListArgs {
    /// List extra metadata, like the repo's size on disk
    #[arg(short, long)]
    pub verbose: bool,
}

#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct SetDescriptionArgs {
    /// The full relative path of the repository, for example git/shuckie/repo.git
    #[arg(value_parser = RelativePathParser)]
    pub directory: PathBuf,
    /// The new description
    pub description: String,
}

#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct SetBranchArgs {
    /// The full relative path of the repository, for example git/shuckie/repo.git
    #[arg(value_parser = RelativePathParser)]
    pub directory: PathBuf,
    /// The new branch name
    pub branch: String,
}

#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct DeleteArgs {
    /// The full relative path of the repository, for example git/shuckie/repo.git
    #[arg(value_parser = RelativePathParser)]
    pub directory: PathBuf,
}

#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct HousekeepingArgs {
    /// The full relative path of the repository, for example
    /// git/shuckie/repo.git. If omitted, all repos will be checked.
    pub directory: Option<PathBuf>,
}

#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct GitUploadPackArgs {
    /// Do not try <directory>/.git/ if <directory> is no Git directory
    #[arg(long, default_value_t = true)]
    pub strict: bool,
    /// Always try <directory>/.git/ if <directory> is no Git directory - this argument is accepted for compatability with git, but is ignored
    #[arg(long)]
    pub no_strict: bool,
    /// Interrupt transfer after <TIMEOUT> seconds of inactivity
    #[arg(long)]
    pub timeout: Option<u32>,
    /// Perform only a single read-write cycle with stdin and stdout
    #[arg(long)]
    pub stateless_rpc: bool,
    /// Only the initial ref advertisement is output, and the program exits immediately
    #[arg(long)]
    pub advertise_refs: bool,
    /// The full relative path of the repository for example git/shuckie/repo.git
    #[arg(value_parser = RelativePathParser)]
    pub directory: PathBuf,
}

#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct GitReceivePackArgs {
    /// The full relative path of the repository for example git/shuckie/repo.git
    #[arg(value_parser = RelativePathParser)]
    pub directory: PathBuf,
}

#[derive(Error, Debug)]
pub enum ParserError {
    #[error(transparent)]
    ClapError(#[from] clap::error::Error),
    #[error("`{0}`")]
    LexerError(String),
}

impl FromStr for ShackleCommand {
    type Err = ParserError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let trimmed = s.trim();
        let lexed = shlex::split(trimmed);
        match lexed {
            None => Err(ParserError::LexerError("Incomplete input".to_string())),
            Some(lexed) => {
                let parsed =
                    ShackleCommand::try_parse_from(["".to_owned()].into_iter().chain(lexed))?;
                Ok(parsed)
            }
        }
    }
}

#[derive(Clone)]
struct RelativePathParser;

impl clap::builder::TypedValueParser for RelativePathParser {
    type Value = std::path::PathBuf;

    fn parse_ref(
        &self,
        cmd: &clap::Command,
        arg: Option<&clap::Arg>,
        value: &std::ffi::OsStr,
    ) -> Result<Self::Value, clap::Error> {
        clap::builder::TypedValueParser::parse(self, cmd, arg, value.to_owned())
    }

    fn parse(
        &self,
        cmd: &clap::Command,
        arg: Option<&clap::Arg>,
        value: std::ffi::OsString,
    ) -> Result<Self::Value, clap::Error> {
        let raw = clap::builder::PathBufValueParser::default().parse(cmd, arg, value)?;
        Ok(raw
            .strip_prefix(Path::new("/~"))
            .or_else(|_| raw.strip_prefix(Path::new("~")))
            .map(|m| m.to_owned())
            .unwrap_or(raw))
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn it_parses_exit_correctly() {
        assert_eq!(
            "exit".parse::<ShackleCommand>().unwrap(),
            ShackleCommand::Exit
        );
    }

    #[test]
    fn it_parses_git_upload_pack_correctly() {
        assert_eq!(
            "git-upload-pack --stateless-rpc foobar.git"
                .parse::<ShackleCommand>()
                .unwrap(),
            ShackleCommand::GitUploadPack(GitUploadPackArgs {
                strict: true,
                no_strict: false,
                timeout: None,
                stateless_rpc: true,
                advertise_refs: false,
                directory: PathBuf::from("foobar.git"),
            })
        );
    }
}