1
1
//! Helpers to gather the VCS information for `cargo package`.
2
2
3
+ use std:: collections:: HashMap ;
3
4
use std:: path:: Path ;
4
5
use std:: path:: PathBuf ;
5
6
@@ -38,19 +39,71 @@ pub struct GitVcsInfo {
38
39
pub struct VcsInfoBuilder < ' a , ' gctx > {
39
40
ws : & ' a Workspace < ' gctx > ,
40
41
opts : & ' a PackageOpts < ' gctx > ,
42
+ /// Map each git workdir path to the workdir's git status cache.
43
+ caches : HashMap < PathBuf , RepoStatusCache > ,
41
44
}
42
45
43
46
impl < ' a , ' gctx > VcsInfoBuilder < ' a , ' gctx > {
44
47
pub fn new (
45
48
ws : & ' a Workspace < ' gctx > ,
46
49
opts : & ' a PackageOpts < ' gctx > ,
47
50
) -> VcsInfoBuilder < ' a , ' gctx > {
48
- VcsInfoBuilder { ws, opts }
51
+ VcsInfoBuilder {
52
+ ws,
53
+ opts,
54
+ caches : Default :: default ( ) ,
55
+ }
49
56
}
50
57
51
58
/// Builds an [`VcsInfo`] for the given `pkg` and its associated `src_files`.
52
59
pub fn build ( & mut self , pkg : & Package , src_files : & [ PathBuf ] ) -> CargoResult < Option < VcsInfo > > {
53
- check_repo_state ( pkg, src_files, self . ws . gctx ( ) , self . opts )
60
+ check_repo_state ( pkg, src_files, self . ws . gctx ( ) , self . opts , & mut self . caches )
61
+ }
62
+ }
63
+
64
+ /// Cache of git status inquries for a Git workdir.
65
+ struct RepoStatusCache {
66
+ repo : git2:: Repository ,
67
+ /// Status of each file path relative to the git workdir path.
68
+ cache : HashMap < PathBuf , git2:: Status > ,
69
+ }
70
+
71
+ impl RepoStatusCache {
72
+ fn new ( repo : git2:: Repository ) -> RepoStatusCache {
73
+ RepoStatusCache {
74
+ repo,
75
+ cache : Default :: default ( ) ,
76
+ }
77
+ }
78
+
79
+ /// Like [`git2::Repository::status_file`] but with cache.
80
+ fn status_file ( & mut self , path : & Path ) -> CargoResult < git2:: Status > {
81
+ use std:: collections:: hash_map:: Entry ;
82
+ match self . cache . entry ( path. into ( ) ) {
83
+ Entry :: Occupied ( entry) => {
84
+ tracing:: trace!(
85
+ target: "cargo_package_vcs_cache" ,
86
+ "git status cache hit for `{}` at workdir `{}`" ,
87
+ path. display( ) ,
88
+ self . repo. workdir( ) . unwrap( ) . display( )
89
+ ) ;
90
+ Ok ( * entry. get ( ) )
91
+ }
92
+ Entry :: Vacant ( entry) => {
93
+ tracing:: trace!(
94
+ target: "cargo_package_vcs_cache" ,
95
+ "git status cache miss for `{}` at workdir `{}`" ,
96
+ path. display( ) ,
97
+ self . repo. workdir( ) . unwrap( ) . display( )
98
+ ) ;
99
+ let status = self . repo . status_file ( path) ?;
100
+ Ok ( * entry. insert ( status) )
101
+ }
102
+ }
103
+ }
104
+
105
+ fn workdir ( & self ) -> & Path {
106
+ self . repo . workdir ( ) . unwrap ( )
54
107
}
55
108
}
56
109
@@ -67,6 +120,7 @@ pub fn check_repo_state(
67
120
src_files : & [ PathBuf ] ,
68
121
gctx : & GlobalContext ,
69
122
opts : & PackageOpts < ' _ > ,
123
+ caches : & mut HashMap < PathBuf , RepoStatusCache > ,
70
124
) -> CargoResult < Option < VcsInfo > > {
71
125
let Ok ( repo) = git2:: Repository :: discover ( p. root ( ) ) else {
72
126
gctx. shell ( ) . verbose ( |shell| {
@@ -86,14 +140,20 @@ pub fn check_repo_state(
86
140
} ;
87
141
88
142
debug ! ( "found a git repo at `{}`" , workdir. display( ) ) ;
143
+
144
+ let cache = caches
145
+ . entry ( workdir. to_path_buf ( ) )
146
+ . or_insert_with ( || RepoStatusCache :: new ( repo) ) ;
147
+
89
148
let path = p. manifest_path ( ) ;
90
- let path = paths:: strip_prefix_canonical ( path, workdir) . unwrap_or_else ( |_| path. to_path_buf ( ) ) ;
91
- let Ok ( status) = repo. status_file ( & path) else {
149
+ let path =
150
+ paths:: strip_prefix_canonical ( path, cache. workdir ( ) ) . unwrap_or_else ( |_| path. to_path_buf ( ) ) ;
151
+ let Ok ( status) = cache. status_file ( & path) else {
92
152
gctx. shell ( ) . verbose ( |shell| {
93
153
shell. warn ( format ! (
94
154
"no (git) Cargo.toml found at `{}` in workdir `{}`" ,
95
155
path. display( ) ,
96
- workdir. display( )
156
+ cache . workdir( ) . display( )
97
157
) )
98
158
} ) ?;
99
159
// No checked-in `Cargo.toml` found. This package may be irrelevant.
@@ -106,7 +166,7 @@ pub fn check_repo_state(
106
166
shell. warn ( format ! (
107
167
"found (git) Cargo.toml ignored at `{}` in workdir `{}`" ,
108
168
path. display( ) ,
109
- workdir. display( )
169
+ cache . workdir( ) . display( )
110
170
) )
111
171
} ) ?;
112
172
// An ignored `Cargo.toml` found. This package may be irrelevant.
@@ -117,14 +177,14 @@ pub fn check_repo_state(
117
177
debug ! (
118
178
"found (git) Cargo.toml at `{}` in workdir `{}`" ,
119
179
path. display( ) ,
120
- workdir. display( ) ,
180
+ cache . workdir( ) . display( ) ,
121
181
) ;
122
182
let path_in_vcs = path
123
183
. parent ( )
124
184
. and_then ( |p| p. to_str ( ) )
125
185
. unwrap_or ( "" )
126
186
. replace ( "\\ " , "/" ) ;
127
- let Some ( git) = git ( p, gctx, src_files, & repo , & opts) ? else {
187
+ let Some ( git) = git ( p, gctx, src_files, cache , & opts) ? else {
128
188
// If the git repo lacks essensial field like `sha1`, and since this field exists from the beginning,
129
189
// then don't generate the corresponding file in order to maintain consistency with past behavior.
130
190
return Ok ( None ) ;
@@ -138,7 +198,7 @@ fn git(
138
198
pkg : & Package ,
139
199
gctx : & GlobalContext ,
140
200
src_files : & [ PathBuf ] ,
141
- repo : & git2 :: Repository ,
201
+ cache : & mut RepoStatusCache ,
142
202
opts : & PackageOpts < ' _ > ,
143
203
) -> CargoResult < Option < GitVcsInfo > > {
144
204
// This is a collection of any dirty or untracked files. This covers:
@@ -147,10 +207,10 @@ fn git(
147
207
// - ignored (in case the user has an `include` directive that
148
208
// conflicts with .gitignore).
149
209
let mut dirty_files = Vec :: new ( ) ;
150
- collect_statuses ( repo, & mut dirty_files) ?;
210
+ collect_statuses ( & cache . repo , & mut dirty_files) ?;
151
211
// Include each submodule so that the error message can provide
152
212
// specifically *which* files in a submodule are modified.
153
- status_submodules ( repo, & mut dirty_files) ?;
213
+ status_submodules ( & cache . repo , & mut dirty_files) ?;
154
214
155
215
// Find the intersection of dirty in git, and the src_files that would
156
216
// be packaged. This is a lazy n^2 check, but seems fine with
@@ -159,7 +219,7 @@ fn git(
159
219
let mut dirty_src_files: Vec < _ > = src_files
160
220
. iter ( )
161
221
. filter ( |src_file| dirty_files. iter ( ) . any ( |path| src_file. starts_with ( path) ) )
162
- . chain ( dirty_metadata_paths ( pkg, repo ) ?. iter ( ) )
222
+ . chain ( dirty_metadata_paths ( pkg, cache ) ?. iter ( ) )
163
223
. map ( |path| {
164
224
pathdiff:: diff_paths ( path, cwd)
165
225
. as_ref ( )
@@ -172,10 +232,10 @@ fn git(
172
232
if !dirty || opts. allow_dirty {
173
233
// Must check whetherthe repo has no commit firstly, otherwise `revparse_single` would fail on bare commit repo.
174
234
// Due to lacking the `sha1` field, it's better not record the `GitVcsInfo` for consistency.
175
- if repo. is_empty ( ) ? {
235
+ if cache . repo . is_empty ( ) ? {
176
236
return Ok ( None ) ;
177
237
}
178
- let rev_obj = repo. revparse_single ( "HEAD" ) ?;
238
+ let rev_obj = cache . repo . revparse_single ( "HEAD" ) ?;
179
239
Ok ( Some ( GitVcsInfo {
180
240
sha1 : rev_obj. id ( ) . to_string ( ) ,
181
241
dirty,
@@ -198,9 +258,8 @@ fn git(
198
258
/// This is required because those paths may link to a file outside the
199
259
/// current package root, but still under the git workdir, affecting the
200
260
/// final packaged `.crate` file.
201
- fn dirty_metadata_paths ( pkg : & Package , repo : & git2 :: Repository ) -> CargoResult < Vec < PathBuf > > {
261
+ fn dirty_metadata_paths ( pkg : & Package , repo : & mut RepoStatusCache ) -> CargoResult < Vec < PathBuf > > {
202
262
let mut dirty_files = Vec :: new ( ) ;
203
- let workdir = repo. workdir ( ) . unwrap ( ) ;
204
263
let root = pkg. root ( ) ;
205
264
let meta = pkg. manifest ( ) . metadata ( ) ;
206
265
for path in [ & meta. license_file , & meta. readme ] {
@@ -212,12 +271,12 @@ fn dirty_metadata_paths(pkg: &Package, repo: &git2::Repository) -> CargoResult<V
212
271
// Inside package root. Don't bother checking git status.
213
272
continue ;
214
273
}
215
- if let Ok ( rel_path) = paths:: strip_prefix_canonical ( abs_path. as_path ( ) , workdir) {
274
+ if let Ok ( rel_path) = paths:: strip_prefix_canonical ( abs_path. as_path ( ) , repo . workdir ( ) ) {
216
275
// Outside package root but under git workdir,
217
276
if repo. status_file ( & rel_path) ? != git2:: Status :: CURRENT {
218
277
dirty_files. push ( if abs_path. is_symlink ( ) {
219
278
// For symlinks, shows paths to symlink sources
220
- workdir. join ( rel_path)
279
+ repo . workdir ( ) . join ( rel_path)
221
280
} else {
222
281
abs_path
223
282
} ) ;
0 commit comments