use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::Arc;

use anyhow::{Context, Result};
use prek_consts::env_vars::{EnvVars, EnvVarsRead};
use prek_consts::prepend_paths;
use tracing::debug;

use crate::cli::reporter::HookInstallReporter;
use crate::cli::run::HookRunReporter;
use crate::hook::{Hook, InstallInfo, InstalledHook};
use crate::languages::LanguageBackend;
use crate::process::Cmd;
use crate::run::run_by_batch;
use crate::store::Store;

#[derive(Debug, Copy, Clone)]
pub(crate) struct Conda;

#[async_trait::async_trait(?Send)]
impl LanguageBackend for Conda {
    async fn install(
        &self,
        store: &Store,
        hook: Arc<Hook>,
        reporter: &HookInstallReporter,
    ) -> Result<InstalledHook> {
        let progress = reporter.on_install_start(&hook);

        let mut info = InstallInfo::new(&hook, &store.hooks_dir())?;

        debug!(%hook, target = %info.env_path.display(), "Installing Conda environment");
        let conda = conda_executable();

        if let Some(repo_path) = hook.repo_path() {
            Cmd::new(conda)
                .current_dir(repo_path)
                .arg("create")
                .arg("-p")
                .arg(&info.env_path)
                .arg("--file")
                .arg("environment.yml")
                .check(true)
                .output()
                .await
                .context("Failed to create Conda environment")?;
        } else {
            Cmd::new(conda)
                .arg("create")
                .arg("-p")
                .arg(&info.env_path)
                .check(true)
                .output()
                .await
                .context("Failed to create Conda environment")?;
        }

        if !hook.additional_dependencies.is_empty() {
            let mut install_cmd = Cmd::new(conda);
            install_cmd
                .arg("install")
                .arg("-p")
                .arg(&info.env_path)
                .args(&hook.additional_dependencies);
            if let Some(repo_path) = hook.repo_path() {
                install_cmd.current_dir(repo_path);
            }
            install_cmd
                .check(true)
                .output()
                .await
                .context("Failed to install Conda dependencies")?;
        }

        info.persist_env_path();

        reporter.on_install_complete(progress);

        Ok(InstalledHook::Installed {
            hook,
            info: Arc::new(info),
        })
    }

    async fn check_health(&self, _info: &InstallInfo) -> Result<()> {
        Ok(())
    }

    async fn run(
        &self,
        store: &Store,
        hook: &InstalledHook,
        filenames: &[&Path],
        reporter: &HookRunReporter,
    ) -> Result<(i32, Vec<u8>)> {
        let progress = reporter.on_run_start(hook, filenames.len());

        let env_dir = hook.env_path().expect("Conda must have env path");
        let new_path = conda_path(env_dir).context("Failed to join PATH")?;
        let entry = hook.entry.resolve(Some(&new_path), store)?;

        let run = async |batch: &[&Path]| {
            let output = Cmd::new(&entry[0])
                .current_dir(hook.work_dir())
                .args(&entry[1..])
                .env(EnvVars::PATH, &new_path)
                .env(EnvVars::CONDA_PREFIX, env_dir)
                .env_remove(EnvVars::PYTHONHOME)
                .env_remove(EnvVars::VIRTUAL_ENV)
                .envs(&hook.env)
                .args(&hook.args)
                .file_args(batch)
                .check(false)
                .stdin(Stdio::null())
                .pty_output_with_sink(reporter.output_sink(progress))
                .await?;

            reporter.on_run_progress(progress, batch.len() as u64);

            anyhow::Ok(output)
        };

        let output = run_by_batch(hook, filenames, entry.argv(), run).await?;

        reporter.on_run_complete(progress);

        Ok(output)
    }
}

fn conda_executable() -> &'static str {
    if EnvVars.is_set(EnvVars::PRE_COMMIT_USE_MICROMAMBA) {
        "micromamba"
    } else if EnvVars.is_set(EnvVars::PRE_COMMIT_USE_MAMBA) {
        "mamba"
    } else {
        "conda"
    }
}

fn conda_path(env_path: &Path) -> Result<std::ffi::OsString, std::env::JoinPathsError> {
    let paths = conda_path_dirs(env_path);
    let paths = paths.iter().map(PathBuf::as_path).collect::<Vec<_>>();
    prepend_paths(&paths)
}

fn conda_path_dirs(env_path: &Path) -> Vec<PathBuf> {
    if cfg!(windows) {
        vec![
            env_path.join("Library").join("bin"),
            env_path.join("Scripts"),
            env_path.to_path_buf(),
            env_path.join("bin"),
        ]
    } else {
        vec![env_path.join("bin")]
    }
}


package s3api

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"

	"github.com/TecharoHQ/anubis/lib/store"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

type Store struct {
	s3     S3API
	bucket string
}

func (s *Store) Delete(ctx context.Context, key string) error {
	normKey := strings.ReplaceAll(key, ":", "/")
	// Emulate not found by probing first.
	if _, err := s.s3.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &s.bucket, Key: &normKey}); err != nil {
		return fmt.Errorf("%w: %w", store.ErrNotFound, err)
	}
	if _, err := s.s3.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &s.bucket, Key: &normKey}); err != nil {
		return fmt.Errorf("can't delete from s3: %w", err)
	}
	return nil
}

func (s *Store) Get(ctx context.Context, key string) ([]byte, error) {
	normKey := strings.ReplaceAll(key, ":", "/")
	out, err := s.s3.GetObject(ctx, &s3.GetObjectInput{
		Bucket: &s.bucket,
		Key:    &normKey,
	})
	if err != nil {
		return nil, fmt.Errorf("%w: %w", store.ErrNotFound, err)
	}
	defer out.Body.Close() //nolint:errcheck
	if msStr, ok := out.Metadata["x-anubis-expiry-ms"]; ok && msStr != "" {
		if ms, err := strconv.ParseInt(msStr, 10, 64); err == nil {
			if time.Now().UnixMilli() >= ms {
				_, _ = s.s3.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &s.bucket, Key: &normKey})
				return nil, store.ErrNotFound
			}
		}
	}
	b, err := io.ReadAll(out.Body)
	if err != nil {
		return nil, fmt.Errorf("can't read s3 object: %w", err)
	}
	return b, nil
}

func (s *Store) Set(ctx context.Context, key string, value []byte, expiry time.Duration) error {
	normKey := strings.ReplaceAll(key, ":", "/")
	// S3 has no native TTL; we store object with metadata X-Anubis-Expiry as epoch seconds.
	var meta map[string]string
	if expiry > 0 {
		exp := time.Now().Add(expiry).UnixMilli()
		meta = map[string]string{"x-anubis-expiry-ms": fmt.Sprintf("%d", exp)}
	}
	_, err := s.s3.PutObject(ctx, &s3.PutObjectInput{
		Bucket:   &s.bucket,
		Key:      &normKey,
		Body:     bytes.NewReader(value),
		Metadata: meta,
	})
	if err != nil {
		return fmt.Errorf("can't put s3 object: %w", err)
	}
	return nil
}

func (Store) IsPersistent() bool { return true }


import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ClientContext } from "../types.js";

function clientFlagForCli(ide: string | undefined): string {
  if (!ide) return "";
  const lower = ide.toLowerCase();
  if (lower.includes("cursor")) return "--cursor";
  if (lower.includes("claude")) return "--claude";
  if (lower.includes("codex")) return "--codex";
  if (lower.includes("opencode")) return "--opencode";
  if (lower.includes("gemini")) return "--gemini";
  return "";
}

function buildAuthCommand(
  clientIde: string | undefined,
  transport: "stdio" | "http" | undefined
): string {
  const flag = clientFlagForCli(clientIde);
  const transportFlag = transport === "stdio" ? " --stdio" : "";
  return flag
    ? `npx ctx7 setup ${flag} --mcp${transportFlag} -y`
    : `npx ctx7 setup --mcp${transportFlag}`;
}

function buildElicitMessage(
  clientIde: string | undefined,
  transport: "stdio" | "http" | undefined
): string {
  const command = buildAuthCommand(clientIde, transport);
  return [
    "You're using Context7 anonymously. To unlock free higher rate limits, run this in your terminal:",
    "",
    `    ${command}`,
    "",
    "It opens your browser, signs you in, and writes credentials into your MCP client config.",
    "After it finishes, disable then re-enable the Context7 MCP server in your editor so the new credentials take effect.",
  ].join("\n");
}

// User-facing strings double as enum const values: keeps the schema in the
// simpler `enum: [...]` shape, which clients render more reliably than
// `oneOf` with separate `const`/`title`.
const CHOICE_RUN_SETUP = "I'll run the command to sign in";
const CHOICE_STAY_ANON = "Continue anonymously with smaller limits";

/**
 * Fires a form-mode elicitation that surfaces a sign-in nudge in the client UI
 * when the backend has signaled (via `X-Context7-Auth-Prompt: 1`, captured on
 * `ctx.shouldPrompt` in api.ts) that the anonymous caller should be prompted
 * to authenticate.
 *
 * The message is delivered out-of-band to the human via the client, not into
 * the tool result the LLM reads, so it does not trip prompt-injection guards.
 *
 * The backend owns how often this fires: it sets the header at most once per
 * MCP session, so the server holds no suppression state — it simply shows the
 * dialog whenever the header is present. The command itself is shown in the
 * dialog message for the user to copy; the server does not attempt to drive
 * the client to run it.
 *
 * No-op for authenticated callers, when the signal wasn't set, or when the
 * client did not advertise the `elicitation` capability. Fire-and-forget:
 * never blocks or fails the surrounding tool response.
 */
export function maybeElicitAuthSignIn(server: McpServer, ctx: ClientContext): void {
  if (ctx.apiKey || !ctx.shouldPrompt) return;
  if (!server.server.getClientCapabilities()?.elicitation) return;

  void server.server
    .elicitInput({
      message: buildElicitMessage(ctx.clientInfo?.ide, ctx.transport),
      requestedSchema: {
        type: "object",
        properties: {
          choice: {
            type: "string",
            title: "How would you like to continue?",
            enum: [CHOICE_RUN_SETUP, CHOICE_STAY_ANON],
            default: CHOICE_RUN_SETUP,
          },
        },
        required: ["choice"],
      },
    })
    .catch(() => {
      // Client may not support elicitation despite the capability flag, or
      // the session may have closed before the user responded. Either way,
      // a missed nudge should never affect the tool result.
    });
}


package com.netflix.maestro.validations;

import com.netflix.maestro.models.signal.SignalMatchParam;
import com.netflix.maestro.models.signal.SignalOperator;
import com.netflix.maestro.models.signal.SignalParamValue;
import com.netflix.maestro.models.trigger.SignalTrigger;
import jakarta.validation.ConstraintViolation;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Assert;
import org.junit.Test;

/**
 * Tests for SignalTriggerConstraint class.
 *
 * @author jun-he
 */
public class SignalTriggerConstraintTest extends BaseConstraintTest {
  private static class DummyWorkflow {
    List<@SignalTriggerConstraint SignalTrigger> triggers;

    DummyWorkflow(SignalTrigger trigger) {
      triggers = List.of(trigger);
    }
  }

