/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::cell::Cell;
use std::collections::BTreeSet;
use std::fs;
use std::io::Cursor;
use std::io::Write;
use std::path::Path;
use std::time::SystemTime;

use anyhow::Result;
use byteorder::BigEndian;
use byteorder::ReadBytesExt;
use byteorder::WriteBytesExt;
use indexedlog::log::IndexOutput;
use indexedlog::rotate::OpenOptions;
use indexedlog::rotate::RotateLog;
use indexedlog::rotate::RotateLowLevelExt;
use lazy_static::lazy_static;
use serde_json::Value;

use super::capture_pattern;
use super::json;
use super::match_pattern;
use crate::event::Event;

/// Local, rotated log consists of events tagged with "Invocation ID" and
/// timestamps.
pub struct Blackbox {
    // Used by singleton.
    pub(crate) log: RotateLog,
    opts: BlackboxOptions,

    // An ID that can be "grouped by" to figure everything about a session.
    pub(crate) session_id: u64,

    // The on-disk files are considered bad (ex. no permissions, or no disk space)
    // and further write attempts will be ignored.
    is_broken: Cell<bool>,
}

#[derive(Copy, Clone)]
pub struct BlackboxOptions {
    max_bytes_per_log: u64,
    max_log_count: u8,
}

/// A wrapper for some serializable data.
///
/// It adds two fields: `timestamp` and `session_id`.
#[derive(Debug)]
pub struct Entry {
    pub timestamp: u64,
    pub session_id: u64,
    pub data: Event,

    // Prevent constructing `Entry` directly.
    phantom: (),
}

/// Convert to JSON Value for pattern matching.
pub trait ToValue {
    fn to_value(&self) -> Value;
}

// The serialized format of `Entry` is:
//
// 8 Bytes: Milliseconds since epoch. Big-Endian.
// 4 Bytes: Session ID. Big-Endian.
// n Bytes: data.serialize() via serde-cbor.
//
// In case the format changes in the future, a simple strategy will be just
// renaming the directory used for logging.

const TIMESTAMP_BYTES: usize = 8;
const SESSION_ID_BYTES: usize = 8;
const HEADER_BYTES: usize = TIMESTAMP_BYTES + SESSION_ID_BYTES;

impl BlackboxOptions {
    /// Create a [`Blackbox`] instance at the given path using the specified options.
    pub fn open(self, path: impl AsRef<Path>) -> Result<Blackbox> {
        let path = path.as_ref();
        let opts = self.rotate_log_open_options();
        let log = match opts.clone().open(path) {
            Err(_) => {
                // Some error at opening (ex. metadata corruption).
                // As a simple recovery strategy, rmdir and retry.
                fs::remove_dir_all(path)?;
                opts.open(path)?
            }
            Ok(log) => log,
        };
        let blackbox = Blackbox {
            log,
            opts: self,
            // pid is used as an initial guess of "unique" session id
            session_id: new_session_id(),
            is_broken: Cell::new(false),
        };
        Ok(blackbox)
    }

    pub fn create_in_memory(self) -> Result<Blackbox> {
        let opts = self.rotate_log_open_options();
        let log = opts.create_in_memory()?;
        Ok(Blackbox {
            log,
            opts: self,
            // pid is used as an initial guess of "unique" session id
            session_id: new_session_id(),
            is_broken: Cell::new(false),
        })
    }

    pub fn new() -> Self {
        Self {
            max_bytes_per_log: 100_000_000,
            max_log_count: 3,
        }
    }

    pub fn max_bytes_per_log(mut self, bytes: u64) -> Self {
        self.max_bytes_per_log = bytes;
        self
    }

    pub fn max_log_count(mut self, count: u8) -> Self {
        self.max_log_count = count;
        self
    }

    fn rotate_log_open_options(&self) -> OpenOptions {
        OpenOptions::new()
            .max_bytes_per_log(self.max_bytes_per_log)
            .max_log_count(self.max_log_count)
            .auto_sync_threshold(1 << 21) // 20MB in-memory buffer
            .index("event", |bytes| {
                // Index on fields of `event`. This index includes fields from some dedicated
                // events. For example, timestamps of Start and Finish, etc.
                let mut result = Vec::new();
                let mut push = |head: u8, data: &[u8]| {
                    let mut bytes = vec![head];
                    bytes.write_all(data).expect("Vec::write should not fail");
                    result.push(IndexOutput::Owned(bytes.into_boxed_slice()));
                };
                if let Some(entry) = Entry::from_slice(bytes) {
                    match entry.data {
                        Event::Start {
                            timestamp_ms, pid, ..
                        } => {
                            push(INDEX_EVENT_START_TIME, &u64_to_slice(timestamp_ms)[..]);
                            push(INDEX_EVENT_START_PID, &u64_to_slice(pid as u64)[..]);
                        }
                        Event::Finish {
                            timestamp_ms,
                            duration_ms,
                            ..
                        } => {
                            push(INDEX_EVENT_FINISH_TIME, &u64_to_slice(timestamp_ms)[..]);
                            push(INDEX_EVENT_FINISH_DURATION, &u64_to_slice(duration_ms)[..]);
                        }
                        Event::Tags { names } => {
                            for name in names {
                                push(INDEX_EVENT_TAG_NAME, name.as_bytes());
                            }
                        }
                        _ => {}
                    }
                }
                result
            })
            .index("session_id", |_| {
                vec![IndexOutput::Reference(
                    TIMESTAMP_BYTES as u64..HEADER_BYTES as u64,
                )]
            })
            .create(true)
    }
}

const INDEX_EVENT_MISC: usize = 0;
const INDEX_SESSION_ID: usize = 1;

// Sub-index used by INDEX_EVENT_MISC.
const INDEX_EVENT_START_TIME: u8 = 0;
const INDEX_EVENT_START_PID: u8 = 1;
const INDEX_EVENT_FINISH_TIME: u8 = 2;
const INDEX_EVENT_FINISH_DURATION: u8 = 3;
const INDEX_EVENT_TAG_NAME: u8 = 4;

lazy_static! {
    static ref START_TIME_PATTERN: Value = json!(
        ["or",
         {"start": {"timestamp_ms": ["prefix", "range", ["capture", "START", "_"], ["capture", "END", "_"]]}},
         ["and",
          ["prefix", "and"],
          ["contain",
           {"start": {"timestamp_ms": ["prefix", "range", ["capture", "START", "_"], ["capture", "END", "_"]]}}]]]);
    static ref START_PID_PATTERN: Value = json!(
        ["or",
         {"start": {"pid": ["capture", "PID", "_"]}},
         ["and",
          ["prefix", "and"],
          ["contain",
           {"start": {"pid": ["capture", "PID", "_"]}}]]]);
    static ref FINISH_TIME_PATTERN: Value = json!(
        ["or",
         {"finish": {"timestamp_ms": ["prefix", "range", ["capture", "START", "_"], ["capture", "END", "_"]]}},
         ["and",
          ["prefix", "and"],
          ["contain",
           {"finish": {"timestamp_ms": ["prefix", "range", ["capture", "START", "_"], ["capture", "END", "_"]]}}]]]);
    static ref FINISH_DURATION_PATTERN: Value = json!(
        ["or",
         {"finish": {"duration_ms": ["prefix", "range", ["capture", "MIN", "_"], ["capture", "MAX", "_"]]}},
         ["and",
          ["prefix", "and"],
          ["contain",
           {"finish": {"duration_ms": ["prefix", "range", ["capture", "MIN", "_"], ["capture", "MAX", "_"]]}}]]]);
    static ref TAG_NAME_PATTERN: Value = json!(
        ["or",
         {"tags": {"names": ["prefix", "contain", ["capture", "NAME", "_"]]}},
         ["and",
          ["prefix", "and"],
          ["contain",
           {"tags": {"names": ["prefix", "contain", ["capture", "NAME", "_"]]}}]]]);
}

impl Blackbox {
    /// Assign a likely unused "Session ID".
    ///
    /// Events logged afterwards with be associated with this ID.
    ///
    /// Currently, uniqueness is not guaranteed, but perhaps "good enough".
    pub fn refresh_session_id(&mut self) {
        let session_id = new_session_id();
        if self.session_id >= session_id {
            self.session_id += 1 << 23;
        } else {
            self.session_id = session_id;
        }
    }

    /// Get the pid stored in session_id.
    pub(crate) fn session_pid(&self) -> u32 {
        (self.session_id & 0xffffff) as u32
    }

    pub fn session_id(&self) -> SessionId {
        SessionId(self.session_id)
    }

    /// Log an event. Maybe write it to disk immediately.
    ///
    /// If an error happens, `log` will try to rotate the bad logs and retry.
    /// If it still fails, `log` will simply give up.
    pub fn log(&mut self, data: &Event) {
        if self.is_broken.get() {
            return;
        }

        let now = time_to_u64(&SystemTime::now());
        if let Some(buf) = Entry::to_vec(data, now, self.session_id) {
            let _ = self.log.append(&buf);
        }
    }

    /// Write buffered data to disk.
    pub fn sync(&mut self) {
        if !self.is_broken.get() {
            // Ignore failures.
            let _ = self.log.sync();
        }
    }

