Skip to content

Commit ffa147d

Browse files
authored
Auto merge of #2781 - alexcrichton:block-if-dirty, r=brson
Prevent packaging a crate if any files are dirty This commit alters Cargo's behavior to prevent publishing a crate by default if any files in that crate are determined to be dirty, that is either modified or not part of the working tree. This can prevent common mistakes like many listed in #2063 and enables features like #841. Closes #1597 Closes #2063
2 parents aec2473 + 088d14a commit ffa147d

File tree

7 files changed

+262
-48
lines changed

7 files changed

+262
-48
lines changed

src/bin/package.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct Options {
1111
flag_no_verify: bool,
1212
flag_no_metadata: bool,
1313
flag_list: bool,
14+
flag_allow_dirty: bool,
1415
}
1516

1617
pub const USAGE: &'static str = "
@@ -24,6 +25,7 @@ Options:
2425
-l, --list Print files included in a package without making one
2526
--no-verify Don't verify the contents by building them
2627
--no-metadata Ignore warnings about a lack of human-usable metadata
28+
--allow-dirty Allow dirty working directories to be packaged
2729
--manifest-path PATH Path to the manifest to compile
2830
-v, --verbose Use verbose output
2931
-q, --quiet No output printed to stdout
@@ -36,9 +38,12 @@ pub fn execute(options: Options, config: &Config) -> CliResult<Option<()>> {
3638
options.flag_quiet,
3739
&options.flag_color));
3840
let root = try!(find_root_manifest_for_wd(options.flag_manifest_path, config.cwd()));
39-
try!(ops::package(&root, config,
40-
!options.flag_no_verify,
41-
options.flag_list,
42-
!options.flag_no_metadata));
41+
try!(ops::package(&root, &ops::PackageOpts {
42+
config: config,
43+
verify: !options.flag_no_verify,
44+
list: options.flag_list,
45+
check_metadata: !options.flag_no_metadata,
46+
allow_dirty: options.flag_allow_dirty,
47+
}));
4348
Ok(None)
4449
}

src/bin/publish.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct Options {
1111
flag_quiet: Option<bool>,
1212
flag_color: Option<String>,
1313
flag_no_verify: bool,
14+
flag_allow_dirty: bool,
1415
}
1516

1617
pub const USAGE: &'static str = "
@@ -24,6 +25,7 @@ Options:
2425
--host HOST Host to upload the package to
2526
--token TOKEN Token to use when uploading
2627
--no-verify Don't verify package tarball before publish
28+
--allow-dirty Allow publishing with a dirty source directory
2729
--manifest-path PATH Path to the manifest of the package to publish
2830
-v, --verbose Use verbose output
2931
-q, --quiet No output printed to stdout
@@ -40,10 +42,17 @@ pub fn execute(options: Options, config: &Config) -> CliResult<Option<()>> {
4042
flag_host: host,
4143
flag_manifest_path,
4244
flag_no_verify: no_verify,
45+
flag_allow_dirty: allow_dirty,
4346
..
4447
} = options;
4548

4649
let root = try!(find_root_manifest_for_wd(flag_manifest_path.clone(), config.cwd()));
47-
try!(ops::publish(&root, config, token, host, !no_verify));
50+
try!(ops::publish(&root, &ops::PublishOpts {
51+
config: config,
52+
token: token,
53+
index: host,
54+
verify: !no_verify,
55+
allow_dirty: allow_dirty,
56+
}));
4857
Ok(None)
4958
}

src/cargo/ops/cargo_package.rs

+65-9
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,37 @@ use std::io::SeekFrom;
33
use std::io::prelude::*;
44
use std::path::{self, Path};
55

6-
use tar::{Archive, Builder, Header};
7-
use flate2::{GzBuilder, Compression};
86
use flate2::read::GzDecoder;
7+
use flate2::{GzBuilder, Compression};
8+
use git2;
9+
use tar::{Archive, Builder, Header};
910

1011
use core::{SourceId, Package, PackageId};
1112
use sources::PathSource;
1213
use util::{self, CargoResult, human, internal, ChainError, Config, FileLock};
1314
use ops;
1415