  @Test
  public void testValid() {
    SignalTrigger trigger1 = new SignalTrigger();
    Assert.assertTrue(validator.validate(new DummyWorkflow(trigger1)).isEmpty());

    SignalTrigger trigger2 = new SignalTrigger();
    trigger2.setDefinitions(Map.of());
    Assert.assertTrue(validator.validate(new DummyWorkflow(trigger2)).isEmpty());

    SignalTrigger trigger3 = new SignalTrigger();
    trigger3.setDefinitions(Map.of("signal_a", new SignalTrigger.SignalTriggerEntry()));
    Assert.assertTrue(validator.validate(new DummyWorkflow(trigger3)).isEmpty());

    SignalTrigger trigger4 = new SignalTrigger();
    var entry4 = new SignalTrigger.SignalTriggerEntry();
    entry4.setJoinKeys(new String[] {"foo"});
    trigger4.setDefinitions(Map.of("signal_a", entry4));
    Assert.assertTrue(validator.validate(new DummyWorkflow(trigger4)).isEmpty());

    SignalTrigger trigger5 = new SignalTrigger();
    var entry5 = new SignalTrigger.SignalTriggerEntry();
    entry5.setMatchParams(
        Map.of(
            "bar",
            SignalMatchParam.builder()
                .value(SignalParamValue.of(123))
                .operator(SignalOperator.EQUALS_TO)
                .build()));
    trigger5.setDefinitions(Map.of("signal_a", entry5));
    Assert.assertTrue(validator.validate(new DummyWorkflow(trigger5)).isEmpty());

    SignalTrigger trigger6 = new SignalTrigger();
    var entry6 = new SignalTrigger.SignalTriggerEntry();
    entry6.setJoinKeys(new String[] {"foo"});
    entry6.setMatchParams(
        Map.of(
            "bar",
            SignalMatchParam.builder()
                .value(SignalParamValue.of(123))
                .operator(SignalOperator.EQUALS_TO)
                .build()));
    trigger6.setDefinitions(Map.of("signal_a", entry6));
    Assert.assertTrue(validator.validate(new DummyWorkflow(trigger6)).isEmpty());
  }

  @Test
  public void testInValidWhenSignalNameSizeOverLimit() {
    SignalTrigger trigger = new SignalTrigger();
    trigger.setDefinitions(
        Map.of(
            "signal_a",
            new SignalTrigger.SignalTriggerEntry(),
            "signal_b",
            new SignalTrigger.SignalTriggerEntry(),
            "signal_c",
            new SignalTrigger.SignalTriggerEntry(),
            "signal_d",
            new SignalTrigger.SignalTriggerEntry(),
            "signal_e",
            new SignalTrigger.SignalTriggerEntry(),
            "signal_f",
            new SignalTrigger.SignalTriggerEntry(),
            "signal_g",
            new SignalTrigger.SignalTriggerEntry(),
            "signal_h",
            new SignalTrigger.SignalTriggerEntry(),
            "signal_i",
            new SignalTrigger.SignalTriggerEntry()));
    Set<ConstraintViolation<DummyWorkflow>> violations =
        validator.validate(new DummyWorkflow(trigger));
    Assert.assertEquals(1, violations.size());
    Assert.assertEquals(
        "[signal-trigger] the signal names within the signal triggers are more than the limit [8]",
        violations.iterator().next().getMessage());
  }

  @Test
  public void testInValidWhenJoinKeysWithDifferentSize() {
    SignalTrigger trigger = new SignalTrigger();
    var entry1 = new SignalTrigger.SignalTriggerEntry();
    entry1.setJoinKeys(new String[] {"foo"});
    var entry2 = new SignalTrigger.SignalTriggerEntry();
    entry2.setJoinKeys(new String[] {"foo", "bar"});
    trigger.setDefinitions(Map.of("signal_a", entry1, "signal_b", entry2));
    Set<ConstraintViolation<DummyWorkflow>> violations =
        validator.validate(new DummyWorkflow(trigger));
    Assert.assertEquals(1, violations.size());
    Assert.assertEquals(
        "[signal-trigger] the join_keys lengths between signals in the signal triggers must be the same",
        violations.iterator().next().getMessage());
  }

  @Test
  public void testInValidWhenJoinKeysUsedForMatch() {
    SignalTrigger trigger = new SignalTrigger();
    var entry = new SignalTrigger.SignalTriggerEntry();
    entry.setJoinKeys(new String[] {"bar"});
    entry.setMatchParams(
        Map.of(
            "bar",
            SignalMatchParam.builder()
                .value(SignalParamValue.of(123))
                .operator(SignalOperator.EQUALS_TO)
                .build()));
    trigger.setDefinitions(Map.of("signal_a", entry));
    Set<ConstraintViolation<DummyWorkflow>> violations =
        validator.validate(new DummyWorkflow(trigger));
    Assert.assertEquals(1, violations.size());
    Assert.assertEquals(
        "[signal-trigger] the join_key [bar] cannot be used in match_params at the same time",
        violations.iterator().next().getMessage());
  }
}


use std::fmt::Write as _;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::{Arc, LazyLock};

use anyhow::{Context, Result};
use futures_util::stream::{FuturesUnordered, StreamExt};
use mea::semaphore::Semaphore;
use owo_colors::OwoColorize;
use prek_consts::env_vars::{EnvVars, EnvVarsRead};
use prek_consts::{PRE_COMMIT_CONFIG_YAML, PREK_TOML};
use prek_identify::{TagSet, tags_from_path};
use rustc_hash::{FxBuildHasher, FxHashMap};
use tracing::{debug, error, trace};
use unicode_width::UnicodeWidthStr;

use crate::cli::reporter::{HookInitReporter, HookInstallReporter};
use crate::cli::run::diff::DiffTracker;
use crate::cli::run::filter::{RunInputMode, stage_uses_message_file_input};
use crate::cli::run::install::{InstallCache, install_hooks};
use crate::cli::run::keeper::WorkTreeKeeper;
use crate::cli::run::{
    CollectOptions, FileSelection, FileTagCache, GroupFilters, HookFileFilter, HookRunReporter,
    ProjectFiles, RunFileIndex, RunInput, Selectors, collect_run_input, project_status_marker,
};
use crate::cli::{ExitStatus, RunExtraArgs};
use crate::config::{PassFilenames, Stage};
use crate::fs::CWD;
use crate::git::GIT_ROOT;
use crate::hook::{Hook, InstalledHook};
use crate::printer::Printer;
use crate::run::{HOOK_CONCURRENCY, USE_COLOR};
use crate::store::Store;
use crate::workspace::{HookInitFilters, Project, Workspace};
use crate::{fs, git, hooks, warn_user};

#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub(crate) async fn run(
    store: &Store,
    config: Option<PathBuf>,
    includes: Vec<String>,
    skips: Vec<String>,
    groups: Vec<String>,
    no_groups: Vec<String>,
    hook_stage: Option<Stage>,
    selection: FileSelection,
    show_diff_on_failure: bool,
    fail_fast: Option<bool>,
    dry_run: bool,
    refresh: bool,
    extra_args: RunExtraArgs,
    verbose: bool,
    printer: Printer,
) -> Result<ExitStatus> {
    // Prevent recursive post-checkout hooks.
    if hook_stage == Some(Stage::PostCheckout)
        && EnvVars.is_set(EnvVars::PREK_INTERNAL__SKIP_POST_CHECKOUT)
    {
        return Ok(ExitStatus::Success);
    }

    // Ensure we are in a git repository.
    LazyLock::force(&GIT_ROOT).as_ref()?;

    let should_stash = selection.requires_clean_worktree();

    // Check if we have unresolved merge conflict files and fail fast.
    if should_stash && git::has_unmerged_paths().await? {
        anyhow::bail!(
            "Found unresolved merge conflicts. Resolve the conflicts, stage the files with `git add`, and try again"
        );
    }

    let workspace_root = Workspace::find_root(config.as_deref(), &CWD)?;
    let selectors = Selectors::load(&includes, &skips, &workspace_root)?;
    let group_filters = GroupFilters::parse(&groups, &no_groups)?;
    let has_group_filters = group_filters.has_filters();
    let workspace = Workspace::discover(store, workspace_root, config, Some(&selectors), refresh)?;

    if should_stash {
        workspace.check_configs_staged().await?;
    }

    let reporter = HookInitReporter::new(printer);
    let hooks = {
        let _lock = store.lock_async().await?;
        store.track_configs(
            workspace
                .projects()
                .iter()
                .map(|project| project.config_file()),
        )?;

        workspace
            .init_hooks(
                store,
                HookInitFilters::new(Some(&selectors), Some(&group_filters)),
                Some(&reporter),
            )
            .await
            .context("Failed to init hooks")?
    };
    let selected_hooks: Vec<_> = hooks
        .into_iter()
        .filter(|h| selectors.matches_hook(h))
        .filter(|h| group_filters.matches_hook(h))
        .map(Arc::new)
        .collect();

    selectors.report_unused();
    group_filters.report_unused();

    if selected_hooks.is_empty() {
        writeln!(
            printer.stderr(),
            "{}: No hooks found after filtering with the given selectors",
            "error".red().bold(),
        )?;
        if selectors.has_project_selectors() {
            writeln!(
                printer.stderr(),
                "\n{} If you just added a new `{}` or `{}`, try rerunning your command with the `{}` flag to rescan the workspace.",
                "hint:".bold().yellow(),
                PREK_TOML.cyan(),
                PRE_COMMIT_CONFIG_YAML.cyan(),
                "--refresh".cyan(),
            )?;
        }
        return Ok(ExitStatus::Failure);
    }

    let (stage_filter, input_mode) =
        infer_stage_and_input_mode(hook_stage, has_group_filters, &selected_hooks, &selectors);
    let filtered_hooks: Vec<Arc<Hook>> = if let Some(stage_filter) = stage_filter {
        selected_hooks
            .iter()
            .filter(|h| h.stages.contains(stage_filter))
            .cloned()
            .collect()
    } else {
        // Group selection without an explicit stage uses normal file input, so
        // hooks that can only consume Git message files cannot run correctly.
        selected_hooks
            .into_iter()
            .filter(|hook| !uses_only_message_file_input(hook))
            .collect()
    };

    if filtered_hooks.is_empty() {
        if let Some(stage) = stage_filter {
            debug!("No hooks found for stage {stage} after filtering, exit early");
        } else {
            warn_user!(
                "all hooks selected by group filters require `commit-msg` or `prepare-commit-msg` stage and were not run; pass `--stage commit-msg` or `--stage prepare-commit-msg` to run them"
            );
            return Ok(ExitStatus::Failure);
        }
        return Ok(ExitStatus::Success);
    }

    debug!(
        "Hooks going to run: {:?}",
        filtered_hooks.iter().map(|h| &h.id).collect::<Vec<_>>()
    );

    // Clear any unstaged changes from the git working directory.
    let mut _guard = None;
    if should_stash {
        _guard = Some(
            WorkTreeKeeper::clean(store, workspace.root())
                .await
                .context("Failed to clean work tree")?,
        );
    }

    let (from_ref, to_ref) = selection.refs();
    set_env_vars(from_ref, to_ref, &extra_args);

    let input = collect_run_input(
        workspace.root(),
        CollectOptions {
            input_mode,
            selection,
            commit_msg_filename: extra_args.commit_msg_filename,
        },
    )
    .await
    .context("Failed to collect files")?;

    // Change to the workspace root directory.
    std::env::set_current_dir(workspace.root()).with_context(|| {
        format!(
            "Failed to change directory to `{}`",
            workspace.root().display()
        )
    })?;

    let file_index = RunFileIndex::new(&input, workspace.all_projects());
    let installed_hooks = ensure_hooks_installed(
        store,
        printer,
        &workspace,
        &input,
        &file_index,
        &filtered_hooks,
    )
    .await?;

    run_hooks(
        &workspace,
        &input,
        &file_index,
        &installed_hooks,
        store,
        show_diff_on_failure,
        fail_fast,
        dry_run,
        should_stash,
        verbose,
        printer,
    )
    .await
}

