Skip to content

Commit 350098d

Browse files
committed
Auto merge of #13250 - weihanglo:cargo-update, r=ehuss
fix(cargo-update): `--precise` accept arbitrary git revisions
2 parents fbf9251 + e6f2dfd commit 350098d

File tree

6 files changed

+175
-54
lines changed

6 files changed

+175
-54
lines changed

Diff for: src/cargo/core/source_id.rs

+1-12
Original file line numberDiff line numberDiff line change
@@ -461,22 +461,11 @@ impl SourceId {
461461

462462
pub fn precise_git_fragment(self) -> Option<&'static str> {
463463
match &self.inner.precise {
464-
Some(Precise::GitUrlFragment(s)) => Some(&s[..8]),
464+
Some(Precise::GitUrlFragment(s)) => Some(&s),
465465
_ => None,
466466
}
467467
}
468468

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-
}
476-
_ => None,
477-
})
478-
}
479-
480469
/// Creates a new `SourceId` from this source with the given `precise`.
481470
pub fn with_git_precise(self, fragment: Option<String>) -> SourceId {
482471
SourceId::wrap(SourceIdInner {

Diff for: src/cargo/ops/cargo_generate_lockfile.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes
167167
format!(
168168
"{} -> #{}",
169169
removed[0],
170-
&added[0].source_id().precise_git_fragment().unwrap()
170+
&added[0].source_id().precise_git_fragment().unwrap()[..8],
171171
)
172172
} else {
173173
format!("{} -> v{}", removed[0], added[0].version())

Diff for: src/cargo/sources/git/source.rs

+71-23
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,10 @@ use url::Url;
6868
pub struct GitSource<'cfg> {
6969
/// The git remote which we're going to fetch from.
7070
remote: GitRemote,
71-
/// The Git reference from the manifest file.
72-
manifest_reference: GitReference,
7371
/// 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>,
72+
///
73+
/// Expected to always be [`Revision::Locked`] after the Git repository is fetched.
74+
locked_rev: Revision,
7675
/// The unique identifier of this source.
7776
source_id: SourceId,
7877
/// The underlying path source to discover packages inside the Git repository.
@@ -102,8 +101,12 @@ impl<'cfg> GitSource<'cfg> {
102101
assert!(source_id.is_git(), "id is not git, id={}", source_id);
103102

104103
let remote = GitRemote::new(source_id.url());
105-
let manifest_reference = source_id.git_reference().unwrap().clone();
106-
let locked_rev = source_id.precise_git_oid()?;
104+
// Fallback to git ref from mainfest if there is no locked revision.
105+
let locked_rev = source_id
106+
.precise_git_fragment()
107+
.map(|s| Revision::new(s.into()))
108+
.unwrap_or_else(|| source_id.git_reference().unwrap().clone().into());
109+
107110
let ident = ident_shallow(
108111
&source_id,
109112
config
@@ -114,7 +117,6 @@ impl<'cfg> GitSource<'cfg> {
114117

115118
let source = GitSource {
116119
remote,
117-
manifest_reference,
118120
locked_rev,
119121
source_id,
120122
path_source: None,
@@ -155,6 +157,48 @@ impl<'cfg> GitSource<'cfg> {
155157
}
156158
}
157159

160+
/// Indicates a [Git revision] that might be locked or deferred to be resolved.
161+
///
162+
/// [Git revision]: https://git-scm.com/docs/revisions
163+
#[derive(Clone, Debug)]
164+
enum Revision {
165+
/// A [Git reference] that would trigger extra fetches when being resolved.
166+
///
167+
/// [Git reference]: https://git-scm.com/book/en/v2/Git-Internals-Git-References
168+
Deferred(GitReference),
169+
/// A locked revision of the actual Git commit object ID.
170+
Locked(git2::Oid),
171+
}
172+
173+
impl Revision {
174+
fn new(rev: &str) -> Revision {
175+
let oid = git2::Oid::from_str(rev).ok();
176+
match oid {
177+
// Git object ID is supposed to be a hex string of 20 (SHA1) or 32 (SHA256) bytes.
178+
// Its length must be double to the underlying bytes (40 or 64),
179+
// otherwise libgit2 would happily zero-pad the returned oid.
180+
// See rust-lang/cargo#13188
181+
Some(oid) if oid.as_bytes().len() * 2 == rev.len() => Revision::Locked(oid),
182+
_ => Revision::Deferred(GitReference::Rev(rev.to_string())),
183+
}
184+
}
185+
}
186+
187+
impl From<GitReference> for Revision {
188+
fn from(value: GitReference) -> Self {
189+
Revision::Deferred(value)
190+
}
191+
}
192+
193+
impl From<Revision> for GitReference {
194+
fn from(value: Revision) -> Self {
195+
match value {
196+
Revision::Deferred(git_ref) => git_ref,
197+
Revision::Locked(oid) => GitReference::Rev(oid.to_string()),
198+
}
199+
}
200+
}
201+
158202
/// Create an identifier from a URL,
159203
/// essentially turning `proto://host/path/repo` into `repo-<hash-of-url>`.
160204
fn ident(id: &SourceId) -> String {
@@ -191,9 +235,12 @@ impl<'cfg> Debug for GitSource<'cfg> {
191235
// TODO(-Znext-lockfile-bump): set it to true when stabilizing
192236
// lockfile v4, because we want Source ID serialization to be
193237
// consistent with lockfile.
194-
match self.manifest_reference.pretty_ref(false) {
195-
Some(s) => write!(f, " ({})", s),
196-
None => Ok(()),
238+
match &self.locked_rev {
239+
Revision::Deferred(git_ref) => match git_ref.pretty_ref(false) {
240+
Some(s) => write!(f, " ({})", s),
241+
None => Ok(()),
242+
},
243+
Revision::Locked(oid) => write!(f, " ({oid})"),
197244
}
198245
}
199246
}
@@ -252,16 +299,17 @@ impl<'cfg> Source for GitSource<'cfg> {
252299
let db_path = db_path.into_path_unlocked();
253300

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

260308
// If we're in offline mode, we're not locked, and we have a
261309
// database, then try to resolve our reference with the preexisting
262310
// repository.
263-
(None, Some(db)) if self.config.offline() => {
264-
let rev = db.resolve(&self.manifest_reference).with_context(|| {
311+
(Revision::Deferred(git_ref), Some(db)) if self.config.offline() => {
312+
let rev = db.resolve(&git_ref).with_context(|| {
265313
"failed to lookup reference in preexisting repository, and \
266314
can't check for updates in offline mode (--offline)"
267315
})?;
@@ -279,6 +327,7 @@ impl<'cfg> Source for GitSource<'cfg> {
279327
self.remote.url()
280328
);
281329
}
330+
282331
if !self.quiet {
283332
self.config.shell().status(
284333
"Updating",
@@ -288,13 +337,9 @@ impl<'cfg> Source for GitSource<'cfg> {
288337

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

291-
self.remote.checkout(
292-
&db_path,
293-
db,
294-
&self.manifest_reference,
295-
locked_rev,
296-
self.config,
297-
)?
340+
let locked_rev = locked_rev.clone().into();
341+
self.remote
342+
.checkout(&db_path, db, &locked_rev, self.config)?
298343
}
299344
};
300345

@@ -321,7 +366,7 @@ impl<'cfg> Source for GitSource<'cfg> {
321366

322367
self.path_source = Some(path_source);
323368
self.short_id = Some(short_id.as_str().into());
324-
self.locked_rev = Some(actual_rev);
369+
self.locked_rev = Revision::Locked(actual_rev);
325370
self.path_source.as_mut().unwrap().update()?;
326371

327372
// Hopefully this shouldn't incur too much of a performance hit since
@@ -350,7 +395,10 @@ impl<'cfg> Source for GitSource<'cfg> {
350395
}
351396

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

356404
fn describe(&self) -> String {

Diff for: src/cargo/sources/git/utils.rs

+2-14
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 {

Diff for: tests/testsuite/git.rs

+2-4
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();

Diff for: tests/testsuite/update.rs

+98
Original file line numberDiff line numberDiff line change
@@ -1272,3 +1272,101 @@ rustdns.workspace = true
12721272
)
12731273
.run();
12741274
}
1275+
1276+
#[cargo_test]
1277+
fn update_precise_git_revisions() {
1278+
let (git_project, git_repo) = git::new_repo("git", |p| {
1279+
p.file("Cargo.toml", &basic_lib_manifest("git"))
1280+
.file("src/lib.rs", "")
1281+
});
1282+
let tag_name = "Nazgûl";
1283+
git::tag(&git_repo, tag_name);
1284+
let tag_commit_id = git_repo.head().unwrap().target().unwrap().to_string();
1285+
1286+
git_project.change_file("src/lib.rs", "fn f() {}");
1287+
git::add(&git_repo);
1288+
let head_id = git::commit(&git_repo).to_string();
1289+
let short_id = &head_id[..8];
1290+
let url = git_project.url();
1291+
1292+
let p = project()
1293+
.file(
1294+
"Cargo.toml",
1295+
&format!(
1296+
r#"
1297+
[package]
1298+
name = "foo"
1299+
version = "0.1.0"
1300+
1301+
[dependencies]
1302+
git = {{ git = '{url}' }}
1303+
"#
1304+
),
1305+
)
1306+
.file("src/lib.rs", "")
1307+
.build();
1308+
1309+
p.cargo("fetch")
1310+
.with_stderr(format!("[UPDATING] git repository `{url}`"))
1311+
.run();
1312+
1313+
assert!(p.read_lockfile().contains(&head_id));
1314+
1315+
p.cargo("update git --precise")
1316+
.arg(tag_name)
1317+
.with_stderr(format!(
1318+
"\
1319+
[UPDATING] git repository `{url}`
1320+
[UPDATING] git v0.5.0 ([..]) -> #{}",
1321+
&tag_commit_id[..8],
1322+
))
1323+
.run();
1324+
1325+
assert!(p.read_lockfile().contains(&tag_commit_id));
1326+
assert!(!p.read_lockfile().contains(&head_id));
1327+
1328+
p.cargo("update git --precise")
1329+
.arg(short_id)
1330+
.with_stderr(format!(
1331+
"\
1332+
[UPDATING] git repository `{url}`
1333+
[UPDATING] git v0.5.0 ([..]) -> #{short_id}",
1334+
))
1335+
.run();
1336+
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],
1349+
))
1350+
.run();
1351+
1352+
assert!(p.read_lockfile().contains(&tag_commit_id));
1353+
assert!(!p.read_lockfile().contains(&head_id));
1354+
1355+
// Now make a tag looks like an oid.
1356+
// It requires a git fetch, as the oid cannot be found in preexisting git db.
1357+
let arbitrary_tag: String = std::iter::repeat('a').take(head_id.len()).collect();
1358+
git::tag(&git_repo, &arbitrary_tag);
1359+
1360+
p.cargo("update git --precise")
1361+
.arg(&arbitrary_tag)
1362+
.with_stderr(format!(
1363+
"\
1364+
[UPDATING] git repository `{url}`
1365+
[UPDATING] git v0.5.0 ([..]) -> #{}",
1366+
&head_id[..8],
1367+
))
1368+
.run();
1369+
1370+
assert!(p.read_lockfile().contains(&head_id));
1371+
assert!(!p.read_lockfile().contains(&tag_commit_id));
1372+
}

0 commit comments

Comments
 (0)