Skip to content

Commit 2734097

Browse files
committed
fix(git): represent deferred and resolved revision with an enum
`cargo update --precise` might pass in any arbitrary Git reference, and `git2::Oid::from_str` would always zero-pad the given str if it is not a full SHA hex string. This introduces an enum `Revision` to represent `locked_rev` that is either deferred or resolved to an actual object ID. When `locked_rev` is a short ID or any other Git reference, Cargo first performs a Git fetch to resolve it (without --offline), and then locks it to an actual commit object ID.
1 parent 4be3614 commit 2734097

File tree

5 files changed

+99
-58
lines changed

5 files changed

+99
-58
lines changed

src/cargo/core/source_id.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -460,21 +460,14 @@ impl SourceId {
460460
}
461461

462462
pub fn precise_git_fragment(self) -> Option<&'static str> {
463-
match &self.inner.precise {
464-
Some(Precise::GitUrlFragment(s)) => Some(&s[..8]),
465-
_ => None,
466-
}
463+
self.precise_full_git_fragment().map(|s| &s[..8])
467464
}
468465

469-
pub fn precise_git_oid(self) -> CargoResult<Option<git2::Oid>> {
470-
Ok(match self.inner.precise.as_ref() {
471-
Some(Precise::GitUrlFragment(s)) => {
472-
Some(git2::Oid::from_str(s).with_context(|| {
473-
format!("precise value for git is not a git revision: {}", s)
474-
})?)
475-
}
466+
pub fn precise_full_git_fragment(self) -> Option<&'static str> {
467+
match &self.inner.precise {
468+
Some(Precise::GitUrlFragment(s)) => Some(&s),
476469
_ => None,
477-
})
470+
}
478471
}
479472

480473
/// Creates a new `SourceId` from this source with the given `precise`.

