node: use ls-remote when in fetching mode
Given the scenario of 3 peers, A, B, and C each having a project R. If peer A has B's fork, but C does not, then when peer A attempts to fetch from C the fetch will be rejected since Git will fail when a refspec that is asked for does not exist on the remote side. To avoid this, the fetch logic is changed so that the client first calls ls-remote to the remote side, which results in the references that the remote has which the client is interested in. The follow-up fetch then uses these refs as the refspecs -- which should succeed unless there was a race for a deletion on the remote side. The rest of the verification logic stays the same, so the storage should still be in a working state before transferring from the staged repository to the production repository. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com> X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
parent
5a48cdf507
commit
694bd345d9
|
|
@ -2,7 +2,7 @@ mod channels;
|
||||||
mod fetch;
|
mod fetch;
|
||||||
mod tunnel;
|
mod tunnel;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::{BTreeSet, HashSet};
|
||||||
use std::io::{prelude::*, BufReader};
|
use std::io::{prelude::*, BufReader};
|
||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
use std::thread::JoinHandle;
|
use std::thread::JoinHandle;
|
||||||
|
|
@ -13,7 +13,7 @@ use crossbeam_channel as chan;
|
||||||
use radicle::identity::Id;
|
use radicle::identity::Id;
|
||||||
use radicle::prelude::NodeId;
|
use radicle::prelude::NodeId;
|
||||||
use radicle::storage::{Namespaces, ReadRepository, RefUpdate};
|
use radicle::storage::{Namespaces, ReadRepository, RefUpdate};
|
||||||
use radicle::{git, Storage};
|
use radicle::{git, storage, Storage};
|
||||||
|
|
||||||
use crate::runtime::Handle;
|
use crate::runtime::Handle;
|
||||||
use crate::wire::StreamId;
|
use crate::wire::StreamId;
|
||||||
|
|
@ -230,35 +230,48 @@ impl Worker {
|
||||||
mut channels: Channels,
|
mut channels: Channels,
|
||||||
) -> Result<(Vec<RefUpdate>, HashSet<NodeId>), FetchError> {
|
) -> Result<(Vec<RefUpdate>, HashSet<NodeId>), FetchError> {
|
||||||
let staging = fetch::StagingPhaseInitial::new(&self.storage, rid, namespaces.clone())?;
|
let staging = fetch::StagingPhaseInitial::new(&self.storage, rid, namespaces.clone())?;
|
||||||
|
let refs = if staging.repo.is_cloning() {
|
||||||
match self._fetch(
|
match self._fetch(
|
||||||
&staging.repo,
|
&staging.repo,
|
||||||
|
staging.repo.is_cloning(),
|
||||||
remote,
|
remote,
|
||||||
staging.refspecs(),
|
staging.refspecs(),
|
||||||
stream,
|
stream,
|
||||||
&mut channels,
|
&mut channels,
|
||||||
) {
|
) {
|
||||||
Ok(()) => log::debug!(target: "worker", "Initial fetch for {rid} exited successfully"),
|
Ok(_) => {
|
||||||
Err(e) => match (&staging.repo, e) {
|
log::debug!(target: "worker", "Initial fetch for {rid} exited successfully")
|
||||||
// When fetching, if the error comes from `git-fetch` returning an error, we
|
|
||||||
// keep going because it could be due to a rejected ref (eg. on `rad/sigrefs`),
|
|
||||||
// and that is non-fatal.
|
|
||||||
(fetch::StagedRepository::Fetching(_), e @ FetchError::CommandFailed { .. }) => {
|
|
||||||
log::warn!(target: "worker", "Initial fetch for {rid} returned an error: {e}");
|
|
||||||
log::warn!(target: "worker", "It's possible that some of the refs were rejected..");
|
|
||||||
}
|
}
|
||||||
// When cloning, any error is fatal, since we'll end up with an empty repository.
|
Err(e) => {
|
||||||
// Likewise, when fetching, if the error is coming from some other place, we
|
|
||||||
// abort the fetch.
|
|
||||||
(fetch::StagedRepository::Cloning(_) | fetch::StagedRepository::Fetching(_), e) => {
|
|
||||||
log::error!(target: "worker", "Initial fetch for {rid} failed: {e}");
|
log::error!(target: "worker", "Initial fetch for {rid} failed: {e}");
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let staging = staging.into_final()?;
|
// TODO(finto): when cloning we simply fetch the special
|
||||||
|
// rad refs from the remote side, however, when the
|
||||||
|
// repository already exists we need to `ls-remote` (see
|
||||||
|
// below). The result of the ls-remote is a BTreeSet of
|
||||||
|
// refs and so we need return an empty set here so that we
|
||||||
|
// can pass them into `into_final`. This is seems like a
|
||||||
|
// code smell to me due to bad boundaries between the
|
||||||
|
// logic in this module and the logic in the fetch module.
|
||||||
|
BTreeSet::new()
|
||||||
|
} else {
|
||||||
|
self.ls_refs(
|
||||||
|
&staging.repo,
|
||||||
|
staging.ls_remote_refs(),
|
||||||
|
remote,
|
||||||
|
stream,
|
||||||
|
&mut channels,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let staging = staging.into_final(refs)?;
|
||||||
|
|
||||||
match self._fetch(
|
match self._fetch(
|
||||||
&staging.repo,
|
&staging.repo,
|
||||||
|
staging.repo.is_cloning(),
|
||||||
remote,
|
remote,
|
||||||
staging.refspecs(),
|
staging.refspecs(),
|
||||||
stream,
|
stream,
|
||||||
|
|
@ -370,9 +383,86 @@ impl Worker {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _fetch<S>(
|
fn ls_refs(
|
||||||
&self,
|
&self,
|
||||||
repo: &fetch::StagedRepository,
|
repo: &fetch::StagedRepository,
|
||||||
|
namespaces: impl IntoIterator<Item = git::PatternString>,
|
||||||
|
remote: NodeId,
|
||||||
|
stream: StreamId,
|
||||||
|
channels: &mut Channels,
|
||||||
|
) -> Result<BTreeSet<git::Namespaced<'static>>, FetchError> {
|
||||||
|
let mut tunnel = Tunnel::with(channels, stream, self.nid, remote, self.handle.clone())?;
|
||||||
|
let tunnel_addr = tunnel.local_addr();
|
||||||
|
let mut cmd = process::Command::new("git");
|
||||||
|
cmd.current_dir(repo.path())
|
||||||
|
.env_clear()
|
||||||
|
.envs(env::vars().filter(|(k, _)| k == "PATH" || k.starts_with("GIT_TRACE")))
|
||||||
|
.envs(git::env::GIT_DEFAULT_CONFIG)
|
||||||
|
.args(["-c", "protocol.version=2"])
|
||||||
|
.arg("ls-remote")
|
||||||
|
.arg(format!("git://{tunnel_addr}/{}", repo.id.canonical()));
|
||||||
|
|
||||||
|
for ns in namespaces.into_iter() {
|
||||||
|
cmd.arg(ns.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.stdout(process::Stdio::piped())
|
||||||
|
.stderr(process::Stdio::piped())
|
||||||
|
.stdin(process::Stdio::piped());
|
||||||
|
|
||||||
|
log::debug!(target: "worker", "Running command: {:?}", cmd);
|
||||||
|
|
||||||
|
let mut child = cmd.spawn()?;
|
||||||
|
let stderr = child.stderr.take().unwrap();
|
||||||
|
let stdout = child.stdout.take().unwrap();
|
||||||
|
|
||||||
|
thread::Builder::new().name(self.name.clone()).spawn(|| {
|
||||||
|
for line in BufReader::new(stderr).lines().flatten() {
|
||||||
|
log::debug!(target: "worker", "Git: {}", line);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tunnel.run(self.timeout)?;
|
||||||
|
|
||||||
|
let result = child.wait()?;
|
||||||
|
if result.success() {
|
||||||
|
let mut refs = BTreeSet::new();
|
||||||
|
|
||||||
|
for line in BufReader::new(stdout).lines().flatten() {
|
||||||
|
log::debug!(target: "worker", "Git: {line}");
|
||||||
|
let r = match line.split_whitespace().next_back() {
|
||||||
|
Some(r) => r,
|
||||||
|
None => {
|
||||||
|
log::trace!(target: "worker", "Git: ls-remote returned unexpected format {line}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match git::RefString::try_from(r) {
|
||||||
|
Ok(r) => {
|
||||||
|
if let Some(ns) = r.to_namespaced() {
|
||||||
|
refs.insert(ns.to_owned());
|
||||||
|
} else {
|
||||||
|
log::debug!(target: "worker", "Git: non-namespaced ref '{r}'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(target: "worker", "Git: invalid refname '{r}' {err}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(refs)
|
||||||
|
} else {
|
||||||
|
Err(FetchError::CommandFailed {
|
||||||
|
code: result.code().unwrap_or(1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _fetch<S>(
|
||||||
|
&self,
|
||||||
|
repo: &storage::git::Repository,
|
||||||
|
is_cloning: bool,
|
||||||
remote: NodeId,
|
remote: NodeId,
|
||||||
specs: S,
|
specs: S,
|
||||||
stream: StreamId,
|
stream: StreamId,
|
||||||
|
|
@ -402,11 +492,11 @@ impl Worker {
|
||||||
.into_refspecs()
|
.into_refspecs()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// Filter out our own refs, if we aren't cloning.
|
// Filter out our own refs, if we aren't cloning.
|
||||||
.filter(|fs| repo.is_cloning() || !fs.dst.starts_with(namespace.as_str()))
|
.filter(|fs| is_cloning || !fs.dst.starts_with(namespace.as_str()))
|
||||||
.map(|spec| spec.to_string())
|
.map(|spec| spec.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if !repo.is_cloning() {
|
if !is_cloning {
|
||||||
// Make sure we don't fetch our own refs via a glob pattern.
|
// Make sure we don't fetch our own refs via a glob pattern.
|
||||||
fetchspecs.push(format!("^refs/namespaces/{}/*", self.nid));
|
fetchspecs.push(format!("^refs/namespaces/{}/*", self.nid));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ pub use refspecs::{AsRefspecs, Refspec, SpecialRefs};
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use radicle::crypto::{PublicKey, Unverified, Verified};
|
use radicle::crypto::{PublicKey, Unverified, Verified};
|
||||||
use radicle::git::url;
|
use radicle::git::{url, Namespaced};
|
||||||
use radicle::prelude::{Doc, Id, NodeId};
|
use radicle::prelude::{Doc, Id, NodeId};
|
||||||
use radicle::storage::git::Repository;
|
use radicle::storage::git::Repository;
|
||||||
use radicle::storage::refs::IDENTITY_BRANCH;
|
use radicle::storage::refs::IDENTITY_BRANCH;
|
||||||
|
|
@ -29,7 +29,7 @@ pub struct StagingPhaseInitial<'a> {
|
||||||
/// The original [`Storage`] we are finalising changes into.
|
/// The original [`Storage`] we are finalising changes into.
|
||||||
production: &'a Storage,
|
production: &'a Storage,
|
||||||
/// The `Namespaces` passed by the fetching caller.
|
/// The `Namespaces` passed by the fetching caller.
|
||||||
namespaces: Namespaces,
|
pub(super) namespaces: Namespaces,
|
||||||
_tmp: tempfile::TempDir,
|
_tmp: tempfile::TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,8 +51,36 @@ impl Deref for StagedRepository {
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
match self {
|
match self {
|
||||||
StagedRepository::Cloning(repo) => repo,
|
Self::Cloning(repo) => repo,
|
||||||
StagedRepository::Fetching(repo) => repo,
|
Self::Fetching(repo) => repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum FinalStagedRepository {
|
||||||
|
Cloning {
|
||||||
|
repo: Repository,
|
||||||
|
trusted: HashSet<NodeId>,
|
||||||
|
},
|
||||||
|
Fetching {
|
||||||
|
repo: Repository,
|
||||||
|
refs: BTreeSet<Namespaced<'static>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FinalStagedRepository {
|
||||||
|
pub fn is_cloning(&self) -> bool {
|
||||||
|
matches!(self, Self::Cloning { .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for FinalStagedRepository {
|
||||||
|
type Target = Repository;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self {
|
||||||
|
Self::Cloning { repo, .. } => repo,
|
||||||
|
Self::Fetching { repo, .. } => repo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,13 +98,9 @@ impl Deref for StagedRepository {
|
||||||
/// [`StagingPhaseFinal::transfer`].
|
/// [`StagingPhaseFinal::transfer`].
|
||||||
pub struct StagingPhaseFinal<'a> {
|
pub struct StagingPhaseFinal<'a> {
|
||||||
/// The inner [`Repository`] for staging fetches into.
|
/// The inner [`Repository`] for staging fetches into.
|
||||||
pub(super) repo: StagedRepository,
|
pub(super) repo: FinalStagedRepository,
|
||||||
/// The original [`Storage`] we are finalising changes into.
|
/// The original [`Storage`] we are finalising changes into.
|
||||||
production: &'a Storage,
|
production: &'a Storage,
|
||||||
/// The delegates and tracked remotes that the fetch is being
|
|
||||||
/// performed for. These are passed through from the
|
|
||||||
/// [`StagingPhaseInitial::namespaces`], if the variant is `Trusted`.
|
|
||||||
trusted: HashSet<RemoteId>,
|
|
||||||
_tmp: tempfile::TempDir,
|
_tmp: tempfile::TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,15 +152,34 @@ impl<'a> StagingPhaseInitial<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ls_remote_refs(&self) -> Vec<git::PatternString> {
|
||||||
|
match &self.namespaces {
|
||||||
|
Namespaces::All => {
|
||||||
|
vec![git::refspec::pattern!("refs/namespaces/*")]
|
||||||
|
}
|
||||||
|
Namespaces::Trusted(trusted) => trusted
|
||||||
|
.iter()
|
||||||
|
.map(|ns| {
|
||||||
|
git::refname!("refs/namespaces")
|
||||||
|
.join(git::Component::from(ns))
|
||||||
|
.with_pattern(git::refspec::STAR)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert the [`StagingPhaseInitial`] into [`StagingPhaseFinal`] to continue
|
/// Convert the [`StagingPhaseInitial`] into [`StagingPhaseFinal`] to continue
|
||||||
/// the fetch process.
|
/// the fetch process.
|
||||||
pub fn into_final(self) -> Result<StagingPhaseFinal<'a>, error::Transition> {
|
pub fn into_final(
|
||||||
let trusted = match &self.repo {
|
self,
|
||||||
|
refs: BTreeSet<Namespaced<'static>>,
|
||||||
|
) -> Result<StagingPhaseFinal<'a>, error::Transition> {
|
||||||
|
let repo = match self.repo {
|
||||||
StagedRepository::Cloning(repo) => {
|
StagedRepository::Cloning(repo) => {
|
||||||
log::debug!(target: "worker", "Loading remotes for clone of {}", self.repo.id);
|
log::debug!(target: "worker", "Loading remotes for clone of {}", repo.id);
|
||||||
let oid = ReadRepository::identity_head(repo)?;
|
let oid = ReadRepository::identity_head(&repo)?;
|
||||||
log::trace!(target: "worker", "Loading 'rad/id' @ {oid}");
|
log::trace!(target: "worker", "Loading 'rad/id' @ {oid}");
|
||||||
let (doc, _) = Doc::<Unverified>::load_at(oid, repo)?;
|
let (doc, _) = Doc::<Unverified>::load_at(oid, &repo)?;
|
||||||
let doc = doc.verified()?;
|
let doc = doc.verified()?;
|
||||||
let mut trusted = match self.namespaces.clone() {
|
let mut trusted = match self.namespaces.clone() {
|
||||||
Namespaces::All => HashSet::new(),
|
Namespaces::All => HashSet::new(),
|
||||||
|
|
@ -144,26 +187,14 @@ impl<'a> StagingPhaseInitial<'a> {
|
||||||
};
|
};
|
||||||
let delegates = doc.delegates.map(PublicKey::from);
|
let delegates = doc.delegates.map(PublicKey::from);
|
||||||
trusted.extend(delegates);
|
trusted.extend(delegates);
|
||||||
trusted
|
FinalStagedRepository::Cloning { repo, trusted }
|
||||||
}
|
|
||||||
StagedRepository::Fetching(repo) => {
|
|
||||||
log::debug!(target: "worker", "Loading remotes for fetching of {}", self.repo.id);
|
|
||||||
match self.namespaces.clone() {
|
|
||||||
Namespaces::All => {
|
|
||||||
let mut trusted = repo.remote_ids()?.collect::<Result<HashSet<_>, _>>()?;
|
|
||||||
trusted.extend(repo.delegates()?.map(PublicKey::from));
|
|
||||||
trusted
|
|
||||||
}
|
|
||||||
|
|
||||||
Namespaces::Trusted(trusted) => trusted,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
StagedRepository::Fetching(repo) => FinalStagedRepository::Fetching { repo, refs },
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(StagingPhaseFinal {
|
Ok(StagingPhaseFinal {
|
||||||
repo: self.repo,
|
repo,
|
||||||
production: self.production,
|
production: self.production,
|
||||||
trusted,
|
|
||||||
_tmp: self._tmp,
|
_tmp: self._tmp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -218,14 +249,11 @@ impl<'a> StagingPhaseFinal<'a> {
|
||||||
/// Return the fetch refspecs for fetching the necessary
|
/// Return the fetch refspecs for fetching the necessary
|
||||||
/// references.
|
/// references.
|
||||||
pub fn refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
|
pub fn refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
|
||||||
match self.repo {
|
match &self.repo {
|
||||||
StagedRepository::Cloning(_) => Namespaces::Trusted(self.trusted.clone()).as_refspecs(),
|
FinalStagedRepository::Cloning { trusted, .. } => {
|
||||||
StagedRepository::Fetching(_) => {
|
Namespaces::Trusted(trusted.clone()).as_refspecs()
|
||||||
self.remotes().fold(Vec::new(), |mut specs, remote| {
|
|
||||||
specs.extend(remote.as_refspecs());
|
|
||||||
specs
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
FinalStagedRepository::Fetching { refs, .. } => refs.as_refspecs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,10 +273,10 @@ impl<'a> StagingPhaseFinal<'a> {
|
||||||
/// All references that were updated are returned as a
|
/// All references that were updated are returned as a
|
||||||
/// [`RefUpdate`].
|
/// [`RefUpdate`].
|
||||||
pub fn transfer(self) -> Result<(Vec<RefUpdate>, HashSet<NodeId>), error::Transfer> {
|
pub fn transfer(self) -> Result<(Vec<RefUpdate>, HashSet<NodeId>), error::Transfer> {
|
||||||
let verifications = self.verify();
|
let verifications = self.verify()?;
|
||||||
let production = match &self.repo {
|
let production = match &self.repo {
|
||||||
StagedRepository::Cloning(repo) => self.production.create(repo.id)?,
|
FinalStagedRepository::Cloning { repo, .. } => self.production.create(repo.id)?,
|
||||||
StagedRepository::Fetching(repo) => self.production.repository(repo.id)?,
|
FinalStagedRepository::Fetching { repo, .. } => self.production.repository(repo.id)?,
|
||||||
};
|
};
|
||||||
let url = url::File::new(self.repo.path().to_path_buf()).to_string();
|
let url = url::File::new(self.repo.path().to_path_buf()).to_string();
|
||||||
let mut remote = production.backend.remote_anonymous(&url)?;
|
let mut remote = production.backend.remote_anonymous(&url)?;
|
||||||
|
|
@ -373,14 +401,22 @@ impl<'a> StagingPhaseFinal<'a> {
|
||||||
Ok((updates, remotes))
|
Ok((updates, remotes))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remotes(&self) -> impl Iterator<Item = Remote> + '_ {
|
fn remotes(&self) -> Result<Box<dyn Iterator<Item = Remote> + '_>, git::raw::Error> {
|
||||||
self.trusted
|
match &self.repo {
|
||||||
|
FinalStagedRepository::Cloning { trusted, .. } => Ok(Box::new(
|
||||||
|
trusted
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|remote| self.repo.remote(remote).ok())
|
.filter_map(|remote| self.repo.remote(remote).ok()),
|
||||||
|
)),
|
||||||
|
FinalStagedRepository::Fetching { repo, .. } => Ok(Box::new(
|
||||||
|
repo.remotes()?.filter_map(|r| r.ok().map(|(_, r)| r)),
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify(&self) -> BTreeMap<RemoteId, VerifiedRemote> {
|
fn verify(&self) -> Result<BTreeMap<RemoteId, VerifiedRemote>, git::raw::Error> {
|
||||||
self.remotes()
|
Ok(self
|
||||||
|
.remotes()?
|
||||||
.map(|remote| {
|
.map(|remote| {
|
||||||
let remote_id = remote.id;
|
let remote_id = remote.id;
|
||||||
let verification = match self.repo.identity_doc_of(&remote_id) {
|
let verification = match self.repo.identity_doc_of(&remote_id) {
|
||||||
|
|
@ -400,7 +436,7 @@ impl<'a> StagingPhaseFinal<'a> {
|
||||||
};
|
};
|
||||||
(remote_id, verification)
|
(remote_id, verification)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
|
|
@ -159,3 +160,23 @@ impl AsRefspecs for Remote {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> AsRefspecs for BTreeSet<git::Namespaced<'a>> {
|
||||||
|
fn as_refspecs(&self) -> Vec<Refspec<git::PatternString, git::PatternString>> {
|
||||||
|
let reserved = [(*IDENTITY_BRANCH).clone(), (*SIGREFS_BRANCH).clone()]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
self.iter()
|
||||||
|
.map(|r| {
|
||||||
|
// Only force ordinary refs.
|
||||||
|
let suffix = r.strip_namespace();
|
||||||
|
let force = !reserved.contains(&suffix);
|
||||||
|
Refspec {
|
||||||
|
src: r.clone().to_ref_string().into(),
|
||||||
|
dst: r.clone().to_ref_string().into(),
|
||||||
|
force,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue