Show More
@@ -233,9 +233,14 b" impl<'changelog> ChangelogRevisionData<'" | |||||
233 | &self.bytes[self.user_end + 1..self.timestamp_end] |
|
233 | &self.bytes[self.user_end + 1..self.timestamp_end] | |
234 | } |
|
234 | } | |
235 |
|
235 | |||
236 |
/// Parsed timestamp |
|
236 | /// Parsed timestamp. | |
237 |
pub fn |
|
237 | pub fn timestamp(&self) -> Result<DateTime<FixedOffset>, HgError> { | |
238 |
|
|
238 | parse_timestamp(self.timestamp_line()) | |
|
239 | } | |||
|
240 | ||||
|
241 | /// Optional commit extras. | |||
|
242 | pub fn extra(&self) -> Result<BTreeMap<String, Vec<u8>>, HgError> { | |||
|
243 | parse_timestamp_line_extra(self.timestamp_line()) | |||
239 | } |
|
244 | } | |
240 |
|
245 | |||
241 | /// The files changed in this revision. |
|
246 | /// The files changed in this revision. | |
@@ -295,33 +300,27 b' fn debug_bytes(bytes: &[u8]) -> String {' | |||||
295 | .to_string() |
|
300 | .to_string() | |
296 | } |
|
301 | } | |
297 |
|
302 | |||
298 | /// Parsed timestamp line, including the timestamp and optional extras. |
|
|||
299 | #[derive(Clone, Debug)] |
|
|||
300 | pub struct TimestampAndExtra { |
|
|||
301 | pub timestamp: DateTime<FixedOffset>, |
|
|||
302 | pub extra: BTreeMap<String, Vec<u8>>, |
|
|||
303 | } |
|
|||
304 |
|
||||
305 | impl TimestampAndExtra { |
|
|||
306 |
|
|
303 | /// Parse the raw bytes of the timestamp line from a changelog entry. | |
307 |
|
|
304 | /// | |
308 |
|
|
305 | /// According to the documentation in `hg help dates` and the | |
309 |
|
|
306 | /// implementation in `changelog.py`, the format of the timestamp line | |
310 |
|
|
307 | /// is `time tz extra\n` where: | |
311 |
|
|
308 | /// | |
312 |
|
|
309 | /// - `time` is an ASCII-encoded signed int or float denoting a UTC timestamp | |
313 |
|
|
310 | /// as seconds since the UNIX epoch. | |
314 |
|
|
311 | /// | |
315 |
|
|
312 | /// - `tz` is the timezone offset as an ASCII-encoded signed integer denoting | |
316 |
|
|
313 | /// seconds WEST of UTC (so negative for timezones east of UTC, which is the | |
317 |
|
|
314 | /// opposite of the sign in ISO 8601 timestamps). | |
318 |
|
|
315 | /// | |
319 |
|
|
316 | /// - `extra` is an optional set of NUL-delimited key-value pairs, with the key | |
320 |
|
|
317 | /// and value in each pair separated by an ASCII colon. Keys are limited to | |
321 |
|
|
318 | /// ASCII letters, digits, hyphens, and underscores, whereas values can be | |
322 |
|
|
319 | /// arbitrary bytes. | |
323 | fn from_bytes(line: &[u8]) -> Result<Self, HgError> { |
|
320 | fn parse_timestamp( | |
324 | let mut parts = line.splitn(3, |c| *c == b' '); |
|
321 | timestamp_line: &[u8], | |
|
322 | ) -> Result<DateTime<FixedOffset>, HgError> { | |||
|
323 | let mut parts = timestamp_line.splitn(3, |c| *c == b' '); | |||
325 |
|
324 | |||
326 |
|
|
325 | let timestamp_bytes = parts | |
327 |
|
|
326 | .next() | |
@@ -357,21 +356,10 b' impl TimestampAndExtra {' | |||||
357 |
|
|
356 | .map_err(|e| { | |
358 |
|
|
357 | HgError::corrupted(format!("timezone is not an integer: {e}")) | |
359 |
|
|
358 | })?; | |
360 | let timezone = |
|
359 | let timezone = FixedOffset::west_opt(timezone_secs) | |
361 | FixedOffset::west_opt(timezone_secs).ok_or_else(|| { |
|
360 | .ok_or_else(|| HgError::corrupted("timezone offset out of bounds"))?; | |
362 | HgError::corrupted("timezone offset out of bounds") |
|
|||
363 | })?; |
|
|||
364 |
|
361 | |||
365 | let timestamp = |
|
362 | Ok(DateTime::from_naive_utc_and_offset(timestamp_utc, timezone)) | |
366 | DateTime::from_naive_utc_and_offset(timestamp_utc, timezone); |
|
|||
367 | let extra = parts |
|
|||
368 | .next() |
|
|||
369 | .map(parse_extra) |
|
|||
370 | .transpose()? |
|
|||
371 | .unwrap_or_default(); |
|
|||
372 |
|
||||
373 | Ok(Self { timestamp, extra }) |
|
|||
374 | } |
|
|||
375 | } |
|
363 | } | |
376 |
|
364 | |||
377 | /// Attempt to parse the given string as floating-point timestamp, and |
|
365 | /// Attempt to parse the given string as floating-point timestamp, and | |
@@ -413,12 +401,12 b' fn parse_float_timestamp(' | |||||
413 | }) |
|
401 | }) | |
414 | } |
|
402 | } | |
415 |
|
403 | |||
416 | /// Parse the "extra" fields from a changeset's timestamp line. |
|
404 | /// Decode changeset extra fields. | |
417 | /// |
|
405 | /// | |
418 | /// Extras are null-delimited key-value pairs where the key consists of ASCII |
|
406 | /// Extras are null-delimited key-value pairs where the key consists of ASCII | |
419 | /// alphanumeric characters plus hyphens and underscores, and the value can |
|
407 | /// alphanumeric characters plus hyphens and underscores, and the value can | |
420 | /// contain arbitrary bytes. |
|
408 | /// contain arbitrary bytes. | |
421 |
fn |
|
409 | fn decode_extra(extra: &[u8]) -> Result<BTreeMap<String, Vec<u8>>, HgError> { | |
422 | extra |
|
410 | extra | |
423 | .split(|c| *c == b'\0') |
|
411 | .split(|c| *c == b'\0') | |
424 | .map(|pair| { |
|
412 | .map(|pair| { | |
@@ -456,6 +444,18 b' fn parse_extra(extra: &[u8]) -> Result<B' | |||||
456 | .collect() |
|
444 | .collect() | |
457 | } |
|
445 | } | |
458 |
|
446 | |||
|
447 | /// Parse the extra fields from a changeset's timestamp line. | |||
|
448 | fn parse_timestamp_line_extra( | |||
|
449 | timestamp_line: &[u8], | |||
|
450 | ) -> Result<BTreeMap<String, Vec<u8>>, HgError> { | |||
|
451 | Ok(timestamp_line | |||
|
452 | .splitn(3, |c| *c == b' ') | |||
|
453 | .nth(2) | |||
|
454 | .map(decode_extra) | |||
|
455 | .transpose()? | |||
|
456 | .unwrap_or_default()) | |||
|
457 | } | |||
|
458 | ||||
459 | /// Decode Mercurial's escaping for changelog extras. |
|
459 | /// Decode Mercurial's escaping for changelog extras. | |
460 | /// |
|
460 | /// | |
461 | /// The `_string_escape` function in `changelog.py` only escapes 4 characters |
|
461 | /// The `_string_escape` function in `changelog.py` only escapes 4 characters | |
@@ -681,7 +681,7 b' message",' | |||||
681 | } |
|
681 | } | |
682 |
|
682 | |||
683 | #[test] |
|
683 | #[test] | |
684 |
fn test_ |
|
684 | fn test_decode_extra() { | |
685 | let extra = [ |
|
685 | let extra = [ | |
686 | ("branch".into(), b"default".to_vec()), |
|
686 | ("branch".into(), b"default".to_vec()), | |
687 | ("key-with-hyphens".into(), b"value1".to_vec()), |
|
687 | ("key-with-hyphens".into(), b"value1".to_vec()), | |
@@ -693,9 +693,9 b' message",' | |||||
693 | .collect::<BTreeMap<String, Vec<u8>>>(); |
|
693 | .collect::<BTreeMap<String, Vec<u8>>>(); | |
694 |
|
694 | |||
695 | let encoded = encode_extra(&extra); |
|
695 | let encoded = encode_extra(&extra); | |
696 |
let |
|
696 | let decoded = decode_extra(&encoded).unwrap(); | |
697 |
|
697 | |||
698 |
assert_eq!(extra, |
|
698 | assert_eq!(extra, decoded); | |
699 | } |
|
699 | } | |
700 |
|
700 | |||
701 | #[test] |
|
701 | #[test] | |
@@ -713,7 +713,7 b' message",' | |||||
713 |
|
713 | |||
714 | for (extra, msg) in test_cases { |
|
714 | for (extra, msg) in test_cases { | |
715 | assert!( |
|
715 | assert!( | |
716 |
|
|
716 | decode_extra(&extra).is_err(), | |
717 | "corrupt extra should have failed to parse: {}", |
|
717 | "corrupt extra should have failed to parse: {}", | |
718 | msg |
|
718 | msg | |
719 | ); |
|
719 | ); | |
@@ -735,12 +735,10 b' message",' | |||||
735 | let mut line: Vec<u8> = b"1115154970 28800 ".to_vec(); |
|
735 | let mut line: Vec<u8> = b"1115154970 28800 ".to_vec(); | |
736 | line.extend_from_slice(&encode_extra(&extra)); |
|
736 | line.extend_from_slice(&encode_extra(&extra)); | |
737 |
|
737 | |||
738 |
let p |
|
738 | let timestamp = parse_timestamp(&line).unwrap(); | |
|
739 | assert_eq!(×tamp.to_rfc3339(), "2005-05-03T13:16:10-08:00"); | |||
739 |
|
740 | |||
740 | assert_eq!( |
|
741 | let parsed_extra = parse_timestamp_line_extra(&line).unwrap(); | |
741 | &parsed.timestamp.to_rfc3339(), |
|
742 | assert_eq!(extra, parsed_extra); | |
742 | "2005-05-03T13:16:10-08:00" |
|
|||
743 | ); |
|
|||
744 | assert_eq!(extra, parsed.extra); |
|
|||
745 | } |
|
743 | } | |
746 | } |
|
744 | } |
General Comments 0
You need to be logged in to leave comments.
Login now