1 use std::path::PathBuf;
2 
3 use super::{make_changelist, parse_changelist};
4 use anyhow::{anyhow, bail};
5 use clap::Parser;
6 use libpijul::changestore::ChangeStore;
7 use libpijul::*;
8 use log::debug;
9 
10 use crate::repository::Repository;
11 
12 #[derive(Parser, Debug)]
13 pub struct Unrecord {
14     /// Set the repository where this command should run. Defaults to the first ancestor of the current directory that contains a `.pijul` directory.
15     #[clap(long = "repository")]
16     repo_path: Option<PathBuf>,
17     /// Unrecord changes from this channel instead of the current channel
18     #[clap(long = "channel")]
19     channel: Option<String>,
20     /// Also undo the changes in the working copy (preserving unrecorded changes if there are any)
21     #[clap(long = "reset")]
22     reset: bool,
23     /// Show N changes in a text editor if no <change-id>s were given.
24     /// Defaults to the value
25     /// of `unrecord_changes` in your global configuration.
26     #[clap(long = "show-changes", value_name = "N", conflicts_with("change-id"))]
27     show_changes: Option<usize>,
28     /// The hash of a change (unambiguous prefixes are accepted)
29     change_id: Vec<String>,
30 }
31 
32 impl Unrecord {
run(self) -> Result<(), anyhow::Error>33     pub fn run(self) -> Result<(), anyhow::Error> {
34         let mut repo = Repository::find_root(self.repo_path)?;
35         debug!("{:?}", repo.config);
36         let txn = repo.pristine.arc_txn_begin()?;
37         let cur = txn
38             .read()
39             .current_channel()
40             .unwrap_or(crate::DEFAULT_CHANNEL)
41             .to_string();
42         let channel_name = if let Some(ref c) = self.channel {
43             c
44         } else {
45             cur.as_str()
46         };
47         let is_current_channel = cur == channel_name;
48         let channel = if let Some(channel) = txn.read().load_channel(&channel_name)? {
49             channel
50         } else {
51             bail!("No such channel: {:?}", channel_name);
52         };
53         let mut hashes = Vec::new();
54 
55         if self.change_id.is_empty() {
56             // No change ids were given, present a list for choosing
57             // The number can be set in the global config or passed as a command-line option
58             let number_of_changes = if let Some(n) = self.show_changes {
59                 n
60             } else {
61                 let (cfg, _) = crate::config::Global::load()?;
62                 cfg.unrecord_changes.ok_or_else(|| {
63                     anyhow!(
64                         "Can't determine how many changes to show. \
65                          Please set the `unrecord_changes` option in \
66                          your global config or run `pijul unrecord` \
67                          with the `--show-changes` option."
68                     )
69                 })?
70             };
71             let txn = txn.read();
72             let hashes_ = txn
73                 .reverse_log(&*channel.read(), None)?
74                 .map(|h| (h.unwrap().1).0.into())
75                 .take(number_of_changes)
76                 .collect::<Vec<_>>();
77             let o = make_changelist(&repo.changes, &hashes_, "unrecord")?;
78             for h in parse_changelist(&edit::edit_bytes(&o[..])?).iter() {
79                 hashes.push((*h, *txn.get_internal(&h.into())?.unwrap()))
80             }
81         } else {
82             let txn = txn.read();
83             for c in self.change_id.iter() {
84                 let (hash, cid) = txn.hash_from_prefix(c)?;
85                 hashes.push((hash, cid))
86             }
87         };
88         let channel_ = channel.read();
89         let mut changes: Vec<(Hash, ChangeId, Option<u64>)> = Vec::new();
90         {
91             let txn = txn.read();
92             for (hash, change_id) in hashes {
93                 let n = txn
94                     .get_changeset(txn.changes(&channel_), &change_id)
95                     .unwrap();
96                 if n.is_none() {
97                     bail!("Change not in channel: {:?}", hash)
98                 }
99                 changes.push((hash, change_id, n.map(|&x| x.into())));
100             }
101         }
102         debug!("changes: {:?}", changes);
103         std::mem::drop(channel_);
104         let pending_hash = if self.reset {
105             super::pending(txn.clone(), &channel, &mut repo)?
106         } else {
107             None
108         };
109         changes.sort_by(|a, b| b.2.cmp(&a.2));
110         for (hash, change_id, _) in changes {
111             let channel_ = channel.read();
112             let txn_ = txn.read();
113             for p in txn_.iter_revdep(&change_id)? {
114                 let (p, d) = p?;
115                 if p < &change_id {
116                     continue;
117                 } else if p > &change_id {
118                     break;
119                 }
120                 if txn_.get_changeset(txn_.changes(&channel_), d)?.is_some() {
121                     let dep: Hash = txn_.get_external(d)?.unwrap().into();
122                     if Some(dep) == pending_hash {
123                         bail!(
124                             "Cannot unrecord change {} because unrecorded changes depend on it",
125                             hash.to_base32()
126                         );
127                     } else {
128                         bail!(
129                             "Cannot unrecord change {} because {} depend on it",
130                             hash.to_base32(),
131                             dep.to_base32()
132                         );
133                     }
134                 }
135             }
136             std::mem::drop(channel_);
137             std::mem::drop(txn_);
138             txn.write().unrecord(&repo.changes, &channel, &hash, 0)?;
139         }
140 
141         if self.reset && is_current_channel {
142             libpijul::output::output_repository_no_pending(
143                 &repo.working_copy,
144                 &repo.changes,
145                 &txn,
146                 &channel,
147                 "",
148                 true,
149                 None,
150                 num_cpus::get(),
151                 0,
152             )?;
153         }
154         if let Some(h) = pending_hash {
155             txn.write().unrecord(&repo.changes, &channel, &h, 0)?;
156             if cfg!(feature = "keep-changes") {
157                 repo.changes.del_change(&h)?;
158             }
159         }
160         txn.commit()?;
161         Ok(())
162     }
163 }
164