gnostr_asyncgit/sync/
commit_details.rs

1use git2::ObjectType;
2use git2::Signature;
3
4//use nostr_sdk_0_37_0::prelude::*;
5use nostr_sdk::prelude::*;
6use scopetime::scope_time;
7
8use super::{commits_info::get_message, CommitId, RepoPath};
9use crate::{error::Result, sync::repository::repo};
10
11///
12#[derive(Debug, PartialEq, Eq, Default, Clone)]
13pub struct CommitSignature {
14    ///
15    pub name: String,
16    ///
17    pub email: String,
18    /// time in secs since Unix epoch
19    pub time: i64,
20}
21
22impl CommitSignature {
23    /// convert from git2-rs `Signature`
24    pub fn from(s: &Signature<'_>) -> Self {
25        Self {
26            name: s.name().unwrap_or("").to_string(),
27            email: s.email().unwrap_or("").to_string(),
28
29            time: s.when().seconds(),
30        }
31    }
32}
33
34///
35#[derive(Default, Clone)]
36pub struct CommitMessage {
37    /// first line
38    pub subject: String,
39    /// remaining lines if more than one
40    pub body: Option<String>,
41}
42
43impl CommitMessage {
44    ///
45    pub fn from(s: &str) -> Self {
46        let mut lines = s.lines();
47        let subject = lines
48            .next()
49            .map_or_else(String::new, std::string::ToString::to_string);
50
51        let body: Vec<String> = lines.map(std::string::ToString::to_string).collect();
52
53        Self {
54            subject,
55            body: if body.is_empty() {
56                None
57            } else {
58                Some(body.join("\n"))
59            },
60        }
61    }
62
63    ///
64    pub fn combine(self) -> String {
65        if let Some(body) = self.body {
66            format!("{}\n{body}", self.subject)
67        } else {
68            self.subject
69        }
70    }
71}
72
73///
74#[derive(Default, Clone)]
75pub struct CommitDetails {
76    ///
77    pub author: CommitSignature,
78    /// committer when differs to `author` otherwise None
79    pub committer: Option<CommitSignature>,
80    ///
81    pub message: Option<CommitMessage>,
82    ///
83    pub hash: String,
84    ///
85    pub keys: Option<Keys>,
86}
87
88impl CommitDetails {
89    ///
90    pub fn short_hash(&self) -> &str {
91        &self.hash[0..7]
92    }
93    ///
94    pub(crate) fn pad_commit_hash(input: &str, padding_char: char) -> String {
95        let target_length = 64;
96        let current_length = input.len();
97        if current_length >= target_length {
98            return input.to_string(); // No padding needed
99        }
100        let padding_needed = target_length - current_length;
101        let padding = padding_char.to_string().repeat(padding_needed);
102        format!("{}{}", input, padding)
103    }
104    ///
105    pub fn padded_hash(&self) -> String {
106        Self::pad_commit_hash(&self.hash, '0')
107        //format!("{:0>64}", "".to_owned() + &self.hash[0..])
108    }
109    ///
110    pub fn padded_short_hash(&self) -> String {
111        Self::pad_commit_hash(&self.hash[0..7], '0')
112    }
113    ///
114    pub fn keys(&self) -> Result<Keys> {
115        Ok(Keys::parse(Self::pad_commit_hash(&self.hash, '0')).unwrap())
116    }
117}
118
119///
120pub fn get_commit_details(repo_path: &RepoPath, id: CommitId) -> Result<CommitDetails> {
121    scope_time!("get_commit_details");
122
123    let repo = repo(repo_path)?;
124    let head = repo.head()?;
125    let obj = head.resolve()?.peel(ObjectType::Commit)?;
126
127    //read top commit
128    let commit = obj.peel_to_commit()?;
129    let commit_id = commit.id().to_string();
130    //some info wrangling
131    //info!("commit_id:\n{}", commit_id);
132    let padded_commit_id = format!("{:0>64}", commit_id);
133
134    //// commit based keys
135    //let keys = generate_nostr_keys_from_commit_hash(&commit_id)?;
136    //info!("keys.secret_key():\n{:?}", keys.secret_key());
137    //info!("keys.public_key():\n{}", keys.public_key());
138
139    //parse keys from sha256 hash
140    let keys = Keys::parse(padded_commit_id).unwrap();
141
142    let commit = repo.find_commit(id.into())?;
143
144    let author = CommitSignature::from(&commit.author());
145    let committer = CommitSignature::from(&commit.committer());
146    let committer = if author == committer {
147        None
148    } else {
149        Some(committer)
150    };
151
152    let msg = CommitMessage::from(get_message(&commit, None).as_str());
153
154    let details = CommitDetails {
155        author,
156        committer,
157        message: Some(msg),
158        hash: id.to_string(),
159        keys: Some(keys),
160    };
161
162    Ok(details)
163}
164
165#[cfg(test)]
166mod tests {
167    use std::{fs::File, io::Write, path::Path};
168
169    use super::{get_commit_details, CommitMessage};
170    use crate::{
171        error::Result,
172        sync::{commit, stage_add_file, tests::repo_init_empty, RepoPath},
173    };
174
175    #[test]
176    fn test_msg_invalid_utf8() -> Result<()> {
177        let file_path = Path::new("foo");
178        let (_td, repo) = repo_init_empty().unwrap();
179        let root = repo.path().parent().unwrap();
180        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
181
182        File::create(root.join(file_path))?.write_all(b"a")?;
183        stage_add_file(repo_path, file_path).unwrap();
184
185        let msg = invalidstring::invalid_utf8("test msg");
186        let id = commit(repo_path, msg.as_str()).unwrap();
187
188        let res = get_commit_details(repo_path, id).unwrap();
189
190        assert_eq!(
191            res.message
192                .as_ref()
193                .unwrap()
194                .subject
195                .starts_with("test msg"),
196            true
197        );
198
199        Ok(())
200    }
201
202    #[test]
203    fn test_msg_linefeeds() -> Result<()> {
204        let msg = CommitMessage::from("foo\nbar\r\ntest");
205
206        assert_eq!(msg.subject, String::from("foo"),);
207        assert_eq!(msg.body, Some(String::from("bar\ntest")),);
208
209        Ok(())
210    }
211
212    #[test]
213    fn test_commit_message_combine() -> Result<()> {
214        let msg = CommitMessage::from("foo\nbar\r\ntest");
215
216        assert_eq!(msg.combine(), String::from("foo\nbar\ntest"));
217
218        Ok(())
219    }
220}