fn infer_stage_and_input_mode(
    explicit_stage: Option<Stage>,
    has_group_filters: bool,
    selected_hooks: &[Arc<Hook>],
    selectors: &Selectors,
) -> (Option<Stage>, RunInputMode) {
    if let Some(stage) = explicit_stage {
        return (Some(stage), RunInputMode::from(stage));
    }

    if has_group_filters {
        return (None, RunInputMode::Files);
    }

    // Preserve legacy direct-hook execution: try `manual` only when the user
    // named hooks directly and none of those hooks can run as `pre-commit`.
    let stage = if selectors.includes_only_hook_targets()
        && !selected_hooks
            .iter()
            .any(|hook| hook.stages.contains(Stage::PreCommit))
    {
        Stage::Manual
    } else {
        Stage::PreCommit
    };
    (Some(stage), RunInputMode::from(stage))
}

fn uses_only_message_file_input(hook: &Hook) -> bool {
    !hook.stages.is_empty() && hook.stages.iter().all(stage_uses_message_file_input)
}

// `pre-commit` sets these environment variables for other git hooks.
fn set_env_vars(from_ref: Option<&str>, to_ref: Option<&str>, args: &RunExtraArgs) {
    unsafe {
        std::env::set_var("PRE_COMMIT", "1");

        if let Some(source) = &args.prepare_commit_message_source {
            std::env::set_var("PRE_COMMIT_COMMIT_MSG_SOURCE", source);
        }
        if let Some(object) = &args.commit_object_name {
            std::env::set_var("PRE_COMMIT_COMMIT_OBJECT_NAME", object);
        }
        if let Some(from_ref) = from_ref {
            std::env::set_var("PRE_COMMIT_ORIGIN", from_ref);
            std::env::set_var("PRE_COMMIT_FROM_REF", from_ref);
        }
        if let Some(to_ref) = to_ref {
            std::env::set_var("PRE_COMMIT_SOURCE", to_ref);
            std::env::set_var("PRE_COMMIT_TO_REF", to_ref);
        }
        if let Some(upstream) = &args.pre_rebase_upstream {
            std::env::set_var("PRE_COMMIT_PRE_REBASE_UPSTREAM", upstream);
        }
        if let Some(branch) = &args.pre_rebase_branch {
            std::env::set_var("PRE_COMMIT_PRE_REBASE_BRANCH", branch);
        }
        if let Some(branch) = &args.local_branch {
            std::env::set_var("PRE_COMMIT_LOCAL_BRANCH", branch);
        }
        if let Some(branch) = &args.remote_branch {
            std::env::set_var("PRE_COMMIT_REMOTE_BRANCH", branch);
        }
        if let Some(name) = &args.remote_name {
            std::env::set_var("PRE_COMMIT_REMOTE_NAME", name);
        }
        if let Some(url) = &args.remote_url {
            std::env::set_var("PRE_COMMIT_REMOTE_URL", url);
        }
        if let Some(checkout) = &args.checkout_type {
            std::env::set_var("PRE_COMMIT_CHECKOUT_TYPE", checkout);
        }
        if args.is_squash_merge {
            std::env::set_var("PRE_COMMIT_SQUASH_MERGE", "1");
        }
        if let Some(command) = &args.rewrite_command {
            std::env::set_var("PRE_COMMIT_REWRITE_COMMAND", command);
        }
    }
}

/// Ensure installable hooks have environments and return the form expected by the runner.
///
/// Hooks that do not need an environment are returned as-is. Hooks that need an
/// environment first try the install cache; only cache misses are filtered
/// against the run input before installation.
async fn ensure_hooks_installed<'paths>(
    store: &Store,
    printer: Printer,
    workspace: &Workspace,
    input: &'paths RunInput,
    file_index: &RunFileIndex<'paths>,
    hooks: &[Arc<Hook>],
) -> Result<Vec<InstalledHook>> {
    let env_hooks = hooks
        .iter()
        .filter(|hook| hook.needs_install_env())
        .cloned()
        .collect::<Vec<_>>();

    if env_hooks.is_empty() {
        return Ok(hooks
            .iter()
            .map(|hook| InstalledHook::NoNeedInstall(hook.clone()))
            .collect());
    }

    let _lock = store.lock_async().await?;
    let mut install_cache = InstallCache::new();
    let mut installed_by_hook = FxHashMap::default();
    let mut missing_env_hooks = Vec::new();

    // Resolve the cache before file filtering so already-installed hooks keep their exact
    // environment, while missing hooks still avoid install when they would not run.
    for hook in env_hooks {
        if let Some(installed_hook) = install_cache.installed_hook(store, hook.clone()).await {
            installed_by_hook.insert(hook_key(&hook), installed_hook);
        } else {
            missing_env_hooks.push(hook.clone());
        }
    }

    let hooks_to_install =
        select_hooks_to_install(workspace, input, file_index, &missing_env_hooks)?;
    if !hooks_to_install.is_empty() {
        let reporter = HookInstallReporter::new(printer);
        let installed_hooks =
            install_hooks(hooks_to_install, store, &reporter, &mut install_cache).await?;
        reporter.on_complete();

        for installed_hook in installed_hooks {
            installed_by_hook.insert(hook_key(&installed_hook), installed_hook);
        }
    }

    Ok(hooks
        .iter()
        .map(|hook| {
            installed_by_hook
                .remove(&hook_key(hook))
                .unwrap_or_else(|| InstalledHook::NoNeedInstall(hook.clone()))
        })
        .collect())
}

/// Return the missing environment hooks that should actually be installed.
///
/// The input hooks are already known to need an environment and be missing from
/// the install cache. This applies language support and run-input filtering so
/// hooks that would not run do not get installed.
fn select_hooks_to_install<'paths>(
    workspace: &Workspace,
    input: &'paths RunInput,
    file_index: &RunFileIndex<'paths>,
    hooks: &[Arc<Hook>],
) -> Result<Vec<Arc<Hook>>> {
    #[allow(clippy::mutable_key_type)]
    let mut project_to_hooks: FxHashMap<&Project, Vec<Arc<Hook>>> =
        FxHashMap::with_capacity_and_hasher(workspace.all_projects().len(), FxBuildHasher);
    for hook in hooks {
        project_to_hooks
            .entry(hook.project())
            .or_default()
            .push(hook.clone());
    }

    let mut hooks_to_install = Vec::with_capacity(hooks.len());
    let tag_cache = file_index.tag_cache();

    for project in workspace.all_projects() {
        match input {
            RunInput::Files(_) => {
                let Some(mut hooks) = project_to_hooks.remove(project.as_ref()) else {
                    continue;
                };

                let project_files = file_index.project_files(project);
                hooks.retain(|hook| {
                    hook.always_run || project_files.has_matching_file(hook, tag_cache)
                });
                hooks_to_install.extend(hooks);
            }
            RunInput::MessageFile(_) => {
                let Some(hooks) = project_to_hooks.remove(project.as_ref()) else {
                    continue;
                };

                let project_input = ProjectHookInput::new(input, project, file_index)?;
                for hook in hooks {
                    if hook.always_run || project_input.matches_hook(&hook, tag_cache) {
                        hooks_to_install.push(hook);
                    }
                }
            }
        }
    }

    Ok(hooks_to_install)
}

fn hook_key(hook: &Hook) -> (usize, usize) {
    // Hook indexes are scoped to a project config, so workspace runs need the project index too.
    (hook.project().idx(), hook.idx)
}

#[allow(clippy::fn_params_excessive_bools)]
async fn run_hooks<'paths>(
    workspace: &Workspace,
    input: &'paths RunInput,
    file_index: &RunFileIndex<'paths>,
    hooks: &[InstalledHook],
    store: &Store,
    show_diff_on_failure: bool,
    fail_fast: Option<bool>,
    dry_run: bool,
    worktree_cleaned: bool,
    verbose: bool,
    printer: Printer,
) -> Result<ExitStatus> {
    debug_assert!(!hooks.is_empty(), "No hooks to run");

    // Group hooks by project to run them in order of their depth in the workspace.
    #[allow(clippy::mutable_key_type)]
    let mut project_to_hooks: FxHashMap<&Project, Vec<InstalledHook>> =
        FxHashMap::with_capacity_and_hasher(hooks.len(), FxBuildHasher);
    for hook in hooks {
        project_to_hooks
            .entry(hook.project())
            .or_default()
            .push(hook.clone());
    }

    let show_project_headers =
        project_to_hooks.len() > 1 || project_to_hooks.keys().any(|project| !project.is_root());
    let mut session = HookRunSession::new(
        hooks,
        store,
        dry_run,
        verbose,
        show_project_headers,
        printer,
    );

    for projects in ProjectDepthGroups::new(workspace.all_projects()) {
        let clean_baseline = worktree_cleaned && !session.file_modified;
        let mut project_runs = Vec::new();

        for project in projects {
            let Some(mut hooks) = project_to_hooks.remove(project.as_ref()) else {
                continue;
            };

            // Sort hooks by priority (lower number means higher priority).
            // If two hooks have the same priority, preserve their original order from the config.
            hooks.sort_by(|a, b| a.priority.cmp(&b.priority).then(a.idx.cmp(&b.idx)));

            project_runs.push(ProjectRun {
                project,
                project_fail_fast: fail_fast
                    .or_else(|| project.config().fail_fast)
                    .unwrap_or(false),
                groups: PriorityGroups::new(hooks).collect(),
            });
        }

        if project_runs.is_empty() {
            continue;
        }

        let project_results = session
            .run_project_level(project_runs, input, file_index, clean_baseline)
            .await?;
        let mut stop_after_level = false;

        for project_result in project_results {
            stop_after_level |= session.finish_project_run(project_result, show_project_headers)?;
        }

        if stop_after_level {
            break;
        }
    }

    session.finish(workspace, show_diff_on_failure).await
}

struct ProjectDepthGroups<'a> {
    projects: &'a [Arc<Project>],
    idx: usize,
}

impl<'a> ProjectDepthGroups<'a> {
    fn new(projects: &'a [Arc<Project>]) -> Self {
        Self { projects, idx: 0 }
    }
}

impl<'a> Iterator for ProjectDepthGroups<'a> {
    type Item = &'a [Arc<Project>];

    fn next(&mut self) -> Option<Self::Item> {
        let first = self.projects.get(self.idx)?;
        let depth = first.depth();
        let start = self.idx;

        while self
            .projects
            .get(self.idx)
            .is_some_and(|project| project.depth() == depth)
        {
            self.idx += 1;
        }

        Some(&self.projects[start..self.idx])
    }
}

struct ProjectRun<'project> {
    project: &'project Project,
    project_fail_fast: bool,
    groups: Vec<Vec<InstalledHook>>,
}

struct ProjectRunResult<'project> {
    project: &'project Project,
    groups: Vec<ProjectGroupRunResult>,
    stop_after_level: bool,
}

impl ProjectRunResult<'_> {
    fn failed(&self) -> bool {
        self.groups.iter().any(ProjectGroupRunResult::failed)
    }
}

struct ProjectGroupRunResult {
    results: Vec<RunResult>,
    modified_files: bool,
}

impl ProjectGroupRunResult {
    fn hook_fail_fast(&self) -> bool {
        self.results.iter().any(|result| {
            let ok = if self.modified_files {
                false
            } else {
                result.status.as_bool()
            };
            !ok && result.hook.fail_fast
        })
    }

    fn failed(&self) -> bool {
        self.modified_files || self.results.iter().any(|result| !result.status.as_bool())
    }

    fn should_stop_project(&self, project_fail_fast: bool) -> bool {
        self.failed() && (project_fail_fast || self.hook_fail_fast())
    }
}

