957fee30 Andy McCall 1 month ago ↶
1
2
3
4
5
use super :: * ;
6
7
8
# [ derive ( Clone, Debug ) ]
9
pub struct BlameLine {
10
pub line : usize ,
11
pub hash : ChangeHash,
12
pub index : u32 ,
13
pub change_id : ChangeId,
14
pub author : String,
15
pub when_ms : u64 ,
16
pub text : Vec< u8 > ,
17
}
18
19
20
# [ derive ( Clone, Debug, PartialEq, Eq ) ]
21
pub enum BisectStep {
22
23
NeedMarks { good: usize , bad: usize } ,
24
25
Testing { heads: Vec< ChangeHash> , remaining: usize , steps_left: u32 } ,
26
27
Found { culprit: ChangeHash } ,
28
29
Inconclusive { candidates: Vec< ChangeHash> } ,
30
}
31
32
# [ derive ( Clone, Debug, Default ) ]
33
struct BisectState {
34
start : Vec< ChangeHash> ,
35
good : Vec< ChangeHash> ,
36
bad : Vec< ChangeHash> ,
37
candidates : Vec< ChangeHash> ,
38
lo : usize ,
39
hi : usize ,
40
41
testing : Option< usize > ,
42
skipped : Vec< usize > ,
43
}
44
45
fn hashes_line ( v : & [ChangeHash]) -> String {
46
v.iter ( ) .map ( | h | h.to_hex ( ) ) .collect:: < Vec< _ > > ( ) .join ( " " )
47
}
48
fn hashes_from ( s : & str ) -> Vec< ChangeHash> {
49
s.split_whitespace ( ) .filter_map ( ObjectRef:: from_hex) .collect ( )
50
}
51
52
impl Repo {
53
54
55
pub fn blame ( & self , path : & str ) -> Result< Vec< BlameLine> > {
56
let snap = self .snapshot ( " oxy blame" ) ? ;
57
let Some ( f) = snap.base_snapshot.files.get ( path) else { return fail ( format! ( " {path} is not in the working copy's base state" ) ) } ;
58
if ! matches! ( f.content, FileContent:: Text { .. } ) {
59
return fail ( format! ( " {path} is not a text file" ) ) ;
60
}
61
let mut out = Vec :: new( ) ;
62
for ( i, ( ( hash, index) , text) ) in snap.base.live_lines ( & f.node) .into_iter ( ) .enumerate ( ) {
63
let meta = snap.base.get ( & hash) ;
64
let author = match meta {
65
Some ( m) if ! m.author.is_zero ( ) => self .store.get_identity ( & m.author) .map ( | id | id.name ) .unwrap_or_else ( | _| m.author.short ( 12 ) ) ,
66
Some ( m) => m.message.lines ( ) .rev ( ) .find_map ( | l | l.strip_prefix ( " Git-Author: " ) ) .map ( | a | a.split ( ' <' ) .next ( ) .unwrap_or ( a) .trim ( ) .to_string ( ) ) .unwrap_or_else ( | | " unknown" .into ( ) ) ,
67
None => " unknown" .into ( ) ,
68
} ;
69
out.push ( BlameLine {
70
line: i + 1 ,
71
hash,
72
index,
73
change_id: meta.map ( | m | m.change_id ) .unwrap_or ( ChangeId( [ 0 ; 16 ] ) ) ,
74
author,
75
when_ms: meta.map ( | m | m.hlc.wall_ms ) .unwrap_or ( 0 ) ,
76
text,
77
} ) ;
78
}
79
Ok ( out)
80
}
81
82
83
84
fn bisect_dir ( & self ) -> PathBuf {
85
self .oxy.join ( " bisect" )
86
}
87
88
fn bisect_load ( & self ) -> Result< Option< BisectState> > {
89
let dir = self .bisect_dir ( ) ;
90
if ! dir.join ( " start" ) .exists ( ) {
91
return Ok ( None ) ;
92
}
93
let read = | name : & str | fs:: read_to_string( dir.join ( name) ) .unwrap_or_default ( ) ;
94
let nums: Vec< usize > = read ( " range" ) .split_whitespace ( ) .filter_map ( | n | n.parse ( ) .ok ( ) ) .collect ( ) ;
95
Ok ( Some ( BisectState {
96
start: hashes_from ( & read ( " start" ) ) ,
97
good: hashes_from ( & read ( " good" ) ) ,
98
bad: hashes_from ( & read ( " bad" ) ) ,
99
candidates: hashes_from ( & read ( " candidates" ) ) ,
100
lo: nums.first ( ) .copied ( ) .unwrap_or ( 0 ) ,
101
hi: nums.get ( 1 ) .copied ( ) .unwrap_or ( 0 ) ,
102
testing: nums.get ( 2 ) .copied ( ) .filter ( | _| nums.len ( ) > 2 ) ,
103
skipped: read ( " skipped" ) .split_whitespace ( ) .filter_map ( | n | n.parse ( ) .ok ( ) ) .collect ( ) ,
104
} ) )
105
}
106
107
fn bisect_save ( & self , st : & BisectState) -> Result< ( ) > {
108
let dir = self .bisect_dir ( ) ;
109
fs:: create_dir_all( & dir) ? ;
110
write_atomic ( & dir.join ( " start" ) , hashes_line ( & st.start) .as_bytes ( ) ) ? ;
111
write_atomic ( & dir.join ( " good" ) , hashes_line ( & st.good) .as_bytes ( ) ) ? ;
112
write_atomic ( & dir.join ( " bad" ) , hashes_line ( & st.bad) .as_bytes ( ) ) ? ;
113
write_atomic ( & dir.join ( " candidates" ) , hashes_line ( & st.candidates) .as_bytes ( ) ) ? ;
114
let mut range = format! ( " {} {} " , st.lo, st.hi) ;
115
if let Some ( t) = st.testing {
116
range.push_str ( & format! ( " {t} " ) ) ;
117
}
118
write_atomic ( & dir.join ( " range" ) , range.as_bytes ( ) ) ? ;
119
write_atomic ( & dir.join ( " skipped" ) , st.skipped.iter ( ) .map ( | n | n.to_string ( ) ) .collect:: < Vec< _ > > ( ) .join ( " " ) .as_bytes ( ) ) ? ;
120
Ok ( ( ) )
121
}
122
123
124
pub fn bisect_start ( & self ) -> Result< ( ) > {
125
if self .bisect_load ( ) ? .is_some ( ) {
126
return fail ( " a bisect is in progress; finish it or run `oxy bisect reset`" ) ;
127
}
128
let ( _ , wc) = self .working_copy ( ) ? ;
129
let start = self .load_state ( & wc.deps) ? .maximal ( & wc.deps) ;
130
self .bisect_save ( & BisectState { start, .. Default :: default( ) } )
131
}
132
133
pub fn bisect_reset ( & self ) -> Result< ( ) > {
134
let Some ( st) = self .bisect_load ( ) ? else { return fail ( " no bisect in progress" ) } ;
135
self .checkout ( & st.start, false , " oxy bisect reset" ) ? ;
136
fs:: remove_dir_all( self .bisect_dir ( ) ) ? ;
137
Ok ( ( ) )
138
}
139
140
pub fn bisect_status ( & self ) -> Result< Option< BisectStep> > {
141
let Some ( st) = self .bisect_load ( ) ? else { return Ok ( None ) } ;
142
Ok ( Some ( self .bisect_step ( & st) ) )
143
}
144
145
fn bisect_step ( & self , st : & BisectState) -> BisectStep {
146
if st.candidates.is_empty ( ) {
147
return BisectStep:: NeedMarks { good: st.good.len ( ) , bad: st.bad.len ( ) } ;
148
}
149
if st.hi < = st.lo + 1 {
150
return BisectStep:: Found { culprit: st.candidates[ st.lo] } ;
151
}
152
match st.testing {
153
Some ( _ ) => {
154
let remaining = st.hi - st.lo;
155
BisectStep:: Testing { heads: self .bisect_heads ( st, st.testing.unwrap ( ) ) , remaining, steps_left: ( remaining as f64 ) .log2 ( ) .ceil ( ) as u32 }
156
}
157
None => BisectStep:: Inconclusive { candidates: st.candidates[ st.lo.. st.hi] .to_vec ( ) } ,
158
}
159
}
160
161
162
fn bisect_heads ( & self , st : & BisectState, k : usize ) -> Vec< ChangeHash> {
163
let mut all = st.good.clone ( ) ;
164
all.extend ( st.candidates[ .. k] .iter ( ) .copied ( ) ) ;
165
self .load_state ( & all) .map ( | s | s.maximal ( & all) ) .unwrap_or ( all)
166
}
167
168
169
170
fn bisect_advance ( & self , st : & mut BisectState) -> Result< BisectStep> {
171
if st.hi < = st.lo + 1 {
172
st.testing = None ;
173
self .bisect_save ( st) ? ;
174
return Ok ( BisectStep:: Found { culprit: st.candidates[ st.lo] } ) ;
175
}
176
let mid = st.lo + ( st.hi - st.lo) / 2 ;
177
178
let mut order = vec! [ mid] ;
179
for d in 1 .. ( st.hi - st.lo) {
180
if mid + d < st.hi {
181
order.push ( mid + d) ;
182
}
183
if mid > st.lo + d {
184
order.push ( mid - d) ;
185
}
186
}
187
let Some ( k) = order.into_iter ( ) .find ( | k | ! st.skipped.contains ( k) ) else {
188
st.testing = None ;
189
self .bisect_save ( st) ? ;
190
return Ok ( BisectStep:: Inconclusive { candidates: st.candidates[ st.lo.. st.hi] .to_vec ( ) } ) ;
191
} ;
192
let heads = self .bisect_heads ( st, k) ;
193
self .checkout ( & heads, false , & format! ( " oxy bisect (testing {k} of {} )" , st.candidates.len ( ) ) ) ? ;
194
st.testing = Some ( k) ;
195
self .bisect_save ( st) ? ;
196
let remaining = st.hi - st.lo;
197
Ok ( BisectStep:: Testing { heads, remaining, steps_left: ( remaining as f64 ) .log2 ( ) .ceil ( ) as u32 } )
198
}
199
200
201
202
203
pub fn bisect_mark ( & self , good : Option< bool > , changes : & [ChangeHash]) -> Result< BisectStep> {
204
let Some ( mut st) = self .bisect_load ( ) ? else { return fail ( " no bisect in progress; run `oxy bisect start`" ) } ;
205
if ! changes.is_empty ( ) {
206
if ! st.candidates.is_empty ( ) {
207
return fail ( " the search has started; mark the state under test with plain `oxy bisect good|bad`" ) ;
208
}
209
match good {
210
Some ( true ) => st.good.extend ( changes.iter ( ) .copied ( ) ) ,
211
Some ( false ) => st.bad.extend ( changes.iter ( ) .copied ( ) ) ,
212
None => return fail ( " skip applies to the state under test" ) ,
213
}
214
if st.good.is_empty ( ) | | st.bad.is_empty ( ) {
215
self .bisect_save ( & st) ? ;
216
return Ok ( BisectStep:: NeedMarks { good: st.good.len ( ) , bad: st.bad.len ( ) } ) ;
217
}
218
let good_heads = self .load_state ( & st.good) ? .maximal ( & st.good) ;
219
let bad_heads = self .load_state ( & st.bad) ? .maximal ( & st.bad) ;
220
st.good = good_heads.clone ( ) ;
221
st.bad = bad_heads.clone ( ) ;
222
st.candidates = self .negotiate ( & good_heads, & bad_heads) ? ;
223
if st.candidates.is_empty ( ) {
224
return fail ( " the bad state adds nothing to the good state; nothing to bisect" ) ;
225
}
226
st.lo = 0 ;
227
st.hi = st.candidates.len ( ) ;
228
return self .bisect_advance ( & mut st) ;
229
}
230
let Some ( k) = st.testing else {
231
return fail ( " no state is under test; give `oxy bisect good|bad` a change or bookmark to start with" ) ;
232
} ;
233
match good {
234
Some ( true ) => st.lo = k,
235
Some ( false ) => st.hi = k,
236
None => st.skipped.push ( k) ,
237
}
238
self .bisect_advance ( & mut st)
239
}
240
241
242
243
pub fn bisect_run ( & self , command : & [String]) -> Result< BisectStep> {
244
let Some ( st) = self .bisect_load ( ) ? else { return fail ( " no bisect in progress; run `oxy bisect start`" ) } ;
245
if st.testing.is_none ( ) {
246
return Ok ( self .bisect_step ( & st) ) ;
247
}
248
loop {
249
let status = std:: process:: Command:: new( & command[ 0 ] ) .args ( & command[ 1 .. ] ) .current_dir ( & self .root) .status ( ) .map_err ( | e | RepoError:: Msg( format! ( " running {} : {e} " , command[ 0 ] ) ) ) ? ;
250
let verdict = match status.code ( ) {
251
Some ( 0 ) => Some ( true ) ,
252
Some ( 125 ) => None ,
253
_ => Some ( false ) ,
254
} ;
255
match self .bisect_mark ( verdict, & [ ] ) ? {
256
BisectStep:: Testing { .. } => continue ,
257
done => return Ok ( done) ,
258
}
259
}
260
}
261
}