cargo/sources/git/
source.rs1use 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 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 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 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}