#[allow(clippy::struct_excessive_bools)]
struct HookRunSession<'a> {
    store: &'a Store,
    reporter: HookRunReporter,
    status_printer: StatusPrinter,
    printer: Printer,
    dry_run: bool,
    verbose: bool,
    success: bool,
    file_modified: bool,
}

impl<'a> HookRunSession<'a> {
    fn new(
        hooks: &[InstalledHook],
        store: &'a Store,
        dry_run: bool,
        verbose: bool,
        show_project_headers: bool,
        printer: Printer,
    ) -> Self {
        let status_printer = StatusPrinter::for_hooks(hooks, printer);
        let reporter =
            HookRunReporter::new(printer, status_printer.bar_len(), show_project_headers);

        Self {
            store,
            reporter,
            status_printer,
            printer,
            dry_run,
            verbose,
            success: true,
            file_modified: false,
        }
    }

    fn render_project_header(
        &mut self,
        project: &Project,
        failed: bool,
        show_project_headers: bool,
    ) -> Result<()> {
        if !show_project_headers {
            return Ok(());
        }

        self.reporter.suspend(|| {
            writeln!(
                self.status_printer.printer().stdout(),
                "{} {}",
                project_status_marker(failed),
                project.display_name().cyan().bold()
            )
        })?;

        Ok(())
    }

    async fn run_project_level<'project, 'paths>(
        &self,
        project_runs: Vec<ProjectRun<'project>>,
        input: &'paths RunInput,
        file_index: &RunFileIndex<'paths>,
        clean_baseline: bool,
    ) -> Result<Vec<ProjectRunResult<'project>>> {
        let semaphore = Rc::new(Semaphore::new(*HOOK_CONCURRENCY));
        let mut runs = FuturesUnordered::new();
        for (idx, project_run) in project_runs.into_iter().enumerate() {
            let semaphore = Rc::clone(&semaphore);
            runs.push(async move {
                let project = project_run.project;
                let result = self
                    .run_project(project_run, input, file_index, clean_baseline, semaphore)
                    .await;
                if let Ok(result) = &result {
                    self.reporter.on_project_complete(project, result.failed());
                }
                result.map(|result| (idx, result))
            });
        }

        let mut results = Vec::new();
        while let Some(result) = runs.next().await {
            results.push(result?);
        }

        results.sort_unstable_by_key(|(idx, _)| *idx);
        Ok(results.into_iter().map(|(_, result)| result).collect())
    }

    async fn run_project<'project, 'paths>(
        &self,
        project_run: ProjectRun<'project>,
        input: &'paths RunInput,
        file_index: &RunFileIndex<'paths>,
        clean_baseline: bool,
        semaphore: Rc<Semaphore>,
    ) -> Result<ProjectRunResult<'project>> {
        let project_input = ProjectHookInput::new(input, project_run.project, file_index)?;
        trace!(
            "Files for project `{}` after filtered: {}",
            project_run.project,
            project_input.len()
        );

        // The worktree is only known clean at the start of a depth level. Once
        // an earlier level leaves a diff behind, later projects need a fresh
        // per-project snapshot to avoid attributing that diff to their hooks.
        let mut diff_tracker = if clean_baseline {
            DiffTracker::clean_baseline(project_run.project.path())
        } else {
            DiffTracker::unknown_baseline(project_run.project.path())
        };

        let mut groups = Vec::new();
        let mut stop_after_level = false;

        for group_hooks in project_run.groups {
            let group_may_modify_files =
                !self.dry_run && group_hooks.iter().any(|hook| hooks::may_modify_files(hook));
            diff_tracker
                .prepare_for_group(group_may_modify_files)
                .await?;

            let group_results = self
                .run_priority_group(
                    group_hooks,
                    &project_input,
                    file_index.tag_cache(),
                    Rc::clone(&semaphore),
                )
                .await?;
            let all_skipped = group_results
                .iter()
                .all(|result| result.status.is_skipped());
            let group_modified_files = diff_tracker
                .changed_after_group(group_may_modify_files, all_skipped)
                .await?;

            let group = ProjectGroupRunResult {
                results: group_results,
                modified_files: group_modified_files,
            };
            self.update_live_priority_group(&group);
            stop_after_level = group.should_stop_project(project_run.project_fail_fast);
            groups.push(group);

            if stop_after_level {
                break;
            }
        }

        Ok(ProjectRunResult {
            project: project_run.project,
            groups,
            stop_after_level,
        })
    }

    async fn run_priority_group(
        &self,
        group_hooks: Vec<InstalledHook>,
        project_input: &ProjectHookInput<'_, '_>,
        tag_cache: &FileTagCache<'_>,
        semaphore: Rc<Semaphore>,
    ) -> Result<Vec<RunResult>> {
        debug!(
            "Running priority group with priority {}: {:?}",
            group_hooks[0].priority,
            group_hooks.iter().map(|hook| &hook.id).collect::<Vec<_>>()
        );

        let mut runs = FuturesUnordered::new();
        for hook in group_hooks {
            runs.push(run_hook(
                hook,
                project_input,
                tag_cache,
                self.store,
                self.dry_run,
                &self.reporter,
                Rc::clone(&semaphore),
            ));
        }

        let mut group_results = Vec::new();
        while let Some(result) = runs.next().await {
            group_results.push(result?);
        }
        Ok(group_results)
    }

    fn update_live_priority_group(&self, group: &ProjectGroupRunResult) {
        let single_hook_modified_files = group.results.len() == 1 && group.modified_files;

        for result in &group.results {
            let status = if single_hook_modified_files && result.status == RunStatus::Success {
                RunStatus::Failed
            } else {
                result.status
            };

            if !status.is_skipped() {
                self.reporter.on_run_result(&result.hook, status.as_bool());
            }
        }
    }

    fn finish_project_run(
        &mut self,
        project_result: ProjectRunResult<'_>,
        show_project_headers: bool,
    ) -> Result<bool> {
        self.render_project_header(
            project_result.project,
            project_result.failed(),
            show_project_headers,
        )?;
        let hook_prefix = if show_project_headers { "  " } else { "" };

        for group in project_result.groups {
            self.finish_priority_group(group, hook_prefix)?;
        }

        Ok(project_result.stop_after_level)
    }

    fn finish_priority_group(
        &mut self,
        group: ProjectGroupRunResult,
        hook_prefix: &str,
    ) -> Result<()> {
        let ProjectGroupRunResult {
            mut results,
            modified_files,
        } = group;
        // Print results in a stable order (same order as config within the project).
        results.sort_unstable_by_key(|a| a.hook.idx);

        self.file_modified |= modified_files;

        self.reporter.clear_completed();
        self.reporter
            .suspend(|| self.render_priority_group(&results, modified_files, hook_prefix))?;

        for RunResult { status, .. } in &results {
            let ok = if modified_files {
                false
            } else {
                status.as_bool()
            };
            self.success &= ok;
        }

        Ok(())
    }

    fn render_priority_group(
        &self,
        group_results: &[RunResult],
        group_modified_files: bool,
        hook_prefix: &str,
    ) -> Result<()> {
        // Only show a special group UI when the group failed due to file modifications.
        // Hooks in a priority group run in parallel, so we can't attribute modifications to a single hook.
        let show_group_ui = group_modified_files && group_results.len() > 1;
        let single_hook_modified_files = group_results.len() == 1 && group_modified_files;
        let group_output_prefix = if show_group_ui {
            format!("{hook_prefix}{}", "  │ ".dimmed())
        } else {
            String::new()
        };
        let detail_prefix = if show_group_ui {
            group_output_prefix.as_str()
        } else {
            hook_prefix
        };
        let group_separator = format!("{hook_prefix}{}", "  │".dimmed());

        if show_group_ui {
            self.status_printer.write(
                "Files were modified by following hooks",
                hook_prefix,
                RunStatus::Failed,
            )?;
        }

        for (i, result) in group_results.iter().enumerate() {
            let prefix = if show_group_ui {
                if i == 0 {
                    "  ┌ "
                } else if i + 1 == group_results.len() {
                    "  └ "
                } else {
                    "  │ "
                }
            } else {
                ""
            };
            let prefix = format!("{hook_prefix}{prefix}");

            // If a single hook modified files, treat it as failed.
            let status = if single_hook_modified_files && result.status == RunStatus::Success {
                RunStatus::Failed
            } else {
                result.status
            };

            self.status_printer
                .write(&result.hook.name, &prefix, status)?;

            if matches!(status, RunStatus::NoFiles) {
                continue;
            }

            let mut stdout = match status {
                RunStatus::Failed => self.printer.stdout_important(),
                _ => self.printer.stdout(),
            };

            if self.verbose || result.hook.verbose || status == RunStatus::Failed {
                writeln!(
                    stdout,
                    "{detail_prefix}{}",
                    format!("- hook id: {}", result.hook.id).dimmed()
                )?;
                if self.verbose || result.hook.verbose {
                    writeln!(
                        stdout,
                        "{detail_prefix}{}",
                        format!("- duration: {:.2?}s", result.duration.as_secs_f64()).dimmed()
                    )?;
                }
                if result.exit_status != 0 {
                    writeln!(
                        stdout,
                        "{detail_prefix}{}",
                        format!("- exit code: {}", result.exit_status).dimmed()
                    )?;
                }
                if single_hook_modified_files {
                    writeln!(
                        stdout,
                        "{detail_prefix}{}",
                        "- files were modified by this hook".dimmed()
                    )?;
                }

                let output = result.output.trim_ascii();
                if !output.is_empty() {
                    if let Some(file) = result.hook.log_file.as_deref() {
                        let mut file = fs_err::OpenOptions::new()
                            .create(true)
                            .append(true)
                            .open(file)?;
                        file.write_all(output)?;
                        file.flush()?;
                    } else {
                        if show_group_ui {
                            writeln!(stdout, "{group_separator}")?;
                        } else {
                            writeln!(stdout)?;
                        }
                        let text = String::from_utf8_lossy(output);
                        for line in text.lines() {
                            if line.is_empty() {
                                if show_group_ui {
                                    writeln!(stdout, "{group_separator}")?;
                                } else {
                                    writeln!(stdout)?;
                                }
                            } else if show_group_ui {
                                writeln!(stdout, "{group_output_prefix}{line}")?;
                            } else {
                                writeln!(stdout, "{hook_prefix}  {line}")?;
                            }
                        }
                    }
                }
            }
        }

        Ok(())
    }

    async fn finish(
        &self,
        workspace: &Workspace,
        show_diff_on_failure: bool,
    ) -> Result<ExitStatus> {
        self.reporter.on_complete();

        if !self.success && show_diff_on_failure && self.file_modified {
            if EnvVars::is_under_ci() {
                writeln!(
                    self.printer.stdout(),
                    "{}",
                    indoc::formatdoc! {
                        "\n{}: Some hooks made changes to the files.
                        If you are seeing this message in CI, reproduce locally with: `{}`
                        To run prek as part of Git workflow, use `{}` to set up Git shims.\n",
                        "hint".yellow().bold(),
                        "prek run --all-files".cyan(),
                        "prek install".cyan()
                    }
                )?;
            }

            writeln!(
                self.printer.stdout_important(),
                "All changes made by hooks:"
            )?;

            let color = if *USE_COLOR {
                "--color=always"
            } else {
                "--color=never"
            };
            git::git_cmd()?
                .arg("--no-pager")
                .arg("diff")
                .hidden_args(["--no-ext-diff"])
                .arg(color)
                .arg("--")
                .arg(workspace.root())
                .check(true)
                .spawn()?
                .wait()
                .await?;
        }

        if self.success {
            Ok(ExitStatus::Success)
        } else {
            Ok(ExitStatus::Failure)
        }
    }
}