src/cargo/sources/git/source.rs

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ pub struct GitSource<'cfg> {
7171
/// The Git reference from the manifest file.
7272
manifest_reference: GitReference,
7373
/// The revision which a git source is locked to.
74-
/// This is expected to be set after the Git repository is fetched.
75-
locked_rev: Option<git2::Oid>,
74+
///
75+
/// Expected to always be [`Revision::Locked`] after the Git repository is fetched.
76+
locked_rev: Revision,
7677
/// The unique identifier of this source.
7778
source_id: SourceId,
7879
/// The underlying path source to discover packages inside the Git repository.
@@ -103,7 +104,11 @@ impl<'cfg> GitSource<'cfg> {
103104

104105
let remote = GitRemote::new(source_id.url());
105106
let manifest_reference = source_id.git_reference().unwrap().clone();
106-
let locked_rev = source_id.precise_git_oid()?;
107+
let locked_rev = source_id
108+
.precise_full_git_fragment()
109+
.map(|s| Revision::new(s.into()))
110+
.unwrap_or_else(|| source_id.git_reference().unwrap().clone().into());
111+
107112
let ident = ident_shallow(
108113
&source_id,
109114
config
@@ -155,6 +160,49 @@ impl<'cfg> GitSource<'cfg> {
155160
}
156161
}
157162

163+
/// Indicates a [Git revision] that might be locked or deferred to be resolved.
164+
///
165+
/// [Git revision]: https://git-scm.com/docs/revisions
166+
#[derive(Clone, Debug)]
167+
enum Revision {
168+
/// A [Git reference] that would trigger extra fetches when being resolved.
169+
///
170+
/// [Git reference]: https://git-scm.com/book/en/v2/Git-Internals-Git-References
171+
Deferred(GitReference),
172+
/// A locked revision of the actual Git commit object ID.
173+
Locked(git2::Oid),
174+
}
175+
176+
impl Revision {
177+
fn new(rev: &str) -> Revision {
178+
let oid = git2::Oid::from_str(rev).ok();
179+
match oid {
180+
// Git object ID is supposed to be a hex string of 20 (SHA1) or 32 (SHA256) bytes.
181+
// Its length must be double to the underlying bytes (40 or 64),
182+
// otherwise libgit2 would happily zero-pad the returned oid.
183+
// See rust-lang/cargo#13188
184+
Some(oid) if oid.as_bytes().len() * 2 == rev.len() => Revision::Locked(oid),
185+
_ => Revision::Deferred(GitReference::Rev(rev.to_string())),
186+
}
187+
}
188+
}
189+
190+
impl From<GitReference> for Revision {
191+
fn from(value: GitReference) -> Self {
192+
Revision::Deferred(value)
193+
}
194+
}
195+
196+
impl From<Revision> for GitReference {
197+
fn from(value: Revision) -> Self {
198+
match value {
199+
Revision::Deferred(git_ref) => git_ref,
200+
Revision::Locked(oid) => GitReference::Rev(oid.to_string()),
201+
}
202+
}
203+
}
204+
205+
158206
/// Create an identifier from a URL,
159207
/// essentially turning `proto://host/path/repo` into `repo-<hash-of-url>`.
160208
fn ident(id: &SourceId) -> String {
@@ -252,16 +300,17 @@ impl<'cfg> Source for GitSource<'cfg> {
252300
let db_path = db_path.into_path_unlocked();
253301

254302
let db = self.remote.db_at(&db_path).ok();
255-
let (db, actual_rev) = match (self.locked_rev, db) {
303+
304+
let (db, actual_rev) = match (&self.locked_rev, db) {
256305
// If we have a locked revision, and we have a preexisting database
257306
// which has that revision, then no update needs to happen.
258-
(Some(rev), Some(db)) if db.contains(rev) => (db, rev),
307+
(Revision::Locked(oid), Some(db)) if db.contains(*oid) => (db, *oid),
259308

260309
// If we're in offline mode, we're not locked, and we have a
261310
// database, then try to resolve our reference with the preexisting
262311
// repository.
263-
(None, Some(db)) if self.config.offline() => {
264-
let rev = db.resolve(&self.manifest_reference).with_context(|| {
312+
(Revision::Deferred(git_ref), Some(db)) if self.config.offline() => {
313+
let rev = db.resolve(&git_ref).with_context(|| {
265314
"failed to lookup reference in preexisting repository, and \
266315
can't check for updates in offline mode (--offline)"
267316
})?;
@@ -279,6 +328,7 @@ impl<'cfg> Source for GitSource<'cfg> {
279328
self.remote.url()
280329
);
281330
}
331+
282332
if !self.quiet {
283333
self.config.shell().status(
284334
"Updating",
@@ -288,13 +338,9 @@ impl<'cfg> Source for GitSource<'cfg> {
288338

289339
trace!("updating git source `{:?}`", self.remote);
290340

291-
self.remote.checkout(
292-
&db_path,
293-
db,
294-
&self.manifest_reference,
295-
locked_rev,
296-
self.config,
297-
)?
341+
let locked_rev = locked_rev.clone().into();
342+
self.remote
343+
.checkout(&db_path, db, &locked_rev, self.config)?
298344
}
299345
};
300346

@@ -321,7 +367,7 @@ impl<'cfg> Source for GitSource<'cfg> {
321367

322368
self.path_source = Some(path_source);
323369
self.short_id = Some(short_id.as_str().into());
324-
self.locked_rev = Some(actual_rev);
370+
self.locked_rev = Revision::Locked(actual_rev);
325371
self.path_source.as_mut().unwrap().update()?;
326372

327373
// Hopefully this shouldn't incur too much of a performance hit since
@@ -350,7 +396,10 @@ impl<'cfg> Source for GitSource<'cfg> {
350396
}
351397

352398
fn fingerprint(&self, _pkg: &Package) -> CargoResult<String> {
353-
Ok(self.locked_rev.as_ref().unwrap().to_string())
399+
match &self.locked_rev {
400+
Revision::Locked(oid) => Ok(oid.to_string()),
401+
_ => unreachable!("locked_rev must be resolved when computing fingerprint"),
402+
}
354403
}
355404

356405
fn describe(&self) -> String {

src/cargo/sources/git/utils.rs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ impl GitRemote {
9595
/// This ensures that it gets the up-to-date commit when a named reference
9696
/// is given (tag, branch, refs/*). Thus, network connection is involved.
9797
///
98-
/// When `locked_rev` is provided, it takes precedence over `reference`.
99-
///
10098
/// If we have a previous instance of [`GitDatabase`] then fetch into that
10199
/// if we can. If that can successfully load our revision then we've
102100
/// populated the database with the latest version of `reference`, so
@@ -106,11 +104,8 @@ impl GitRemote {
106104
into: &Path,
107105
db: Option<GitDatabase>,
108106
reference: &GitReference,
109-
locked_rev: Option<git2::Oid>,
110107
cargo_config: &Config,
111108
) -> CargoResult<(GitDatabase, git2::Oid)> {
112-
let locked_ref = locked_rev.map(|oid| GitReference::Rev(oid.to_string()));
113-
let reference = locked_ref.as_ref().unwrap_or(reference);
114109
if let Some(mut db) = db {
115110
fetch(
116111
&mut db.repo,
@@ -121,11 +116,7 @@ impl GitRemote {
121116
)
122117
.with_context(|| format!("failed to fetch into: {}", into.display()))?;
123118

124-
let resolved_commit_hash = match locked_rev {
125-
Some(rev) => db.contains(rev).then_some(rev),
126-
None => resolve_ref(reference, &db.repo).ok(),
127-
};
128-
if let Some(rev) = resolved_commit_hash {
119+
if let Some(rev) = resolve_ref(reference, &db.repo).ok() {
129120
return Ok((db, rev));
130121
}
131122
}
@@ -146,10 +137,7 @@ impl GitRemote {
146137
RemoteKind::GitDependency,
147138
)
148139
.with_context(|| format!("failed to clone into: {}", into.display()))?;
149-
let rev = match locked_rev {
150-
Some(rev) => rev,
151-
None => resolve_ref(reference, &repo)?,
152-
};
140+
let rev = resolve_ref(reference, &repo)?;
153141

154142
Ok((
155143
GitDatabase {

tests/testsuite/git.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -757,13 +757,11 @@ fn update_with_shared_deps() {
757757
.with_status(101)
758758
.with_stderr(
759759
"\
760+
[UPDATING] git repository [..]
760761
[ERROR] Unable to update [..]
761762
762763
Caused by:
763-
precise value for git is not a git revision: 0.1.2
764-
765-
Caused by:
766-
unable to parse OID - contains invalid characters; class=Invalid (3)
764+
revspec '0.1.2' not found; class=Reference (4); code=NotFound (-3)
767765
",
768766
)
769767
.run();

tests/testsuite/update.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,7 @@ fn update_precise_git_revisions() {
12811281
});
12821282
let tag_name = "Nazgûl";
12831283
git::tag(&git_repo, tag_name);
1284+
let tag_commit_id = git_repo.head().unwrap().target().unwrap().to_string();
12841285

12851286
git_project.change_file("src/lib.rs", "fn f() {}");
12861287
git::add(&git_repo);
@@ -1313,29 +1314,41 @@ fn update_precise_git_revisions() {
13131314

13141315
p.cargo("update git --precise")
13151316
.arg(tag_name)
1316-
.with_status(101)
13171317
.with_stderr(format!(
13181318
"\
1319-
[ERROR] Unable to update {url}#{tag_name}
1320-
1321-
Caused by:
1322-
precise value for git is not a git revision: {tag_name}
1323-
1324-
Caused by:
1325-
[..]"
1319+
[UPDATING] git repository `{url}`
1320+
[UPDATING] git v0.5.0 ([..]) -> #{}",
1321+
&tag_commit_id[..8],
13261322
))
13271323
.run();
13281324

1325+
assert!(p.read_lockfile().contains(&tag_commit_id));
1326+
assert!(!p.read_lockfile().contains(&head_id));
1327+
13291328
p.cargo("update git --precise")
13301329
.arg(short_id)
1331-
.with_status(101)
13321330
.with_stderr(format!(
13331331
"\
13341332
[UPDATING] git repository `{url}`
1335-
[ERROR] Unable to update {url}#{short_id}
1333+
[UPDATING] git v0.5.0 ([..]) -> #{short_id}",
1334+
))
1335+
.run();
13361336

1337-
Caused by:
1338-
object not found - no match for id ({short_id}00000000000000[..]); [..]",
1337+
assert!(p.read_lockfile().contains(&head_id));
1338+
assert!(!p.read_lockfile().contains(&tag_commit_id));
1339+
1340+
// updating back to tag still requires a git fetch,
1341+
// as the ref may change over time.
1342+
p.cargo("update git --precise")
1343+
.arg(tag_name)
1344+
.with_stderr(format!(
1345+
"\
1346+
[UPDATING] git repository `{url}`
1347+
[UPDATING] git v0.5.0 ([..]) -> #{}",
1348+
&tag_commit_id[..8],
13391349
))
13401350
.run();
1351+
1352+
assert!(p.read_lockfile().contains(&tag_commit_id));
1353+
assert!(!p.read_lockfile().contains(&head_id));
13411354
}

0 commit comments

Comments
 (0)