Patchwork [1,of,8] rust-hglib: import the latest version and update URLs

login
register
mail settings
Submitter Yuya Nishihara
Date April 1, 2018, 11:14 a.m.
Message ID <9e25c96124d51e11022b.1522581257@mimosa>
Download mbox | patch
Permalink /patch/30116/
State Accepted
Headers show

Comments

Yuya Nishihara - April 1, 2018, 11:14 a.m.
# HG changeset patch
# User Kevin Bullock <kbullock@ringworld.org>
# Date 1522477348 -32400
#      Sat Mar 31 15:22:28 2018 +0900
# Node ID 9e25c96124d51e11022b0ce64783f5f333ede7fb
# Parent  2ed180117f7658d0cbf6a1ece20944465c55c947
rust-hglib: import the latest version and update URLs

Source:
  http://kbullock.ringworld.org/hg/rust-hglib/ -r 742ad4a344d7
  "merge rust-1.4 into default" (2016-07-12 12:27 -0500)

All mercurial.selenic.com URLs are replaced with www.mercurial-scm.org
to silence the check-code.

Committed by Yuya Nishihara <yuya@tcha.org>
Gregory Szorc - April 1, 2018, 6:53 p.m.
On Sun, Apr 1, 2018 at 4:14 AM, Yuya Nishihara <yuya@tcha.org> wrote:

> # HG changeset patch
> # User Kevin Bullock <kbullock@ringworld.org>
> # Date 1522477348 -32400
> #      Sat Mar 31 15:22:28 2018 +0900
> # Node ID 9e25c96124d51e11022b0ce64783f5f333ede7fb
> # Parent  2ed180117f7658d0cbf6a1ece20944465c55c947
> rust-hglib: import the latest version and update URLs
>

Queued parts 1-3 because vendoring rust-hglib has been talked about and
agreed upon IIRC. They are pretty mechanical changes and I don't see any
major issues with them.

The remaining parts look reasonable to me. But I figured I'd wait for
someone with more Rust experience to look at them.

Also, Kevin Kox has been giving good Rust reviews. But I believe they are
only active in Phabricator. Consider submitting futures Rust patches to
Phabricator or CC'ing Kevin explicitly.


