atom/publish/git/
inner.rs

1use std::io::{self, Read};
2use std::os::unix::ffi::OsStrExt;
3use std::path::{Path, PathBuf};
4
5use gix::actor::Signature;
6use gix::diff::object::Commit as AtomCommit;
7use gix::object::tree::Entry;
8use gix::objs::WriteTo;
9use gix::protocol::transport::client::Transport;
10use gix::{ObjectId, Reference};
11use tracing_indicatif::span_ext::IndicatifSpanExt;
12
13use super::{AtomContext, AtomRef, GitContext, GitResult, RefKind};
14use crate::core::AtomPaths;
15use crate::publish::error::git::Error;
16use crate::publish::{
17    ATOM_FORMAT_VERSION, ATOM_MANIFEST, ATOM_ORIGIN, ATOM_REFS, EMPTY_SIG, META_REFS,
18};
19use crate::store::git;
20use crate::{Atom, AtomId, Manifest};
21impl<'a> GitContext<'a> {
22    /// Method to verify the manifest of an entry
23    pub(super) fn verify_manifest(&self, obj: &Object, path: &Path) -> GitResult<Atom> {
24        let content = read_blob(obj, |reader| {
25            let mut content = String::new();
26            reader.read_to_string(&mut content)?;
27            Ok(content)
28        })?;
29
30        Manifest::get_atom(&content).map_err(|e| Error::Invalid(e, Box::new(path.into())))
31    }
32
33    /// Compute the [`ObjectId`] of the given proto-object in memory
34    fn _compute_hash(&self, obj: &dyn WriteTo) -> GitResult<ObjectId> {
35        let mut buf = Vec::with_capacity(obj.size() as usize);
36
37        obj.write_to(&mut buf)?;
38
39        let oid = gix::objs::compute_hash(self.repo.object_hash(), obj.kind(), buf.as_ref());
40
41        Ok(oid?)
42    }
43
44    /// Helper function to write an object to the repository
45    fn write_object(&self, obj: impl WriteTo) -> GitResult<gix::ObjectId> {
46        Ok(self.repo.write_object(obj).map(gix::Id::detach)?)
47    }
48
49    /// Helper function to return an entry by path from the repo tree
50    ///
51    /// # Errors
52    ///
53    /// This function will return an error if the call to
54    /// [`gix::object::tree::Tree::lookup_entry`] fails.
55    pub fn tree_search(&self, path: &Path) -> GitResult<Option<Entry<'a>>> {
56        let search = path.components().map(|c| c.as_os_str().as_bytes());
57        Ok(self.tree.clone().lookup_entry(search)?)
58    }
59
60    pub(super) fn find_and_verify_atom(
61        &self,
62        path: &Path,
63    ) -> GitResult<(FoundAtom, AtomPaths<PathBuf>)> {
64        let paths = AtomPaths::new(path);
65        let entry = self
66            .tree_search(paths.spec())?
67            .ok_or(Error::NotAnAtom(path.into()))?;
68
69        if !entry.mode().is_blob() || !paths.spec().starts_with(paths.content()) {
70            return Err(Error::NotAnAtom(path.into()));
71        }
72
73        if paths.content().to_str() == Some("") {
74            return Err(Error::NoRootAtom);
75        }
76
77        let content = self
78            .tree_search(paths.content())?
79            .and_then(|e| e.mode().is_tree().then_some(e))
80            .ok_or(Error::NotAnAtom(path.into()))?
81            .detach();
82
83        let tree_id = content.oid;
84        let spec_id = entry.id().detach();
85
86        self.verify_manifest(&entry.object()?, paths.spec())
87            .and_then(|spec| {
88                let id = AtomId::construct(&self.commit, spec.tag.clone()).map_err(Box::new)?;
89                if self.root != *id.root() {
90                    return Err(Error::InconsistentRoot {
91                        remote: self.root,
92                        atom: *id.root(),
93                    });
94                };
95                Ok((
96                    FoundAtom {
97                        spec,
98                        id,
99                        tree_id,
100                        spec_id,
101                    },
102                    paths,
103                ))
104            })
105    }
106
107    /// return a mutable reference to the transport
108    pub fn transport(&mut self) -> &mut Box<dyn Transport + Send> {
109        &mut self.transport
110    }
111}
112
113use semver::Version;
114
115impl<'a> AtomRef<'a> {
116    fn new(tag: String, kind: RefKind, version: &'a Version) -> Self {
117        AtomRef { tag, kind, version }
118    }
119}
120
121use std::fmt;
122
123impl<'a> fmt::Display for AtomRef<'a> {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match self.kind {
126            RefKind::Content => {
127                write!(f, "{}/{}/{}", ATOM_REFS.as_str(), self.tag, self.version)
128            },
129            RefKind::Origin => write!(
130                f,
131                "{}/{}/{}/{}",
132                META_REFS.as_str(),
133                self.tag,
134                self.version,
135                ATOM_ORIGIN
136            ),
137            RefKind::Spec => write!(
138                f,
139                "{}/{}/{}/{}",
140                META_REFS.as_str(),
141                self.tag,
142                self.version,
143                ATOM_MANIFEST
144            ),
145        }
146    }
147}
148
149impl<'a> AtomContext<'a> {
150    pub(super) fn refs(&self, kind: RefKind) -> AtomRef<'_> {
151        AtomRef::new(
152            self.atom.id.tag().to_string(),
153            kind,
154            &self.atom.spec.version,
155        )
156    }
157
158    /// Method to write atom commits
159    pub(super) fn write_atom_commit(&self, tree: ObjectId) -> GitResult<CommittedAtom> {
160        let sig = Signature {
161            email: EMPTY_SIG.into(),
162            name: EMPTY_SIG.into(),
163            time: gix::date::Time {
164                seconds: 0,
165                offset: 0,
166            },
167        };
168        let commit = AtomCommit {
169            tree,
170            parents: vec![].into(),
171            author: sig.clone(),
172            committer: sig,
173            encoding: None,
174            message: format!("{}: {}", self.atom.spec.tag, self.atom.spec.version).into(),
175            extra_headers: [
176                (ATOM_ORIGIN.into(), self.git.commit.id.to_string().into()),
177                (
178                    "path".into(),
179                    self.paths
180                        .content()
181                        .to_str()
182                        .and_then(|s| if s.is_empty() { None } else { Some(s) })
183                        .unwrap_or("/")
184                        .into(),
185                ),
186                ("format".into(), ATOM_FORMAT_VERSION.into()),
187            ]
188            .into(),
189        };
190        let id = self.git.write_object(commit.clone())?;
191        Ok(CommittedAtom { commit, id })
192    }
193}
194
195/// Method to write a single reference to the repository
196fn write_ref<'a>(
197    atom: &'a AtomContext,
198    id: ObjectId,
199    atom_ref: AtomRef,
200) -> GitResult<Reference<'a>> {
201    use gix::refs::transaction::PreviousValue;
202
203    tracing::debug!("writing atom ref: {}", atom_ref);
204
205    let AtomContext { atom, git, .. } = atom;
206
207    Ok(git.repo.reference(
208        atom_ref.to_string(),
209        id,
210        PreviousValue::MustNotExist,
211        format!(
212            "publish: {}: {}-{}",
213            atom.spec.tag, atom.spec.version, atom_ref
214        ),
215    )?)
216}
217use super::{CommittedAtom, FoundAtom};
218
219impl<'a> CommittedAtom {
220    /// Method to write references for the committed atom
221    pub(super) fn write_refs(&'a self, atom: &'a AtomContext) -> GitResult<AtomReferences<'a>> {
222        let Self { id, .. } = self;
223
224        // filter out the content tree
225        let spec = atom.atom.spec_id;
226        let src = atom.git.commit.id;
227
228        Ok(AtomReferences {
229            spec: write_ref(atom, spec, atom.refs(RefKind::Spec))?,
230            content: write_ref(atom, *id, atom.refs(RefKind::Content))?,
231            origin: write_ref(atom, src, atom.refs(RefKind::Origin))?,
232        })
233    }
234}
235
236use super::{AtomReferences, GitContent};
237
238impl<'a> AtomReferences<'a> {
239    /// Publish atom's to the specified git remote
240    ///
241    /// Currently the implementation just calls the `git` binary.
242    /// Once `gix` is further along we can use it directly.
243    pub(super) fn push(self, atom: &'a AtomContext) -> GitContent {
244        let remote = atom.git.remote_str.to_owned();
245        let mut tasks = atom.git.push_tasks.borrow_mut();
246
247        tracing::info!(
248            message = "pushing",
249            atom = %atom.atom.id.tag(),
250            remote = %remote
251        );
252        for r in [&self.content, &self.spec, &self.origin] {
253            let r = r.name().as_bstr().to_string();
254            let remote = remote.clone();
255            let parent = tracing::Span::current();
256            let progress = atom.git.progress.clone();
257            let task = async move {
258                let _guard = parent.enter();
259                let span = tracing::info_span!("push", msg = %r, %remote);
260                crate::log::set_sub_task(&span, &format!("🚀 push: {}", r));
261                let _enter = span.enter();
262                let result = git::run_git_command(&["push", &remote, format!("{r}:{r}").as_str()])?;
263
264                progress.pb_inc(1);
265
266                Ok(result)
267            };
268            tasks.spawn(task);
269        }
270
271        GitContent {
272            spec: self.spec.detach(),
273            content: self.content.detach(),
274            origin: self.origin.detach(),
275            path: atom.paths.spec().to_path_buf(),
276        }
277    }
278}
279
280use gix::Object;
281/// Helper function to read a blob from an object
282fn read_blob<F, R>(obj: &Object, mut f: F) -> GitResult<R>
283where
284    F: FnMut(&mut dyn Read) -> io::Result<R>,
285{
286    let mut reader = obj.data.as_slice();
287    Ok(f(&mut reader)?)
288}
289
290impl CommittedAtom {
291    #[must_use]
292    /// Returns a reference to the commit of this [`CommittedAtom`].
293    pub fn commit(&self) -> &AtomCommit {
294        &self.commit
295    }
296
297    #[must_use]
298    /// Returns a reference to the tip of this [`CommittedAtom`].
299    pub fn tip(&self) -> &ObjectId {
300        &self.id
301    }
302}