Short hashes lookup: implement suggestions the same way as in Mercurial.

Summary:
Suggestions come in the error message as it is currently implemented in
Mercurial code. Format of suggestions also stays the same.

We give the hash, time, author and the title.

All suggestions are ordered (most recent go first).

We don't show them if there are two many.

Reviewed By: krallin

Differential Revision: D19732053

fbshipit-source-id: b94154cbc5a4f440a0053fc3fac2bca2ae0b7119
This commit is contained in:
Liubov Dmitrieva 2020-02-06 07:35:58 -08:00 committed by Facebook Github Bot
parent af2f50d644
commit 8228f84a60
4 changed files with 145 additions and 38 deletions

View File

@ -1315,7 +1315,7 @@ impl BlobRepo {
})
.traced(
&ctx.trace(),
"generate_hg_chengeset",
"generate_hg_changeset",
trace_args! {"changeset" => bcs_id.to_hex().to_string()},
)
.timed(move |stats, _| {

View File

@ -19,6 +19,7 @@ use failure_ext::FutureFailureErrorExt;
use futures::future::{Either, Future, IntoFuture};
use futures_ext::{BoxFuture, FutureExt};
use mononoke_types::DateTime;
use std::fmt::{self, Display};
use std::{collections::BTreeMap, io::Write};
const STEP_PARENTS_METADATA_KEY: &str = "stepparents";
@ -312,3 +313,24 @@ impl Loadable for HgChangesetId {
.boxify()
}
}
impl Display for HgBlobChangeset {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let comments = self.comments();
let title_end = comments
.iter()
.enumerate()
.find(|(_, &c)| c == b'\n')
.map(|(i, _)| i)
.unwrap_or(comments.len());
write!(
f,
"changeset: {}\nauthor: {}\ndate: {}\nsummary: {}\n",
self.changesetid,
String::from_utf8_lossy(&self.user()),
self.time().as_chrono().to_rfc2822(),
String::from_utf8_lossy(&self.comments()[0..title_end])
)
}
}

View File