    /// Filter blackbox by patterns.
    /// See `match_pattern.rs` for how to specify patterns.
    ///
    /// The pattern will match again the JSON form of an `Event`. For example,
    /// - Pattern `{"alias": {"from": "foo" }}` matches `Event::Alias { from, to }`
    ///   where `from` is `"foo"`.
    /// - Pattern `{"finish": {"duration_ms": ["range", 1000, 2000] }}` matches
    ///   `Event::Finish { duration_ms, ... }` where `duration_ms` is between
    ///   1000 and 2000.
    pub fn session_ids_by_pattern(&self, pattern: &Value) -> BTreeSet<SessionId> {
        let index: Option<(u8, _, _)> = capture_pattern(pattern, &START_TIME_PATTERN)
            .map(|captured| {
                let start = captured["START"].as_u64().unwrap_or(0);
                let end = captured["END"].as_u64().unwrap_or(0);
                (
                    INDEX_EVENT_START_TIME,
                    u64_to_boxed_slice(start),
                    u64_to_boxed_slice(start.max(end)),
                )
            })
            .or_else(|| {
                capture_pattern(pattern, &START_PID_PATTERN).map(|captured| {
                    let pid = captured["PID"].as_u64().unwrap_or(0);
                    (
                        INDEX_EVENT_START_PID,
                        u64_to_boxed_slice(pid),
                        u64_to_boxed_slice(pid),
                    )
                })
            })
            .or_else(|| {
                capture_pattern(pattern, &FINISH_TIME_PATTERN).map(|captured| {
                    let start = captured["START"].as_u64().unwrap_or(0);
                    let end = captured["END"].as_u64().unwrap_or(0);
                    (
                        INDEX_EVENT_FINISH_TIME,
                        u64_to_boxed_slice(start),
                        u64_to_boxed_slice(start.max(end)),
                    )
                })
            })
            .or_else(|| {
                capture_pattern(pattern, &FINISH_DURATION_PATTERN).map(|captured| {
                    let min = captured["MIN"].as_u64().unwrap_or(0);
                    let max = captured["MAX"].as_u64().unwrap_or(0);
                    (
                        INDEX_EVENT_FINISH_DURATION,
                        u64_to_boxed_slice(min),
                        u64_to_boxed_slice(max.max(min)),
                    )
                })
            })
            .or_else(|| {
                capture_pattern(pattern, &TAG_NAME_PATTERN).and_then(|captured| {
                    let name = captured["NAME"].as_str().unwrap_or("");
                    if name.is_empty() {
                        None
                    } else {
                        let bytes = name.as_bytes().to_vec().into_boxed_slice();
                        Some((INDEX_EVENT_TAG_NAME, bytes.clone(), bytes))
                    }
                })
            });

        let mut result = BTreeSet::new();
        match &index {
            Some((index_id, start, end)) => {
                // Use index to narrow down session_ids. Then search through the session_ids.
                // The real index key has the index_id has its header byte.
                let start: Vec<u8> = [&[*index_id][..], &start[..]].concat();
                let end: Vec<u8> = [&[*index_id][..], &end[..]].concat();
                let mut candidate_session_ids = Vec::new();
                for log in self.log.logs().iter() {
                    if let Ok(iter) = log.lookup_range(INDEX_EVENT_MISC, &start[..]..=&end[..]) {
                        for pair in iter {
                            if let Ok((_key, values)) = pair {
                                for value in values {
                                    if let Ok(bytes) = value {
                                        if let Some(session_id) =
                                            Entry::session_id_from_slice(bytes)
                                        {
                                            candidate_session_ids.push(session_id)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                'next_session_id: for session_id in candidate_session_ids {
                    if let Ok(iter) = self
                        .log
                        .lookup(INDEX_SESSION_ID, u64_to_slice(session_id.0).to_vec())
                    {
                        for bytes in iter {
                            if let Ok(bytes) = bytes {
                                if let Some(entry) = Entry::from_slice(bytes) {
                                    if entry.match_pattern(pattern) {
                                        result.insert(session_id);
                                        continue 'next_session_id;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            None => {
                // Cannot use index. Go through every entry.
                for next in self.log.iter() {
                    if let Ok(bytes) = next {
                        let session_id = match Entry::session_id_from_slice(bytes) {
                            Some(id) => id,
                            None => continue,
                        };
                        if result.contains(&session_id) {
                            // The session_id is already included in the result set.
                            // Skip deserializing it.
                            continue;
                        }
                        if let Some(entry) = Entry::from_slice(bytes) {
                            if entry.match_pattern(pattern) {
                                result.insert(session_id);
                            }
                        }
                    }
                }
            }
        }
        result
    }

    /// Get all [`Entry`]s with specified `session_id`s.
    ///
    /// This function is usually used together with `session_ids_by_pattern`.
    ///
    /// Entries that cannot be read or deserialized are ignored silently.
    pub fn entries_by_session_ids(
        &self,
        session_ids: impl IntoIterator<Item = SessionId>,
    ) -> Vec<Entry> {
        let mut result = Vec::new();
        for session_id in session_ids {
            if let Ok(iter) = self
                .log
                .lookup(INDEX_SESSION_ID, u64_to_slice(session_id.0).to_vec())
            {
                for bytes in iter {
                    if let Ok(bytes) = bytes {
                        if let Some(entry) = Entry::from_slice(bytes) {
                            result.push(entry)
                        }
                    }
                }
            }
        }
        result.reverse();
        result
    }

    pub fn entries_by_session_id(&self, session_id: SessionId) -> Vec<Entry> {
        self.entries_by_session_ids(vec![session_id])
    }
}

/// Session Id used in public APIs.
#[derive(Copy, Clone, Ord, Eq, PartialOrd, PartialEq, Debug)]
pub struct SessionId(pub u64);

impl Drop for Blackbox {
    fn drop(&mut self) {
        self.sync();
    }
}

impl Entry {
    /// Test if `Entry` matches a specific pattern or not.
    pub fn match_pattern(&self, pattern: &Value) -> bool {
        match_pattern(&self.data.to_value(), pattern)
    }

    /// Partially decode `bytes` into session_id and timestamp.
    fn session_id_from_slice(bytes: &[u8]) -> Option<SessionId> {
        if bytes.len() >= HEADER_BYTES {
            let mut cur = Cursor::new(bytes);
            let _timestamp = cur.read_u64::<BigEndian>().unwrap();
            let session_id = cur.read_u64::<BigEndian>().unwrap();
            Some(SessionId(session_id))
        } else {
            None
        }
    }

    fn from_slice(bytes: &[u8]) -> Option<Self> {
        if bytes.len() >= HEADER_BYTES {
            let mut cur = Cursor::new(bytes);
            let timestamp = cur.read_u64::<BigEndian>().unwrap();
            let session_id = cur.read_u64::<BigEndian>().unwrap();
            let pos = cur.position();
            let bytes = cur.into_inner();
            let bytes = &bytes[pos as usize..];
            if let Ok(data) = serde_cbor::from_slice(bytes) {
                let entry = Entry {
                    timestamp,
                    session_id,
                    data,
                    phantom: (),
                };
                return Some(entry);
            }
        }
        None
    }
}

impl Entry {
    fn to_vec(data: &Event, timestamp: u64, session_id: u64) -> Option<Vec<u8>> {
        let mut buf = Vec::with_capacity(32);
        buf.write_u64::<BigEndian>(timestamp).unwrap();
        buf.write_u64::<BigEndian>(session_id).unwrap();

        if serde_cbor::to_writer(&mut buf, data).is_ok() {
            Some(buf)
        } else {
            None
        }
    }
}

fn u64_to_slice(value: u64) -> [u8; 8] {
    // The field can be used for index range query. So it has to be BE.
    unsafe { std::mem::transmute(value.to_be()) }
}

fn u64_to_boxed_slice(value: u64) -> Box<[u8]> {
    (&u64_to_slice(value)[..]).to_vec().into_boxed_slice()
}

fn time_to_u64(time: &SystemTime) -> u64 {
    time.duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64
}

// The session_id is intended to be:
// 1. Somehow unique among multiple machines for at least 3 months
//    (for analysis over time).
// 2. Related to timestamp. So Scuba might be able to delta-compress them.
//
// At the time of writing, millisecond percision seems already enough to
// distinguish sessions across machines. To make it more "future proof", take
// some bits from the pid.
//
// At the time of writing, /proc/sys/kernel/pid_max shows pid can fit in 3
// bytes.
fn new_session_id() -> u64 {
    // 40 bits from millisecond timestamp. That's 34 years.
    // 24 bits from pid.
    ((time_to_u64(&SystemTime::now()) & 0xffffffffff) << 24)
        | ((unsafe { libc::getpid() } as u64) & 0xffffff)
}

#[cfg(test)]
pub(crate) mod tests {
    use serde_json::json;
    use tempfile::tempdir;

    use super::*;

    #[test]
    fn test_query_by_session_ids() {
        let dir = tempdir().unwrap();
        let mut blackbox = BlackboxOptions::new().open(&dir.path()).unwrap();

        let events = [
            Event::Alias {
                from: "a".to_string(),
                to: "b".to_string(),
            },
            Event::Debug {
                value: json!([1, 2, 3]),
            },
            Event::Alias {
                from: "x".to_string(),
                to: "y".to_string(),
            },
            Event::Debug {
                value: json!("foo"),
            },
            Event::Debug {
                value: json!({"p": "q"}),
            },
        ];

        // Write some events.
        // Session 0
        let mut session_ids = Vec::new();
        blackbox.log(&events[0]);
        blackbox.log(&events[1]);
        session_ids.push(blackbox.session_id());

        // Session 1
        blackbox.refresh_session_id();
        blackbox.log(&events[2]);
        blackbox.log(&events[3]);
        session_ids.push(blackbox.session_id());

        // Session 2
        blackbox.refresh_session_id();
        blackbox.log(&events[4]);
        session_ids.push(blackbox.session_id());

        let query = |pattern: serde_json::Value| -> Vec<usize> {
            let ids = blackbox.session_ids_by_pattern(&pattern);
            session_ids
                .iter()
                .enumerate()
                .filter(|(_i, session_id)| ids.contains(session_id))
                .map(|(i, _)| i)
                .collect()
        };

        // Query session_ids by patterns.
        // Only Session 0 and 1 have Event::Alias.
        assert_eq!(query(json!({"alias": "_"})), [0, 1]);
        // Only Session 1 has Event::Alias with from = "x".
        assert_eq!(query(json!({"alias": {"from": "x"}})), [1]);
        // All sessions have Event::Debug.
        assert_eq!(query(json!({"debug": "_"})), [0, 1, 2]);
        // Session 0 has Event::Debug with 2 in its "value" array.
        assert_eq!(query(json!({"debug": {"value": ["contain", 2]}})), [0]);
        // Session 2 has Event::Debug with the "p" key in its "value" object.
        assert_eq!(query(json!({"debug": {"value": {"p": "_"}}})), [2]);

        // Query Events by session_ids.
        let query = |i: usize| -> Vec<Event> {
            blackbox
                .entries_by_session_id(session_ids[i])
                .into_iter()
                .map(|e| e.data)
                .collect()
        };
        assert_eq!(query(0), &events[0..2]);
        assert_eq!(query(1), &events[2..4]);
        assert_eq!(query(2), &events[4..5]);
    }

    pub(crate) fn all_entries(blackbox: &Blackbox) -> Vec<Entry> {
        let session_ids = blackbox.session_ids_by_pattern(&json!("_"));
        session_ids
            .into_iter()
            .flat_map(|id| blackbox.entries_by_session_id(id))
            .collect()
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Events used in blackbox.
//!
//! This is specified to the host application (source control) use-case. Other
//! applications might want to define different events.
//!
//! This module assumes that all events are known here. There are no external
//! types of events that are outside this module.

use std::collections::BTreeMap;
use std::fmt;

use anyhow::Result;
use serde_alt::serde_alt;
use serde_derive::Deserialize;
use serde_derive::Serialize;
use serde_json::Value;

use super::ToValue;

// Most serde attributes are used extensively to reduce the space usage.
//
// The 'alias' attribute is used for converting from JSON, as an easy way to
// construct the native Event type from a JSON coming from the Python land.

/// All possible [`Event`]s for the (source control) application.
///
/// Changing this `enum` and its dependencies needs to be careful to avoid
/// breaking the ability to read old data. Namely:
///
/// - Use (short) `serde rename` and (long) `serde alias` everywhere.
///   Once a `rename` was assigned, do not change its value.
/// - When adding new fields to an event type, consider `serde default` to
///   make it compatible with old data.
/// - Always use enum struct form `Event::TypeName { a: .., b: .. }`,
///   instead of enum tuple form `Event::TypeName(a, b)`.
#[serde_alt]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum Event {
    /// Resolved alias.
    #[serde(rename = "A", alias = "alias")]
    Alias {
        #[serde(rename = "F", alias = "from")]
        from: String,

        #[serde(rename = "T", alias = "to")]
        to: String,
    },

    /// Waiting for other operations (ex. editor).
    ///
    /// Not including watchman commands or network operations.
    /// They have dedicated event types.
    #[serde(rename = "B", alias = "blocked")]
    Blocked {
        #[serde(rename = "O", alias = "op")]
        op: BlockedOp,

        #[serde(
            rename = "N",
            alias = "name",
            default,
            skip_serializing_if = "is_default"
        )]
        name: Option<String>,

        #[serde(rename = "D", alias = "duration_ms")]
        duration_ms: u64,
    },

    /// Commit Cloud Sync
    #[serde(rename = "CCS", alias = "commit_cloud_sync")]
    CommitCloudSync {
        #[serde(rename = "O", alias = "op")]
        op: CommitCloudSyncOp,

        #[serde(rename = "PV", alias = "prev_version")]
        prev_version: u64,

        #[serde(rename = "V", alias = "version")]
        version: u64,

        #[serde(rename = "AH", alias = "added_heads")]
        added_heads: ShortList,

        #[serde(rename = "RH", alias = "removed_heads")]
        removed_heads: ShortList,

        #[serde(rename = "AB", alias = "added_bookmarks")]
        added_bookmarks: ShortList,

        #[serde(rename = "RB", alias = "removed_bookmarks")]
        removed_bookmarks: ShortList,

        #[serde(rename = "ARB", alias = "added_remote_bookmarks")]
        added_remote_bookmarks: ShortList,

        #[serde(rename = "RRB", alias = "removed_remote_bookmarks")]
        removed_remote_bookmarks: ShortList,
    },

    /// A subset of interesting configs.
    #[serde(rename = "C", alias = "config")]
    Config {
        #[serde(rename = "I", alias = "interactive")]
        interactive: bool,

        #[serde(rename = "M", alias = "items")]
        items: BTreeMap<String, String>,
    },

    /// Client Telemetry Data
    #[serde(rename = "CT", alias = "clienttelemetry")]
    ClientTelemetry {
        #[serde(rename = "P", alias = "peername")]
        peer_name: String,

        #[serde(rename = "I", alias = "peerinfo")]
        peer_info: BTreeMap<String, String>,
    },

    /// Free-form debug message.
    #[serde(rename = "D", alias = "debug")]
    Debug {
        #[serde(rename = "V", alias = "value")]
        value: Value,
    },

    /// An EdenApi HTTP request was dispatched.
    #[serde(rename = "EA", alias = "edenapi")]
    EdenApi {
        #[serde(rename = "U")]
        url: Option<String>,
        #[serde(rename = "T")]
        status: u32,
        #[serde(rename = "I")]
        session_id: Option<String>,
        #[serde(rename = "DD")]
        downloaded: f64,
        #[serde(rename = "UD")]
        uploaded: f64,
    },

    #[serde(rename = "E", alias = "exception")]
    Exception {
        #[serde(rename = "M", alias = "msg")]
        msg: String,
    },

    /// Information collected at the end of the process.
    #[serde(rename = "F", alias = "finish")]
    Finish {
        #[serde(rename = "E", alias = "exit_code")]
        exit_code: u8,

        #[serde(rename = "R", alias = "max_rss")]
        max_rss: u64,

        #[serde(rename = "D", alias = "duration_ms")]
        duration_ms: u64,

        /// Timestamp in milliseconds.
        #[serde(rename = "T", alias = "timestamp_ms")]
        timestamp_ms: u64,
    },

    /// A watchman query with related context.
    #[serde(rename = "FQ", alias = "fsmonitor")]
    FsmonitorQuery {
        #[serde(
            rename = "1",
            alias = "old_clock",
            default,
            skip_serializing_if = "is_default"
        )]
        old_clock: String,

        #[serde(
            rename = "2",
            alias = "old_files",
            default,
            skip_serializing_if = "is_default"
        )]
        old_files: ShortList,

        #[serde(
            rename = "3",
            alias = "new_clock",
            default,
            skip_serializing_if = "is_default"
        )]
        new_clock: String,

        #[serde(
            rename = "4",
            alias = "new_files",
            default,
            skip_serializing_if = "is_default"
        )]
        new_files: ShortList,

        #[serde(
            rename = "F",
            alias = "is_fresh",
            default,
            skip_serializing_if = "is_default"
        )]
        is_fresh: bool,

        #[serde(
            rename = "E",
            alias = "is_error",
            default,
            skip_serializing_if = "is_default"
        )]
        is_error: bool,
    },

    /// Legacy blackbox message for compatibility.
    #[serde(rename = "L", alias = "legacy_log")]
    LegacyLog {
        // Matches `ui.log(service, *msg, **opts)` API.
        #[serde(rename = "S", alias = "service")]
        service: String,

        #[serde(
            rename = "M",
            alias = "msg",
            default,
            skip_serializing_if = "is_default"
        )]
        msg: String,

        #[serde(
            rename = "O",
            alias = "opts",
            default,
            skip_serializing_if = "is_default"
        )]
        opts: Value,
    },

    /// A single network operation.
    #[serde(rename = "N", alias = "network")]
    Network {
        #[serde(rename = "O", alias = "op")]
        op: NetworkOp,

        #[serde(
            rename = "R",
            alias = "read_bytes",
            default,
            skip_serializing_if = "is_default"
        )]
        read_bytes: u64,

        #[serde(
            rename = "W",
            alias = "write_bytes",
            default,
            skip_serializing_if = "is_default"
        )]
        write_bytes: u64,

        #[serde(
            rename = "C",
            alias = "calls",
            default,
            skip_serializing_if = "is_default"
        )]
        calls: u64,

        #[serde(
            rename = "D",
            alias = "duration_ms",
            default,
            skip_serializing_if = "is_default"
        )]
        duration_ms: u64,

        #[serde(
            rename = "L",
            alias = "latency_ms",
            default,
            skip_serializing_if = "is_default"
        )]
        latency_ms: u64,

        /// Optional free-form extra metadata about the result.
        #[serde(
            rename = "RR",
            alias = "result",
            default,
            skip_serializing_if = "is_default"
        )]
        result: Option<Value>,

        #[serde(rename = "U", default, skip_serializing_if = "is_default")]
        url: String,

        #[serde(rename = "I", default, skip_serializing_if = "is_default")]
        session_id: String,
    },

    #[serde(rename = "PE", alias = "perftrace")]
    PerfTrace {
        #[serde(rename = "M", alias = "msg")]
        msg: String,
    },

    /// Process tree.
    ///
    /// When collecting this information, the parent processes might exit.
    /// So it's a best effort.
    #[serde(rename = "PR", alias = "process_tree")]
    ProcessTree {
        #[serde(rename = "N", alias = "names")]
        names: Vec<String>,

        #[serde(rename = "P", alias = "pids")]
        pids: Vec<u32>,
    },

    #[serde(rename = "P", alias = "profile")]
    Profile {
        #[serde(rename = "M", alias = "msg")]
        msg: String,
    },

    /// Repo initialization with basic information attached.
    #[serde(rename = "R", alias = "repo")]
    Repo {
        #[serde(rename = "P", alias = "path")]
        path: String,

        #[serde(rename = "N", alias = "name")]
        name: String,
    },

    /// Immutable process environment.
    #[serde(rename = "S", alias = "start")]
    Start {
        #[serde(
            rename = "P",
            alias = "pid",
            default,
            skip_serializing_if = "is_default"
        )]
        pid: u32,

        #[serde(
            rename = "U",
            alias = "uid",
            default,
            skip_serializing_if = "is_default"
        )]
        uid: u32,

        #[serde(
            rename = "N",
            alias = "nice",
            default,
            skip_serializing_if = "is_default"
        )]
        nice: i32,

        #[serde(rename = "A", alias = "args")]
        args: Vec<String>,

        /// Timestamp in milliseconds.
        #[serde(rename = "T", alias = "timestamp_ms")]
        timestamp_ms: u64,
    },

    /// Tag the session with some names.
    #[serde(rename = "T", alias = "tags")]
    Tags {
        #[serde(rename = "N", alias = "names")]
        names: Vec<String>,
    },

    #[serde(rename = "TD", alias = "tracing_data")]
    TracingData {
        #[serde(rename = "S", alias = "serialized")]
        serialized: Binary,
    },

    /// A watchman command has finished.
    #[serde(rename = "W", alias = "watchman")]
    Watchman {
        #[serde(rename = "A", alias = "args")]
        args: Value,

        #[serde(rename = "D", alias = "duration_ms")]
        duration_ms: u64,

        /// Optional free-form extra metadata about the result.
        #[serde(
            rename = "R",
            alias = "result",
            default,
            skip_serializing_if = "is_default"
        )]
        result: Option<Value>,
    },
}

