1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
use eyre::{Result, bail};
use super::display_arg;
use crate::system::history::shadow::DiffOpts;
use crate::system::history::tracked::display_to_tree_path;
/// Compare checkpoints, or the working tree against one
///
/// Without arguments, shows what changed by hand since the latest
/// checkpoint. With one reference, shows what that checkpoint changed
/// against the one before it. With two, compares the two states.
#[derive(Debug, usage_rs::Args)]
#[usage(verbatim_doc_comment)]
pub(crate) struct HistoryDiff {
/// Numeric checkpoint ID, `latest`, `latest~N`, or `commit:<sha>`
#[usage(value_name = "A")]
a: Option<String>,
/// Compare `A` with this checkpoint instead of its predecessor
#[usage(value_name = "B")]
b: Option<String>,
/// Compare an operation with its recorded protective checkpoint
///
/// With no reference, use the newest operation, ignoring later saves.
/// With one reference, use that operation. Fails if its before checkpoint
/// is unavailable instead of comparing an unrelated preceding save.
#[usage(long)]
operation: bool,
/// Print the full patch instead of a per-file summary
#[usage(long, short)]
patch: bool,
/// Restrict to one path (a file or a directory)
#[usage(long, value_name = "PATH")]
path: Option<String>,
/// Exit 1 when the two sides differ
#[usage(long)]
exit_code: bool,
}
impl HistoryDiff {
pub(crate) async fn run(self) -> Result<()> {
let (store, tracked, entries) = super::open().await?;
let Some(repo) = store.repo() else {
bail!("comparing checkpoints requires git");
};
let path = self.path.as_deref().map(display_arg);
let (from, to, label) =
if self.operation {
if self.b.is_some() {
bail!("--operation takes at most one checkpoint reference");
}
let outcome = match &self.a {
Some(reference) => super::resolve(reference, &entries, None)?,
None => entries
.iter()
.rev()
.find(|entry| entry.checkpoint.operation.is_some())
.cloned()
.ok_or_else(|| eyre::eyre!("no recorded operation"))?,
};
let operation =
outcome.checkpoint.operation.as_ref().ok_or_else(|| {
eyre::eyre!("checkpoint {} is not an operation", outcome.id)
})?;
let before = operation
.before
.as_ref()
.and_then(|uuid| entries.iter().find(|entry| &entry.checkpoint.uuid == uuid))
.ok_or_else(|| {
eyre::eyre!("operation's protective checkpoint is unavailable or pruned")
})?;
(
tree_of(before)?,
tree_of(&outcome)?,
format!(
"operation {}: checkpoint {} -> {}",
outcome.id, before.id, outcome.id
),
)
} else {
match (&self.a, &self.b) {
(None, _) => {
let latest = super::resolve("latest", &entries, path.as_deref())?;
let tree = tree_of(&latest)?;
let current = crate::system::history::replay::live_tree(repo, &tracked)?;
(
tree,
current,
format!("checkpoint {} -> working tree", latest.id),
)
}
(Some(a), None) => {
let a = super::resolve(a, &entries, path.as_deref())?;
let previous = entries.iter().rev().find(|entry| {
entry.id < a.id && entry.checkpoint.tree.snapshot.is_some()
});
let previous = match previous {
Some(previous) => tree_of(previous)?,
None => repo.empty_object("tree")?,
};
(previous, tree_of(&a)?, format!("checkpoint {}", a.id))
}
(Some(a), Some(b)) => {
let a = super::resolve(a, &entries, path.as_deref())?;
let b = super::resolve(b, &entries, path.as_deref())?;
(
tree_of(&a)?,
tree_of(&b)?,
format!("checkpoint {} -> {}", a.id, b.id),
)
}
}
};
let paths = path
.as_deref()
.map(|path| -> Result<_> {
let local =
crate::system::history::tracked::normalize_target(std::path::Path::new(path));
let mapped = |tree: &str| -> Result<String> {
if let Some(manifest) =
crate::system::history::manifest::Manifest::read(repo, tree)?
{
let inventory = manifest.tracking()?;
if let Some(entry) = inventory.entry_for(&local) {
return entry.tree_path(&local);
}
}
Ok(display_to_tree_path(path))
};
Ok((mapped(&from)?, mapped(&to)?))
})
.transpose()?;
let result = repo.diff(
&from,
&to,
&DiffOpts {
patch: self.patch,
// a patch is written as git produces it; a summary is small
stream: self.patch,
color: console::colors_enabled(),
paths,
},
)?;
if result.changed {
if !result.output.is_empty() {
miseprint!("{}", String::from_utf8_lossy(&result.output))?;
}
} else {
info!("{label}: no differences");
}
if self.exit_code && result.changed {
return Err(crate::request_exit(1));
}
Ok(())
}
}
fn tree_of(entry: &crate::system::history::store::Entry) -> Result<String> {
entry
.checkpoint
.tree
.snapshot
.clone()
.ok_or_else(|| eyre::eyre!("checkpoint {} has no content snapshot", entry.id))
}