jujutsu_lib/
git.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{BTreeMap, HashSet};
16use std::default::Default;
17use std::path::PathBuf;
18
19use git2::Oid;
20use itertools::Itertools;
21use thiserror::Error;
22
23use crate::backend::{CommitId, ObjectId};
24use crate::commit::Commit;
25use crate::git_backend::NO_GC_REF_NAMESPACE;
26use crate::op_store::RefTarget;
27use crate::repo::{MutableRepo, Repo};
28use crate::settings::GitSettings;
29use crate::view::RefName;
30
31#[derive(Error, Debug, PartialEq)]
32pub enum GitImportError {
33    #[error("Unexpected git error when importing refs: {0}")]
34    InternalGitError(#[from] git2::Error),
35}
36
37fn parse_git_ref(ref_name: &str) -> Option<RefName> {
38    if let Some(branch_name) = ref_name.strip_prefix("refs/heads/") {
39        Some(RefName::LocalBranch(branch_name.to_string()))
40    } else if let Some(remote_and_branch) = ref_name.strip_prefix("refs/remotes/") {
41        remote_and_branch
42            .split_once('/')
43            .map(|(remote, branch)| RefName::RemoteBranch {
44                remote: remote.to_string(),
45                branch: branch.to_string(),
46            })
47    } else {
48        ref_name
49            .strip_prefix("refs/tags/")
50            .map(|tag_name| RefName::Tag(tag_name.to_string()))
51    }
52}
53
54fn prevent_gc(git_repo: &git2::Repository, id: &CommitId) {
55    git_repo
56        .reference(
57            &format!("{}{}", NO_GC_REF_NAMESPACE, id.hex()),
58            Oid::from_bytes(id.as_bytes()).unwrap(),
59            true,
60            "used by jj",
61        )
62        .unwrap();
63}
64
65/// Reflect changes made in the underlying Git repo in the Jujutsu repo.
66pub fn import_refs(
67    mut_repo: &mut MutableRepo,
68    git_repo: &git2::Repository,
69    git_settings: &GitSettings,
70) -> Result<(), GitImportError> {
71    let store = mut_repo.store().clone();
72    let mut existing_git_refs = mut_repo.view().git_refs().clone();
73    let mut old_git_heads = existing_git_refs
74        .values()
75        .flat_map(|old_target| old_target.adds())
76        .collect_vec();
77    if let Some(old_git_head) = mut_repo.view().git_head() {
78        old_git_heads.extend(old_git_head.adds());
79    }
80
81    let mut new_git_heads = HashSet::new();
82    // TODO: Should this be a separate function? We may not always want to import
83    // the Git HEAD (and add it to our set of heads).
84    if let Ok(head_git_commit) = git_repo
85        .head()
86        .and_then(|head_ref| head_ref.peel_to_commit())
87    {
88        let head_commit_id = CommitId::from_bytes(head_git_commit.id().as_bytes());
89        let head_commit = store.get_commit(&head_commit_id).unwrap();
90        new_git_heads.insert(head_commit_id.clone());
91        prevent_gc(git_repo, &head_commit_id);
92        mut_repo.add_head(&head_commit);
93        mut_repo.set_git_head(RefTarget::Normal(head_commit_id));
94    } else {
95        mut_repo.clear_git_head();
96    }
97
98    let mut changed_git_refs = BTreeMap::new();
99    let git_refs = git_repo.references()?;
100    for git_ref in git_refs {
101        let git_ref = git_ref?;
102        if !(git_ref.is_tag() || git_ref.is_branch() || git_ref.is_remote())
103            || git_ref.name().is_none()
104        {
105            // Skip other refs (such as notes) and symbolic refs, as well as non-utf8 refs.
106            continue;
107        }
108        let full_name = git_ref.name().unwrap().to_string();
109        if let Some(RefName::RemoteBranch { branch, remote: _ }) = parse_git_ref(&full_name) {
110            // "refs/remotes/origin/HEAD" isn't a real remote-tracking branch
111            if &branch == "HEAD" {
112                continue;
113            }
114        }
115        let git_commit = match git_ref.peel_to_commit() {
116            Ok(git_commit) => git_commit,
117            Err(_) => {
118                // Perhaps a tag pointing to a GPG key or similar. Just skip it.
119                continue;
120            }
121        };
122        let id = CommitId::from_bytes(git_commit.id().as_bytes());
123        new_git_heads.insert(id.clone());
124        // TODO: Make it configurable which remotes are publishing and update public
125        // heads here.
126        let old_target = existing_git_refs.remove(&full_name);
127        let new_target = Some(RefTarget::Normal(id.clone()));
128        if new_target != old_target {
129            prevent_gc(git_repo, &id);
130            mut_repo.set_git_ref(full_name.clone(), RefTarget::Normal(id.clone()));
131            let commit = store.get_commit(&id).unwrap();
132            mut_repo.add_head(&commit);
133            changed_git_refs.insert(full_name, (old_target, new_target));
134        }
135    }
136    for (full_name, target) in existing_git_refs {
137        mut_repo.remove_git_ref(&full_name);
138        changed_git_refs.insert(full_name, (Some(target), None));
139    }
140    for (full_name, (old_git_target, new_git_target)) in changed_git_refs {
141        if let Some(ref_name) = parse_git_ref(&full_name) {
142            // Apply the change that happened in git since last time we imported refs
143            mut_repo.merge_single_ref(&ref_name, old_git_target.as_ref(), new_git_target.as_ref());
144            // If a git remote-tracking branch changed, apply the change to the local branch
145            // as well
146            if !git_settings.auto_local_branch {
147                continue;
148            }
149            if let RefName::RemoteBranch { branch, remote: _ } = ref_name {
150                mut_repo.merge_single_ref(
151                    &RefName::LocalBranch(branch),
152                    old_git_target.as_ref(),
153                    new_git_target.as_ref(),
154                );
155            }
156        }
157    }
158
159    // Find commits that are no longer referenced in the git repo and abandon them
160    // in jj as well.
161    let new_git_heads = new_git_heads.into_iter().collect_vec();
162    // We could use mut_repo.record_rewrites() here but we know we only need to care
163    // about abandoned commits for now. We may want to change this if we ever
164    // add a way of preserving change IDs across rewrites by `git` (e.g. by
165    // putting them in the commit message).
166    let abandoned_commits = mut_repo
167        .index()
168        .walk_revs(&old_git_heads, &new_git_heads)
169        .map(|entry| entry.commit_id())
170        .collect_vec();
171    let root_commit_id = mut_repo.store().root_commit_id().clone();
172    for abandoned_commit in abandoned_commits {
173        if abandoned_commit != root_commit_id {
174            mut_repo.record_abandoned_commit(abandoned_commit);
175        }
176    }
177
178    Ok(())
179}
180
181#[derive(Error, Debug, PartialEq)]
182pub enum GitExportError {
183    #[error("Cannot export conflicted branch '{0}'")]
184    ConflictedBranch(String),
185    #[error("Failed to read export state: {0}")]
186    ReadStateError(String),
187    #[error("Failed to write export state: {0}")]
188    WriteStateError(String),
189    #[error("Git error: {0}")]
190    InternalGitError(#[from] git2::Error),
191}
192
193/// Reflect changes made in the Jujutsu repo compared to our current view of the
194/// Git repo in `mut_repo.view().git_refs()`. Returns a list of names of
195/// branches that failed to export.
196// TODO: Also indicate why we failed to export these branches
197pub fn export_refs(
198    mut_repo: &mut MutableRepo,
199    git_repo: &git2::Repository,
200) -> Result<Vec<String>, GitExportError> {
201    // First find the changes we want need to make without modifying mut_repo
202    let mut branches_to_update = BTreeMap::new();
203    let mut branches_to_delete = BTreeMap::new();
204    let mut failed_branches = vec![];
205    let view = mut_repo.view();
206    let all_branch_names: HashSet<&str> = view
207        .git_refs()
208        .keys()
209        .filter_map(|git_ref| git_ref.strip_prefix("refs/heads/"))
210        .chain(view.branches().keys().map(AsRef::as_ref))
211        .collect();
212    for branch_name in all_branch_names {
213        let old_branch = view.get_git_ref(&format!("refs/heads/{branch_name}"));
214        let new_branch = view.get_local_branch(branch_name);
215        if new_branch == old_branch {
216            continue;
217        }
218        let old_oid = match old_branch {
219            None => None,
220            Some(RefTarget::Normal(id)) => Some(Oid::from_bytes(id.as_bytes()).unwrap()),
221            Some(RefTarget::Conflict { .. }) => {
222                // The old git ref should only be a conflict if there were concurrent import
223                // operations while the value changed. Don't overwrite these values.
224                failed_branches.push(branch_name.to_owned());
225                continue;
226            }
227        };
228        if let Some(new_branch) = new_branch {
229            match new_branch {
230                RefTarget::Normal(id) => {
231                    let new_oid = Oid::from_bytes(id.as_bytes());
232                    branches_to_update.insert(branch_name.to_owned(), (old_oid, new_oid.unwrap()));
233                }
234                RefTarget::Conflict { .. } => {
235                    // Skip conflicts and leave the old value in git_refs
236                    continue;
237                }
238            }
239        } else {
240            branches_to_delete.insert(branch_name.to_owned(), old_oid.unwrap());
241        }
242    }
243    // TODO: Also check other worktrees' HEAD.
244    if let Ok(head_ref) = git_repo.find_reference("HEAD") {
245        if let (Some(head_git_ref), Ok(current_git_commit)) =
246            (head_ref.symbolic_target(), head_ref.peel_to_commit())
247        {
248            if let Some(branch_name) = head_git_ref.strip_prefix("refs/heads/") {
249                let detach_head =
250                    if let Some((_old_oid, new_oid)) = branches_to_update.get(branch_name) {
251                        *new_oid != current_git_commit.id()
252                    } else {
253                        branches_to_delete.contains_key(branch_name)
254                    };
255                if detach_head {
256                    git_repo.set_head_detached(current_git_commit.id())?;
257                }
258            }
259        }
260    }
261    for (branch_name, old_oid) in branches_to_delete {
262        let git_ref_name = format!("refs/heads/{branch_name}");
263        let success = if let Ok(mut git_ref) = git_repo.find_reference(&git_ref_name) {
264            if git_ref.target() == Some(old_oid) {
265                // The branch has not been updated by git, so go ahead and delete it
266                git_ref.delete().is_ok()
267            } else {
268                // The branch was updated by git
269                false
270            }
271        } else {
272            // The branch is already deleted
273            true
274        };
275        if success {
276            mut_repo.remove_git_ref(&git_ref_name);
277        } else {
278            failed_branches.push(branch_name);
279        }
280    }
281    for (branch_name, (old_oid, new_oid)) in branches_to_update {
282        let git_ref_name = format!("refs/heads/{branch_name}");
283        let success = match old_oid {
284            None => {
285                if let Ok(git_ref) = git_repo.find_reference(&git_ref_name) {
286                    // The branch was added in jj and in git. Iff git already pointed it to our
287                    // desired target, we're good
288                    git_ref.target() == Some(new_oid)
289                } else {
290                    // The branch was added in jj but still doesn't exist in git, so add it
291                    git_repo
292                        .reference(&git_ref_name, new_oid, true, "export from jj")
293                        .is_ok()
294                }
295            }
296            Some(old_oid) => {
297                // The branch was modified in jj. We can use libgit2's API for updating under a
298                // lock.
299                if git_repo
300                    .reference_matching(&git_ref_name, new_oid, true, old_oid, "export from jj")
301                    .is_ok()
302                {
303                    // Successfully updated from old_oid to new_oid (unchanged in git)
304                    true
305                } else {
306                    // The reference was probably updated in git
307                    if let Ok(git_ref) = git_repo.find_reference(&git_ref_name) {
308                        // Iff it was updated to our desired target, we still consider it a success
309                        git_ref.target() == Some(new_oid)
310                    } else {
311                        // The reference was deleted in git
312                        false
313                    }
314                }
315            }
316        };
317        if success {
318            mut_repo.set_git_ref(
319                git_ref_name,
320                RefTarget::Normal(CommitId::from_bytes(new_oid.as_bytes())),
321            );
322        } else {
323            failed_branches.push(branch_name);
324        }
325    }
326    Ok(failed_branches)
327}
328
329#[derive(Error, Debug, PartialEq)]
330pub enum GitFetchError {
331    #[error("No git remote named '{0}'")]
332    NoSuchRemote(String),
333    // TODO: I'm sure there are other errors possible, such as transport-level errors.
334    #[error("Unexpected git error when fetching: {0}")]
335    InternalGitError(#[from] git2::Error),
336}
337
338#[tracing::instrument(skip(mut_repo, git_repo, callbacks))]
339pub fn fetch(
340    mut_repo: &mut MutableRepo,
341    git_repo: &git2::Repository,
342    remote_name: &str,
343    callbacks: RemoteCallbacks<'_>,
344    git_settings: &GitSettings,
345) -> Result<Option<String>, GitFetchError> {
346    let mut remote =
347        git_repo
348            .find_remote(remote_name)
349            .map_err(|err| match (err.class(), err.code()) {
350                (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => {
351                    GitFetchError::NoSuchRemote(remote_name.to_string())
352                }
353                (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => {
354                    GitFetchError::NoSuchRemote(remote_name.to_string())
355                }
356                _ => GitFetchError::InternalGitError(err),
357            })?;
358    let mut fetch_options = git2::FetchOptions::new();
359    let mut proxy_options = git2::ProxyOptions::new();
360    proxy_options.auto();
361    fetch_options.proxy_options(proxy_options);
362    let callbacks = callbacks.into_git();
363    fetch_options.remote_callbacks(callbacks);
364    let refspec: &[&str] = &[];
365    tracing::debug!("remote.download");
366    remote.download(refspec, Some(&mut fetch_options))?;
367    tracing::debug!("remote.prune");
368    remote.prune(None)?;
369    tracing::debug!("remote.update_tips");
370    remote.update_tips(None, false, git2::AutotagOption::Unspecified, None)?;
371    // TODO: We could make it optional to get the default branch since we only care
372    // about it on clone.
373    let mut default_branch = None;
374    if let Ok(default_ref_buf) = remote.default_branch() {
375        if let Some(default_ref) = default_ref_buf.as_str() {
376            // LocalBranch here is the local branch on the remote, so it's really the remote
377            // branch
378            if let Some(RefName::LocalBranch(branch_name)) = parse_git_ref(default_ref) {
379                tracing::debug!(default_branch = branch_name);
380                default_branch = Some(branch_name);
381            }
382        }
383    }
384    tracing::debug!("remote.disconnect");
385    remote.disconnect()?;
386    tracing::debug!("import_refs");
387    import_refs(mut_repo, git_repo, git_settings).map_err(|err| match err {
388        GitImportError::InternalGitError(source) => GitFetchError::InternalGitError(source),
389    })?;
390    Ok(default_branch)
391}
392
393#[derive(Error, Debug, PartialEq)]
394pub enum GitPushError {
395    #[error("No git remote named '{0}'")]
396    NoSuchRemote(String),
397    #[error("Push is not fast-forwardable")]
398    NotFastForward,
399    #[error("Remote rejected the update of some refs (do you have permission to push to {0:?}?)")]
400    RefUpdateRejected(Vec<String>),
401    // TODO: I'm sure there are other errors possible, such as transport-level errors,
402    // and errors caused by the remote rejecting the push.
403    #[error("Unexpected git error when pushing: {0}")]
404    InternalGitError(#[from] git2::Error),
405}
406
407pub fn push_commit(
408    git_repo: &git2::Repository,
409    target: &Commit,
410    remote_name: &str,
411    remote_branch: &str,
412    // TODO: We want this to be an Option<CommitId> for the expected current commit on the remote.
413    // It's a blunt "force" option instead until git2-rs supports the "push negotiation" callback
414    // (https://github.com/rust-lang/git2-rs/issues/733).
415    force: bool,
416    callbacks: RemoteCallbacks<'_>,
417) -> Result<(), GitPushError> {
418    push_updates(
419        git_repo,
420        remote_name,
421        &[GitRefUpdate {
422            qualified_name: format!("refs/heads/{remote_branch}"),
423            force,
424            new_target: Some(target.id().clone()),
425        }],
426        callbacks,
427    )
428}
429
430pub struct GitRefUpdate {
431    pub qualified_name: String,
432    // TODO: We want this to be a `current_target: Option<CommitId>` for the expected current
433    // commit on the remote. It's a blunt "force" option instead until git2-rs supports the
434    // "push negotiation" callback (https://github.com/rust-lang/git2-rs/issues/733).
435    pub force: bool,
436    pub new_target: Option<CommitId>,
437}
438
439pub fn push_updates(
440    git_repo: &git2::Repository,
441    remote_name: &str,
442    updates: &[GitRefUpdate],
443    callbacks: RemoteCallbacks<'_>,
444) -> Result<(), GitPushError> {
445    let mut temp_refs = vec![];
446    let mut qualified_remote_refs = vec![];
447    let mut refspecs = vec![];
448    for update in updates {
449        qualified_remote_refs.push(update.qualified_name.as_str());
450        if let Some(new_target) = &update.new_target {
451            // Create a temporary ref to work around https://github.com/libgit2/libgit2/issues/3178
452            let temp_ref_name = format!("refs/jj/git-push/{}", new_target.hex());
453            temp_refs.push(git_repo.reference(
454                &temp_ref_name,
455                git2::Oid::from_bytes(new_target.as_bytes()).unwrap(),
456                true,
457                "temporary reference for git push",
458            )?);
459            refspecs.push(format!(
460                "{}{}:{}",
461                (if update.force { "+" } else { "" }),
462                temp_ref_name,
463                update.qualified_name
464            ));
465        } else {
466            refspecs.push(format!(":{}", update.qualified_name));
467        }
468    }
469    let result = push_refs(
470        git_repo,
471        remote_name,
472        &qualified_remote_refs,
473        &refspecs,
474        callbacks,
475    );
476    for mut temp_ref in temp_refs {
477        // TODO: Figure out how to do the equivalent of absl::Cleanup for
478        // temp_ref.delete().
479        if let Err(err) = temp_ref.delete() {
480            // Propagate error only if we don't already have an error to return and it's not
481            // NotFound (there may be duplicates if the list if multiple branches moved to
482            // the same commit).
483            if result.is_ok() && err.code() != git2::ErrorCode::NotFound {
484                return Err(GitPushError::InternalGitError(err));
485            }
486        }
487    }
488    result
489}
490
491fn push_refs(
492    git_repo: &git2::Repository,
493    remote_name: &str,
494    qualified_remote_refs: &[&str],
495    refspecs: &[String],
496    callbacks: RemoteCallbacks<'_>,
497) -> Result<(), GitPushError> {
498    let mut remote =
499        git_repo
500            .find_remote(remote_name)
501            .map_err(|err| match (err.class(), err.code()) {
502                (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => {
503                    GitPushError::NoSuchRemote(remote_name.to_string())
504                }
505                (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => {
506                    GitPushError::NoSuchRemote(remote_name.to_string())
507                }
508                _ => GitPushError::InternalGitError(err),
509            })?;
510    let mut remaining_remote_refs: HashSet<_> = qualified_remote_refs.iter().copied().collect();
511    let mut push_options = git2::PushOptions::new();
512    let mut proxy_options = git2::ProxyOptions::new();
513    proxy_options.auto();
514    push_options.proxy_options(proxy_options);
515    let mut callbacks = callbacks.into_git();
516    callbacks.push_update_reference(|refname, status| {
517        // The status is Some if the ref update was rejected
518        if status.is_none() {
519            remaining_remote_refs.remove(refname);
520        }
521        Ok(())
522    });
523    push_options.remote_callbacks(callbacks);
524    remote
525        .push(refspecs, Some(&mut push_options))
526        .map_err(|err| match (err.class(), err.code()) {
527            (git2::ErrorClass::Reference, git2::ErrorCode::NotFastForward) => {
528                GitPushError::NotFastForward
529            }
530            _ => GitPushError::InternalGitError(err),
531        })?;
532    drop(push_options);
533    if remaining_remote_refs.is_empty() {
534        Ok(())
535    } else {
536        Err(GitPushError::RefUpdateRejected(
537            remaining_remote_refs
538                .iter()
539                .sorted()
540                .map(|name| name.to_string())
541                .collect(),
542        ))
543    }
544}
545
546#[non_exhaustive]
547#[derive(Default)]
548#[allow(clippy::type_complexity)]
549pub struct RemoteCallbacks<'a> {
550    pub progress: Option<&'a mut dyn FnMut(&Progress)>,
551    pub get_ssh_key: Option<&'a mut dyn FnMut(&str) -> Option<PathBuf>>,
552    pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
553    pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
554}
555
556impl<'a> RemoteCallbacks<'a> {
557    fn into_git(mut self) -> git2::RemoteCallbacks<'a> {
558        let mut callbacks = git2::RemoteCallbacks::new();
559        if let Some(progress_cb) = self.progress {
560            callbacks.transfer_progress(move |progress| {
561                progress_cb(&Progress {
562                    bytes_downloaded: (progress.received_objects() < progress.total_objects())
563                        .then(|| progress.received_bytes() as u64),
564                    overall: (progress.indexed_objects() + progress.indexed_deltas()) as f32
565                        / (progress.total_objects() + progress.total_deltas()) as f32,
566                });
567                true
568            });
569        }
570        // TODO: We should expose the callbacks to the caller instead -- the library
571        // crate shouldn't read environment variables.
572        callbacks.credentials(move |url, username_from_url, allowed_types| {
573            let span = tracing::debug_span!("RemoteCallbacks.credentials");
574            let _ = span.enter();
575
576            let git_config = git2::Config::open_default();
577            let credential_helper = git_config
578                .and_then(|conf| git2::Cred::credential_helper(&conf, url, username_from_url));
579            if let Ok(creds) = credential_helper {
580                tracing::debug!("using credential_helper");
581                return Ok(creds);
582            } else if let Some(username) = username_from_url {
583                if allowed_types.contains(git2::CredentialType::SSH_KEY) {
584                    if std::env::var("SSH_AUTH_SOCK").is_ok()
585                        || std::env::var("SSH_AGENT_PID").is_ok()
586                    {
587                        tracing::debug!(username, "using ssh_key_from_agent");
588                        return git2::Cred::ssh_key_from_agent(username).map_err(|err| {
589                            tracing::error!(err = %err);
590                            err
591                        });
592                    }
593                    if let Some(ref mut cb) = self.get_ssh_key {
594                        if let Some(path) = cb(username) {
595                            tracing::debug!(username, path = ?path, "using ssh_key");
596                            return git2::Cred::ssh_key(username, None, &path, None).map_err(
597                                |err| {
598                                    tracing::error!(err = %err);
599                                    err
600                                },
601                            );
602                        }
603                    }
604                }
605                if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
606                    if let Some(ref mut cb) = self.get_password {
607                        if let Some(pw) = cb(url, username) {
608                            tracing::debug!(
609                                username,
610                                "using userpass_plaintext with username from url"
611                            );
612                            return git2::Cred::userpass_plaintext(username, &pw).map_err(|err| {
613                                tracing::error!(err = %err);
614                                err
615                            });
616                        }
617                    }
618                }
619            } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
620                if let Some(ref mut cb) = self.get_username_password {
621                    if let Some((username, pw)) = cb(url) {
622                        tracing::debug!(username, "using userpass_plaintext");
623                        return git2::Cred::userpass_plaintext(&username, &pw).map_err(|err| {
624                            tracing::error!(err = %err);
625                            err
626                        });
627                    }
628                }
629            }
630            tracing::debug!("using default");
631            git2::Cred::default()
632        });
633        callbacks
634    }
635}
636
637pub struct Progress {
638    /// `Some` iff data transfer is currently in progress
639    pub bytes_downloaded: Option<u64>,
640    pub overall: f32,
641}