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