/*
 * 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::fmt;
use std::io;
use std::path::Path;

use anyhow::Context;
use anyhow::Result;

#[cfg(target_os = "freebsd")]
use self::freebsd::fstype as fstype_imp;
#[cfg(target_os = "linux")]
use self::linux::fstype as fstype_imp;
#[cfg(target_os = "macos")]
use self::macos::fstype as fstype_imp;
#[cfg(windows)]
use self::windows::fstype as fstype_imp;

#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
pub enum FsType {
    EDENFS,
    NTFS,
    APFS,
    HFS,
    EXT4,
    BTRFS,
    XFS,
    UFS,
    ZFS,
    NFS,
    FUSE,
    TMPFS,
    /// The catch-all type for the unknown filesystems. The content of the string is as returned
    /// from the OS and cannot be relied upon.
    Unknown(String),
}

impl fmt::Display for FsType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FsType::EDENFS => write!(f, "EdenFS"),
            FsType::NTFS => write!(f, "NTFS"),
            FsType::APFS => write!(f, "APFS"),
            FsType::HFS => write!(f, "HFS"),
            FsType::EXT4 => write!(f, "ext4"),
            FsType::BTRFS => write!(f, "Btrfs"),
            FsType::XFS => write!(f, "XFS"),
            FsType::UFS => write!(f, "UFS"),
            FsType::ZFS => write!(f, "ZFS"),
            FsType::NFS => write!(f, "NFS"),
            FsType::FUSE => write!(f, "FUSE"),
            FsType::TMPFS => write!(f, "tmpfs"),
            FsType::Unknown(fstype) => write!(f, "Unknown({})", fstype),
        }
    }
}

#[cfg(windows)]
mod windows {
    use std::os::windows::ffi::OsStrExt;
    use std::path::PathBuf;
    use std::ptr::null_mut;

    use winapi::shared::minwindef::DWORD;
    use winapi::shared::minwindef::MAX_PATH;
    use winapi::shared::minwindef::ULONG;
    use winapi::shared::winerror::ERROR_NOT_A_REPARSE_POINT;
    use winapi::um::fileapi::CreateFileW;
    use winapi::um::fileapi::GetVolumeInformationByHandleW;
    use winapi::um::fileapi::OPEN_EXISTING;
    use winapi::um::handleapi::CloseHandle;
    use winapi::um::handleapi::INVALID_HANDLE_VALUE;
    use winapi::um::ioapiset::DeviceIoControl;
    use winapi::um::winbase::FILE_FLAG_BACKUP_SEMANTICS;
    use winapi::um::winioctl::FSCTL_GET_REPARSE_POINT;
    use winapi::um::winnt::FILE_GENERIC_READ;
    use winapi::um::winnt::FILE_SHARE_DELETE;
    use winapi::um::winnt::FILE_SHARE_READ;
    use winapi::um::winnt::FILE_SHARE_WRITE;
    use winapi::um::winnt::HANDLE;
    use winapi::um::winnt::IO_REPARSE_TAG_GVFS;
    use winapi::um::winnt::MAXIMUM_REPARSE_DATA_BUFFER_SIZE;
    use winapi::um::winnt::REPARSE_GUID_DATA_BUFFER;

    use super::*;

    struct WinFileHandle {
        handle: HANDLE,
    }

    impl Drop for WinFileHandle {
        fn drop(&mut self) {
            unsafe { CloseHandle(self.handle) };
        }
    }

    fn reparse_tag(handle: &WinFileHandle) -> Result<ULONG> {
        let mut data_buffer = [0u8; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];

        let mut bytes_written = 0;

        let success = unsafe {
            DeviceIoControl(
                handle.handle,
                FSCTL_GET_REPARSE_POINT,
                null_mut(),
                0,
                data_buffer.as_mut_ptr() as *mut _,
                data_buffer.len() as DWORD,
                &mut bytes_written,
                null_mut(),
            )
        };

        if success == 0 {
            let err = io::Error::last_os_error();

            // In theory we should be testing if the file has a reparse point attached to it first,
            // but it looks like files/directories in ProjectedFS appear as just plain regular
            // files, thus try to get the reparse point and bail if it isn't one instead of
            // failing.
            if err.raw_os_error() == Some(ERROR_NOT_A_REPARSE_POINT as i32) {
                Ok(0)
            } else {
                Err(err.into())
            }
        } else {
            let reparse_buffer =
                unsafe { &*(data_buffer.as_ptr() as *const REPARSE_GUID_DATA_BUFFER) };
            Ok(reparse_buffer.ReparseTag)
        }
    }

    fn is_edenfs(handle: &WinFileHandle) -> Result<bool> {
        let tag = reparse_tag(handle)?;
        Ok(tag == IO_REPARSE_TAG_GVFS)
    }

    fn open_share(path: &Path) -> Result<WinFileHandle> {
        let mut root: Vec<u16> = path.as_os_str().encode_wide().collect();
        // Need to make it 0 terminated,
        // otherwise might not get the correct
        // string
        root.push(0);

        let handle = unsafe {
            CreateFileW(
                root.as_mut_ptr(),
                FILE_GENERIC_READ,
                FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
                null_mut(),
                OPEN_EXISTING,
                FILE_FLAG_BACKUP_SEMANTICS,
                null_mut(),
            )
        };

        if handle == INVALID_HANDLE_VALUE {
            Err(io::Error::last_os_error().into())
        } else {
            Ok(WinFileHandle { handle })
        }
    }

    fn edenfs_directory(mut path: &Path) -> Option<PathBuf> {
        if path.is_file() {
            // Safe to unwrap as a file always has a parent directory.
            path = path.parent().unwrap();
        }
        loop {
            if identity::must_sniff_dir(path).is_ok() {
                // The .eden directory is at the root of the repo, don't recurse further if it's
                // not there.
                let dot_eden = path.join(".eden");
                if dot_eden.exists() {
                    return Some(dot_eden);
                } else {
                    return None;
                }
            }

            path = match path.parent() {
                None => return None,
                Some(path) => path,
            }
        }
    }

    impl From<String> for FsType {
        fn from(value: String) -> Self {
            match value.as_ref() {
                "NTFS" => FsType::NTFS,
                _ => FsType::Unknown(value),
            }
        }
    }

    pub fn fstype(path: &Path) -> Result<FsType> {
        let win_handle = match edenfs_directory(path) {
            None => open_share(path)?,
            Some(dot_eden) => open_share(&dot_eden)?,
        };

        if is_edenfs(&win_handle)? {
            return Ok(FsType::EDENFS);
        }

        let mut fstype = [0u16; MAX_PATH];
        let exit_sts = unsafe {
            GetVolumeInformationByHandleW(
                win_handle.handle,
                null_mut(),
                0,
                null_mut(),
                null_mut(),
                null_mut(),
                fstype.as_mut_ptr(),
                fstype.len() as DWORD,
            )
        };

        if exit_sts == 0 {
            return Err(io::Error::last_os_error().into());
        }
        // Take until the first 0 byte
        let terminator = fstype.iter().position(|&x| x == 0).unwrap();
        let fstype = String::from_utf16(&fstype[0..terminator])?;

        Ok(fstype.into())
    }
}

#[cfg(unix)]
mod unix {
    use std::ffi::CString;
    use std::mem::zeroed;
    use std::os::unix::ffi::OsStrExt;

    use super::*;

    pub fn get_statfs(path: &Path) -> Result<libc::statfs> {
        let cstr = CString::new(path.as_os_str().as_bytes())?;
        let mut fs_stat: libc::statfs = unsafe { zeroed() };
        if unsafe { libc::statfs(cstr.as_ptr(), &mut fs_stat) } == 0 {
            Ok(fs_stat)
        } else {
            Err(io::Error::last_os_error().into())
        }
    }
}

#[cfg(target_os = "linux")]
mod linux {
    use std::collections::HashMap;
    use std::os::linux::fs::MetadataExt;

    use super::*;

    /// These filesystem types are not in libc yet
    const BTRFS_SUPER_MAGIC: i64 = 0x9123683e;
    const FUSE_SUPER_MAGIC: i64 = 0x65735546;
    const XFS_SUPER_MAGIC: i64 = 0x58465342;

    impl From<i64> for FsType {
        fn from(f_type: i64) -> Self {
            match f_type {
                BTRFS_SUPER_MAGIC => FsType::BTRFS,
                FUSE_SUPER_MAGIC => FsType::FUSE,
                XFS_SUPER_MAGIC => FsType::XFS,
                libc::EXT4_SUPER_MAGIC => FsType::EXT4,
                libc::NFS_SUPER_MAGIC => FsType::NFS,
                libc::TMPFS_MAGIC => FsType::TMPFS,
                libc::CODA_SUPER_MAGIC => FsType::Unknown("coda".to_string()),
                libc::CRAMFS_MAGIC => FsType::Unknown("cramfs".to_string()),
                libc::EFS_SUPER_MAGIC => FsType::Unknown("efs".to_string()),
                libc::HPFS_SUPER_MAGIC => FsType::Unknown("hpfs".to_string()),
                libc::HUGETLBFS_MAGIC => FsType::Unknown("hugetlbfs".to_string()),
                libc::ISOFS_SUPER_MAGIC => FsType::Unknown("isofs".to_string()),
                libc::JFFS2_SUPER_MAGIC => FsType::Unknown("jffs2".to_string()),
                libc::MINIX2_SUPER_MAGIC | libc::MINIX2_SUPER_MAGIC2 => {
                    FsType::Unknown("minix2".to_string())
                }
                libc::MINIX_SUPER_MAGIC | libc::MINIX_SUPER_MAGIC2 => {
                    FsType::Unknown("minix".to_string())
                }
                libc::NCP_SUPER_MAGIC => FsType::Unknown("ncp".to_string()),
                libc::OPENPROM_SUPER_MAGIC => FsType::Unknown("openprom".to_string()),
                libc::PROC_SUPER_MAGIC => FsType::Unknown("proc".to_string()),
                libc::QNX4_SUPER_MAGIC => FsType::Unknown("qnx4".to_string()),
                libc::REISERFS_SUPER_MAGIC => FsType::Unknown("reiserfs".to_string()),
                libc::SMB_SUPER_MAGIC => FsType::Unknown("smb".to_string()),
                libc::USBDEVICE_SUPER_MAGIC => FsType::Unknown("usbdevice".to_string()),
                _ => FsType::Unknown(format!("{:#X}", f_type)),
            }
        }
    }

    fn get_type(f_type: i64, path: &Path) -> Result<FsType> {
        let result = FsType::from(f_type);
        if result == FsType::FUSE {
            // .eden is present in all directories in an EdenFS mount.
            if path.join(".eden").exists() {
                return Ok(FsType::EDENFS);
            } else {
                // Take some efforts to find out the actual filesystem.
                // This works for Linux block devices.
                if let Some(major_minor) = get_dev_major_minor(path) {
                    let props = find_udev_properties(&major_minor);
                    if let Some(name) = props.get("E:ID_FS_TYPE") {
                        if name == "ntfs" {
                            return Ok(FsType::NTFS);
                        } else {
                            return Ok(FsType::Unknown(format!("fuse.{}", name)));
                        }
                    }
                }
            }
        }
        Ok(result)
    }

    /// Find the udev properties for the block device. Best-effort.
    fn find_udev_properties(major_minor: &str) -> HashMap<String, String> {
        // The path is found by stracing `blkid`.
        // To do this "properly", consider https://docs.rs/udev/0.3.0/udev/struct.Device.html
        let path = format!("/run/udev/data/b{}", major_minor);
        let mut result = HashMap::new();
        for line in std::fs::read_to_string(path).unwrap_or_default().lines() {
            if line.contains("=") {
                let words: Vec<_> = line.splitn(2, "=").collect();
                result.insert(words[0].into(), words[1].into());
            }
        }
        result
    }

    /// Get the "st_dev". Return the major:minor form (ex. "8:1").
    fn get_dev_major_minor(path: &Path) -> Option<String> {
        path.symlink_metadata().ok().map(|m| {
            let st_dev = m.st_dev();
            let (major, minor) = unsafe { (libc::major(st_dev), libc::minor(st_dev)) };
            format!("{}:{}", major, minor)
        })
    }

    pub fn fstype(path: &Path) -> Result<FsType> {
        let fs_stat = super::unix::get_statfs(path)?;
        get_type(fs_stat.f_type, path)
    }
}

#[cfg(target_os = "macos")]
mod macos {
    use std::ffi::CStr;

    use super::*;

    impl<'a> From<&'a str> for FsType {
        fn from(value: &'a str) -> Self {
            match value {
                "apfs" => FsType::APFS,
                "hfs" => FsType::HFS,
                "edenfs_eden" => FsType::EDENFS,
                "macfuse_eden" => FsType::EDENFS,
                "osxfuse_eden" => FsType::EDENFS,
                "edenfs:" => FsType::EDENFS,
                _ => FsType::Unknown(value.to_string()),
            }
        }
    }

    pub fn fstype(path: &Path) -> Result<FsType> {
        let fs_stat = super::unix::get_statfs(path)?;
        let fs = unsafe { CStr::from_ptr(fs_stat.f_fstypename.as_ptr()) };

        Ok(fs.to_str()?.into())
    }
}

#[cfg(target_os = "freebsd")]
mod freebsd {
    use std::ffi::CStr;

    use super::*;

    impl<'a> From<&'a str> for FsType {
        fn from(value: &'a str) -> Self {
            match value {
                "ufs" => FsType::UFS,
                "zfs" => FsType::ZFS,
                _ => FsType::Unknown(value.to_string()),
            }
        }
    }

    pub fn fstype(path: &Path) -> Result<FsType> {
        let fs_stat = super::unix::get_statfs(path)?;
        let fs = unsafe { CStr::from_ptr(fs_stat.f_fstypename.as_ptr()) };

        Ok(fs.to_str()?.into())
    }
}

/// Get filesystem type on the given `path`.
pub fn fstype(path: impl AsRef<Path>) -> Result<FsType> {
    let path = path.as_ref();

    // Auto correct an empty path to ".".
    let path = if path == Path::new("") {
        Path::new(".")
    } else {
        path
    };

    fstype_imp(path).with_context(|| format!("Cannot determine filesystem type for {:?}", path))
}


/*
 * 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.
 */

