@@ -6,10 +6,13 @@ use std::path::PathBuf;
6
6
7
7
use anyhow:: Context as _;
8
8
use cargo_util:: paths;
9
+ use cargo_util_schemas:: manifest:: TomlManifest ;
9
10
use serde:: Serialize ;
10
11
use tracing:: debug;
11
12
12
13
use crate :: core:: Package ;
14
+ use crate :: core:: Workspace ;
15
+ use crate :: core:: WorkspaceRootConfig ;
13
16
use crate :: sources:: PathEntry ;
14
17
use crate :: CargoResult ;
15
18
use crate :: GlobalContext ;
@@ -44,9 +47,10 @@ pub struct GitVcsInfo {
44
47
pub fn check_repo_state (
45
48
p : & Package ,
46
49
src_files : & [ PathEntry ] ,
47
- gctx : & GlobalContext ,
50
+ ws : & Workspace < ' _ > ,
48
51
opts : & PackageOpts < ' _ > ,
49
52
) -> CargoResult < Option < VcsInfo > > {
53
+ let gctx = ws. gctx ( ) ;
50
54
let Ok ( repo) = git2:: Repository :: discover ( p. root ( ) ) else {
51
55
gctx. shell ( ) . verbose ( |shell| {
52
56
shell. warn ( format ! ( "no (git) VCS found for `{}`" , p. root( ) . display( ) ) )
@@ -105,7 +109,7 @@ pub fn check_repo_state(
105
109
. and_then ( |p| p. to_str ( ) )
106
110
. unwrap_or ( "" )
107
111
. replace ( "\\ " , "/" ) ;
108
- let Some ( git) = git ( p, gctx , src_files, & repo, & opts) ? else {
112
+ let Some ( git) = git ( p, ws , src_files, & repo, & opts) ? else {
109
113
// If the git repo lacks essensial field like `sha1`, and since this field exists from the beginning,
110
114
// then don't generate the corresponding file in order to maintain consistency with past behavior.
111
115
return Ok ( None ) ;
@@ -163,11 +167,12 @@ fn warn_symlink_checked_out_as_plain_text_file(
163
167
/// The real git status check starts from here.
164
168
fn git (
165
169
pkg : & Package ,
166
- gctx : & GlobalContext ,
170
+ ws : & Workspace < ' _ > ,
167
171
src_files : & [ PathEntry ] ,
168
172
repo : & git2:: Repository ,
169
173
opts : & PackageOpts < ' _ > ,
170
174
) -> CargoResult < Option < GitVcsInfo > > {
175
+ let gctx = ws. gctx ( ) ;
171
176
// This is a collection of any dirty or untracked files. This covers:
172
177
// - new/modified/deleted/renamed/type change (index or worktree)
173
178
// - untracked files (which are "new" worktree files)
@@ -189,7 +194,7 @@ fn git(
189
194
. iter ( )
190
195
. filter ( |src_file| dirty_files. iter ( ) . any ( |path| src_file. starts_with ( path) ) )
191
196
. map ( |p| p. as_ref ( ) )
192
- . chain ( dirty_files_outside_pkg_root ( pkg, repo, src_files) ?. iter ( ) )
197
+ . chain ( dirty_files_outside_pkg_root ( ws , pkg, repo, src_files) ?. iter ( ) )
193
198
. map ( |path| {
194
199
pathdiff:: diff_paths ( path, cwd)
195
200
. as_ref ( )
@@ -233,6 +238,7 @@ fn git(
233
238
/// current package root, but still under the git workdir, affecting the
234
239
/// final packaged `.crate` file.
235
240
fn dirty_files_outside_pkg_root (
241
+ ws : & Workspace < ' _ > ,
236
242
pkg : & Package ,
237
243
repo : & git2:: Repository ,
238
244
src_files : & [ PathEntry ] ,
@@ -247,7 +253,7 @@ fn dirty_files_outside_pkg_root(
247
253
. map ( |path| paths:: normalize_path ( & pkg_root. join ( path) ) )
248
254
. collect ( ) ;
249
255
250
- let mut dirty_symlinks = HashSet :: new ( ) ;
256
+ let mut dirty_files = HashSet :: new ( ) ;
251
257
for rel_path in src_files
252
258
. iter ( )
253
259
. filter ( |p| p. is_symlink_or_under_symlink ( ) )
@@ -259,10 +265,151 @@ fn dirty_files_outside_pkg_root(
259
265
. filter_map ( |p| paths:: strip_prefix_canonical ( p, workdir) . ok ( ) )
260
266
{
261
267
if repo. status_file ( & rel_path) ? != git2:: Status :: CURRENT {
262
- dirty_symlinks . insert ( workdir. join ( rel_path) ) ;
268
+ dirty_files . insert ( workdir. join ( rel_path) ) ;
263
269
}
264
270
}
265
- Ok ( dirty_symlinks)
271
+
272
+ if let Some ( dirty_ws_manifest) = dirty_workspace_manifest ( ws, pkg, repo) ? {
273
+ dirty_files. insert ( dirty_ws_manifest) ;
274
+ }
275
+ Ok ( dirty_files)
276
+ }
277
+
278
+ fn dirty_workspace_manifest (
279
+ ws : & Workspace < ' _ > ,
280
+ pkg : & Package ,
281
+ repo : & git2:: Repository ,
282
+ ) -> CargoResult < Option < PathBuf > > {
283
+ let workdir = repo. workdir ( ) . unwrap ( ) ;
284
+ let ws_manifest_path = ws. root_manifest ( ) ;
285
+ if pkg. manifest_path ( ) == ws_manifest_path {
286
+ // The workspace manifest is also the primary package manifest.
287
+ // Normal file statuc check should have covered it.
288
+ return Ok ( None ) ;
289
+ }
290
+ if paths:: strip_prefix_canonical ( ws_manifest_path, pkg. root ( ) ) . is_ok ( ) {
291
+ // Inside package root. Don't bother checking git status.
292
+ return Ok ( None ) ;
293
+ }
294
+ let Ok ( rel_path) = paths:: strip_prefix_canonical ( ws_manifest_path, workdir) else {
295
+ // Completely outside this git workdir.
296
+ return Ok ( None ) ;
297
+ } ;
298
+
299
+ // Outside package root but under git workdir.
300
+ if repo. status_file ( & rel_path) ? == git2:: Status :: CURRENT {
301
+ return Ok ( None ) ;
302
+ }
303
+
304
+ let from_index = ws_manifest_and_root_config_from_index ( ws, repo, & rel_path) ;
305
+ // If there is no workable workspace manifest in Git index,
306
+ // create a default inheritable fields.
307
+ // With it, we can detect any member manifest has inherited fields,
308
+ // and then the workspace manifest should be considered dirty.
309
+ let inheritable = if let Some ( fields) = from_index
310
+ . as_ref ( )
311
+ . map ( |( _, root_config) | root_config. inheritable ( ) )
312
+ {
313
+ fields
314
+ } else {
315
+ & Default :: default ( )
316
+ } ;
317
+
318
+ let empty = Vec :: new ( ) ;
319
+ let cargo_features = crate :: core:: Features :: new (
320
+ from_index
321
+ . as_ref ( )
322
+ . and_then ( |( manifest, _) | manifest. cargo_features . as_ref ( ) )
323
+ . unwrap_or ( & empty) ,
324
+ ws. gctx ( ) ,
325
+ & mut Default :: default ( ) ,
326
+ pkg. package_id ( ) . source_id ( ) . is_path ( ) ,
327
+ )
328
+ . unwrap_or_default ( ) ;
329
+
330
+ let dirty_path = || Ok ( Some ( workdir. join ( & rel_path) ) ) ;
331
+ let dirty = |msg| {
332
+ debug ! (
333
+ "{msg} for `{}` of repo at `{}`" ,
334
+ rel_path. display( ) ,
335
+ workdir. display( ) ,
336
+ ) ;
337
+ dirty_path ( )
338
+ } ;
339
+
340
+ let Ok ( normalized_toml) = crate :: util:: toml:: normalize_toml (
341
+ pkg. manifest ( ) . original_toml ( ) ,
342
+ & cargo_features,
343
+ & || Ok ( inheritable) ,
344
+ pkg. manifest_path ( ) ,
345
+ ws. gctx ( ) ,
346
+ & mut Default :: default ( ) ,
347
+ & mut Default :: default ( ) ,
348
+ ) else {
349
+ return dirty ( "failed to normalize pkg manifest from index" ) ;
350
+ } ;
351
+
352
+ let Ok ( from_index) = toml:: to_string_pretty ( & normalized_toml) else {
353
+ return dirty ( "failed to serialize pkg manifest from index" ) ;
354
+ } ;
355
+
356
+ let Ok ( from_working_dir) = toml:: to_string_pretty ( pkg. manifest ( ) . normalized_toml ( ) ) else {
357
+ return dirty ( "failed to serialize pkg manifest from working directory" ) ;
358
+ } ;
359
+
360
+ if from_index != from_working_dir {
361
+ tracing:: trace!( "--- from index ---\n {from_index}" ) ;
362
+ tracing:: trace!( "--- from working dir ---\n {from_working_dir}" ) ;
363
+ return dirty ( "normalized manifests from index and in working directory mismatched" ) ;
364
+ }
365
+
366
+ Ok ( None )
367
+ }
368
+
369
+ /// Gets workspace manifest and workspace root config from Git index.
370
+ ///
371
+ /// This returns an `Option` because workspace manifest might be broken or not
372
+ /// exist at all.
373
+ fn ws_manifest_and_root_config_from_index (
374
+ ws : & Workspace < ' _ > ,
375
+ repo : & git2:: Repository ,
376
+ ws_manifest_rel_path : & Path ,
377
+ ) -> Option < ( TomlManifest , WorkspaceRootConfig ) > {
378
+ let workdir = repo. workdir ( ) . unwrap ( ) ;
379
+ let dirty = |msg| {
380
+ debug ! (
381
+ "{msg} for `{}` of repo at `{}`" ,
382
+ ws_manifest_rel_path. display( ) ,
383
+ workdir. display( ) ,
384
+ ) ;
385
+ None
386
+ } ;
387
+ let Ok ( index) = repo. index ( ) else {
388
+ debug ! ( "no index for repo at `{}`" , workdir. display( ) ) ;
389
+ return None ;
390
+ } ;
391
+ let Some ( entry) = index. get_path ( ws_manifest_rel_path, 0 ) else {
392
+ return dirty ( "workspace manifest not found" ) ;
393
+ } ;
394
+ let Ok ( blob) = repo. find_blob ( entry. id ) else {
395
+ return dirty ( "failed to find manifest blob" ) ;
396
+ } ;
397
+ let Ok ( contents) = String :: from_utf8 ( blob. content ( ) . to_vec ( ) ) else {
398
+ return dirty ( "failed parse as UTF-8 encoding" ) ;
399
+ } ;
400
+ let Ok ( document) = crate :: util:: toml:: parse_document ( & contents) else {
401
+ return dirty ( "failed to parse file" ) ;
402
+ } ;
403
+ let Ok ( ws_manifest_from_index) = crate :: util:: toml:: deserialize_toml ( & document) else {
404
+ return dirty ( "failed to deserialize doc" ) ;
405
+ } ;
406
+ let Some ( toml_workspace) = ws_manifest_from_index. workspace . as_ref ( ) else {
407
+ return dirty ( "not a workspace manifest" ) ;
408
+ } ;
409
+
410
+ let ws_root_config =
411
+ crate :: util:: toml:: to_workspace_root_config ( toml_workspace, ws. root_manifest ( ) ) ;
412
+ Some ( ( ws_manifest_from_index, ws_root_config) )
266
413
}
267
414
268
415
/// Helper to collect dirty statuses for a single repo.
0 commit comments