@ -921,63 +921,129 @@ impl HgCommands for RepoClient {
buf.freeze()
}
fn check_bookmark_exists(
// Generate positive response including HgChangesetId as hex.
fn generate_changeset_resp_buf(csid: HgChangesetId) -> HgCommandRes<Bytes> {
Ok(generate_resp_buf(true, csid.to_hex().as_bytes()))
.into_future()
.boxify()
}
// Generate error response with the message including suggestions (commits info).
// Suggestions are ordered by commit time (most recent first).
fn generate_suggestions_resp_buf(
ctx: CoreContext,
repo: BlobRepo,
bookmark: BookmarkName,
suggestion_cids: Vec<HgChangesetId>,
) -> HgCommandRes<Bytes> {
repo.get_bookmark(ctx, &bookmark)
.map(move |csid| match csid {
Some(csid) => generate_resp_buf(true, csid.to_hex().as_bytes()),
None => generate_resp_buf(false, format!("{} not found", bookmark).as_bytes()),
let futs = suggestion_cids
.into_iter()
.map(|hg_csid| {
hg_csid
.load(ctx.clone(), repo.blobstore())
.from_err()
.map(move |cs| (cs.to_string().into_bytes(), cs.time().clone()))
})
.collect::<Vec<_>>();
future::join_all(futs)
.map(|mut info_plus_date| {
info_plus_date.sort_by_key(|&(_, time)| time);
let mut infos = info_plus_date
.into_iter()
.map(|(info, _)| info)
.collect::<Vec<_>>();
infos.push(b"ambiguous identifier\nsuggestions are:\n".to_vec());
infos.reverse();
generate_resp_buf(false, &infos.join(&[b'\n'][..]))
})
.boxify()
}
// If the string is a valid full changeset hash, parse it.
// If the string is a valid prefix of changeset hash, resolve it to a changeset.
// TODO(liubovd): support ambiguity and suggestions
// Controls how many suggestions to fetch in case of ambiguous outcome of prefix lookup.
const MAX_NUMBER_OF_SUGGESTIONS_TO_FETCH: usize = 10;
// Resolves changeset or set of suggestions from the key (full hex hash or a prefix) if exist.
// Note: `get_many_hg_by_prefix` works for the full hex hashes well but
// `changeset_exists` has better caching and is preferable for the full length hex hashes.
let node_fut = match HgChangesetId::from_str(&key) {
Ok(node) => ok(Some(node)).boxify(),
Ok(csid) => repo
.changeset_exists(ctx.clone(), csid)
.map(move |exists| {
if exists {
HgChangesetIdsResolvedFromPrefix::Single(csid)
} else {
HgChangesetIdsResolvedFromPrefix::NoMatch
}
})
.boxify(),
Err(_) => match HgChangesetIdPrefix::from_str(&key) {
Ok(cs_prefix) => repo
.get_bonsai_hg_mapping()
.get_many_hg_by_prefix(ctx.clone(), repo.get_repoid(), cs_prefix, 1)
.map(move |resolved_cids| match resolved_cids {
HgChangesetIdsResolvedFromPrefix::Single(cs) => Some(cs),
_ => None,
})
.get_many_hg_by_prefix(
ctx.clone(),
repo.get_repoid(),
cs_prefix,
MAX_NUMBER_OF_SUGGESTIONS_TO_FETCH,
)
.boxify(),
Err(_) => ok(None).boxify(),
Err(_) => ok(HgChangesetIdsResolvedFromPrefix::NoMatch).boxify(),
},
};
let bookmark = BookmarkName::new(&key).ok();
// The lookup order:
// If there is an exact commit match, return that even if the key is the prefix of the hash.
// If there is a bookmark match, return that.
// If there are suggestions, show them. This happens in case of ambiguous outcome of prefix lookup.
// Otherwise, show an error.
let bookmark = BookmarkName::new(&key).ok();
let lookup_fut = node_fut
.and_then(move |node| match (node, bookmark) {
(Some(node), Some(bookmark)) => {
let csid = node;
repo.changeset_exists(ctx.clone(), csid)
.and_then({
cloned!(ctx);
move |exists| {
if exists {
Ok(generate_resp_buf(true, node.to_hex().as_bytes()))
.into_future()
.boxify()
} else {
check_bookmark_exists(ctx, repo, bookmark)
}
.and_then(move |resolved_cids| {
use HgChangesetIdsResolvedFromPrefix::*;
// Describing the priority relative to bookmark presence for the key.
enum LookupOutcome {
HighPriority(HgCommandRes<Bytes>),
LowPriority(HgCommandRes<Bytes>),
};
let outcome = match resolved_cids {
Single(csid) => LookupOutcome::HighPriority(generate_changeset_resp_buf(csid)),
Multiple(suggestion_cids) => LookupOutcome::LowPriority(
generate_suggestions_resp_buf(ctx.clone(), repo.clone(), suggestion_cids),
),
TooMany(_) => LookupOutcome::LowPriority(
Ok(generate_resp_buf(
false,
format!("ambiguous identifier '{}'", key).as_bytes(),
))
.into_future()
.boxify(),
),
NoMatch => LookupOutcome::LowPriority(
Ok(generate_resp_buf(
false,
format!("{} not found", key).as_bytes(),
))
.into_future()
.boxify(),
),
};
match (outcome, bookmark) {
(LookupOutcome::HighPriority(res), _) => res,
(LookupOutcome::LowPriority(res), Some(bookmark)) => repo
.get_bookmark(ctx.clone(), &bookmark)
.and_then(move |maybe_csid| {
if let Some(csid) = maybe_csid {
generate_changeset_resp_buf(csid)
} else {
res
}
})
.boxify()
.boxify(),
(LookupOutcome::LowPriority(res), None) => res,
}
(None, Some(bookmark)) => check_bookmark_exists(ctx.clone(), repo, bookmark),
// Failed to parse as a hash or bookmark.
_ => Ok(generate_resp_buf(false, "invalid input".as_bytes()))
.into_future()
.boxify(),
})
.boxify();

View File

@ -153,6 +153,7 @@ Do infinitepush (aka commit cloud) push, to a bookmark
remote: Unknown bookmark: scratch/123. Use --create to create one.
abort: stream ended unexpectedly (got 0 bytes, expected 4)
[255]
$ hgmn push ssh://user@dummy/repo -r . --to "scratch/123" --create
pushing to ssh://user@dummy/repo
searching for changes
@ -180,6 +181,7 @@ Do infinitepush (aka commit cloud) push, to a bookmark
remote: Non fastforward bookmark move from * to * (try --force?) (glob)
abort: stream ended unexpectedly (got 0 bytes, expected 4)
[255]
$ hgmn push ssh://user@dummy/repo -r 3903775176ed --to "scratch/123" --force
pushing to ssh://user@dummy/repo
searching for changes
@ -604,6 +606,23 @@ More sophisticated test for phases
abort: 'listkeyspatterns' command is not supported for the server ssh://user@dummy/repo
[255]
$ hgmn pull -r b # test ambiguous prefix
pulling from ssh://user@dummy/repo
abort: ambiguous identifier
suggestions are:
changeset: bf677f20a49dc5ac94946f3d91ad181f8a6fdbab
author: test
date: Thu, 01 Jan 1970 00:00:00 +0000
summary: zzz
changeset: b9f080ea95005f3513a22aa15f1f74d7371ce5d4
author: test
date: Thu, 01 Jan 1970 00:00:00 +0000
summary: zzzzz
!
[255]
$ hgmn pull -r 5e59ac0f4dd0 -r bf677f20a49d -r 7d67c7248d48 -r b9f080ea9500 -q
$ tglogpnr -r "::b9f080ea9500 - ::default/master_bookmark"