Skip to main content

tackler_core/parser/
tackler_txns.rs

1/*
2 * Tackler-NG 2023-2025
3 * SPDX-License-Identifier: Apache-2.0
4 */
5use itertools::Itertools;
6use std::collections::HashSet;
7use std::error::Error;
8use std::path::{Path, PathBuf};
9use std::str;
10//use std::time::{SystemTime, UNIX_EPOCH};
11
12use crate::kernel::Settings;
13use crate::kernel::settings::GitInputSelector;
14use crate::model::{TxnData, Txns};
15use crate::parser::tackler_parser;
16use crate::tackler;
17use gix as git;
18use gix::date::time::CustomFormat;
19use gix::hash as gix_hash;
20use gix::objs::tree::EntryKind;
21use tackler_api::metadata::items::{GitInputReference, MetadataItem};
22
23/// # Errors
24/// Returns `Err` in case of parse or semantic error
25pub fn string_to_txns(
26    input: &mut &str,
27    settings: &mut Settings,
28) -> Result<TxnData, tackler::Error> {
29    let txns = tackler_parser::txns_text(input, settings)?;
30
31    // feature: a94d4a60-40dc-4ec0-97a3-eeb69399f01b
32    // coverage: "sorted" tested by 200aad57-9275-4d16-bdad-2f1c484bcf17
33
34    TxnData::try_from(None, txns, settings)
35}
36
37/// # Errors
38/// Returns `Err` in case of parse or semantic error
39pub fn paths_to_txns(
40    paths: &[PathBuf],
41    settings: &mut Settings,
42) -> Result<TxnData, tackler::Error> {
43    let txns: Result<Txns, tackler::Error> = paths
44        .iter()
45        .map(|p| tackler_parser::txns_file(p, settings))
46        .flatten_ok()
47        .collect();
48
49    TxnData::try_from(None, txns?, settings)
50}
51
52/// # Errors
53/// Returns `Err` in case of parse or semantic error
54#[allow(clippy::too_many_lines)]
55pub fn git_to_txns(
56    repo_path: &Path,
57    dir: &str,
58    extension: &str,
59    input_selector: GitInputSelector,
60    settings: &mut Settings,
61) -> Result<TxnData, tackler::Error> {
62    // perf: let mut ts_par_total: u128 = 0;
63    // perf: let ts_start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(/*:test:*/);
64
65    let repo = git::open(repo_path)?;
66
67    let (object, reference) = match input_selector {
68        GitInputSelector::CommitId(id) => {
69            let mut candidates = Some(HashSet::default());
70            let prefix = match gix_hash::Prefix::try_from(id.as_str()) {
71                Ok(v) => v,
72                Err(err) => {
73                    let msg = format!("Invalid commit id '{id}': {err}");
74                    return Err(msg.into());
75                }
76            };
77
78            let res = repo.objects.lookup_prefix(prefix, candidates.as_mut())?;
79            let object_id = match res {
80                Some(Ok(id)) => id,
81                Some(Err(())) => return Err(format!("Ambiguous abbreviated commit id {id}").into()),
82                None => return Err(format!("Unknown commit id '{id}'").into()),
83            };
84            // This is originally commit, so no need to peel it
85            (repo.find_object(object_id)?.try_into_commit()?, None)
86        }
87        GitInputSelector::Reference(ref_str) => {
88            let id_res = repo.rev_parse_single(ref_str.as_bytes());
89            let id = match id_res {
90                Ok(id) => id,
91                Err(err) => {
92                    let msg = if let Some(source) = err.source() {
93                        format!("{source}")
94                    } else {
95                        format!("{err}")
96                    };
97                    return Err(msg.into());
98                }
99            };
100            let reference = if id.to_string().starts_with(ref_str.as_str()) {
101                // This is tackler specific logic: don't show ref if it's plain commit id
102                None
103            } else {
104                Some(ref_str.clone())
105            };
106            // Peel it so that tags are ok
107            (id.object()?.peel_to_commit()?, reference)
108        }
109    };
110
111    let signature = object.author()?;
112    let author = format!("{} <{}>", signature.name, signature.email);
113    let date = signature
114        .time()?
115        .format(CustomFormat::new("%Y-%m-%d %H:%M:%S %z"))?
116        .clone();
117
118    let gitmd = GitInputReference {
119        commit: object.id.to_string(),
120        reference,
121        dir: dir.to_string(),
122        extension: extension.to_string(),
123        subject: object.message()?.summary().to_string(),
124        author,
125        date,
126    };
127
128    let dir = if dir.ends_with('/') {
129        dir.to_string()
130    } else {
131        format!("{dir}/")
132    };
133    let ext = format!(".{extension}");
134
135    let tree = object.tree()?;
136    // fixme: Optimization
137    //      In the future, this could be optimized with custom walker,
138    //      which does the filtering in the first place.
139    let txns: Result<Txns, tackler::Error> = tree
140        .traverse()
141        .breadthfirst
142        .files()?
143        .iter()
144        .map(|entry| {
145            use git::objs::tree::EntryKind::{Blob, Link};
146            match EntryKind::from(entry.mode) {
147                Blob => {
148                    if entry.filepath.starts_with(str::as_bytes(dir.as_str()))
149                        && entry.filepath.ends_with(str::as_bytes(ext.as_str()))
150                    {
151                        let obj = repo.find_object(entry.oid)?;
152                        // perf: let ts_par_start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(/*:test:*/);
153
154                        let par_res =
155                            tackler_parser::txns_text(&mut str::from_utf8(&obj.data)?, settings);
156
157                        // perf: let ts_par_end = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(/*:test:*/);
158                        // perf: ts_par_total = ts_par_total + (ts_par_end.as_millis() - ts_par_start.as_millis());
159                        match par_res {
160                            Ok(txns) => Ok(txns),
161                            Err(err) => {
162                                let msg = format!(
163                                    "\
164                                    GIT: Error while processing git object\n\
165                                    \x20  commit id: {}\n\
166                                    \x20  object id: {}\n\
167                                    \x20  path: {}\n\
168                                    \x20  msg: {}\
169                                    ",
170                                    object.id, obj.id, entry.filepath, err
171                                );
172                                Err(msg.into())
173                            }
174                        }
175                    } else {
176                        // It's blob but outside of our file path filter
177                        Ok(Vec::default())
178                    }
179                }
180                Link => {
181                    let obj = repo.find_object(entry.oid)?;
182                    let msg = format!(
183                        "\
184                        GIT: Error while processing git object\n\
185                        \x20  commit id: {}\n\
186                        \x20  object id: {}\n\
187                        \x20  path: {}\n\
188                        \x20  msg: {}\
189                        ",
190                        object.id,
191                        obj.id,
192                        entry.filepath,
193                        "Links inside repository are not supported"
194                    );
195                    Err(msg.into())
196                }
197                // It's not a blob
198                _ => Ok(Vec::default()),
199            }
200        })
201        .flatten_ok()
202        .collect::<Result<Txns, tackler::Error>>();
203
204    // perf: let ts_end = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(/*:test:*/);
205    // perf: eprintln!("total time: {}ms, parse time: {}ms, git: {}ms", (ts_end.as_millis() - ts_start.as_millis()), ts_par_total, (ts_end.as_millis() - ts_start.as_millis())-ts_par_total);
206
207    TxnData::try_from(
208        Some(MetadataItem::GitInputReference(gitmd)),
209        txns?,
210        settings,
211    )
212}