Skip to content

Commit 32f52fd

Browse files
committed
Warn when default branch is not master
This commit implements a warning in Cargo for when a dependency directive is using `DefaultBranch` but the default branch of the remote repository is not actually `master`. We will eventually break this dependency directive and the warning indicates the fix, which is to write down `branch = "master"`.
1 parent 538fb1b commit 32f52fd

File tree

4 files changed

+164
-12
lines changed

4 files changed

+164
-12
lines changed

src/cargo/sources/git/source.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,12 @@ impl<'cfg> Source for GitSource<'cfg> {
126126
// database, then try to resolve our reference with the preexisting
127127
// repository.
128128
(None, Some(db)) if self.config.offline() => {
129-
let rev = db.resolve(&self.manifest_reference).with_context(|| {
130-
"failed to lookup reference in preexisting repository, and \
131-
can't check for updates in offline mode (--offline)"
132-
})?;
129+
let rev = db
130+
.resolve(&self.manifest_reference, None)
131+
.with_context(|| {
132+
"failed to lookup reference in preexisting repository, and \
133+
can't check for updates in offline mode (--offline)"
134+
})?;
133135
(db, rev)
134136
}
135137

src/cargo/sources/git/utils.rs

Lines changed: 149 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,111 @@
1+
//! Utilities for handling git repositories, mainly around
2+
//! authentication/cloning.
3+
//!
4+
//! # `DefaultBranch` vs `Branch("master")`
5+
//!
6+
//! Long ago in a repository not so far away, an author (*cough* me *cough*)
7+
//! didn't understand how branches work in Git. This led the author to
8+
//! interpret these two dependency declarations the exact same way with the
9+
//! former literally internally desugaring to the latter:
10+
//!
11+
//! ```toml
12+
//! [dependencies]
13+
//! foo = { git = "https://example.org/foo" }
14+
//! foo = { git = "https://example.org/foo", branch = "master" }
15+
//! ```
16+
//!
17+
//! It turns out there's this things called `HEAD` in git remotes which points
18+
//! to the "main branch" of a repository, and the main branch is not always
19+
//! literally called master. What Cargo would like to do is to differentiate
20+
//! these two dependency directives, with the first meaning "depend on `HEAD`".
21+
//!
22+
//! Unfortunately implementing this is a breaking change. This was first
23+
//! attempted in #8364 but resulted in #8468 which has two independent bugs
24+
//! listed on that issue. Despite this breakage we would still like to roll out
25+
//! this change in Cargo, but we're now going to take it very slow and try to
26+
//! break as few people as possible along the way. These comments are intended
27+
//! to log the current progress and what wonkiness you might see within Cargo
28+
//! when handling `DefaultBranch` vs `Branch("master")`
29+
//!
30+
//! ### Repositories with `master` and a default branch
31+
//!
32+
//! This is one of the most obvious sources of breakage. If our `foo` example
33+
//! in above had two branches, one called `master` and another which was
34+
//! actually the main branch, then Cargo's change will always be a breaking
35+
//! change. This is because what's downloaded is an entirely different branch
36+
//! if we change the meaning of the dependency directive.
37+
//!
38+
//! It's expected this is quite rare, but to handle this case nonetheless when
39+
//! Cargo fetches from a git remote and the dependency specification is
40+
//! `DefaultBranch` then it will issue a warning if the `HEAD` reference
41+
//! doesn't match `master`. It's expected in this situation that authors will
42+
//! fix builds locally by specifying `branch = 'master'`.
43+
//!
44+
//! ### Differences in `cargo vendor` configuration
45+
//!
46+
//! When executing `cargo vendor` it will print out configuration which can
47+
//! then be used to configure Cargo to use the `vendor` directory. Historically
48+
//! this configuration looked like:
49+
//!
50+
//! ```toml
51+
//! [source."https://example.org/foo"]
52+
//! git = "https://example.org/foo"
53+
//! branch = "master"
54+
//! replace-with = "vendored-sources"
55+
//! ```
56+
//!
57+
//! We would like to, however, transition this to not include the `branch =
58+
//! "master"` unless the dependency directive actually mentions a branch.
59+
//! Conveniently older Cargo implementations all interpret a missing `branch`
60+
//! as `branch = "master"` so it's a backwards-compatible change to remove the
61+
//! `branch = "master"` directive. As a result, `cargo vendor` will no longer
62+
//! emit a `branch` if the git reference is `DefaultBranch`
63+
//!
64+
//! ### Differences in lock file formats
65+
//!
66+
//! Another issue pointed out in #8364 was that `Cargo.lock` files were no
67+
//! longer compatible on stable and nightly with each other. The underlying
68+
//! issue is that Cargo was serializing `branch = "master"` *differently* on
69+
//! nightly than it was on stable. Historical implementations of Cargo
70+
//! desugared `branch = "master"` to having not dependency directives in
71+
//! `Cargo.lock`, which means when reading `Cargo.lock` we can't differentiate
72+
//! what's for the default branch and what's for the `master` branch.
73+
//!
74+
//! To handle this difference in encoding of `Cargo.lock` we'll be employing
75+
//! the standard scheme to change `Cargo.lock`:
76+
//!
77+
//! * Add support in Cargo for a future format, don't turn it on.
78+
//! * Wait a long time
79+
//! * Turn on the future format
80+
//!
81+
//! Here the "future format" is `branch=master` shows up if you have a `branch`
82+
//! in `Cargo.toml`, and otherwise nothing shows up in URLs. Due to the effect
83+
//! on crate graph resolution, however, this flows into the next point..
84+
//!
85+
//! ### Unification in the Cargo dependency graph
86+
//!
87+
//! Today dependencies with `branch = "master"` will unify with dependencies
88+
//! that say nothing. (that's because the latter simply desugars). This means
89+
//! the two `foo` directives above will resolve to the same dependency.
90+
//!
91+
//! The best idea I've got to fix this is to basically get everyone (if anyone)
92+
//! to stop doing this today. The crate graph resolver will start to warn if it
93+
//! detects that multiple `Cargo.toml` directives are detected and mixed. The
94+
//! thinking is that when we turn on the new lock file format it'll also be
95+
//! hard breaking change for any project which still has dependencies to
96+
//! both the `master` branch and not.
97+
//!
98+
//! ### What we're doing today
99+
//!
100+
//! The general goal of Cargo today is to internally distinguish
101+
//! `DefaultBranch` and `Branch("master")`, but for the time being they should
102+
//! be functionally equivalent in terms of builds. The hope is that we'll let
103+
//! all these warnings and such bake for a good long time, and eventually we'll
104+
//! flip some switches if your build has no warnings it'll work before and
105+
//! after.
106+
//!
107+
//! That's the dream at least, we'll see how this plays out.
108+
1109
use crate::core::GitReference;
2110
use crate::util::errors::{CargoResult, CargoResultExt};
3111
use crate::util::paths;
@@ -74,7 +182,7 @@ impl GitRemote {
74182
}
75183

76184
pub fn rev_for(&self, path: &Path, reference: &GitReference) -> CargoResult<git2::Oid> {
77-
reference.resolve(&self.db_at(path)?.repo)
185+
reference.resolve(&self.db_at(path)?.repo, None)
78186
}
79187

80188
pub fn checkout(
@@ -99,7 +207,7 @@ impl GitRemote {
99207
}
100208
}
101209
None => {
102-
if let Ok(rev) = reference.resolve(&db.repo) {
210+
if let Ok(rev) = reference.resolve(&db.repo, Some((&self.url, cargo_config))) {
103211
return Ok((db, rev));
104212
}
105213
}
@@ -118,7 +226,7 @@ impl GitRemote {
118226
.context(format!("failed to clone into: {}", into.display()))?;
119227
let rev = match locked_rev {
120228
Some(rev) => rev,
121-
None => reference.resolve(&repo)?,
229+
None => reference.resolve(&repo, Some((&self.url, cargo_config)))?,
122230
};
123231

124232
Ok((
@@ -187,13 +295,21 @@ impl GitDatabase {
187295
self.repo.revparse_single(&oid.to_string()).is_ok()
188296
}
189297

190-
pub fn resolve(&self, r: &GitReference) -> CargoResult<git2::Oid> {
191-
r.resolve(&self.repo)
298+
pub fn resolve(
299+
&self,
300+
r: &GitReference,
301+
remote_and_config: Option<(&Url, &Config)>,
302+
) -> CargoResult<git2::Oid> {
303+
r.resolve(&self.repo, remote_and_config)
192304
}
193305
}
194306

195307
impl GitReference {
196-
pub fn resolve(&self, repo: &git2::Repository) -> CargoResult<git2::Oid> {
308+
pub fn resolve(
309+
&self,
310+
repo: &git2::Repository,
311+
remote_and_config: Option<(&Url, &Config)>,
312+
) -> CargoResult<git2::Oid> {
197313
let id = match self {
198314
// Note that we resolve the named tag here in sync with where it's
199315
// fetched into via `fetch` below.
@@ -227,6 +343,28 @@ impl GitReference {
227343
.get()
228344
.target()
229345
.ok_or_else(|| anyhow::format_err!("branch `master` did not have a target"))?;
346+
347+
if let Some((remote, config)) = remote_and_config {
348+
let head_id = repo.refname_to_id("refs/remotes/origin/HEAD")?;
349+
let head = repo.find_object(head_id, None)?;
350+
let head = head.peel(ObjectType::Commit)?.id();
351+
352+
if head != master {
353+
config.shell().warn(&format!(
354+
"\
355+
fetching `master` branch from `{}` but the `HEAD` \
356+
reference for this repository is not the \
357+
`master` branch. This behavior will change \
358+
in Cargo in the future and your build may \
359+
break, so it's recommended to place \
360+
`branch = \"master\"` in Cargo.toml when \
361+
depending on this git repository to ensure \
362+
that your build will continue to work.\
363+
",
364+
remote,
365+
))?;
366+
}
367+
}
230368
master
231369
}
232370

@@ -762,6 +900,7 @@ pub fn fetch(
762900
}
763901

764902
GitReference::DefaultBranch => {
903+
// See the module docs for why we're fetching `master` here.
765904
refspecs.push(format!("refs/heads/master:refs/remotes/origin/master"));
766905
refspecs.push(String::from("HEAD:refs/remotes/origin/HEAD"));
767906
}
@@ -1032,7 +1171,10 @@ fn github_up_to_date(
10321171
handle.useragent("cargo")?;
10331172
let mut headers = List::new();
10341173
headers.append("Accept: application/vnd.github.3.sha")?;
1035-
headers.append(&format!("If-None-Match: \"{}\"", reference.resolve(repo)?))?;
1174+
headers.append(&format!(
1175+
"If-None-Match: \"{}\"",
1176+
reference.resolve(repo, None)?
1177+
))?;
10361178
handle.http_headers(headers)?;
10371179
handle.perform()?;
10381180
Ok(handle.response_code()? == 304)

src/cargo/sources/registry/remote.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ impl<'cfg> RemoteRegistry<'cfg> {
102102
fn head(&self) -> CargoResult<git2::Oid> {
103103
if self.head.get().is_none() {
104104
let repo = self.repo()?;
105-
let oid = self.index_git_ref.resolve(repo)?;
105+
let oid = self.index_git_ref.resolve(repo, None)?;
106106
self.head.set(Some(oid));
107107
}
108108
Ok(self.head.get().unwrap())

tests/testsuite/git.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2831,6 +2831,14 @@ fn default_not_master() {
28312831
.with_stderr(
28322832
"\
28332833
[UPDATING] git repository `[..]`
2834+
warning: fetching `master` branch from `[..]` but the `HEAD` \
2835+
reference for this repository is not the \
2836+
`master` branch. This behavior will change \
2837+
in Cargo in the future and your build may \
2838+
break, so it's recommended to place \
2839+
`branch = \"master\"` in Cargo.toml when \
2840+
depending on this git repository to ensure \
2841+
that your build will continue to work.
28342842
[COMPILING] dep1 v0.5.0 ([..])
28352843
[COMPILING] foo v0.5.0 ([..])
28362844
[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]",

0 commit comments

Comments
 (0)