135212de Andy McCall 1 month ago ↶
1
2
3
4
5
6
7
8
9
10
use super :: * ;
11
use std:: time:: UNIX_EPOCH ;
12
13
# [ derive ( Debug, Clone, Default, PartialEq, Eq ) ]
14
pub struct PromptInfo {
15
pub bookmark : String,
16
17
pub change_id : String,
18
pub heads : usize ,
19
pub dirty : bool ,
20
pub conflicted : bool ,
21
22
pub ahead : Option< usize > ,
23
pub behind : Option< usize > ,
24
}
25
26
impl PromptInfo {
27
28
29
30
pub fn format ( & self , fmt : & str ) -> String {
31
let mut out = String :: new( ) ;
32
let mut chars = fmt.chars ( ) ;
33
while let Some ( c) = chars.next ( ) {
34
if c ! = ' %' {
35
out.push ( c) ;
36
continue ;
37
}
38
match chars.next ( ) {
39
Some ( ' b' ) => out.push_str ( & self .bookmark) ,
40
Some ( ' c' ) => out.push_str ( & self .change_id) ,
41
Some ( ' h' ) => out.push_str ( & self .heads.to_string ( ) ) ,
42
Some ( ' d' ) => {
43
if self .dirty {
44
out.push ( ' *' ) ;
45
}
46
}
47
Some ( ' x' ) => {
48
if self .conflicted {
49
out.push ( ' !' ) ;
50
}
51
}
52
Some ( ' a' ) => out.push_str ( & self .ahead.map ( | n | n.to_string ( ) ) .unwrap_or_default ( ) ) ,
53
Some ( ' B' ) => out.push_str ( & self .behind.map ( | n | n.to_string ( ) ) .unwrap_or_default ( ) ) ,
54
Some ( ' A' ) => {
55
if let Some ( n) = self .ahead.filter ( | n | * n > 0 ) {
56
out.push_str ( & format! ( " +{n} " ) ) ;
57
}
58
}
59
Some ( ' D' ) => {
60
if let Some ( n) = self .behind.filter ( | n | * n > 0 ) {
61
out.push_str ( & format! ( " -{n} " ) ) ;
62
}
63
}
64
Some ( ' %' ) => out.push ( ' %' ) ,
65
Some ( other) => {
66
out.push ( ' %' ) ;
67
out.push ( other) ;
68
}
69
None => out.push ( ' %' ) ,
70
}
71
}
72
out
73
}
74
}
75
76
77
type Manifest = BTreeMap< String, ( u64 , u128, String) > ;
78
79
pub const DEFAULT_PROMPT_FORMAT : & str = " %b %c%d%x%A%D" ;
80
81
82
fn manifest_line ( path : & str , size : u64 , mtime_ns : u128, hash : & blake3:: Hash) -> String {
83
format! ( " {size} \t {mtime_ns} \t {} \t {path} \n " , hash.to_hex ( ) )
84
}
85
86
impl Repo {
87
fn manifest_path ( & self ) -> PathBuf {
88
self .oxy.join ( " workspaces" ) .join ( self .workspace.to_hex ( ) ) .join ( " manifest" )
89
}
90
91
92
93
94
pub ( crate ) fn write_manifest ( & self , conflicted : bool , uncommitted : bool ) -> Result< ( ) > {
95
let mut text = format! ( " # conflicted={} \n # uncommitted={} \n " , conflicted as u8 , uncommitted as u8 ) ;
96
for ( path, e) in self .walk_working_copy ( ) ? {
97
let full = self .root.join ( & path) ;
98
let ( size, mtime_ns) = match fs:: symlink_metadata( & full) {
99
Ok ( m) => ( m.len ( ) , m.modified ( ) .ok ( ) .and_then ( | t | t.duration_since ( UNIX_EPOCH ) .ok ( ) ) .map ( | d | d.as_nanos ( ) ) .unwrap_or ( 0 ) ) ,
100
Err ( _ ) => ( e.content.len ( ) as u64 , 0 ) ,
101
} ;
102
text.push_str ( & manifest_line ( & path, size, mtime_ns, & blake3:: hash( & e.content) ) ) ;
103
}
104
let p = self .manifest_path ( ) ;
105
fs:: create_dir_all( p.parent ( ) .unwrap ( ) ) ? ;
106
write_atomic ( & p, text.as_bytes ( ) )
107
}
108
109
110
111
112
pub ( crate ) fn drop_manifest ( & self ) {
113
let _ = fs:: remove_file( self .manifest_path ( ) ) ;
114
}
115
116
fn read_manifest ( & self ) -> Result< Option< ( bool , bool , Manifest) > > {
117
let text = match fs:: read_to_string( self .manifest_path ( ) ) {
118
Ok ( t) => t,
119
Err ( e) if e.kind ( ) = = std:: io:: ErrorKind:: NotFound => return Ok ( None ) ,
120
Err ( e) => return Err ( e.into ( ) ) ,
121
} ;
122
let mut out = BTreeMap:: new( ) ;
123
let mut conflicted = false ;
124
let mut uncommitted = false ;
125
for line in text.lines ( ) {
126
if let Some ( v) = line.strip_prefix ( " # conflicted=" ) {
127
conflicted = v.trim ( ) = = " 1" ;
128
continue ;
129
}
130
if let Some ( v) = line.strip_prefix ( " # uncommitted=" ) {
131
uncommitted = v.trim ( ) = = " 1" ;
132
continue ;
133
}
134
let f: Vec< & str > = line.splitn ( 4 , ' \t ' ) .collect ( ) ;
135
if f.len ( ) = = 4 {
136
out.insert ( f[ 3 ] .to_string ( ) , ( f[ 0 ] .parse ( ) .unwrap_or ( 0 ) , f[ 1 ] .parse ( ) .unwrap_or ( 0 ) , f[ 2 ] .to_string ( ) ) ) ;
137
}
138
}
139
Ok ( Some ( ( conflicted, uncommitted, out) ) )
140
}
141
142
143
144
pub fn quick_dirty ( & self ) -> Result< Option< bool > > {
145
let Some ( ( _ , uncommitted, manifest) ) = self .read_manifest ( ) ? else { return Ok ( None ) } ;
146
if uncommitted {
147
return Ok ( Some ( true ) ) ;
148
}
149
let mut seen = 0 usize ;
150
let mut wb = ignore:: WalkBuilder:: new( & self .root) ;
151
wb.hidden ( false ) .git_ignore ( false ) .git_global ( false ) .git_exclude ( false ) .require_git ( false ) .follow_links ( false ) .add_custom_ignore_filename ( " .oxyignore" ) ;
152
wb.filter_entry ( | e | e.file_name ( ) ! = OXY_DIR ) ;
153
for entry in wb.build ( ) {
154
let Ok ( entry) = entry else { continue } ;
155
let Some ( ft) = entry.file_type ( ) else { continue } ;
156
if ft.is_dir ( ) {
157
continue ;
158
}
159
let rel: String = entry.path ( ) .strip_prefix ( & self .root) .unwrap ( ) .components ( ) .map ( | c | c.as_os_str ( ) .to_string_lossy ( ) .to_string ( ) ) .collect:: < Vec< _ > > ( ) .join ( " /" ) ;
160
let Some ( ( size, mtime_ns, hash) ) = manifest.get ( & rel) else { return Ok ( Some ( true ) ) } ;
161
seen + = 1 ;
162
let m = fs:: symlink_metadata( entry.path ( ) ) ? ;
163
let now_mtime = m.modified ( ) .ok ( ) .and_then ( | t | t.duration_since ( UNIX_EPOCH ) .ok ( ) ) .map ( | d | d.as_nanos ( ) ) .unwrap_or ( 0 ) ;
164
if ft.is_symlink ( ) {
165
let target = fs:: read_link( entry.path ( ) ) ? ;
166
if blake3:: hash( target.to_string_lossy ( ) .as_bytes ( ) ) .to_hex ( ) .as_str ( ) ! = hash {
167
return Ok ( Some ( true ) ) ;
168
}
169
continue ;
170
}
171
if m.len ( ) ! = * size {
172
return Ok ( Some ( true ) ) ;
173
}
174
if now_mtime ! = * mtime_ns & & blake3:: hash( & fs:: read( entry.path ( ) ) ? ) .to_hex ( ) .as_str ( ) ! = hash {
175
return Ok ( Some ( true ) ) ;
176
}
177
}
178
Ok ( Some ( seen ! = manifest.len ( ) ) )
179
}
180
181
182
pub fn quick_status ( & self ) -> Result< PromptInfo> {
183
let view = self .view ( ) ? ;
184
let bookmark = self .config.bookmark.clone ( ) ;
185
let heads = self .heads_of ( & view, & bookmark) ? ;
186
let wc_hash = view.workspaces.get ( & self .workspace) .copied ( ) ;
187
let change_id = match wc_hash {
188
Some ( h) => self .store.get_change ( & h) ? .change_id.short ( 8 ) ,
189
None => String :: new( ) ,
190
} ;
191
192
193
let manifest = self .read_manifest ( ) ? ;
194
let conflicted = manifest.as_ref ( ) .map ( | ( c , _, _) | * c) .unwrap_or ( false ) ;
195
let dirty = self .quick_dirty ( ) ? .unwrap_or ( false ) ;
196
let ( ahead, behind) = match view.remotes.get ( " origin" ) .and_then ( | b | b.get ( & bookmark) ) {
197
Some ( st) => {
198
let remote_heads = self .store.get_state ( st) ? .heads;
199
let ahead = self .negotiate ( & remote_heads, & heads) .map ( | v | v.len ( ) ) .ok ( ) ;
200
let behind = self .negotiate ( & heads, & remote_heads) .map ( | v | v.len ( ) ) .ok ( ) ;
201
( ahead, behind)
202
}
203
None => ( None , None ) ,
204
} ;
205
Ok ( PromptInfo { bookmark, change_id, heads: heads.len ( ) , dirty, conflicted, ahead, behind } )
206
}
207
}