/// A simple wrapper to (potentially long) `Vec<u8>` that has a simple `Debug` implementation.
#[serde_alt]
#[derive(Serialize, Deserialize, Default, PartialEq)]
pub struct Binary(pub Vec<u8>);

/// A truncated (file) list.
#[serde_alt]
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
pub struct ShortList {
    #[serde(
        rename = "S",
        alias = "short_list",
        default,
        skip_serializing_if = "is_default"
    )]
    short_list: Vec<String>,

    #[serde(
        rename = "L",
        alias = "len",
        default,
        skip_serializing_if = "is_default"
    )]
    len: usize,
}

#[serde_alt]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum NetworkOp {
    #[serde(rename = "t", alias = "ssh_gettreepack")]
    SshGetTreePack,

    #[serde(rename = "f", alias = "ssh_getfiles")]
    SshGetFiles,

    #[serde(rename = "p", alias = "ssh_getpack")]
    SshGetPack,

    #[serde(rename = "T", alias = "http_gettreepack")]
    HttpGetTreePack,

    #[serde(rename = "F", alias = "http_getfiles")]
    HttpGetFiles,

    #[serde(rename = "P", alias = "http_getpack")]
    HttpGetPack,

    #[serde(rename = "E", alias = "edenapi")]
    EdenApiRequest,
}

