summaryrefslogtreecommitdiff
path: root/src/cmd/topic
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/topic')
-rw-r--r--src/cmd/topic/comment.rs68
-rw-r--r--src/cmd/topic/ls.rs32
-rw-r--r--src/cmd/topic/show.rs34
-rw-r--r--src/cmd/topic/unbundle.rs174
4 files changed, 308 insertions, 0 deletions
diff --git a/src/cmd/topic/comment.rs b/src/cmd/topic/comment.rs
new file mode 100644
index 0000000..121dabb
--- /dev/null
+++ b/src/cmd/topic/comment.rs
@@ -0,0 +1,68 @@
+// Copyright © 2022 Kim Altintop <kim@eagain.io>
+// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception
+
+use crate::{
+ cmd::{
+ self,
+ patch,
+ },
+ patches,
+};
+
+#[derive(Debug, clap::Subcommand)]
+pub enum Cmd {
+ /// Record the comment with a local drop history
+ Record(Record),
+ /// Submit the comment to a remote drop
+ Submit(Submit),
+}
+
+impl Cmd {
+ pub fn run(self) -> cmd::Result<cmd::Output> {
+ match self {
+ Self::Record(args) => record(args),
+ Self::Submit(args) => submit(args),
+ }
+ .map(cmd::IntoOutput::into_output)
+ }
+}
+
+#[derive(Debug, clap::Args)]
+pub struct Record {
+ #[clap(flatten)]
+ common: patch::Common,
+ #[clap(flatten)]
+ comment: patch::Comment,
+}
+
+#[derive(Debug, clap::Args)]
+pub struct Submit {
+ #[clap(flatten)]
+ common: patch::Common,
+ #[clap(flatten)]
+ comment: patch::Comment,
+ #[clap(flatten)]
+ remote: patch::Remote,
+}
+
+pub fn record(Record { common, comment }: Record) -> cmd::Result<patches::Record> {
+ patch::create(patch::Kind::Comment {
+ common,
+ remote: None,
+ comment,
+ })
+}
+
+pub fn submit(
+ Submit {
+ common,
+ comment,
+ remote,
+ }: Submit,
+) -> cmd::Result<patches::Record> {
+ patch::create(patch::Kind::Comment {
+ common,
+ remote: Some(remote),
+ comment,
+ })
+}
diff --git a/src/cmd/topic/ls.rs b/src/cmd/topic/ls.rs
new file mode 100644
index 0000000..430cc6e
--- /dev/null
+++ b/src/cmd/topic/ls.rs
@@ -0,0 +1,32 @@
+// Copyright © 2022 Kim Altintop <kim@eagain.io>
+// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception
+
+use crate::{
+ git,
+ patches::{
+ self,
+ Topic,
+ },
+};
+
+use super::Common;
+use crate::cmd;
+
+#[derive(Debug, clap::Args)]
+pub struct Ls {
+ #[clap(flatten)]
+ common: Common,
+}
+
+#[derive(serde::Serialize)]
+pub struct Output {
+ topic: Topic,
+ subject: String,
+}
+
+pub fn ls(args: Ls) -> cmd::Result<Vec<cmd::Result<Output>>> {
+ let repo = git::repo::open(&args.common.git_dir)?;
+ Ok(patches::iter::unbundled::topics_with_subject(&repo)
+ .map(|i| i.map(|(topic, subject)| Output { topic, subject }))
+ .collect())
+}
diff --git a/src/cmd/topic/show.rs b/src/cmd/topic/show.rs
new file mode 100644
index 0000000..1d19720
--- /dev/null
+++ b/src/cmd/topic/show.rs
@@ -0,0 +1,34 @@
+// Copyright © 2022 Kim Altintop <kim@eagain.io>
+// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception
+
+use super::Common;
+use crate::{
+ cmd,
+ git,
+ patches::{
+ self,
+ iter::Note,
+ Topic,
+ },
+};
+
+#[derive(Debug, clap::Args)]
+pub struct Show {
+ #[clap(flatten)]
+ common: Common,
+ /// Traverse the topic in reverse order, ie. oldest first
+ #[clap(long, value_parser)]
+ reverse: bool,
+ #[clap(value_parser)]
+ topic: Topic,
+}
+
+pub fn show(args: Show) -> cmd::Result<Vec<cmd::Result<Note>>> {
+ let repo = git::repo::open(&args.common.git_dir)?;
+ let iter = patches::iter::topic(&repo, &args.topic);
+ if args.reverse {
+ Ok(iter.rev().collect())
+ } else {
+ Ok(iter.collect())
+ }
+}
diff --git a/src/cmd/topic/unbundle.rs b/src/cmd/topic/unbundle.rs
new file mode 100644
index 0000000..3aab54b
--- /dev/null
+++ b/src/cmd/topic/unbundle.rs
@@ -0,0 +1,174 @@
+// Copyright © 2022 Kim Altintop <kim@eagain.io>
+// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception
+
+use std::{
+ collections::{
+ BTreeMap,
+ BTreeSet,
+ },
+ path::PathBuf,
+};
+
+use anyhow::anyhow;
+use clap::ValueHint;
+
+use super::Common;
+use crate::{
+ cmd::{
+ self,
+ ui::{
+ debug,
+ info,
+ warn,
+ },
+ Aborted,
+ },
+ git::{
+ self,
+ if_not_found_none,
+ refs,
+ Refname,
+ },
+ metadata::{
+ self,
+ git::FromGit,
+ },
+ patches::{
+ self,
+ iter::dropped,
+ Bundle,
+ Record,
+ Topic,
+ REF_IT_BUNDLES,
+ REF_IT_PATCHES,
+ TOPIC_MERGES,
+ TOPIC_SNAPSHOTS,
+ },
+ paths,
+};
+
+// TODO:
+//
+// - don't require patch bundle to be present on-disk when snapshots would do
+
+#[derive(Debug, clap::Args)]
+pub struct Unbundle {
+ #[clap(flatten)]
+ common: Common,
+ /// The directory where to write the bundle to
+ ///
+ /// Unless this is an absolute path, it is treated as relative to $GIT_DIR.
+ #[clap(
+ long,
+ value_parser,
+ value_name = "DIR",
+ default_value_os_t = paths::bundles().to_owned(),
+ value_hint = ValueHint::DirPath,
+ )]
+ bundle_dir: PathBuf,
+ /// The topic to unbundle
+ #[clap(value_parser)]
+ topic: Topic,
+ /// The drop history to find the topic in
+ #[clap(value_parser)]
+ drop: Option<String>,
+}
+
+#[derive(serde::Serialize)]
+pub struct Output {
+ updated: BTreeMap<Refname, git::serde::oid::Oid>,
+}
+
+pub fn unbundle(args: Unbundle) -> cmd::Result<Output> {
+ let repo = git::repo::open(&args.common.git_dir)?;
+ let bundle_dir = if args.bundle_dir.is_relative() {
+ repo.path().join(args.bundle_dir)
+ } else {
+ args.bundle_dir
+ };
+ let drop = match args.drop {
+ Some(rev) => if_not_found_none(repo.resolve_reference_from_short_name(&rev))?
+ .ok_or_else(|| anyhow!("no ref matching {rev} found"))?
+ .name()
+ .ok_or_else(|| anyhow!("invalid drop"))?
+ .to_owned(),
+ None => REF_IT_PATCHES.to_owned(),
+ };
+
+ let filter = [&args.topic, &TOPIC_MERGES, &TOPIC_SNAPSHOTS];
+ let mut on_topic: Vec<Record> = Vec::new();
+ let mut checkpoints: Vec<Record> = Vec::new();
+ for row in dropped::topics(&repo, &drop) {
+ let (t, id) = row?;
+
+ if filter.into_iter().any(|f| f == &t) {
+ let commit = repo.find_commit(id)?;
+ let record = Record::from_commit(&repo, &commit)?;
+ if t == args.topic {
+ on_topic.push(record);
+ continue;
+ }
+
+ // Skip checkpoint which came after the most recent record on the topic
+ if !on_topic.is_empty() {
+ checkpoints.push(record);
+ }
+ }
+ }
+
+ let odb = repo.odb()?;
+
+ info!("Indexing checkpoints...");
+ for rec in checkpoints.into_iter().rev() {
+ Bundle::from_stored(&bundle_dir, rec.bundle_info().as_expect())?
+ .packdata()?
+ .index(&odb)?
+ }
+
+ let mut missing = BTreeSet::new();
+ for oid in on_topic
+ .iter()
+ .flat_map(|rec| &rec.bundle_info().prerequisites)
+ {
+ let oid = git2::Oid::try_from(oid)?;
+ if !odb.exists(oid) {
+ missing.insert(oid);
+ }
+ }
+
+ if !missing.is_empty() {
+ warn!("Unable to satisfy all prerequisites");
+ info!("The following prerequisite commits are missing:\n");
+ for oid in missing {
+ info!("{oid}");
+ }
+ info!("\nYou may try to unbundle the entire drop history");
+ cmd::abort!();
+ }
+
+ info!("Unbundling topic records...");
+ let mut tx = refs::Transaction::new(&repo)?;
+ let topic_ref = tx.lock_ref(args.topic.as_refname())?;
+ let mut up = BTreeMap::new();
+ for rec in on_topic.into_iter().rev() {
+ let hash = rec.bundle_hash();
+ let bundle = Bundle::from_stored(&bundle_dir, rec.bundle_info().as_expect())?;
+ if bundle.is_encrypted() {
+ warn!("Skipping encrypted bundle {hash}");
+ continue;
+ }
+ bundle.packdata()?.index(&odb)?;
+ debug!("{hash}: unbundle");
+ let updated = patches::unbundle(&odb, &mut tx, REF_IT_BUNDLES, &rec)?;
+ for (name, oid) in updated {
+ up.insert(name, oid.into());
+ }
+ debug!("{hash}: merge notes");
+ let submitter = metadata::Identity::from_content_hash(&repo, &rec.meta.signature.signer)?
+ .verified(metadata::git::find_parent(&repo))?;
+ patches::merge_notes(&repo, &submitter, &topic_ref, &rec)?;
+ }
+ tx.commit()?;
+
+ Ok(Output { updated: up })
+}