1use std::collections::{HashMap, HashSet};
7use std::sync::{Arc, Mutex};
8use std::time::SystemTime;
9
10use bstr::{ByteSlice, ByteVec};
11use cursive::theme::BaseColor;
12use cursive::utils::markup::StyledString;
13use lazy_static::lazy_static;
14use regex::Regex;
15use tracing::instrument;
16
17use crate::core::config::{
18 get_commit_descriptors_branches, get_commit_descriptors_differential_revision,
19 get_commit_descriptors_relative_time,
20};
21use crate::git::{
22 CategorizedReferenceName, Commit, NonZeroOid, ReferenceName, Repo, ResolvedReferenceInfo,
23};
24
25use super::eventlog::{Event, EventCursor, EventReplayer};
26use super::formatting::{Glyphs, StyledStringBuilder};
27use super::repo_ext::RepoReferencesSnapshot;
28use super::rewrite::find_rewrite_target;
29
30#[derive(Clone, Debug)]
32pub enum NodeObject<'repo> {
33 Commit {
35 commit: Commit<'repo>,
37 },
38
39 GarbageCollected {
42 oid: NonZeroOid,
44 },
45}
46
47impl<'repo> NodeObject<'repo> {
48 fn get_oid(&self) -> NonZeroOid {
49 match self {
50 NodeObject::Commit { commit } => commit.get_oid(),
51 NodeObject::GarbageCollected { oid } => *oid,
52 }
53 }
54
55 fn get_short_oid(&self) -> eyre::Result<String> {
56 match self {
57 NodeObject::Commit { commit } => Ok(commit.get_short_oid()?),
58 NodeObject::GarbageCollected { oid } => {
59 Ok(oid.to_string()[..7].to_string())
61 }
62 }
63 }
64}
65
66#[derive(Debug)]
69pub enum Redactor {
70 Disabled,
72
73 Enabled {
75 preserved_ref_names: HashSet<ReferenceName>,
78
79 ref_names: Arc<Mutex<HashMap<ReferenceName, ReferenceName>>>,
81 },
82}
83
84impl Redactor {
85 pub fn new(preserved_ref_names: HashSet<ReferenceName>) -> Self {
87 Self::Enabled {
88 preserved_ref_names,
89 ref_names: Default::default(),
90 }
91 }
92
93 pub fn redact_ref_name(&self, ref_name: ReferenceName) -> ReferenceName {
95 match self {
96 Redactor::Disabled => ref_name,
97 Redactor::Enabled {
98 preserved_ref_names,
99 ref_names,
100 } => {
101 if preserved_ref_names.contains(&ref_name) || !ref_name.as_str().contains('/') {
102 return ref_name;
103 }
104
105 let mut ref_names = ref_names.lock().expect("Poisoned mutex");
106 let len = ref_names.len();
107 ref_names
108 .entry(ref_name)
109 .or_insert_with_key(|ref_name| {
110 let categorized_ref_name = CategorizedReferenceName::new(ref_name);
111 let prefix = match categorized_ref_name {
112 CategorizedReferenceName::LocalBranch { name: _, prefix } => prefix,
113 CategorizedReferenceName::RemoteBranch { name: _, prefix } => prefix,
114 CategorizedReferenceName::OtherRef { name: _ } => "",
115 };
116 format!("{prefix}redacted-ref-{len}").into()
117 })
118 .clone()
119 }
120 }
121 }
122
123 pub fn redact_commit_summary(&self, summary: String) -> String {
125 match self {
126 Redactor::Disabled => summary,
127 Redactor::Enabled {
128 preserved_ref_names: _,
129 ref_names: _,
130 } => summary
131 .chars()
132 .map(|char| {
133 if char.is_ascii_whitespace() {
134 char
135 } else {
136 'x'
137 }
138 })
139 .collect(),
140 }
141 }
142}
143
144pub trait NodeDescriptor {
146 fn describe_node(
151 &mut self,
152 glyphs: &Glyphs,
153 object: &NodeObject,
154 ) -> eyre::Result<Option<StyledString>>;
155}
156
157#[instrument(skip(node_descriptors))]
159pub fn render_node_descriptors(
160 glyphs: &Glyphs,
161 object: &NodeObject,
162 node_descriptors: &mut [&mut dyn NodeDescriptor],
163) -> eyre::Result<StyledString> {
164 let descriptions = node_descriptors
165 .iter_mut()
166 .filter_map(|provider: &mut &mut dyn NodeDescriptor| {
167 provider.describe_node(glyphs, object).transpose()
168 })
169 .collect::<eyre::Result<Vec<_>>>()?;
170 let result = StyledStringBuilder::join(" ", descriptions);
171 Ok(result)
172}
173
174#[derive(Debug)]
176pub struct CommitOidDescriptor {
177 use_color: bool,
178}
179
180impl CommitOidDescriptor {
181 pub fn new(use_color: bool) -> eyre::Result<Self> {
183 Ok(CommitOidDescriptor { use_color })
184 }
185}
186
187impl NodeDescriptor for CommitOidDescriptor {
188 #[instrument]
189 fn describe_node(
190 &mut self,
191 _glyphs: &Glyphs,
192 object: &NodeObject,
193 ) -> eyre::Result<Option<StyledString>> {
194 let oid = object.get_short_oid()?;
195 let oid = if self.use_color {
196 StyledString::styled(oid, BaseColor::Yellow.dark())
197 } else {
198 StyledString::plain(oid)
199 };
200 Ok(Some(oid))
201 }
202}
203
204#[derive(Debug)]
206pub struct CommitMessageDescriptor<'a> {
207 redactor: &'a Redactor,
208}
209
210impl<'a> CommitMessageDescriptor<'a> {
211 pub fn new(redactor: &'a Redactor) -> eyre::Result<Self> {
213 Ok(CommitMessageDescriptor { redactor })
214 }
215}
216
217impl<'a> NodeDescriptor for CommitMessageDescriptor<'a> {
218 #[instrument]
219 fn describe_node(
220 &mut self,
221 _glyphs: &Glyphs,
222 object: &NodeObject,
223 ) -> eyre::Result<Option<StyledString>> {
224 let summary = match object {
225 NodeObject::Commit { commit } => {
226 let summary = commit.get_summary()?.to_vec();
227 summary.into_string_lossy()
228 }
229 NodeObject::GarbageCollected { oid: _ } => "<garbage collected>".to_string(),
230 };
231 let summary = self.redactor.redact_commit_summary(summary);
232 Ok(Some(StyledString::plain(summary)))
233 }
234}
235
236pub struct ObsolescenceExplanationDescriptor<'a> {
238 event_replayer: &'a EventReplayer,
239 event_cursor: EventCursor,
240}
241
242impl<'a> ObsolescenceExplanationDescriptor<'a> {
243 pub fn new(event_replayer: &'a EventReplayer, event_cursor: EventCursor) -> eyre::Result<Self> {
245 Ok(ObsolescenceExplanationDescriptor {
246 event_replayer,
247 event_cursor,
248 })
249 }
250}
251
252impl<'a> NodeDescriptor for ObsolescenceExplanationDescriptor<'a> {
253 fn describe_node(
254 &mut self,
255 _glyphs: &Glyphs,
256 object: &NodeObject,
257 ) -> eyre::Result<Option<StyledString>> {
258 let event = self
259 .event_replayer
260 .get_cursor_commit_latest_event(self.event_cursor, object.get_oid());
261
262 let event = match event {
263 Some(event) => event,
264 None => return Ok(None),
265 };
266
267 let result = match event {
268 Event::RewriteEvent { .. } => {
269 let rewrite_target =
270 find_rewrite_target(self.event_replayer, self.event_cursor, object.get_oid());
271 rewrite_target.map(|rewritten_oid| {
272 StyledString::styled(
273 format!("(rewritten as {})", &rewritten_oid.to_string()[..8]),
274 BaseColor::Black.light(),
275 )
276 })
277 }
278
279 Event::ObsoleteEvent { .. } => Some(StyledString::styled(
280 "(manually hidden)",
281 BaseColor::Black.light(),
282 )),
283
284 Event::RefUpdateEvent { .. }
285 | Event::CommitEvent { .. }
286 | Event::UnobsoleteEvent { .. }
287 | Event::WorkingCopySnapshot { .. } => None,
288 };
289 Ok(result)
290 }
291}
292
293#[derive(Debug)]
295pub struct BranchesDescriptor<'a> {
296 is_enabled: bool,
297 head_info: &'a ResolvedReferenceInfo,
298 references_snapshot: &'a RepoReferencesSnapshot,
299 redactor: &'a Redactor,
300}
301
302impl<'a> BranchesDescriptor<'a> {
303 pub fn new(
305 repo: &Repo,
306 head_info: &'a ResolvedReferenceInfo,
307 references_snapshot: &'a RepoReferencesSnapshot,
308 redactor: &'a Redactor,
309 ) -> eyre::Result<Self> {
310 let is_enabled = get_commit_descriptors_branches(repo)?;
311 Ok(BranchesDescriptor {
312 is_enabled,
313 head_info,
314 references_snapshot,
315 redactor,
316 })
317 }
318}
319
320impl<'a> NodeDescriptor for BranchesDescriptor<'a> {
321 #[instrument]
322 fn describe_node(
323 &mut self,
324 glyphs: &Glyphs,
325 object: &NodeObject,
326 ) -> eyre::Result<Option<StyledString>> {
327 if !self.is_enabled {
328 return Ok(None);
329 }
330
331 let branch_names: HashSet<ReferenceName> = match self
332 .references_snapshot
333 .branch_oid_to_names
334 .get(&object.get_oid())
335 {
336 Some(branch_names) => branch_names
337 .iter()
338 .map(|branch_name| self.redactor.redact_ref_name(branch_name.to_owned()))
339 .collect(),
340 None => HashSet::new(),
341 };
342
343 if branch_names.is_empty() {
344 Ok(None)
345 } else {
346 let mut branch_names: Vec<String> = branch_names
347 .into_iter()
348 .map(|branch_name| {
349 let is_checked_out_branch =
350 self.head_info.reference_name.as_ref() == Some(&branch_name);
351 let icon = if is_checked_out_branch {
352 format!("{} ", glyphs.branch_arrow)
353 } else {
354 "".to_string()
355 };
356
357 match CategorizedReferenceName::new(&branch_name) {
358 reference_name @ CategorizedReferenceName::LocalBranch { .. } => {
359 format!("{}{}", icon, reference_name.render_suffix())
360 }
361 reference_name @ CategorizedReferenceName::RemoteBranch { .. } => {
362 format!("{}remote {}", icon, reference_name.render_suffix())
363 }
364 reference_name @ CategorizedReferenceName::OtherRef { .. } => {
365 format!("{}ref {}", icon, reference_name.render_suffix())
366 }
367 }
368 })
369 .collect();
370 branch_names.sort_unstable();
371 let result = StyledString::styled(
372 format!("({})", branch_names.join(", ")),
373 BaseColor::Green.light(),
374 );
375 Ok(Some(result))
376 }
377 }
378}
379
380#[derive(Debug)]
382pub struct DifferentialRevisionDescriptor<'a> {
383 is_enabled: bool,
384 redactor: &'a Redactor,
385}
386
387impl<'a> DifferentialRevisionDescriptor<'a> {
388 pub fn new(repo: &Repo, redactor: &'a Redactor) -> eyre::Result<Self> {
390 let is_enabled = get_commit_descriptors_differential_revision(repo)?;
391 Ok(DifferentialRevisionDescriptor {
392 is_enabled,
393 redactor,
394 })
395 }
396}
397
398fn extract_diff_number(message: &str) -> Option<String> {
399 lazy_static! {
400 static ref RE: Regex = Regex::new(
401 r"(?mx)
402^
403Differential[\ ]Revision:[\ ]
404 (.+ /)?
405 (?P<diff>D[0-9]+)
406$",
407 )
408 .expect("Failed to compile `extract_diff_number` regex");
409 }
410 let captures = RE.captures(message)?;
411 let diff_number = &captures["diff"];
412 Some(diff_number.to_owned())
413}
414
415impl<'a> NodeDescriptor for DifferentialRevisionDescriptor<'a> {
416 #[instrument]
417 fn describe_node(
418 &mut self,
419 _glyphs: &Glyphs,
420 object: &NodeObject,
421 ) -> eyre::Result<Option<StyledString>> {
422 match self.redactor {
423 Redactor::Enabled { .. } => return Ok(None),
424 Redactor::Disabled => {}
425 }
426 if !self.is_enabled {
427 return Ok(None);
428 }
429 let commit = match object {
430 NodeObject::Commit { commit } => commit,
431 NodeObject::GarbageCollected { oid: _ } => return Ok(None),
432 };
433
434 let diff_number = match extract_diff_number(&commit.get_message_raw().to_str_lossy()) {
435 Some(diff_number) => diff_number,
436 None => return Ok(None),
437 };
438 let result = StyledString::styled(diff_number, BaseColor::Green.dark());
439 Ok(Some(result))
440 }
441}
442
443#[derive(Debug)]
445pub struct RelativeTimeDescriptor {
446 is_enabled: bool,
447 now: SystemTime,
448}
449
450impl RelativeTimeDescriptor {
451 pub fn new(repo: &Repo, now: SystemTime) -> eyre::Result<Self> {
453 let is_enabled = get_commit_descriptors_relative_time(repo)?;
454 Ok(RelativeTimeDescriptor { is_enabled, now })
455 }
456
457 pub fn is_enabled(&self) -> bool {
460 self.is_enabled
461 }
462
463 pub fn describe_time_delta(now: SystemTime, previous_time: SystemTime) -> eyre::Result<String> {
465 let mut delta: i64 = if previous_time < now {
466 let delta = now.duration_since(previous_time)?;
467 delta.as_secs().try_into()?
468 } else {
469 let delta = previous_time.duration_since(now)?;
470 -(delta.as_secs().try_into()?)
471 };
472
473 if delta < 60 {
474 return Ok(format!("{delta}s"));
475 }
476 delta /= 60;
477
478 if delta < 60 {
479 return Ok(format!("{delta}m"));
480 }
481 delta /= 60;
482
483 if delta < 24 {
484 return Ok(format!("{delta}h"));
485 }
486 delta /= 24;
487
488 if delta < 365 {
489 return Ok(format!("{delta}d"));
490 }
491 delta /= 365;
492
493 Ok(format!("{delta}y"))
495 }
496}
497
498impl NodeDescriptor for RelativeTimeDescriptor {
499 #[instrument]
500 fn describe_node(
501 &mut self,
502 _glyphs: &Glyphs,
503 object: &NodeObject,
504 ) -> eyre::Result<Option<StyledString>> {
505 if !self.is_enabled {
506 return Ok(None);
507 }
508 let commit = match object {
509 NodeObject::Commit { commit } => commit,
510 NodeObject::GarbageCollected { oid: _ } => return Ok(None),
511 };
512
513 let description = Self::describe_time_delta(self.now, commit.get_time().to_system_time()?)?;
514 let result = StyledString::styled(description, BaseColor::Green.dark());
515 Ok(Some(result))
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use std::ops::{Add, Sub};
522 use std::time::Duration;
523
524 use super::*;
525
526 #[test]
527 fn test_extract_diff_number() -> eyre::Result<()> {
528 let message = "\
529This is a message
530
531Differential Revision: D123";
532 assert_eq!(extract_diff_number(message), Some(String::from("D123")));
533
534 let message = "\
535This is a message
536
537Differential Revision: phabricator.com/D123";
538 assert_eq!(extract_diff_number(message), Some(String::from("D123")));
539
540 let message = "This is a message";
541 assert_eq!(extract_diff_number(message), None);
542
543 Ok(())
544 }
545
546 #[test]
547 fn test_describe_time_delta() -> eyre::Result<()> {
548 let test_cases: Vec<(isize, &str)> = vec![
549 (-100000, "-100000s"),
551 (-1, "-1s"),
552 (0, "0s"),
553 (10, "10s"),
554 (60, "1m"),
555 (90, "1m"),
556 (120, "2m"),
557 (135, "2m"),
558 (60 * 45, "45m"),
559 (60 * 60 - 1, "59m"),
560 (60 * 60, "1h"),
561 (60 * 60 * 24 * 3, "3d"),
562 (60 * 60 * 24 * 300, "300d"),
563 (60 * 60 * 24 * 400, "1y"),
564 ];
565
566 for (delta, expected) in test_cases {
567 let now = SystemTime::now();
568 let previous_time = if delta < 0 {
569 let delta = -delta;
570 now.add(Duration::from_secs(delta.try_into()?))
571 } else {
572 now.sub(Duration::from_secs(delta.try_into()?))
573 };
574 let delta = RelativeTimeDescriptor::describe_time_delta(now, previous_time)?;
575 assert_eq!(delta, expected);
576 }
577
578 Ok(())
579 }
580}