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::path::{Path, PathBuf};
8use std::str;
9//use std::time::{SystemTime, UNIX_EPOCH};
10
11use crate::kernel::Settings;
12use crate::model::{TxnData, Txns};
13use crate::parser::tackler_parser;
14use crate::tackler;
15use gix as git;
16use gix::hash as gix_hash;
17use gix::objs::tree::EntryKind;
18use tackler_api::metadata::items::{GitInputReference, MetadataItem};
19
20pub enum GitInputSelector {
21    CommitId(String),
22    Reference(String),
23}
24
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::from(None, txns, &settings.get_hash())
35}
36
37pub fn paths_to_txns(
38    paths: &[PathBuf],
39    settings: &mut Settings,
40) -> Result<TxnData, tackler::Error> {
41    let txns: Result<Txns, tackler::Error> = paths
42        .iter()
43        .map(|p| tackler_parser::txns_file(p, settings))
44        .flatten_ok()
45        .collect();
46
47    TxnData::from(None, txns?, &settings.get_hash())
48}
49
50pub fn git_to_txns(
51    repo_path: &Path,
52    dir: &str,
53    extension: &str,
54    input_selector: GitInputSelector,
55    settings: &mut Settings,
56) -> Result<TxnData, tackler::Error> {
57    // perf: let mut ts_par_total: u128 = 0;
58    // perf: let ts_start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(/*:test:*/);
59
60    let repo = git::open(repo_path)?;
61
62    let (object, reference) = match input_selector {
63        GitInputSelector::CommitId(id) => {
64            let mut candidates = Some(HashSet::default());
65            let prefix = match gix_hash::Prefix::try_from(id.as_str()) {
66                Ok(v) => v,
67                Err(err) => {
68                    let msg = format!("Invalid commit id '{id}': {err}");
69                    return Err(msg.into());
70                }
71            };
72
73            let res = repo.objects.lookup_prefix(prefix, candidates.as_mut())?;
74            let object_id = match res {
75                Some(Ok(id)) => id,
76                Some(Err(())) => return Err(format!("Ambiguous abbreviated commit id {id}").into()),
77                None => return Err(format!("Unknown commit id '{id}'").into()),
78            };
79            // This is originally commit, so no need to peel it
80            (repo.find_object(object_id)?.try_into_commit()?, None)
81        }
82        GitInputSelector::Reference(ref_str) => {
83            let id = repo.rev_parse_single(ref_str.as_bytes())?;
84            let reference = if id.to_string().starts_with(ref_str.as_str()) {
85                // This is tackler specific logic: don't show ref if it's plain commit id
86                None
87            } else {
88                Some(ref_str.clone())
89            };
90            // Peel it so that tags are ok
91            (id.object()?.peel_to_commit()?, reference)
92        }
93    };
94
95    let gitmd = GitInputReference {
96        commit: object.id.to_string(),
97        reference,
98        dir: dir.to_string(),
99        suffix: extension.to_string(),
100        message: object.message()?.title.to_string(),
101    };
102
103    let tree = object.tree()?;
104    // fixme: Optimization
105    //      In the future, this could be optimized with custom walker,
106    //      which does the filtering in the first place.
107    let txns: Result<Txns, tackler::Error> = tree
108        .traverse()
109        .breadthfirst
110        .files()?
111        .iter()
112        .map(|entry| {
113            use git::objs::tree::EntryKind::{Blob, Link};
114            match EntryKind::from(entry.mode) {
115                Blob => {
116                    if entry.filepath.starts_with(str::as_bytes(dir))
117                        && entry.filepath.ends_with(str::as_bytes(extension))
118                    {
119                        let obj = repo.find_object(entry.oid)?;
120                        // perf: let ts_par_start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(/*:test:*/);
121
122                        let par_res =
123                            tackler_parser::txns_text(&mut str::from_utf8(&obj.data)?, settings);
124
125                        // perf: let ts_par_end = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(/*:test:*/);
126                        // perf: ts_par_total = ts_par_total + (ts_par_end.as_millis() - ts_par_start.as_millis());
127                        match par_res {
128                            Ok(txns) => Ok(txns),
129                            Err(err) => {
130                                let msg = format!(
131                                    "\
132                                    GIT: Error while processing git object\n\
133                                    \x20  commit id: {}\n\
134                                    \x20  object id: {}\n\
135                                    \x20  path: {}\n\
136                                    \x20  msg: {}\
137                                    ",
138                                    object.id, obj.id, entry.filepath, err
139                                );
140                                Err(msg.into())
141                            }
142                        }
143                    } else {
144                        // It's blob but outside of our file path filter
145                        Ok(Vec::default())
146                    }
147                }
148                Link => {
149                    let obj = repo.find_object(entry.oid)?;
150                    let msg = format!(
151                        "\
152                        GIT: Error while processing git object\n\
153                        \x20  commit id: {}\n\
154                        \x20  object id: {}\n\
155                        \x20  path: {}\n\
156                        \x20  msg: {}\
157                        ",
158                        object.id,
159                        obj.id,
160                        entry.filepath,
161                        "Links inside repository are not supported"
162                    );
163                    Err(msg.into())
164                }
165                // It's not a blob
166                _ => Ok(Vec::default()),
167            }
168        })
169        .flatten_ok()
170        .collect::<Result<Txns, tackler::Error>>();
171
172    // perf: let ts_end = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(/*:test:*/);
173    // 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);
174
175    let hash = &settings.get_hash();
176    TxnData::from(Some(MetadataItem::GitInputReference(gitmd)), txns?, hash)
177}