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 gl = globals();
70    let jid = job.jid;
71    let git_dir = format!("{task_tmpdir}/package.git");
72
73    fs::create_dir(&git_dir)
74        .with_context(|| git_dir.clone()).context("mkdir")
75        .map_err(PE::Local)?;
76
77    let acq = TagDataAcquirer {
78        git_dir: &git_dir,
79        job,
80    };
81
82    acq.run_git("init", &["init", "--bare"])
83        .await?
84        .map_err(PE::Local)?;
85
86    let url = &*job.data.repo_git_url;
87
88    test_hook_url!(url);
89
90    let refname = format!("refs/tags/{}", job.data.tag_name);
91    let refspec = format!("+{}:{}", refname, refname);
92
93    debug!(jid=%jid, url=?url, "git fetch...");
94
95    // Check that the repo isn't 404
96
97    match gl.http_fetch_raw(
98        Url::parse(&url)
99            .context("parse url")
100            .map_err(PE::ForgePerm)
101            ?,
102    )
103        .await.context("http fetch").map_err(PE::ForgeTemp)?
104    {
105        HttpFetchedRaw::Success { .. } |
106        HttpFetchedRaw::SuccessFile { .. } => {},
107        HttpFetchedRaw::NotFound(e) => Err(PE::ForgePerm(e.into()))?,
108    }
109
110    // Check that the ref exists
111
112    let () = tokio::time::timeout(
113        *globals().config.timeouts.git_query,
114        acq.run_git(
115            "ls-remote",
116            &["ls-remote", "--refs", &url, &refname],
117        )
118    )
119        .await
120        .context("ls-remote timeout").map_err(PE::ForgeTemp)?
121        ?
122        .map_err(PE::ForgeTemp)?
123        .stdout
124        .lines()
125        .find_map(|l| {
126            let found_ref = l
127                .split_once('\t')?.1
128                .trim_end();
129            (found_ref == refname).then_some(())
130        })
131        .ok_or_else(|| anyhow!("tag does not exist at repository"))
132        .map_err(PE::ForgePerm)?;
133
134    // Actually fetch
135
136    tokio::time::timeout(
137        *gl.config.timeouts.git_clone,
138        acq.run_git(
139            "fetch",
140            &["fetch", "--no-tags", "--progress", "--depth", "1",
141              &url, &refspec],
142        )
143    )
144        .await
145        .context("clone, to inspect tag").map_err(PE::ForgeTemp)?
146        ?
147        .map_err(PE::ForgeTemp)?;
148
149    let tag_objectid = acq.run_git(
150        "rev-parse",
151        &["rev-parse", &refname],
152    )
153        .await?
154        .map_err(PE::Local)?
155        .stdout
156        .trim_end().parse::<GitObjectId>()
157        .context("parse output of git-rev-parse").map_err(PE::Local)?;
158
159    MismatchError::check(
160        "git object id",
161        &job.data.tag_objectid,
162        &tag_objectid,
163    )?;
164
165    let obj_type = acq.run_git(
166        "cat-file -t",
167        &["cat-file", "-t", &refname],
168    )
169        .await?
170        .map_err(PE::Local)?
171        .stdout;
172
173    let obj_type = obj_type.trim();
174    if obj_type != "tag" {
175        Err(PE::ForgePerm(anyhow!(
176            "ref {refname:?} referenced {obj_type}, not a tag"
177        )))?;
178    }
179
180    let tag_data = acq.run_git(
181        "cat-file tag",
182        &["cat-file", "tag", &refname],
183    )
184        .await?
185        .map_err(PE::Local)?
186        .stdout;
187
188    let tag_data = TagObjectData::try_from(tag_data)
189        .context("tag data obtained via git clone")
190        .map_err(PE::ForgePerm)?;
191
192    let tagger_date =  {
193        let tagger_line = acq.run_git(
194            "for-each-ref ...taggerdate...",
195            &["for-each-ref", "--format=%(taggerdate:raw)",
196              // This defeats git-for-each-ref's parsing, which allows both
197              // (a) glob patterns and (b) exact prefix matches.
198              // We provide (a), and it won't match via (b) because
199              // the glob metacharacter `[` is not legal in a ref name.
200              &format!("[r]efs/tags/{}", job.data.tag_name)]
201        )
202            .await?
203            .map_err(PE::Local)?
204            .stdout;
205
206        let time_t = tagger_line
207            .split_once(' ').map(|(lhs, _)| lhs).unwrap_or(&tagger_line)
208            .parse::<u64>().context("parse time_t from git tagger line")
209            .map_err(PE::ForgePerm)?;
210
211        let time_t = SystemTime::UNIX_EPOCH
212            .checked_add(Duration::from_secs(time_t))
213            .ok_or_else(|| PE::ForgePerm(anyhow!(
214                "tagger date out of plausible range"
215            )))?;
216
217        time_t
218    };
219
220    let is_recent_enough = gl.check_tag_recency(tagger_date)?;
221
222    Ok(ValidTagObjectData {
223        tag_data,
224        is_recent_enough,
225    })
226}