98286406 Andy McCall 1 month ago ↶
1
2
3
use super :: * ;
4
5
# [ derive ( Debug, Default, Clone, PartialEq, Eq ) ]
6
pub struct CheckoutReport {
7
pub written : usize ,
8
pub removed : usize ,
9
pub conflicted : bool ,
4dee277c Andy McCall 1 month ago ↶
10
11
pub notes : Vec< String> ,
98286406 Andy McCall 1 month ago ↶
12
}
13
14
impl Repo {
15
16
17
18
pub fn checkout ( & self , heads : & [ChangeHash], discard : bool , command : & str ) -> Result< CheckoutReport> {
19
let snap = self .snapshot ( command) ? ;
20
if ! discard & & ( ! snap.wc.tree_ops.is_empty ( ) | | ! snap.wc.edits.is_empty ( ) ) {
21
return fail ( " the working copy has changes; commit them first or pass --discard" ) ;
22
}
23
let target_state = self .load_state ( heads) ? ;
24
let heads = target_state.maximal ( heads) ;
25
let target = target_state.materialize ( ) ;
cdb5beba Andy McCall 1 month ago ↶
26
let attrs = Repo:: state_attributes( & target) ;
98286406 Andy McCall 1 month ago ↶
27
let current = self .walk_working_copy ( ) ? ;
28
let mut report = CheckoutReport:: default( ) ;
29
4dee277c Andy McCall 1 month ago ↶
30
31
98286406 Andy McCall 1 month ago ↶
32
let mut dirs_to_prune: BTreeSet< PathBuf> = BTreeSet:: new( ) ;
4dee277c Andy McCall 1 month ago ↶
33
for ( path, entry) in & current {
98286406 Andy McCall 1 month ago ↶
34
if ! target.files.contains_key ( path) {
4dee277c Andy McCall 1 month ago ↶
35
if entry.kind = = NodeKind:: Subrepo {
36
report.notes.push ( format! ( " {path} : nested repository left in place" ) ) ;
37
continue ;
38
}
98286406 Andy McCall 1 month ago ↶
39
let full = self .safe_path ( path) ? ;
40
fs:: remove_file( & full) ? ;
41
report.removed + = 1 ;
42
if let Some ( parent) = full.parent ( ) {
43
dirs_to_prune.insert ( parent.to_path_buf ( ) ) ;
44
}
45
}
46
}
7421309f Andy McCall 1 month ago ↶
47
98286406 Andy McCall 1 month ago ↶
48
for ( path, f) in & target.files {
7421309f Andy McCall 1 month ago ↶
49
if ! self .in_scope ( path) {
50
continue ;
51
}
98286406 Andy McCall 1 month ago ↶
52
match & f.content {
53
FileContent:: Dir => continue ,
54
FileContent:: Text { bytes, .. } => {
cdb5beba Andy McCall 1 month ago ↶
55
let a = attrs.lookup ( path) ;
56
let on_disk: std:: borrow:: Cow< [ u8 ] > = if a.eol = = Some ( crate :: attributes:: Eol:: Crlf) { crate :: attributes:: to_crlf( bytes) .into ( ) } else { bytes.as_slice ( ) .into ( ) } ;
57
let same = current.get ( path) .is_some_and ( | c | c.kind = = NodeKind:: File & & c.content = = * on_disk & & c.exec = = f.mode.exec ) ;
98286406 Andy McCall 1 month ago ↶
58
if ! same {
cdb5beba Andy McCall 1 month ago ↶
59
self .write_file ( path, & on_disk, f.mode.exec) ? ;
98286406 Andy McCall 1 month ago ↶
60
report.written + = 1 ;
61
}
cdb5beba Andy McCall 1 month ago ↶
62
if a.lockable & & ! self .is_locked ( path) {
63
let _ = set_readonly ( & self .root.join ( path) , true ) ;
64
}
98286406 Andy McCall 1 month ago ↶
65
}
66
FileContent:: Opaque { blobs } => {
67
let Some ( blob) = blobs.first ( ) else { continue } ;
68
let bytes = self .read_blob ( blob) ? ;
4dee277c Andy McCall 1 month ago ↶
69
if f.kind = = NodeKind:: Subrepo {
70
let pin = crate :: subrepo:: Pin:: decode( & bytes) ? ;
71
match self .sync_subrepo ( path, & pin, false ) ? {
72
SubrepoSync:: Updated => report.written + = 1 ,
73
SubrepoSync:: UpToDate => { }
74
SubrepoSync:: Missing => report.notes.push ( format! ( " {path} : nested repository not present; run oxy submodule update --init" ) ) ,
75
SubrepoSync:: Dirty => report.notes.push ( format! ( " {path} : nested repository has uncommitted changes; left as is" ) ) ,
76
SubrepoSync:: Failed( e) => report.notes.push ( format! ( " {path} : {e} " ) ) ,
77
}
78
continue ;
79
}
98286406 Andy McCall 1 month ago ↶
80
if f.kind = = NodeKind:: Symlink {
81
let same = current.get ( path) .is_some_and ( | c | c.kind = = NodeKind:: Symlink & & c.content = = bytes ) ;
82
if ! same {
83
self .write_symlink ( path, & bytes) ? ;
84
report.written + = 1 ;
85
}
86
} else {
87
let same = current.get ( path) .is_some_and ( | c | c.kind = = NodeKind:: File & & c.content = = bytes & & c.exec = = f.mode.exec ) ;
88
if ! same {
89
self .write_file ( path, & bytes, f.mode.exec) ? ;
90
report.written + = 1 ;
91
}
cdb5beba Andy McCall 1 month ago ↶
92
if attrs.lookup ( path) .lockable & & ! self .is_locked ( path) {
93
let _ = set_readonly ( & self .root.join ( path) , true ) ;
94
}
98286406 Andy McCall 1 month ago ↶
95
}
96
if blobs.len ( ) > 1 {
97
for ( i, b) in blobs.iter ( ) .enumerate ( ) .skip ( 1 ) {
98
self .write_file ( & format! ( " {path} .oxy-conflict-{} " , i + 1 ) , & self .read_blob ( b) ? , f.mode.exec) ? ;
99
}
100
}
101
}
102
}
103
}
104
105
for d in dirs_to_prune.iter ( ) .rev ( ) {
106
let mut d = d.clone ( ) ;
107
while d ! = self .root & & fs:: read_dir( & d) .map ( | mut i | i.next ( ) .is_none ( ) ) .unwrap_or ( false ) {
108
fs:: remove_dir( & d) ? ;
109
let Some ( p) = d.parent ( ) else { break } ;
110
d = p.to_path_buf ( ) ;
111
}
112
}
113
114
let ( op_ref, op) = self .head_op ( ) ? ;
6d3eefbc Andy McCall 1 month ago ↶
115
let wc = self .new_working_copy ( heads.clone ( ) , " " ) ;
98286406 Andy McCall 1 month ago ↶
116
let wc_hash = self .store.put_change ( & wc) ? ;
117
let mut view = op.view.clone ( ) ;
118
view.workspaces.insert ( self .workspace, wc_hash) ;
119
self .record_op ( & [ op_ref] , command, view) ? ;
120
report.conflicted = target.is_conflicted ( ) ;
b7f102fc Andy McCall 1 month ago ↶
121
let blobs: HashMap< String, ObjectRef> = target
122
.files
123
.iter ( )
124
.filter_map ( | ( p , f ) | match & f.content {
125
FileContent:: Opaque { blobs } if f.kind = = NodeKind:: File & & blobs.len ( ) = = 1 => Some ( ( p.clone ( ) , blobs[ 0 ] ) ) ,
126
_ => None ,
127
} )
128
.collect ( ) ;
4dee277c Andy McCall 1 month ago ↶
129
let entries = self .walk_working_copy_with ( None , & Self :: tracked_kinds( & target) ) ? ;
b7f102fc Andy McCall 1 month ago ↶
130
self .write_manifest_entries ( report.conflicted, false , & entries, & blobs) ? ;
6d3eefbc Andy McCall 1 month ago ↶
131
let _ = self .run_hook (
132
" post-checkout" ,
133
& format! ( " {{ \" event\" :\" post-checkout\" ,\" heads\" :{} ,\" written\" :{} ,\" removed\" :{} }} " , json_array ( heads.iter ( ) .map ( | h | json_str ( & h.to_hex ( ) ) ) ) , report.written, report.removed) ,
134
) ;
98286406 Andy McCall 1 month ago ↶
135
Ok ( report)
136
}
137
138
139
140
fn safe_path ( & self , rel : & str ) -> Result< PathBuf> {
141
let mut full = self .root.clone ( ) ;
142
let comps: Vec< & str > = rel.split ( ' /' ) .collect ( ) ;
143
for ( i, comp) in comps.iter ( ) .enumerate ( ) {
144
if comp.is_empty ( ) | | * comp = = " ." | | * comp = = " .." | | comp.contains ( ' \\ ' ) {
145
return fail ( format! ( " refusing unsafe path {rel:?} " ) ) ;
146
}
147
if i = = 0 & & * comp = = OXY_DIR {
148
return fail ( format! ( " refusing to write inside {OXY_DIR} : {rel:?} " ) ) ;
149
}
150
if i + 1 < comps.len ( ) {
151
full.push ( comp) ;
152
if let Ok ( m) = fs:: symlink_metadata( & full) {
153
if m.file_type ( ) .is_symlink ( ) {
154
return fail ( format! ( " refusing to write through symlink {} " , full.display ( ) ) ) ;
155
}
156
}
157
} else {
158
full.push ( comp) ;
159
}
160
}
161
Ok ( full)
162
}
163
c6aa5bd9 Andy McCall 1 month ago ↶
164
pub ( crate ) fn write_file ( & self , rel : & str , bytes : & [u8 ], exec : bool ) -> Result< ( ) > {
98286406 Andy McCall 1 month ago ↶
165
let full = self .safe_path ( rel) ? ;
166
if let Some ( parent) = full.parent ( ) {
167
fs:: create_dir_all( parent) ? ;
168
}
169
if fs:: symlink_metadata( & full) .is_ok_and ( | m | m.file_type ( ) .is_symlink ( ) | | m.is_dir ( ) ) {
170
remove_any ( & full) ? ;
171
}
cdb5beba Andy McCall 1 month ago ↶
172
if fs:: metadata( & full) .is_ok_and ( | m | m.permissions ( ) .readonly ( ) ) {
173
set_readonly ( & full, false ) ? ;
174
}
98286406 Andy McCall 1 month ago ↶
175
fs:: write( & full, bytes) ? ;
176
set_executable ( & full, exec) ? ;
177
Ok ( ( ) )
178
}
179
c6aa5bd9 Andy McCall 1 month ago ↶
180
pub ( crate ) fn write_symlink ( & self , rel : & str , target : & [u8 ]) -> Result< ( ) > {
98286406 Andy McCall 1 month ago ↶
181
let full = self .safe_path ( rel) ? ;
182
if let Some ( parent) = full.parent ( ) {
183
fs:: create_dir_all( parent) ? ;
184
}
185
if fs:: symlink_metadata( & full) .is_ok ( ) {
186
remove_any ( & full) ? ;
187
}
188
# [ cfg ( unix ) ]
189
{
190
std:: os:: unix:: fs:: symlink( String :: from_utf8_lossy( target) .as_ref ( ) , & full) ? ;
191
Ok ( ( ) )
192
}
193
# [ cfg ( not ( unix ) ) ]
194
{
195
196
fs:: write( & full, target) ? ;
197
Ok ( ( ) )
198
}
199
}
200
}