>
> Source:
>   http://kbullock.ringworld.org/hg/rust-hglib/ -r 742ad4a344d7
>   "merge rust-1.4 into default" (2016-07-12 12:27 -0500)
>
> All mercurial.selenic.com URLs are replaced with www.mercurial-scm.org
> to silence the check-code.
>
> Committed by Yuya Nishihara <yuya@tcha.org>
>
> diff --git a/rust/hglib/Cargo.toml b/rust/hglib/Cargo.toml
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/Cargo.toml
> @@ -0,0 +1,25 @@
> +[package]
> +name = "hglib"
> +version = "0.1.1"
> +authors = ["Kevin Bullock <kbullock@ringworld.org>"]
> +description = "Mercurial command server client library."
> +readme = "README.md"
> +documentation = "http://kbullock.ringworld.org/rustdoc/hglib/"
> +repository = "http://kbullock.ringworld.org/hg/rust-hglib/"
> +license = "MIT"
> +
> +include = [
> +    "LICENSE",
> +    "README.md",
> +    "RELEASES.md",
> +    "src/**/*.rs",
> +    "Cargo.toml",
> +    "examples/**/*.rs",
> +]
> +
> +[dependencies]
> +byteorder = "0.3.13"
> +
> +[dev-dependencies]
> +tempdir = "0.3.4"
> +lazy_static = "0.1.14"
> diff --git a/rust/hglib/LICENSE b/rust/hglib/LICENSE
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/LICENSE
> @@ -0,0 +1,20 @@
> +Copyright (c) 2015 Kevin R. Bullock
> +
> +Permission is hereby granted, free of charge, to any person obtaining
> +a copy of this software and associated documentation files (the
> +"Software"), to deal in the Software without restriction, including
> +without limitation the rights to use, copy, modify, merge, publish,
> +distribute, sublicense, and/or sell copies of the Software, and to
> +permit persons to whom the Software is furnished to do so, subject to
> +the following conditions:
> +
> +The above copyright notice and this permission notice shall be
> +included in all copies or substantial portions of the Software.
> +
> +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
> +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
> +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
> +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
> +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
> +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
> +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
> diff --git a/rust/hglib/README.md b/rust/hglib/README.md
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/README.md
> @@ -0,0 +1,22 @@
> +HgLib: Rust Client Library for Mercurial Command Server
> +=======================================================
> +
> +This crate provides a client interface to the Mercurial distributed
> +version control system (DVCS) in Rust, using Mercurial's
> +[command server][]. The command server is designed to allow tools to be
> +built around Mercurial repositories, without being tied into Mercurial's
> +internal API or licensing.
> +
> +[command server]: https://www.mercurial-scm.org/wiki/CommandServer
> +
> +[![](http://meritbadge.herokuapp.com/hglib)](https://crates
> .io/crates/hglib)
> +
> +API documentation: <http://kbullock.ringworld.org/rustdoc/hglib/>
> +
> +Installation
> +------------
> +
> +To use hglib, add the crate to your Cargo.toml:
> +
> +    [dependencies]
> +    hglib = "0.1.1"
> diff --git a/rust/hglib/RELEASES.md b/rust/hglib/RELEASES.md
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/RELEASES.md
> @@ -0,0 +1,9 @@
> +Version 0.1.1 (2015-09-23)
> +==========================
> +
> +* Now builds on Rust 1.3 (stable).
> +
> +Version 0.1.0 (2015-09-21)
> +==========================
> +
> +* Initial release.
> diff --git a/rust/hglib/examples/client.rs b/rust/hglib/examples/client.rs
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/examples/client.rs
> @@ -0,0 +1,48 @@
> +extern crate hglib;
> +extern crate tempdir;
> +
> +use hglib::cmdserver::CommandServer;
> +use hglib::Chunk;
> +use std::env;
> +use tempdir::TempDir;
> +
> +fn run_command(cmdserver: &mut CommandServer, command: Vec<&[u8]>) ->
> (i32, Vec<u8>, Vec<u8>) {
> +    let (mut result, mut output, mut error) =
> +        (-1i32, vec![], vec![]);
> +    let run = cmdserver.connection
> +        .raw_command(command)
> +        .expect("failed to send 'log' command");
> +
> +    for chunk in run {
> +        match chunk {
> +            Ok(Chunk::Output(s)) => output.extend(s),
> +            Ok(Chunk::Error(s)) => error.extend(s),
> +            Ok(Chunk::Result(r)) => { result = r },
> +            Ok(_) => unimplemented!(),
> +            Err(e) => panic!("failed to read command results: {}", e),
> +        }
> +    }
> +
> +    (result, output, error)
> +}
> +
> +fn main() {
> +    let tmpdir = TempDir::new("hglib").unwrap();
> +    env::set_current_dir(tmpdir.path()).unwrap();
> +
> +    let mut cmdserver = CommandServer::new().expect("failed to start
> command server");
> +    println!("capabilities: {:?}", cmdserver.capabilities);
> +    println!("encoding: {:?}", cmdserver.encoding);
> +
> +    let _ = run_command(&mut cmdserver, vec![b"init"]);
> +
> +    let (result, output, error) =
> +        run_command(&mut cmdserver, vec![b"log", b"-l", b"5"]);
> +    cmdserver.connection.close().expect("command server did not stop
> cleanly");
> +
> +    println!("output: {}",
> +             String::from_utf8(output).unwrap().trim_right_matches('\n')
> );
> +    println!("error: {}",
> +             String::from_utf8(error).unwrap().trim_right_matches('\n'));
> +    println!("result: {:?}", result);
> +}
> diff --git a/rust/hglib/src/cmdserver.rs b/rust/hglib/src/cmdserver.rs
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/src/cmdserver.rs
> @@ -0,0 +1,46 @@
> +//! High-level interface to the Mercurial command server.
> +//!
> +//! This module is currently only a skeletal implementation. In the
> +//! future this will allow you to run Mercurial commands by calling
> +//! methods of the same name:
> +//!
> +//! ```ignore
> +//! use hglib::cmdserver::CommandServer;
> +//! let cmdserver = CommandServer::new().expect("failed to start command
> server");
> +//! let statcmd = cmdserver.status();
> +//! ```
> +//!
> +//! and get the results back as native Rust data types. The details of
> +//! this implementation are yet to be determined. For an idea of what's
> +//! in store, see the [Python hglib documentation][python-hglib].
> +//!
> +//! [python-hglib]: https://www.mercurial-scm.org/wiki/PythonHglib
> +
> +use std::io;
> +use connection::*;
> +
> +/// Spawns and communicates with a command server process.
> +pub struct CommandServer {
> +    /// A handle on the spawned process.
> +    pub connection: Connection,
> +    /// The list of capabilities the server reported on startup.
> +    pub capabilities: Vec<String>,
> +    /// The character encoding the server reported on startup.
> +    pub encoding: String,
> +}
> +
> +impl CommandServer {
> +    /// Constructs and starts up a command server instance, or returns an
> error.
> +    pub fn new() -> io::Result<CommandServer> {
> +        let mut conn = try!(Connection::new());
> +        let (capabilities, encoding) = match conn.read_hello() {
> +            Ok((caps, enc)) => (caps, enc),
> +            Err(e)   => panic!("failed to read server hello: {}", e),
> +        };
> +        Ok(CommandServer {
> +            connection: conn,
> +            capabilities: capabilities,
> +            encoding: encoding,
> +        })
> +    }
> +}
> diff --git a/rust/hglib/src/connection.rs b/rust/hglib/src/connection.rs
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/src/connection.rs
> @@ -0,0 +1,263 @@
> +//! Raw command server API.
> +//!
> +//! Creating a [`Connection`](struct.Connection.html) spawns an instance
> +//! of the command server and allows you to interact with it. When it
> +//! starts up, the command server sends a hello message on its output
> +//! channel, which can be read and parsed by
> +//! [`read_hello()`](struct.Connection.html#method.read_hello):
> +//!
> +//! ```rust
> +//! # use std::io;
> +//! # use hglib::connection::Connection;
> +//! # use hglib::Chunk;
> +//! let mut conn = Connection::new().expect("failed to start command
> server");
> +//! let (capabilities, encoding) =
> +//!     conn.read_hello().expect("failed to read server hello");
> +//! ```
> +
> +use std::ascii::AsciiExt;
> +use std::error::Error;
> +use std::fmt::{self, Display};
> +use std::io;
> +use std::io::prelude::*;
> +use std::process::{Command, Stdio, Child, ChildStdout, ExitStatus};
> +use std::str;
> +
> +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
> +
> +#[derive(Debug)]
> +pub struct UnimplementedChannelError(u8);
> +
> +impl Display for UnimplementedChannelError {
> +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
> +        write!(f, "{}", self.0 as char)
> +    }
> +}
> +
> +impl Error for UnimplementedChannelError {
> +    fn description(&self) -> &str {
> +        if (self.0 as char).is_uppercase() {
> +            return "unimplemented required channel";
> +        }
> +        "unimplemented channel"
> +    }
> +}
> +
> +impl From<UnimplementedChannelError> for io::Error {
> +    fn from(err: UnimplementedChannelError) -> io::Error {
> +        io::Error::new(io::ErrorKind::Other, err)
> +    }
> +}
> +
> +#[repr(u8)]
> +enum Channel {
> +    Output = b'o',
> +    Error = b'e',
> +    Result = b'r',
> +    Debug = b'd',
> +    Input = b'I',
> +    LineInput = b'L',
> +}
> +
> +impl Channel {
> +    fn from_u8(byte: u8) -> Result<Channel, UnimplementedChannelError> {
> +        match byte {
> +            b'o' => Ok(Channel::Output),
> +            b'e' => Ok(Channel::Error),
> +            b'r' => Ok(Channel::Result),
> +            b'd' => Ok(Channel::Debug),
> +            b'I' => Ok(Channel::Input),
> +            b'L' => Ok(Channel::LineInput),
> +            _ => Err(UnimplementedChannelError(byte)),
> +        }
> +    }
> +}
> +
> +use Chunk;
> +
> +/// An iterator over the results of a single Mercurial command.
> +///
> +/// Each iteration yields a [`Chunk`](../enum.Chunk.html) with the
> +/// output data or length of input the server is expecting.
> +pub struct CommandRun<'a> {
> +    connection: &'a mut Connection,
> +    done: bool,
> +}
> +
> +impl<'a> Iterator for CommandRun<'a> {
> +    type Item = io::Result<Chunk>;
> +
> +    fn next(&mut self) -> Option<Self::Item> {
> +        if self.done {
> +            return None;
> +        }
> +        let (chan, length) = match self.connection.read_header() {
> +            Ok(t) => t,
> +            Err(e) => return Some(Err(e)),
> +        };
> +        match chan {
> +            Channel::Output => {
> +                Some(self.connection.read_body
> (length).map(Chunk::Output))
> +            },
> +            Channel::Error => {
> +                Some(self.connection.read_body(length).map(Chunk::Error))
> +            },
> +            Channel::Result => {
> +                self.done = true;
> +                Some(self.connection.read_result().map(Chunk::Result))
> +            },
> +            Channel::Debug => {
> +                unimplemented!()
> +            },
> +            Channel::Input => {
> +                Some(Ok(Chunk::Input(length)))
> +            },
> +            Channel::LineInput => {
> +                Some(Ok(Chunk::LineInput(length)))
> +            },
> +        }
> +    }
> +}
> +
> +/// A handle to a running command server instance.
> +pub struct Connection {
> +    child: Child,
> +}
> +
> +impl Connection {
> +    /// Spawns a new command server process.
> +    pub fn new() -> io::Result<Connection> {
> +        let cmdserver = try!(
> +            Command::new("hg")
> +                .args(&["serve", "--cmdserver", "pipe", "--config",
> "ui.interactive=True"])
> +                .stdin(Stdio::piped())
> +                .stdout(Stdio::piped())
> +                .spawn());
> +
> +        Ok(Connection {
> +            child: cmdserver,
> +        })
> +    }
> +
> +    fn child_stdout(&mut self) -> &mut ChildStdout {
> +        // We just unwrap the Option<ChildStdout> because we know that
> +        // we set up the pipe in Connection::new(). We have to call
> +        // .as_mut() because .unwrap()'s signature moves the `self`
> +        // value out.
> +        self.child.stdout.as_mut().unwrap()
> +    }
> +
> +    /// Reads and parses the server hello message. Returns a tuple of
> +    /// ([capabilities], encoding).
> +    ///
> +    /// ## Errors
> +    ///
> +    /// Returns an I/O error if reading or parsing the hello message
> +    /// failed.
> +    pub fn read_hello(&mut self) -> io::Result<(Vec<String>, String)> {
> +        fn fetch_field(line: Option<&[u8]>, field: &[u8]) ->
> io::Result<Vec<u8>>
> +        {
> +            let mut label = field.to_vec();
> +            label.extend(b": ");
> +
> +            match line {
> +                Some(l) if l.is_ascii() && l.starts_with(&label) => {
> +                    Ok(l[label.len()..].to_vec())
> +                },
> +                Some(l) => {
> +                    let err_data = match str::from_utf8(l) {
> +                        Ok(s)  => s.to_string(),
> +                        Err(e) => format!("{:?} (bad encoding: {})", l,
> e),
> +                    };
> +                    return Err(io::Error::new(
> +                        io::ErrorKind::InvalidData,
> +                        format!("expected '{}: ', got {:?}",
> +                                String::from_utf8_lossy(field),
> err_data)));
> +                },
> +                None => return Err(io::Error::new(
> +                    // TODO: When read_exact stabilizes we can use
> UnexpectedEOF
> +                    io::ErrorKind::Other,
> +                    format!("missing field '{}' in server hello",
> +                            String::from_utf8_lossy(field)))),
> +            }
> +        }
> +
> +        fn parse_capabilities(cap_line: Vec<u8>) -> Vec<String> {
> +            let mut caps = vec![];
> +            for cap in cap_line.split(|byte| *byte == b' ') {
> +                let cap = str::from_utf8(cap)
> +                    .expect("failed to decode ASCII as UTF-8?!");
> +                caps.push(cap.to_string());
> +            }
> +            caps
> +        }
> +
> +        let (_, length) = try!(self.read_header());
> +        let hello = try!(self.read_body(length));
> +        let mut hello = hello.split(|byte| *byte == b'\n');
> +
> +        let caps = try!(fetch_field(hello.next(), b"capabilities")
> +                        .map(|l| parse_capabilities(l)));
> +        let enc = try!(fetch_field(hello.next(), b"encoding")
> +                       .map(|l| String::from_utf8(l)
> +                            .expect("failed to decode ASCII as
> UTF-8?!")));
> +        Ok((caps, enc))
> +    }
> +
> +    fn read_header(&mut self) -> io::Result<(Channel, i32)> {
> +        let pout = self.child_stdout();
> +        let chan = try!(pout.read_u8());
> +        let chan = try!(Channel::from_u8(chan));
> +        let length = try!(pout.read_i32::<BigEndian>());
> +        Ok((chan, length))
> +    }
> +
> +    fn read_body(&mut self, length: i32) -> io::Result<Vec<u8>> {
> +        let pout = self.child_stdout();
> +        let mut buf = Vec::with_capacity(length as usize);
> +        try!(pout.take(length as u64).read_to_end(&mut buf));
> +        Ok(buf)
> +    }
> +
> +    fn read_result(&mut self) -> io::Result<i32> {
> +        let pout = self.child_stdout();
> +        let result = try!(pout.read_i32::<BigEndian>());
> +        Ok(result)
> +    }
> +
> +    fn _raw_command(&mut self, command: Vec<&[u8]>) -> io::Result<()> {
> +        // sum of lengths of all arguments, plus null byte separators
> +        let len = command.iter().map(|item| item.len())
> +            .fold(command.len() - 1, |acc, l| acc + l);
> +        if len > i32::max_value() as usize {
> +            return Err(io::Error::new(io::ErrorKind::InvalidInput,
> "message too long"));
> +        }
> +
> +        let pin = self.child.stdin.as_mut().unwrap();
> +        try!(pin.write(b"runcommand\n"));
> +        try!(pin.write_i32::<BigEndian>(len as i32));
> +        try!(pin.write(command[0]));
> +        for arg in &command[1..] {
> +            try!(pin.write(b"\0"));
> +            try!(pin.write(arg));
> +        }
> +        Ok(())
> +    }
> +
> +    /// Sends the given `command` to Mercurial, returning an iterator
> +    /// over the results.
> +    pub fn raw_command(&mut self, command: Vec<&[u8]>) ->
> io::Result<CommandRun> {
> +        try!(self._raw_command(command));
> +        Ok(CommandRun {
> +            connection: self,
> +            done: false,
> +        })
> +    }
> +
> +    /// Shuts down the command server process.
> +    pub fn close(&mut self) -> io::Result<ExitStatus> {
> +        // This will close the command server's stdin, which signals
> +        // that it should exit. Returns the command server's exit code.
> +        self.child.wait()
> +    }
> +}
> diff --git a/rust/hglib/src/lib.rs b/rust/hglib/src/lib.rs
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/src/lib.rs
> @@ -0,0 +1,76 @@
> +//! Client library for the Mercurial command server
> +//!
> +//! This crate provides a client interface to the Mercurial distributed
> +//! version control system (DVCS) in Rust, using Mercurial's [command
> +//! server][]. The command server is designed to allow tools to be built
> +//! around Mercurial repositories, without being tied into Mercurial's
> +//! internal API or licensing.
> +//!
> +//! [command server]: https://www.mercurial-scm.org/wiki/CommandServer
> +//!
> +//! ## High-level API
> +//!
> +//! The [`cmdserver`](cmdserver/index.html) module provides a high-level
> +//! interface which manages spawning and communicating with a command
> +//! server instance:
> +//!
> +//! ```rust
> +//! use hglib::cmdserver::CommandServer;
> +//! let cmdserver = CommandServer::new().expect("failed to start command
> server");
> +//! ```
> +//!
> +//! This high-level interface is largely unimplemented so far, but
> +//! builds on the raw API that is already functional.
> +//!
> +//! ## Raw API
> +//!
> +//! The lower-level API in the [`connection`](connection/index.html)
> +//! module allows you to run commands at the level of the command server
> +//! protocol. Assembling the command and reading the result
> +//! chunk-by-chunk is done manually.
> +//!
> +//! ```rust
> +//! # use std::io;
> +//! # use std::io::prelude::*;
> +//! use hglib::connection::Connection;
> +//! use hglib::Chunk;
> +//! let mut conn = Connection::new().expect("failed to start command
> server");
> +//! let (capabilities, encoding) =
> +//!     conn.read_hello().expect("failed to read server hello");
> +//!
> +//! let cmditer =
> +//!     conn.raw_command(vec![b"log", b"-l", b"5"])
> +//!         .expect("failed to send raw command");
> +//! for chunk in cmditer {
> +//!     match chunk {
> +//!         Ok(Chunk::Output(s)) => { io::stdout().write(&s); },
> +//!         Ok(Chunk::Error(s)) => { io::stdout().write(&s); },
> +//!         Ok(Chunk::Result(r)) => println!("command exited with status:
> {}", r),
> +//!         Ok(_) => {},
> +//!         Err(e) => panic!("failed to read chunk: {}", e),
> +//!     }
> +//! }
> +//! ```
> +
> +extern crate byteorder;
> +
> +/// A type representing a "chunk" of data received from the command
> server.
> +#[derive(Debug)]
> +pub enum Chunk {
> +    /// Data received on the output channel (equivalent to stdout).
> +    Output(Vec<u8>),
> +    /// Data received on the error channel (equivalent to stderr).
> +    Error(Vec<u8>),
> +    /// Data received on the debug channel (log entries).
> +    Debug(Vec<u8>),
> +    /// The exit code of a Mercurial command.
> +    Result(i32),
> +    /// Indicates that the client should send input of the given maximum
> length.
> +    Input(i32),
> +    /// Indicates that the client should send line-oriented input of the
> +    /// given maximum length.
> +    LineInput(i32),
> +}
> +
> +pub mod connection;
> +pub mod cmdserver;
> diff --git a/rust/hglib/tests/cmdserver.rs b/rust/hglib/tests/cmdserver.rs
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/tests/cmdserver.rs
> @@ -0,0 +1,10 @@
> +extern crate hglib;
> +
> +use hglib::cmdserver::CommandServer;
> +
> +#[test]
> +fn capabilities() {
> +    let cmdserver = CommandServer::new().unwrap();
> +    assert!(cmdserver.capabilities.len() > 0);
> +    assert!(cmdserver.capabilities.iter().any(|cap| cap ==
> "runcommand"));
> +}
> diff --git a/rust/hglib/tests/connection.rs b/rust/hglib/tests/connection.
> rs
> new file mode 100644
> --- /dev/null
> +++ b/rust/hglib/tests/connection.rs
> @@ -0,0 +1,88 @@
> +extern crate hglib;
> +extern crate tempdir;
> +#[macro_use] extern crate lazy_static;
> +
> +use hglib::connection::*;
> +use hglib::Chunk;
> +use std::env;
> +use std::path::PathBuf;
> +use std::process::Command;
> +use std::sync::Mutex;
> +use tempdir::TempDir;
> +
> +lazy_static! {
> +    static ref WDLOCK: Mutex<PathBuf> = {
> +        let oldwd = env::current_dir().unwrap();
> +        Mutex::new(oldwd)
> +    };
> +}
> +
> +fn with_temp_repo<F>(f: F) where F : Fn() {
> +    let tmpdir = TempDir::new("hglib")
> +        .expect("failed to create temporary directory for tests");
> +
> +    let lock = (*WDLOCK).lock().unwrap();
> +    env::set_current_dir(tmpdir.path())
> +        .expect("failed to change into temporary directory");
> +
> +    // FIXME: make this work on platforms other than Unix
> +    Command::new("/bin/sh").arg("-c")
> +        .arg("hg init && echo a>a && hg ci -qAm0")
> +        .spawn()
> +        .expect("failed to initialize temporary repository")
> +        .wait().unwrap();
> +
> +    f();
> +
> +    env::set_current_dir(&*lock).unwrap();
> +}
> +
> +#[test]
> +fn raw_command() {
> +    with_temp_repo(|| {
> +        let mut connection = Connection::new().unwrap();
> +        connection.read_hello().unwrap();
> +        let (mut result, mut output) = (-1i32, vec![]);
> +        {
> +            let run = connection.raw_command(vec![b"summary"]).unwrap();
> +            for chunk in run {
> +                match chunk {
> +                    Ok(Chunk::Output(s)) => output.extend(s),
> +                    Ok(Chunk::Error(_)) => continue,
> +                    Ok(Chunk::Result(r)) => result = r,
> +                    Ok(c) => panic!("unexpected chunk: {:?}", c),
> +                    Err(e) => panic!("failed to read command results:
> {}", e),
> +                }
> +            }
> +        }
> +        assert!(output.starts_with(b"parent:"));
> +        assert_eq!(result, 0);
> +
> +        connection.close().unwrap();
> +    });
> +}
> +
> +#[test]
> +fn raw_command_error() {
> +    with_temp_repo(|| {
> +        let mut connection = Connection::new().unwrap();
> +        connection.read_hello().unwrap();
> +        let (mut result, mut error) = (-1i32, vec![]);
> +        {
> +            let run = connection.raw_command(vec![b"noexist"]).unwrap();
> +            for chunk in run {
> +                match chunk {
> +                    Ok(Chunk::Output(_)) => continue,
> +                    Ok(Chunk::Error(s)) => error.extend(s),
> +                    Ok(Chunk::Result(r)) => { result = r },
> +                    Ok(c) => panic!("unexpected chunk: {:?}", c),
> +                    Err(e) => panic!("failed to read command results:
> {}", e),
> +                }
> +            }
> +        }
> +        assert_eq!(result, 255);
> +        assert!(error.starts_with(b"hg: unknown command 'noexist'"));
> +
> +        connection.close().unwrap();
> +    });
> +}
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>
Yuya Nishihara - April 2, 2018, 9:47 a.m.
On Sun, 1 Apr 2018 11:53:47 -0700, Gregory Szorc wrote:
> Also, Kevin Kox has been giving good Rust reviews. But I believe they are
> only active in Phabricator. Consider submitting futures Rust patches to
> Phabricator or CC'ing Kevin explicitly.

Ok, thanks. *a big sigh*
Gregory Szorc - April 3, 2018, 1:06 a.m.
On Mon, Apr 2, 2018 at 5:53 PM, Kevin Bullock <
kbullock+mercurial@ringworld.org> wrote:

> > On Apr 1, 2018, at 13:53, Gregory Szorc <gregory.szorc@gmail.com> wrote:
> >
> > On Sun, Apr 1, 2018 at 4:14 AM, Yuya Nishihara <yuya@tcha.org> wrote:
> > # HG changeset patch
> > # User Kevin Bullock <kbullock@ringworld.org>
> > # Date 1522477348 -32400
> > #      Sat Mar 31 15:22:28 2018 +0900
> > # Node ID 9e25c96124d51e11022b0ce64783f5f333ede7fb
> > # Parent  2ed180117f7658d0cbf6a1ece20944465c55c947
> > rust-hglib: import the latest version and update URLs
> >
> > Queued parts 1-3 because vendoring rust-hglib has been talked about and
> agreed upon IIRC.
>
> Hmm, it has? Have we talked about vendoring other hglibs (c-hglib,
> python-hglib)?
>
> I recall talking about consolidating them onto m-s.o. If we decide to
> vendor them into the hg repo I think we should talk more about source
> layout first, and start w/python-hglib.
>
> That being said, I'm excited to see interest around rust-hglib!
>

I could be mistaken.

Obviously since I queued this, I have no problems taking the hglib clients
into the main repo. I'm also fine putting rust-hglib on another repo on
m-s.o/repo.

We know we want to rewrite chg in Rust. That will presumably use
rust-hglib. I think it is easier to have the library vendored. But with
Cargo, it doesn't matter too much: it will fetch dependencies easily
enough. That being said, we've punted on vendoring Rust dependencies for
the moment because Rust support is so young and we're not shipping anything
relying on Rust. But I reckon we will eventually vendor all Rust
dependencies so builds don't have to go to the Internet and can therefore
be reliable and deterministic over time. That means we'll vendor rust-hglib
into the main repo at some point. The question is whether that vendored
copy will be the canonical copy or a read-only vendored version. Since
these client libraries are maintained by the official project, I think it
makes sense to put them in the main repo.

If we proceed with vendoring the client libraries, I don't think we need to
start with python-hglib. Unlike rust-hglib, I don't foresee a use of
python-hglib in the main project. So there's less justification for
vendoring python-hglib than rust-hglib. We should be talking about
rust-hglib and ignore python-hglib (unless we want to put them all in the
same place in the repo - but that's a bikeshed and we can always move files
around later if we need to).
Yuya Nishihara - April 3, 2018, 12:04 p.m.
On Mon, 2 Apr 2018 18:06:56 -0700, Gregory Szorc wrote:
> On Mon, Apr 2, 2018 at 5:53 PM, Kevin Bullock <
> kbullock+mercurial@ringworld.org> wrote:
> > > On Apr 1, 2018, at 13:53, Gregory Szorc <gregory.szorc@gmail.com> wrote:
> > > On Sun, Apr 1, 2018 at 4:14 AM, Yuya Nishihara <yuya@tcha.org> wrote:
> > > # HG changeset patch
> > > # User Kevin Bullock <kbullock@ringworld.org>
> > > # Date 1522477348 -32400
> > > #      Sat Mar 31 15:22:28 2018 +0900
> > > # Node ID 9e25c96124d51e11022b0ce64783f5f333ede7fb
> > > # Parent  2ed180117f7658d0cbf6a1ece20944465c55c947
> > > rust-hglib: import the latest version and update URLs
> > >
> > > Queued parts 1-3 because vendoring rust-hglib has been talked about and
> > agreed upon IIRC.
> >
> > Hmm, it has? Have we talked about vendoring other hglibs (c-hglib,
> > python-hglib)?
> >
> > I recall talking about consolidating them onto m-s.o. If we decide to
> > vendor them into the hg repo I think we should talk more about source
> > layout first, and start w/python-hglib.
> >
> > That being said, I'm excited to see interest around rust-hglib!
> >
> 
> I could be mistaken.
> 
> Obviously since I queued this, I have no problems taking the hglib clients
> into the main repo. I'm also fine putting rust-hglib on another repo on
> m-s.o/repo.

Dropped the patches from hg-committed for now.

Only reason I vendored rust-hglib was otherwise it would get complicated
to rewrite chg in Rust. But we can instead put chg into rust-hglib repository
(or a temporary fork of it.) That's probably less controversial and easier
to throw them away if the porting work turns out to be impractical.

Patch

diff --git a/rust/hglib/Cargo.toml b/rust/hglib/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/rust/hglib/Cargo.toml
@@ -0,0 +1,25 @@ 
+[package]
+name = "hglib"
+version = "0.1.1"
+authors = ["Kevin Bullock <kbullock@ringworld.org>"]
+description = "Mercurial command server client library."
+readme = "README.md"
+documentation = "http://kbullock.ringworld.org/rustdoc/hglib/"
+repository = "http://kbullock.ringworld.org/hg/rust-hglib/"
+license = "MIT"
+
+include = [
+    "LICENSE",
+    "README.md",
+    "RELEASES.md",
+    "src/**/*.rs",
+    "Cargo.toml",
+    "examples/**/*.rs",
+]
+
+[dependencies]
+byteorder = "0.3.13"
+
+[dev-dependencies]
+tempdir = "0.3.4"
+lazy_static = "0.1.14"
diff --git a/rust/hglib/LICENSE b/rust/hglib/LICENSE
new file mode 100644
--- /dev/null
+++ b/rust/hglib/LICENSE
@@ -0,0 +1,20 @@ 
+Copyright (c) 2015 Kevin R. Bullock
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/rust/hglib/README.md b/rust/hglib/README.md
new file mode 100644
--- /dev/null
+++ b/rust/hglib/README.md
@@ -0,0 +1,22 @@ 
+HgLib: Rust Client Library for Mercurial Command Server
+=======================================================
+
+This crate provides a client interface to the Mercurial distributed
+version control system (DVCS) in Rust, using Mercurial's
+[command server][]. The command server is designed to allow tools to be
+built around Mercurial repositories, without being tied into Mercurial's
+internal API or licensing.
+
+[command server]: https://www.mercurial-scm.org/wiki/CommandServer
+
+[![](http://meritbadge.herokuapp.com/hglib)](https://crates.io/crates/hglib)
+
+API documentation: <http://kbullock.ringworld.org/rustdoc/hglib/>
+
+Installation
+------------
+
+To use hglib, add the crate to your Cargo.toml:
+
+    [dependencies]
+    hglib = "0.1.1"
diff --git a/rust/hglib/RELEASES.md b/rust/hglib/RELEASES.md
new file mode 100644
--- /dev/null
+++ b/rust/hglib/RELEASES.md
@@ -0,0 +1,9 @@ 
+Version 0.1.1 (2015-09-23)
+==========================
+
+* Now builds on Rust 1.3 (stable).
+
+Version 0.1.0 (2015-09-21)
+==========================
+
+* Initial release.
diff --git a/rust/hglib/examples/client.rs b/rust/hglib/examples/client.rs
new file mode 100644
--- /dev/null
+++ b/rust/hglib/examples/client.rs
@@ -0,0 +1,48 @@ 
+extern crate hglib;
+extern crate tempdir;
+
+use hglib::cmdserver::CommandServer;
+use hglib::Chunk;
+use std::env;
+use tempdir::TempDir;
+
+fn run_command(cmdserver: &mut CommandServer, command: Vec<&[u8]>) -> (i32, Vec<u8>, Vec<u8>) {
+    let (mut result, mut output, mut error) =
+        (-1i32, vec![], vec![]);
+    let run = cmdserver.connection
+        .raw_command(command)
+        .expect("failed to send 'log' command");
+
+    for chunk in run {
+        match chunk {
+            Ok(Chunk::Output(s)) => output.extend(s),
+            Ok(Chunk::Error(s)) => error.extend(s),
+            Ok(Chunk::Result(r)) => { result = r },
+            Ok(_) => unimplemented!(),
+            Err(e) => panic!("failed to read command results: {}", e),
+        }
+    }
+
+    (result, output, error)
+}
+
+fn main() {
+    let tmpdir = TempDir::new("hglib").unwrap();
+    env::set_current_dir(tmpdir.path()).unwrap();
+
+    let mut cmdserver = CommandServer::new().expect("failed to start command server");
+    println!("capabilities: {:?}", cmdserver.capabilities);
+    println!("encoding: {:?}", cmdserver.encoding);
+
+    let _ = run_command(&mut cmdserver, vec![b"init"]);
+
+    let (result, output, error) =
+        run_command(&mut cmdserver, vec![b"log", b"-l", b"5"]);
+    cmdserver.connection.close().expect("command server did not stop cleanly");
+
+    println!("output: {}",
+             String::from_utf8(output).unwrap().trim_right_matches('\n'));
+    println!("error: {}",
+             String::from_utf8(error).unwrap().trim_right_matches('\n'));
+    println!("result: {:?}", result);
+}
diff --git a/rust/hglib/src/cmdserver.rs b/rust/hglib/src/cmdserver.rs
new file mode 100644
--- /dev/null
+++ b/rust/hglib/src/cmdserver.rs
@@ -0,0 +1,46 @@ 
+//! High-level interface to the Mercurial command server.
+//!
+//! This module is currently only a skeletal implementation. In the
+//! future this will allow you to run Mercurial commands by calling
+//! methods of the same name:
+//!
+//! ```ignore
+//! use hglib::cmdserver::CommandServer;
+//! let cmdserver = CommandServer::new().expect("failed to start command server");
+//! let statcmd = cmdserver.status();
+//! ```
+//!
+//! and get the results back as native Rust data types. The details of
+//! this implementation are yet to be determined. For an idea of what's
+//! in store, see the [Python hglib documentation][python-hglib].
+//!
+//! [python-hglib]: https://www.mercurial-scm.org/wiki/PythonHglib
+
+use std::io;
+use connection::*;
+
+/// Spawns and communicates with a command server process.
+pub struct CommandServer {
+    /// A handle on the spawned process.
+    pub connection: Connection,
+    /// The list of capabilities the server reported on startup.
+    pub capabilities: Vec<String>,
+    /// The character encoding the server reported on startup.
+    pub encoding: String,
+}
+
+impl CommandServer {
+    /// Constructs and starts up a command server instance, or returns an error.
+    pub fn new() -> io::Result<CommandServer> {
+        let mut conn = try!(Connection::new());
+        let (capabilities, encoding) = match conn.read_hello() {
+            Ok((caps, enc)) => (caps, enc),
+            Err(e)   => panic!("failed to read server hello: {}", e),
+        };
+        Ok(CommandServer {
+            connection: conn,
+            capabilities: capabilities,
+            encoding: encoding,
+        })
+    }
+}
diff --git a/rust/hglib/src/connection.rs b/rust/hglib/src/connection.rs
new file mode 100644
--- /dev/null
+++ b/rust/hglib/src/connection.rs
@@ -0,0 +1,263 @@ 
+//! Raw command server API.
+//!
+//! Creating a [`Connection`](struct.Connection.html) spawns an instance
+//! of the command server and allows you to interact with it. When it
+//! starts up, the command server sends a hello message on its output
+//! channel, which can be read and parsed by
+//! [`read_hello()`](struct.Connection.html#method.read_hello):
+//!
+//! ```rust
+//! # use std::io;
+//! # use hglib::connection::Connection;
+//! # use hglib::Chunk;
+//! let mut conn = Connection::new().expect("failed to start command server");
+//! let (capabilities, encoding) =
+//!     conn.read_hello().expect("failed to read server hello");
+//! ```
+
+use std::ascii::AsciiExt;
+use std::error::Error;
+use std::fmt::{self, Display};
+use std::io;
+use std::io::prelude::*;
+use std::process::{Command, Stdio, Child, ChildStdout, ExitStatus};
+use std::str;
+
+use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
+
+#[derive(Debug)]
+pub struct UnimplementedChannelError(u8);
+
+impl Display for UnimplementedChannelError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.0 as char)
+    }
+}
+
+impl Error for UnimplementedChannelError {
+    fn description(&self) -> &str {
+        if (self.0 as char).is_uppercase() {
+            return "unimplemented required channel";
+        }
+        "unimplemented channel"
+    }
+}
+
+impl From<UnimplementedChannelError> for io::Error {
+    fn from(err: UnimplementedChannelError) -> io::Error {
+        io::Error::new(io::ErrorKind::Other, err)
+    }
+}
+
+#[repr(u8)]
+enum Channel {
+    Output = b'o',
+    Error = b'e',
+    Result = b'r',
+    Debug = b'd',
+    Input = b'I',
+    LineInput = b'L',
+}
+
+impl Channel {
+    fn from_u8(byte: u8) -> Result<Channel, UnimplementedChannelError> {
+        match byte {
+            b'o' => Ok(Channel::Output),
+            b'e' => Ok(Channel::Error),
+            b'r' => Ok(Channel::Result),
+            b'd' => Ok(Channel::Debug),
+            b'I' => Ok(Channel::Input),
+            b'L' => Ok(Channel::LineInput),
+            _ => Err(UnimplementedChannelError(byte)),
+        }
+    }
+}
+
+use Chunk;
+
+/// An iterator over the results of a single Mercurial command.
+///
+/// Each iteration yields a [`Chunk`](../enum.Chunk.html) with the
+/// output data or length of input the server is expecting.
+pub struct CommandRun<'a> {
+    connection: &'a mut Connection,
+    done: bool,
+}
+
+impl<'a> Iterator for CommandRun<'a> {
+    type Item = io::Result<Chunk>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.done {
+            return None;
+        }
+        let (chan, length) = match self.connection.read_header() {
+            Ok(t) => t,
+            Err(e) => return Some(Err(e)),
+        };
+        match chan {
+            Channel::Output => {
+                Some(self.connection.read_body(length).map(Chunk::Output))
+            },
+            Channel::Error => {
+                Some(self.connection.read_body(length).map(Chunk::Error))
+            },
+            Channel::Result => {
+                self.done = true;
+                Some(self.connection.read_result().map(Chunk::Result))
+            },
+            Channel::Debug => {
+                unimplemented!()
+            },
+            Channel::Input => {
+                Some(Ok(Chunk::Input(length)))
+            },
+            Channel::LineInput => {
+                Some(Ok(Chunk::LineInput(length)))
+            },
+        }
+    }
+}
+
+/// A handle to a running command server instance.
+pub struct Connection {
+    child: Child,
+}
+
+impl Connection {
+    /// Spawns a new command server process.
+    pub fn new() -> io::Result<Connection> {
+        let cmdserver = try!(
+            Command::new("hg")
+                .args(&["serve", "--cmdserver", "pipe", "--config", "ui.interactive=True"])
+                .stdin(Stdio::piped())
+                .stdout(Stdio::piped())
+                .spawn());
+
+        Ok(Connection {
+            child: cmdserver,
+        })
+    }
+
+    fn child_stdout(&mut self) -> &mut ChildStdout {
+        // We just unwrap the Option<ChildStdout> because we know that
+        // we set up the pipe in Connection::new(). We have to call
+        // .as_mut() because .unwrap()'s signature moves the `self`
+        // value out.
+        self.child.stdout.as_mut().unwrap()
+    }
+
+    /// Reads and parses the server hello message. Returns a tuple of
+    /// ([capabilities], encoding).
+    ///
+    /// ## Errors
+    ///
+    /// Returns an I/O error if reading or parsing the hello message
+    /// failed.
+    pub fn read_hello(&mut self) -> io::Result<(Vec<String>, String)> {
+        fn fetch_field(line: Option<&[u8]>, field: &[u8]) -> io::Result<Vec<u8>>
+        {
+            let mut label = field.to_vec();
+            label.extend(b": ");
+
+            match line {
+                Some(l) if l.is_ascii() && l.starts_with(&label) => {
+                    Ok(l[label.len()..].to_vec())
+                },
+                Some(l) => {
+                    let err_data = match str::from_utf8(l) {
+                        Ok(s)  => s.to_string(),
+                        Err(e) => format!("{:?} (bad encoding: {})", l, e),
+                    };
+                    return Err(io::Error::new(
+                        io::ErrorKind::InvalidData,
+                        format!("expected '{}: ', got {:?}",
+                                String::from_utf8_lossy(field), err_data)));
+                },
+                None => return Err(io::Error::new(
+                    // TODO: When read_exact stabilizes we can use UnexpectedEOF
+                    io::ErrorKind::Other,
+                    format!("missing field '{}' in server hello",
+                            String::from_utf8_lossy(field)))),
+            }
+        }
+
+        fn parse_capabilities(cap_line: Vec<u8>) -> Vec<String> {
+            let mut caps = vec![];
+            for cap in cap_line.split(|byte| *byte == b' ') {
+                let cap = str::from_utf8(cap)
+                    .expect("failed to decode ASCII as UTF-8?!");
+                caps.push(cap.to_string());
+            }
+            caps
+        }
+
+        let (_, length) = try!(self.read_header());
+        let hello = try!(self.read_body(length));
+        let mut hello = hello.split(|byte| *byte == b'\n');
+
+        let caps = try!(fetch_field(hello.next(), b"capabilities")
+                        .map(|l| parse_capabilities(l)));
+        let enc = try!(fetch_field(hello.next(), b"encoding")
+                       .map(|l| String::from_utf8(l)
+                            .expect("failed to decode ASCII as UTF-8?!")));
+        Ok((caps, enc))
+    }
+
+    fn read_header(&mut self) -> io::Result<(Channel, i32)> {
+        let pout = self.child_stdout();
+        let chan = try!(pout.read_u8());
+        let chan = try!(Channel::from_u8(chan));
+        let length = try!(pout.read_i32::<BigEndian>());
+        Ok((chan, length))
+    }
+
+    fn read_body(&mut self, length: i32) -> io::Result<Vec<u8>> {
+        let pout = self.child_stdout();
+        let mut buf = Vec::with_capacity(length as usize);
+        try!(pout.take(length as u64).read_to_end(&mut buf));
+        Ok(buf)
+    }
+
+    fn read_result(&mut self) -> io::Result<i32> {
+        let pout = self.child_stdout();
+        let result = try!(pout.read_i32::<BigEndian>());
+        Ok(result)
+    }
+
+    fn _raw_command(&mut self, command: Vec<&[u8]>) -> io::Result<()> {
+        // sum of lengths of all arguments, plus null byte separators
+        let len = command.iter().map(|item| item.len())
+            .fold(command.len() - 1, |acc, l| acc + l);
+        if len > i32::max_value() as usize {
+            return Err(io::Error::new(io::ErrorKind::InvalidInput, "message too long"));
+        }
+
+        let pin = self.child.stdin.as_mut().unwrap();
+        try!(pin.write(b"runcommand\n"));
+        try!(pin.write_i32::<BigEndian>(len as i32));
+        try!(pin.write(command[0]));
+        for arg in &command[1..] {
+            try!(pin.write(b"\0"));
+            try!(pin.write(arg));
+        }
+        Ok(())
+    }
+
+    /// Sends the given `command` to Mercurial, returning an iterator
+    /// over the results.
+    pub fn raw_command(&mut self, command: Vec<&[u8]>) -> io::Result<CommandRun> {
+        try!(self._raw_command(command));
+        Ok(CommandRun {
+            connection: self,
+            done: false,
+        })
+    }
+
+    /// Shuts down the command server process.
+    pub fn close(&mut self) -> io::Result<ExitStatus> {
+        // This will close the command server's stdin, which signals
+        // that it should exit. Returns the command server's exit code.
+        self.child.wait()
+    }
+}
diff --git a/rust/hglib/src/lib.rs b/rust/hglib/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/rust/hglib/src/lib.rs
@@ -0,0 +1,76 @@ 
+//! Client library for the Mercurial command server
+//!
+//! This crate provides a client interface to the Mercurial distributed
+//! version control system (DVCS) in Rust, using Mercurial's [command
+//! server][]. The command server is designed to allow tools to be built
+//! around Mercurial repositories, without being tied into Mercurial's
+//! internal API or licensing.
+//!
+//! [command server]: https://www.mercurial-scm.org/wiki/CommandServer
+//!
+//! ## High-level API
+//!
+//! The [`cmdserver`](cmdserver/index.html) module provides a high-level
+//! interface which manages spawning and communicating with a command
+//! server instance:
+//!
+//! ```rust
+//! use hglib::cmdserver::CommandServer;
+//! let cmdserver = CommandServer::new().expect("failed to start command server");
+//! ```
+//!
+//! This high-level interface is largely unimplemented so far, but
+//! builds on the raw API that is already functional.
+//!
+//! ## Raw API
+//!
+//! The lower-level API in the [`connection`](connection/index.html)
+//! module allows you to run commands at the level of the command server
+//! protocol. Assembling the command and reading the result
+//! chunk-by-chunk is done manually.
+//!
+//! ```rust
+//! # use std::io;
+//! # use std::io::prelude::*;
+//! use hglib::connection::Connection;
+//! use hglib::Chunk;
+//! let mut conn = Connection::new().expect("failed to start command server");
+//! let (capabilities, encoding) =
+//!     conn.read_hello().expect("failed to read server hello");
+//!
+//! let cmditer =
+//!     conn.raw_command(vec![b"log", b"-l", b"5"])
+//!         .expect("failed to send raw command");
+//! for chunk in cmditer {
+//!     match chunk {
+//!         Ok(Chunk::Output(s)) => { io::stdout().write(&s); },
+//!         Ok(Chunk::Error(s)) => { io::stdout().write(&s); },
+//!         Ok(Chunk::Result(r)) => println!("command exited with status: {}", r),
+//!         Ok(_) => {},
+//!         Err(e) => panic!("failed to read chunk: {}", e),
+//!     }
+//! }
+//! ```
+
+extern crate byteorder;
+
+/// A type representing a "chunk" of data received from the command server.
+#[derive(Debug)]
+pub enum Chunk {
+    /// Data received on the output channel (equivalent to stdout).
+    Output(Vec<u8>),
+    /// Data received on the error channel (equivalent to stderr).
+    Error(Vec<u8>),
+    /// Data received on the debug channel (log entries).
+    Debug(Vec<u8>),
+    /// The exit code of a Mercurial command.
+    Result(i32),
+    /// Indicates that the client should send input of the given maximum length.
+    Input(i32),
+    /// Indicates that the client should send line-oriented input of the
+    /// given maximum length.
+    LineInput(i32),
+}
+
+pub mod connection;
+pub mod cmdserver;
diff --git a/rust/hglib/tests/cmdserver.rs b/rust/hglib/tests/cmdserver.rs
new file mode 100644
--- /dev/null
+++ b/rust/hglib/tests/cmdserver.rs
@@ -0,0 +1,10 @@ 
+extern crate hglib;
+
+use hglib::cmdserver::CommandServer;
+
+#[test]
+fn capabilities() {
+    let cmdserver = CommandServer::new().unwrap();
+    assert!(cmdserver.capabilities.len() > 0);
+    assert!(cmdserver.capabilities.iter().any(|cap| cap == "runcommand"));
+}
diff --git a/rust/hglib/tests/connection.rs b/rust/hglib/tests/connection.rs
new file mode 100644
--- /dev/null
+++ b/rust/hglib/tests/connection.rs
@@ -0,0 +1,88 @@ 
+extern crate hglib;
+extern crate tempdir;
+#[macro_use] extern crate lazy_static;
+
+use hglib::connection::*;
+use hglib::Chunk;
+use std::env;
+use std::path::PathBuf;
+use std::process::Command;
+use std::sync::Mutex;
+use tempdir::TempDir;
+
+lazy_static! {
+    static ref WDLOCK: Mutex<PathBuf> = {
+        let oldwd = env::current_dir().unwrap();
+        Mutex::new(oldwd)
+    };
+}
+
+fn with_temp_repo<F>(f: F) where F : Fn() {
+    let tmpdir = TempDir::new("hglib")
+        .expect("failed to create temporary directory for tests");
+
+    let lock = (*WDLOCK).lock().unwrap();
+    env::set_current_dir(tmpdir.path())
+        .expect("failed to change into temporary directory");
+
+    // FIXME: make this work on platforms other than Unix
+    Command::new("/bin/sh").arg("-c")
+        .arg("hg init && echo a>a && hg ci -qAm0")
+        .spawn()
+        .expect("failed to initialize temporary repository")
+        .wait().unwrap();
+
+    f();
+
+    env::set_current_dir(&*lock).unwrap();
+}
+
+#[test]
+fn raw_command() {
+    with_temp_repo(|| {
+        let mut connection = Connection::new().unwrap();
+        connection.read_hello().unwrap();
+        let (mut result, mut output) = (-1i32, vec![]);
+        {
+            let run = connection.raw_command(vec![b"summary"]).unwrap();
+            for chunk in run {
+                match chunk {
+                    Ok(Chunk::Output(s)) => output.extend(s),
+                    Ok(Chunk::Error(_)) => continue,
+                    Ok(Chunk::Result(r)) => result = r,
+                    Ok(c) => panic!("unexpected chunk: {:?}", c),
+                    Err(e) => panic!("failed to read command results: {}", e),
+                }
+            }
+        }
+        assert!(output.starts_with(b"parent:"));
+        assert_eq!(result, 0);
+
+        connection.close().unwrap();
+    });
+}
+
+#[test]
+fn raw_command_error() {
+    with_temp_repo(|| {
+        let mut connection = Connection::new().unwrap();
+        connection.read_hello().unwrap();
+        let (mut result, mut error) = (-1i32, vec![]);
+        {
+            let run = connection.raw_command(vec![b"noexist"]).unwrap();
+            for chunk in run {
+                match chunk {
+                    Ok(Chunk::Output(_)) => continue,
+                    Ok(Chunk::Error(s)) => error.extend(s),
+                    Ok(Chunk::Result(r)) => { result = r },
+                    Ok(c) => panic!("unexpected chunk: {:?}", c),
+                    Err(e) => panic!("failed to read command results: {}", e),
+                }
+            }
+        }
+        assert_eq!(result, 255);
+        assert!(error.starts_with(b"hg: unknown command 'noexist'"));
+
+        connection.close().unwrap();
+    });
+}