1
+ use bstr:: ByteSlice ;
1
2
use git_features:: { fs, progress:: Progress } ;
2
3
use std:: path:: { Path , PathBuf } ;
3
4
@@ -13,30 +14,130 @@ impl Default for Mode {
13
14
}
14
15
}
15
16
16
- // TODO: handle nested repos, skip everything inside a parent directory.
17
- fn find_git_repositories ( root : impl AsRef < Path > ) -> impl Iterator < Item = PathBuf > {
17
+ // TODO: handle nested repos, skip everything inside a parent directory, stop recursing into git workdirs
18
+ fn find_git_repository_workdirs ( root : impl AsRef < Path > , mut progress : impl Progress ) -> impl Iterator < Item = PathBuf > {
19
+ progress. init ( None , git_features:: progress:: count ( "filesystem items" ) ) ;
18
20
fn is_repository ( path : & PathBuf ) -> bool {
19
- path. is_dir ( ) && path. ends_with ( ".git" )
21
+ if !( path. is_dir ( ) && path. ends_with ( ".git" ) ) {
22
+ return false ;
23
+ }
24
+ path. join ( "HEAD" ) . is_file ( ) && path. join ( "config" ) . is_file ( )
25
+ }
26
+ fn into_workdir ( path : PathBuf ) -> PathBuf {
27
+ fn is_bare ( path : & Path ) -> bool {
28
+ !path. join ( "index" ) . exists ( )
29
+ }
30
+ if is_bare ( & path) {
31
+ path
32
+ } else {
33
+ path. parent ( ) . expect ( "git is never in the root" ) . to_owned ( )
34
+ }
20
35
}
21
36
22
37
let walk = fs:: sorted ( fs:: WalkDir :: new ( root) . follow_links ( false ) ) ;
23
38
walk. into_iter ( )
24
- . filter_map ( Result :: ok)
39
+ . filter_map ( move |entry| {
40
+ progress. step ( ) ;
41
+ match entry {
42
+ Ok ( entry) => Some ( entry) ,
43
+ Err ( err) => {
44
+ progress. fail ( format ! ( "Ignored: {}" , err. to_string( ) ) ) ;
45
+ None
46
+ }
47
+ }
48
+ } )
25
49
. map ( |entry : fs:: DirEntry | fs:: direntry_path ( & entry) )
26
50
. filter ( is_repository)
51
+ . map ( into_workdir)
27
52
}
28
53
29
- pub fn run ( _mode : Mode , source_dir : PathBuf , _destination : PathBuf , _progress : impl Progress ) -> anyhow:: Result < ( ) > {
30
- let _repo_paths = find_git_repositories ( source_dir) ;
54
+ fn find_origin_remote ( repo : & Path ) -> anyhow:: Result < Option < git_url:: Url > > {
55
+ let out = std:: process:: Command :: new ( "git" )
56
+ . args ( & [ "remote" , "--verbose" ] )
57
+ . current_dir ( repo)
58
+ . output ( ) ?;
59
+ if out. status . success ( ) {
60
+ Ok ( parse:: remotes_from_git_remote_verbose ( & out. stdout ) ?
61
+ . into_iter ( )
62
+ . find_map ( |( origin, url) | if origin == "origin" { Some ( url) } else { None } ) )
63
+ } else {
64
+ anyhow:: bail!(
65
+ "git invocation failed with code {:?}: {}" ,
66
+ out. status. code( ) ,
67
+ out. stderr. as_bstr( )
68
+ )
69
+ }
70
+ }
71
+
72
+ fn handle ( mode : Mode , git_workdir : & Path , destination : & Path , progress : & mut impl Progress ) -> anyhow:: Result < ( ) > {
73
+ fn to_relative ( path : PathBuf ) -> PathBuf {
74
+ std:: iter:: once ( std:: path:: Component :: CurDir )
75
+ . chain ( path. components ( ) )
76
+ . collect ( )
77
+ }
78
+
79
+ let url = match find_origin_remote ( git_workdir) ? {
80
+ None => {
81
+ progress. info ( format ! (
82
+ "Skipping repository {:?} as it does not have any remote" ,
83
+ git_workdir. display( )
84
+ ) ) ;
85
+ return Ok ( ( ) ) ;
86
+ }
87
+ Some ( url) => url,
88
+ } ;
89
+ if url. path . is_empty ( ) {
90
+ progress. info ( format ! (
91
+ "Skipping repository at {:?} whose remote does not have a path: {:?}" ,
92
+ git_workdir. display( ) ,
93
+ url. to_string( )
94
+ ) ) ;
95
+ return Ok ( ( ) ) ;
96
+ }
97
+
98
+ let destination = destination. join ( to_relative ( git_url:: expand_path ( None , url. path . as_bstr ( ) ) ?) ) ;
99
+ match mode {
100
+ Mode :: Simulate => progress. info ( format ! (
101
+ "WOULD move {} to {}" ,
102
+ git_workdir. display( ) ,
103
+ destination. display( )
104
+ ) ) ,
105
+ Mode :: Execute => {
106
+ std:: fs:: rename ( git_workdir, & destination) ?;
107
+ progress. info ( format ! ( "Moved {} to {}" , git_workdir. display( ) , destination. display( ) ) )
108
+ }
109
+ }
31
110
Ok ( ( ) )
32
111
}
33
112
113
+ pub fn run ( mode : Mode , source_dir : PathBuf , destination : PathBuf , mut progress : impl Progress ) -> anyhow:: Result < ( ) > {
114
+ let search_progress = progress. add_child ( "Searching repositories" ) ;
115
+
116
+ let mut num_errors = 0usize ;
117
+ for path_to_move in find_git_repository_workdirs ( source_dir, search_progress) {
118
+ if let Err ( err) = handle ( mode, & path_to_move, & destination, & mut progress) {
119
+ progress. fail ( format ! (
120
+ "Error when handling directory {:?}: {}" ,
121
+ path_to_move. display( ) ,
122
+ err. to_string( )
123
+ ) ) ;
124
+ num_errors += 1 ;
125
+ }
126
+ }
127
+
128
+ if num_errors > 0 {
129
+ anyhow:: bail!( "Failed to handle {} repositories" , num_errors)
130
+ } else {
131
+ Ok ( ( ) )
132
+ }
133
+ }
134
+
34
135
mod parse {
35
136
use anyhow:: { bail, Context } ;
36
137
use bstr:: { BStr , ByteSlice } ;
37
138
38
139
#[ allow( unused) ]
39
- fn remotes_from_git_remote_verbose ( input : & [ u8 ] ) -> anyhow:: Result < Vec < ( & BStr , git_url:: Url ) > > {
140
+ pub fn remotes_from_git_remote_verbose ( input : & [ u8 ] ) -> anyhow:: Result < Vec < ( & BStr , git_url:: Url ) > > {
40
141
fn parse_line ( line : & BStr ) -> anyhow:: Result < ( & BStr , git_url:: Url ) > {
41
142
let mut tokens = line. splitn ( 2 , |b| * b == b'\t' ) ;
42
143
Ok ( match ( tokens. next ( ) , tokens. next ( ) , tokens. next ( ) ) {
0 commit comments