#[serde_alt]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum BlockedOp {
    #[serde(rename = "E", alias = "editor")]
    Editor,

    #[serde(rename = "P", alias = "pager")]
    Pager,

    #[serde(rename = "D", alias = "extdiff")]
    ExtDiff,

    #[serde(rename = "H", alias = "exthook")]
    ExtHook,

    // Note: MergeDriver belongs to PythonHook.
    #[serde(rename = "h", alias = "pythonhook")]
    PythonHook,

    #[serde(rename = "B", alias = "bisect_check")]
    BisectCheck,

    #[serde(rename = "X", alias = "histedit_exec")]
    HisteditExec,

    #[serde(rename = "C", alias = "curses")]
    Curses,

    #[serde(rename = "PR", alias = "prompt")]
    Prompt,

    // Do not reuse "M" or "mergedriver".
    // #[serde(rename = "M", alias = "mergedriver")]
    // MergeDriver,
    #[serde(rename = "m", alias = "mergetool")]
    MergeTool,
}

#[serde_alt]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum CommitCloudSyncOp {
    #[serde(rename = "F", alias = "from_cloud")]
    FromCloud,

    #[serde(rename = "T", alias = "to_cloud")]
    ToCloud,
}

fn is_default<T: PartialEq + Default>(value: &T) -> bool {
    value == &Default::default()
}

fn json_to_string(value: &Value) -> String {
    serde_json::to_string(value).unwrap_or_else(|_| "<cannot decode>".to_string())
}

impl Event {
    pub fn from_json(json: &str) -> Result<Self> {
        Ok(serde_json::from_str(json)?)
    }
}

impl ToValue for Event {
    /// Convert to human-friendly [`serde_json::Value`].
    fn to_value(&self) -> Value {
        // This value is using the "rename" field, suitable for storage, but
        // bad for human consumption.
        let value = serde_json::to_value(self).unwrap();

        // Round-trip through `EventAlt` (generated by serde_alt) to get the
        // human-friendly version.
        let event_alt: EventAlt = serde_json::from_value(value).unwrap();
        serde_json::to_value(&event_alt).unwrap()
    }
}

#[serde_alt]
impl fmt::Debug for Binary {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "<binary data ({} bytes)>", self.0.len())
    }
}

impl fmt::Display for ShortList {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.short_list.len() >= self.len {
            write!(f, "{:?}", self.short_list)?;
        } else {
            let remaining = self.len - self.short_list.len();
            write!(f, "{:?} and {} entries", self.short_list, remaining)?;
        }
        Ok(())
    }
}

