cargo/sources/git/
source.rs

1use std::fmt::{self, Debug, Formatter};
2
3use log::trace;
4use url::Url;
5
6use crate::core::source::{MaybePackage, Source, SourceId};
7use crate::core::GitReference;
8use crate::core::{Dependency, Package, PackageId, Summary};
9use crate::sources::git::utils::{GitRemote, GitRevision};
10use crate::sources::PathSource;
11use crate::util::errors::CargoResult;
12use crate::util::hex::short_hash;
13use crate::util::Config;
14
15pub struct GitSource<'cfg> {
16    remote: GitRemote,
17    reference: GitReference,
18    source_id: SourceId,
19    path_source: Option<PathSource<'cfg>>,
20    rev: Option<GitRevision>,
21    ident: String,
22    config: &'cfg Config,
23}
24
25impl<'cfg> GitSource<'cfg> {
26    pub fn new(source_id: SourceId, config: &'cfg Config) -> CargoResult<GitSource<'cfg>> {
27        assert!(source_id.is_git(), "id is not git, id={}", source_id);
28
29        let remote = GitRemote::new(source_id.url());
30        let ident = ident(&source_id);
31
32        let reference = match source_id.precise() {
33            Some(s) => GitReference::Rev(s.to_string()),
34            None => source_id.git_reference().unwrap().clone(),
35        };
36
37        let source = GitSource {
38            remote,
39            reference,
40            source_id,
41            path_source: None,
42            rev: None,
43            ident,
44            config,
45        };
46
47        Ok(source)
48    }
49
50    pub fn url(&self) -> &Url {
51        self.remote.url()
52    }
53
54    pub fn read_packages(&mut self) -> CargoResult<Vec<Package>> {
55        if self.path_source.is_none() {
56            self.update()?;
57        }
58        self.path_source.as_mut().unwrap().read_packages()
59    }
60}
61
62fn ident(id: &SourceId) -> String {
63    let ident = id
64        .canonical_url()
65        .raw_canonicalized_url()
66        .path_segments()
67        .and_then(|s| s.rev().next())
68        .unwrap_or("");
69
70    let ident = if ident == "" { "_empty" } else { ident };
71
72    format!("{}-{}", ident, short_hash(id.canonical_url()))
73}
74
75impl<'cfg> Debug for GitSource<'cfg> {
76    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
77        write!(f, "git repo at {}", self.remote.url())?;
78
79        match self.reference.pretty_ref() {
80            Some(s) => write!(f, " ({})", s),
81            None => Ok(()),
82        }
83    }
84}
85
86impl<'cfg> Source for GitSource<'cfg> {
87    fn query(&mut self, dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
88        let src = self
89            .path_source
90            .as_mut()
91            .expect("BUG: `update()` must be called before `query()`");
92        src.query(dep, f)
93    }
94
95    fn fuzzy_query(&mut self, dep: &Dependency, f: &mut dyn FnMut(Summary)) -> CargoResult<()> {
96        let src = self
97            .path_source
98            .as_mut()
99            .expect("BUG: `update()` must be called before `query()`");
100        src.fuzzy_query(dep, f)
101    }
102
103    fn supports_checksums(&self) -> bool {
104        false
105    }
106
107    fn requires_precise(&self) -> bool {
108        true
109    }
110
111    fn source_id(&self) -> SourceId {
112        self.source_id
113    }
114
115    fn update(&mut self) -> CargoResult<()> {
116        let git_path = self.config.git_path();
117        let git_path = self.config.assert_package_cache_locked(&git_path);
118        let db_path = git_path.join("db").join(&self.ident);
119
120        if self.config.offline() && !db_path.exists() {
121            anyhow::bail!(
122                "can't checkout from '{}': you are in the offline mode (--offline)",
123                self.remote.url()
124            );
125        }
126
127        // Resolve our reference to an actual revision, and check if the
128        // database already has that revision. If it does, we just load a
129        // database pinned at that revision, and if we don't we issue an update
130        // to try to find the revision.
131        let actual_rev = self.remote.rev_for(&db_path, &self.reference);
132        let should_update = actual_rev.is_err() || self.source_id.precise().is_none();
133
134        let (db, actual_rev) = if should_update && !self.config.offline() {
135            self.config.shell().status(
136                "Updating",
137                format!("git repository `{}`", self.remote.url()),
138            )?;
139
140            trace!("updating git source `{:?}`", self.remote);
141
142            self.remote
143                .checkout(&db_path, &self.reference, self.config)?
144        } else {
145            (self.remote.db_at(&db_path)?, actual_rev.unwrap())
146        };
147
148        // Don’t use the full hash, in order to contribute less to reaching the
149        // path length limit on Windows. See
150        // <https://github.com/servo/servo/pull/14397>.
151        let short_id = db.to_short_id(&actual_rev).unwrap();
152
153        let checkout_path = git_path
154            .join("checkouts")
155            .join(&self.ident)
156            .join(short_id.as_str());
157
158        // Copy the database to the checkout location.
159        db.copy_to(actual_rev.clone(), &checkout_path, self.config)?;
160
161        let source_id = self.source_id.with_precise(Some(actual_rev.to_string()));
162        let path_source = PathSource::new_recursive(&checkout_path, source_id, self.config);
163
164        self.path_source = Some(path_source);
165        self.rev = Some(actual_rev);
166        self.path_source.as_mut().unwrap().update()
167    }
168
169    fn download(&mut self, id: PackageId) -> CargoResult<MaybePackage> {
170        trace!(
171            "getting packages for package ID `{}` from `{:?}`",
172            id,
173            self.remote
174        );
175        self.path_source
176            .as_mut()
177            .expect("BUG: `update()` must be called before `get()`")
178            .download(id)
179    }
180
181    fn finish_download(&mut self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
182        panic!("no download should have started")
183    }
184
185    fn fingerprint(&self, _pkg: &Package) -> CargoResult<String> {
186        Ok(self.rev.as_ref().unwrap().to_string())
187    }
188
189    fn describe(&self) -> String {
190        format!("Git repository {}", self.source_id)
191    }
192
193    fn add_to_yanked_whitelist(&mut self, _pkgs: &[PackageId]) {}
194
195    fn is_yanked(&mut self, _pkg: PackageId) -> CargoResult<bool> {
196        Ok(false)
197    }
198}
199
200#[cfg(test)]
201mod test {
202    use super::ident;
203    use crate::core::{GitReference, SourceId};
204    use crate::util::IntoUrl;
205
206    #[test]
207    pub fn test_url_to_path_ident_with_path() {
208        let ident = ident(&src("https://github.com/carlhuda/cargo"));
209        assert!(ident.starts_with("cargo-"));
210    }
211
212    #[test]
213    pub fn test_url_to_path_ident_without_path() {
214        let ident = ident(&src("https://github.com"));
215        assert!(ident.starts_with("_empty-"));
216    }
217
218    #[test]
219    fn test_canonicalize_idents_by_stripping_trailing_url_slash() {
220        let ident1 = ident(&src("https://github.com/PistonDevelopers/piston/"));
221        let ident2 = ident(&src("https://github.com/PistonDevelopers/piston"));
222        assert_eq!(ident1, ident2);
223    }
224
225    #[test]
226    fn test_canonicalize_idents_by_lowercasing_github_urls() {
227        let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
228        let ident2 = ident(&src("https://github.com/pistondevelopers/piston"));
229        assert_eq!(ident1, ident2);
230    }
231
232    #[test]
233    fn test_canonicalize_idents_by_stripping_dot_git() {
234        let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
235        let ident2 = ident(&src("https://github.com/PistonDevelopers/piston.git"));
236        assert_eq!(ident1, ident2);
237    }
238
239    #[test]
240    fn test_canonicalize_idents_different_protocols() {
241        let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
242        let ident2 = ident(&src("git://github.com/PistonDevelopers/piston"));
243        assert_eq!(ident1, ident2);
244    }
245
246    fn src(s: &str) -> SourceId {
247        SourceId::for_git(
248            &s.into_url().unwrap(),
249            GitReference::Branch("master".to_string()),
250        )
251        .unwrap()
252    }
253}