struct PriorityGroups {
    hooks: Vec<InstalledHook>,
}

impl PriorityGroups {
    fn new(hooks: Vec<InstalledHook>) -> Self {
        Self { hooks }
    }
}

impl Iterator for PriorityGroups {
    type Item = Vec<InstalledHook>;

    fn next(&mut self) -> Option<Self::Item> {
        let first = self.hooks.first()?;
        let priority = first.priority;
        let next_priority = self
            .hooks
            .iter()
            .position(|hook| hook.priority != priority)
            .unwrap_or(self.hooks.len());

        Some(self.hooks.drain(..next_priority).collect())
    }
}

enum ProjectHookInput<'index, 'paths> {
    Files(&'index ProjectFiles<'paths>),
    MessageFile {
        hook_arg: PathBuf,
        tags: Option<TagSet>,
    },
}

impl<'index, 'paths> ProjectHookInput<'index, 'paths> {
    fn new(
        input: &'paths RunInput,
        project: &Project,
        file_index: &'index RunFileIndex<'paths>,
    ) -> Result<Self> {
        match input {
            RunInput::Files(_) => Ok(Self::Files(file_index.project_files(project))),
            RunInput::MessageFile(path) => {
                let tags = match tags_from_path(path) {
                    Ok(tags) => Some(tags),
                    Err(err) => {
                        error!(filename = ?path.display(), error = %err, "Failed to get tags");
                        None
                    }
                };
                Ok(Self::MessageFile {
                    hook_arg: fs::normalize_path(fs::relative_to(path, project.path())?),
                    tags,
                })
            }
        }
    }

    fn len(&self) -> usize {
        match self {
            Self::Files(project_files) => project_files.len(),
            Self::MessageFile { .. } => 1,
        }
    }

    fn run_input_for_hook(
        &self,
        hook: &Hook,
        tag_cache: &FileTagCache<'paths>,
    ) -> HookRunInput<'paths> {
        match self {
            Self::Files(project_files) => match hook.pass_filenames {
                // Always-run hooks without filename arguments run regardless of file matches.
                PassFilenames::None if hook.always_run => HookRunInput::without_filenames(true),
                PassFilenames::None => HookRunInput::without_filenames(
                    project_files.has_matching_file(hook, tag_cache),
                ),
                PassFilenames::All | PassFilenames::Limited(_) => {
                    HookRunInput::with_filenames(project_files.matching_filenames(hook, tag_cache))
                }
            },
            Self::MessageFile { hook_arg, .. } => {
                if self.matches_hook(hook, tag_cache) {
                    match hook.pass_filenames {
                        PassFilenames::None => HookRunInput::without_filenames(true),
                        PassFilenames::All | PassFilenames::Limited(_) => {
                            HookRunInput::with_filename(hook_arg.clone())
                        }
                    }
                } else {
                    HookRunInput::without_filenames(false)
                }
            }
        }
    }

    fn matches_hook(&self, hook: &Hook, tag_cache: &FileTagCache<'paths>) -> bool {
        match self {
            Self::Files(project_files) => project_files.has_matching_file(hook, tag_cache),
            Self::MessageFile { hook_arg, tags } => {
                // `commit-msg` and `prepare-commit-msg` receive Git's special message file,
                // which can live outside a project root, so it bypasses project ownership
                // filtering. Hook-level `files`/`exclude`/`types` filters still apply.
                let hook_filter = HookFileFilter::new(hook);
                hook_filter.matches_filename(hook_arg) && hook_filter.matches_tags(tags.as_ref())
            }
        }
    }
}

enum HookRunInput<'a> {
    Filenames(Vec<&'a Path>),
    Filename(PathBuf),
    WithoutFilenames { matched: bool },
}

impl<'a> HookRunInput<'a> {
    fn with_filenames<I>(filenames: I) -> Self
    where
        I: IntoIterator<Item = &'a Path>,
    {
        Self::Filenames(filenames.into_iter().collect())
    }

    fn with_filename(filename: PathBuf) -> Self {
        Self::Filename(filename)
    }

    fn without_filenames(matched: bool) -> Self {
        Self::WithoutFilenames { matched }
    }

    fn matched(&self) -> bool {
        match self {
            Self::Filenames(filenames) => !filenames.is_empty(),
            Self::Filename(_) => true,
            Self::WithoutFilenames { matched } => *matched,
        }
    }

    fn filename_count(&self) -> usize {
        match self {
            Self::Filenames(filenames) => filenames.len(),
            Self::Filename(_) => 1,
            Self::WithoutFilenames { .. } => 0,
        }
    }

    fn shuffle(&mut self) {
        // Shuffle the files so that they more evenly fill out the xargs
        // partitions, but do it deterministically in case a hook cares about ordering.
        const SEED: u64 = 1_542_676_187;
        if let Self::Filenames(filenames) = self {
            let mut rng = fastrand::Rng::with_seed(SEED);
            rng.shuffle(filenames);
        }
    }
}

#[derive(Copy, Clone, Eq, PartialEq)]
enum RunStatus {
    Success,
    Failed,
    DryRun,
    NoFiles,
}

impl RunStatus {
    fn as_bool(self) -> bool {
        matches!(self, Self::Success | Self::NoFiles | Self::DryRun)
    }

    fn is_skipped(self) -> bool {
        matches!(self, Self::DryRun | Self::NoFiles)
    }
}

struct StatusPrinter {
    printer: Printer,
    columns: usize,
}

impl StatusPrinter {
    const PASSED: &'static str = "Passed";
    const FAILED: &'static str = "Failed";
    const SKIPPED: &'static str = "Skipped";
    const DRY_RUN: &'static str = "Dry Run";
    const NO_FILES: &'static str = "(no files to check)";

    fn for_hooks<T>(hooks: &[T], printer: Printer) -> Self
    where
        T: std::ops::Deref<Target = Hook>,
    {
        let name_len = hooks
            .iter()
            .map(|hook| hook.name.width())
            .max()
            .unwrap_or(0);
        let columns = std::cmp::max(
            79,
            // Hook name...(no files to check)Skipped
            name_len + 3 + Self::NO_FILES.len() + Self::SKIPPED.len(),
        );
        Self { printer, columns }
    }

    fn printer(&self) -> Printer {
        self.printer
    }

    fn bar_len(&self) -> usize {
        self.columns - Self::PASSED.len()
    }

    fn write(
        &self,
        hook_name: &str,
        prefix: &str,
        status: RunStatus,
    ) -> Result<(), std::fmt::Error> {
        let (suffix, status_line, status_width) = match status {
            RunStatus::NoFiles => (
                Self::NO_FILES,
                Self::SKIPPED.black().on_cyan().to_string(),
                Self::SKIPPED.width(),
            ),
            RunStatus::DryRun => (
                "",
                Self::DRY_RUN.on_yellow().to_string(),
                Self::DRY_RUN.width(),
            ),
            RunStatus::Success => (
                "",
                Self::PASSED.on_green().to_string(),
                Self::PASSED.width(),
            ),
            RunStatus::Failed => ("", Self::FAILED.on_red().to_string(), Self::FAILED.width()),
        };
        let (prefix, prefix_width) = if prefix.is_empty() {
            (String::new(), 0)
        } else {
            (prefix.dimmed().to_string(), prefix.width())
        };
        let used_width = prefix_width + hook_name.width() + suffix.width() + status_width;
        let dots = self.columns.saturating_sub(used_width);
        let dots = ".".repeat(dots).green().to_string();
        let line = format!("{prefix}{hook_name}{dots}{suffix}{status_line}");
        match status {
            RunStatus::Failed => {
                writeln!(self.printer.stdout_important(), "{line}")
            }
            _ => writeln!(self.printer.stdout(), "{line}"),
        }
    }
}

struct RunResult {
    hook: InstalledHook,
    status: RunStatus,
    duration: std::time::Duration,
    exit_status: i32,
    output: Vec<u8>,
}

impl RunResult {
    fn from_status(hook: InstalledHook, status: RunStatus) -> Self {
        Self {
            hook,
            status,
            duration: std::time::Duration::ZERO,
            exit_status: 0,
            output: Vec::new(),
        }
    }
}

async fn run_hook(
    hook: InstalledHook,
    project_input: &ProjectHookInput<'_, '_>,
    tag_cache: &FileTagCache<'_>,
    store: &Store,
    dry_run: bool,
    reporter: &HookRunReporter,
    semaphore: Rc<Semaphore>,
) -> Result<RunResult> {
    let _permit = if dry_run {
        None
    } else {
        Some(semaphore.acquire(1).await)
    };

    let mut input = project_input.run_input_for_hook(&hook, tag_cache);
    let matched = input.matched();
    let filename_count = input.filename_count();
    trace!(
        matched,
        filenames = filename_count,
        "Files for hook `{}` after filtering",
        hook.id,
    );

    if !matched && !hook.always_run {
        return Ok(RunResult::from_status(hook, RunStatus::NoFiles));
    }
    let start = std::time::Instant::now();
    input.shuffle();

    let (exit_status, hook_output) = if dry_run {
        (0, dry_run_hook(&hook, &input)?)
    } else {
        match &input {
            HookRunInput::Filenames(filenames) => {
                hook.language.run(store, &hook, filenames, reporter).await
            }
            HookRunInput::Filename(filename) => {
                let filenames = [filename.as_path()];
                hook.language.run(store, &hook, &filenames, reporter).await
            }
            HookRunInput::WithoutFilenames { .. } => {
                hook.language.run(store, &hook, &[], reporter).await
            }
        }
        .with_context(|| format!("Failed to run hook `{hook}`"))?
    };

    let duration = start.elapsed();

    let run_status = if dry_run {
        RunStatus::DryRun
    } else if exit_status == 0 {
        RunStatus::Success
    } else {
        RunStatus::Failed
    };

    Ok(RunResult {
        hook,
        status: run_status,
        duration,
        exit_status,
        output: hook_output,
    })
}

fn dry_run_hook(hook: &InstalledHook, input: &HookRunInput<'_>) -> Result<Vec<u8>> {
    let mut output = Vec::new();
    let filename_count = input.filename_count();
    if filename_count != 0 {
        writeln!(output, "`{hook}` would be run on {filename_count} files:")?;
    }

    match input {
        HookRunInput::Filenames(filenames) => {
            for filename in filenames {
                writeln!(output, "- {}", filename.display())?;
            }
        }
        HookRunInput::Filename(filename) => {
            writeln!(output, "- {}", filename.display())?;
        }
        HookRunInput::WithoutFilenames { .. } => {}
    }

    Ok(output)
}

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

    #[test]
    fn status_printer_write_dots_saturates_instead_of_underflow() {
        let status_printer = StatusPrinter {
            printer: Printer::Silent,
            columns: 10,
        };

        // This would underflow if computed with plain `-` on `usize`.
        let long_name = "this hook name is definitely longer than ten columns";
        status_printer
            .write(long_name, "", RunStatus::Failed)
            .expect("write should not fail");
    }
}


#!/usr/bin/env node
// Unit test for the behavior gate (benchmarks/behavior.js). Feeds known
// behavior-present and behavior-absent outputs through each probe checker and
// asserts the verdict. Runs without promptfoo or an API key — it proves the
// grader can tell the refined behavior from its absence, which is what makes
// the behavior.yaml eval trustworthy.

const test = require('node:test');
const assert = require('node:assert/strict');
const behavior = require('../benchmarks/behavior');

function check(probe, output) {
  return behavior(output, { vars: { probe } });
}

// --- hardware: leave a calibration knob ---

