tag2upload_service_manager/
gitclone.rs

1
2use crate::prelude::*;
3use tokio::process::Command;
4
5pub struct TagDataAcquirer<'a> {
6    git_dir: &'a str,
7    job: &'a JobInWorkflow,
8}
9
10struct GitOutput {
11    stdout: String,
12    #[allow(dead_code)] // TODO print stderr if git's output seems wrong
13    stderr: String,
14}
15
16impl TagDataAcquirer<'_> {
17    async fn run_git(
18        &self,
19        what: &str,
20        args: &[&str],
21    ) -> Result<Result<GitOutput, AE>, ProcessingError> {
22        let jid = &self.job.jid;
23        trace!(%jid, ?args, "git ...");
24
25        let mut cmd = Command::new("git");
26        cmd
27            .current_dir(&self.git_dir)
28            .kill_on_drop(true)
29            .args(args);
30
31        let std::process::Output { stdout, stderr, status } =
32            cmd.output().await
33            .context("run git").map_err(PE::Local)?;
34
35        let stderr = String::from_utf8(stderr)
36            .unwrap_or_else(|e| {
37                format!("Invalid UTF-8 error; approximation follows: {}",
38                        String::from_utf8_lossy(e.as_bytes()))
39            });
40
41        if !status.success() {
42            let err = anyhow!(
43                "git {what} failed, {status}; stderr: {stderr:?}"
44            );
45            trace!(%jid, ?args, %err, "git failed");
46            return Ok(Err(err));
47        }
48
49        let Ok(stdout) = String::from_utf8(stdout)
50        else {
51            let err = anyhow!(
52                "git {what} produced invalid UTF-8 on stdout; \
53                 on stderr it printed: {stderr:?}"
54            );
55            trace!(%jid, ?args, %err, "git misbehaved");
56            return Ok(Err(err));
57        };
58
59        trace!(%jid, ?args, ?stdout, "git succeeded");
60        
61        Ok(Ok(GitOutput { stdout, stderr }))
62    }
63}
64
65pub async fn fetch_tags_via_clone(
66    job: &mut JobInWorkflow,
67    task_tmpdir: &str,
68) -> Result<ValidTagObjectData, FetchError> {
69    let jid = job.jid;
70    let git_dir = format!("{task_tmpdir}/package.git");
71
72    fs::create_dir(&git_dir)
73        .with_context(|| git_dir.clone()).context("mkdir")
74        .map_err(PE::Local)?;
75
76    let acq = TagDataAcquirer {
77        git_dir: &git_dir,
78        job,
79    };
80
81    acq.run_git("init", &["init", "--bare"])
82        .await?
83        .map_err(PE::Local)?;
84
85    let url = &*job.data.repo_git_url;
86
87    test_hook_url!(url);
88
89    let refname = format!("refs/tags/{}", job.data.tag_name);
90    let refspec = format!("+{}:{}", refname, refname);
91
92    debug!(jid=%jid, url=?url, "git fetch...");
93
94    tokio::time::timeout(
95        *globals().config.timeouts.git_clone,
96        acq.run_git(
97            "fetch",
98            &["fetch", "--no-tags", "--progress", "--depth", "1",
99              &url, &refspec],
100        )
101    )
102        .await
103        .context("clone, to inspect tag").map_err(PE::Forge)?
104        ?
105        .map_err(PE::Forge)?;
106
107    let tag_objectid: GitObjectId = acq.run_git(
108        "rev-parse",
109        &["rev-parse", &refname],
110    )
111        .await?
112        .map_err(PE::Local)?
113        .stdout
114        .trim_end().parse()
115        .context("parse output of git-rev-parse").map_err(PE::Local)?;
116
117    MismatchError::check(
118        "git object id",
119        &job.data.tag_objectid,
120        &tag_objectid,
121    )?;
122
123    let obj_type = acq.run_git(
124        "cat-file -t",
125        &["cat-file", "-t", &refname],
126    )
127        .await?
128        .map_err(PE::Local)?
129        .stdout;
130
131    let obj_type = obj_type.trim();
132    if obj_type != "tag" {
133        Err(PE::Forge(anyhow!(
134            "ref {refname:?} referenced {obj_type}, not a tag"
135        )))?;
136    }
137
138    let tag_data = acq.run_git(
139        "cat-file tag",
140        &["cat-file", "tag", &refname],
141    )
142        .await?
143        .map_err(PE::Local)?
144        .stdout;
145
146    let tag_data: TagObjectData = tag_data.try_into()
147        .context("tag data obtained via git clone")
148        .map_err(PE::Forge)?;
149
150    let tagger_date =  {
151        let tagger_line = acq.run_git(
152            "for-each-ref ...taggerdate...",
153            &["for-each-ref", "--format=%(taggerdate:raw)",
154              // This defeats git-for-each-ref's parsing, which allows both
155              // (a) glob patterns and (b) exact prefix matches.
156              // We provide (a), and it won't match via (b) because
157              // the glob metacharacter `[` is not legal in a ref name.
158              &format!("[r]efs/tags/{}", job.data.tag_name)]
159        )
160            .await?
161            .map_err(PE::Local)?
162            .stdout;
163
164        let time_t = tagger_line
165            .split_once(' ').map(|(lhs, _)| lhs).unwrap_or(&tagger_line)
166            .parse::<u64>().context("parse time_t from git tagger line")
167            .map_err(PE::Forge)?;
168
169        let time_t = SystemTime::UNIX_EPOCH
170            .checked_add(Duration::from_secs(time_t))
171            .ok_or_else(|| PE::Forge(anyhow!(
172                "tagger date out of plausible range"
173            )))?;
174
175        time_t
176    };
177
178    let is_recent_enough = globals().check_tag_recency(tagger_date)?;
179
180    Ok(ValidTagObjectData {
181        tag_data,
182        is_recent_enough,
183    })
184}