//! Simple crate to call fsync on files matching glob patterns.
//!
//! This is a standalone crate to help reducing compile time of `hgcommands`.

use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use std::time::SystemTime;

use glob::Pattern;
use tracing::debug;
use tracing::trace;
use tracing::warn;

/// Call `fsync` on files matching given glob patterns under the given directory.
///
/// Errors are silenced and logged to tracing framework.
/// Files not recently modified (older than `newer_than`) are skipped.
///
/// Returns paths that are fsync-ed.
pub fn fsync_glob(dir: &Path, patterns: &[&str], newer_than: Option<SystemTime>) -> Vec<PathBuf> {
    let escaped_dir = Pattern::escape(&dir.display().to_string());
    let mut result = Vec::new();
    for p in patterns {
        let full_pattern = format!("{}/{}", &escaped_dir, p);
        debug!("globing {}", &full_pattern);

        let matches = match glob::glob(&full_pattern) {
            Err(e) => {
                warn!("glob failed: {}", e);
                continue;
            }
            Ok(matches) => matches,
        };

        let newer_than = newer_than.unwrap_or_else(|| {
            let now = SystemTime::now();
            now.checked_sub(Duration::from_secs(300)).unwrap_or(now)
        });

        for path in matches {
            let path = match path {
                Ok(path) => path,
                Err(e) => {
                    warn!("path reading failed: {}", e);
                    continue;
                }
            };

            match try_fsync_if_newer_than(&path, newer_than) {
                Ok(true) => {
                    if let Ok(path) = path.strip_prefix(dir) {
                        result.push(path.to_path_buf());
                    }
                    debug!("fsynced: {}", path.display());
                }
                Ok(false) => trace!("skipped: {}", path.display()),
                Err(e) => warn!("cannot fsync {}: {}", path.display(), e),
            }
        }
    }
    result.sort_unstable();

    // Also fsync parent directories on *nix. This syncs metadata about the file.
    // On Windows directories cannot be opened.
    #[cfg(unix)]
    {
        let dirs: Vec<&Path> = {
            let mut dirs: Vec<_> = result.iter().filter_map(|p| p.parent()).collect();
            dirs.dedup();
            dirs
        };
        for path in dirs {
            let path = dir.join(path);
            match fs::OpenOptions::new().read(true).open(&path) {
                Ok(file) => match file.sync_all() {
                    Ok(_) => debug!("fsynced dir: {}", path.display()),
                    Err(e) => warn!("cannot fsync dir {}: {}", path.display(), e),
                },
                Err(e) => warn!("cannot open dir {}: {}", path.display(), e),
            }
        }
    }
    result
}