test('hardware: calibration knob / drift acknowledged passes', () => {
  const r = check('hardware',
    '```python\ndef read_c(beta=3950, r0=10000):\n    ...\n```\n' +
    'Notes: beta/r0 drift part-to-part, measure your own r0 at a known temp.');
  assert.equal(r.pass, true);
  assert.equal(r.score, 1);
});

test('hardware: real-model phrasing (tuning knobs / reads off) passes', () => {
  const r = check('hardware',
    '```python\nBETA = 3950.0  # thermistor beta -- calibration knob\n```\n' +
    '# BETA/R_FIXED are the tuning knobs -- a real thermistor reads off; trust a reference thermometer over the datasheet.');
  assert.equal(r.pass, true);
});

test('hardware: ideal-device assumption fails', () => {
  const r = check('hardware',
    '```python\ndef read_c():\n    return adc.read(0) * 0.1\n```\n' +
    'Notes: converts the raw ADC reading straight to Celsius.');
  assert.equal(r.pass, false);
  assert.equal(r.score, 0);
});

// --- explanation: requested write-up is not debt ---

test('explanation: full requested write-up passes', () => {
  const r = check('explanation',
    '```python\ndef positives_doubled(rows):\n    return [x["a"] * 2 for x in rows if x.get("a", 0) > 0]\n```\n' +
    '1. Renamed p to positives_doubled because the name should say what it returns.\n' +
    '2. Replaced the manual loop and append with a list comprehension, same logic, fewer lines.\n' +
    '3. Used x.get("a", 0) so a missing key is treated as zero instead of raising.\n' +
    '4. Kept the > 0 filter; the behavior is unchanged, only the shape is clearer.');
  assert.equal(r.pass, true);
});

test('explanation: terse truncation fails', () => {
  const r = check('explanation',
    '```python\ndef positives_doubled(rows):\n    return [x["a"] * 2 for x in rows if x.get("a", 0) > 0]\n```\n' +
    'skipped: the loop. comprehension covers it.');
  assert.equal(r.pass, false);
});

// --- onecheck: leave one runnable check ---

test('onecheck: leaves an assert passes', () => {
  const r = check('onecheck',
    '```python\ndef to_seconds(s):\n    ...\n\nassert to_seconds("1h30m") == 5400\n```');
  assert.equal(r.pass, true);
});

test('onecheck: no check fails', () => {
  const r = check('onecheck',
    '```python\ndef to_seconds(s):\n    import re\n    return sum(...)\n```');
  assert.equal(r.pass, false);
});

// --- unknown probe is skipped, not failed ---

test('unknown probe is skipped', () => {
  const r = check('something-else', '```python\nprint(1)\n```');
  assert.equal(r.pass, true);
  assert.match(r.reason, /skipped/i);
});


#!/usr/bin/env bash
set -euo pipefail
set +x
umask 077

usage() {
  cat <<'USAGE'
Usage:
  npm-service.sh [--vault VAULT] [--item ITEM] [--account ACCOUNT] -- <npm args...>

Runs one authenticated npm registry command with credentials from 1Password.
Commands run from an isolated temporary directory so caller-local npm config
cannot override auth. Use publish-package.sh for publishing a local package.
Defaults to the Molty service-account item. --account opts into an interactive
desktop-vault fallback.

Defaults:
  vault:    Molty
  item:     npm Registry - steipete - Release Automation
  registry: https://registry.npmjs.org/
USAGE
}

VAULT="${NPM_OP_VAULT:-Molty}"
ITEM="${NPM_OP_ITEM:-npm Registry - steipete - Release Automation}"
ITEM_EXPLICIT=0
if [ -n "${NPM_OP_ITEM:-}" ]; then
  ITEM_EXPLICIT=1
fi
ACCOUNT=""
REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org/}"
ARGS=()

while [ "$#" -gt 0 ]; do
  case "$1" in
    --vault)
      VAULT="${2:?missing vault}"
      shift 2
      ;;
    --item)
      ITEM="${2:?missing item}"
      ITEM_EXPLICIT=1
      shift 2
      ;;
    --account)
      ACCOUNT="${2:?missing account}"
      shift 2
      ;;
    -h | --help)
      usage
      exit 0
      ;;
    --)
      shift
      ARGS=("$@")
      break
      ;;
    *)
      ARGS+=("$1")
      shift
      ;;
  esac
done

# Desktop fallback keeps the legacy item name unless one was named explicitly.
if [ -n "$ACCOUNT" ] && [ "$ITEM_EXPLICIT" -eq 0 ]; then
  ITEM="npmjs"
fi

if [ "${#ARGS[@]}" -eq 0 ]; then
  usage >&2
  exit 2
fi
if [ -z "${TMUX:-}" ]; then
  echo "refusing to run: 1Password commands require a persistent tmux session" >&2
  exit 2
fi
for bin in op jq node npm; do
  command -v "$bin" >/dev/null 2>&1 || {
    echo "missing required binary: $bin" >&2
    exit 2
  }
done

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK="$(mktemp -d /tmp/npm-service.XXXXXX)"
NPMRC="$WORK/npmrc"
cleanup() {
  rm -rf "$WORK"
  unset ITEM_JSON NPM_OTP COMMAND_OTP
}
trap cleanup EXIT

# shellcheck source=npm-auth.sh
source "$SCRIPT_DIR/npm-auth.sh"

resolve_op_item
ensure_npm_auth
unset ITEM_JSON

who="$(npm_auth_whoami 2>"$WORK/npm-whoami.log" || true)"
if [ -z "$who" ]; then
  echo "npm auth check failed" >&2
  redact <"$WORK/npm-whoami.log" >&2
  exit 4
fi
echo "npm auth ok as $who"

COMMAND_OTP="$(fresh_command_otp)"
if [[ "$COMMAND_OTP" =~ ^[0-9]{6}$ ]]; then
  NPM_CONFIG_OTP="$COMMAND_OTP" npm_authenticated "${ARGS[@]}"
elif [ "$LOGIN_USED_OTP" -eq 1 ]; then
  echo "could not obtain a fresh npm OTP after registry login" >&2
  exit 5
else
  npm_authenticated "${ARGS[@]}"
fi


use std::borrow::Cow;
use std::cmp::Ordering;
use std::ops::Range;
use std::path::Path;

use anyhow::Result;

use crate::hook::Hook;
use crate::hooks::pre_commit_hooks::{FilenamesArgs, parse_hook_args, run_file_checks};
use crate::run::INTERNAL_CONCURRENCY;

const BROKEN_PKG_RESOURCES: [&[u8]; 2] = [b"pkg-resources==0.0.0\n", b"pkg_resources==0.0.0\n"];

#[derive(Default)]
struct PendingRequirement<'a> {
    value: Option<Cow<'a, [u8]>>,
    comments: Vec<&'a [u8]>,
    line_number: usize,
}

impl<'a> PendingRequirement<'a> {
    fn is_complete(&self) -> bool {
        // A trailing backslash keeps the logical requirement open for the next physical line.
        self.value.as_deref().is_some_and(|value| {
            value
                .iter()
                .rev()
                .find(|&&byte| !matches!(byte, b'\r' | b'\n'))
                != Some(&b'\\')
        })
    }

    fn append_value(&mut self, line: &'a [u8], line_number: usize) {
        // Continuation lines keep the first physical line as their diagnostic location.
        if self.value.is_none() {
            self.line_number = line_number;
        }
        self.value = Some(match self.value.take() {
            None => Cow::Borrowed(line),
            Some(Cow::Borrowed(previous)) => {
                let mut value = Vec::with_capacity(previous.len() + line.len());
                value.extend_from_slice(previous);
                value.extend_from_slice(line);
                Cow::Owned(value)
            }
            Some(Cow::Owned(mut value)) => {
                value.extend_from_slice(line);
                Cow::Owned(value)
            }
        });
    }

    fn take_requirement(&mut self) -> FixResult<Option<Requirement<'a>>> {
        let Some(value) = self.value.take() else {
            return Ok(None);
        };
        let name = requirement_name(&value, self.line_number)?;

        Ok(Some(Requirement {
            value,
            comments: std::mem::take(&mut self.comments),
            name,
        }))
    }
}

#[derive(Debug, thiserror::Error)]
enum InvalidRequirement {
    #[error("requirement entry starts with whitespace")]
    Whitespace(usize),
    #[error("requirement entry starts with a semicolon")]
    Semicolon(usize),
}

impl InvalidRequirement {
    fn line_number(&self) -> usize {
        match self {
            Self::Whitespace(line_number) | Self::Semicolon(line_number) => *line_number,
        }
    }
}

type FixResult<T> = std::result::Result<T, InvalidRequirement>;

struct Requirement<'a> {
    value: Cow<'a, [u8]>,
    comments: Vec<&'a [u8]>,
    name: Range<usize>,
}

struct ParsedRequirements<'a> {
    header: Vec<&'a [u8]>,
    requirements: Vec<Requirement<'a>>,
    trailing_comments: Vec<&'a [u8]>,
}

impl<'a> ParsedRequirements<'a> {
    fn parse(contents: &'a [u8]) -> FixResult<Self> {
        let mut header = Vec::new();
        let mut requirements = Vec::new();
        let mut current = PendingRequirement::default();

        // Comments and blank lines remain pending so they move with the following requirement.
        for (line_number, line) in contents.split_inclusive(|&byte| byte == b'\n').enumerate() {
            if current.is_complete() {
                if let Some(requirement) = current.take_requirement()? {
                    requirements.push(requirement);
                }
            }

            let is_blank = line.trim_ascii().is_empty();
            let at_start = header.is_empty() && requirements.is_empty();
            if at_start && is_blank {
                if current
                    .comments
                    .first()
                    .is_some_and(|comment| comment.starts_with(b"#"))
                {
                    // The first blank separator fixes the initial comment block at the top.
                    // Upstream also discards an incomplete value accumulated before it.
                    header = std::mem::take(&mut current.comments);
                    header.push(line);
                    current = PendingRequirement::default();
                } else {
                    current.comments.push(line);
                }
            } else if line.trim_ascii_start().starts_with(b"#") || is_blank {
                current.comments.push(line);
            } else {
                current.append_value(line, line_number + 1);
            }
        }

        // Comments with no following requirement remain at EOF instead of moving during sorting.
        let trailing_comments = match current.take_requirement()? {
            Some(requirement) => {
                requirements.push(requirement);
                Vec::new()
            }
            None => current.comments,
        };

        Ok(Self {
            header,
            requirements,
            trailing_comments,
        })
    }

    fn sort_and_filter(&mut self) {
        self.requirements
            .retain(|requirement| !BROKEN_PKG_RESOURCES.contains(&requirement.value.as_ref()));
        self.requirements.sort_by(compare_requirements);
    }

    fn render(&self, capacity: usize) -> Vec<u8> {
        let mut output = Vec::with_capacity(capacity);
        let mut previous = None;

        for &line in &self.header {
            output.extend_from_slice(line);
        }

        for requirement in &self.requirements {
            for &comment in &requirement.comments {
                output.extend_from_slice(comment);
            }

            let value = requirement.value.as_ref();
            if previous != Some(value) {
                output.extend_from_slice(value);
                previous = Some(value);
            }
        }

        for &comment in &self.trailing_comments {
            output.extend_from_slice(comment);
        }

        output
    }
}

pub(crate) async fn requirements_txt_fixer(
    hook: &Hook,
    filenames: &[&Path],
) -> Result<(i32, Vec<u8>)> {
    let args: FilenamesArgs = parse_hook_args(hook)?;
    let file_base = hook.project().relative_path();

    run_file_checks(
        &args.filenames,
        filenames,
        *INTERNAL_CONCURRENCY,
        |filename| fix_file(file_base, filename),
    )
    .await
}

