hglib/
client.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this file,
3// You can obtain one at http://mozilla.org/MPL/2.0/.
4
5extern crate byteorder;
6extern crate subprocess;
7
8use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
9use std::env;
10use std::ffi::OsString;
11use std::fs::File;
12use std::io::{Cursor, Read, Write};
13use std::path::{Path, PathBuf};
14use std::result::Result;
15use subprocess::{Popen, PopenConfig, Redirection};
16
17pub trait Runner {
18    /// Run a command
19    fn runcommand<'a>(
20        &mut self,
21        args: &'a [&str],
22        prompt: Option<Box<dyn Prompt + 'a>>,
23    ) -> Result<(Vec<u8>, i32), HglibError>;
24}
25
26pub trait Prompt {
27    fn call(&mut self, size: usize) -> &[u8];
28}
29
30#[derive(Debug)]
31pub struct Client {
32    /// the server process
33    server: Popen,
34    /// the encoding used for this process
35    encoding: String,
36    /// the canonicalized path
37    path: PathBuf,
38}
39
40pub struct Basic {}
41
42#[derive(Debug)]
43pub struct HglibError {
44    pub code: i32,
45    pub out: Option<Vec<u8>>,
46    msg: String,
47}
48
49impl HglibError {
50    pub(crate) fn handle_err(x: Result<(Vec<u8>, i32), HglibError>) -> Result<bool, HglibError> {
51        match x {
52            Ok((_, code)) => Ok(code == 0),
53            Err(err) => {
54                if err.code == 0 {
55                    Ok(true)
56                } else if err.code == 1 {
57                    Ok(false)
58                } else {
59                    Err(err)
60                }
61            }
62        }
63    }
64}
65
66impl<T: std::string::ToString> From<T> for HglibError {
67    fn from(err: T) -> HglibError {
68        HglibError {
69            code: -1,
70            out: None,
71            msg: err.to_string(),
72        }
73    }
74}
75
76impl Drop for Client {
77    fn drop(&mut self) {
78        self.close().unwrap();
79    }
80}
81
82impl Client {
83    /// Open a new hglib client
84    /// # Example
85    /// ```
86    /// extern crate hglib;
87    ///
88    /// use hglib::{commit, hg, init, log, Basic, Client, HG};
89    /// use std::fs::File;
90    /// use std::io::Write;
91    ///
92    /// fn main() {
93    ///     let path = "my_hg_repo";
94    ///     assert!(HG!(init, dest = &path).is_ok());
95    ///
96    ///     let mut client = Client::open(&path, "UTF-8", &[]).unwrap();
97    ///     let path = client.get_path();
98    ///
99    ///     let mut file = File::create(path.join("hello.world")).unwrap();
100    ///     file.write_all(b"Hello, world!").unwrap();
101    ///
102    ///     hg!(
103    ///         client,
104    ///         commit,
105    ///         message = "My first commit",
106    ///         addremove = true,
107    ///         user = "foo@bar.com"
108    ///     )
109    ///     .unwrap();
110    ///
111    ///     let rev = hg!(client, log).unwrap();
112    ///
113    ///     println!("{:?}", rev);
114    /// }
115    /// ```
116    pub fn open<P: AsRef<Path>>(
117        path: P,
118        encoding: &str,
119        configs: &[&str],
120    ) -> Result<Client, HglibError> {
121        let mut env: Vec<(OsString, OsString)> = env::vars_os().collect();
122        env.push((OsString::from("HGPLAIN"), OsString::from("1")));
123        if !encoding.is_empty() {
124            env.push((OsString::from("HGENCODING"), OsString::from(encoding)));
125        }
126
127        let path = path.as_ref().to_path_buf().canonicalize()?;
128        let path_str = path.to_str().unwrap();
129
130        let mut args = vec!["hg", "serve", "--cmdserver", "pipe", "-R", path_str];
131        for c in configs.iter() {
132            args.push("--config");
133            args.push(c);
134        }
135        let mut server = Popen::create(
136            &args,
137            PopenConfig {
138                stdout: Redirection::Pipe,
139                stdin: Redirection::Pipe,
140                stderr: Redirection::Pipe,
141                env: Some(env),
142                cwd: Some(OsString::from(path_str)),
143                ..Default::default()
144            },
145        )?;
146        let encoding = Client::read_hello(&mut server)?;
147        let client = Client {
148            server,
149            encoding,
150            path,
151        };
152        Ok(client)
153    }
154
155    /// Close the client
156    pub fn close(&mut self) -> Result<(), HglibError> {
157        self.server.terminate()?;
158        self.server.wait()?;
159        Ok(())
160    }
161
162    /// Get the canonicalized path for this repository
163    pub fn get_path(&self) -> &PathBuf {
164        &self.path
165    }
166
167    fn read_hello(server: &mut Popen) -> Result<String, HglibError> {
168        let stdout = server.stdout.as_mut().unwrap();
169        let mut chan: Vec<u8> = vec![0; 1];
170        let n = stdout.read(&mut chan)?;
171        if n != 1 || chan[0] != b'o' {
172            return Err("Cannot read hello".into());
173        }
174
175        let len = stdout.read_u32::<BigEndian>()? as usize;
176        let mut data: Vec<u8> = vec![0; len];
177
178        let n = stdout.read(&mut data)?;
179        if n != len {
180            return Err("Cannot read hello (invalid length)".into());
181        }
182
183        let out = std::str::from_utf8(&data)?;
184        let out: Vec<&str> = out.split('\n').collect();
185
186        if !out[0].contains("capabilities: ") {
187            return Err("Cannot read hello: no capabilities ".into());
188        }
189
190        if !out[1].contains("encoding: ") {
191            return Err("Cannot read hello: no encoding ".into());
192        }
193
194        Ok(out[1]["encoding: ".len()..].to_string())
195    }
196
197    fn read_data(
198        mut to_read: usize,
199        output: &mut Vec<u8>,
200        stdout: &mut File,
201    ) -> Result<(), HglibError> {
202        let mut pos = output.len();
203        output.resize(pos + to_read, 0);
204        loop {
205            let n = stdout.read(&mut output[pos..])?;
206            if n == to_read {
207                break;
208            }
209            to_read -= n;
210            pos += n;
211        }
212        Ok(())
213    }
214
215    pub fn encoding(&self) -> &str {
216        &self.encoding
217    }
218}
219
220impl Runner for Client {
221    fn runcommand<'a>(
222        &mut self,
223        args: &'a [&str],
224        mut prompt: Option<Box<dyn Prompt + 'a>>,
225    ) -> Result<(Vec<u8>, i32), HglibError> {
226        /* Write the data on stdin:
227        runcommand\n
228        len(arg0\0arg1\0arg2...)
229        arg0\0arg1\0arg2... */
230        let mut stdin = self.server.stdin.as_mut().unwrap();
231        let args_size: usize = args.iter().map(|arg| -> usize { arg.len() }).sum();
232        let size = args_size + args.len() - 1;
233        writeln!(&mut stdin, "runcommand")?;
234        stdin.write_u32::<BigEndian>(size as u32)?;
235        if let Some((first, args)) = args.split_first() {
236            write!(&mut stdin, "{}", first)?;
237            for arg in args {
238                write!(&mut stdin, "\0{}", arg)?;
239            }
240        }
241        stdin.flush()?;
242
243        /* Read the data on stdout:
244        o{u32 = len}{data}
245        ...
246        r{u32} */
247        let stdout = self.server.stdout.as_mut().unwrap();
248        let mut out = Vec::<u8>::with_capacity(4096);
249        let mut chan: Vec<u8> = vec![0; 1];
250        //let mut returned_err: Option<String> = None;
251        loop {
252            let n = stdout.read(&mut chan)?;
253            if n != 1 {
254                return Err("Empty stdout".into());
255            }
256            let len = stdout.read_u32::<BigEndian>()? as usize;
257            match chan[0] {
258                b'e' => {
259                    // We've an error
260                    let mut err = Vec::<u8>::with_capacity(512);
261                    Client::read_data(len, &mut err, stdout)?;
262                    //let err = String::from_utf8(err)?;
263                    //returned_err = Some(err);
264                }
265                b'o' => {
266                    Client::read_data(len, &mut out, stdout)?;
267                }
268                b'r' => {
269                    let mut code: Vec<u8> = vec![0; len];
270                    stdout.read_exact(&mut code)?;
271                    let mut cur = Cursor::new(&code);
272                    let code = cur.read_i32::<BigEndian>()?;
273                    // TODO: error may have been set and code == 0 when we've a warning
274                    // so handle that with an error handler
275                    return /* if let Some(msg) = returned_err {
276                        Err(HglibError {
277                            code,
278                            out: Some(out),
279                            msg,
280                        })
281                    } else */ if code != 0 {
282                        Err(HglibError {
283                            code,
284                            out: Some(out.clone()),
285                            msg: String::from_utf8(out).unwrap(),
286                        })
287                    } else {
288                        Ok((out, code))
289                    };
290                }
291                b'L' => {
292                    if let Some(prompt) = prompt.as_mut() {
293                        let buf = prompt.call(len);
294                        stdin.write_u32::<BigEndian>(buf.len() as u32)?;
295                        stdin.write_all(buf)?;
296                        stdin.flush()?;
297                    } else {
298                        stdin.write_u32::<BigEndian>(0)?;
299                        stdin.flush()?;
300                        return Err("Hglib error: something is expected on stdin, please implement a Prompt".into());
301                    }
302                }
303                _ => {
304                    return Err(format!("Hglib error: invalid channel {}", chan[0] as char).into());
305                }
306            }
307        }
308    }
309}
310
311impl Runner for Basic {
312    fn runcommand<'a>(
313        &mut self,
314        args: &'a [&str],
315        _: Option<Box<dyn Prompt + 'a>>,
316    ) -> Result<(Vec<u8>, i32), HglibError> {
317        let env: Vec<(OsString, OsString)> = env::vars_os().collect();
318        let mut command = Vec::with_capacity(args.len() + 1);
319        command.push("hg");
320        command.extend(args);
321
322        let mut process = Popen::create(
323            &command,
324            PopenConfig {
325                stdout: Redirection::Pipe,
326                cwd: Some(OsString::from(std::env::current_dir().unwrap())),
327                env: Some(env),
328                ..Default::default()
329            },
330        )?;
331
332        process.wait()?;
333
334        Ok((Vec::new(), 0))
335    }
336}