From d2f423521ec76406944ad83098ec33afe20c692b Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Mon, 9 Jan 2023 13:18:33 +0100 Subject: This is it Squashed commit of all the exploration history. Development starts here. Signed-off-by: Kim Altintop --- src/cmd/drop/init.rs | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 src/cmd/drop/init.rs (limited to 'src/cmd/drop/init.rs') diff --git a/src/cmd/drop/init.rs b/src/cmd/drop/init.rs new file mode 100644 index 0000000..b843255 --- /dev/null +++ b/src/cmd/drop/init.rs @@ -0,0 +1,194 @@ +// Copyright © 2022 Kim Altintop +// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception + +use std::{ + iter, + num::NonZeroUsize, + path::PathBuf, +}; + +use anyhow::{ + anyhow, + ensure, +}; + +use super::{ + find_id, + Common, + Editable, +}; +use crate::{ + cfg, + cmd::{ + self, + args::Refname, + ui::{ + self, + edit_metadata, + }, + }, + git::{ + self, + if_not_found_none, + refs, + }, + json, + metadata::{ + self, + git::META_FILE_DROP, + Metadata, + }, + patches::{ + REF_HEADS_PATCHES, + REF_IT_PATCHES, + }, +}; + +#[derive(Debug, clap::Args)] +pub struct Init { + #[clap(flatten)] + common: Common, + /// A description for this drop instance, max. 128 characters + #[clap(long, value_parser, value_name = "STRING")] + description: metadata::drop::Description, + /// If the repository does not already exist, initialise it as non-bare + /// + /// A drop is usually initialised inside an already existing git repository, + /// or as a standalone drop repository. The latter is advisable for serving + /// over the network. + /// + /// When init is given a directory which does not already exist, it is + /// assumed that a standalone drop should be created, and thus the + /// repository is initialised as bare. This behaviour can be overridden + /// by --no-bare. + #[clap(long, value_parser)] + no_bare: bool, +} + +#[derive(serde::Serialize)] +pub struct Output { + repo: PathBuf, + #[serde(rename = "ref")] + refname: Refname, + #[serde(with = "crate::git::serde::oid")] + commit: git2::Oid, +} + +pub fn init(args: Init) -> cmd::Result { + let Common { git_dir, id_path } = args.common; + let drop_ref: Refname = REF_IT_PATCHES.parse().unwrap(); + + let repo = git::repo::open_or_init( + git_dir, + git::repo::InitOpts { + bare: !args.no_bare, + description: "`it` drop", + initial_head: &drop_ref, + }, + )?; + + let mut tx = refs::Transaction::new(&repo)?; + let drop_ref = tx.lock_ref(drop_ref)?; + ensure!( + if_not_found_none(repo.refname_to_id(drop_ref.name()))?.is_none(), + "{} already exists", + drop_ref + ); + + let id_path = id_path.open_git(); + git::add_alternates(&repo, &id_path)?; + + let cfg = repo.config()?.snapshot()?; + let mut signer = cfg::signer(&cfg, ui::askpass)?; + let signer_id = { + let iid = + cfg::git::identity(&cfg)?.ok_or_else(|| anyhow!("signer identity not in gitconfig"))?; + let id = find_id(&repo, &id_path, &iid)?; + let keyid = metadata::KeyId::from(signer.ident()); + ensure!( + id.signed.keys.contains_key(&keyid), + "signing key {keyid} is not in identity {iid}" + ); + + iid + }; + + let default = { + let default_role = metadata::drop::Role { + ids: [signer_id].into(), + threshold: NonZeroUsize::new(1).unwrap(), + }; + let default_branch = cfg::git::default_branch(&cfg)?; + + metadata::Drop { + spec_version: crate::SPEC_VERSION, + description: args.description, + prev: None, + custom: Default::default(), + roles: metadata::drop::Roles { + root: default_role.clone(), + snapshot: default_role.clone(), + mirrors: default_role.clone(), + branches: [( + default_branch, + metadata::drop::Annotated { + role: default_role, + description: metadata::drop::Description::try_from( + "the default branch".to_owned(), + ) + .unwrap(), + }, + )] + .into(), + }, + } + }; + let meta: metadata::Drop = edit_metadata(Editable::from(default))?.try_into()?; + ensure!( + meta.roles.root.ids.contains(&signer_id), + "signing identity {signer_id} is lacking the drop role required to sign the metadata" + ); + let signed = Metadata::drop(&meta).sign(iter::once(&mut signer))?; + + let mut root = repo.treebuilder(None)?; + let mut ids = repo.treebuilder(None)?; + let identities = meta + .roles + .ids() + .into_iter() + .map(|id| find_id(&repo, &id_path, &id).map(|signed| (id, signed))) + .collect::, _>>()?; + for (iid, id) in identities { + let iid = iid.to_string(); + let mut tb = repo.treebuilder(None)?; + metadata::identity::fold_to_tree(&repo, &mut tb, id)?; + ids.insert(&iid, tb.write()?, git2::FileMode::Tree.into())?; + } + root.insert("ids", ids.write()?, git2::FileMode::Tree.into())?; + root.insert( + META_FILE_DROP, + json::to_blob(&repo, &signed)?, + git2::FileMode::Blob.into(), + )?; + let tree = repo.find_tree(root.write()?)?; + let msg = format!("Create drop '{}'", meta.description); + let commit = git::commit_signed(&mut signer, &repo, msg, &tree, &[])?; + + if repo.is_bare() { + // Arrange refs to be `git-clone`-friendly + let heads_patches = tx.lock_ref(REF_HEADS_PATCHES.parse()?)?; + heads_patches.set_target(commit, "it: create"); + drop_ref.set_symbolic_target(heads_patches.name().clone(), String::new()); + repo.set_head(heads_patches.name())?; + } else { + drop_ref.set_target(commit, "it: create"); + } + + tx.commit()?; + + Ok(Output { + repo: repo.path().to_owned(), + refname: drop_ref.into(), + commit, + }) +} -- cgit v1.2.3