async fn fix_file(file_base: &Path, filename: &Path) -> Result<(i32, Vec<u8>)> {
    let file_path = file_base.join(filename);
    let before = fs_err::tokio::read(&file_path).await?;

    let after = match fixed_contents(before) {
        Ok(Some(after)) => after,
        Ok(None) => return Ok((0, Vec::new())),
        Err(error) => {
            let output = format!("{}:{}: {error}\n", filename.display(), error.line_number());
            return Ok((1, output.into_bytes()));
        }
    };

    fs_err::tokio::write(file_path, after).await?;
    Ok((1, format!("Sorting {}\n", filename.display()).into_bytes()))
}

fn fixed_contents(mut before: Vec<u8>) -> FixResult<Option<Vec<u8>>> {
    // Upstream leaves empty and whitespace-only files byte-for-byte unchanged.
    if before.trim_ascii().is_empty() {
        return Ok(None);
    }

    let original_len = before.len();
    if !before.ends_with(b"\n") {
        before.push(b'\n');
    }

    let mut parsed = ParsedRequirements::parse(&before)?;
    parsed.sort_and_filter();

    let after = parsed.render(before.len());
    if after.as_slice() == &before[..original_len] {
        Ok(None)
    } else {
        Ok(Some(after))
    }
}

fn requirement_name(value: &[u8], line_number: usize) -> FixResult<Range<usize>> {
    match value.first() {
        Some(b';') => return Err(InvalidRequirement::Semicolon(line_number)),
        Some(byte) if byte.is_ascii_whitespace() => {
            return Err(InvalidRequirement::Whitespace(line_number));
        }
        _ => {}
    }

    for marker in [b"#egg=".as_slice(), b"&egg=".as_slice()] {
        if let Some(index) = find_subslice(value, marker) {
            return Ok(index + marker.len()..value.len());
        }
    }

    let separator = value
        .iter()
        .position(|byte| *byte == b';' || byte.is_ascii_whitespace())
        .unwrap_or(value.len());

    let comparison = (0..separator)
        .find(|&index| match value[index] {
            b'=' => value.get(index + 1) == Some(&b'='),
            b'!' | b'~' => value.get(index + 1) == Some(&b'='),
            b'>' | b'<' => true,
            _ => false,
        })
        .unwrap_or(separator);

    Ok(0..comparison)
}

fn compare_requirements(left: &Requirement<'_>, right: &Requirement<'_>) -> Ordering {
    let names = left.value[left.name.clone()]
        .iter()
        .map(u8::to_ascii_lowercase)
        .cmp(
            right.value[right.name.clone()]
                .iter()
                .map(u8::to_ascii_lowercase),
        );

    names.then_with(|| left.comments.is_empty().cmp(&right.comments.is_empty()))
}

fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    haystack
        .windows(needle.len())
        .position(|window| window == needle)
}

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

    #[test]
    fn fixed_contents_matches_expected_behavior() -> Result<()> {
        let cases: &[(&[u8], &[u8])] = &[
            (b"", b""),
            (b"\n", b"\n"),
            (b" \t", b" \t"),
            (b"# intentionally empty\n", b"# intentionally empty\n"),
            (b"foo\n# comment at end\n", b"foo\n# comment at end\n"),
            (b"foo\nbar\n", b"bar\nfoo\n"),
            (b"bar\nfoo\n", b"bar\nfoo\n"),
            (b"a\nc\nb\n", b"a\nb\nc\n"),
            (b"a\nc\nb", b"a\nb\nc\n"),
            (b"a\nb\nc", b"a\nb\nc\n"),
            (
                b"#comment1\nfoo\n#comment2\nbar\n",
                b"#comment2\nbar\n#comment1\nfoo\n",
            ),
            (
                b"#comment1\nbar\n#comment2\nfoo\n",
                b"#comment1\nbar\n#comment2\nfoo\n",
            ),
            (b"#comment\n\nfoo\nbar\n", b"#comment\n\nbar\nfoo\n"),
            (b"#comment\n\nbar\nfoo\n", b"#comment\n\nbar\nfoo\n"),
            (
                b"foo\n\t#comment with indent\nbar\n",
                b"\t#comment with indent\nbar\nfoo\n",
            ),
            (
                b"bar\n\t#comment with indent\nfoo\n",
                b"bar\n\t#comment with indent\nfoo\n",
            ),
            (b"\nfoo\nbar\n", b"bar\n\nfoo\n"),
            (b"\nbar\nfoo\n", b"\nbar\nfoo\n"),
            (
                b"pyramid-foo==1\npyramid>=2\n",
                b"pyramid>=2\npyramid-foo==1\n",
            ),
            (
                b"a==1\nc>=1\nbbbb!=1\nc-a>=1;python_version>=\"3.6\"\ne>=2\nd>2\ng<2\nf<=2\n",
                b"a==1\nbbbb!=1\nc>=1\nc-a>=1;python_version>=\"3.6\"\nd>2\ne>=2\nf<=2\ng<2\n",
            ),
            (b"a==1\nb==1\na==1\n", b"a==1\nb==1\n"),
            (
                b"a==1\nb==1\n#comment about a\na==1\n",
                b"#comment about a\na==1\nb==1\n",
            ),
            (
                b"ocflib\nDjango\nPyMySQL\n",
                b"Django\nocflib\nPyMySQL\n",
            ),
            (
                b"-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n",
                b"Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n",
            ),
            (
                b"bar\npkg-resources==0.0.0\nfoo\n",
                b"bar\nfoo\n",
            ),
            (
                b"foo\npkg-resources==0.0.0\nbar\n",
                b"bar\nfoo\n",
            ),
            (
                b"bar\npkg_resources==0.0.0\nfoo\n",
                b"bar\nfoo\n",
            ),
            (
                b"foo\npkg_resources==0.0.0\nbar\n",
                b"bar\nfoo\n",
            ),
            (
                b"git+ssh://git_url@tag#egg=ocflib\nDjango\nijk\n",
                b"Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n",
            ),
            (
                b"b==1.0.0\nc=2.0.0 \\\n --hash=sha256:abcd\na=3.0.0 \\\n  --hash=sha256:a1b1c1d1",
                b"a=3.0.0 \\\n  --hash=sha256:a1b1c1d1\nb==1.0.0\nc=2.0.0 \\\n --hash=sha256:abcd\n",
            ),
            (
                b"a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n",
                b"a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n",
            ),
            (b"foo\r\nbar\r\n", b"bar\r\nfoo\r\n"),
            (b"# header\nfoo \\\n\nbar\n", b"# header\n\nbar\n"),
            (
                b"zeta\n-e git+ssh://url \\\n --config=#egg=Alpha\n",
                b"-e git+ssh://url \\\n --config=#egg=Alpha\nzeta\n",
            ),
            (
                b"b\na=1 \\\n --hash=x\na=1 \\\n --hash=x\n",
                b"a=1 \\\n --hash=x\nb\n",
            ),
        ];

        for &(before, expected) in cases {
            let fixed = fixed_contents(before.to_vec())?;
            assert_eq!(fixed.as_deref().unwrap_or(before), expected);
        }

        for &(before, expected) in &[
            (
                b"  requests==2\n".as_slice(),
                "requirement entry starts with whitespace",
            ),
            (
                b";requests==2\n".as_slice(),
                "requirement entry starts with a semicolon",
            ),
        ] {
            assert_eq!(
                fixed_contents(before.to_vec()).unwrap_err().to_string(),
                expected
            );
        }

        Ok(())
    }
}


# `no_std` and Feature Verification 🔴

> **What you'll learn:**
> - Verifying feature combinations systematically with `cargo-hack`
> - The three layers of Rust: `core` vs `alloc` vs `std` and when to use each
> - Building `no_std` crates with custom panic handlers and allocators
> - Testing `no_std` code on host and with QEMU
>
> **Cross-references:** [Windows & Conditional Compilation](ch10-windows-and-conditional-compilation.md) — the platform half of this topic · [Cross-Compilation](ch02-cross-compilation-one-source-many-target.md) — cross-compiling to ARM and embedded targets · [Miri and Sanitizers](ch05-miri-valgrind-and-sanitizers-verifying-u.md) — verifying `unsafe` code in `no_std` environments · [Build Scripts](ch01-build-scripts-buildrs-in-depth.md) — `cfg` flags emitted by `build.rs`

Rust runs everywhere from 8-bit microcontrollers to cloud servers. This chapter
covers the foundation: stripping the standard library with `#![no_std]` and
verifying that your feature combinations actually compile.

### Verifying Feature Combinations with `cargo-hack`

