git_ref/store/file/log/
line.rs

1use git_hash::ObjectId;
2
3use crate::{log::Line, store_impl::file::log::LineRef};
4
5impl<'a> LineRef<'a> {
6    /// Convert this instance into its mutable counterpart
7    pub fn to_owned(&self) -> Line {
8        self.clone().into()
9    }
10}
11
12mod write {
13    use std::io;
14
15    use git_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("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::new(io::ErrorKind::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, mut out: impl io::Write) -> io::Result<()> {
37            write!(out, "{} {} ", self.previous_oid, self.new_oid)?;
38            self.signature.write_to(&mut 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<'a> LineRef<'a> {
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 git_object::bstr::{BStr, ByteSlice};
77    use nom::{
78        bytes::complete::{tag, take_while},
79        combinator::opt,
80        error::{context, ContextError, ParseError},
81        sequence::{terminated, tuple},
82        IResult,
83    };
84
85    use crate::{file::log::LineRef, parse::hex_hash};
86
87    ///
88    mod error {
89        use git_object::bstr::{BString, ByteSlice};
90
91        /// The error returned by [from_bytes(…)][super::Line::from_bytes()]
92        #[derive(Debug)]
93        pub struct Error {
94            pub input: BString,
95        }
96
97        impl std::fmt::Display for Error {
98            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99                write!(
100                    f,
101                    "{:?} did not match '<old-hexsha> <new-hexsha> <name> <<email>> <timestamp> <tz>\\t<message>'",
102                    self.input
103                )
104            }
105        }
106
107        impl std::error::Error for Error {}
108
109        impl Error {
110            pub(crate) fn new(input: &[u8]) -> Self {
111                Error {
112                    input: input.as_bstr().to_owned(),
113                }
114            }
115        }
116    }
117    pub use error::Error;
118
119    impl<'a> LineRef<'a> {
120        /// Decode a line from the given bytes which are expected to start at a hex sha.
121        pub fn from_bytes(input: &'a [u8]) -> Result<LineRef<'a>, Error> {
122            one::<()>(input).map(|(_, l)| l).map_err(|_| Error::new(input))
123        }
124    }
125
126    fn message<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], &'a BStr, E> {
127        if i.is_empty() {
128            Ok((&[], i.as_bstr()))
129        } else {
130            terminated(take_while(|c| c != b'\n'), opt(tag(b"\n")))(i).map(|(i, o)| (i, o.as_bstr()))
131        }
132    }
133
134    fn one<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(bytes: &'a [u8]) -> IResult<&[u8], LineRef<'a>, E> {
135        let (i, (old, new, signature, message_sep, message)) = context(
136            "<old-hexsha> <new-hexsha> <name> <<email>> <timestamp> <tz>\\t<message>",
137            tuple((
138                context("<old-hexsha>", terminated(hex_hash, tag(b" "))),
139                context("<new-hexsha>", terminated(hex_hash, tag(b" "))),
140                context("<name> <<email>> <timestamp>", git_actor::signature::decode),
141                opt(tag(b"\t")),
142                context("<optional message>", message),
143            )),
144        )(bytes)?;
145
146        if message_sep.is_none() {
147            if let Some(first) = message.first() {
148                if !first.is_ascii_whitespace() {
149                    return Err(nom::Err::Error(E::add_context(
150                        i,
151                        "log message must be separated from signature with whitespace",
152                        E::from_error_kind(i, nom::error::ErrorKind::MapRes),
153                    )));
154                }
155            }
156        }
157
158        Ok((
159            i,
160            LineRef {
161                previous_oid: old,
162                new_oid: new,
163                signature,
164                message,
165            },
166        ))
167    }
168
169    #[cfg(test)]
170    mod test {
171        use git_actor::{Sign, Time};
172        use git_object::bstr::ByteSlice;
173
174        use super::*;
175
176        /// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_.
177        fn hex_to_oid(hex: &str) -> git_hash::ObjectId {
178            git_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
179        }
180
181        fn with_newline(mut v: Vec<u8>) -> Vec<u8> {
182            v.push(b'\n');
183            v
184        }
185
186        mod invalid {
187            use git_testtools::to_bstr_err;
188            use nom::error::VerboseError;
189
190            use super::one;
191
192            #[test]
193            fn completely_bogus_shows_error_with_context() {
194                let err = one::<VerboseError<&[u8]>>(b"definitely not a log entry")
195                    .map_err(to_bstr_err)
196                    .expect_err("this should fail");
197                assert!(err.to_string().contains("<old-hexsha> <new-hexsha>"));
198            }
199
200            #[test]
201            fn missing_whitespace_between_signature_and_message() {
202                let line = "0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000message";
203                let err = one::<VerboseError<&[u8]>>(line.as_bytes())
204                    .map_err(to_bstr_err)
205                    .expect_err("this should fail");
206                assert!(err
207                    .to_string()
208                    .contains("log message must be separated from signature with whitespace"));
209            }
210        }
211
212        const NULL_SHA1: &[u8] = b"0000000000000000000000000000000000000000";
213
214        #[test]
215        fn entry_with_empty_message() {
216            let line_without_nl: Vec<_> = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 name <foo@example.com> 1234567890 -0000".to_vec();
217            let line_with_nl = with_newline(line_without_nl.clone());
218            for input in &[line_without_nl, line_with_nl] {
219                assert_eq!(
220                    one::<nom::error::Error<_>>(input).expect("successful parsing").1,
221                    LineRef {
222                        previous_oid: NULL_SHA1.as_bstr(),
223                        new_oid: NULL_SHA1.as_bstr(),
224                        signature: git_actor::SignatureRef {
225                            name: b"name".as_bstr(),
226                            email: b"foo@example.com".as_bstr(),
227                            time: Time {
228                                seconds_since_unix_epoch: 1234567890,
229                                offset_in_seconds: 0,
230                                sign: Sign::Minus
231                            }
232                        },
233                        message: b"".as_bstr(),
234                    }
235                );
236            }
237        }
238
239        #[test]
240        fn entry_with_message_without_newline_and_with_newline() {
241            let line_without_nl: Vec<_> = b"a5828ae6b52137b913b978e16cd2334482eb4c1f 89b43f80a514aee58b662ad606e6352e03eaeee4 Sebastian Thiel <foo@example.com> 1618030561 +0800\tpull --ff-only: Fast-forward".to_vec();
242            let line_with_nl = with_newline(line_without_nl.clone());
243
244            for input in &[line_without_nl, line_with_nl] {
245                let (remaining, res) = one::<nom::error::Error<_>>(input).expect("successful parsing");
246                assert!(remaining.is_empty(), "all consuming even without trailing newline");
247                let actual = LineRef {
248                    previous_oid: b"a5828ae6b52137b913b978e16cd2334482eb4c1f".as_bstr(),
249                    new_oid: b"89b43f80a514aee58b662ad606e6352e03eaeee4".as_bstr(),
250                    signature: git_actor::SignatureRef {
251                        name: b"Sebastian Thiel".as_bstr(),
252                        email: b"foo@example.com".as_bstr(),
253                        time: Time {
254                            seconds_since_unix_epoch: 1618030561,
255                            offset_in_seconds: 28800,
256                            sign: Sign::Plus,
257                        },
258                    },
259                    message: b"pull --ff-only: Fast-forward".as_bstr(),
260                };
261                assert_eq!(res, actual);
262                assert_eq!(
263                    actual.previous_oid(),
264                    hex_to_oid("a5828ae6b52137b913b978e16cd2334482eb4c1f")
265                );
266                assert_eq!(actual.new_oid(), hex_to_oid("89b43f80a514aee58b662ad606e6352e03eaeee4"));
267            }
268        }
269
270        #[test]
271        fn two_lines_in_a_row_with_and_without_newline() {
272            let lines = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one <foo@example.com> 1234567890 -0000\t\n0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 two <foo@example.com> 1234567890 -0000\thello";
273            let (remainder, parsed) = one::<nom::error::Error<_>>(lines).expect("parse single line");
274            assert_eq!(parsed.message, b"".as_bstr(), "first message is empty");
275
276            let (remainder, parsed) = one::<nom::error::Error<_>>(remainder).expect("parse single line");
277            assert_eq!(
278                parsed.message,
279                b"hello".as_bstr(),
280                "second message is not and contains no newline"
281            );
282            assert!(remainder.is_empty());
283        }
284    }
285}