mirror of
https://github.com/facebook/sapling.git
synced 2024-10-10 16:57:49 +03:00
bookmarks_movement: refactor bookmark movement for pushrebase
Summary: Refactor control of movement of non-scratch bookmarks through pushrebase. Reviewed By: krallin Differential Revision: D22920694 fbshipit-source-id: 347777045b4995b69973118781511686cf34bdba
This commit is contained in:
parent
a16b88d1c5
commit
c529e6a527
@ -13,10 +13,14 @@ bonsai_git_mapping = { path = "../../bonsai_git_mapping" }
|
||||
bookmarks = { path = ".." }
|
||||
bookmarks_types = { path = "../bookmarks_types" }
|
||||
context = { path = "../../server/context" }
|
||||
git_mapping_pushrebase_hook = { path = "../../bonsai_git_mapping/git_mapping_pushrebase_hook" }
|
||||
globalrev_pushrebase_hook = { path = "../../bonsai_globalrev_mapping/globalrev_pushrebase_hook" }
|
||||
metaconfig_types = { path = "../../metaconfig/types" }
|
||||
mononoke_types = { path = "../../mononoke_types" }
|
||||
pushrebase = { path = "../../pushrebase" }
|
||||
reachabilityindex = { path = "../../reachabilityindex" }
|
||||
scuba_ext = { path = "../../common/scuba_ext" }
|
||||
futures_stats = { git = "https://github.com/facebookexperimental/rust-shed.git", branch = "master" }
|
||||
anyhow = "1.0"
|
||||
futures = { version = "0.3.5", features = ["async-await", "compat"] }
|
||||
thiserror = "1.0"
|
||||
|
@ -5,22 +5,27 @@
|
||||
* GNU General Public License version 2.
|
||||
*/
|
||||
|
||||
#![allow(unused)]
|
||||
#![deny(warnings)]
|
||||
|
||||
use bookmarks_types::BookmarkName;
|
||||
use context::CoreContext;
|
||||
use metaconfig_types::{BookmarkAttrs, InfinitepushParams};
|
||||
use mononoke_types::ChangesetId;
|
||||
use pushrebase::PushrebaseError;
|
||||
use thiserror::Error;
|
||||
|
||||
mod create;
|
||||
mod delete;
|
||||
mod git_mapping;
|
||||
mod globalrev_mapping;
|
||||
mod pushrebase_onto;
|
||||
mod update;
|
||||
|
||||
pub use pushrebase::PushrebaseOutcome;
|
||||
|
||||
pub use crate::create::CreateBookmarkOp;
|
||||
pub use crate::delete::DeleteBookmarkOp;
|
||||
pub use crate::pushrebase_onto::PushrebaseOntoBookmarkOp;
|
||||
pub use crate::update::{BookmarkUpdatePolicy, BookmarkUpdateTargets, UpdateBookmarkOp};
|
||||
|
||||
/// How authorization for the bookmark move should be determined.
|
||||
@ -132,6 +137,9 @@ pub enum BookmarkMovementError {
|
||||
#[error("Bookmark transaction failed")]
|
||||
TransactionFailed,
|
||||
|
||||
#[error("Pushrebase failed")]
|
||||
PushrebaseError(#[source] PushrebaseError),
|
||||
|
||||
#[error(transparent)]
|
||||
Error(#[from] anyhow::Error),
|
||||
}
|
||||
|
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This software may be used and distributed according to the terms of the
|
||||
* GNU General Public License version 2.
|
||||
*/
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use blobrepo::BlobRepo;
|
||||
use bookmarks_types::BookmarkName;
|
||||
use context::CoreContext;
|
||||
use futures_stats::TimedFutureExt;
|
||||
use git_mapping_pushrebase_hook::GitMappingPushrebaseHook;
|
||||
use globalrev_pushrebase_hook::GlobalrevPushrebaseHook;
|
||||
use metaconfig_types::{BookmarkAttrs, InfinitepushParams, PushrebaseParams};
|
||||
use mononoke_types::BonsaiChangeset;
|
||||
use scuba_ext::ScubaSampleBuilderExt;
|
||||
|
||||
use crate::{BookmarkKindRestrictions, BookmarkMoveAuthorization, BookmarkMovementError};
|
||||
|
||||
pub struct PushrebaseOntoBookmarkOp<'op> {
|
||||
bookmark: &'op BookmarkName,
|
||||
changesets: &'op HashSet<BonsaiChangeset>,
|
||||
auth: BookmarkMoveAuthorization,
|
||||
kind_restrictions: BookmarkKindRestrictions,
|
||||
hg_replay: Option<&'op pushrebase::HgReplayData>,
|
||||
}
|
||||
|
||||
#[must_use = "PushrebaseOntoBookmarkOp must be run to have an effect"]
|
||||
impl<'op> PushrebaseOntoBookmarkOp<'op> {
|
||||
pub fn new(
|
||||
bookmark: &'op BookmarkName,
|
||||
changesets: &'op HashSet<BonsaiChangeset>,
|
||||
) -> PushrebaseOntoBookmarkOp<'op> {
|
||||
PushrebaseOntoBookmarkOp {
|
||||
bookmark,
|
||||
changesets,
|
||||
auth: BookmarkMoveAuthorization::Context,
|
||||
kind_restrictions: BookmarkKindRestrictions::AnyKind,
|
||||
hg_replay: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn only_if_scratch(mut self) -> Self {
|
||||
self.kind_restrictions = BookmarkKindRestrictions::OnlyScratch;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn only_if_public(mut self) -> Self {
|
||||
self.kind_restrictions = BookmarkKindRestrictions::OnlyPublic;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hg_replay_data(mut self, hg_replay: Option<&'op pushrebase::HgReplayData>) -> Self {
|
||||
self.hg_replay = hg_replay;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
self,
|
||||
ctx: &'op CoreContext,
|
||||
repo: &'op BlobRepo,
|
||||
infinitepush_params: &'op InfinitepushParams,
|
||||
pushrebase_params: &'op PushrebaseParams,
|
||||
bookmark_attrs: &'op BookmarkAttrs,
|
||||
) -> Result<pushrebase::PushrebaseOutcome, BookmarkMovementError> {
|
||||
self.auth
|
||||
.check_authorized(ctx, bookmark_attrs, self.bookmark)?;
|
||||
|
||||
if pushrebase_params.block_merges {
|
||||
let any_merges = self.changesets.iter().any(BonsaiChangeset::is_merge);
|
||||
if any_merges {
|
||||
return Err(anyhow!(
|
||||
"Pushrebase blocked because it contains a merge commit.\n\
|
||||
If you need this for a specific use case please contact\n\
|
||||
the Source Control team at https://fburl.com/27qnuyl2"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
self.kind_restrictions
|
||||
.check_kind(infinitepush_params, self.bookmark)?;
|
||||
|
||||
let mut pushrebase_hooks = Vec::new();
|
||||
|
||||
if pushrebase_params.assign_globalrevs {
|
||||
let hook = GlobalrevPushrebaseHook::new(
|
||||
repo.bonsai_globalrev_mapping().clone(),
|
||||
repo.get_repoid(),
|
||||
);
|
||||
pushrebase_hooks.push(hook);
|
||||
}
|
||||
|
||||
if pushrebase_params.populate_git_mapping {
|
||||
let hook = GitMappingPushrebaseHook::new(repo.bonsai_git_mapping().clone());
|
||||
pushrebase_hooks.push(hook);
|
||||
}
|
||||
|
||||
let mut flags = pushrebase_params.flags.clone();
|
||||
if let Some(rewritedates) = bookmark_attrs.should_rewrite_dates(self.bookmark) {
|
||||
// Bookmark config overrides repo flags.rewritedates config
|
||||
flags.rewritedates = rewritedates;
|
||||
}
|
||||
|
||||
ctx.scuba().clone().log_with_msg("Pushrebase started", None);
|
||||
let (stats, result) = pushrebase::do_pushrebase_bonsai(
|
||||
ctx,
|
||||
repo,
|
||||
&flags,
|
||||
self.bookmark,
|
||||
self.changesets,
|
||||
self.hg_replay,
|
||||
pushrebase_hooks.as_slice(),
|
||||
)
|
||||
.timed()
|
||||
.await;
|
||||
|
||||
let mut scuba_logger = ctx.scuba().clone();
|
||||
scuba_logger.add_future_stats(&stats);
|
||||
match &result {
|
||||
Ok(outcome) => scuba_logger
|
||||
.add("pushrebase_retry_num", outcome.retry_num.0)
|
||||
.log_with_msg("Pushrebase finished", None),
|
||||
Err(err) => scuba_logger.log_with_msg("Pushrebase failed", Some(format!("{:#?}", err))),
|
||||
}
|
||||
|
||||
result.map_err(BookmarkMovementError::PushrebaseError)
|
||||
}
|
||||
}
|
@ -10,24 +10,23 @@ use crate::{
|
||||
PostResolveBookmarkOnlyPushRebase, PostResolveInfinitePush, PostResolvePush,
|
||||
PostResolvePushRebase, PushrebaseBookmarkSpec,
|
||||
};
|
||||
use anyhow::{anyhow, format_err, Context, Error, Result};
|
||||
use anyhow::{anyhow, Context, Error, Result};
|
||||
use blobrepo::BlobRepo;
|
||||
use blobrepo_hg::BlobRepoHg;
|
||||
use bookmarks::{BookmarkName, BookmarkUpdateReason, BundleReplay};
|
||||
use bookmarks_movement::{BookmarkUpdatePolicy, BookmarkUpdateTargets};
|
||||
use bookmarks_movement::{BookmarkMovementError, BookmarkUpdatePolicy, BookmarkUpdateTargets};
|
||||
use context::CoreContext;
|
||||
use futures::{
|
||||
compat::Future01CompatExt,
|
||||
future::try_join,
|
||||
stream::{FuturesUnordered, TryStreamExt},
|
||||
};
|
||||
use futures_stats::TimedFutureExt;
|
||||
use git_mapping_pushrebase_hook::GitMappingPushrebaseHook;
|
||||
use globalrev_pushrebase_hook::GlobalrevPushrebaseHook;
|
||||
use mercurial_bundle_replay_data::BundleReplayData;
|
||||
use metaconfig_types::{BookmarkAttrs, InfinitepushParams, PushParams, PushrebaseParams};
|
||||
use mononoke_types::{BonsaiChangeset, ChangesetId, RawBundle2Id};
|
||||
use pushrebase::PushrebaseHook;
|
||||
use pushrebase::{PushrebaseError, PushrebaseHook};
|
||||
use reachabilityindex::LeastCommonAncestorsHint;
|
||||
use reverse_filler_queue::ReverseFillerQueue;
|
||||
use scribe_commit_queue::{self, LogToScribe};
|
||||
@ -438,7 +437,6 @@ async fn run_pushrebase(
|
||||
) -> Result<UnbundlePushRebaseResponse, BundleResolverError> {
|
||||
debug!(ctx.logger(), "unbundle processing: running pushrebase.");
|
||||
let PostResolvePushRebase {
|
||||
any_merges,
|
||||
bookmark_push_part_id,
|
||||
bookmark_spec,
|
||||
maybe_hg_replay_data,
|
||||
@ -462,7 +460,6 @@ async fn run_pushrebase(
|
||||
repo,
|
||||
&pushrebase_params,
|
||||
&uploaded_bonsais,
|
||||
any_merges,
|
||||
&onto_bookmark,
|
||||
&maybe_hg_replay_data,
|
||||
bookmark_attrs,
|
||||
@ -616,73 +613,29 @@ async fn normal_pushrebase(
|
||||
repo: &BlobRepo,
|
||||
pushrebase_params: &PushrebaseParams,
|
||||
changesets: &HashSet<BonsaiChangeset>,
|
||||
any_merges: bool,
|
||||
bookmark: &BookmarkName,
|
||||
maybe_hg_replay_data: &Option<pushrebase::HgReplayData>,
|
||||
bookmark_attrs: &BookmarkAttrs,
|
||||
infinitepush_params: &InfinitepushParams,
|
||||
) -> Result<(ChangesetId, Vec<pushrebase::PushrebaseChangesetPair>), BundleResolverError> {
|
||||
check_plain_bookmark_move_preconditions(
|
||||
&ctx,
|
||||
&bookmark,
|
||||
"pushrebase",
|
||||
&bookmark_attrs,
|
||||
&infinitepush_params,
|
||||
)?;
|
||||
|
||||
let block_merges = pushrebase_params.block_merges.clone();
|
||||
if block_merges && any_merges {
|
||||
return Err(format_err!(
|
||||
"Pushrebase blocked because it contains a merge commit.\n\
|
||||
If you need this for a specific use case please contact\n\
|
||||
the Source Control team at https://fburl.com/27qnuyl2"
|
||||
bookmarks_movement::PushrebaseOntoBookmarkOp::new(bookmark, changesets)
|
||||
.only_if_public()
|
||||
.with_hg_replay_data(maybe_hg_replay_data.as_ref())
|
||||
.run(
|
||||
ctx,
|
||||
repo,
|
||||
infinitepush_params,
|
||||
pushrebase_params,
|
||||
bookmark_attrs,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let hooks = get_pushrebase_hooks(&repo, &pushrebase_params);
|
||||
|
||||
let mut flags = pushrebase_params.flags.clone();
|
||||
if let Some(rewritedates) = bookmark_attrs.should_rewrite_dates(bookmark) {
|
||||
// Bookmark config overrides repo flags.rewritedates config
|
||||
flags.rewritedates = rewritedates;
|
||||
}
|
||||
|
||||
ctx.scuba().clone().log_with_msg("Pushrebase started", None);
|
||||
let (stats, result) = pushrebase::do_pushrebase_bonsai(
|
||||
&ctx,
|
||||
&repo,
|
||||
&flags,
|
||||
bookmark,
|
||||
&changesets,
|
||||
maybe_hg_replay_data.as_ref(),
|
||||
&hooks[..],
|
||||
)
|
||||
.timed()
|
||||
.await;
|
||||
|
||||
let mut scuba_logger = ctx.scuba().clone();
|
||||
scuba_logger.add_future_stats(&stats);
|
||||
|
||||
match result {
|
||||
Ok(ref res) => {
|
||||
scuba_logger
|
||||
.add("pushrebase_retry_num", res.retry_num.0)
|
||||
.log_with_msg("Pushrebase finished", None);
|
||||
}
|
||||
Err(ref err) => {
|
||||
scuba_logger.log_with_msg("Pushrebase failed", Some(format!("{:#?}", err)));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
.await
|
||||
.map(|outcome| (outcome.head, outcome.rebased_changesets))
|
||||
.map_err(|err| match err {
|
||||
pushrebase::PushrebaseError::Conflicts(conflicts) => {
|
||||
BookmarkMovementError::PushrebaseError(PushrebaseError::Conflicts(conflicts)) => {
|
||||
BundleResolverError::PushrebaseConflicts(conflicts)
|
||||
}
|
||||
_ => BundleResolverError::Error(format_err!("pushrebase failed {:?}", err)),
|
||||
_ => BundleResolverError::Error(err.into()),
|
||||
})
|
||||
.map(|res| (res.head, res.rebased_changesets))
|
||||
}
|
||||
|
||||
async fn force_pushrebase(
|
||||
@ -778,36 +731,6 @@ async fn force_pushrebase(
|
||||
Ok((new_target, Vec::new()))
|
||||
}
|
||||
|
||||
fn check_plain_bookmark_move_preconditions(
|
||||
ctx: &CoreContext,
|
||||
bookmark: &BookmarkName,
|
||||
reason: &'static str,
|
||||
bookmark_attrs: &BookmarkAttrs,
|
||||
infinitepush_params: &InfinitepushParams,
|
||||
) -> Result<()> {
|
||||
let user = ctx.user_unix_name();
|
||||
if !bookmark_attrs.is_allowed_user(user, bookmark) {
|
||||
return Err(format_err!(
|
||||
"[{}] This user `{:?}` is not allowed to move `{:?}`",
|
||||
reason,
|
||||
user,
|
||||
bookmark
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(ref namespace) = infinitepush_params.namespace {
|
||||
if namespace.matches_bookmark(bookmark) {
|
||||
return Err(format_err!(
|
||||
"[{}] Only Infinitepush bookmarks are allowed to match pattern {}",
|
||||
reason,
|
||||
namespace.as_str(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn log_commits_to_scribe(
|
||||
ctx: &CoreContext,
|
||||
repo: &BlobRepo,
|
||||
|
@ -267,7 +267,6 @@ impl PushRedirector {
|
||||
// entry for the large repo will point to a blobstore key, which does not
|
||||
// exist in that large repo.
|
||||
let PostResolvePushRebase {
|
||||
any_merges,
|
||||
bookmark_push_part_id,
|
||||
bookmark_spec,
|
||||
maybe_hg_replay_data,
|
||||
@ -315,7 +314,6 @@ impl PushRedirector {
|
||||
});
|
||||
|
||||
let action = PostResolvePushRebase {
|
||||
any_merges,
|
||||
bookmark_push_part_id,
|
||||
bookmark_spec,
|
||||
maybe_hg_replay_data,
|
||||
|
@ -200,7 +200,6 @@ pub struct PostResolveInfinitePush {
|
||||
/// Data, needed to perform post-resolve `PushRebase` action
|
||||
#[derive(Clone)]
|
||||
pub struct PostResolvePushRebase {
|
||||
pub any_merges: bool,
|
||||
pub bookmark_push_part_id: Option<PartId>,
|
||||
pub bookmark_spec: PushrebaseBookmarkSpec<ChangesetId>,
|
||||
pub maybe_hg_replay_data: Option<HgReplayData>,
|
||||
@ -570,11 +569,6 @@ async fn resolve_pushrebase<'r>(
|
||||
None => return Err(format_err!("onto is not specified").into()),
|
||||
};
|
||||
|
||||
let changesets = &cg_push.changesets.clone();
|
||||
let any_merges = changesets
|
||||
.iter()
|
||||
.any(|(_, revlog_cs)| revlog_cs.p1.is_some() && revlog_cs.p2.is_some());
|
||||
|
||||
let will_rebase = onto_bookmark != *DONOTREBASEBOOKMARK;
|
||||
// Mutation information must not be present in public commits
|
||||
// See T54101162, S186586
|
||||
@ -643,7 +637,6 @@ async fn resolve_pushrebase<'r>(
|
||||
});
|
||||
|
||||
Ok(PostResolveAction::PushRebase(PostResolvePushRebase {
|
||||
any_merges,
|
||||
bookmark_push_part_id,
|
||||
bookmark_spec,
|
||||
maybe_hg_replay_data,
|
||||
|
@ -148,13 +148,18 @@ pushrebase
|
||||
searching for changes
|
||||
remote: Command failed
|
||||
remote: Error:
|
||||
remote: [pushrebase] This user `Some("a")` is not allowed to move `BookmarkName { bookmark: "C" }`
|
||||
remote: User 'a' is not permitted to move 'C'
|
||||
remote:
|
||||
remote: Root cause:
|
||||
remote: [pushrebase] This user `Some("a")` is not allowed to move `BookmarkName { bookmark: "C" }`
|
||||
remote: User 'a' is not permitted to move 'C'
|
||||
remote:
|
||||
remote: Debug context:
|
||||
remote: "[pushrebase] This user `Some(\"a\")` is not allowed to move `BookmarkName { bookmark: \"C\" }`"
|
||||
remote: PermissionDeniedUser {
|
||||
remote: user: "a",
|
||||
remote: bookmark: BookmarkName {
|
||||
remote: bookmark: "C",
|
||||
remote: },
|
||||
remote: }
|
||||
abort: stream ended unexpectedly (got 0 bytes, expected 4)
|
||||
[255]
|
||||
$ MOCK_USERNAME="c" hgmn push -r . --to C
|
||||
|
@ -86,6 +86,8 @@ Try to push merge commit
|
||||
remote: the Source Control team at https://fburl.com/27qnuyl2
|
||||
remote:
|
||||
remote: Debug context:
|
||||
remote: "Pushrebase blocked because it contains a merge commit.\nIf you need this for a specific use case please contact\nthe Source Control team at https://fburl.com/27qnuyl2"
|
||||
remote: Error(
|
||||
remote: "Pushrebase blocked because it contains a merge commit.\nIf you need this for a specific use case please contact\nthe Source Control team at https://fburl.com/27qnuyl2",
|
||||
remote: )
|
||||
abort: stream ended unexpectedly (got 0 bytes, expected 4)
|
||||
[255]
|
||||
|
@ -37,13 +37,29 @@ Push another commit that conflicts
|
||||
searching for changes
|
||||
remote: Command failed
|
||||
remote: Error:
|
||||
remote: pushrebase failed Error(Conflict detected while inserting git mappings (tried inserting: [BonsaiGitMappingEntry { git_sha1: GitSha1(2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b), bcs_id: ChangesetId(Blake2(3fa7acdeb82ac4f96a7bf1e7b5fa8f661c9921954a46164cbbfa828c0485595b)) }]))
|
||||
remote: Pushrebase failed
|
||||
remote:
|
||||
remote: Root cause:
|
||||
remote: pushrebase failed Error(Conflict detected while inserting git mappings (tried inserting: [BonsaiGitMappingEntry { git_sha1: GitSha1(2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b), bcs_id: ChangesetId(Blake2(3fa7acdeb82ac4f96a7bf1e7b5fa8f661c9921954a46164cbbfa828c0485595b)) }]))
|
||||
remote: Conflict detected while inserting git mappings (tried inserting: [BonsaiGitMappingEntry { git_sha1: GitSha1(2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b), bcs_id: ChangesetId(Blake2(3fa7acdeb82ac4f96a7bf1e7b5fa8f661c9921954a46164cbbfa828c0485595b)) }])
|
||||
remote:
|
||||
remote: Caused by:
|
||||
remote: Conflict detected while inserting git mappings (tried inserting: [BonsaiGitMappingEntry { git_sha1: GitSha1(2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b), bcs_id: ChangesetId(Blake2(3fa7acdeb82ac4f96a7bf1e7b5fa8f661c9921954a46164cbbfa828c0485595b)) }])
|
||||
remote:
|
||||
remote: Debug context:
|
||||
remote: "pushrebase failed Error(Conflict detected while inserting git mappings (tried inserting: [BonsaiGitMappingEntry { git_sha1: GitSha1(2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b), bcs_id: ChangesetId(Blake2(3fa7acdeb82ac4f96a7bf1e7b5fa8f661c9921954a46164cbbfa828c0485595b)) }]))"
|
||||
remote: PushrebaseError(
|
||||
remote: Error(
|
||||
remote: Conflict(
|
||||
remote: [
|
||||
remote: BonsaiGitMappingEntry {
|
||||
remote: git_sha1: GitSha1(2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b),
|
||||
remote: bcs_id: ChangesetId(
|
||||
remote: Blake2(3fa7acdeb82ac4f96a7bf1e7b5fa8f661c9921954a46164cbbfa828c0485595b),
|
||||
remote: ),
|
||||
remote: },
|
||||
remote: ],
|
||||
remote: ),
|
||||
remote: ),
|
||||
remote: )
|
||||
abort: stream ended unexpectedly (got 0 bytes, expected 4)
|
||||
[255]
|
||||
|
||||
|
@ -307,7 +307,6 @@ async fn maybe_unbundle(
|
||||
};
|
||||
|
||||
let PostResolvePushRebase {
|
||||
any_merges: _,
|
||||
bookmark_push_part_id: _,
|
||||
bookmark_spec,
|
||||
maybe_hg_replay_data: _,
|
||||
|
Loading…
Reference in New Issue
Block a user