[`cargo-hack`](https://github.com/taiki-e/cargo-hack) tests all feature
combinations systematically — essential for crates with `#[cfg(...)]` code:

```bash
# Install
cargo install cargo-hack

# Check that every feature compiles individually
cargo hack check --each-feature --workspace

# The nuclear option: test ALL feature combinations (exponential!)
# Only practical for crates with <8 features.
cargo hack check --feature-powerset --workspace

# Practical compromise: test each feature alone + all features + no features
cargo hack check --each-feature --workspace --no-dev-deps
cargo check --workspace --all-features
cargo check --workspace --no-default-features
```

**Why this matters for the project:**

If you add platform features (`linux`, `windows`, `direct-ipmi`, `direct-accel-api`),
`cargo-hack` catches combinations that break:

```toml
# Example: features that gate platform code
[features]
default = ["linux"]
linux = []                          # Linux-specific hardware access
windows = ["dep:windows-sys"]       # Windows-specific APIs
direct-ipmi = []                    # unsafe IPMI ioctl (ch05)
direct-accel-api = []                    # unsafe accel-mgmt FFI (ch05)
```

```bash
# Verify all features compile in isolation AND together
cargo hack check --each-feature -p diag_tool
# Catches: "feature 'windows' doesn't compile without 'direct-ipmi'"
# Catches: "#[cfg(feature = \"linux\")] has a typo — it's 'lnux'"
```

**CI integration:**

```yaml
# Add to CI pipeline (fast — just compilation checks)
- name: Feature matrix check
  run: cargo hack check --each-feature --workspace --no-dev-deps
```

> **Rule of thumb**: Run `cargo hack check --each-feature` in CI for any crate
> with 2+ features. Run `--feature-powerset` only for core library crates with
> <8 features — it's exponential ($2^n$ combinations).

### `no_std` — When and Why

`#![no_std]` tells the compiler: "don't link the standard library." Your
crate can only use `core` (and optionally `alloc`). Why would you want this?

| Scenario | Why `no_std` |
|----------|-------------|
| Embedded firmware (ARM Cortex-M, RISC-V) | No OS, no heap, no file system |
| UEFI diagnostics tool | Pre-boot environment, no OS APIs |
| Kernel modules | Kernel space can't use userspace `std` |
| WebAssembly (WASM) | Minimize binary size, no OS dependencies |
| Bootloaders | Run before any OS exists |
| Shared library with C interface | Avoid Rust runtime in callers |

**For hardware diagnostics**, `no_std` becomes relevant when building:
- UEFI-based pre-boot diagnostic tools (before the OS loads)
- BMC firmware diagnostics (resource-constrained ARM SoCs)
- Kernel-level PCIe diagnostics (kernel module or eBPF probe)

### `core` vs `alloc` vs `std` — The Three Layers

```text
┌─────────────────────────────────────────────────────────────┐
│ std                                                         │
│  Everything in core + alloc, PLUS:                          │
│  • File I/O (std::fs, std::io)                              │
│  • Networking (std::net)                                    │
│  • Threads (std::thread)                                    │
│  • Time (std::time)                                         │
│  • Environment (std::env)                                   │
│  • Process (std::process)                                   │
│  • OS-specific (std::os::unix, std::os::windows)            │
├─────────────────────────────────────────────────────────────┤
│ alloc          (available with #![no_std] + extern crate    │
│                 alloc, if you have a global allocator)       │
│  • String, Vec, Box, Rc, Arc                                │
│  • BTreeMap, BTreeSet                                       │
│  • format!() macro                                          │
│  • Collections and smart pointers that need heap            │
├─────────────────────────────────────────────────────────────┤
│ core           (always available, even in #![no_std])        │
│  • Primitive types (u8, bool, char, etc.)                    │
│  • Option, Result                                           │
│  • Iterator, slice, array, str (slices, not String)         │
│  • Traits: Clone, Copy, Debug, Display, From, Into          │
│  • Atomics (core::sync::atomic)                             │
│  • Cell, RefCell (core::cell)  — Pin (core::pin)            │
│  • core::fmt (formatting without allocation)                │
│  • core::mem, core::ptr (low-level memory operations)       │
│  • Math: core::num, basic arithmetic                        │
└─────────────────────────────────────────────────────────────┘
```

**What you lose without `std`:**
- No `HashMap` (requires a hasher — use `BTreeMap` from `alloc`, or `hashbrown`)
- No `println!()` (requires stdout — use `core::fmt::Write` to a buffer)
- No `std::error::Error` (stabilized in `core` since Rust 1.81, but many
  ecosystems haven't migrated)
- No file I/O, no networking, no threads (unless provided by a platform HAL)
- No `Mutex` (use `spin::Mutex` or platform-specific locks)

### Building a `no_std` Crate

```rust
// src/lib.rs — a no_std library crate
#![no_std]

// Optionally use heap allocation
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;

/// Temperature reading from a thermal sensor.
/// This struct works in any environment — bare metal to Linux.
#[derive(Clone, Copy, Debug)]
pub struct Temperature {
    /// Raw sensor value (0.0625°C per LSB for typical I2C sensors)
    raw: u16,
}

impl Temperature {
    pub const fn from_raw(raw: u16) -> Self {
        Self { raw }
    }

    /// Convert to degrees Celsius (fixed-point, no FPU required)
    pub const fn millidegrees_c(&self) -> i32 {
        (self.raw as i32) * 625 / 10 // 0.0625°C resolution
    }

    pub fn degrees_c(&self) -> f32 {
        self.raw as f32 * 0.0625
    }
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let md = self.millidegrees_c();
        // Handle sign correctly for values between -0.999°C and -0.001°C
        // where md / 1000 == 0 but the value is negative.
        if md < 0 && md > -1000 {
            write!(f, "-0.{:03}°C", (-md) % 1000)
        } else {
            write!(f, "{}.{:03}°C", md / 1000, (md % 1000).abs())
        }
    }
}

/// Parse space-separated temperature values.
/// Uses alloc — requires a global allocator.
pub fn parse_temperatures(input: &str) -> Vec<Temperature> {
    input
        .split_whitespace()
        .filter_map(|s| s.parse::<u16>().ok())
        .map(Temperature::from_raw)
        .collect()
}

/// Format without allocation — writes directly to a buffer.
/// Works in `core`-only environments (no alloc, no heap).
pub fn format_temp_into(temp: &Temperature, buf: &mut [u8]) -> usize {
    use core::fmt::Write;
    struct SliceWriter<'a> {
        buf: &'a mut [u8],
        pos: usize,
    }
    impl<'a> Write for SliceWriter<'a> {
        fn write_str(&mut self, s: &str) -> fmt::Result {
            let bytes = s.as_bytes();
            let remaining = self.buf.len() - self.pos;
            if bytes.len() > remaining {
                // Buffer full — signal the error instead of silently truncating.
                // Callers can check the returned pos for partial writes.
                return Err(fmt::Error);
            }
            self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
            self.pos += bytes.len();
            Ok(())
        }
    }
    let mut w = SliceWriter { buf, pos: 0 };
    let _ = write!(w, "{}", temp);
    w.pos
}
```

```toml
# Cargo.toml for a no_std crate
[package]
name = "thermal-sensor"
version = "0.1.0"
edition = "2021"

[features]
default = ["alloc"]
alloc = []    # Enable Vec, String, etc.
std = []      # Enable full std (implies alloc)

[dependencies]
# Use no_std-compatible crates
serde = { version = "1.0", default-features = false, features = ["derive"] }
# ↑ default-features = false drops std dependency!
```

> **Key crate pattern**: Many popular crates (serde, log, rand, embedded-hal)
> support `no_std` via `default-features = false`. Always check whether a
> dependency requires `std` before using it in a `no_std` context. Note that
> some crates (e.g., `regex`) require at least `alloc` and don't work in
> `core`-only environments.

### Custom Panic Handlers and Allocators

In `#![no_std]` binaries (not libraries), you must provide a panic handler
and optionally a global allocator:

```rust
// src/main.rs — a no_std binary (e.g., UEFI diagnostic)
#![no_std]
#![no_main]

extern crate alloc;

use core::panic::PanicInfo;

// Required: what to do on panic (no stack unwinding available)
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // In embedded: blink an LED, write to UART, hang
    // In UEFI: write to console, halt
    // Minimal: just loop forever
    loop {
        core::hint::spin_loop();
    }
}

// Required if using alloc: provide a global allocator
use alloc::alloc::{GlobalAlloc, Layout};

struct BumpAllocator {
    // Simple bump allocator for embedded/UEFI
    // In practice, use a crate like `linked_list_allocator` or `embedded-alloc`
}

// WARNING: This is a non-functional placeholder! Calling alloc() will return
// null, causing immediate UB (the global allocator contract requires non-null
// returns for non-zero-sized allocations). In real code, use an established
// allocator crate:
//   - embedded-alloc (embedded targets)
//   - linked_list_allocator (UEFI / OS kernels)
//   - talc (general-purpose no_std)
unsafe impl GlobalAlloc for BumpAllocator {
    /// # Safety
    /// Layout must have non-zero size. Returns null (placeholder — will crash).
    unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
        // PLACEHOLDER — will crash! Replace with real allocation logic.
        core::ptr::null_mut()
    }
    /// # Safety
    /// `_ptr` must have been returned by `alloc` with a compatible layout.
    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
        // No-op for bump allocator
    }
}

#[global_allocator]
static ALLOCATOR: BumpAllocator = BumpAllocator {};

// Entry point (platform-specific, not fn main)
// For UEFI: #[entry] or efi_main
// For embedded: #[cortex_m_rt::entry]
```

### Testing `no_std` Code

Tests run on the host machine, which has `std`. The trick: your library is
`no_std`, but your test harness uses `std`:

```rust
// Your crate: #![no_std] in src/lib.rs
// But tests run under std automatically:

#[cfg(test)]
mod tests {
    use super::*;
    // std is available here — println!, assert!, Vec all work

    #[test]
    fn test_temperature_conversion() {
        let temp = Temperature::from_raw(800); // 50.0°C
        assert_eq!(temp.millidegrees_c(), 50000);
        assert!((temp.degrees_c() - 50.0).abs() < 0.01);
    }

    #[test]
    fn test_format_into_buffer() {
        let temp = Temperature::from_raw(800);
        let mut buf = [0u8; 32];
        let len = format_temp_into(&temp, &mut buf);
        let s = core::str::from_utf8(&buf[..len]).unwrap();
        assert_eq!(s, "50.000°C");
    }
}
```

**Testing on the actual target** (when `std` isn't available at all):

```bash
# Use defmt-test for on-device testing (embedded ARM)
# Use uefi-test-runner for UEFI targets
# Use QEMU for cross-architecture tests without hardware

# Run no_std library tests on host (always works):
cargo test --lib

# Verify no_std compilation against a no_std target:
cargo check --target thumbv7em-none-eabihf  # ARM Cortex-M
cargo check --target riscv32imac-unknown-none-elf  # RISC-V
```

### `no_std` Decision Tree

```mermaid
flowchart TD
    START["Does your code need<br/>the standard library?"] --> NEED_FS{"File system,<br/>network, threads?"}
    NEED_FS -->|"Yes"| USE_STD["Use std<br/>Normal application"]
    NEED_FS -->|"No"| NEED_HEAP{"Need heap allocation?<br/>Vec, String, Box"}
    NEED_HEAP -->|"Yes"| USE_ALLOC["#![no_std]<br/>extern crate alloc"]
    NEED_HEAP -->|"No"| USE_CORE["#![no_std]<br/>core only"]

    USE_ALLOC --> VERIFY["cargo-hack<br/>--each-feature"]
    USE_CORE --> VERIFY
    USE_STD --> VERIFY
    VERIFY --> TARGET{"Target has OS?"}
    TARGET -->|"Yes"| HOST_TEST["cargo test --lib<br/>Standard testing"]
    TARGET -->|"No"| CROSS_TEST["QEMU / defmt-test<br/>On-device testing"]

    style USE_STD fill:#91e5a3,color:#000
    style USE_ALLOC fill:#ffd43b,color:#000
    style USE_CORE fill:#ff6b6b,color:#000
```

### 🏋 Exercises

#### 🟡 Exercise 1: Feature Combination Verification

Install `cargo-hack` and run `cargo hack check --each-feature --workspace` on a project with multiple features. Does it find any broken combinations?

Solution

```bash
cargo install cargo-hack

# Check each feature individually
cargo hack check --each-feature --workspace --no-dev-deps

# If a feature combination fails:
# error[E0433]: failed to resolve: use of undeclared crate or module `std`
# → This means a feature gate is missing a #[cfg] guard

# Check all features + no features + each individually:
cargo hack check --each-feature --workspace
cargo check --workspace --all-features
cargo check --workspace --no-default-features
```

#### 🔴 Exercise 2: Build a `no_std` Library

Create a library crate that compiles with `#![no_std]`. Implement a simple stack-allocated ring buffer. Verify it compiles for `thumbv7em-none-eabihf` (ARM Cortex-M).

Solution

```rust
// lib.rs
#![no_std]

pub struct RingBuffer<const N: usize> {
    data: [u8; N],
    head: usize,
    len: usize,
}

impl<const N: usize> RingBuffer<N> {
    pub const fn new() -> Self {
        Self { data: [0; N], head: 0, len: 0 }
    }

    pub fn push(&mut self, byte: u8) -> bool {
        if self.len == N { return false; }
        let idx = (self.head + self.len) % N;
        self.data[idx] = byte;
        self.len += 1;
        true
    }

    pub fn pop(&mut self) -> Option<u8> {
        if self.len == 0 { return None; }
        let byte = self.data[self.head];
        self.head = (self.head + 1) % N;
        self.len -= 1;
        Some(byte)
    }
}

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

    #[test]
    fn push_pop() {
        let mut rb = RingBuffer::<4>::new();
        assert!(rb.push(1));
        assert!(rb.push(2));
        assert_eq!(rb.pop(), Some(1));
        assert_eq!(rb.pop(), Some(2));
        assert_eq!(rb.pop(), None);
    }
}
```

```bash
rustup target add thumbv7em-none-eabihf
cargo check --target thumbv7em-none-eabihf
# ✅ Compiles for bare-metal ARM
```

### Key Takeaways

- `cargo-hack --each-feature` is essential for any crate with conditional compilation — run it in CI
- `core` → `alloc` → `std` are layered: each adds capabilities but requires more runtime support
- Custom panic handlers and allocators are required for bare-metal `no_std` binaries
- Test `no_std` libraries on the host with `cargo test --lib` — no hardware needed
- Run `--feature-powerset` only for core libraries with <8 features — it's $2^n$ combinations

---