impl fmt::Display for Event {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use Event::*;
        match self {
            Alias { from, to } => write!(f, "[command_alias] {:?} expands to {:?}", from, to)?,
            Blocked {
                op,
                name,
                duration_ms,
            } => match name {
                Some(name) => write!(
                    f,
                    "[blocked] {:?} ({}) blocked for {} ms",
                    op, name, duration_ms
                )?,
                None => write!(f, "[blocked] {:?} blocked for {} ms", op, duration_ms)?,
            },
            CommitCloudSync {
                op,
                prev_version,
                version,
                added_heads,
                removed_heads,
                added_bookmarks,
                removed_bookmarks,
                added_remote_bookmarks,
                removed_remote_bookmarks,
            } => {
                let direction = match op {
                    CommitCloudSyncOp::ToCloud => "to",
                    CommitCloudSyncOp::FromCloud => "from",
                };
                write!(
                    f,
                    "[commit_cloud_sync] sync {} cloud version {}, previous version was {}",
                    direction, version, prev_version
                )?;
                if added_heads.len > 0 {
                    write!(f, "; heads added {}", added_heads)?;
                }
                if removed_heads.len > 0 {
                    write!(f, "; heads removed {}", removed_heads)?;
                }
                if added_bookmarks.len > 0 {
                    write!(f, "; bookmarks added {}", added_bookmarks)?;
                }
                if removed_bookmarks.len > 0 {
                    write!(f, "; bookmarks removed {}", removed_bookmarks)?;
                }
                if added_remote_bookmarks.len > 0 {
                    write!(f, "; remote bookmarks added {}", added_remote_bookmarks)?;
                }
                if removed_remote_bookmarks.len > 0 {
                    write!(f, "; remote bookmarks removed {}", removed_remote_bookmarks)?;
                }
            }
            Config { items, interactive } => {
                let interactive = if *interactive {
                    "interactive"
                } else {
                    "non-interactive"
                };
                write!(
                    f,
                    "[config] {} {}",
                    interactive,
                    items
                        .iter()
                        .map(|(k, v)| format!("{}={}", k, v))
                        .collect::<Vec<_>>()
                        .join(" ")
                )?;
            }
            ClientTelemetry {
                peer_name,
                peer_info,
            } => write!(
                f,
                "[clienttelemetry] peer name: {} {}",
                peer_name,
                peer_info
                    .iter()
                    .map(|(k, v)| format!("{}={}", k, v))
                    .collect::<Vec<_>>()
                    .join(" ")
            )?,
            Debug { value } => write!(f, "[debug] {}", json_to_string(value))?,
            Exception { msg } => write!(f, "[command_exception] {}", msg)?,
            Finish {
                exit_code,
                max_rss,
                duration_ms,
                timestamp_ms: _,
            } => {
                write!(
                    f,
                    "[commmand_finish] exited {} in {} ms, max RSS: {} bytes",
                    exit_code, duration_ms, max_rss
                )?;
            }
            FsmonitorQuery {
                old_clock,
                old_files,
                new_clock,
                new_files,
                is_error,
                is_fresh,
            } => {
                let msg = if *is_error {
                    "query failed".to_string()
                } else if *is_fresh {
                    format!(
                        "clock: {:?} -> {:?}; need check: {} + all files",
                        old_clock, new_clock, old_files
                    )
                } else {
                    format!(
                        "clock: {:?} -> {:?}; need check: {} + {}",
                        old_clock, new_clock, old_files, new_files
                    )
                };
                write!(f, "[fsmonitor] {}", msg)?;
            }
            LegacyLog {
                service,
                msg,
                opts: _,
            } => {
                write!(f, "[legacy][{}] {}", service, msg,)?;
            }
            Network {
                op,
                read_bytes,
                write_bytes,
                calls,
                duration_ms,
                latency_ms,
                session_id,
                url,
                result,
            } => {
                let result = match result {
                    Some(result) => format!(" with result {}", json_to_string(result)),
                    None => "".to_string(),
                };
                write!(
                    f,
                    "[network] {:?} finished in {} calls, duration {} ms, latency {} ms, read {} bytes, write {} bytes, session id {}, url {}{}",
                    op,
                    calls,
                    duration_ms,
                    latency_ms,
                    read_bytes,
                    write_bytes,
                    session_id,
                    url,
                    result,
                )?;
            }
            Start {
                pid,
                uid,
                nice,
                args,
                timestamp_ms: _,
            } => {
                write!(
                    f,
                    "[command] {:?} started by uid {} as pid {} with nice {}",
                    args, uid, pid, nice
                )?;
            }
            PerfTrace { msg } => write!(f, "[perftrace] {}", msg)?,
            ProcessTree { names, pids } => {
                write!(f, "[process_tree]")?;
                for (name, pid) in names.iter().rev().zip(pids.iter().rev()) {
                    write!(f, " {} ({}) ->", name, pid)?;
                }
                write!(f, " (this process)")?;
            }
            Profile { msg } => write!(f, "[profile] {}", msg)?,
            Tags { names } => write!(f, "[tags] {}", names.join(", "))?,
            TracingData { serialized } => {
                write!(f, "[tracing] (binary data of {} bytes)", serialized.0.len())?
            }
            Watchman {
                args,
                duration_ms,
                result,
            } => {
                let result = match result {
                    Some(result) => format!(" with result {}", json_to_string(result)),
                    None => "".to_string(),
                };
                write!(
                    f,
                    "[watchman] command {} finished in {} ms{}",
                    json_to_string(args),
                    duration_ms,
                    result,
                )?;
            }
            _ => {
                // Fallback to "Debug"
                write!(f, "[uncategorized] {:?}", self)?;
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_json_to_string() {
        // Construct Event from plain JSON, then convert it to String (Display).
        // Does not cover every type. But some interesting ones.

        assert_eq!(
            f(r#"{"alias":{"from":"a","to":"b"}}"#),
            "[command_alias] \"a\" expands to \"b\""
        );

        assert_eq!(
            f(r#"{"blocked":{"op":"editor","duration_ms":3000}}"#),
            "[blocked] Editor blocked for 3000 ms"
        );

        assert_eq!(
            f(r#"{"blocked":{"op":"pythonhook","name":"foo","duration_ms":50}}"#),
            "[blocked] PythonHook (foo) blocked for 50 ms"
        );

        assert_eq!(
            f(r#"{"config":{"interactive":false,"items":{"a.b":"1","a.c":"2"}}}"#),
            "[config] non-interactive a.b=1 a.c=2"
        );

        assert_eq!(
            f(r#"{"debug":{"value":["debug","msg"]}}"#),
            "[debug] [\"debug\",\"msg\"]"
        );

        assert_eq!(
            f(r#"{"legacy_log":{"service":"fsmonitor","msg":"command completed"}}"#),
            "[legacy][fsmonitor] command completed"
        );

        assert_eq!(
            f(r#"{"process_tree":{"names":["systemd","bash","node"],"pids":[1,2,3]}}"#),
            "[process_tree] node (3) -> bash (2) -> systemd (1) -> (this process)"
        );

        assert_eq!(
            f(r#"{"watchman":{"args":["state-enter","update",{"rev":"abcd"}],"duration_ms":42}}"#),
            "[watchman] command [\"state-enter\",\"update\",{\"rev\":\"abcd\"}] finished in 42 ms"
        );
    }

    #[test]
    fn test_to_value() {
        assert_eq!(
            v(r#"{"alias":{"from":"a","to":"b"}}"#),
            "{\"alias\":{\"from\":\"a\",\"to\":\"b\"}}"
        );

        assert_eq!(
            v(r#"{"network":{"op":"http_getfiles","calls":3, "result": 123, "read_bytes": 456}}"#),
            "{\"network\":{\"calls\":3,\"op\":\"http_getfiles\",\"read_bytes\":456,\"result\":123}}"
        );
    }

    /// Convenient way to convert from a JSON string to human-readable message.
    fn f(s: &str) -> String {
        format!("{}", Event::from_json(s).unwrap())
    }

    /// Convenient way to convert from a JSON string to Event, then to JSON string.
    fn v(s: &str) -> String {
        json_to_string(&Event::from_json(s).unwrap().to_value())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Logging support
//!
//! - The `blackbox` module provides building blocks for logging serializable
//!   objects, searching them by time range, etc. It is application agnostic.
//! - The `event` module assumes the source control application. It is not
//!   designed to be general purpose.

// This is a library exporting functions that seem "dead" if compiled alone.
#![allow(dead_code)]

mod blackbox;
mod match_pattern;
mod singleton;

pub use match_pattern::capture_pattern;
pub use match_pattern::match_pattern;
pub use serde_json;
pub use serde_json::json;
pub use serde_json::Value;

pub use self::blackbox::Blackbox;
pub use self::blackbox::BlackboxOptions;
pub use self::blackbox::Entry;
pub use self::blackbox::SessionId;
pub use self::blackbox::ToValue;
pub use self::singleton::init;
pub use self::singleton::log;
pub use self::singleton::reset;
pub use self::singleton::sync;
pub use self::singleton::SINGLETON;

pub mod event;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Pattern matching for JSON value.

use std::collections::HashMap;

use serde_json::Value;

/// Test if a JSON value matches the given pattern.
///
/// Examples:
///
/// ```
/// use blackbox::match_pattern;
/// use serde_json::json;
///
/// // Value matches if they are equal. No magic convertion between types.
/// assert!(match_pattern(&json!(true), &json!(true)));
/// assert!(match_pattern(&json!(42), &json!(42)));
/// assert!(!match_pattern(&json!(true), &json!("true")));
/// assert!(!match_pattern(&json!("true"), &json!(true)));
///
/// // Object matches if the pattern is a subset of the target object.
/// assert!(match_pattern(&json!({"a": 1, "b": 2}), &json!({"b": 2})));
/// assert!(!match_pattern(&json!({"a": 1}), &json!({"b": 2, "a": 1})));
///
/// // "_" matches anything.
/// assert!(match_pattern(&json!({"a": ["b"]}), &json!({"a": "_"})));
///
/// // Array is handled specially. Its first element is always treated
/// // as an operator.
///
/// // ["or", ...]: matches if any item in the rest of the array matches.
/// assert!(match_pattern(&json!(42), &json!(["or", 1, 42, 98])));
///
/// // ["not", pattern]: negates a match.
/// assert!(match_pattern(&json!(42), &json!(["not", 43])));
///
/// // ["and", ...]: matches if every item match.
/// assert!(match_pattern(
///     &json!({"a": 1, "b": 2}),
///     &json!(["and", {"a": 1}, ["not", {"b": 3}]])));
///
/// // ["range", start, end]: matches a number x, if start <= x <= end.
/// assert!(match_pattern(&json!(42), &json!(["range", 1, 100])));
///
/// // ["prefix", ...]: matches an array if the prefix matches.
/// assert!(match_pattern(&json!(["a", "b", "c", "d"]), &json!(["prefix", "a", "_", "c"])));
///
/// // ["contain", pattern]: matches an array if one item matches.
/// assert!(match_pattern(&json!([1, 10, 50, 100]), &json!(["contain", 50])));
/// ```
pub fn match_pattern(value: &Value, pattern: &Value) -> bool {
    let mut capture = Default::default();
    match_pattern_captured(value, pattern, &mut capture)
}

/// Similar to `match_pattern`, but also support capturing matches into `capture`.
/// To capture a value, use `["capture", name, pattern]`. The captured value will
/// be stored in the returned [`HashMap`].
///
/// Examples:
///
/// ```
/// use blackbox::capture_pattern;
/// use serde_json::json;
///
/// // Capture nested objects.
/// assert_eq!(capture_pattern(
///     &json!({"a":{"b": 3}}),
///     &json!({"a":{"b":["capture", "B", "_"]}})).unwrap()["B"],
///     &json!(3));
///
/// // Capture with conditions.
/// assert_eq!(capture_pattern(
///     &json!(50),
///     &json!(["capture", "INT", ["range", 0, 100]])).unwrap()["INT"],
///     &json!(50));
///
/// // Logical expression.
/// let obj = json!(["a", "b"]);
/// let pat = json!(["or", ["capture", "INT", ["range", 0, 100]],
///                        ["capture", "LIST", ["contain", "b"]]]);
/// let captured = capture_pattern(&obj, &pat).unwrap();
/// assert_eq!(captured["LIST"], &json!(["a", "b"]));
/// assert!(!captured.contains_key("INT"));
///
/// // Not matched.
/// assert!(capture_pattern(&json!("c"), &pat).is_none());
/// ```
pub fn capture_pattern<'a, 'b>(value: &'a Value, pattern: &'b Value) -> Option<Capture<'b, 'a>> {
    let mut capture = Default::default();
    if match_pattern_captured(value, pattern, &mut capture) {
        Some(capture)
    } else {
        None
    }
}

fn match_pattern_captured<'a, 'b>(
    value: &'a Value,
    pattern: &'b Value,
    capture: &mut Capture<'b, 'a>,
) -> bool {
    use Value::*;

    match pattern {
        // Concrete value.
        Null | Bool(_) | Number(_) => value == pattern,

        // "_" matches anything.
        String(s) => s == "_" || value == pattern,

        // Treat array as meaningful expressions.
        Array(v) => {
            if let Some(String(op_name)) = v.get(0) {
                match op_name.as_ref() {
                    "or" => v[1..]
                        .iter()
                        .any(|pat| match_pattern_captured(value, pat, capture)),
                    "and" => v[1..]
                        .iter()
                        .all(|pat| match_pattern_captured(value, pat, capture)),
                    "not" if v.len() == 2 => !match_pattern_captured(value, &v[1], capture),
                    "range" if v.len() == 3 => {
                        if let (Number(start), Number(end), Number(value)) = (&v[1], &v[2], value) {
                            // Unfortunately, Number does not implement PartialOrd.
                            if let (Some(start), Some(end), Some(value)) =
                                (start.as_u64(), end.as_u64(), value.as_u64())
                            {
                                start <= value && value <= end
                            } else if let (Some(start), Some(end), Some(value)) =
                                (start.as_i64(), end.as_i64(), value.as_i64())
                            {
                                start <= value && value <= end
                            } else if let (Some(start), Some(end), Some(value)) =
                                (start.as_f64(), end.as_f64(), value.as_f64())
                            {
                                start <= value && value <= end
                            } else {
                                false
                            }
                        } else {
                            false
                        }
                    }
                    "prefix" => {
                        if let Array(value) = value {
                            v[1..]
                                .iter()
                                .enumerate()
                                .all(|(i, pat)| match_pattern_captured(&value[i], pat, capture))
                        } else {
                            false
                        }
                    }
                    "contain" if v.len() == 2 => {
                        if let Array(value) = value {
                            value
                                .iter()
                                .any(|value| match_pattern_captured(value, &v[1], capture))
                        } else {
                            false
                        }
                    }
                    "capture" if v.len() == 3 => {
                        if let String(captured_name) = &v[1] {
                            let pattern = &v[2];
                            if match_pattern_captured(value, pattern, capture) {
                                capture.insert(captured_name, value);
                                true
                            } else {
                                false
                            }
                        } else {
                            false
                        }
                    }
                    _ => false,
                }
            } else {
                // Not an expression.
                false
            }
        }

        // Match if pattern is a subset of the target.
        Object(m) => m.iter().all(|(k, pat)| {
            value
                .get(k)
                .map_or(false, |value| match_pattern_captured(value, pat, capture))
        }),
    }
}

pub type Capture<'k, 'v> = HashMap<&'k str, &'v Value>;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Per-process [`Blackbox`] singleton.
//!
//! Useful for cases where it's inconvenient to pass [`Blackbox`] around.

use std::ops::DerefMut;

use lazy_static::lazy_static;
use parking_lot::Mutex;

use crate::event::Event;
use crate::Blackbox;
use crate::BlackboxOptions;

lazy_static! {
    pub static ref SINGLETON: Mutex<Blackbox> =
        Mutex::new(BlackboxOptions::new().create_in_memory().unwrap());
}

/// Replace the global [`Blackbox`] instance.
///
/// The session ID of the old blackbox will be reused.
///
/// If [`log`] was called, their side effects will be re-applied to the
/// specified blackbox.
pub fn init(mut blackbox: Blackbox) {
    let mut singleton = SINGLETON.lock();

    // Insert dirty entries to the new blackbox.
    let old_blackbox = singleton.deref_mut();
    for entry in old_blackbox.log.iter_dirty() {
        if let Ok(entry) = entry {
            let _ = blackbox.log.append(entry);
        }
    }

    // Perserve session_id if pid hasn't been changed.
    if blackbox.session_pid() == old_blackbox.session_pid() {
        blackbox.session_id = old_blackbox.session_id;
    }

    *singleton.deref_mut() = blackbox;
}

/// Log to the global [`Blackbox`] instance.
///
/// If [`init`] was not called, log requests will be buffered in memory.
pub fn log(data: &Event) {
    SINGLETON.lock().log(data);
}

/// Write buffered data to disk.
pub fn sync() {
    SINGLETON.lock().sync();
}

/// Reset the blackbox to an in-memory instance.
/// This releases file handlers on Windows so related files
/// can be deleted.
pub fn reset() {
    let blackbox = BlackboxOptions::new().create_in_memory().unwrap();
    init(blackbox);
}

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::*;
    use crate::blackbox::tests::all_entries;

    #[test]
    fn test_buffered_writes() {
        let events = vec![
            Event::Alias {
                from: "x".to_string(),
                to: "y".to_string(),
            },
            Event::Alias {
                from: "p".to_string(),
                to: "q".to_string(),
            },
        ];
        for e in &events {
            log(e);
        }

        let dir = tempdir().unwrap();
        let blackbox = BlackboxOptions::new().open(&dir).unwrap();
        init(blackbox);

        for e in &events[1..2] {
            log(e);
        }

        let mut singleton = SINGLETON.lock();
        let blackbox = singleton.deref_mut();
        assert_eq!(all_entries(blackbox).len(), 3);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

// [cats]
// entry_name.priority=20
// entry_name.path=/var/boo/cat
// different_entry_name.priority=5
// different_entry_name.more_custom_data=/some/other

use std::collections::HashMap;
use std::path::PathBuf;
use std::str;

use anyhow::Result;
use configmodel::Config;
use configmodel::Text;
use indexmap::IndexMap;
use serde::Deserialize;
use serde::Serialize;
use thiserror::Error;
use util::path::expand_path;

#[derive(Debug, Error)]
#[error("File containing CATs not found in {missing:?}.")]
pub struct MissingCATs {
    missing: Vec<PathBuf>,
}

#[derive(Serialize, Deserialize)]
struct Cats {
    crypto_auth_tokens: String,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CatGroup {
    pub name: String,
    pub priority: i32,
    pub path: Option<PathBuf>,
}

impl CatGroup {
    fn new(group: &str, mut settings: HashMap<&str, Text>) -> Result<Self> {
        let name = group.into();

        let path = settings
            .remove("path")
            .filter(|s| !s.is_empty())
            .map(expand_path);

        let priority = settings
            .remove("priority")
            .map(|s| s.parse())
            .transpose()?
            .unwrap_or_default();

        Ok(Self {
            name,
            priority,
            path,
        })
    }
}

#[derive(Clone)]
pub struct CatsSection<'a> {
    groups: Vec<CatGroup>,
    #[allow(dead_code)]
    config: &'a dyn Config,
}

impl<'a> CatsSection<'a> {
    pub fn from_config(config: &'a dyn Config, section_name: &str) -> Self {
        // Use an IndexMap to preserve ordering; needed to correctly handle precedence.
        let mut groups = IndexMap::new();

        let keys = config.keys(section_name);
        for key in &keys {
            // Skip keys that aren't valid UTF-8 or that don't match
            // the expected cats key format of `group.setting`.
            let (group, setting) = match key.find('.') {
                Some(i) => (&key[..i], &key[i + 1..]),
                None => continue,
            };
            if let Some(value) = config.get(section_name, key) {
                groups
                    .entry(group)
                    .or_insert_with(HashMap::new)
                    .insert(setting, value);
            }
        }

        let groups = groups
            .into_iter()
            .filter_map(|(group, settings)| CatGroup::new(group, settings).ok())
            .collect();

        Self { groups, config }
    }

    /// Find existing cats with highest priority.
    pub fn find_cats(&self) -> Result<Option<CatGroup>, MissingCATs> {
        let mut best: Option<&CatGroup> = None;
        let mut missing = Vec::new();

        for group in &self.groups {
            // If there is an existing candidate, check whether the current
            // cats entry is a more specific match.
            if let Some(ref best) = best {
                // If prefixes are the same, break the tie using priority.
                if group.priority < best.priority {
                    continue;
                }
            }

            // Skip this group is any of the files are missing.
            match &group.path {
                Some(path) if !path.is_file() => {
                    tracing::debug!(
                        "Ignoring [cats] group {:?} because of missing {:?}",
                        &group.name,
                        &path
                    );
                    missing.push(path.to_path_buf());
                    continue;
                }
                _ => {}
            }

            best = Some(group);
        }

        if let Some(best) = best {
            Ok(Some(best.clone()))
        } else if !missing.is_empty() {
            Err(MissingCATs { missing })
        } else {
            Ok(None)
        }
    }

    pub fn get_cats(&self) -> Result<Option<String>> {
        if let Some(cats_group) = self.find_cats()? {
            if let Some(path) = cats_group.path {
                let f = std::fs::File::open(path)?;
                let reader = std::io::BufReader::new(f);

                let cats: Cats = serde_json::from_reader(reader)?;
                let cats_data = cats.crypto_auth_tokens;

                return Ok(Some(cats_data));
            }
        }
        Ok(None)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fmt;
use std::ops::Deref;
use std::ops::DerefMut;
use std::sync::Arc;

use anyhow::Result;
use manifest::DiffEntry;
use manifest::DiffType;
use manifest::FileMetadata;
use manifest::FileType;
use manifest::Manifest;
use pathmatcher::Matcher;
use pathmatcher::XorMatcher;
use progress_model::ProgressBar;
use tracing::instrument;
use types::RepoPathBuf;

/// Map of simple actions that needs to be performed to move between revisions without conflicts.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct ActionMap {
    map: HashMap<RepoPathBuf, Action>,
}

/// Basic update action.
/// Diff between regular(no conflict checkin) commit generates list of such actions.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Action {
    Update(UpdateAction),
    Remove,
    UpdateExec(bool),
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UpdateAction {
    pub from: Option<FileMetadata>,
    pub to: FileMetadata,
}

impl ActionMap {
    // This is similar to CheckoutPlan::new
    // Eventually CheckoutPlan::new will migrate to take (Conflict)ActionMap instead of a Diff and there won't be code duplication
    #[instrument(skip_all)]
    pub fn from_diff<D: Iterator<Item = Result<DiffEntry>>>(diff: D) -> Result<Self> {
        let mut map = HashMap::new();
        for entry in diff {
            let entry = entry?;
            match entry.diff_type {
                DiffType::LeftOnly(_) => {
                    map.insert(entry.path, Action::Remove);
                }
                DiffType::RightOnly(meta) => {
                    if meta.file_type != FileType::GitSubmodule {
                        map.insert(entry.path, Action::Update(UpdateAction::new(None, meta)));
                    }
                }
                DiffType::Changed(old, new) => {
                    match (old.hgid == new.hgid, old.file_type, new.file_type) {
                        (true, FileType::Executable, FileType::Regular) => {
                            map.insert(entry.path, Action::UpdateExec(false));
                        }
                        (true, FileType::Regular, FileType::Executable) => {
                            map.insert(entry.path, Action::UpdateExec(true));
                        }
                        _ => {
                            if new.file_type != FileType::GitSubmodule {
                                map.insert(
                                    entry.path,
                                    Action::Update(UpdateAction::new(Some(old), new)),
                                );
                            }
                        }
                    }
                }
            }
        }
        Ok(Self { map })
    }

    pub fn with_sparse_profile_change<
        M1: 'static + Matcher + Send + Sync,
        M2: 'static + Matcher + Send + Sync,
    >(
        mut self,
        old_matcher: M1,
        new_matcher: M2,
        old_manifest: &impl Manifest,
        new_manifest: &impl Manifest,
    ) -> Result<Self> {
        let _prog = ProgressBar::register_new("sparse config", 0, "");

        // First - remove all the files that were scheduled for update, but actually aren't in new sparse profile
        let mut result = Ok(());
        self.map.retain(|path, action| {
            if result.is_err() {
                return true;
            }
            if matches!(action, Action::Remove) {
                return true;
            }
            match new_matcher.matches_file(path.as_ref()) {
                Ok(v) => v,
                Err(err) => {
                    result = Err(err);
                    true
                }
            }
        });
        result?;

        // Second - handle files in a new manifest, that were affected by sparse profile change
        let new_matcher = Arc::new(new_matcher);
        let xor_matcher = XorMatcher::new(old_matcher, new_matcher.clone());
        for file in new_manifest.files(xor_matcher) {
            let file = file?;
            if new_matcher.matches_file(&file.path)? {
                match self.map.entry(file.path) {
                    Entry::Vacant(va) => {
                        va.insert(Action::Update(UpdateAction::new(None, file.meta)));
                    }
                    Entry::Occupied(mut oc) => match oc.get() {
                        Action::Remove | Action::Update(_) => {}
                        Action::UpdateExec(_) => {
                            oc.insert(Action::Update(UpdateAction::new(None, file.meta)));
                        }
                    },
                }
            } else {
                // By definition of xor matcher this means old_matcher.matches_file==true.
                // Remove it if it existed before.
                if old_manifest.get(&file.path)?.is_some() {
                    self.map.insert(file.path, Action::Remove);
                }
            }
        }

        Ok(self)
    }

    #[cfg(test)]
    pub fn empty() -> Self {
        Self {
            map: Default::default(),
        }
    }
}

impl Deref for ActionMap {
    type Target = HashMap<RepoPathBuf, Action>;

    fn deref(&self) -> &Self::Target {
        &self.map
    }
}

impl DerefMut for ActionMap {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.map
    }
}

impl UpdateAction {
    pub fn new(from: Option<FileMetadata>, to: FileMetadata) -> Self {
        Self { from, to }
    }
}

impl IntoIterator for ActionMap {
    type Item = (RepoPathBuf, Action);
    type IntoIter = <HashMap<RepoPathBuf, Action> as IntoIterator>::IntoIter;

    fn into_iter(self) -> Self::IntoIter {
        self.map.into_iter()
    }
}

impl fmt::Display for ActionMap {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (path, action) in &self.map {
            match action {
                Action::Update(up) => write!(f, "up {}=>{}\n", path, up.to.hgid)?,
                Action::UpdateExec(x) => write!(f, "{} {}\n", if *x { "+x" } else { "-x" }, path)?,
                Action::Remove => write!(f, "rm {}\n", path)?,
            }
        }
        Ok(())
    }
}

impl Action {
    pub fn pymerge_action(&self) -> (&'static str, (&'static str, bool), &'static str) {
        match self {
            Action::Update(up) => ("g", (pyflags(&up.to.file_type), false), "created/changed"),
            Action::Remove => ("r", ("", false), "deleted"),
            Action::UpdateExec(x) => (
                "e",
                (if *x { "x" } else { "" }, false),
                "update permissions",
            ),
        }
    }
}

fn pyflags(t: &FileType) -> &'static str {
    match t {
        FileType::Symlink => "l",
        FileType::Regular => "",
        FileType::Executable => "x",
        // NOTE: hg does not have git submoduel "type". Is this code path actually used?
        FileType::GitSubmodule => "",
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use manifest_tree::testutil::make_tree_manifest_from_meta;
    use manifest_tree::testutil::TestStore;
    use pathmatcher::TreeMatcher;
    use types::HgId;

    use super::*;

    #[test]
    fn test_with_sparse_profile_change() -> Result<()> {
        let store = Arc::new(TestStore::new());
        let a = (rp("a"), FileMetadata::regular(hgid(1)));
        let b = (rp("b"), FileMetadata::regular(hgid(2)));
        let c = (rp("c"), FileMetadata::regular(hgid(3)));
        let ab_profile = Arc::new(TreeMatcher::from_rules(["a", "b"].iter(), true)?);
        let ac_profile = Arc::new(TreeMatcher::from_rules(["a", "c"].iter(), true)?);
        let old_manifest = make_tree_manifest_from_meta(store.clone(), vec![]);
        let manifest = make_tree_manifest_from_meta(store, vec![a, b, c]);

        let actions = ActionMap::empty().with_sparse_profile_change(
            ab_profile.clone(),
            ab_profile.clone(),
            &old_manifest,
            &manifest,
        )?;
        assert_eq!("", &actions.to_string());

        let mut expected_actions = ActionMap::empty();
        expected_actions.map.insert(
            rp("c"),
            Action::Update(UpdateAction::new(None, FileMetadata::regular(hgid(3)))),
        );

        let actions = ActionMap::empty().with_sparse_profile_change(
            ab_profile.clone(),
            ac_profile.clone(),
            &old_manifest,
            &manifest,
        )?;
        assert_eq!(expected_actions, actions);

        let mut actions = ActionMap::empty();
        actions.map.insert(
            rp("b"),
            Action::Update(UpdateAction::new(None, FileMetadata::regular(hgid(10)))),
        );
        actions.map.insert(rp("b"), Action::UpdateExec(true));
        let actions = actions.with_sparse_profile_change(
            ab_profile.clone(),
            ac_profile.clone(),
            &old_manifest,
            &manifest,
        )?;
        assert_eq!(expected_actions, actions);

        let mut actions = ActionMap::empty();
        actions.map.insert(
            rp("c"),
            Action::Update(UpdateAction::new(None, FileMetadata::regular(hgid(3)))),
        );
        let actions = actions.with_sparse_profile_change(
            ab_profile.clone(),
            ac_profile.clone(),
            &old_manifest,
            &manifest,
        )?;

        assert_eq!(expected_actions, actions);

        Ok(())
    }

    fn rp(p: &str) -> RepoPathBuf {
        RepoPathBuf::from_string(p.to_string()).unwrap()
    }

    fn hgid(p: u8) -> HgId {
        let mut r = HgId::default().into_byte_array();
        r[0] = p;
        HgId::from_byte_array(r)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeMap;
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
use std::time::SystemTime;

use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use configmodel::convert::ByteCount;
use configmodel::Config;
use configmodel::ConfigExt;
use manifest_tree::Diff;
use manifest_tree::TreeManifest;
use pathmatcher::Matcher;
use progress_model::ProgressBar;
use repolock::RepoLocker;
use storemodel::ReadFileContents;
use tracing::instrument;
use treestate::dirstate::Dirstate;
use treestate::dirstate::TreeStateFields;
use treestate::serialization::Serializable;
use treestate::treestate::TreeState;
use types::hgid::NULL_ID;
use types::HgId;
use util::file::atomic_open;
use util::file::atomic_write;
use util::path::remove_file;
use vfs::VFS;
use workingcopy::sparse;

use crate::file_state;
use crate::ActionMap;
use crate::Checkout;
use crate::CheckoutPlan;

pub struct CheckoutStats {
    pub updated: usize,
    pub merged: usize,
    pub removed: usize,
    pub unresolved: usize,
}

impl std::fmt::Display for CheckoutStats {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut first = true;
        for (name, val) in [
            ("updated", self.updated),
            ("merged", self.merged),
            ("removed", self.removed),
            ("unresolved", self.unresolved),
        ] {
            if val == 0 {
                continue;
            }

            if !first {
                write!(f, ", ")?;
            }
            first = false;

            write!(f, "{} files {}", val, name)?;
        }

        Ok(())
    }
}

#[derive(Debug, thiserror::Error)]
#[error("checkout error: {source}")]
pub struct CheckoutError {
    pub resumable: bool,
    pub source: anyhow::Error,
}

/// A somewhat simplified/specialized checkout suitable for use during a clone.
#[instrument(skip_all, fields(path=%dot_path.display(), %target), err)]
pub fn checkout(
    config: &dyn Config,
    dot_path: &Path,
    source_mf: &TreeManifest,
    target_mf: &TreeManifest,
    file_store: Arc<dyn ReadFileContents<Error = anyhow::Error> + Send + Sync>,
    ts: &mut TreeState,
    target: HgId,
    locker: &RepoLocker,
) -> anyhow::Result<CheckoutStats, CheckoutError> {
    let mut state = CheckoutState::default();
    state
        .checkout(
            config, dot_path, source_mf, target_mf, file_store, ts, target, locker,
        )
        .map_err(|err| CheckoutError {
            resumable: state.resumable,
            source: err,
        })
}

#[derive(Default)]
struct CheckoutState {
    resumable: bool,
}

impl CheckoutState {
    fn checkout(
        &mut self,
        config: &dyn Config,
        dot_path: &Path,
        source_mf: &TreeManifest,
        target_mf: &TreeManifest,
        file_store: Arc<dyn ReadFileContents<Error = anyhow::Error> + Send + Sync>,
        ts: &mut TreeState,
        target: HgId,
        locker: &RepoLocker,
    ) -> anyhow::Result<CheckoutStats> {
        let wc_path = match dot_path.parent() {
            Some(p) => p,
            None => bail!("invalid dot path {}", dot_path.display()),
        };

        let _wlock = locker.lock_working_copy(dot_path.to_owned())?;

        let mut sparse_overrides = None;

        let vfs = VFS::new(wc_path.to_path_buf())?;

        let (matcher, _hash): (Arc<dyn Matcher + Sync + Send>, u64) =
            match util::file::exists(dot_path.join("sparse"))? {
                Some(_) => {
                    let overrides = sparse::config_overrides(config);
                    sparse_overrides = Some(overrides.clone());
                    sparse::repo_matcher_with_overrides(
                        &vfs,
                        dot_path,
                        target_mf.clone(),
                        file_store.clone(),
                        &overrides,
                    )?
                }
                None => None,
            }
            .unwrap_or_else(|| (Arc::new(pathmatcher::AlwaysMatcher::new()), 0));

        let diff =
            Diff::new(source_mf, target_mf, &matcher).context("error creating checkout diff")?;
        let actions = ActionMap::from_diff(diff).context("error creating checkout action map")?;

        let checkout = Checkout::from_config(vfs.clone(), config)?;
        let mut plan = checkout.plan_action_map(actions);

        // Write out overrides first so they don't change when resuming
        // this checkout.
        if let Some(sparse_overrides) = sparse_overrides {
            atomic_write(&dot_path.join(sparse::CONFIG_OVERRIDE_CACHE), |f| {
                serde_json::to_writer(f, &sparse_overrides)?;
                Ok(())
            })?;
        }

        if config.get_or_default("checkout", "resumable")? {
            let progress_path = dot_path.join("updateprogress");
            plan.add_progress(&progress_path).with_context(|| {
                format!(
                    "error loading checkout progress '{}'",
                    progress_path.display()
                )
            })?;
            self.resumable = true;
        }

        atomic_write(&dot_path.join("updatestate"), |f| {
            f.write_all(target.to_hex().as_bytes())
        })?;

        plan.blocking_apply_store(&file_store)?;

        ts.set_metadata(BTreeMap::from([("p1".to_string(), target.to_hex())]))?;

        update_dirstate(&plan, ts, &vfs)?;
        flush_dirstate(config, ts, dot_path, target)?;

        remove_file(dot_path.join("updatestate"))?;

        Ok(CheckoutStats {
            updated: plan.stats().0,
            merged: 0,
            removed: 0,
            unresolved: 0,
        })
    }
}

#[instrument(skip_all, err)]
fn update_dirstate(plan: &CheckoutPlan, ts: &mut TreeState, vfs: &VFS) -> anyhow::Result<()> {
    let (update_count, remove_count) = plan.stats();
    let bar = ProgressBar::register_new("recording", (update_count + remove_count) as u64, "files");

    // Probably not required for clone.
    for removed in plan.removed_files() {
        ts.remove(removed)?;
        bar.increase_position(1);
    }

    for updated in plan
        .updated_content_files()
        .chain(plan.updated_meta_files())
    {
        let fstate = file_state(&vfs, updated)?;
        ts.insert(updated, &fstate)?;
        bar.increase_position(1);
    }

    Ok(())
}

pub fn flush_dirstate(
    config: &dyn Config,
    ts: &mut TreeState,
    dot_hg_path: &Path,
    target: HgId,
) -> anyhow::Result<()> {
    // Flush treestate then write .hg/dirstate that points to the
    // current treestate file.

    let dirstate_path = dot_hg_path.join("dirstate");
    let mut dirstate_file = atomic_open(&dirstate_path)?;

    // Get "now" from the atomic temp file we just created's mtime.
    // This ensures we use a sane mtime in case the file system
    // doesn't match our local clock, for whatever reason.
    let now = dirstate_file
        .as_file()
        .metadata()?
        .modified()?
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs();

    // Invalidate entries with mtime >= now so we can notice user
    // edits to files in the same second the checkout completes.
    ts.invalidate_mtime(now.try_into()?)
        .context("error invalidating dirstate mtime")?;

    let tree_root_id = ts.flush()?;

    let tree_file = ts
        .path()
        .ok_or_else(|| anyhow!("TreeState shouldn't be backed by memory but a physical file"))?
        .file_name()
        .ok_or_else(|| anyhow!("bad treestate path: {:?}", ts.path()))?;

    let mut threshold = 0;
    let min_repack_threshold = config
        .get_or_default::<ByteCount>("treestate", "minrepackthreshold")?
        .value();
    if tree_root_id.0 > min_repack_threshold {
        if let Some(factor) = config.get_nonempty_opt::<u64>("treestate", "repackfactor")? {
            threshold = tree_root_id.0 * factor;
        }
    }
    let ds = Dirstate {
        p1: target,
        p2: NULL_ID,
        tree_state: Some(TreeStateFields {
            tree_filename: tree_file.to_owned().into_string().map_err(|_| {
                anyhow!(
                    "can't convert treestate file name to String: {:?}",
                    tree_file
                )
            })?,
            tree_root_id,
            repack_threshold: Some(threshold),
        }),
    };

    ds.serialize(dirstate_file.as_file())?;

    dirstate_file.save()?;

    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::HashMap;
use std::fmt;
use std::ops::Deref;
use std::ops::DerefMut;

use manifest::FileMetadata;
use types::RepoPathBuf;

use crate::actions::UpdateAction;

pub enum Conflict {
    // ("m", (f, f, f, False, pa.node()), "versions differ")
    // -or-
    // ("m", (f, f, None, False, pa.node()), "both created")
    BothChanged {
        ancestor: Option<FileMetadata>,
        dest: FileMetadata,
        src: FileMetadata,
    },
    SrcRemovedDstChanged(UpdateAction),
    // ("cd", (f, None, f, False, pa.node()), "prompt changed/deleted")
    DstRemovedSrcChanged(UpdateAction), // ("dc", (None, f, f, False, pa.node()), "prompt deleted/changed")
}

// mergestate in python
#[derive(Default)]
pub struct ConflictState {
    map: HashMap<RepoPathBuf, Conflict>,
}

impl Deref for ConflictState {
    type Target = HashMap<RepoPathBuf, Conflict>;

    fn deref(&self) -> &Self::Target {
        &self.map
    }
}

impl DerefMut for ConflictState {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.map
    }
}

impl fmt::Display for ConflictState {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (path, conflict) in &self.map {
            match conflict {
                Conflict::SrcRemovedDstChanged(up) => write!(f, "cd {}=>{}\n", path, up.to.hgid)?,
                Conflict::DstRemovedSrcChanged(up) => write!(f, "dc {}=>{}\n", path, up.to.hgid)?,
                Conflict::BothChanged { dest, src, .. } => {
                    write!(f, "m  {} [src=>{}, dest=>{}]\n", path, src.hgid, dest.hgid)?
                }
            }
        }
        Ok(())
    }
}