libimaginitcmd/
lib.rs

1//
2// imag - the personal information management suite for the commandline
3// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors
4//
5// This library is free software; you can redistribute it and/or
6// modify it under the terms of the GNU Lesser General Public
7// License as published by the Free Software Foundation; version
8// 2.1 of the License.
9//
10// This library is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13// Lesser General Public License for more details.
14//
15// You should have received a copy of the GNU Lesser General Public
16// License along with this library; if not, write to the Free Software
17// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18//
19
20#![forbid(unsafe_code)]
21
22#![deny(
23    non_camel_case_types,
24    non_snake_case,
25    path_statements,
26    trivial_numeric_casts,
27    unstable_features,
28    unused_allocation,
29    unused_import_braces,
30    unused_imports,
31    unused_must_use,
32    unused_mut,
33    unused_qualifications,
34    while_true,
35)]
36
37extern crate clap;
38#[macro_use] extern crate failure;
39
40#[cfg(test)]
41extern crate toml;
42
43#[macro_use] extern crate libimagrt;
44extern crate libimagerror;
45
46mod ui;
47
48use std::fs::OpenOptions;
49use std::io::Write;
50use std::path::PathBuf;
51use std::path::Path;
52use std::process::Command;
53
54use failure::Fallible as Result;
55use failure::ResultExt;
56use failure::Error;
57use failure::err_msg;
58
59use libimagrt::runtime::Runtime;
60use libimagrt::application::ImagApplication;
61
62use clap::App;
63
64const CONFIGURATION_STR : &str = include_str!("../imagrc.toml");
65
66const GITIGNORE_STR : &str = r#"
67# We ignore the imagrc.toml file by default
68#
69# That is because we expect the user to put
70# this dotfile into his dotfile repository
71# and symlink it here.
72# If you do not do this, feel free to remove
73# this line from the gitignore and add the
74# configuration to this git repository.
75
76imagrc.toml
77"#;
78
79/// Marker enum for implementing ImagApplication on
80///
81/// This is used by binaries crates to execute business logic
82/// or to build a CLI completion.
83pub enum ImagInit {}
84impl ImagApplication for ImagInit {
85    fn run(_rt: Runtime) -> Result<()> {
86        panic!("imag-init needs to be run as a seperate binary, or we'll need to figure something out here!");
87    }
88
89    fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
90        ui::build_ui(app)
91    }
92
93    fn name() -> &'static str {
94        env!("CARGO_PKG_NAME")
95    }
96
97    fn description() -> &'static str {
98        "Intialize the imag store"
99    }
100
101    fn version() -> &'static str {
102        env!("CARGO_PKG_VERSION")
103    }
104}
105
106pub fn imag_init() -> Result<()> {
107    let version = make_imag_version!();
108    let app     = ui::build_ui(Runtime::get_default_cli_builder(
109        "imag-init",
110        version.as_str(),
111        "Intializes the imag store, optionally with git"));
112    let matches = app.get_matches();
113    let mut out = ::std::io::stdout();
114
115    let path = if let Some(p) = matches.value_of("path") {
116        PathBuf::from(String::from(p))
117    } else {
118        ::std::env::var("HOME")
119            .map_err(Error::from)
120            .map(PathBuf::from)
121            .map(|mut p| { p.push(".imag"); p })
122            .and_then(|path| if path.exists() {
123                Err(format_err!("Cannot continue: Path '{}' already exists", path.display()))
124            } else {
125                Ok(path)
126            })
127            .map_err(|_| err_msg("Failed to retrieve/build path for imag directory."))?
128    };
129
130    {
131        let mut store_path = path.clone();
132        store_path.push("store");
133        println!("Creating {}", store_path.display());
134
135        ::std::fs::create_dir_all(store_path).context("Failed to create directory")?;
136    }
137
138    let config_path = {
139        let mut config_path = path.clone();
140        config_path.push("imagrc.toml");
141        config_path
142    };
143
144    OpenOptions::new()
145        .write(true)
146        .create(true)
147        .open(config_path)
148        .map_err(Error::from)
149        .and_then(|mut f| {
150            let content = if matches.is_present("devel") {
151                get_config_devel()
152            } else {
153                get_config()
154            };
155
156            f.write_all(content.as_bytes())
157                .context("Failed to write complete config to file")
158                .map_err(Error::from)
159        })
160        .context("Failed to open new configuration file")?;
161
162    if find_command("git").is_some() && !matches.is_present("nogit") {
163        // we initialize a git repository
164        writeln!(out, "Going to initialize a git repository in the imag directory...")?;
165
166        let gitignore_path = {
167            let mut gitignore_path = path.clone();
168            gitignore_path.push(".gitignore");
169            gitignore_path.to_str().map(String::from)
170        }.ok_or_else(|| err_msg("Cannot convert path to string"))?;
171
172        OpenOptions::new()
173            .write(true)
174            .create(true)
175            .open(gitignore_path.clone())
176            .map_err(Error::from)
177            .and_then(|mut f| {
178                f.write_all(GITIGNORE_STR.as_bytes())
179                    .context("Failed to write complete gitignore to file")
180                    .map_err(Error::from)
181            })
182            .context("Failed to open new configuration file")?;
183
184        let path_str = path.to_str().map(String::from).ok_or_else(|| err_msg("Cannot convert path to string"))?;
185        let worktree = format!("--work-tree={}", path_str);
186        let gitdir   = format!("--git-dir={}/.git", path_str);
187
188        {
189            let output = Command::new("git")
190                .args(&[&worktree, &gitdir, "--no-pager", "init"])
191                .output()
192                .context("Calling 'git init' failed")?;
193
194            if output.status.success() {
195                writeln!(out, "{}", String::from_utf8(output.stdout).expect("No UTF-8 output"))?;
196                writeln!(out, "'git {} {} --no-pager init' succeeded", worktree, gitdir)?;
197            } else {
198                writeln!(out, "{}", String::from_utf8(output.stderr).expect("No UTF-8 output"))?;
199                if !output.status.success() {
200                    return Err(err_msg("Failed to execute git command"));
201                }
202            }
203        }
204
205        {
206            let output = Command::new("git")
207                .args(&[&worktree, &gitdir, "--no-pager", "add", &gitignore_path])
208                .output()
209                .context("Calling 'git add' failed")?;
210
211            if output.status.success() {
212                writeln!(out, "{}", String::from_utf8(output.stdout).expect("No UTF-8 output"))?;
213                writeln!(out, "'git {} {} --no-pager add {}' succeeded", worktree, gitdir, gitignore_path)?;
214            } else {
215                writeln!(out, "{}", String::from_utf8(output.stderr).expect("No UTF-8 output"))?;
216                if !output.status.success() {
217                    return Err(err_msg("Failed to execute git command"));
218                }
219            }
220        }
221
222        {
223            let output = Command::new("git")
224                .args(&[&worktree, &gitdir, "--no-pager", "commit", &gitignore_path, "-m", "'Initial import'"])
225                .output()
226                .context("Calling 'git commit' failed")?;
227            if output.status.success() {
228                writeln!(out, "{}", String::from_utf8(output.stdout).expect("No UTF-8 output"))?;
229                writeln!(out, "'git {} {} --no-pager commit {} -m 'Initial import'' succeeded", worktree, gitdir, gitignore_path)?;
230            } else {
231                writeln!(out, "{}", String::from_utf8(output.stderr).expect("No UTF-8 output"))?;
232                if !output.status.success() {
233                    return Err(err_msg("Failed to execute git command"));
234                }
235            }
236        }
237
238        writeln!(out, "git stuff finished!")?;
239    } else {
240        writeln!(out, "No git repository will be initialized")?;
241    }
242
243    writeln!(out, "Ready. Have fun with imag!").map_err(Error::from)
244}
245
246fn get_config() -> String {
247    get_config_devel()
248        .replace(
249            r#"level = "debug""#,
250            r#"level = "info""#
251        )
252}
253
254fn get_config_devel() -> String {
255    String::from(CONFIGURATION_STR)
256}
257
258fn find_command<P: AsRef<Path>>(exe_name: P) -> Option<PathBuf> {
259    ::std::env::var_os("PATH")
260        .and_then(|paths| {
261            ::std::env::split_paths(&paths)
262                .filter_map(|dir| {
263                    let full_path = dir.join(&exe_name);
264                    if full_path.is_file() {
265                        Some(full_path)
266                    } else {
267                        None
268                    }
269                })
270                .next()
271        })
272}
273
274#[cfg(test)]
275mod tests {
276    use toml::from_str;
277    use toml::Value;
278    use super::get_config;
279    use super::get_config_devel;
280
281    #[test]
282    fn test_config() {
283        let val = from_str::<Value>(&get_config()[..]);
284        assert!(val.is_ok(), "Config parsing errored: {:?}", val);
285
286        let val = from_str::<Value>(&get_config_devel()[..]);
287        assert!(val.is_ok(), "Config parsing errored: {:?}", val);
288    }
289
290}