Skip to main content

gix_ref/store/file/log/
line.rs

1use gix_hash::ObjectId;
2
3use crate::{log::Line, store_impl::file::log::LineRef};
4
5impl LineRef<'_> {
6    /// Convert this instance into its mutable counterpart
7    pub fn to_owned(&self) -> Line {
8        (*self).into()
9    }
10}
11
12mod write {
13    use std::io;
14
15    use gix_object::bstr::{BStr, ByteSlice};
16
17    use crate::log::Line;
18
19    /// The Error produced by [`Line::write_to()`] (but wrapped in an io error).
20    #[derive(Debug, thiserror::Error)]
21    #[allow(missing_docs)]
22    enum Error {
23        #[error(r"Messages must not contain newlines (\n)")]
24        IllegalCharacter,
25    }
26
27    impl From<Error> for io::Error {
28        fn from(err: Error) -> Self {
29            io::Error::other(err)
30        }
31    }
32
33    /// Output
34    impl Line {
35        /// Serialize this instance to `out` in the git serialization format for ref log lines.
36        pub fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> {
37            write!(out, "{} {} ", self.previous_oid, self.new_oid)?;
38            self.signature.write_to(out)?;
39            writeln!(out, "\t{}", check_newlines(self.message.as_ref())?)
40        }
41    }
42
43    fn check_newlines(input: &BStr) -> Result<&BStr, Error> {
44        if input.find_byte(b'\n').is_some() {
45            return Err(Error::IllegalCharacter);
46        }
47        Ok(input)
48    }
49}
50
51impl LineRef<'_> {
52    /// The previous object id of the ref. It will be a null hash if there was no previous id as
53    /// this ref is being created.
54    pub fn previous_oid(&self) -> ObjectId {
55        ObjectId::from_hex(self.previous_oid).expect("parse validation")
56    }
57    /// The new object id of the ref, or a null hash if it is removed.
58    pub fn new_oid(&self) -> ObjectId {
59        ObjectId::from_hex(self.new_oid).expect("parse validation")
60    }
61}
62
63impl<'a> From<LineRef<'a>> for Line {
64    fn from(v: LineRef<'a>) -> Self {
65        Line {
66            previous_oid: v.previous_oid(),
67            new_oid: v.new_oid(),
68            signature: v.signature.into(),
69            message: v.message.into(),
70        }
71    }
72}
73
74///
75pub mod decode {
76    use gix_object::bstr::{BStr, ByteSlice};
77
78    use crate::{file::log::LineRef, parse::hex_hash_any};
79
80    ///
81    mod error {
82        use gix_object::bstr::{BString, ByteSlice};
83
84        /// The error returned by [`from_bytes(…)`][super::Line::from_bytes()]
85        #[derive(Debug)]
86        pub struct Error {
87            pub input: BString,
88        }
89
90        impl std::fmt::Display for Error {
91            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92                write!(
93                    f,
94                    r"{:?} did not match '<old-hexsha> <new-hexsha> <name> <<email>> <timestamp> <tz>\t<message>'",
95                    self.input
96                )
97            }
98        }
99
100        impl std::error::Error for Error {}
101
102        impl Error {
103            pub(crate) fn new(input: &[u8]) -> Self {
104                Error {
105                    input: input.as_bstr().to_owned(),
106                }
107            }
108        }
109    }
110    pub use error::Error;
111
112    impl<'a> LineRef<'a> {
113        /// Decode a reflog line from the given bytes.
114        ///
115        /// Valid input starts with the previous object id, the new object id, a
116        /// signature, and an optional tab-separated message, for example:
117        ///
118        /// `0123456789012345678901234567890123456789 89abcdef89abcdef89abcdef89abcdef89abcdef Name <name@example.com> 1700000000 +0000\tmessage`
119        pub fn from_bytes(input: &'a [u8]) -> Result<LineRef<'a>, Error> {
120            decode(input).map_err(|_| Error::new(first_line(input)))
121        }
122    }
123
124    /// Return the first line from `input`, without its trailing newline.
125    ///
126    /// If `input` contains no newline, all of `input` is returned.
127    fn first_line(input: &[u8]) -> &[u8] {
128        let line_end = input.iter().position(|b| *b == b'\n').unwrap_or(input.len());
129        &input[..line_end]
130    }
131
132    /// Parse one reflog line from `bytes`.
133    ///
134    /// Only one line is parsed; any bytes after the first newline are
135    /// ignored. If the line has no tab separator, the message is empty.
136    ///
137    /// Return an error if the first line does not match the reflog line
138    /// format.
139    fn decode(bytes: &[u8]) -> Result<LineRef<'_>, ()> {
140        let line = first_line(bytes);
141        let (mut head, message) = match line.find_byte(b'\t') {
142            Some(tab) => (&line[..tab], line[tab + 1..].as_bstr()),
143            None => (line, BStr::new(b"")),
144        };
145
146        let old = hex_hash_any(&mut head)?;
147        head = head.strip_prefix(b" ").ok_or(())?;
148        let new = hex_hash_any(&mut head)?;
149        head = head.strip_prefix(b" ").ok_or(())?;
150        let signature = gix_actor::signature::decode(&mut head).map_err(|_| ())?;
151        if !head.is_empty() {
152            return Err(());
153        }
154        Ok(LineRef {
155            previous_oid: old,
156            new_oid: new,
157            signature,
158            message,
159        })
160    }
161
162    #[cfg(test)]
163    mod test_decode {
164        use super::*;
165
166        /// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_.
167        fn hex_to_oid(hex: &str) -> gix_hash::ObjectId {
168            gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
169        }
170
171        fn with_newline(mut v: Vec<u8>) -> Vec<u8> {
172            v.push(b'\n');
173            v
174        }
175
176        mod invalid {
177            use super::decode;
178
179            #[test]
180            fn completely_bogus_shows_error_with_context() {
181                let input = b"definitely not a log entry".as_slice();
182                decode(input).expect_err("this should fail");
183            }
184
185            #[test]
186            fn missing_whitespace_between_signature_and_message() {
187                let line = "0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000message";
188                decode(line.as_bytes()).expect_err("this should fail");
189            }
190        }
191
192        const NULL_SHA1: &[u8] = b"0000000000000000000000000000000000000000";
193
194        #[test]
195        fn entry_with_empty_message() {
196            let line_without_nl: Vec<_> = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 name <foo@example.com> 1234567890 -0000".to_vec();
197            let line_with_nl = with_newline(line_without_nl.clone());
198            for input in &[line_without_nl, line_with_nl] {
199                assert_eq!(
200                    decode(input.as_slice()).expect("successful parsing"),
201                    LineRef {
202                        previous_oid: NULL_SHA1.as_bstr(),
203                        new_oid: NULL_SHA1.as_bstr(),
204                        signature: gix_actor::SignatureRef {
205                            name: b"name".as_bstr(),
206                            email: b"foo@example.com".as_bstr(),
207                            time: "1234567890 -0000"
208                        },
209                        message: b"".as_bstr(),
210                    }
211                );
212            }
213        }
214
215        #[test]
216        fn entry_with_message_without_newline_and_with_newline() {
217            let line_without_nl: Vec<_> = b"a5828ae6b52137b913b978e16cd2334482eb4c1f 89b43f80a514aee58b662ad606e6352e03eaeee4 Sebastian Thiel <foo@example.com> 1618030561 +0800\tpull --ff-only: Fast-forward".to_vec();
218            let line_with_nl = with_newline(line_without_nl.clone());
219
220            for input in &[line_without_nl, line_with_nl] {
221                let res = decode(input.as_slice()).expect("successful parsing");
222                let actual = LineRef {
223                    previous_oid: b"a5828ae6b52137b913b978e16cd2334482eb4c1f".as_bstr(),
224                    new_oid: b"89b43f80a514aee58b662ad606e6352e03eaeee4".as_bstr(),
225                    signature: gix_actor::SignatureRef {
226                        name: b"Sebastian Thiel".as_bstr(),
227                        email: b"foo@example.com".as_bstr(),
228                        time: "1618030561 +0800",
229                    },
230                    message: b"pull --ff-only: Fast-forward".as_bstr(),
231                };
232                assert_eq!(res, actual);
233                assert_eq!(
234                    actual.previous_oid(),
235                    hex_to_oid("a5828ae6b52137b913b978e16cd2334482eb4c1f")
236                );
237                assert_eq!(actual.new_oid(), hex_to_oid("89b43f80a514aee58b662ad606e6352e03eaeee4"));
238            }
239        }
240
241        #[test]
242        fn two_lines_in_a_row_with_and_without_newline() {
243            let lines = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000\t\n0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 two <foo@example.com> 1234567890 -0000\thello";
244            let parsed = decode(lines.as_slice()).expect("parse single line");
245            assert_eq!(parsed.message, b"".as_bstr(), "first message is empty");
246        }
247    }
248}