1
- use color_eyre:: eyre:: { eyre, WrapErr } ;
2
- pub use color_eyre:: Result ;
3
1
use std:: fmt;
4
2
use std:: str:: FromStr ;
5
- use strum_macros :: { Display , EnumString , VariantNames } ;
6
- use tracing :: debug ;
3
+ use strum :: { Display , EnumString , VariantNames } ;
4
+ use thiserror :: Error ;
7
5
use url:: Url ;
8
6
7
+ #[ cfg( feature = "tracing" ) ]
8
+ use tracing:: debug;
9
+
9
10
/// Supported uri schemes for parsing
10
11
#[ derive( Debug , PartialEq , Eq , EnumString , VariantNames , Clone , Display , Copy ) ]
11
12
#[ strum( serialize_all = "kebab_case" ) ]
@@ -135,7 +136,7 @@ impl Default for GitUrl {
135
136
}
136
137
137
138
impl FromStr for GitUrl {
138
- type Err = color_eyre :: Report ;
139
+ type Err = GitUrlParseError ;
139
140
140
141
fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
141
142
GitUrl :: parse ( s)
@@ -153,14 +154,22 @@ impl GitUrl {
153
154
}
154
155
155
156
/// Returns a `Result<GitUrl>` after normalizing and parsing `url` for metadata
156
- pub fn parse ( url : & str ) -> Result < GitUrl > {
157
+ pub fn parse ( url : & str ) -> Result < GitUrl , GitUrlParseError > {
157
158
// Normalize the url so we can use Url crate to process ssh urls
158
- let normalized = normalize_url ( url)
159
- . with_context ( || "Url normalization into url::Url failed" . to_string ( ) ) ?;
159
+ let normalized = if let Ok ( url) = normalize_url ( url) {
160
+ url
161
+ } else {
162
+ return Err ( GitUrlParseError :: UrlNormalizeFailed ) ;
163
+ } ;
160
164
161
165
// Some pre-processing for paths
162
- let scheme = Scheme :: from_str ( normalized. scheme ( ) )
163
- . with_context ( || format ! ( "Scheme unsupported: {:?}" , normalized. scheme( ) ) ) ?;
166
+ let scheme = if let Ok ( scheme) = Scheme :: from_str ( normalized. scheme ( ) ) {
167
+ scheme
168
+ } else {
169
+ return Err ( GitUrlParseError :: UnsupportedScheme (
170
+ normalized. scheme ( ) . to_string ( ) ,
171
+ ) ) ;
172
+ } ;
164
173
165
174
// Normalized ssh urls can always have their first '/' removed
166
175
let urlpath = match & scheme {
@@ -176,6 +185,7 @@ impl GitUrl {
176
185
177
186
// Parse through path for name,owner,organization
178
187
// Support organizations for Azure Devops
188
+ #[ cfg( feature = "tracing" ) ]
179
189
debug ! ( "The urlpath: {:?}" , & urlpath) ;
180
190
181
191
// Most git services use the path for metadata in the same way, so we're going to separate
@@ -186,10 +196,14 @@ impl GitUrl {
186
196
//
187
197
// organizations are going to be supported on a per-host basis
188
198
let splitpath = & urlpath. rsplit_terminator ( '/' ) . collect :: < Vec < & str > > ( ) ;
199
+
200
+ #[ cfg( feature = "tracing" ) ]
189
201
debug ! ( "rsplit results for metadata: {:?}" , splitpath) ;
190
202
191
203
let name = splitpath[ 0 ] . trim_end_matches ( ".git" ) . to_string ( ) ;
192
204
205
+ // TODO: I think here is where we want to update the url pattern identification step.. I want to be able to have a hint that the user can pass
206
+
193
207
let ( owner, organization, fullname) = match & scheme {
194
208
// We're not going to assume anything about metadata from a filepath
195
209
Scheme :: File => ( None :: < String > , None :: < String > , name. clone ( ) ) ,
@@ -200,12 +214,15 @@ impl GitUrl {
200
214
let hosts_w_organization_in_path = vec ! [ "dev.azure.com" , "ssh.dev.azure.com" ] ;
201
215
//vec!["dev.azure.com", "ssh.dev.azure.com", "visualstudio.com"];
202
216
203
- let host_str = normalized
204
- . host_str ( )
205
- . ok_or ( eyre ! ( "Host from URL could not be represented as str" ) ) ?;
217
+ let host_str = if let Some ( host) = normalized. host_str ( ) {
218
+ host
219
+ } else {
220
+ return Err ( GitUrlParseError :: UnsupportedUrlHostFormat ) ;
221
+ } ;
206
222
207
223
match hosts_w_organization_in_path. contains ( & host_str) {
208
224
true => {
225
+ #[ cfg( feature = "tracing" ) ]
209
226
debug ! ( "Found a git provider with an org" ) ;
210
227
211
228
// The path differs between git:// and https:// schemes
@@ -241,16 +258,18 @@ impl GitUrl {
241
258
fullname. join ( "/" ) ,
242
259
)
243
260
}
244
- _ => return Err ( eyre ! ( "Scheme not supported for host" ) ) ,
261
+
262
+ // TODO: I'm not sure if I want to support throwing this error long-term
263
+ _ => return Err ( GitUrlParseError :: UnexpectedScheme ) ,
245
264
}
246
265
}
247
266
false => {
248
267
if !url. starts_with ( "ssh" ) && splitpath. len ( ) < 2 {
249
- return Err ( eyre ! ( "git url is not of expected format" ) ) ;
268
+ return Err ( GitUrlParseError :: UnexpectedFormat ) ;
250
269
}
251
270
252
271
let position = match splitpath. len ( ) {
253
- 0 => return Err ( eyre ! ( "git url is not of expected format" ) ) ,
272
+ 0 => return Err ( GitUrlParseError :: UnexpectedFormat ) ,
254
273
1 => 0 ,
255
274
_ => 1 ,
256
275
} ;
@@ -312,52 +331,60 @@ impl GitUrl {
312
331
/// Prepends `ssh://` to url
313
332
///
314
333
/// Supports absolute and relative paths
315
- fn normalize_ssh_url ( url : & str ) -> Result < Url > {
334
+ fn normalize_ssh_url ( url : & str ) -> Result < Url , GitUrlParseError > {
316
335
let u = url. split ( ':' ) . collect :: < Vec < & str > > ( ) ;
317
336
318
337
match u. len ( ) {
319
338
2 => {
339
+ #[ cfg( feature = "tracing" ) ]
320
340
debug ! ( "Normalizing ssh url: {:?}" , u) ;
321
341
normalize_url ( & format ! ( "ssh://{}/{}" , u[ 0 ] , u[ 1 ] ) )
322
342
}
323
343
3 => {
344
+ #[ cfg( feature = "tracing" ) ]
324
345
debug ! ( "Normalizing ssh url with ports: {:?}" , u) ;
325
346
normalize_url ( & format ! ( "ssh://{}:{}/{}" , u[ 0 ] , u[ 1 ] , u[ 2 ] ) )
326
347
}
327
- _default => Err ( eyre ! ( "SSH normalization pattern not covered for: {:?}" , u ) ) ,
348
+ _default => Err ( GitUrlParseError :: UnsupportedSshUrlFormat ) ,
328
349
}
329
350
}
330
351
331
352
/// `normalize_file_path` takes in a filepath and uses `Url::from_file_path()` to parse
332
353
///
333
354
/// Prepends `file://` to url
334
355
#[ cfg( any( unix, windows, target_os = "redox" , target_os = "wasi" ) ) ]
335
- fn normalize_file_path ( filepath : & str ) -> Result < Url > {
356
+ fn normalize_file_path ( filepath : & str ) -> Result < Url , GitUrlParseError > {
336
357
let fp = Url :: from_file_path ( filepath) ;
337
358
338
359
match fp {
339
360
Ok ( path) => Ok ( path) ,
340
- Err ( _e) => Ok ( normalize_url ( & format ! ( "file://{}" , filepath) )
341
- . with_context ( || "file:// normalization failed" . to_string ( ) ) ?) ,
361
+ Err ( _e) => {
362
+ if let Ok ( file_url) = normalize_url ( & format ! ( "file://{}" , filepath) ) {
363
+ Ok ( file_url)
364
+ } else {
365
+ return Err ( GitUrlParseError :: FileUrlNormalizeFailedSchemeAdded ) ;
366
+ }
367
+ }
342
368
}
343
369
}
344
370
345
371
#[ cfg( target_arch = "wasm32" ) ]
346
- fn normalize_file_path ( _filepath : & str ) -> Result < Url > {
372
+ fn normalize_file_path ( _filepath : & str ) -> Result < Url , GitUrlParseError > {
347
373
unreachable ! ( )
348
374
}
349
375
350
376
/// `normalize_url` takes in url as `&str` and takes an opinionated approach to identify
351
377
/// `ssh://` or `file://` urls that require more information to be added so that
352
378
/// they can be parsed more effectively by `url::Url::parse()`
353
- pub fn normalize_url ( url : & str ) -> Result < Url > {
379
+ pub fn normalize_url ( url : & str ) -> Result < Url , GitUrlParseError > {
380
+ #[ cfg( feature = "tracing" ) ]
354
381
debug ! ( "Processing: {:?}" , & url) ;
355
382
356
383
// TODO: Should this be extended to check for any whitespace?
357
384
// Error if there are null bytes within the url
358
385
// https://github.com/tjtelan/git-url-parse-rs/issues/16
359
386
if url. contains ( '\0' ) {
360
- return Err ( eyre ! ( "Found null bytes within input url before parsing" ) ) ;
387
+ return Err ( GitUrlParseError :: FoundNullBytes ) ;
361
388
}
362
389
363
390
// We're going to remove any trailing slash before running through Url::parse
@@ -366,10 +393,7 @@ pub fn normalize_url(url: &str) -> Result<Url> {
366
393
// TODO: Remove support for this form when I go to next major version.
367
394
// I forget what it supports, and it isn't obvious after searching for examples
368
395
// normalize short git url notation: git:host/path
369
- let url_to_parse = if trim_url. starts_with ( "git:" ) && !trim_url. starts_with ( "git://" )
370
- //.with_context(|| "Failed to build short git url regex for testing against url".to_string())?
371
- //.is_match(trim_url)
372
- {
396
+ let url_to_parse = if trim_url. starts_with ( "git:" ) && !trim_url. starts_with ( "git://" ) {
373
397
trim_url. replace ( "git:" , "git://" )
374
398
} else {
375
399
trim_url. to_string ( )
@@ -383,18 +407,20 @@ pub fn normalize_url(url: &str) -> Result<Url> {
383
407
Ok ( _p) => u,
384
408
Err ( _e) => {
385
409
// Catch case when an ssh url is given w/o a user
410
+ #[ cfg( feature = "tracing" ) ]
386
411
debug ! ( "Scheme parse fail. Assuming a userless ssh url" ) ;
387
- normalize_ssh_url ( trim_url) . with_context ( || {
388
- "No url scheme was found, then failed to normalize as ssh url." . to_string ( )
389
- } ) ?
412
+ if let Ok ( ssh_url) = normalize_ssh_url ( trim_url) {
413
+ ssh_url
414
+ } else {
415
+ return Err ( GitUrlParseError :: SshUrlNormalizeFailedNoScheme ) ;
416
+ }
390
417
}
391
418
}
392
419
}
393
420
421
+ // If we're here, we're only looking for Scheme::Ssh or Scheme::File
394
422
// TODO: Add test for this
395
423
Err ( url:: ParseError :: RelativeUrlWithoutBase ) => {
396
- // If we're here, we're only looking for Scheme::Ssh or Scheme::File
397
-
398
424
// Assuming we have found Scheme::Ssh if we can find an "@" before ":"
399
425
// Otherwise we have Scheme::File
400
426
//let re = Regex::new(r"^\S+(@)\S+(:).*$").with_context(|| {
@@ -403,19 +429,19 @@ pub fn normalize_url(url: &str) -> Result<Url> {
403
429
404
430
match is_ssh_url ( trim_url) {
405
431
true => {
432
+ #[ cfg( feature = "tracing" ) ]
406
433
debug ! ( "Scheme::SSH match for normalization" ) ;
407
- normalize_ssh_url ( trim_url)
408
- . with_context ( || "Failed to normalize as ssh url" . to_string ( ) ) ?
434
+ normalize_ssh_url ( trim_url) ?
409
435
}
410
436
false => {
437
+ #[ cfg( feature = "tracing" ) ]
411
438
debug ! ( "Scheme::File match for normalization" ) ;
412
- normalize_file_path ( trim_url)
413
- . with_context ( || "Failed to normalize as file url" . to_string ( ) ) ?
439
+ normalize_file_path ( trim_url) ?
414
440
}
415
441
}
416
442
}
417
443
Err ( err) => {
418
- return Err ( eyre ! ( "url parsing failed: {:?}" , err) ) ;
444
+ return Err ( GitUrlParseError :: from ( err) ) ;
419
445
}
420
446
} )
421
447
}
@@ -446,9 +472,61 @@ fn is_ssh_url(url: &str) -> bool {
446
472
447
473
// it's an ssh url if we have a domain:path pattern
448
474
let parts: Vec < & str > = url. split ( ':' ) . collect ( ) ;
475
+
476
+ // FIXME: I am not sure how to validate a url with a port
477
+ //if parts.len() != 3 && !parts[0].is_empty() && !parts[1].is_empty() && !parts[2].is_empty() {
478
+ // return false;
479
+ //}
480
+
481
+ // This should also handle if a port is specified
482
+ // no port example: ssh://user@domain:path/to/repo.git
483
+ // port example: ssh://user@domain:port/path/to/repo.git
449
484
if parts. len ( ) != 2 && !parts[ 0 ] . is_empty ( ) && !parts[ 1 ] . is_empty ( ) {
450
485
return false ;
451
486
} else {
452
487
return true ;
453
488
}
454
489
}
490
+
491
+ #[ derive( Error , Debug ) ]
492
+ pub enum GitUrlParseError {
493
+ #[ error( "Error from Url crate" ) ]
494
+ UrlParseError ( #[ from] url:: ParseError ) ,
495
+
496
+ #[ error( "Url normalization into url::Url failed" ) ]
497
+ UrlNormalizeFailed ,
498
+
499
+ #[ error( "No url scheme was found, then failed to normalize as ssh url." ) ]
500
+ SshUrlNormalizeFailedNoScheme ,
501
+
502
+ #[ error( "No url scheme was found, then failed to normalize as ssh url after adding 'ssh://'" ) ]
503
+ SshUrlNormalizeFailedSchemeAdded ,
504
+
505
+ #[ error( "Failed to normalize as ssh url after adding 'ssh://'" ) ]
506
+ SshUrlNormalizeFailedSchemeAddedWithPorts ,
507
+
508
+ #[ error( "No url scheme was found, then failed to normalize as file url." ) ]
509
+ FileUrlNormalizeFailedNoScheme ,
510
+
511
+ #[ error(
512
+ "No url scheme was found, then failed to normalize as file url after adding 'file://'"
513
+ ) ]
514
+ FileUrlNormalizeFailedSchemeAdded ,
515
+
516
+ #[ error( "Git Url not in expected format" ) ]
517
+ UnexpectedFormat ,
518
+
519
+ // FIXME: Keep an eye on this error for removal
520
+ #[ error( "Git Url for host using unexpected scheme" ) ]
521
+ UnexpectedScheme ,
522
+
523
+ #[ error( "Scheme unsupported: {0}" ) ]
524
+ UnsupportedScheme ( String ) ,
525
+ #[ error( "Host from Url cannot be str or does not exist" ) ]
526
+ UnsupportedUrlHostFormat ,
527
+ #[ error( "Git Url not in expected format for SSH" ) ]
528
+ UnsupportedSshUrlFormat ,
529
+
530
+ #[ error( "Found null bytes within input url before parsing" ) ]
531
+ FoundNullBytes ,
532
+ }
0 commit comments