2017-07-28 04:00:19 +03:00
|
|
|
// Copyright (c) 2004-present, Facebook, Inc.
|
|
|
|
// All Rights Reserved.
|
|
|
|
//
|
|
|
|
// This software may be used and distributed according to the terms of the
|
|
|
|
// GNU General Public License version 2 or any later version.
|
|
|
|
|
|
|
|
#![deny(warnings)]
|
|
|
|
|
|
|
|
extern crate ascii;
|
|
|
|
#[macro_use]
|
|
|
|
#[cfg(test)]
|
|
|
|
extern crate assert_matches;
|
|
|
|
#[macro_use]
|
2017-12-06 04:52:31 +03:00
|
|
|
extern crate failure_ext as failure;
|
2017-07-28 04:00:19 +03:00
|
|
|
extern crate futures;
|
2017-10-06 19:23:52 +03:00
|
|
|
extern crate futures_ext;
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
extern crate bookmarks;
|
|
|
|
extern crate mercurial_types;
|
2017-11-27 18:56:51 +03:00
|
|
|
#[cfg(test)]
|
|
|
|
extern crate mercurial_types_mocks;
|
2017-10-10 17:25:38 +03:00
|
|
|
extern crate storage_types;
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::fs;
|
|
|
|
use std::io::{self, BufRead, BufReader, Read};
|
2017-10-10 17:25:36 +03:00
|
|
|
use std::path::PathBuf;
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
use ascii::AsciiStr;
|
2017-12-06 04:52:31 +03:00
|
|
|
use failure::{Error, Result, ResultExt};
|
2017-11-27 18:56:51 +03:00
|
|
|
use futures::future;
|
2017-10-06 19:23:52 +03:00
|
|
|
use futures::stream::{self, Stream};
|
2017-11-27 18:56:51 +03:00
|
|
|
use futures_ext::{BoxFuture, BoxStream, StreamExt};
|
2017-07-28 04:00:19 +03:00
|
|
|
|
2017-10-10 17:25:38 +03:00
|
|
|
use bookmarks::Bookmarks;
|
2018-03-16 03:23:19 +03:00
|
|
|
use mercurial_types::nodehash::HgChangesetId;
|
2017-10-10 17:25:38 +03:00
|
|
|
use storage_types::Version;
|
2017-07-28 04:00:19 +03:00
|
|
|
|
2017-12-06 04:52:31 +03:00
|
|
|
#[derive(Debug, Fail)]
|
|
|
|
pub enum ErrorKind {
|
|
|
|
#[fail(display = "invalid bookmarks line: {}", _0)] InvalidBookmarkLine(String),
|
|
|
|
#[fail(display = "invalid hash: {}", _0)] InvalidHash(String),
|
|
|
|
}
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
/// Implementation of bookmarks as they exist in stock Mercurial inside `.hg/bookmarks`.
|
|
|
|
/// The file has a list of entries:
|
|
|
|
///
|
|
|
|
/// ```
|
|
|
|
/// <hash1> <bookmark1-name>
|
|
|
|
/// <hash2> <bookmark2-name>
|
|
|
|
/// ...
|
|
|
|
/// ```
|
|
|
|
///
|
2018-03-16 03:23:19 +03:00
|
|
|
/// Bookmark names are arbitrary bytestrings, and hashes are always HgChangesetIds.
|
2017-07-28 04:00:19 +03:00
|
|
|
///
|
|
|
|
/// This implementation is read-only -- implementing write support would require interacting with
|
|
|
|
/// the locking mechanism Mercurial uses, and generally seems like it wouldn't be very useful.
|
2017-11-27 18:56:51 +03:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct StockBookmarks {
|
2018-03-16 03:23:19 +03:00
|
|
|
bookmarks: HashMap<Vec<u8>, HgChangesetId>,
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
|
2017-11-27 18:56:51 +03:00
|
|
|
impl StockBookmarks {
|
2017-07-28 04:00:19 +03:00
|
|
|
pub fn read<P: Into<PathBuf>>(base: P) -> Result<Self> {
|
|
|
|
let base = base.into();
|
|
|
|
|
|
|
|
let file = fs::File::open(base.join("bookmarks"));
|
|
|
|
match file {
|
|
|
|
Ok(file) => Self::from_reader(file),
|
|
|
|
Err(ref err) if err.kind() == io::ErrorKind::NotFound => {
|
|
|
|
// The .hg/bookmarks file is not guaranteed to exist. Treat it is empty if it
|
|
|
|
// doesn't.
|
2017-08-03 03:32:17 +03:00
|
|
|
Ok(StockBookmarks {
|
|
|
|
bookmarks: HashMap::new(),
|
|
|
|
})
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
Err(err) => Err(err.into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn from_reader<R: Read>(reader: R) -> Result<Self> {
|
|
|
|
let mut bookmarks = HashMap::new();
|
|
|
|
|
|
|
|
// Bookmark names might not be valid UTF-8, so use split() instead of lines().
|
|
|
|
for line in BufReader::new(reader).split(b'\n') {
|
|
|
|
let line = line?;
|
|
|
|
// <hash><space><bookmark name>, where hash is 40 bytes, the space is 1 byte
|
|
|
|
// and the bookmark name is at least 1 byte.
|
|
|
|
if line.len() < 42 || line[40] != b' ' {
|
2017-12-08 00:53:05 +03:00
|
|
|
bail_err!(ErrorKind::InvalidBookmarkLine(
|
|
|
|
String::from_utf8_lossy(line.as_ref()).into_owned(),
|
|
|
|
));
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
let bmname = &line[41..];
|
|
|
|
let hash_slice = &line[..40];
|
2017-12-06 04:52:31 +03:00
|
|
|
let hash = AsciiStr::from_ascii(&hash_slice).context(ErrorKind::InvalidHash(
|
|
|
|
String::from_utf8_lossy(hash_slice).into_owned(),
|
|
|
|
))?;
|
2017-07-28 04:00:19 +03:00
|
|
|
bookmarks.insert(
|
|
|
|
bmname.into(),
|
2018-03-16 03:23:19 +03:00
|
|
|
HgChangesetId::from_ascii_str(hash).context(ErrorKind::InvalidHash(
|
2017-12-06 04:52:31 +03:00
|
|
|
String::from_utf8_lossy(hash_slice).into_owned(),
|
|
|
|
))?,
|
2017-07-28 04:00:19 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-11-27 18:56:51 +03:00
|
|
|
Ok(StockBookmarks { bookmarks })
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-27 18:56:51 +03:00
|
|
|
impl Bookmarks for StockBookmarks {
|
2018-03-16 03:23:19 +03:00
|
|
|
fn get(&self, name: &AsRef<[u8]>) -> BoxFuture<Option<(HgChangesetId, Version)>, Error> {
|
2017-07-28 04:00:19 +03:00
|
|
|
let value = match self.bookmarks.get(name.as_ref()) {
|
|
|
|
Some(hash) => Some((*hash, Version::from(1))),
|
|
|
|
None => None,
|
|
|
|
};
|
2017-11-27 18:56:51 +03:00
|
|
|
Box::new(future::result(Ok(value)))
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
|
2017-12-06 04:52:31 +03:00
|
|
|
fn keys(&self) -> BoxStream<Vec<u8>, Error> {
|
2017-07-28 04:00:19 +03:00
|
|
|
// collect forces evaluation early, so that the stream can safely outlive self
|
2017-10-06 19:23:52 +03:00
|
|
|
stream::iter_ok(
|
2017-07-28 04:00:19 +03:00
|
|
|
self.bookmarks
|
|
|
|
.keys()
|
|
|
|
.map(|k| Ok(k.to_vec()))
|
|
|
|
.collect::<Vec<_>>(),
|
2017-10-06 19:23:52 +03:00
|
|
|
).and_then(|x| x)
|
|
|
|
.boxify()
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
2017-12-06 04:52:31 +03:00
|
|
|
use failure::Context;
|
2017-07-28 04:00:19 +03:00
|
|
|
use futures::Future;
|
2017-11-27 18:56:51 +03:00
|
|
|
use mercurial_types_mocks::nodehash;
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
fn assert_bookmark_get(
|
|
|
|
bookmarks: &StockBookmarks,
|
|
|
|
key: &AsRef<[u8]>,
|
2018-03-16 03:23:19 +03:00
|
|
|
expected: Option<HgChangesetId>,
|
2017-07-28 04:00:19 +03:00
|
|
|
) {
|
|
|
|
let expected = match expected {
|
|
|
|
Some(hash) => Some((hash, Version::from(1))),
|
|
|
|
None => None,
|
|
|
|
};
|
|
|
|
assert_eq!(bookmarks.get(key).wait().unwrap(), expected);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse() {
|
|
|
|
let disk_bookmarks = b"\
|
|
|
|
1111111111111111111111111111111111111111 abc\n\
|
|
|
|
2222222222222222222222222222222222222222 def\n\
|
|
|
|
1111111111111111111111111111111111111111 test123\n";
|
|
|
|
let reader = Cursor::new(&disk_bookmarks[..]);
|
|
|
|
|
|
|
|
let bookmarks = StockBookmarks::from_reader(reader).unwrap();
|
2018-03-16 03:23:19 +03:00
|
|
|
assert_bookmark_get(
|
|
|
|
&bookmarks,
|
|
|
|
&"abc",
|
|
|
|
Some(HgChangesetId::new(nodehash::ONES_HASH)),
|
|
|
|
);
|
|
|
|
assert_bookmark_get(
|
|
|
|
&bookmarks,
|
|
|
|
&"def",
|
|
|
|
Some(HgChangesetId::new(nodehash::TWOS_HASH)),
|
|
|
|
);
|
|
|
|
assert_bookmark_get(
|
|
|
|
&bookmarks,
|
|
|
|
&"test123",
|
|
|
|
Some(HgChangesetId::new(nodehash::ONES_HASH)),
|
|
|
|
);
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// Bookmarks that aren't present
|
|
|
|
assert_bookmark_get(&bookmarks, &"abcdef", None);
|
|
|
|
|
|
|
|
// keys should return all the keys here
|
|
|
|
let mut list = bookmarks.keys().collect().wait().unwrap();
|
|
|
|
list.sort();
|
|
|
|
assert_eq!(list, vec![&b"abc"[..], &b"def"[..], &b"test123"[..]]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test a bunch of invalid bookmark lines
|
|
|
|
#[test]
|
|
|
|
fn test_invalid() {
|
|
|
|
let reader = Cursor::new(&b"111\n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-10-10 17:25:36 +03:00
|
|
|
assert_matches!(
|
2017-12-06 04:52:31 +03:00
|
|
|
bookmarks.unwrap_err().downcast::<ErrorKind>().unwrap(),
|
|
|
|
ErrorKind::InvalidBookmarkLine(_)
|
2017-10-10 17:25:36 +03:00
|
|
|
);
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// no space or bookmark name
|
|
|
|
let reader = Cursor::new(&b"1111111111111111111111111111111111111111\n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-10-10 17:25:36 +03:00
|
|
|
assert_matches!(
|
2017-12-06 04:52:31 +03:00
|
|
|
bookmarks.unwrap_err().downcast::<ErrorKind>().unwrap(),
|
|
|
|
ErrorKind::InvalidBookmarkLine(_)
|
2017-10-10 17:25:36 +03:00
|
|
|
);
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// no bookmark name
|
|
|
|
let reader = Cursor::new(&b"1111111111111111111111111111111111111111 \n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-10-10 17:25:36 +03:00
|
|
|
assert_matches!(
|
2017-12-06 04:52:31 +03:00
|
|
|
bookmarks.unwrap_err().downcast::<ErrorKind>().unwrap(),
|
|
|
|
ErrorKind::InvalidBookmarkLine(_)
|
2017-10-10 17:25:36 +03:00
|
|
|
);
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// no space after hash
|
|
|
|
let reader = Cursor::new(&b"1111111111111111111111111111111111111111ab\n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-10-10 17:25:36 +03:00
|
|
|
assert_matches!(
|
2017-12-06 04:52:31 +03:00
|
|
|
bookmarks.unwrap_err().downcast::<ErrorKind>().unwrap(),
|
|
|
|
ErrorKind::InvalidBookmarkLine(_)
|
2017-10-10 17:25:36 +03:00
|
|
|
);
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// short hash
|
|
|
|
let reader = Cursor::new(&b"111111111111111111111111111111111111111 1ab\n"[..]);
|
2017-11-27 18:56:51 +03:00
|
|
|
let bookmarks = StockBookmarks::from_reader(reader);
|
2017-12-06 04:52:31 +03:00
|
|
|
let err = bookmarks.unwrap_err();
|
|
|
|
match err.downcast::<Context<ErrorKind>>() {
|
|
|
|
Ok(ctxt) => match ctxt.get_context() {
|
|
|
|
ok @ &ErrorKind::InvalidHash(..) => println!("OK: {:?}", ok),
|
|
|
|
bad => panic!("unexpected error {}", bad),
|
|
|
|
},
|
|
|
|
Err(bad) => panic!("other error: {:?}", bad),
|
|
|
|
};
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// non-ASCII
|
|
|
|
let reader = Cursor::new(&b"111111111111111111111111111111111111111\xff test\n"[..]);
|
2017-12-06 04:52:31 +03:00
|
|
|
let err = StockBookmarks::from_reader(reader).unwrap_err();
|
|
|
|
match err.downcast::<Context<ErrorKind>>() {
|
|
|
|
Ok(ctxt) => match ctxt.get_context() {
|
|
|
|
ok @ &ErrorKind::InvalidHash(..) => println!("OK: {:?}", ok),
|
|
|
|
bad => panic!("unexpected error {}", bad),
|
|
|
|
},
|
|
|
|
Err(bad) => panic!("other error: {:?}", bad),
|
|
|
|
};
|
2017-07-28 04:00:19 +03:00
|
|
|
|
|
|
|
// not a valid hex string
|
|
|
|
let reader = Cursor::new(&b"abcdefgabcdefgabcdefgabcdefgabcdefgabcde test\n"[..]);
|
2017-12-06 04:52:31 +03:00
|
|
|
let err = StockBookmarks::from_reader(reader).unwrap_err();
|
|
|
|
match err.downcast::<Context<ErrorKind>>() {
|
|
|
|
Ok(ctxt) => match ctxt.get_context() {
|
|
|
|
ok @ &ErrorKind::InvalidHash(..) => println!("OK: {:?}", ok),
|
|
|
|
bad => panic!("unexpected error {}", bad),
|
|
|
|
},
|
|
|
|
Err(bad) => panic!("other error: {:?}", bad),
|
|
|
|
};
|
2017-07-28 04:00:19 +03:00
|
|
|
}
|
|
|
|
}
|