16+
pub struct PackageOpts<'cfg> {
17+
pub config: &'cfg Config,
18+
pub list: bool,
19+
pub check_metadata: bool,
20+
pub allow_dirty: bool,
21+
pub verify: bool,
22+
}
23+
1524
pub fn package(manifest_path: &Path,
16-
config: &Config,
17-
verify: bool,
18-
list: bool,
19-
metadata: bool) -> CargoResult<Option<FileLock>> {
25+
opts: &PackageOpts) -> CargoResult<Option<FileLock>> {
26+
let config = opts.config;
2027
let path = manifest_path.parent().unwrap();
2128
let id = try!(SourceId::for_path(path));
2229
let mut src = PathSource::new(path, &id, config);
2330
let pkg = try!(src.root_package());
2431

25-
if metadata {
32+
if opts.check_metadata {
2633
try!(check_metadata(&pkg, config));
2734
}
2835

29-
if list {
36+
if opts.list {
3037
let root = pkg.root();
3138
let mut list: Vec<_> = try!(src.list_files(&pkg)).iter().map(|file| {
3239
util::without_prefix(&file, &root).unwrap().to_path_buf()
@@ -38,6 +45,10 @@ pub fn package(manifest_path: &Path,
3845
return Ok(None)
3946
}
4047

48+
if !opts.allow_dirty {
49+
try!(check_not_dirty(&pkg, &src));
50+
}
51+
4152
let filename = format!("{}-{}.crate", pkg.name(), pkg.version());
4253
let dir = config.target_dir(&pkg).join("package");
4354
let mut dst = match dir.open_ro(&filename, config, "packaged crate") {
@@ -57,7 +68,7 @@ pub fn package(manifest_path: &Path,
5768
try!(tar(&pkg, &src, config, dst.file(), &filename).chain_error(|| {
5869
human("failed to prepare local package for uploading")
5970
}));
60-
if verify {
71+
if opts.verify {
6172
try!(dst.seek(SeekFrom::Start(0)));
6273
try!(run_verify(config, &pkg, dst.file()).chain_error(|| {
6374
human("failed to verify package tarball")
@@ -109,6 +120,51 @@ fn check_metadata(pkg: &Package, config: &Config) -> CargoResult<()> {
109120
Ok(())
110121
}
111122

123+
fn check_not_dirty(p: &Package, src: &PathSource) -> CargoResult<()> {
124+
if let Ok(repo) = git2::Repository::discover(p.root()) {
125+
if let Some(workdir) = repo.workdir() {
126+
debug!("found a git repo at {:?}, checking if index present",
127+
workdir);
128+
let path = p.manifest_path();
129+
let path = path.strip_prefix(workdir).unwrap_or(path);
130+
if let Ok(status) = repo.status_file(path) {
131+
if (status & git2::STATUS_IGNORED).is_empty() {
132+
debug!("Cargo.toml found in repo, checking if dirty");
133+
return git(p, src, &repo)
134+
}
135+
}
136+
}
137+
}
138+
139+
// No VCS recognized, we don't know if the directory is dirty or not, so we
140+
// have to assume that it's clean.
141+
return Ok(());
142+
143+
fn git(p: &Package,
144+
src: &PathSource,
145+
repo: &git2::Repository) -> CargoResult<()> {
146+
let workdir = repo.workdir().unwrap();
147+
let dirty = try!(src.list_files(p)).iter().filter(|file| {
148+
let relative = file.strip_prefix(workdir).unwrap();
149+
if let Ok(status) = repo.status_file(relative) {
150+
status != git2::STATUS_CURRENT
151+
} else {
152+
false
153+
}
154+
}).map(|path| {
155+
path.strip_prefix(p.root()).unwrap_or(path).display().to_string()
156+
}).collect::<Vec<_>>();
157+
if dirty.is_empty() {
158+
Ok(())
159+
} else {
160+
bail!("{} dirty files found in the working directory:\n\n{}\n\n\
161+
to publish despite this, pass `--allow-dirty` to \
162+
`cargo publish`",
163+
dirty.len(), dirty.join("\n"))
164+
}
165+
}
166+
}
167+
112168
fn tar(pkg: &Package,
113169
src: &PathSource,
114170
config: &Config,

src/cargo/ops/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ pub use self::cargo_generate_lockfile::{update_lockfile};
1515
pub use self::cargo_generate_lockfile::UpdateOptions;
1616
pub use self::lockfile::{load_pkg_lockfile, write_pkg_lockfile};
1717
pub use self::cargo_test::{run_tests, run_benches, TestOptions};
18-
pub use self::cargo_package::package;
18+
pub use self::cargo_package::{package, PackageOpts};
1919
pub use self::registry::{publish, registry_configuration, RegistryConfig};
2020
pub use self::registry::{registry_login, search, http_proxy_exists, http_handle};
21-
pub use self::registry::{modify_owners, yank, OwnersOptions};
21+
pub use self::registry::{modify_owners, yank, OwnersOptions, PublishOpts};
2222
pub use self::cargo_fetch::{fetch, get_resolved_packages};
2323
pub use self::cargo_pkgid::pkgid;
2424
pub use self::resolve::{resolve_pkg, resolve_with_previous};

src/cargo/ops/registry.rs

+21-10
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,39 @@ pub struct RegistryConfig {
2929
pub token: Option<String>,
3030
}
3131

32-
pub fn publish(manifest_path: &Path,
33-
config: &Config,
34-
token: Option<String>,
35-
index: Option<String>,
36-
verify: bool) -> CargoResult<()> {
37-
let pkg = try!(Package::for_path(&manifest_path, config));
32+
pub struct PublishOpts<'cfg> {
33+
pub config: &'cfg Config,
34+
pub token: Option<String>,
35+
pub index: Option<String>,
36+
pub verify: bool,
37+
pub allow_dirty: bool,
38+
}
39+
40+
pub fn publish(manifest_path: &Path, opts: &PublishOpts) -> CargoResult<()> {
41+
let pkg = try!(Package::for_path(&manifest_path, opts.config));
3842

3943
if !pkg.publish() {
4044
bail!("some crates cannot be published.\n\
4145
`{}` is marked as unpublishable", pkg.name());
4246
}
4347

44-
let (mut registry, reg_id) = try!(registry(config, token, index));
48+
let (mut registry, reg_id) = try!(registry(opts.config,
49+
opts.token.clone(),
50+
opts.index.clone()));
4551
try!(verify_dependencies(&pkg, &reg_id));
4652

4753
// Prepare a tarball, with a non-surpressable warning if metadata
4854
// is missing since this is being put online.
49-
let tarball = try!(ops::package(manifest_path, config, verify,
50-
false, true)).unwrap();
55+
let tarball = try!(ops::package(manifest_path, &ops::PackageOpts {
56+
config: opts.config,
57+
verify: opts.verify,
58+
list: false,
59+
check_metadata: true,
60+
allow_dirty: opts.allow_dirty,
61+
})).unwrap();
5162

5263
// Upload said tarball to the specified destination
53-
try!(config.shell().status("Uploading", pkg.package_id().to_string()));
64+
try!(opts.config.shell().status("Uploading", pkg.package_id().to_string()));
5465
try!(transmit(&pkg, tarball.file(), &mut registry));
5566

5667
Ok(())

tests/package.rs

-22
Original file line numberDiff line numberDiff line change
@@ -269,28 +269,6 @@ fn package_lib_with_bin() {
269269
execs().with_status(0));
270270
}
271271

272-
#[test]
273-
fn package_new_git_repo() {
274-
let p = project("foo")
275-
.file("Cargo.toml", r#"
276-
[project]
277-
name = "foo"
278-
version = "0.0.1"
279-
"#)
280-
.file("src/main.rs", "fn main() {}");
281-
p.build();
282-
git2::Repository::init(&p.root()).unwrap();
283-
284-
assert_that(cargo_process().arg("package").cwd(p.root())
285-
.arg("--no-verify").arg("-v"),
286-
execs().with_status(0).with_stderr("\
287-
[WARNING] manifest has no description[..]
288-
[PACKAGING] foo v0.0.1 ([..])
289-
[ARCHIVING] [..]
290-
[ARCHIVING] [..]
291-
"));
292-
}
293-
294272
#[test]
295273
fn package_git_submodule() {
296274
let project = git::new("foo", |project| {

0 commit comments

Comments
 (0)