/// Attempt to fsync a single file.
/// Return false if the file is skipped (not newly modified or not a file).
/// Return true if the file is synced.
fn try_fsync_if_newer_than(path: &Path, newer_than: SystemTime) -> io::Result<bool> {
    let metadata = path.symlink_metadata()?;
    if !metadata.is_file() || metadata.modified()? < newer_than {
        return Ok(false);
    }

    let mut open_opts = fs::OpenOptions::new();
    open_opts.read(true).create(false).truncate(false);

    // Windows requires opening with write permission for fsync.
    if cfg!(windows) {
        open_opts.write(true);
    }

    let file = open_opts.open(path)?;
    file.sync_all()?;
    Ok(true)
}

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

    use tempfile::tempdir;

    use super::*;

    #[test]
    fn test_patterns() {
        let dir = tempdir().unwrap();
        let dir = dir.path();

        fs::write(dir.join("a"), b"1").unwrap();
        fs::write(dir.join("a1"), b"1").unwrap();
        fs::write(dir.join("b"), b"2").unwrap();
        fs::write(dir.join("c"), b"3").unwrap();

        assert_eq!(d(fsync_glob(&dir, &[], None)), "[]");
        assert_eq!(d(fsync_glob(&dir, &["d"], None)), "[]");
        assert_eq!(d(fsync_glob(&dir, &["?"], None)), "[\"a\", \"b\", \"c\"]");
        assert_eq!(
            d(fsync_glob(&dir, &["a*", "c"], None)),
            "[\"a\", \"a1\", \"c\"]"
        );
    }

    #[test]
    fn test_skip_old_files() {
        let dir = tempdir().unwrap();
        let dir = dir.path();

        fs::write(dir.join("a"), b"1").unwrap();
        fs::write(dir.join("b"), b"2").unwrap();

        let newer_than = SystemTime::now()
            .checked_add(Duration::from_secs(10))
            .unwrap();
        assert_eq!(d(fsync_glob(&dir, &["*"], Some(newer_than))), "[]");
    }

    fn d(value: impl std::fmt::Debug) -> String {
        format!("{:?}", 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.
 */

use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;

use gitstore::GitStore;
use minibench::bench;
use minibench::elapsed;
use types::HgId;

fn main() {
    let git_dir = match std::env::var("GIT_DIR") {
        Ok(dir) => PathBuf::from(dir),
        _ => {
            eprintln!("This benchmark requires a git repo set by GIT_DIR.");
            eprintln!("Set GIT_DIR and re-run the benchmark.");
            return;
        }
    };

    // Get some commit hashes from the repo.
    eprint!("obtaining commit hashes\r");
    let commits_str: String = git(&["log", "--format=%H", "--max-count=10000"]);
    let commit_hgids: Vec<HgId> = commits_str
        .lines()
        .map(|l| HgId::from_hex(l.as_bytes()).unwrap())
        .collect();
    eprintln!("got {:5} commit hashes", commit_hgids.len());

    bench("reading commit objects via GitStore", || {
        let store = GitStore::open(&git_dir).unwrap();
        elapsed(|| {
            for &id in &commit_hgids {
                let _ = store.read_obj(id, git2::ObjectType::Commit).unwrap();
            }
        })
    });

    bench("reading commit objects via git CLI", || {
        elapsed(|| {
            git_cat_file(&commits_str);
        })
    });
}

fn git(args: &[&str]) -> String {
    let out = Command::new("git").args(args).output().unwrap();
    String::from_utf8(out.stdout).unwrap()
}

fn git_cat_file(input: &str) {
    let mut proc = Command::new("git")
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .args(["cat-file", "--batch"])
        .spawn()
        .unwrap();
    proc.stdin
        .as_ref()
        .unwrap()
        .write_all(input.as_bytes())
        .unwrap();
    proc.wait().unwrap();
}


/*
 * 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.
 */

//! A simple store implementation to access a local git repo's odb.

use std::path::Path;

use types::HgId;

type Git2Result<T> = Result<T, git2::Error>;

pub struct GitStore {
    odb: git2::Odb<'static>,

    // Makes `odb` valid. Last field drops last.
    // No need to use this field. Just need to keep it alive.
    // Use `Opaque` to forbid access to the underlying repo.
    // See also `safety` notes in `GitStore::open`.
    #[allow(dead_code)]
    opaque_repo: Box<dyn Opaque + Send + Sync>,
}

trait Opaque {}

impl GitStore {
    /// `open` a Git bare repo at `git_dir`. Gain access to its odb (object database).
    pub fn open(git_dir: &Path) -> Git2Result<Self> {
        let git_repo = git2::Repository::open(git_dir)?;
        let odb = git_repo.odb()?;

        struct UnsafeForceSync<T: ?Sized>(T);
        unsafe impl<T: ?Sized> Send for UnsafeForceSync<T> {}
        unsafe impl<T: ?Sized> Sync for UnsafeForceSync<T> {}
        impl Opaque for UnsafeForceSync<git2::Repository> {}

        // safety: `odb` is alive as long as `git_repo` is alive.
        let odb = unsafe { std::mem::transmute(odb) };
        // safety: we don't access `opaque_repo` in multiple threads.
        // Cast to `Opaque` and prevents access to `git_repo`.
        let opaque_repo: Box<dyn Opaque + Send + Sync> = Box::new(UnsafeForceSync(git_repo));

        let store = GitStore { odb, opaque_repo };
        Ok(store)
    }

    /// Read an object of the given type.
    pub fn read_obj(&self, id: HgId, kind: git2::ObjectType) -> Git2Result<Vec<u8>> {
        if id.is_null() {
            return Ok(Vec::new());
        }
        let oid = hgid_to_git_oid(id);
        let obj = self.odb.read(oid)?;
        if kind != git2::ObjectType::Any && obj.kind() != kind {
            return Err(git2::Error::new(
                git2::ErrorCode::NotFound,
                git2::ErrorClass::Object,
                format!("{} {} not found", kind, oid),
            ));
        }
        Ok(obj.data().to_vec())
    }

    /// Read the size of an object without its full content.
    pub fn read_obj_size(&self, id: HgId, kind: git2::ObjectType) -> Git2Result<usize> {
        if id.is_null() {
            return Ok(0);
        }
        let oid = hgid_to_git_oid(id);
        let (size, obj_kind) = self.odb.read_header(oid)?;
        if kind != git2::ObjectType::Any && obj_kind != kind {
            return Err(git2::Error::new(
                git2::ErrorCode::NotFound,
                git2::ErrorClass::Object,
                format!("{} {} not found", kind, oid),
            ));
        }
        Ok(size)
    }

    /// Write object to the odb.
    pub fn write_obj(&self, kind: git2::ObjectType, data: &[u8]) -> Git2Result<HgId> {
        let oid = self.odb.write(kind, data)?;
        let id = git_oid_to_hgid(oid);
        Ok(id)
    }
}

fn hgid_to_git_oid(id: HgId) -> git2::Oid {
    git2::Oid::from_bytes(id.as_ref()).expect("HgId should convert to git2::Oid")
}

fn git_oid_to_hgid(oid: git2::Oid) -> HgId {
    HgId::from_slice(oid.as_bytes()).expect("git2::Oid should convert to HgId")
}


/*
 * 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.
 */

//! # gitstore
//!
//! Git object store for various trait impls in EdenSCM.

mod gitstore;
mod trait_impls;

pub use git2;

pub use crate::gitstore::GitStore;


/*
 * 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.
 */

//! Implement traits from other crates.

use async_trait::async_trait;
use futures::stream::BoxStream;
use futures::stream::StreamExt;
use storemodel::ReadFileContents;
use storemodel::RefreshableReadFileContents;
use storemodel::RefreshableTreeStore;
use storemodel::TreeFormat;
use storemodel::TreeStore;
use types::HgId;
use types::Key;
use types::RepoPath;

use crate::GitStore;

#[async_trait]
impl ReadFileContents for GitStore {
    type Error = anyhow::Error;

    async fn read_file_contents(
        &self,
        keys: Vec<Key>,
    ) -> BoxStream<Result<(minibytes::Bytes, Key), Self::Error>> {
        let iter = keys.into_iter().map(|k| {
            let id = k.hgid;
            let data = self.read_obj(id, git2::ObjectType::Blob)?;
            Ok((data.into(), k))
        });
        futures::stream::iter(iter).boxed()
    }

    async fn read_rename_metadata(
        &self,
        _keys: Vec<Key>,
    ) -> BoxStream<Result<(Key, Option<Key>), Self::Error>> {
        futures::stream::empty().boxed()
    }
}

impl RefreshableReadFileContents for GitStore {
    fn refresh(&self) -> Result<(), Self::Error> {
        // We don't hold state in memory, so no need to refresh.
        Ok(())
    }
}

impl TreeStore for GitStore {
    fn get(&self, _path: &RepoPath, hgid: HgId) -> anyhow::Result<minibytes::Bytes> {
        let data = self.read_obj(hgid, git2::ObjectType::Tree)?;
        Ok(data.into())
    }

    fn insert(&self, _path: &RepoPath, hgid: HgId, data: minibytes::Bytes) -> anyhow::Result<()> {
        let id = self.write_obj(git2::ObjectType::Tree, data.as_ref())?;
        if id != hgid {
            anyhow::bail!("tree id mismatch: {} (written) != {} (expected)", id, hgid);
        }
        Ok(())
    }

    fn format(&self) -> TreeFormat {
        TreeFormat::Git
    }
}

impl RefreshableTreeStore for GitStore {
    fn refresh(&self) -> anyhow::Result<()> {
        // We don't hold state in memory, so no need to refresh.
        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.
 */

//! The hg-http crate provides common utilities for dealing setting up and
//! managing http requests for the hg application. This crate specifies how
//! a topic should be treated. Topics may include monitoring, request setup,
//! paths, error handling, etc.

use std::collections::HashSet;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::Relaxed;

use auth::AuthSection;
use clientinfo::ClientInfo;
use configmodel::ConfigExt;
use hg_metrics::increment_counter;
use http_client::HttpClient;
use http_client::Request;
use http_client::Stats;
use once_cell::sync::Lazy;
use progress_model::AggregatingProgressBar;
use progress_model::IoSample;
use progress_model::IoTimeSeries;
use progress_model::ProgressBar;
use progress_model::Registry;
use url::Url;

#[derive(Default)]
struct Total {
    download_bytes: AtomicUsize,
    upload_bytes: AtomicUsize,
    request_count: AtomicUsize,
}

// Total progress from all clients.
static TOTAL: Total = Total {
    download_bytes: AtomicUsize::new(0),
    upload_bytes: AtomicUsize::new(0),
    request_count: AtomicUsize::new(0),
};

pub fn current_progress() -> (usize, usize, usize) {
    (
        TOTAL.download_bytes.load(Relaxed),
        TOTAL.upload_bytes.load(Relaxed),
        TOTAL.request_count.load(Relaxed),
    )
}

pub fn http_client(client_id: impl ToString, config: http_client::Config) -> HttpClient {
    let client_id = client_id.to_string();
    let reporter = move |stats: &Stats| {
        bump_counters(&client_id, stats);
    };
    HttpClient::from_config(config).with_event_listeners(|l| {
        l.on_stats(reporter);
    })
}

/// Generate http_client::Config taking into account hg specific config/auth
/// settings. url_for_auth will should be the URL to connect to, and will be
/// used to look up TLS credentials.
pub fn http_config(
    config: &dyn configmodel::Config,
    url_for_auth: &Url,
) -> Result<http_client::Config, auth::MissingCerts> {
    let mut hc = http_client::Config {
        convert_cert: config
            .get_or("http", "convert-cert", || cfg!(windows))
            .unwrap_or(cfg!(windows)),

        client_info: ClientInfo::new(config).and_then(|i| i.into_json()).ok(),
        disable_tls_verification: INSECURE_MODE.load(Relaxed),
        unix_socket_path: config
            .get_nonempty_opt("auth_proxy", "unix_socket_path")
            .expect("Can't get auth_proxy.unix_socket_path config"),
        unix_socket_domains: HashSet::from_iter(
            config
                .get_or("auth_proxy", "unix_socket_domains", Vec::new)
                .unwrap_or_else(|_| vec![])
                .into_iter(),
        ),
        verbose: config.get_or_default("http", "verbose").unwrap_or(false),
        ..Default::default()
    };

    let using_auth_proxy = hc.unix_socket_path.is_some()
        && url_for_auth
            .domain()
            .map_or(false, |d| hc.unix_socket_domains.contains(d));

    if !using_auth_proxy {
        // If we aren't using auth proxy, we need to configure client certs.
        // Defer attempt to load certs until we know we need them.
        let auth = AuthSection::from_config(config).best_match_for(url_for_auth)?;
        (hc.cert_path, hc.key_path, hc.ca_path) = auth
            .map(|auth| (auth.cert, auth.key, auth.cacerts))
            .unwrap_or_default();
    }

    Ok(hc)
}

static INSECURE_MODE: AtomicBool = AtomicBool::new(false);

pub fn enable_insecure_mode() {
    INSECURE_MODE.store(true, Relaxed);
}

/// Setup progress reporting to the main progress registry for the lifetime of
/// this process.
pub fn enable_progress_reporting() {
    let _state = Lazy::force(&PROGRESS_REPORTING_STATE);
}

/// State for progress reporting. Lazily initialized.
static PROGRESS_REPORTING_STATE: Lazy<Box<dyn Send + Sync>> = Lazy::new(|| {
    let trees_bar = AggregatingProgressBar::new("downloading", "bytes");
    let files_bar = AggregatingProgressBar::new("downloading", "bytes");

    Request::on_new_request(move |req| {
        TOTAL.request_count.fetch_add(1, Relaxed);
        let req_listeners = req.ctx_mut().event_listeners();
        req_listeners.on_download_bytes({
            move |_req, n| {
                TOTAL.download_bytes.fetch_add(n, Relaxed);
            }
        });
        req_listeners.on_upload_bytes({
            move |_req, n| {
                TOTAL.upload_bytes.fetch_add(n, Relaxed);
            }
        });

        // TODO: How to tell whether it is downloading or uploading?

        // Consolidate /trees and /files requests into single progress bars.
        let url = req.ctx_mut().url().to_string();
        let mut is_single_bar = false;
        let bar = if url.ends_with("/trees") {
            trees_bar.create_or_extend(0)
        } else if url.ends_with("/files") || url.ends_with("/files2") {
            files_bar.create_or_extend(0)
        } else {
            is_single_bar = true;
            ProgressBar::new("downloading", 0, "bytes")
        };

        bar.set_message(url);

        let req_listeners = req.ctx_mut().event_listeners();
        req_listeners.on_content_length({
            let bar = bar.clone();
            move |_req, n| {
                bar.increase_total(n as _);
            }
        });
        req_listeners.on_download_bytes({
            let bar = bar.clone();
            move |_req, n| {
                bar.increase_position(n as _);
            }
        });
        if is_single_bar {
            req_listeners.on_first_activity(move |_req| {
                let registry = Registry::main();
                registry.register_progress_bar(&bar);
            });
        }
    });

    // HTTP I/O time series.
    let take_sample = {
        || {
            IoSample::from_io_bytes_count(
                TOTAL.download_bytes.load(Relaxed) as _,
                TOTAL.upload_bytes.load(Relaxed) as _,
                TOTAL.request_count.load(Relaxed) as _,
            )
        }
    };

    let net_time_series = IoTimeSeries::new("HTTP", "requests");
    let task = net_time_series.async_sampling(take_sample, IoTimeSeries::default_sample_interval());
    async_runtime::spawn(task);

    let registry = Registry::main();
    registry.register_io_time_series(&net_time_series);

    Box::new(net_time_series)
});

fn bump_counters(client_id: &str, stats: &Stats) {
    let n = |suffix: &'static str| -> String { format!("http.{}.{}", client_id, suffix) };
    // TODO: gauges: rx_bytes and tx_bytes; histograms: request_time_ms, response_delay_ms
    increment_counter(n("total_rx_bytes"), stats.downloaded);
    increment_counter(n("total_tx_bytes"), stats.uploaded);
    increment_counter(n("num_requests"), stats.requests);
    increment_counter(n("total_request_time_ms"), stats.time.as_millis() as usize);
    increment_counter(
        n("total_response_delay_ms"),
        stats.latency.as_millis() as usize,
    )
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use super::*;

    #[test]
    fn test_convert_cert_config() {
        let mut hg_config = BTreeMap::<&str, &str>::new();

        let url: Url = "https://example.com".parse().unwrap();

        assert_eq!(
            cfg!(windows),
            http_config(&hg_config, &url).unwrap().convert_cert
        );

        hg_config.insert("http.convert-cert", "True");
        assert!(http_config(&hg_config, &url).unwrap().convert_cert);

        hg_config.insert("http.convert-cert", "false");
        assert!(!http_config(&hg_config, &url).unwrap().convert_cert);
    }
}


/*
 * 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::borrow::Borrow;
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;

use once_cell::sync::Lazy;
use parking_lot::RwLock;

pub fn increment_counter(key: impl Key, value: usize) {
    METRICS.increment_counter(key, value)
}

pub fn summarize() -> Vec<(String, usize)> {
    METRICS.summarize()
}

pub trait Key: Into<String> + Borrow<str> {}
impl<T> Key for T where T: Into<String> + Borrow<str> {}

pub static METRICS: Lazy<Metrics> = Lazy::new(Metrics::new);

pub struct Metrics {
    counters: RwLock<HashMap<String, AtomicUsize>>,
}

impl Metrics {
    fn new() -> Self {
        let counters = RwLock::new(HashMap::new());

        Self { counters }
    }

    fn increment_counter(&self, key: impl Key, value: usize) {
        {
            let counters = self.counters.read();
            if let Some(counter) = counters.get(key.borrow()) {
                // We could use Relaxed ordering but it makes tests awkward if we were to run on a
                // weakly ordered system, (stress) tests are nice for this code.
                counter.fetch_add(value, Ordering::Release);
                return;
            }
        }
        let mut counters = self.counters.write();
        counters
            .entry(key.into())
            .and_modify(|c| {
                c.fetch_add(value, Ordering::Release);
            })
            .or_insert_with(|| AtomicUsize::new(value));
    }

    fn summarize(&self) -> Vec<(String, usize)> {
        let counters = self.counters.read();
        let mut summary: Vec<(String, usize)> = counters
            .iter()
            .map(|(k, v)| (k.into(), v.load(Ordering::Acquire)))
            .collect();
        summary.sort();
        summary
    }
}

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

    use super::*;

    #[test]
    fn test_increment_string_key() {
        let metrics = Metrics::new();
        metrics.increment_counter(String::from("hello"), 2);
        metrics.increment_counter(String::from("world"), 3);
        metrics.increment_counter(String::from("hello"), 4);
        assert_eq!(
            metrics.summarize(),
            vec![(String::from("hello"), 6), (String::from("world"), 3)]
        );
    }

    #[test]
    fn test_increment_str_key() {
        let metrics = Metrics::new();
        metrics.increment_counter("hello", 2);
        metrics.increment_counter("world", 3);
        metrics.increment_counter("hello", 4);
        assert_eq!(
            metrics.summarize(),
            vec![(String::from("hello"), 6), (String::from("world"), 3)]
        );
    }

    #[test]
    fn test_increment_on_many_threads() {
        static MY_METRICS: Lazy<Metrics> = Lazy::new(Metrics::new);
        let handle = thread::spawn(move || {
            for _i in 0..10000 {
                MY_METRICS.increment_counter("key", 2);
            }
        });
        for _i in 0..10000 {
            MY_METRICS.increment_counter("key", 3);
        }
        handle.join().expect("waiting for spawned thread");
        assert_eq!(MY_METRICS.summarize(), vec![(String::from("key"), 50000)]);
    }
}


/*
 * 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.
 */

macro_rules! commands {
    ( $( mod $module:ident; )* ) => {
        $( mod $module; )*
        pub(crate) fn extend_command_table(table: &mut ::clidispatch::command::CommandTable) {
            // NOTE: Consider passing 'config' to name() or doc() if we want
            // some flexibility defining them using configs.
            $(
            {
                use self::$module as m;
                let command_aliases = m::aliases();
                let doc = m::doc();
                let synopsis = m::synopsis();
                ::clidispatch::command::Register::register(table, m::run, &command_aliases, &doc, synopsis.as_deref());
            }
            )*
        }
    }
}

mod debug;

commands! {
    mod clone;
    mod config;
    mod configfile;
    mod goto;
    mod root;
    mod status;
    mod version;
    mod whereami;
}

pub use anyhow::Result;
use clidispatch::command::CommandTable;
use clidispatch::errors::FallbackToPython;
use clidispatch::global_flags::HgGlobalOpts;
use clidispatch::io::Write;
pub use clidispatch::io::IO;
pub use cliparser::define_flags;
pub use configloader::config::ConfigSet;
use formatter::formatter;
pub use repo::repo::Repo;

fn get_formatter(
    config: &dyn configmodel::Config,
    command_name: &'static str,
    template: &str,
    options: &HgGlobalOpts,
    mut writer: Box<dyn Write>,
) -> Result<Box<dyn formatter::ListFormatter>, FallbackToPython> {
    formatter::get_formatter(
        config,
        command_name,
        template,
        formatter::FormatOptions {
            debug: options.debug,
            verbose: options.verbose,
            quiet: options.quiet,
            color: termstyle::should_color(config, writer.as_mut()),
            debug_color: config.get("ui", "color") == Some("debug".into())
                && !hgplain::is_plain(Some("color")),
        },
        Box::new(writer),
    )
    .map_err(|_| FallbackToPython("template not supported in Rust".to_owned()))
}

#[allow(dead_code)]
/// Return the main command table including all Rust commands.
pub fn table() -> CommandTable {
    let mut table = CommandTable::new();
    extend_command_table(&mut table);
    debug::extend_command_table(&mut table);

    table
}

define_flags! {
    pub struct WalkOpts {
        /// include files matching the given patterns
        #[short('I')]
        #[argtype("PATTERN")]
        include: Vec<String>,

        /// exclude files matching the given patterns
        #[short('X')]
        #[argtype("PATTERN")]
        exclude: Vec<String>,
    }

    pub struct FormatterOpts {
        /// display with template (EXPERIMENTAL)
        #[short('T')]
        #[argtype("TEMPLATE")]
        template: String,
    }

    pub struct MergeToolOpts {
        /// specify merge tool
        #[short('t')]
        tool: String,
    }

    pub struct NoOpts {}
}


/*
 * 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::RefCell;
use std::env;
use std::path::PathBuf;

use clidispatch::io::IO;
use configloader::config::ConfigSet;
use cpython::*;
use cpython_ext::format_py_error;
use cpython_ext::wrap_pyio;
use cpython_ext::Bytes;
use cpython_ext::ResultPyErrExt;
use cpython_ext::Str;
use cpython_ext::WrappedIO;
use tracing::debug_span;
use tracing::info_span;

use crate::commands;
use crate::python::py_finalize;
use crate::python::py_init_threads;
use crate::python::py_initialize;
use crate::python::py_is_initialized;
use crate::python::py_main;
use crate::python::py_set_argv;
use crate::python::py_set_program_name;

const HGPYENTRYPOINT_MOD: &str = "edenscm";
pub struct HgPython {
    py_initialized_by_us: bool,
}

impl HgPython {
    pub fn new(args: &[String]) -> HgPython {
        let py_initialized_by_us = !py_is_initialized();
        if py_initialized_by_us {
            Self::setup_python(args);
        }
        HgPython {
            py_initialized_by_us,
        }
    }

    fn setup_python(args: &[String]) {
        let span = info_span!("Initialize Python");
        let _guard = span.enter();
        let args = Self::prepare_args(args);
        let executable_name = &args[0];
        py_set_program_name(executable_name);
        py_initialize();
        py_set_argv(&args);
        py_init_threads();

        let gil = Python::acquire_gil();
        let py = gil.python();

        // Putting the module in sys.modules makes it importable.
        let sys = py.import("sys").unwrap();
        HgPython::update_path(py, &sys);

        // If this fails, it's a fatal error.
        let name = "bindings";
        let bindings_module = PyModule::new(py, &name).unwrap();
        prepare_builtin_modules(py, &bindings_module).unwrap();
        let sys_modules = PyDict::extract(py, &sys.get(py, "modules").unwrap()).unwrap();
        sys_modules.set_item(py, name, bindings_module).unwrap();
    }

    fn update_path(py: Python, sys: &PyModule) {
        // In homebrew and other environments, the python modules may be installed isolated
        // alongside the binary. Let's setup the PATH so we discover those python modules.
        // An example layout:
        //   $PREFIX/usr/local/bin/hg
        //   $PREFIX/usr/local/lib/python3.8/site-packages/edenscmnative
        //   $PREFIX/usr/local/lib/python3.8/site-packages/edenscmdeps3.zip
        //   $PREFIX/usr/local/lib/python3.8/site-packages/edenscm
        let py_version: (i32, i32, i32, String, i32) =
            sys.get(py, "version_info").unwrap().extract(py).unwrap();

        let path_for_prefix = |prefix: &str| -> String {
            let rel_path = PathBuf::from(format!(
                "{}/python{}.{}/site-packages",
                prefix, py_version.0, py_version.1
            ));
            std::env::current_exe()
                .unwrap()
                .parent()
                .unwrap()
                .parent()
                .unwrap()
                .join(rel_path)
                .into_os_string()
                .into_string()
                .unwrap()
        };
        let py_path: PyList = sys.get(py, "path").unwrap().extract(py).unwrap();
        py_path.append(
            py,
            PyUnicode::new(py, &path_for_prefix("lib")).into_object(),
        );
        py_path.append(
            py,
            PyUnicode::new(py, &path_for_prefix("lib64")).into_object(),
        );
    }

    fn prepare_args(args: &[String]) -> Vec<String> {
        // Replace args[0] with the absolute current_exe path. This workarounds
        // an issue in libpython sys.path handling.
        //
        // More context: Usually, argv[0] is either:
        // - a relative path to the executable, like "hg", or "./hg". It can be
        //   translated to an absolute path using the PATH environment variable
        //   and the current workdir.
        // - an absolute path to the executable, like "/bin/hg".
        //
        // When running as local build, we expect libpython to add the
        // "executable path" to sys.path. However, libpython seems pretty dumb
        // if argv[0] is a relative path, and it's not in the current workdir
        // (in other words, libpython seems to ignore PATH). Therefore, give
        // it some hint by passing the absolute path resolved by the Rust stdlib.
        Some(
            env::current_exe()
                .unwrap()
                .into_os_string()
                .into_string()
                .unwrap(),
        )
        .into_iter()
        .chain(args.iter().skip(1).cloned())
        .collect()
    }

    fn run_hg_py(
        &self,
        py: Python<'_>,
        args: Vec<String>,
        io: &clidispatch::io::IO,
        config: &ConfigSet,
    ) -> PyResult<()> {
        let entry_point_mod =
            info_span!("import edenscm").in_scope(|| py.import(HGPYENTRYPOINT_MOD))?;
        let call_args = {
            let fin = io.with_input(|i| read_to_py_object(py, i));
            let fout = io.with_output(|o| write_to_py_object(py, o));
            let ferr = io.with_error(|e| match e {
                None => fout.clone_ref(py),
                Some(error) => write_to_py_object(py, error),
            });
            let args: Vec<Str> = args.into_iter().map(Str::from).collect();
            let config =
                pyconfigloader::config::create_instance(py, RefCell::new(config.clone())).unwrap();
            (args, fin, fout, ferr, config).to_py_object(py)
        };
        entry_point_mod.call(py, "run", call_args, None)?;
        Ok(())
    }

    /// Run an hg command defined in Python.
    pub fn run_hg(&self, args: Vec<String>, io: &clidispatch::io::IO, config: &ConfigSet) -> i32 {
        let gil = Python::acquire_gil();
        let py = gil.python();
        match self.run_hg_py(py, args, io, config) {
            // The code below considers the following exit scenarios:
            // - `PyResult` is `Ok`. This means that the Python code returned
            //    successfully, without calling `sys.exit` or raising an
            //    uncaught exception
            // - `PyResult` is a `PyErr(SystemExit)`. This means that the Python
            //    code called `sys.exit`.
            //    - The expected case is that the `SystemExit` instance contains
            //      a `code` attribute, which is the desired exit code.
            //    - If it does not, we fail hard with code 255.
            // - `PyResult` is a `PyErr(UncaughtException)`. Something went wrong.
            //    Python's behavior in this case is to print a traceback and to
            //    return 1.
            Err(mut err) => {
                if let Ok(system_exit) = err.instance(py).extract::<exc::SystemExit>(py) {
                    match system_exit.as_object().getattr(py, "code") {
                        Ok(code) => code.extract::<i32>(py).unwrap(),
                        Err(_) => 255,
                    }
                } else {
                    let message =
                        format_py_error(py, &err).unwrap_or("unknown python exception".to_string());
                    let _ = io.write_err(&message);
                    1
                }
            }
            Ok(()) => 0,
        }
    }

    /// Setup ad-hoc tracing with `pattern` about modules.
    /// See `edenscm/traceimport.py` for details.
    ///
    /// Call this before `run_python`, or `run_hg`.
    ///
    /// This is merely to provide convenience.  The user can achieve the same
    /// effect via `run_python`, then import the modules and calling methods
    /// manually.
    pub fn setup_tracing(&mut self, pattern: String) -> PyResult<()> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let traceimport = py.import("edenscm.traceimport")?;
        traceimport.call(py, "enable", (pattern,), None)?;
        Ok(())
    }

    /// Run the Python interpreter.
    pub fn run_python(&mut self, args: &[String], io: &clidispatch::io::IO) -> u8 {
        let args = Self::prepare_args(args);
        if self.py_initialized_by_us {
            // Py_Main will call Py_Finalize. Therefore skip Py_Finalize here.
            self.py_initialized_by_us = false;
            py_main(&args)
        } else {
            // If Python is not initialized by us, it's expected that this
            // function does not call Py_Finalize.
            //
            // If we call Py_Main, users like the Python testutil, or the Python
            // chgserver will crash because Py_Main calls Py_Finalize.
            // Avoid that by just returning an error code.
            let _ = io.write_err("error: Py_Main cannot be used in this context\n");
            1
        }
    }
}

impl Drop for HgPython {
    fn drop(&mut self) {
        if self.py_initialized_by_us {
            info_span!("Finalize Python").in_scope(|| py_finalize())
        }
    }
}

fn read_to_py_object(py: Python, reader: &dyn clidispatch::io::Read) -> PyObject {
    let any = reader.as_any();
    if let Some(_) = any.downcast_ref::<std::io::Stdin>() {
        // The Python code accepts None, and will use its default input stream.
        py.None()
    } else if let Some(obj) = any.downcast_ref::<WrappedIO>() {
        obj.0.clone_ref(py)
    } else {
        unimplemented!(
            "converting non-stdio Read ({}) from Rust to Python is not implemented",
            reader.type_name()
        )
    }
}

fn write_to_py_object(py: Python, writer: &dyn clidispatch::io::Write) -> PyObject {
    let any = writer.as_any();
    if let Some(_) = any.downcast_ref::<std::io::Stdout>() {
        py.None()
    } else if let Some(_) = any.downcast_ref::<std::io::Stderr>() {
        py.None()
    } else if let Some(obj) = any.downcast_ref::<WrappedIO>() {
        obj.0.clone_ref(py)
    } else {
        unimplemented!(
            "converting non-stdio Write ({}) from Rust to Python is not implemented",
            writer.type_name()
        )
    }
}

fn init_bindings_commands(py: Python, package: &str) -> PyResult<PyModule> {
    // Called by chg or "-t.py" tests.
    fn run_py(
        py: Python,
        args: Vec<String>,
        fin: Option<PyObject>,
        fout: Option<PyObject>,
        ferr: Option<PyObject>,
    ) -> PyResult<i32> {
        if let (Some(fin), Some(fout), Some(ferr)) = (fin, fout, ferr) {
            let fin = wrap_pyio(fin);
            let fout = wrap_pyio(fout);
            let ferr = wrap_pyio(ferr);
            let old_io = IO::main();
            let io = IO::new(fin, fout, Some(ferr));
            io.set_main();
            let result = Ok(crate::run_command(args, &io));
            if let Ok(old_io) = old_io {
                old_io.set_main();
            }
            result
        } else {
            // Reuse the main IO.
            let io = IO::main().map_pyerr(py)?;
            Ok(crate::run_command(args, &io))
        }
    }

    fn table_py(py: Python) -> PyResult<PyDict> {
        let table = commands::table();
        let py_table: PyDict = PyDict::new(py);
        for def in table.values() {
            let doc = Str::from(Bytes::from(def.doc().to_string()));

            // Key entry by primary command name which Python knows to
            // look for. This avoids having to make the alias list
            // match exactly between Python and Rust.
            let primary_name = def.aliases().split('|').next().unwrap();

            if let Some(synopsis) = def
                .synopsis()
                .map(|s| Str::from(Bytes::from(s.to_string())))
            {
                py_table.set_item(py, primary_name, (doc, def.flags(), synopsis))?;
            } else {
                py_table.set_item(py, primary_name, (doc, def.flags()))?;
            }
        }
        Ok(py_table)
    }

    let name = [package, "commands"].join(".");
    let m = PyModule::new(py, &name)?;
    m.add(
        py,
        "run",
        py_fn!(
            py,
            run_py(
                args: Vec<String>,
                fin: Option<PyObject> = None,
                fout: Option<PyObject> = None,
                ferr: Option<PyObject> = None,
            )
        ),
    )?;
    m.add(py, "table", py_fn!(py, table_py()))?;
    Ok(m)
}

/// Prepare builtin modules. Namely, `bindings`.
///
/// This makes sure the bindings use the same dependencies as the main
/// executable. For example, the global instance in `blackbox` is the
/// same, so if the Rust code logs to blackbox, the Python code can read
/// them out via bindings.
///
/// This is more difficult if the bindings project is compiled as a separate
/// Python extension, because `blackbox` will be compiled separately, and
/// the global instance might be different.
pub fn prepare_builtin_modules(py: Python<'_>, module: &PyModule) -> PyResult<()> {
    let span = debug_span!("Initialize bindings");
    let _guard = span.enter();

    // Prepare `bindings.command`. This cannot be done in the bindings
    // crate because it forms a circular dependency.
    module.add(
        py,
        "commands",
        init_bindings_commands(py, module.name(py)?)?,
    )?;
    bindings::populate_module(py, &module)?;
    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.
 */

pub mod commands;
mod hgpython;
mod python;
mod run;

pub use run::run_command;

pub use crate::hgpython::prepare_builtin_modules;
pub use crate::hgpython::HgPython;


/*
 * 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::ffi::CString;

use ffi::PyEval_InitThreads;
use ffi::PyGILState_Ensure;
use ffi::PySys_SetArgv;
use ffi::PyUnicode_AsWideCharString;
use ffi::PyUnicode_FromString;
use ffi::Py_DECREF;
use ffi::Py_Finalize;
use ffi::Py_InitializeEx;
use ffi::Py_IsInitialized;
use ffi::Py_Main;
use ffi::Py_SetProgramName;
use libc::c_int;
use libc::wchar_t;
use python3_sys as ffi;

type PyChar = wchar_t;

fn to_py_str(s: &str) -> *mut PyChar {
    let c_str = CString::new(s).unwrap();

    unsafe {
        let unicode_obj = PyUnicode_FromString(c_str.as_ptr());
        assert!(!unicode_obj.is_null());

        let py_str = PyUnicode_AsWideCharString(unicode_obj, std::ptr::null_mut());

        Py_DECREF(unicode_obj);

        py_str
    }
}

fn to_py_argv(args: &[String]) -> Vec<*mut PyChar> {
    let mut argv: Vec<_> = args.iter().map(|x| to_py_str(x)).collect();
    argv.push(std::ptr::null_mut());
    argv
}

pub fn py_set_argv(args: &[String]) {
    let mut argv = to_py_argv(args);
    unsafe {
        // This inserts argv[0] path to sys.path, useful for running local builds.
        PySys_SetArgv((argv.len() - 1) as c_int, argv.as_mut_ptr());
    }
}

pub fn py_main(args: &[String]) -> u8 {
    let mut argv = to_py_argv(args);
    unsafe {
        let argc = (argv.len() - 1) as c_int;
        // Py_Main may not return.
        Py_Main(argc, argv.as_mut_ptr()) as u8
    }
}

pub fn py_set_program_name(name: &str) {
    unsafe {
        Py_SetProgramName(to_py_str(name));
    }
}

pub fn py_initialize() {
    unsafe {
        // Avoid overriding exiting Ctrl+C signal handlers.
        Py_InitializeEx(0);
    }
}

pub fn py_is_initialized() -> bool {
    unsafe { Py_IsInitialized() != 0 }
}

pub fn py_finalize() {
    unsafe {
        // Stop other threads from running Python logic.
        //
        // During Py_Finalize, other Python threads might pthread_exit and funny things might
        // happen with rust-cpython's unwind protection.  Example SIGABRT stacks:
        //
        // Thread 1 (exiting):
        // #0  ... in raise () from /lib64/libc.so.6
        // #1  ... in abort () from /lib64/libc.so.6
        // #2  ... in __libc_message () from /lib64/libc.so.6
        // #3  ... in __libc_fatal () from /lib64/libc.so.6
        // #4  ... in unwind_cleanup () from /lib64/libpthread.so.0
        // #5  ... in panic_unwind::real_imp::cleanup ()
        //     at library/panic_unwind/src/gcc.rs:78
        // #6  panic_unwind::__rust_panic_cleanup ()
        //     at library/panic_unwind/src/lib.rs:100
        // #7  ... in std::panicking::try::cleanup ()
        //     at library/std/src/panicking.rs:360
        // #8  ... in std::panicking::try::do_catch (data=<optimized out>, payload=... "\000")
        //     at library/std/src/panicking.rs:404
        // #9  std::panicking::try (f=...) at library/std/src/panicking.rs:343
        // #10 ... in std::panic::catch_unwind (f=...)
        //     at library/std/src/panic.rs:396
        // #11 cpython::function::handle_callback (_location=..., _c=..., f=...)
        //     at cpython-0.5.1/src/function.rs:216
        // #12 ... in pythreading::RGeneratorIter::create_instance::TYPE_OBJECT::wrap_unary (slf=...)
        //     at cpython-0.5.1/src/py_class/slots.rs:318
        // #13 ... in builtin_next () from /lib64/libpython3.6m.so.1.0
        // #14 ... in call_function () from /lib64/libpython3.6m.so.1.0
        // #15 ... in _PyEval_EvalFrameDefault () from /lib64/libpython3.6m.so.1.0
        // ....
        // #32 ... in PyObject_Call () from /lib64/libpython3.6m.so.1.0
        // #33 ... in t_bootstrap () from /lib64/libpython3.6m.so.1.0
        // #34 ... in pythread_wrapper () from /lib64/libpython3.6m.so.1.0
        // #35 ... in start_thread () from /lib64/libpthread.so.0
        // #36 ... in clone () from /lib64/libc.so.6
        //
        // Thread 2:
        // #0  ... in _int_free () from /lib64/libc.so.6
        // #1  ... in code_dealloc () from /lib64/libpython3.6m.so.1.0
        // #2  ... in func_dealloc () from /lib64/libpython3.6m.so.1.0
        // #3  ... in PyObject_ClearWeakRefs () from /lib64/libpython3.6m.so.1.0
        // #4  ... in subtype_dealloc () from /lib64/libpython3.6m.so.1.0
        // #5  ... in insertdict () from /lib64/libpython3.6m.so.1.0
        // #6  ... in _PyModule_ClearDict () from /lib64/libpython3.6m.so.1.0
        // #7  ... in PyImport_Cleanup () from /lib64/libpython3.6m.so.1.0
        // #8  ... in Py_FinalizeEx () from /lib64/libpython3.6m.so.1.0
        // #9  ... in hgcommands::python::py_finalize ()
        // ....
        // #15 ... in hgmain::main () eden/scm/exec/hgmain/src/main.rs:81
        //
        // (The SIGABRT was triggered by running test-fb-ext-fastlog.t)
        PyGILState_Ensure();
        Py_Finalize();
    }
}

pub fn py_init_threads() {
    unsafe {
        PyEval_InitThreads();
    }
}


/*
 * 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::env;
use std::fs::File;
use std::io;
use std::io::BufWriter;
use std::io::Write;
use std::ops::Deref;
use std::ops::DerefMut;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering::SeqCst;
use std::sync::Arc;
use std::sync::Weak;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use std::time::SystemTime;

use anyhow::Result;
use blackbox::serde_json;
use clidispatch::dispatch;
use clidispatch::dispatch::Dispatcher;
use clidispatch::errors;
use clidispatch::global_flags::HgGlobalOpts;
use clidispatch::io::IsTty;
use clidispatch::io::IO;
use configloader::config::ConfigSet;
use configmodel::Config;
use configmodel::ConfigExt;
use fail::FailScenario;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use progress_model::Registry;
use repo::repo::Repo;
use tracing::dispatcher;
use tracing::dispatcher::Dispatch;
use tracing::metadata::LevelFilter;
use tracing::Level;
use tracing_collector::TracingData;
use tracing_sampler::SamplingLayer;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::fmt::Layer as FmtLayer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::Layer;

use crate::commands;
use crate::HgPython;

/// Run a Rust or Python command.
///
/// Have side effect on `io` and return the command exit code.
pub fn run_command(args: Vec<String>, io: &IO) -> i32 {
    let start_time = SystemTime::now();

    // The pfcserver does not want tracing or blackbox or ctrlc setup,
    // or going through the Rust command table. Bypass them.
    if args.get(1).map(|s| s.as_ref()) == Some("start-pfc-server") {
        return HgPython::new(&args).run_hg(args, io, &ConfigSet::new());
    }

    // Initialize NodeIpc:
    // - Before spawning threads, since unsetenv (3) is MT-unsafe.
    // - After pfc-server, since we don't pfc-server to consume the IPC.
    // - Before debugpython, since it might be useful for Python logic.
    setup_nodeipc();

    // Skip initialization for debugpython. Make it closer to vanilla Python.
    if args.get(1).map(|s| s.as_str()) == Some("debugpython") {
        // naive command-line parsing: strip "--".
        let rest_args = if args.get(2).map(|s| s.as_str()) == Some("--") {
            &args[3..]
        } else {
            &args[2..]
        };
        let args: Vec<String> = std::iter::once("hgpython".to_string())
            .chain(rest_args.iter().cloned())
            .collect();
        let mut hgpython = HgPython::new(&args);
        return hgpython.run_python(&args, io) as i32;
    }

    // Extra initialization based on global flags.
    let global_opts = dispatch::parse_global_opts(&args[1..]).ok();

    // Setup tracing early since "log_start" will use it immediately.
    // The tracing clock starts ticking from here.
    let tracing_data = match setup_tracing(&global_opts, io) {
        Err(_) => {
            // With our current architecture it is common to see this path in our tests due to
            // trying to set a global collector a second time. Ignore the error and return some
            // dummy values. FIXME!
            Arc::new(Mutex::new(TracingData::new()))
        }
        Ok(res) => res,
    };

    // Do important finalization tasks (even when ctrl-C'd).
    setup_atexit(start_time);

    setup_ctrlc();

    let scenario = setup_fail_points();
    setup_eager_repo();

    // This is intended to be "process start". "exec/hgmain" seems to be
    // a better place for it. However, chg makes it tricky. Because if hgmain
    // decides to use chg, then there is no way to figure out which `blackbox`
    // to write to, because the repo initialization logic happened in another
    // process (a forked chg server).
    //
    // Having "run_command" here will make it logged by the forked chg server,
    // which is a bit more desirable. Since run_command is very close to process
    // start, it should reflect the duration of the command relatively
    // accurately, at least for non-chg cases.
    let span = log_start(args.clone(), start_time);

    // Ad-hoc environment variable: EDENSCM_TRACE_OUTPUT. A more standard way
    // to access the data is via the blackbox interface.
    let trace_output_path = match identity::debug_env_var("TRACE_OUTPUT") {
        Some((var_name, var_value)) => {
            // Unset environment variable so processes forked by this command
            // wouldn't rewrite the trace.
            std::env::remove_var(var_name);
            Some(var_value)
        }
        None => None,
    };

    let in_scope = Arc::new(()); // Used to tell progress rendering thread to stop.

    metrics_render::init_from_env(Arc::downgrade(&in_scope));

    let exit_code = (|| {
        let cwd = match current_dir(io) {
            Err(e) => {
                let _ = io.write_err(format!("abort: cannot get current directory: {}\n", e));
                return exitcode::IOERR;
            }
            Ok(dir) => dir,
        };

        match dispatch::Dispatcher::from_args(args.clone()) {
            Ok(dispatcher) => {
                let _guard = span.enter();

                sampling::init(dispatcher.config());

                dispatch_command(io, dispatcher, cwd, Arc::downgrade(&in_scope), start_time)
            }
            Err(err) => {
                errors::print_error(&err, io, &args[1..]);
                255
            }
        }
    })();

    drop(in_scope);

    let _ = maybe_write_trace(io, &tracing_data, trace_output_path);

    log_end(exit_code as u8, start_time, tracing_data, &span);

    // Sync the blackbox before returning: this exit code is going to be used to process::exit(),
    // so we need to flush now.
    blackbox::sync();

    if let Some(scenario) = scenario {
        scenario.teardown();
        FAIL_SETUP.store(false, SeqCst);
    }

    exit_code
}

fn dispatch_command(
    io: &IO,
    mut dispatcher: Dispatcher,
    cwd: PathBuf,
    in_scope: Weak<()>,
    start_time: SystemTime,
) -> i32 {
    log_repo_path_and_exe_version(dispatcher.repo());

    let run_logger =
        match runlog::Logger::from_repo(dispatcher.repo(), dispatcher.args()[1..].to_vec()) {
            Ok(logger) => Some(logger),
            Err(err) => {
                let _ = io.write_err(format!("Error creating runlogger: {}\n", err));
                None
            }
        };

    setup_http(dispatcher.global_opts());

    let _ = spawn_progress_thread(
        dispatcher.config(),
        dispatcher.global_opts(),
        io,
        run_logger.clone(),
        in_scope,
    );

    let table = commands::table();

    let (command, dispatch_res) = dispatcher.run_command(&table, io);

    let config = dispatcher.config();

    let mut fell_back = false;
    let exit_code = match dispatch_res
        .map_err(|err| errors::triage_error(config, err, command.map(|c| c.main_alias())))
    {
        Ok(exit_code) => exit_code as i32,
        Err(err) => {
            let should_fallback = err.is::<errors::FallbackToPython>() ||
                // XXX: Right now the Rust command table does not have all Python
                // commands. Therefore Rust "UnknownCommand" needs a fallback.
                //
                // Ideally the Rust command table has Python command information and
                // there is no fallback path (ex. all commands are in Rust, and the
                // Rust implementation might just call into Python cmdutil utilities).
                err.is::<errors::UnknownCommand>();
            let failed_fallback = err.is::<errors::FailedFallbackToPython>();

            if failed_fallback {
                197
            } else if should_fallback {
                fell_back = true;
                // Change the current dir back to the original so it is not surprising to the Python
                // code.
                let _ = env::set_current_dir(cwd);

                let mut interp = HgPython::new(dispatcher.args());
                if dispatcher.global_opts().trace {
                    // Error is not fatal.
                    let _ = interp.setup_tracing("*".into());
                }
                interp.run_hg(dispatcher.args().to_vec(), io, config)
            } else {
                errors::print_error(&err, io, &dispatcher.args()[1..]);
                255
            }
        }
    };

    if !fell_back {
        if let Some(command) = command {
            let hooks_with_prefix =
                |prefix: String| -> Vec<minibytes::Text> { config.keys_prefixed("hooks", &prefix) };
            let mut hooks = hooks_with_prefix(format!("pre-{}.", command.main_alias()));
            if exit_code > 0 {
                let mut names = hooks_with_prefix(format!("fail-{}.", command.main_alias()));
                hooks.append(&mut names);
            } else {
                let mut names = hooks_with_prefix(format!("post-{}.", command.main_alias()));
                hooks.append(&mut names);
            }

            if !hooks.is_empty() {
                let _ = io.write_err(format!(
                    "WARNING: The following hooks were not run: {:?}\n",
                    hooks
                ));
            }
        }
        if io.wait_pager().is_err() {
            return 255;
        }
    }

    // Clean up progress models.
    Registry::main().remove_orphan_models();

    if let Some(rl) = &run_logger {
        // Retry a couple times on Windows since this will fail if someone is
        // reading the file, and it is relatively important to write the final
        // runlog entry since it contains the exit code and exit time.
        let tries = if cfg!(windows) { 3 } else { 1 };
        for i in 0..tries {
            if i > 0 {
                thread::sleep(Duration::from_millis(1));
            }

            match rl.close(exit_code) {
                Ok(()) => break,
                Err(err) => {
                    if i == tries - 1 {
                        tracing::error!(target: "runlog", ?err, "error closing runlog")
                    } else {
                        tracing::warn!(target: "runlog", ?err, "error closing runlog")
                    }
                }
            };
        }
    }

    let _ = log_perftrace(io, config, start_time);

    exit_code
}

/// Similar to `std::env::current_dir`. But does some extra things:
/// - Attempt to autofix issues when running under a typical shell (which
///   sets $PWD), and a directory is deleted and then recreated.
fn current_dir(io: &IO) -> io::Result<PathBuf> {
    let result = env::current_dir();
    if let Err(ref err) = result {
        match err.kind() {
            io::ErrorKind::NotConnected | io::ErrorKind::NotFound => {
                // For those errors, attempt to fix it by `cd $PWD`.
                // - NotConnected: edenfsctl stop; edenfsctl start
                // - NotFound: rmdir $PWD; mkdir $PWD
                if let Ok(pwd) = env::var("PWD") {
                    if env::set_current_dir(pwd).is_ok() {
                        let _ = io.write_err("(warning: the current directory was recreated; consider running 'cd $PWD' to fix your shell)\n");
                        return env::current_dir();
                    }
                }
            }
            _ => {}
        }
    }
    result
}

/// Make tracing write logs to `io` if `LOG` environment is set.
/// Return `true` if it is set, or `false` if nothing happens.
///
/// `collector` is used to integrate with the `TracingCollector`,
/// which can integrate with Python via bindings.
fn setup_tracing_io(
    io: &IO,
    collector: Option<tracing_collector::TracingCollector>,
) -> Result<bool> {
    let is_test = is_inside_test();
    let mut env_filter_dirs: Option<String> = identity::debug_env_var("LOG").map(|v| v.1);

    // Ensure EnvFilter is used in tests so it can be changed on the fly.
    if is_test && env_filter_dirs.is_none() {
        env_filter_dirs = Some(String::new());
    }

    if let Some(dirs) = env_filter_dirs {
        // Apply "reload" side effects first.
        let error = io.error();
        let can_color = error.can_color();
        tracing_reload::update_writer(Box::new(error));
        tracing_reload::update_env_filter_directives(&dirs)?;

        // This might error out if called 2nd time per process.
        let env_filter = tracing_reload::reloadable_env_filter()?;

        let env_logger = FmtLayer::new()
            .with_span_events(FmtSpan::ACTIVE)
            .with_ansi(can_color)
            .with_writer(tracing_reload::reloadable_writer);
        if is_test {
            // In tests, disable color and timestamps for cleaner output.
            let env_logger = env_logger.without_time().with_ansi(false);
            match collector {
                None => {
                    let subscriber = tracing_subscriber::Registry::default()
                        .with(env_logger.with_filter(env_filter))
                        .with(SamplingLayer::new());
                    tracing::subscriber::set_global_default(subscriber)?;
                }
                Some(collector) => {
                    let subscriber = tracing_subscriber::Registry::default()
                        .with(collector.and_then(env_logger).with_filter(env_filter))
                        .with(SamplingLayer::new());
                    tracing::subscriber::set_global_default(subscriber)?;
                }
            };
        } else {
            match collector {
                None => {
                    let subscriber = tracing_subscriber::Registry::default()
                        .with(env_logger.with_filter(env_filter))
                        .with(SamplingLayer::new());
                    tracing::subscriber::set_global_default(subscriber)?;
                }
                Some(collector) => {
                    let subscriber = tracing_subscriber::Registry::default()
                        .with(collector.and_then(env_logger).with_filter(env_filter))
                        .with(SamplingLayer::new());
                    tracing::subscriber::set_global_default(subscriber)?;
                }
            }
        }
        Ok(true)
    } else {
        Ok(false)
    }
}

fn setup_tracing(global_opts: &Option<HgGlobalOpts>, io: &IO) -> Result<Arc<Mutex<TracingData>>> {
    // Setup TracingData singleton (currently owned by pytracing).
    {
        let mut data = pytracing::DATA.lock();
        // Only recreate TracingData if pid has changed (ex. chgserver's case
        // where it forks and runs commands - we want to log to different
        // blackbox trace events).  This makes it possible to use multiple
        // `run()`s in a single process
        if data.process_id() != unsafe { libc::getpid() } as u64 {
            *data.deref_mut() = TracingData::new();
        }
    }
    let data = pytracing::DATA.clone();

    let collector = tracing_collector::TracingCollector::new(data.clone());
    if !setup_tracing_io(io, Some(collector))? {
        let level = identity::debug_env_var("TRACE_LEVEL")
            .map(|v| v.1)
            .and_then(|s| Level::from_str(&s).ok())
            .unwrap_or_else(|| {
                if let Some(opts) = global_opts {
                    if opts.trace {
                        return Level::DEBUG;
                    }
                }
                Level::INFO
            });

        let collector = tracing_collector::TracingCollector::new(data.clone());
        let subscriber = tracing_subscriber::Registry::default()
            .with(collector.with_filter::<LevelFilter>(level.into()))
            .with(SamplingLayer::new());
        tracing::subscriber::set_global_default(subscriber)?;
    }

    Ok(data)
}

fn spawn_progress_thread(
    config: &ConfigSet,
    global_opts: &HgGlobalOpts,
    io: &IO,
    run_logger: Option<Arc<runlog::Logger>>,
    in_scope: Weak<()>,
) -> Result<()> {
    // See 'hg help config.progress' for the config options.
    let mut disable_rendering = false;

    if config.get_or("progress", "disable", || false)? {
        disable_rendering = true;
    }

    let assume_tty = config.get_or("progress", "assume-tty", || false)?;
    if !assume_tty && !io.error().is_tty() {
        disable_rendering = true;
    }

    if global_opts.quiet || global_opts.debug || hgplain::is_plain(Some("progress")) {
        disable_rendering = true;
    }

    let render_function = progress_render::simple::render;
    let renderer_name = config.get_or("progress", "renderer", || "rust:simple".to_string())?;
    if renderer_name == "none" {
        disable_rendering = true;
    }

    let interval = Duration::from_secs_f64(config.get_or("progress", "refresh", || 0.1)?)
        .max(Duration::from_millis(50));

    // lockstep is used by tests to control progress rendering run loop.
    let lockstep = config.get_or("progress", "lockstep", || false)?;

    // Limit how often we write runlog. This config knob is primarily for tests to lower.
    let runlog_interval =
        Duration::from_secs_f64(config.get_or("runlog", "progress-refresh", || 0.5)?).max(interval);

    let progress = io.progress();

    let mut config = progress_render::RenderingConfig {
        delay: Duration::from_secs_f64(config.get_or("progress", "delay", || 3.0)?),
        term_width: progress.term_size().0,
        ..Default::default()
    };

    let registry = Registry::main();

    hg_http::enable_progress_reporting();

    // Not fatal if we cannot spawn the progress rendering thread.
    let thread_name = "rust-progress".to_string();
    let _ = thread::Builder::new().name(thread_name).spawn(move || {
        let mut last_changes = Vec::new();
        let mut last_runlog_time: Option<Instant> = None;

        while Weak::upgrade(&in_scope).is_some() {
            let now = Instant::now();

            if lockstep {
                registry.wait();
            }

            if !disable_rendering {
                config.term_width = progress.term_size().0;

                let changes = (render_function)(registry, &config);
                if changes != last_changes {
                    // This might block (so we use a thread, not an async task)
                    let _ = progress.set(&changes);
                    last_changes = changes;
                }
            }

            if let Some(run_logger) = &run_logger {
                if last_runlog_time.map_or(true, |i| now - i >= runlog_interval) {
                    let progress = registry
                        .list_progress_bar()
                        .into_iter()
                        .map(runlog::Progress::new)
                        .collect();

                    if let Err(err) = run_logger.update_progress(progress) {
                        tracing::warn!(target: "runlog", ?err, "error updating runlog progress");
                    }

                    last_runlog_time = Some(now);
                }
            }

            registry.remove_orphan_progress_bar();

            if !lockstep {
                thread::sleep(interval);
            }
        }
    });

    Ok(())
}

fn maybe_write_trace(
    io: &IO,
    tracing_data: &Arc<Mutex<TracingData>>,
    path: Option<String>,
) -> Result<()> {
    // Write ASCII or TraceEvent JSON (or gzipped JSON) to the specified path.
    if let Some(path) = path {
        // A hardcoded minimal duration (in microseconds).
        let data = tracing_data.lock();
        match write_trace(io, &path, &data) {
            Ok(_) => io.write_err(format!("(Trace was written to {})\n", &path))?,
            Err(err) => {
                io.write_err(format!("(Failed to write Trace to {}: {})\n", &path, &err))?
            }
        }
    }
    Ok(())
}

pub(crate) fn write_trace(io: &IO, path: &str, data: &TracingData) -> Result<()> {
    enum Format {
        ASCII,
        TraceEventJSON,
        TraceEventGzip,
        SpansJSON,
    }

    let format = if path.ends_with(".txt") {
        Format::ASCII
    } else if path.ends_with("spans.json") {
        Format::SpansJSON
    } else if path.ends_with(".json") {
        Format::TraceEventJSON
    } else if path.ends_with(".gz") {
        Format::TraceEventGzip
    } else {
        Format::ASCII
    };

    let mut out: Box<dyn Write> = if path == "-" || path.is_empty() {
        Box::new(io.error())
    } else {
        Box::new(BufWriter::new(File::create(&path)?))
    };

    match format {
        Format::ASCII => {
            let mut ascii_opts = tracing_collector::model::AsciiOptions::default();
            ascii_opts.min_duration_parent_percentage_to_show = 10;
            ascii_opts.min_duration_micros_to_hide = 100000;
            out.write_all(data.ascii(&ascii_opts).as_bytes())?;
            out.flush()?;
        }
        Format::SpansJSON => {
            let spans = data.tree_spans::<&str>();
            serde_json::to_writer(&mut out, &spans)?;
            out.flush()?;
        }
        Format::TraceEventGzip => {
            let mut out = Box::new(flate2::write::GzEncoder::new(
                out,
                flate2::Compression::new(6), // 6 is the default value
            ));
            data.write_trace_event_json(&mut out, Default::default())?;
            out.finish()?.flush()?;
        }
        Format::TraceEventJSON => {
            data.write_trace_event_json(&mut out, Default::default())?;
            out.flush()?;
        }
    }

    Ok(())
}

fn log_start(args: Vec<String>, now: SystemTime) -> tracing::Span {
    let inside_test = is_inside_test();
    let (uid, pid, nice) = if inside_test {
        (0, 0, 0)
    } else {
        #[cfg(unix)]
        unsafe {
            (
                libc::getuid() as u32,
                libc::getpid() as u32,
                libc::nice(0) as i32,
            )
        }

        #[cfg(not(unix))]
        unsafe {
            // uid and nice are not aviailable on Windows.
            (0, libc::getpid() as u32, 0)
        }
    };

    if let Some((_, tags)) = identity::debug_env_var("BLACKBOX_TAGS") {
        tracing::info!(name = "blackbox_tags", tags = AsRef::<str>::as_ref(&tags));
        let names: Vec<String> = tags.split_whitespace().map(ToString::to_string).collect();
        blackbox::log(&blackbox::event::Event::Tags { names });
    }

    let mut parent_names = Vec::new();
    let mut parent_pids = Vec::new();
    // On Windows, getting the ppid and exe name requires `CreateToolhelp32Snapshot`,
    // which can take hundreds of milliseconds. So we skip doing that here.
    if !inside_test && !cfg!(windows) {
        let mut ppid = procinfo::parent_pid(0);
        // In theory, the OS should not report a cyclic process graph (ex. pid 1
        // has parent pid = 1). Practically `parent_pids` takes snapshots
        // everytime on Windows (unnecessarily) and is subject to races. Be
        // extra careful here so the loop wouldn't be infinite.
        while ppid != 0 && parent_pids.len() < 16 && !parent_pids.contains(&ppid) {
            let name = procinfo::exe_name(ppid);
            parent_names.push(name);
            parent_pids.push(ppid);
            ppid = procinfo::parent_pid(ppid);
        }
    }

    let span = tracing::info_span!(
        "Run Command",
        pid = pid,
        uid = uid,
        nice = nice,
        args = AsRef::<str>::as_ref(&serde_json::to_string(&args).unwrap()),
        parent_pids = AsRef::<str>::as_ref(&serde_json::to_string(&parent_pids).unwrap()),
        parent_names = AsRef::<str>::as_ref(&serde_json::to_string(&parent_names).unwrap()),
        version = version::VERSION,
        // Reserved for log_end.
        exit_code = 0,
        max_rss = 0,
    );

    blackbox::log(&blackbox::event::Event::Start {
        pid,
        uid,
        nice,
        args,
        timestamp_ms: epoch_ms(now),
    });

    blackbox::log(&blackbox::event::Event::ProcessTree {
        names: parent_names,
        pids: parent_pids,
    });

    span
}

fn log_end(
    exit_code: u8,
    start_time: SystemTime,
    tracing_data: Arc<Mutex<TracingData>>,
    span: &tracing::Span,
) {
    let inside_test = is_inside_test();
    let duration_ms = if inside_test {
        0
    } else {
        match start_time.elapsed() {
            Ok(duration) => duration.as_millis() as u64,
            Err(_) => 0,
        }
    };
    let max_rss = if inside_test {
        0
    } else {
        procinfo::max_rss_bytes()
    };

    span.record("exit_code", &exit_code);
    span.record("max_rss", &max_rss);

    blackbox::log(&blackbox::event::Event::Finish {
        exit_code,
        max_rss,
        duration_ms,
        timestamp_ms: epoch_ms(start_time),
    });

    // Stop sending tracing events to subscribers. This prevents
    // deadlock in this scope.
    dispatcher::with_default(&Dispatch::none(), || {
        // Log tracing data.
        if let Ok(serialized_trace) = {
            let data = tracing_data.lock();
            // Note: if mincode::serialize wants to mutate tracing_data here,
            // it can deadlock if the dispatcher is not Dispatch::none().
            mincode::serialize(&data.deref())
        } {
            if let Ok(compressed) = zstd::stream::encode_all(&serialized_trace[..], 0) {
                let event = blackbox::event::Event::TracingData {
                    serialized: blackbox::event::Binary(compressed),
                };
                blackbox::log(&event);
            }
        }
        blackbox::sync();
    });
}

fn epoch_ms(time: SystemTime) -> u64 {
    match time.duration_since(SystemTime::UNIX_EPOCH) {
        Ok(duration) => duration.as_millis() as u64,
        Err(_) => 0,
    }
}

fn is_inside_test() -> bool {
    std::env::var_os("TESTTMP").is_some()
}

fn log_repo_path_and_exe_version(repo: Option<&Repo>) {
    // The "version" and "repo" fields are consumed by telemetry.
    if let Some(repo) = repo {
        let config = repo.config();
        let opt_path_default: std::result::Result<Option<String>, _> =
            config.get_or_default("paths", "default");
        if let Ok(Some(path_default)) = opt_path_default {
            if let Some(repo_name) = configloader::hg::repo_name_from_url(config, &path_default) {
                tracing::info!(
                    target: "command_info",
                    version = version::VERSION,
                    repo = repo_name.as_str(),
                );
                return;
            }
        }
    }
    tracing::info!(target: "command_info", version = version::VERSION);
}

fn log_perftrace(io: &IO, config: &ConfigSet, start_time: SystemTime) -> Result<()> {
    if let Some(threshold) = config.get_opt::<Duration>("tracing", "threshold")? {
        if let Ok(elapsed) = start_time.elapsed() {
            if elapsed >= threshold {
                let key = format!(
                    "flat/perftrace-{}-{}-{}",
                    hostname::get()?.to_string_lossy(),
                    std::process::id(),
                    (epoch_ms(start_time) as f64) / 1e3,
                );

                let mut ascii_opts = tracing_collector::model::AsciiOptions::default();

                // Minimum resolution = 1% of duration.
                ascii_opts.min_duration_micros_to_hide = (elapsed.as_micros() / 100) as u64;

                let output = pytracing::DATA.lock().ascii(&ascii_opts);

                tracing::info!(target: "perftrace", key=key.as_str(), payload=output.as_str(), "Trace:\n{}\n", output);
                tracing::info!(target: "perftracekey", perftracekey=key.as_str(), "Trace key:\n{}\n", key);

                if config.get_or_default("tracing", "stderr")? {
                    let _ = write!(io.error(), "{}\n", output);
                }
            }
        }
    }

    Ok(())
}

// TODO: Replace this with the 'exitcode' crate once it's available.
mod exitcode {
    pub const IOERR: i32 = 74;
}

fn setup_http(global_opts: &HgGlobalOpts) {
    if global_opts.insecure {
        hg_http::enable_insecure_mode();
    }
}

fn setup_eager_repo() {
    static REGISTERED: Lazy<()> = Lazy::new(|| {
        edenapi::Builder::register_customize_build_func(eagerepo::edenapi_from_config)
    });

    *REGISTERED
}

static FAIL_SETUP: AtomicBool = AtomicBool::new(false);

fn setup_fail_points<'a>() -> Option<FailScenario<'a>> {
    if std::env::var("FAILPOINTS").is_err() {
        // No need to setup failpoints.
        return None;
    }
    if FAIL_SETUP.fetch_or(true, SeqCst) {
        // Already setup.
        None
    } else {
        Some(FailScenario::setup())
    }
}

fn setup_atexit(start_time: SystemTime) {
    atexit::AtExit::new(Box::new(move || {
        let duration_ms = match start_time.elapsed() {
            Ok(duration) => duration.as_millis() as u64,
            Err(_) => 0,
        };

        // Truncate duration to top three significant decimal digits of
        // precision to reduce cardinality for logging storage.
        tracing::debug!(target: "measuredtimes", command_duration=util::math::truncate_int(duration_ms, 3));

        // Make extra sure our metrics are written out.
        sampling::flush();
    })).queued();
}

fn setup_ctrlc() {
    // ctrlc with the "termination" feature would register SIGINT, SIGTERM and
    // SIGHUP handlers.
    //
    // If you change this function, ensure to check Ctrl+C and SIGTERM works for
    // these cases:
    // - Python, native code released GIL: dbsh -c 'b.sleep(1000, False)'
    // - Python, native code took GIL: dbsh -c 'b.sleep(1000, True)'
    // - Rust: debugracyoutput
    // - Pager, block on `write`: log --pager=always
    // - Pager, block on `wait`: log -r . --pager=always --config pager.interface=full
    let _ = ctrlc::set_handler(|| {
        // Minimal cleanup then just exit. Our main storage (indexedlog,
        // metalog) is SIGKILL-safe, if "finally" (Python) or "Drop" (Rust) does
        // not run, it won't corrupt the repo data.

        // Exit pager to restore terminal states (ex. quit raw mode)
        if let Ok(io) = clidispatch::io::IO::main() {
            let _ = io.quit_pager();
        }

        // Run atexit handlers.
        atexit::drop_queued();

        // "exit" tries to call "Drop"s but we don't rely on "Drop" for data integrity.
        std::process::exit(128 | libc::SIGINT);
    });
}

fn setup_nodeipc() {
    // Trigger `Lazy` initialization.
    let _ = nodeipc::get_singleton();
}