##// END OF EJS Templates
rust-changelog: added a test for `NULL_REVISION` special case...
Georges Racinet -
r51267:b5dd6d6d default
parent child Browse files
Show More
@@ -1,271 +1,289
1 1 use crate::errors::HgError;
2 2 use crate::revlog::Revision;
3 3 use crate::revlog::{Node, NodePrefix};
4 4 use crate::revlog::{Revlog, RevlogEntry, RevlogError};
5 5 use crate::utils::hg_path::HgPath;
6 6 use crate::vfs::Vfs;
7 7 use itertools::Itertools;
8 8 use std::ascii::escape_default;
9 9 use std::borrow::Cow;
10 10 use std::fmt::{Debug, Formatter};
11 11
12 12 /// A specialized `Revlog` to work with changelog data format.
13 13 pub struct Changelog {
14 14 /// The generic `revlog` format.
15 15 pub(crate) revlog: Revlog,
16 16 }
17 17
18 18 impl Changelog {
19 19 /// Open the `changelog` of a repository given by its root.
20 20 pub fn open(store_vfs: &Vfs, use_nodemap: bool) -> Result<Self, HgError> {
21 21 let revlog =
22 22 Revlog::open(store_vfs, "00changelog.i", None, use_nodemap)?;
23 23 Ok(Self { revlog })
24 24 }
25 25
26 26 /// Return the `ChangelogRevisionData` for the given node ID.
27 27 pub fn data_for_node(
28 28 &self,
29 29 node: NodePrefix,
30 30 ) -> Result<ChangelogRevisionData, RevlogError> {
31 31 let rev = self.revlog.rev_from_node(node)?;
32 32 self.data_for_rev(rev)
33 33 }
34 34
35 35 /// Return the `RevlogEntry` for the given revision number.
36 36 pub fn entry_for_rev(
37 37 &self,
38 38 rev: Revision,
39 39 ) -> Result<RevlogEntry, RevlogError> {
40 40 self.revlog.get_entry(rev)
41 41 }
42 42
43 43 /// Return the [`ChangelogRevisionData`] for the given revision number.
44 44 pub fn data_for_rev(
45 45 &self,
46 46 rev: Revision,
47 47 ) -> Result<ChangelogRevisionData, RevlogError> {
48 48 let bytes = self.revlog.get_rev_data(rev)?;
49 49 if bytes.is_empty() {
50 50 Ok(ChangelogRevisionData::null())
51 51 } else {
52 52 Ok(ChangelogRevisionData::new(bytes).map_err(|err| {
53 53 RevlogError::Other(HgError::CorruptedRepository(format!(
54 54 "Invalid changelog data for revision {}: {:?}",
55 55 rev, err
56 56 )))
57 57 })?)
58 58 }
59 59 }
60 60
61 61 pub fn node_from_rev(&self, rev: Revision) -> Option<&Node> {
62 62 self.revlog.node_from_rev(rev)
63 63 }
64 64
65 65 pub fn rev_from_node(
66 66 &self,
67 67 node: NodePrefix,
68 68 ) -> Result<Revision, RevlogError> {
69 69 self.revlog.rev_from_node(node)
70 70 }
71 71 }
72 72
73 73 /// `Changelog` entry which knows how to interpret the `changelog` data bytes.
74 74 #[derive(PartialEq)]
75 75 pub struct ChangelogRevisionData<'changelog> {
76 76 /// The data bytes of the `changelog` entry.
77 77 bytes: Cow<'changelog, [u8]>,
78 78 /// The end offset for the hex manifest (not including the newline)
79 79 manifest_end: usize,
80 80 /// The end offset for the user+email (not including the newline)
81 81 user_end: usize,
82 82 /// The end offset for the timestamp+timezone+extras (not including the
83 83 /// newline)
84 84 timestamp_end: usize,
85 85 /// The end offset for the file list (not including the newline)
86 86 files_end: usize,
87 87 }
88 88
89 89 impl<'changelog> ChangelogRevisionData<'changelog> {
90 90 fn new(bytes: Cow<'changelog, [u8]>) -> Result<Self, HgError> {
91 91 let mut line_iter = bytes.split(|b| b == &b'\n');
92 92 let manifest_end = line_iter
93 93 .next()
94 94 .expect("Empty iterator from split()?")
95 95 .len();
96 96 let user_slice = line_iter.next().ok_or_else(|| {
97 97 HgError::corrupted("Changeset data truncated after manifest line")
98 98 })?;
99 99 let user_end = manifest_end + 1 + user_slice.len();
100 100 let timestamp_slice = line_iter.next().ok_or_else(|| {
101 101 HgError::corrupted("Changeset data truncated after user line")
102 102 })?;
103 103 let timestamp_end = user_end + 1 + timestamp_slice.len();
104 104 let mut files_end = timestamp_end + 1;
105 105 loop {
106 106 let line = line_iter.next().ok_or_else(|| {
107 107 HgError::corrupted("Changeset data truncated in files list")
108 108 })?;
109 109 if line.is_empty() {
110 110 if files_end == bytes.len() {
111 111 // The list of files ended with a single newline (there
112 112 // should be two)
113 113 return Err(HgError::corrupted(
114 114 "Changeset data truncated after files list",
115 115 ));
116 116 }
117 117 files_end -= 1;
118 118 break;
119 119 }
120 120 files_end += line.len() + 1;
121 121 }
122 122
123 123 Ok(Self {
124 124 bytes,
125 125 manifest_end,
126 126 user_end,
127 127 timestamp_end,
128 128 files_end,
129 129 })
130 130 }
131 131
132 132 fn null() -> Self {
133 133 Self::new(Cow::Borrowed(
134 134 b"0000000000000000000000000000000000000000\n\n0 0\n\n",
135 135 ))
136 136 .unwrap()
137 137 }
138 138
139 139 /// Return an iterator over the lines of the entry.
140 140 pub fn lines(&self) -> impl Iterator<Item = &[u8]> {
141 141 self.bytes.split(|b| b == &b'\n')
142 142 }
143 143
144 144 /// Return the node id of the `manifest` referenced by this `changelog`
145 145 /// entry.
146 146 pub fn manifest_node(&self) -> Result<Node, HgError> {
147 147 let manifest_node_hex = &self.bytes[..self.manifest_end];
148 148 Node::from_hex_for_repo(manifest_node_hex)
149 149 }
150 150
151 151 /// The full user string (usually a name followed by an email enclosed in
152 152 /// angle brackets)
153 153 pub fn user(&self) -> &[u8] {
154 154 &self.bytes[self.manifest_end + 1..self.user_end]
155 155 }
156 156
157 157 /// The full timestamp line (timestamp in seconds, offset in seconds, and
158 158 /// possibly extras)
159 159 // TODO: We should expose this in a more useful way
160 160 pub fn timestamp_line(&self) -> &[u8] {
161 161 &self.bytes[self.user_end + 1..self.timestamp_end]
162 162 }
163 163
164 164 /// The files changed in this revision.
165 165 pub fn files(&self) -> impl Iterator<Item = &HgPath> {
166 166 self.bytes[self.timestamp_end + 1..self.files_end]
167 167 .split(|b| b == &b'\n')
168 168 .map(HgPath::new)
169 169 }
170 170
171 171 /// The change description.
172 172 pub fn description(&self) -> &[u8] {
173 173 &self.bytes[self.files_end + 2..]
174 174 }
175 175 }
176 176
177 177 impl Debug for ChangelogRevisionData<'_> {
178 178 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
179 179 f.debug_struct("ChangelogRevisionData")
180 180 .field("bytes", &debug_bytes(&self.bytes))
181 181 .field("manifest", &debug_bytes(&self.bytes[..self.manifest_end]))
182 182 .field(
183 183 "user",
184 184 &debug_bytes(
185 185 &self.bytes[self.manifest_end + 1..self.user_end],
186 186 ),
187 187 )
188 188 .field(
189 189 "timestamp",
190 190 &debug_bytes(
191 191 &self.bytes[self.user_end + 1..self.timestamp_end],
192 192 ),
193 193 )
194 194 .field(
195 195 "files",
196 196 &debug_bytes(
197 197 &self.bytes[self.timestamp_end + 1..self.files_end],
198 198 ),
199 199 )
200 200 .field(
201 201 "description",
202 202 &debug_bytes(&self.bytes[self.files_end + 2..]),
203 203 )
204 204 .finish()
205 205 }
206 206 }
207 207
208 208 fn debug_bytes(bytes: &[u8]) -> String {
209 209 String::from_utf8_lossy(
210 210 &bytes.iter().flat_map(|b| escape_default(*b)).collect_vec(),
211 211 )
212 212 .to_string()
213 213 }
214 214
215 215 #[cfg(test)]
216 216 mod tests {
217 217 use super::*;
218 use crate::vfs::Vfs;
219 use crate::NULL_REVISION;
218 220 use pretty_assertions::assert_eq;
219 221
220 222 #[test]
221 223 fn test_create_changelogrevisiondata_invalid() {
222 224 // Completely empty
223 225 assert!(ChangelogRevisionData::new(Cow::Borrowed(b"abcd")).is_err());
224 226 // No newline after manifest
225 227 assert!(ChangelogRevisionData::new(Cow::Borrowed(b"abcd")).is_err());
226 228 // No newline after user
227 229 assert!(ChangelogRevisionData::new(Cow::Borrowed(b"abcd\n")).is_err());
228 230 // No newline after timestamp
229 231 assert!(
230 232 ChangelogRevisionData::new(Cow::Borrowed(b"abcd\n\n0 0")).is_err()
231 233 );
232 234 // Missing newline after files
233 235 assert!(ChangelogRevisionData::new(Cow::Borrowed(
234 236 b"abcd\n\n0 0\nfile1\nfile2"
235 237 ))
236 238 .is_err(),);
237 239 // Only one newline after files
238 240 assert!(ChangelogRevisionData::new(Cow::Borrowed(
239 241 b"abcd\n\n0 0\nfile1\nfile2\n"
240 242 ))
241 243 .is_err(),);
242 244 }
243 245
244 246 #[test]
245 247 fn test_create_changelogrevisiondata() {
246 248 let data = ChangelogRevisionData::new(Cow::Borrowed(
247 249 b"0123456789abcdef0123456789abcdef01234567
248 250 Some One <someone@example.com>
249 251 0 0
250 252 file1
251 253 file2
252 254
253 255 some
254 256 commit
255 257 message",
256 258 ))
257 259 .unwrap();
258 260 assert_eq!(
259 261 data.manifest_node().unwrap(),
260 262 Node::from_hex("0123456789abcdef0123456789abcdef01234567")
261 263 .unwrap()
262 264 );
263 265 assert_eq!(data.user(), b"Some One <someone@example.com>");
264 266 assert_eq!(data.timestamp_line(), b"0 0");
265 267 assert_eq!(
266 268 data.files().collect_vec(),
267 269 vec![HgPath::new("file1"), HgPath::new("file2")]
268 270 );
269 271 assert_eq!(data.description(), b"some\ncommit\nmessage");
270 272 }
273
274 #[test]
275 fn test_data_from_rev_null() -> Result<(), RevlogError> {
276 // an empty revlog will be enough for this case
277 let temp = tempfile::tempdir().unwrap();
278 let vfs = Vfs { base: temp.path() };
279 std::fs::write(temp.path().join("foo.i"), b"").unwrap();
280 let revlog = Revlog::open(&vfs, "foo.i", None, false).unwrap();
281
282 let changelog = Changelog { revlog };
283 assert_eq!(
284 changelog.data_for_rev(NULL_REVISION)?,
285 ChangelogRevisionData::null()
286 );
287 Ok(())
288 }
271 289 }
General Comments 0
You need to be logged in to leave comments. Login now