michal/tit

Diff

b2db8e312f8f360d3f97fad0

.github/workflows/ci.yml

Mode 100644100644; object 9d0d0500710b0e619c444441

@@ -34,10 +34,9 @@
           persist-credentials: false
       - run: cargo --version
       - run: cargo test --locked --all-targets --all-features
-      - run: cargo test --locked --release --test auth --test ssh --test account_lifecycle --test web_session --test repository_policy
-      - run: cargo test --locked --release --test git_repository --test git_http --test git_ssh --test public_routes
-      - run: cargo test --locked --release --test git_reads measures_bounded_search_without_an_index -- --ignored --nocapture
-      - run: cargo test --locked --release --test sqlite_workload -- --ignored --nocapture
+      - run: cargo test --locked --release --lib
+      - run: cargo test --locked --release --lib git_reads_tests::measures_bounded_search_without_an_index -- --ignored --nocapture
+      - run: cargo test --locked --release --lib sqlite_workload_tests:: -- --ignored --nocapture
       - run: cargo build --locked --release
       - run: wc -c target/release/tit
       - run: ./scripts/package-release target/release/tit dist

Cargo.toml

Mode 100644100644; object d345a7602e0aae0036c6d6ec

@@ -3,12 +3,21 @@
 version = "0.1.0"
 edition = "2024"
 rust-version = "1.96"
+autotests = false
 license = "MIT"
 description = "A small self-hosted collaborative development environment"
 
 [[bin]]
 name = "tit"
 path = "src/main.rs"
+
+[[test]]
+name = "cli"
+path = "tests/cli.rs"
+
+[[test]]
+name = "serve"
+path = "tests/serve.rs"
 
 [dependencies]
 ammonia = "4.1.4"

README.md

Mode 100644100644; object 01bda513ad510614f38501ef

@@ -266,12 +266,13 @@
 The ignored workload tests are explicit performance checks:
 
 ```text
-cargo test --locked --release --test git_reads \
-  measures_bounded_search_without_an_index -- --ignored --nocapture
-cargo test --locked --release --test metadata_search \
-  measures_bounded_repository_name_search_without_an_index \
+cargo test --locked --release --lib \
+  git_reads_tests::measures_bounded_search_without_an_index \
   -- --ignored --nocapture
-cargo test --locked --release --test sqlite_workload \
+cargo test --locked --release --lib \
+  metadata_search_tests::measures_bounded_repository_name_search_without_an_index \
+  -- --ignored --nocapture
+cargo test --locked --release --lib sqlite_workload_tests:: \
   -- --ignored --nocapture
 ```
 

src/account.rs

Mode 100644100644; object 8eca2830f84d322c6ba645f4

@@ -1,15 +1,16 @@
 use std::path::{Path, PathBuf};
-use std::time::{SystemTime, UNIX_EPOCH};
 
 use rand::TryRng;
 use sha2::{Digest, Sha256};
 use thiserror::Error;
 
 use crate::auth::{AuthError, SshPublicKey, validate_username};
+use crate::codec::encode_lower_hex;
 use crate::store::{
     AccountKeyAuthorization, AccountRecovery, InvitedAccount, KeyInspection, NewAuditEvent,
     NewSshKey, PublicProfile, Store, StoreError,
 };
+use crate::system::unix_timestamp;
 
 const INVITATION_PREFIX: &str = "tit-invite-v1:";
 const RECOVERY_PREFIX: &str = "tit-recovery-v1:";
@@ -432,10 +433,7 @@
         .map_err(|_| AccountError::Random)?;
     let mut value = String::with_capacity(prefix.len() + SECRET_BYTES * 2);
     value.push_str(prefix);
-    for byte in bytes {
-        use std::fmt::Write as _;
-        write!(value, "{byte:02x}").expect("writing to a string cannot fail");
-    }
+    value.push_str(&encode_lower_hex(&bytes));
     Ok(value)
 }
 
@@ -444,12 +442,7 @@
 }
 
 fn now() -> Result<i64, AccountError> {
-    SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .map_err(|_| AccountError::Clock)?
-        .as_secs()
-        .try_into()
-        .map_err(|_| AccountError::Clock)
+    unix_timestamp().ok_or(AccountError::Clock)
 }
 
 #[derive(Debug, Error)]

src/admin.rs

Mode 100644100644; object 9773e8d9a77895a52f7afb01

@@ -1,9 +1,7 @@
 use std::fs;
 use std::path::{Path, PathBuf};
-use std::time::{SystemTime, UNIX_EPOCH};
 
 use gix::hash::Kind;
-use rand::TryRng;
 use thiserror::Error;
 
 use crate::auth::{AuthError, validate_username};
@@ -15,6 +13,7 @@
     AuditContext, NewRepository, NewRepositoryReference, RepositoryOrigin, RepositoryRecord, Store,
     StoreError,
 };
+use crate::system::{random_lower_hex, unix_timestamp};
 
 const ADMIN_ACTOR: &str = "admin-cli";
 
@@ -425,20 +424,11 @@
 }
 
 fn random_id() -> Result<String, AdminError> {
-    let mut bytes = [0_u8; 16];
-    rand::rngs::SysRng
-        .try_fill_bytes(&mut bytes)
-        .map_err(|_| AdminError::Random)?;
-    Ok(bytes.iter().map(|byte| format!("{byte:02x}")).collect())
+    random_lower_hex::<16>().ok_or(AdminError::Random)
 }
 
 fn timestamp() -> Result<i64, AdminError> {
-    SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .map_err(|_| AdminError::Clock)?
-        .as_secs()
-        .try_into()
-        .map_err(|_| AdminError::Clock)
+    unix_timestamp().ok_or(AdminError::Clock)
 }
 
 fn object_format_name(kind: Kind) -> Result<&'static str, AdminError> {

src/app.rs

Mode 100644; object 3f2282587361

@@ -1,0 +1,427 @@
+use std::process::ExitCode;
+use std::{io, io::Write};
+
+use crate::{
+    account, admin, backup, bootstrap,
+    cli::{
+        AccountCommand, AdminCommand, Cli, CollaboratorRole, Command, InspectCommand,
+        RepairCommand, RepositoryCommand, RepositoryVisibility, SetupCommand,
+    },
+    config, control, diagnostics, instance, repair, serve, store,
+};
+use clap::Parser;
+
+pub async fn run() -> ExitCode {
+    let cli = match Cli::try_parse() {
+        Ok(cli) => cli,
+        Err(error) => {
+            let code = error.exit_code();
+            let _ = error.print();
+            return ExitCode::from(u8::try_from(code).unwrap_or(2));
+        }
+    };
+
+    if let Some(Command::Restore { archive, target }) = &cli.command {
+        return run_restore(archive, target);
+    }
+
+    match config::load(&cli) {
+        Ok(config) => match cli.command {
+            None => ExitCode::SUCCESS,
+            Some(Command::Serve) => match serve::run(&config).await {
+                Ok(()) => ExitCode::SUCCESS,
+                Err(error) => {
+                    eprintln!("tit: {error}");
+                    ExitCode::FAILURE
+                }
+            },
+            Some(Command::InviteCode) => {
+                match control::request_invitation(&config.instance_dir).await {
+                    Ok(code) => match writeln!(io::stdout().lock(), "Signup code: {code}") {
+                        Ok(()) => ExitCode::SUCCESS,
+                        Err(error) => {
+                            eprintln!("tit: cannot write the signup code: {error}");
+                            ExitCode::FAILURE
+                        }
+                    },
+                    Err(error) => {
+                        eprintln!("tit: {error}");
+                        ExitCode::FAILURE
+                    }
+                }
+            }
+            Some(Command::Doctor { backups }) => match diagnostics::doctor(&config, &backups) {
+                Ok(()) => ExitCode::SUCCESS,
+                Err(error) => {
+                    eprintln!("tit: {error}");
+                    ExitCode::FAILURE
+                }
+            },
+            Some(Command::Inspect { command }) => run_inspect_command(&config, command),
+            Some(Command::Dump) => run_dump_command(&config),
+            Some(Command::Repair { command }) => {
+                let result = match command {
+                    RepairCommand::Intents => repair::intents(&config.instance_dir),
+                    RepairCommand::Quarantine => repair::quarantine(&config.instance_dir),
+                };
+                match result {
+                    Ok(()) => ExitCode::SUCCESS,
+                    Err(error) => {
+                        eprintln!("tit: {error}");
+                        ExitCode::FAILURE
+                    }
+                }
+            }
+            Some(Command::Maintenance { retention_days }) => {
+                match admin::maintain(&config.instance_dir, retention_days) {
+                    Ok(result) => match writeln!(
+                        io::stdout().lock(),
+                        "Pruned {} terminal records.",
+                        result.deleted
+                    ) {
+                        Ok(()) => ExitCode::SUCCESS,
+                        Err(error) => {
+                            eprintln!("tit: cannot write maintenance information: {error}");
+                            ExitCode::FAILURE
+                        }
+                    },
+                    Err(error) => {
+                        eprintln!("tit: {error}");
+                        ExitCode::FAILURE
+                    }
+                }
+            }
+            Some(Command::Backup { output }) => run_backup(&config, &output).await,
+            Some(Command::Restore { .. }) => {
+                unreachable!("the restore command runs before configuration is loaded")
+            }
+            Some(Command::Setup {
+                command:
+                    SetupCommand::Admin {
+                        username,
+                        ssh_public_key,
+                    },
+            }) => match bootstrap::setup_administrator(
+                &config.instance_dir,
+                &username,
+                &ssh_public_key,
+            ) {
+                Ok(recovery_code) => {
+                    let mut output = io::stdout().lock();
+                    match writeln!(output, "Recovery code: {recovery_code}") {
+                        Ok(()) => ExitCode::SUCCESS,
+                        Err(error) => {
+                            eprintln!("tit: cannot write the recovery code: {error}");
+                            ExitCode::FAILURE
+                        }
+                    }
+                }
+                Err(error) => {
+                    eprintln!("tit: {error}");
+                    ExitCode::FAILURE
+                }
+            },
+            Some(Command::Admin {
+                command: AdminCommand::Repository { command },
+            }) => run_repository_command(&config.instance_dir, command),
+            Some(Command::Admin {
+                command: AdminCommand::Account { command },
+            }) => run_account_command(&config.instance_dir, command),
+            Some(Command::Admin {
+                command: AdminCommand::Audit { limit },
+            }) => run_audit_command(&config.instance_dir, limit),
+        },
+        Err(error) => {
+            eprintln!("tit: {error}");
+            ExitCode::FAILURE
+        }
+    }
+}
+
+fn run_inspect_command(config: &config::Config, command: InspectCommand) -> ExitCode {
+    let result = match command {
+        InspectCommand::Account { username } => {
+            serialize_inspection(diagnostics::inspect_account(config, &username))
+        }
+        InspectCommand::Repository { owner, slug } => {
+            serialize_inspection(diagnostics::inspect_repository(config, &owner, &slug))
+        }
+        InspectCommand::Intent { id } => {
+            serialize_inspection(diagnostics::inspect_intent(config, &id))
+        }
+    };
+    match result {
+        Ok(line) => match writeln!(io::stdout().lock(), "{line}") {
+            Ok(()) => ExitCode::SUCCESS,
+            Err(error) => {
+                eprintln!("tit: cannot write inspect information: {error}");
+                ExitCode::FAILURE
+            }
+        },
+        Err(error) => {
+            eprintln!("tit: {error}");
+            ExitCode::FAILURE
+        }
+    }
+}
+
+fn serialize_inspection(
+    result: Result<impl serde::Serialize, diagnostics::DiagnosticError>,
+) -> Result<String, Box<dyn std::error::Error>> {
+    Ok(serde_json::to_string(&result?)?)
+}
+
+fn run_dump_command(config: &config::Config) -> ExitCode {
+    let result = (|| -> Result<(), Box<dyn std::error::Error>> {
+        let mut output = io::stdout().lock();
+        let mut output_error = None;
+        diagnostics::dump(config, |row| {
+            let result = serde_json::to_writer(&mut output, &row)
+                .map_err(|error| Box::new(error) as Box<dyn std::error::Error>)
+                .and_then(|()| {
+                    writeln!(output).map_err(|error| Box::new(error) as Box<dyn std::error::Error>)
+                });
+            match result {
+                Ok(()) => true,
+                Err(error) => {
+                    output_error = Some(error);
+                    false
+                }
+            }
+        })?;
+        if let Some(error) = output_error {
+            return Err(error);
+        }
+        Ok(())
+    })();
+    match result {
+        Ok(()) => ExitCode::SUCCESS,
+        Err(error) => {
+            eprintln!("tit: {error}");
+            ExitCode::FAILURE
+        }
+    }
+}
+
+async fn run_backup(config: &config::Config, output: &std::path::Path) -> ExitCode {
+    let result = match backup::create_offline(&config.instance_dir, &config.config_path, output) {
+        Ok(()) => Ok(()),
+        Err(backup::BackupError::Instance(instance::InstanceError::Locked)) => {
+            control::request_backup(&config.instance_dir, output)
+                .await
+                .map_err(|error| Box::new(error) as Box<dyn std::error::Error>)
+        }
+        Err(error) => Err(Box::new(error) as Box<dyn std::error::Error>),
+    };
+    match result {
+        Ok(()) => match writeln!(
+            io::stdout().lock(),
+            "Backup: {}\nWarning: This backup contains credentials.",
+            output.display()
+        ) {
+            Ok(()) => ExitCode::SUCCESS,
+            Err(error) => {
+                eprintln!("tit: cannot write backup information: {error}");
+                ExitCode::FAILURE
+            }
+        },
+        Err(error) => {
+            eprintln!("tit: {error}");
+            ExitCode::FAILURE
+        }
+    }
+}
+
+fn run_restore(archive: &std::path::Path, target: &std::path::Path) -> ExitCode {
+    match backup::restore(archive, target) {
+        Ok(()) => match writeln!(
+            io::stdout().lock(),
+            "Restore: {}\nThe restored instance is not active.",
+            target.display()
+        ) {
+            Ok(()) => ExitCode::SUCCESS,
+            Err(error) => {
+                eprintln!("tit: cannot write restore information: {error}");
+                ExitCode::FAILURE
+            }
+        },
+        Err(error) => {
+            eprintln!("tit: {error}");
+            ExitCode::FAILURE
+        }
+    }
+}
+
+fn run_audit_command(instance_dir: &std::path::Path, limit: usize) -> ExitCode {
+    let result = (|| -> Result<(), Box<dyn std::error::Error>> {
+        let _lock = instance::InstanceLock::acquire(instance_dir)?;
+        let database = instance::prepare_database(instance_dir)?;
+        let events = store::Store::open(&database)?.audit_events(limit)?;
+        let mut output = io::stdout().lock();
+        for event in events {
+            writeln!(output, "id={}", event.id)?;
+            writeln!(output, "action={}", event.action)?;
+            writeln!(output, "actor={}", event.actor)?;
+            writeln!(output, "target={}", event.target)?;
+            writeln!(output, "outcome={}", event.outcome)?;
+            writeln!(output, "correlation-id={}", event.correlation_id)?;
+            writeln!(output, "created-at={}", event.created_at)?;
+            writeln!(output)?;
+        }
+        Ok(())
+    })();
+    match result {
+        Ok(()) => ExitCode::SUCCESS,
+        Err(error) => {
+            eprintln!("tit: {error}");
+            ExitCode::FAILURE
+        }
+    }
+}
+
+fn run_account_command(instance_dir: &std::path::Path, command: AccountCommand) -> ExitCode {
+    let result = (|| -> Result<Option<String>, Box<dyn std::error::Error>> {
+        let _lock = instance::InstanceLock::acquire(instance_dir)?;
+        let database = instance::prepare_database(instance_dir)?;
+        let accounts = account::AccountService::new(database);
+        let correlation_id = format!("{:032x}", rand::random::<u128>());
+        match command {
+            AccountCommand::KeyAdd {
+                username,
+                label,
+                ssh_public_key,
+            } => {
+                let fingerprint = accounts.add_key(
+                    &username,
+                    &label,
+                    &ssh_public_key,
+                    "admin-cli",
+                    &correlation_id,
+                )?;
+                Ok(Some(fingerprint))
+            }
+            AccountCommand::KeyRevoke {
+                username,
+                fingerprint,
+            } => {
+                accounts.revoke_key(&username, &fingerprint, "admin-cli", &correlation_id)?;
+                Ok(None)
+            }
+            AccountCommand::Suspend { username } => {
+                accounts.suspend(&username, true, "admin-cli", &correlation_id)?;
+                Ok(None)
+            }
+            AccountCommand::Resume { username } => {
+                accounts.suspend(&username, false, "admin-cli", &correlation_id)?;
+                Ok(None)
+            }
+        }
+    })();
+    match result {
+        Ok(Some(fingerprint)) => match writeln!(io::stdout().lock(), "fingerprint={fingerprint}") {
+            Ok(()) => ExitCode::SUCCESS,
+            Err(error) => {
+                eprintln!("tit: cannot write account information: {error}");
+                ExitCode::FAILURE
+            }
+        },
+        Ok(None) => ExitCode::SUCCESS,
+        Err(error) => {
+            eprintln!("tit: {error}");
+            ExitCode::FAILURE
+        }
+    }
+}
+
+fn run_repository_command(instance_dir: &std::path::Path, command: RepositoryCommand) -> ExitCode {
+    let result = match command {
+        RepositoryCommand::Create { owner, slug } => {
+            admin::create_repository(instance_dir, &owner, &slug, gix::hash::Kind::Sha1)
+        }
+        RepositoryCommand::Import {
+            owner,
+            slug,
+            source,
+        } => admin::import_repository(instance_dir, &owner, &slug, &source),
+        RepositoryCommand::Rename {
+            owner,
+            old_slug,
+            new_slug,
+        } => admin::rename_repository(instance_dir, &owner, &old_slug, &new_slug),
+        RepositoryCommand::Archive { owner, slug } => {
+            admin::archive_repository(instance_dir, &owner, &slug)
+        }
+        RepositoryCommand::Visibility {
+            owner,
+            slug,
+            visibility,
+        } => admin::set_repository_visibility(
+            instance_dir,
+            &owner,
+            &slug,
+            match visibility {
+                RepositoryVisibility::Public => "public",
+                RepositoryVisibility::Private => "private",
+            },
+        ),
+        RepositoryCommand::CollaboratorSet {
+            owner,
+            slug,
+            username,
+            role,
+        } => admin::set_repository_collaborator(
+            instance_dir,
+            &owner,
+            &slug,
+            &username,
+            match role {
+                CollaboratorRole::Maintainer => "maintainer",
+                CollaboratorRole::Writer => "writer",
+                CollaboratorRole::Reader => "reader",
+            },
+        ),
+        RepositoryCommand::CollaboratorRemove {
+            owner,
+            slug,
+            username,
+        } => admin::remove_repository_collaborator(instance_dir, &owner, &slug, &username),
+        RepositoryCommand::Inspect { owner, slug } => {
+            admin::inspect_repository(instance_dir, &owner, &slug)
+        }
+    };
+
+    match result {
+        Ok(repository) => {
+            let path = match admin::repository_path(instance_dir, &repository) {
+                Ok(path) => path,
+                Err(error) => {
+                    eprintln!("tit: {error}");
+                    return ExitCode::FAILURE;
+                }
+            };
+            let archived_at = repository
+                .archived_at
+                .map_or_else(|| "-".to_owned(), |value| value.to_string());
+            let mut output = io::stdout().lock();
+            let written = writeln!(output, "id={}", repository.id)
+                .and_then(|()| writeln!(output, "owner={}", repository.owner))
+                .and_then(|()| writeln!(output, "slug={}", repository.slug))
+                .and_then(|()| writeln!(output, "visibility={}", repository.visibility))
+                .and_then(|()| writeln!(output, "state={}", repository.state))
+                .and_then(|()| writeln!(output, "created-at={}", repository.created_at))
+                .and_then(|()| writeln!(output, "archived-at={archived_at}"))
+                .and_then(|()| writeln!(output, "path={}", path.display()));
+            match written {
+                Ok(()) => ExitCode::SUCCESS,
+                Err(error) => {
+                    eprintln!("tit: cannot write repository information: {error}");
+                    ExitCode::FAILURE
+                }
+            }
+        }
+        Err(error) => {
+            eprintln!("tit: {error}");
+            ExitCode::FAILURE
+        }
+    }
+}

src/auth.rs

Mode 100644100644; object 77d812fb2775b7adc33b23b9

@@ -5,6 +5,8 @@
 use sha2::{Digest, Sha256};
 use ssh_key::{Algorithm, EcdsaCurve, HashAlg, PublicKey, SshSig};
 use thiserror::Error;
+
+use crate::codec::{decode_lower_hex, encode_lower_hex};
 use url::Url;
 
 const CHALLENGE_HEADER: &str = "tit-auth-v1";
@@ -70,7 +72,7 @@
 ) -> String {
     format!(
         "{KEYLESS_CHALLENGE_HEADER}\npurpose={CHALLENGE_PURPOSE}\norigin={origin}\nusername={username}\nnonce={}\nissued-at={issued_at}\nexpires-at={expires_at}\n",
-        encode_hex(nonce)
+        encode_lower_hex(nonce)
     )
 }
 
@@ -232,7 +234,7 @@
     format!(
         "{CHALLENGE_HEADER}\npurpose={CHALLENGE_PURPOSE}\norigin={origin}\nusername={username}\nfingerprint={}\nnonce={}\nissued-at={issued_at}\nexpires-at={expires_at}\n",
         key.fingerprint(),
-        encode_hex(nonce)
+        encode_lower_hex(nonce)
     )
 }
 
@@ -489,31 +491,10 @@
     Sha256::digest(nonce).into()
 }
 
-fn encode_hex(bytes: &[u8]) -> String {
-    const HEX: &[u8; 16] = b"0123456789abcdef";
-    let mut encoded = String::with_capacity(bytes.len() * 2);
-    for byte in bytes {
-        encoded.push(char::from(HEX[usize::from(byte >> 4)]));
-        encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
-    }
-    encoded
-}
-
 fn decode_hex(value: &str) -> Result<[u8; NONCE_BYTES], AuthError> {
-    if value.len() != NONCE_BYTES * 2 {
-        return Err(AuthError::MalformedChallenge);
-    }
-    let mut bytes = [0_u8; NONCE_BYTES];
-    for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
-        bytes[index] = (decode_hex_digit(pair[0])? << 4) | decode_hex_digit(pair[1])?;
-    }
-    Ok(bytes)
-}
-
-fn decode_hex_digit(value: u8) -> Result<u8, AuthError> {
-    match value {
-        b'0'..=b'9' => Ok(value - b'0'),
-        b'a'..=b'f' => Ok(value - b'a' + 10),
-        _ => Err(AuthError::MalformedChallenge),
-    }
+    let decoded = decode_lower_hex(value.as_bytes()).ok_or(AuthError::MalformedChallenge)?;
+    let decoded: [u8; NONCE_BYTES] = decoded
+        .try_into()
+        .map_err(|_| AuthError::MalformedChallenge)?;
+    Ok(decoded)
 }

src/backup.rs

Mode 100644100644; object 1b4e679f5af0660d5149e67c

@@ -11,6 +11,7 @@
 use tar::{Archive, Builder, EntryType, Header};
 use thiserror::Error;
 
+use crate::codec::{decode_lower_hex, encode_lower_hex};
 use crate::git::repository::{GitRepository, GitRepositoryError};
 use crate::instance::{InstanceError, InstanceLock, REPOSITORY_DIRECTORY};
 use crate::maintenance::MaintenanceGate;
@@ -654,37 +655,11 @@
 }
 
 fn encode_hex(bytes: impl AsRef<[u8]>) -> String {
-    const HEX: &[u8; 16] = b"0123456789abcdef";
-    let bytes = bytes.as_ref();
-    let mut encoded = String::with_capacity(bytes.len() * 2);
-    for byte in bytes {
-        encoded.push(char::from(HEX[(byte >> 4) as usize]));
-        encoded.push(char::from(HEX[(byte & 0x0f) as usize]));
-    }
-    encoded
+    encode_lower_hex(bytes.as_ref())
 }
 
 fn decode_hex(encoded: &str) -> Result<Vec<u8>, BackupError> {
-    if !encoded.len().is_multiple_of(2) {
-        return Err(BackupError::InvalidManifest);
-    }
-    encoded
-        .as_bytes()
-        .chunks_exact(2)
-        .map(|pair| {
-            let high = decode_nibble(pair[0])?;
-            let low = decode_nibble(pair[1])?;
-            Ok((high << 4) | low)
-        })
-        .collect()
-}
-
-fn decode_nibble(byte: u8) -> Result<u8, BackupError> {
-    match byte {
-        b'0'..=b'9' => Ok(byte - b'0'),
-        b'a'..=b'f' => Ok(byte - b'a' + 10),
-        _ => Err(BackupError::InvalidManifest),
-    }
+    decode_lower_hex(encoded.as_bytes()).ok_or(BackupError::InvalidManifest)
 }
 
 struct DigestWriter<'a>(&'a mut Sha256);

src/codec.rs

Mode 100644; object 1a9295a4a918

@@ -1,0 +1,43 @@
+#![allow(
+    dead_code,
+    reason = "integration test crates use only the codec policies required by their imported modules"
+)]
+
+pub(crate) fn encode_lower_hex(bytes: &[u8]) -> String {
+    const HEX: &[u8; 16] = b"0123456789abcdef";
+    let mut encoded = String::with_capacity(bytes.len().saturating_mul(2));
+    for byte in bytes {
+        encoded.push(char::from(HEX[usize::from(byte >> 4)]));
+        encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
+    }
+    encoded
+}
+
+pub(crate) fn decode_lower_hex(encoded: &[u8]) -> Option<Vec<u8>> {
+    decode_hex(encoded, false)
+}
+
+pub(crate) fn decode_ascii_hex(encoded: &[u8]) -> Option<Vec<u8>> {
+    decode_hex(encoded, true)
+}
+
+fn decode_hex(encoded: &[u8], uppercase: bool) -> Option<Vec<u8>> {
+    if !encoded.len().is_multiple_of(2) {
+        return None;
+    }
+    encoded
+        .chunks_exact(2)
+        .map(|pair| {
+            Some((decode_nibble(pair[0], uppercase)? << 4) | decode_nibble(pair[1], uppercase)?)
+        })
+        .collect()
+}
+
+fn decode_nibble(byte: u8, uppercase: bool) -> Option<u8> {
+    match byte {
+        b'0'..=b'9' => Some(byte - b'0'),
+        b'a'..=b'f' => Some(byte - b'a' + 10),
+        b'A'..=b'F' if uppercase => Some(byte - b'A' + 10),
+        _ => None,
+    }
+}

src/control.rs

Mode 100644100644; object 583aac731c2a1091d56f52d1

@@ -13,6 +13,8 @@
 
 use crate::account::{AccountError, AccountService};
 use crate::backup::OnlineBackupService;
+use crate::codec::{decode_lower_hex, encode_lower_hex};
+use crate::telemetry::Telemetry;
 
 pub(crate) const CONTROL_SOCKET_FILE: &str = "control.sock";
 const REQUEST: &[u8] = b"invite-code\n";
@@ -36,21 +38,23 @@
         instance_dir: &Path,
         accounts: AccountService,
     ) -> Result<Self, ControlError> {
-        Self::start_inner(instance_dir, accounts, None)
+        Self::start_inner(instance_dir, accounts, None, Telemetry::default())
     }
 
-    pub(crate) fn start_with_backup(
+    pub(crate) fn start_with_backup_and_telemetry(
         instance_dir: &Path,
         accounts: AccountService,
         backup: OnlineBackupService,
+        telemetry: Telemetry,
     ) -> Result<Self, ControlError> {
-        Self::start_inner(instance_dir, accounts, Some(backup))
+        Self::start_inner(instance_dir, accounts, Some(backup), telemetry)
     }
 
     fn start_inner(
         instance_dir: &Path,
         accounts: AccountService,
         backup: Option<OnlineBackupService>,
+        telemetry: Telemetry,
     ) -> Result<Self, ControlError> {
         let path = instance_dir.join(CONTROL_SOCKET_FILE);
         refuse_existing_path(&path)?;
@@ -100,14 +104,25 @@
                         let (stream, _) = accepted.map_err(ControlError::Accept)?;
                         let service = accounts.clone();
                         let backup = backup.clone();
+                        let telemetry = telemetry.clone();
                         connections.spawn(async move {
-                            let _ = handle(stream, service, backup).await;
+                            if let Err(error) = handle(stream, service, backup).await {
+                                telemetry.failure("control.request", None, &error.to_string());
+                            }
                         });
                     },
-                    _ = connections.join_next(), if !connections.is_empty() => {}
+                    joined = connections.join_next(), if !connections.is_empty() => {
+                        if let Some(Err(error)) = joined {
+                            telemetry.failure("control.task", None, &error.to_string());
+                        }
+                    }
                 }
             }
-            while connections.join_next().await.is_some() {}
+            while let Some(joined) = connections.join_next().await {
+                if let Err(error) = joined {
+                    telemetry.failure("control.task", None, &error.to_string());
+                }
+            }
             Ok(())
         });
         Ok(Self { shutdown, task })
@@ -145,7 +160,7 @@
 
 pub(crate) async fn request_backup(instance_dir: &Path, output: &Path) -> Result<(), ControlError> {
     let mut request_bytes = BACKUP_REQUEST_PREFIX.to_vec();
-    request_bytes.extend_from_slice(encode_hex(output.as_os_str().as_bytes()).as_bytes());
+    request_bytes.extend_from_slice(encode_lower_hex(output.as_os_str().as_bytes()).as_bytes());
     request_bytes.push(b'\n');
     let response = request(instance_dir, &request_bytes, BACKUP_TIMEOUT).await?;
     if response == "ok" {
@@ -229,7 +244,7 @@
             stream.write_all(b"error backup-unavailable\n").await?;
             return Ok(());
         };
-        let output = match decode_hex(encoded) {
+        let output = match decode_lower_hex(encoded) {
             Some(path) => PathBuf::from(OsString::from_vec(path)),
             None => {
                 stream.write_all(b"error invalid-request\n").await?;
@@ -245,34 +260,6 @@
     }
     stream.shutdown().await?;
     Ok(())
-}
-
-fn encode_hex(bytes: &[u8]) -> String {
-    const HEX: &[u8; 16] = b"0123456789abcdef";
-    let mut encoded = String::with_capacity(bytes.len() * 2);
-    for byte in bytes {
-        encoded.push(char::from(HEX[(byte >> 4) as usize]));
-        encoded.push(char::from(HEX[(byte & 0x0f) as usize]));
-    }
-    encoded
-}
-
-fn decode_hex(encoded: &[u8]) -> Option<Vec<u8>> {
-    if !encoded.len().is_multiple_of(2) {
-        return None;
-    }
-    encoded
-        .chunks_exact(2)
-        .map(|pair| Some((decode_nibble(pair[0])? << 4) | decode_nibble(pair[1])?))
-        .collect()
-}
-
-fn decode_nibble(byte: u8) -> Option<u8> {
-    match byte {
-        b'0'..=b'9' => Some(byte - b'0'),
-        b'a'..=b'f' => Some(byte - b'a' + 10),
-        _ => None,
-    }
 }
 
 fn refuse_existing_path(path: &Path) -> Result<(), ControlError> {
@@ -430,10 +417,11 @@
         let backup_directory = TempDir::new().expect("create a backup directory");
         let output = backup_directory.path().join("instance.tar");
         let service = OnlineBackupService::new(directory.path().to_owned(), config, gate.clone());
-        let server = RunningControlServer::start_with_backup(
+        let server = RunningControlServer::start_with_backup_and_telemetry(
             directory.path(),
             AccountService::new(database),
             service,
+            Telemetry::default(),
         )
         .expect("start the control server");
 

src/feed_token.rs

Mode 100644100644; object 149f167735136a5ca8e821c3

@@ -1,14 +1,15 @@
 use std::path::{Path, PathBuf};
-use std::time::{SystemTime, UNIX_EPOCH};
 
 use rand::TryRng;
 use sha2::{Digest, Sha256};
 use thiserror::Error;
 
 use crate::auth::{AuthError, validate_username};
+use crate::codec::encode_lower_hex;
 use crate::store::{
     ActivityCursor, ActivityPage, FeedTokenRecord, Store, StoreError, TokenFeedPage,
 };
+use crate::system::unix_timestamp;
 
 const TOKEN_BYTES: usize = 32;
 
@@ -86,7 +87,7 @@
     rand::rngs::SysRng
         .try_fill_bytes(&mut bytes)
         .map_err(|_| FeedTokenError::Random)?;
-    Ok(encode_hex(&bytes))
+    Ok(encode_lower_hex(&bytes))
 }
 
 fn hash(token: &str) -> [u8; 32] {
@@ -107,22 +108,8 @@
     Ok(())
 }
 
-fn encode_hex(bytes: &[u8]) -> String {
-    let mut output = String::with_capacity(bytes.len() * 2);
-    for byte in bytes {
-        use std::fmt::Write as _;
-        write!(output, "{byte:02x}").expect("writing to a string cannot fail");
-    }
-    output
-}
-
 fn now() -> Result<i64, FeedTokenError> {
-    SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .map_err(|_| FeedTokenError::Clock)?
-        .as_secs()
-        .try_into()
-        .map_err(|_| FeedTokenError::Clock)
+    unix_timestamp().ok_or(FeedTokenError::Clock)
 }
 
 #[derive(Debug, Error)]

src/git/read.rs

Mode 100644100644; object 0aedb2803695619287c199cb

@@ -278,7 +278,13 @@
             .iter()
             .map(|id| {
                 budget.check()?;
-                Ok(self.read_commit(*id, &budget).ok())
+                match self.read_commit(*id, &budget) {
+                    Ok(commit) => Ok(Some(commit)),
+                    Err(ReadError::ObjectNotFound(_) | ReadError::WrongObjectKind { .. }) => {
+                        Ok(None)
+                    }
+                    Err(error) => Err(error),
+                }
             })
             .collect::<Result<Vec<_>, ReadError>>()?;
         budget.check()?;
@@ -291,7 +297,20 @@
         cancellation: &ReadCancellation,
     ) -> Result<Vec<CommitInfo>, ReadError> {
         let budget = self.budget(cancellation);
-        self.history_with_budget(start, &budget)
+        self.history_with_budget(start, &budget, self.limits.max_history_commits, false)
+    }
+
+    pub(crate) fn history_prefix(
+        &self,
+        start: ObjectId,
+        maximum: usize,
+        cancellation: &ReadCancellation,
+    ) -> Result<Vec<CommitInfo>, ReadError> {
+        if maximum == 0 || maximum > self.limits.max_history_commits {
+            return Err(ReadError::InvalidLimits);
+        }
+        let budget = self.budget(cancellation);
+        self.history_with_budget(start, &budget, maximum, true)
     }
 
     pub(crate) fn tree(
@@ -434,8 +453,10 @@
         cancellation: &ReadCancellation,
     ) -> Result<Comparison, ReadError> {
         let budget = self.budget(cancellation);
-        let base_history = self.history_with_budget(base_commit, &budget)?;
-        let head_history = self.history_with_budget(head_commit, &budget)?;
+        let base_history =
+            self.history_with_budget(base_commit, &budget, self.limits.max_history_commits, false)?;
+        let head_history =
+            self.history_with_budget(head_commit, &budget, self.limits.max_history_commits, false)?;
         if base_history.len().saturating_add(head_history.len()) > self.limits.max_history_commits {
             return Err(ReadError::Limit("comparison commits"));
         }
@@ -592,7 +613,8 @@
     ) -> Result<Vec<BlameHunk>, ReadError> {
         validate_path(path, false, self.limits.max_path_bytes)?;
         let budget = self.budget(cancellation);
-        let history = self.history_with_budget(commit, &budget)?;
+        let history =
+            self.history_with_budget(commit, &budget, self.limits.max_history_commits, false)?;
         let mut candidate_bytes = 0_usize;
         for candidate in &history {
             budget.check()?;
@@ -807,6 +829,8 @@
         &self,
         start: ObjectId,
         budget: &ReadBudget<'_>,
+        maximum: usize,
+        truncate: bool,
     ) -> Result<Vec<CommitInfo>, ReadError> {
         let mut pending = VecDeque::from([start]);
         let mut seen = HashSet::new();
@@ -816,7 +840,10 @@
             if !seen.insert(id) {
                 continue;
             }
-            if history.len() >= self.limits.max_history_commits {
+            if history.len() >= maximum {
+                if truncate {
+                    break;
+                }
                 return Err(ReadError::Limit("history commits"));
             }
             let commit = self.read_commit(id, budget)?;

src/git/receive_pack.rs

Mode 100644100644; object e28b0255ac190d565cc91cc3

@@ -4,20 +4,21 @@
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use std::sync::atomic::{AtomicBool, Ordering};
-use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
+use std::time::{Duration, Instant};
 
 use gix::bstr::ByteSlice;
 use gix::hash::{Kind, ObjectId};
 use gix::objs::{CommitRef, Kind as ObjectKind, TagRef, TreeRefIter};
 use gix::refs::transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog};
 use gix::refs::{FullName, Target};
-use rand::TryRng;
 use thiserror::Error;
 
 use super::packetline::{Packet, PacketLineError, decode, encode_data, encode_flush};
+use super::repository::GitRepository;
 use super::upload_pack::hash_name;
 use crate::policy::{PolicyError, RefChange, RepositoryPolicy};
 use crate::store::{GitIntentRecord, GitOperationIntent, NewAuditEvent, Store};
+use crate::system::{random_lower_hex, unix_timestamp};
 
 const MAX_COMMANDS: usize = 256;
 const MAX_OBJECTS: usize = 100_000;
@@ -103,19 +104,13 @@
     }
 
     pub(crate) fn advertisement(&self) -> Result<Vec<u8>, ReceivePackError> {
-        let repository = open_bare(&self.repository_path)?;
-        let mut references = repository
-            .references()
-            .map_err(|error| ReceivePackError::Repository(error.to_string()))?
-            .all()
-            .map_err(|error| ReceivePackError::Repository(error.to_string()))?
-            .filter_map(|reference| reference.ok())
-            .filter_map(|reference| {
-                let id = reference.try_id()?.detach();
-                Some((reference.name().as_bstr().to_vec(), id))
-            })
+        let references = GitRepository::open(&self.repository_path)
+            .and_then(|repository| repository.references())
+            .map_err(|error| ReceivePackError::Repository(error.to_string()))?;
+        let references = references
+            .into_iter()
+            .filter(|reference| reference.name != b"HEAD")
             .collect::<Vec<_>>();
-        references.sort_by(|left, right| left.0.cmp(&right.0));
         let capabilities = format!(
             "report-status report-status-v2 delete-refs atomic ofs-delta object-format={} agent=tit/{}",
             hash_name(self.object_format),
@@ -132,14 +127,14 @@
                 &mut output,
             )?;
         } else {
-            for (index, (name, id)) in references.iter().enumerate() {
+            for (index, reference) in references.iter().enumerate() {
                 let suffix = if index == 0 {
-                    format!("\0{capabilities}")
+                    Some(capabilities.as_bytes())
                 } else {
-                    String::new()
+                    None
                 };
                 encode_data(
-                    format!("{id} {}{suffix}\n", String::from_utf8_lossy(name)).as_bytes(),
+                    &advertised_ref(reference.target, &reference.name, suffix),
                     &mut output,
                 )?;
             }
@@ -163,12 +158,7 @@
         let proposed = serialize_refs(&commands, true);
         let repository_text = path_text(&self.repository_path)?;
         let quarantine_text = path_text(&self.quarantine)?;
-        let created_at = SystemTime::now()
-            .duration_since(UNIX_EPOCH)
-            .map_err(|_| ReceivePackError::Clock)?
-            .as_secs()
-            .try_into()
-            .map_err(|_| ReceivePackError::Clock)?;
+        let created_at = unix_timestamp().ok_or(ReceivePackError::Clock)?;
         let mut store = Store::open(&self.database_path)?;
         store.begin_git_intent(&GitOperationIntent {
             id: &self.intent_id,
@@ -230,12 +220,7 @@
             .and_then(|name| name.to_str())
             .and_then(|name| name.strip_suffix(".git"))
             .unwrap_or("repository");
-        let created_at = SystemTime::now()
-            .duration_since(UNIX_EPOCH)
-            .map_err(|_| ReceivePackError::Clock)?
-            .as_secs()
-            .try_into()
-            .map_err(|_| ReceivePackError::Clock)?;
+        let created_at = unix_timestamp().ok_or(ReceivePackError::Clock)?;
         store.record_audit_event(&NewAuditEvent {
             action: "ref.update",
             actor: &self.actor,
@@ -265,9 +250,8 @@
             return Vec::new();
         }
         for command in commands {
-            let name = String::from_utf8_lossy(command.name.as_bstr());
-            let line = format!("ng {name} {}\n", error.client_reason());
-            if encode_data(line.as_bytes(), &mut output).is_err() {
+            let line = status_line(b"ng ", command.name.as_bstr(), Some(error.client_reason()));
+            if encode_data(&line, &mut output).is_err() {
                 return Vec::new();
             }
         }
@@ -958,12 +942,11 @@
     let mut output = Vec::new();
     encode_data(b"unpack ok\n", &mut output)?;
     for command in commands {
-        let name = String::from_utf8_lossy(command.name.as_bstr());
         let line = match error {
-            Some(error) => format!("ng {name} {error}\n"),
-            None => format!("ok {name}\n"),
+            Some(error) => status_line(b"ng ", command.name.as_bstr(), Some(error)),
+            None => status_line(b"ok ", command.name.as_bstr(), None),
         };
-        encode_data(line.as_bytes(), &mut output)?;
+        encode_data(&line, &mut output)?;
     }
     encode_flush(&mut output);
     Ok(output)
@@ -973,13 +956,34 @@
     let mut output = Vec::new();
     for command in commands {
         let id = if proposed { command.new } else { command.old };
-        writeln!(
-            output,
-            "{id} {}",
-            String::from_utf8_lossy(command.name.as_bstr())
-        )
-        .expect("a vector write cannot fail");
+        write!(output, "{id} ").expect("a vector write cannot fail");
+        output.extend_from_slice(command.name.as_bstr());
+        output.push(b'\n');
     }
+    output
+}
+
+fn advertised_ref(target: ObjectId, name: &[u8], capabilities: Option<&[u8]>) -> Vec<u8> {
+    let mut output = Vec::new();
+    write!(output, "{target} ").expect("a vector write cannot fail");
+    output.extend_from_slice(name);
+    if let Some(capabilities) = capabilities {
+        output.push(0);
+        output.extend_from_slice(capabilities);
+    }
+    output.push(b'\n');
+    output
+}
+
+fn status_line(prefix: &[u8], name: &[u8], error: Option<&str>) -> Vec<u8> {
+    let mut output = Vec::new();
+    output.extend_from_slice(prefix);
+    output.extend_from_slice(name);
+    if let Some(error) = error {
+        output.push(b' ');
+        output.extend_from_slice(error.as_bytes());
+    }
+    output.push(b'\n');
     output
 }
 
@@ -993,11 +997,7 @@
 }
 
 fn random_id() -> Result<String, ReceivePackError> {
-    let mut bytes = [0_u8; 16];
-    rand::rngs::SysRng
-        .try_fill_bytes(&mut bytes)
-        .map_err(|_| ReceivePackError::Random)?;
-    Ok(bytes.iter().map(|byte| format!("{byte:02x}")).collect())
+    random_lower_hex::<16>().ok_or(ReceivePackError::Random)
 }
 
 fn path_text(path: &Path) -> Result<&str, ReceivePackError> {

src/git/repository.rs

Mode 100644100644; object 6a1ef5c21725dc6134ed2a40

@@ -12,6 +12,8 @@
 use gix_pack::data::output::{Count, Entry, bytes::FromEntriesIter};
 use thiserror::Error;
 
+pub(crate) const MAX_REFERENCES: usize = 10_000;
+const MAX_REFERENCE_NAME_BYTES: usize = 4_096;
 const MAX_OBJECTS_PER_PACK: usize = 100_000;
 const MAX_OBJECT_BYTES: usize = 64 * 1024 * 1024;
 const MAX_PACK_BYTES: usize = 256 * 1024 * 1024;
@@ -80,6 +82,16 @@
     }
 
     pub(crate) fn references(&self) -> Result<Vec<GitReference>, GitRepositoryError> {
+        self.references_with_limit(MAX_REFERENCES)
+    }
+
+    pub(crate) fn references_with_limit(
+        &self,
+        maximum: usize,
+    ) -> Result<Vec<GitReference>, GitRepositoryError> {
+        if maximum == 0 || maximum > MAX_REFERENCES {
+            return Err(GitRepositoryError::ReferenceLimit);
+        }
         let mut references = Vec::new();
         let platform = self
             .repository
@@ -90,12 +102,18 @@
             .map_err(|error| GitRepositoryError::References(error.to_string()))?;
 
         for reference in iterator {
+            if references.len() >= maximum {
+                return Err(GitRepositoryError::ReferenceLimit);
+            }
             let reference =
                 reference.map_err(|error| GitRepositoryError::References(error.to_string()))?;
             let Some(target) = reference.try_id().map(gix::Id::detach) else {
                 continue;
             };
             let name = reference.name().as_bstr().to_vec();
+            if name.len() > MAX_REFERENCE_NAME_BYTES {
+                return Err(GitRepositoryError::ReferenceLimit);
+            }
             let peeled = if name.starts_with(b"refs/tags/") {
                 let mut candidate = reference.clone();
                 let candidate = candidate
@@ -120,6 +138,9 @@
             .head_ref()
             .map_err(|error| GitRepositoryError::References(error.to_string()))?
         {
+            if references.len() >= maximum {
+                return Err(GitRepositoryError::ReferenceLimit);
+            }
             let target = head.id().detach();
             references.insert(
                 0,
@@ -559,6 +580,8 @@
     NotBare(PathBuf),
     #[error("cannot read Git references: {0}")]
     References(String),
+    #[error("Git reference count or name size exceeds the limit")]
+    ReferenceLimit,
     #[error("Git branch name is not valid")]
     InvalidBranch,
     #[error("Git reference name is not valid: {0}")]

src/git/upload_pack.rs

Mode 100644100644; object fa3304db424cd891d052c330

@@ -93,28 +93,18 @@
         } else {
             for (index, reference) in references.iter().enumerate() {
                 let suffix = if index == 0 {
-                    format!("\0{capabilities}")
+                    Some(capabilities.as_bytes())
                 } else {
-                    String::new()
+                    None
                 };
                 encode_data(
-                    format!(
-                        "{} {}{suffix}\n",
-                        reference.target,
-                        String::from_utf8_lossy(&reference.name)
-                    )
-                    .as_bytes(),
+                    &advertised_ref(reference.target, &reference.name, suffix),
                     output,
                 )?;
                 if let Some(peeled) = reference.peeled {
-                    encode_data(
-                        format!(
-                            "{peeled} {}^{{}}\n",
-                            String::from_utf8_lossy(&reference.name)
-                        )
-                        .as_bytes(),
-                        output,
-                    )?;
+                    let mut name = reference.name.clone();
+                    name.extend_from_slice(b"^{}");
+                    encode_data(&advertised_ref(peeled, &name, None), output)?;
                 }
             }
         }
@@ -357,18 +347,31 @@
 }
 
 fn format_ref(reference: &GitReference, symrefs: bool, peel: bool) -> Vec<u8> {
-    let mut output = format!(
-        "{} {}",
-        reference.target,
-        String::from_utf8_lossy(&reference.name)
-    )
-    .into_bytes();
+    let mut output = Vec::new();
+    write!(output, "{} ", reference.target).expect("a vector write cannot fail");
+    output.extend_from_slice(&reference.name);
     if symrefs && let Some(target) = &reference.symbolic_target {
         output.extend_from_slice(b" symref-target:");
         output.extend_from_slice(target);
     }
     if peel && let Some(target) = reference.peeled {
         output.extend_from_slice(format!(" peeled:{target}").as_bytes());
+    }
+    output.push(b'\n');
+    output
+}
+
+pub(crate) fn advertised_ref(
+    target: ObjectId,
+    name: &[u8],
+    capabilities: Option<&[u8]>,
+) -> Vec<u8> {
+    let mut output = Vec::new();
+    write!(output, "{target} ").expect("a vector write cannot fail");
+    output.extend_from_slice(name);
+    if let Some(capabilities) = capabilities {
+        output.push(0);
+        output.extend_from_slice(capabilities);
     }
     output.push(b'\n');
     output

src/http/issues.rs

Mode 100644100644; object 9552def48e4e3d80123a607e

@@ -62,6 +62,9 @@
     let authenticated = actor.0.is_some();
     let state_filter = query.state.unwrap_or_else(|| "open".to_owned());
     let page_number = query.page.unwrap_or(1);
+    if page_number == 0 {
+        return issue_bad_request(&request_id.0);
+    }
     let state_for_job = state_filter.clone();
     let result = issue_job(state, move || {
         service.list_page(
@@ -132,6 +135,9 @@
     let number = path.number;
     let comments_page = query.comments_page.unwrap_or(1);
     let timeline_page = query.timeline_page.unwrap_or(1);
+    if comments_page == 0 || timeline_page == 0 {
+        return issue_bad_request(&request_id.0);
+    }
     let result = issue_job(state, move || {
         service.get_page(
             &owner,

src/http/mod.rs

Mode 100644100644; object 4ebbd91076364c631c013b60

@@ -1946,10 +1946,14 @@
 ) -> Response {
     let username_for_audit = username.to_owned();
     let correlation_id = request_id.to_owned();
-    let _ = login_job(state, move |login| {
+    let telemetry = state.telemetry.clone();
+    if let Err(error) = login_job(state, move |login| {
         login.record_login_failure(&username_for_audit, &correlation_id)
     })
-    .await;
+    .await
+    {
+        telemetry.failure("login.audit", Some(request_id), &error.to_string());
+    }
     login_error(request_id, username, error)
 }
 

src/http/public.rs

Mode 100644100644; object e5ee3edf115b2ad0c4f8cb0a

@@ -21,6 +21,7 @@
 use tokio_stream::wrappers::ReceiverStream;
 
 use crate::auth::validate_username;
+use crate::codec::encode_lower_hex;
 use crate::domain::repository::validate_slug;
 use crate::feed::{FeedPage, PAGE_SIZE, RepositoryFeedKind};
 use crate::git::packetline::MAX_REQUEST_BYTES;
@@ -476,7 +477,7 @@
                         let (entries, truncated) =
                             service.tree_prefix(head, &[], MAX_SUMMARY_FILES, &cancellation)?;
                         (
-                            service.history(head, &cancellation)?,
+                            service.history_prefix(head, MAX_SUMMARY_COMMITS, &cancellation)?,
                             service.readme(head, &cancellation)?,
                             entries,
                             truncated,
@@ -1066,7 +1067,7 @@
     is_public: bool,
 ) -> Response {
     let digest = Sha256::digest(body.as_bytes());
-    let etag = format!("\"{}\"", encode_hex(&digest));
+    let etag = format!("\"{}\"", encode_lower_hex(&digest));
     let modified = u64::try_from(timestamp)
         .ok()
         .and_then(|seconds| UNIX_EPOCH.checked_add(Duration::from_secs(seconds)))
@@ -1113,15 +1114,6 @@
         let candidate = candidate.trim();
         candidate == "*" || candidate.strip_prefix("W/").unwrap_or(candidate) == etag
     })
-}
-
-fn encode_hex(bytes: &[u8]) -> String {
-    let mut output = String::with_capacity(bytes.len() * 2);
-    for byte in bytes {
-        output.push(char::from(b"0123456789abcdef"[usize::from(byte >> 4)]));
-        output.push(char::from(b"0123456789abcdef"[usize::from(byte & 0x0f)]));
-    }
-    output
 }
 
 fn route_error(error: RouteError, request_id: &str) -> Response {
@@ -1535,12 +1527,7 @@
             .to_owned();
         page.has_head = summary.head.is_some();
         page.commit_id = summary.head.map(|id| id.to_string()).unwrap_or_default();
-        page.history = summary
-            .history
-            .into_iter()
-            .take(MAX_SUMMARY_COMMITS)
-            .map(CommitView::from)
-            .collect();
+        page.history = summary.history.into_iter().map(CommitView::from).collect();
         page.tags = summary
             .tags
             .into_iter()

src/http/pull_requests.rs

Mode 100644100644; object dc3b69b51f7bfb379512815a

@@ -7,6 +7,7 @@
 use axum::routing::{get, post};
 use serde::Deserialize;
 
+use crate::codec::{decode_ascii_hex, encode_lower_hex};
 use crate::markdown::{self, RenderedMarkdown};
 use crate::pull_request::PullRequestError;
 use crate::store::StoreError;
@@ -124,6 +125,9 @@
     let actor_for_list = actor_name.clone();
     let state_filter = query.state.unwrap_or_else(|| "open".to_owned());
     let page_number = query.page.unwrap_or(1);
+    if page_number == 0 {
+        return bad_request(&request_id.0);
+    }
     let state_for_job = state_filter.clone();
     let result = job(state.clone(), move || {
         service.list_page(
@@ -139,10 +143,13 @@
         Ok((record, page, can_create)) => {
             let csrf = cookie(&headers, CSRF_COOKIE).unwrap_or_default();
             let branches = match state.public.as_ref() {
-                Some(public) => public
+                Some(public) => match public
                     .branch_names(actor_name, record.owner.clone(), record.slug.clone())
                     .await
-                    .unwrap_or_default(),
+                {
+                    Ok(branches) => branches,
+                    Err(_) => return internal(&request_id.0),
+                },
                 None => Vec::new(),
             };
             let default_owner = record.owner.clone();
@@ -150,8 +157,11 @@
             let default_branch = super::repository_job(state, move |repositories| {
                 repositories.default_branch(&default_owner, &default_repository)
             })
-            .await
-            .unwrap_or_else(|_| "refs/heads/main".to_owned());
+            .await;
+            let default_branch = match default_branch {
+                Ok(default_branch) => default_branch,
+                Err(_) => return internal(&request_id.0),
+            };
             render(
                 StatusCode::OK,
                 &PullRequestListTemplate {
@@ -204,6 +214,11 @@
     let owner = path.owner.clone();
     let repository = path.repository.clone();
     let signed_in = actor.0.is_some();
+    let reviews_page = query.reviews_page.unwrap_or(1);
+    let timeline_page = query.timeline_page.unwrap_or(1);
+    if reviews_page == 0 || timeline_page == 0 {
+        return bad_request(&request_id.0);
+    }
     let result = job(state, move || {
         service.compare_page(
             &owner,
@@ -211,8 +226,8 @@
             path.number,
             query.revision,
             actor.0.as_deref(),
-            query.reviews_page.unwrap_or(1),
-            query.timeline_page.unwrap_or(1),
+            reviews_page,
+            timeline_page,
         )
     })
     .await;
@@ -632,14 +647,7 @@
     if value.is_empty() || !value.len().is_multiple_of(2) {
         return None;
     }
-    value
-        .as_bytes()
-        .chunks_exact(2)
-        .map(|pair| {
-            let pair = std::str::from_utf8(pair).ok()?;
-            u8::from_str_radix(pair, 16).ok()
-        })
-        .collect()
+    decode_ascii_hex(value.as_bytes())
 }
 
 fn bad_request(request_id: &str) -> Response {
@@ -847,7 +855,7 @@
                 .iter()
                 .map(|file| DiffView {
                     path: String::from_utf8_lossy(&file.path).into_owned(),
-                    path_hex: encode_hex(&file.path),
+                    path_hex: encode_lower_hex(&file.path),
                     binary: file.binary,
                     has_base: file.old_id.is_some(),
                     has_head: file.new_id.is_some(),
@@ -856,13 +864,4 @@
                 .collect(),
         }
     }
-}
-
-fn encode_hex(bytes: &[u8]) -> String {
-    let mut encoded = String::with_capacity(bytes.len().saturating_mul(2));
-    for byte in bytes {
-        use std::fmt::Write;
-        write!(encoded, "{byte:02x}").expect("a string write cannot fail");
-    }
-    encoded
 }

src/issue.rs

Mode 100644100644; object 430e2843e7d9b72a6637278b

@@ -1,5 +1,4 @@
 use std::path::{Path, PathBuf};
-use std::time::{SystemTime, UNIX_EPOCH};
 
 use thiserror::Error;
 
@@ -9,6 +8,7 @@
     IssueChange, IssueDetail, IssueRecord, NewIssue, RecordPage, RepositoryRecord, Store,
     StoreError,
 };
+use crate::system::unix_timestamp;
 
 pub(crate) const MAX_TITLE_BYTES: usize = 200;
 pub(crate) const MAX_BODY_BYTES: usize = 256 * 1024;
@@ -230,11 +230,7 @@
 }
 
 fn timestamp() -> Result<i64, IssueError> {
-    let seconds = SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .map_err(|_| IssueError::Clock)?
-        .as_secs();
-    i64::try_from(seconds).map_err(|_| IssueError::Clock)
+    unix_timestamp().ok_or(IssueError::Clock)
 }
 
 #[derive(Debug, Error)]

src/lib.rs

Mode 100644; object 8016bfbcf273

@@ -1,0 +1,99 @@
+pub mod app;
+
+pub(crate) mod account;
+pub(crate) mod admin;
+#[allow(
+    dead_code,
+    reason = "the current login flow does not use the legacy challenge API"
+)]
+pub(crate) mod auth;
+pub(crate) mod backup;
+pub(crate) mod bootstrap;
+pub(crate) mod cli;
+pub(crate) mod codec;
+pub(crate) mod config;
+pub(crate) mod control;
+pub(crate) mod diagnostics;
+pub(crate) mod domain;
+pub(crate) mod feed;
+pub(crate) mod feed_token;
+#[allow(
+    dead_code,
+    reason = "the test suite exercises Git transports that the server composes indirectly"
+)]
+pub(crate) mod git;
+#[allow(
+    dead_code,
+    reason = "the test suite starts isolated Web servers through additional constructors"
+)]
+pub(crate) mod http;
+pub(crate) mod instance;
+pub(crate) mod issue;
+pub(crate) mod maintenance;
+pub(crate) mod markdown;
+pub(crate) mod policy;
+pub(crate) mod pull_request;
+pub(crate) mod rate_limit;
+pub(crate) mod repair;
+pub(crate) mod repository;
+pub(crate) mod search;
+pub(crate) mod serve;
+pub(crate) mod session;
+#[allow(
+    dead_code,
+    reason = "the test suite starts isolated SSH servers through additional constructors"
+)]
+pub(crate) mod ssh;
+pub(crate) mod store;
+pub(crate) mod system;
+pub(crate) mod telemetry;
+pub(crate) mod watch;
+
+#[cfg(test)]
+#[path = "../tests/account_lifecycle.rs"]
+mod account_lifecycle_tests;
+#[cfg(test)]
+#[path = "../tests/auth.rs"]
+mod auth_tests;
+#[cfg(test)]
+#[path = "../tests/git_http.rs"]
+mod git_http_tests;
+#[cfg(test)]
+#[path = "../tests/git_push_ssh.rs"]
+mod git_push_ssh_tests;
+#[cfg(test)]
+#[path = "../tests/git_reads.rs"]
+mod git_reads_tests;
+#[cfg(test)]
+#[path = "../tests/git_repository.rs"]
+mod git_repository_tests;
+#[cfg(test)]
+#[path = "../tests/git_ssh.rs"]
+mod git_ssh_tests;
+#[cfg(test)]
+#[path = "../tests/metadata_search.rs"]
+mod metadata_search_tests;
+#[cfg(test)]
+#[path = "../tests/public_routes.rs"]
+mod public_routes_tests;
+#[cfg(test)]
+#[path = "../tests/pull_requests.rs"]
+mod pull_requests_tests;
+#[cfg(test)]
+#[path = "../tests/repository_policy.rs"]
+mod repository_policy_tests;
+#[cfg(test)]
+#[path = "../tests/sqlite.rs"]
+mod sqlite_tests;
+#[cfg(test)]
+#[path = "../tests/sqlite_workload.rs"]
+mod sqlite_workload_tests;
+#[cfg(test)]
+#[path = "../tests/ssh.rs"]
+mod ssh_tests;
+#[cfg(test)]
+#[path = "../tests/web_session.rs"]
+mod web_session_tests;
+#[cfg(test)]
+#[path = "../tests/web_shell.rs"]
+mod web_shell_tests;

src/main.rs

Mode 100644100644; object 6f26ca4293861844ce1bc694

@@ -1,463 +1,6 @@
-mod account;
-mod admin;
-#[allow(
-    dead_code,
-    reason = "the bootstrap command uses only part of the authentication API"
-)]
-mod auth;
-mod backup;
-mod bootstrap;
-mod cli;
-mod config;
-mod control;
-mod diagnostics;
-mod domain;
-mod feed;
-mod feed_token;
-#[allow(dead_code, reason = "the server uses only part of the shared Git API")]
-mod git;
-#[allow(dead_code, reason = "the server uses only part of the shared HTTP API")]
-mod http;
-mod instance;
-mod issue;
-mod maintenance;
-mod markdown;
-mod policy;
-mod pull_request;
-mod rate_limit;
-mod repair;
-mod repository;
-mod search;
-mod serve;
-mod session;
-#[allow(dead_code, reason = "the server uses only part of the shared SSH API")]
-mod ssh;
-mod store;
-mod telemetry;
-mod watch;
-
 use std::process::ExitCode;
-use std::{io, io::Write};
-
-use clap::Parser;
-
-use crate::cli::{
-    AccountCommand, AdminCommand, Cli, CollaboratorRole, Command, InspectCommand, RepairCommand,
-    RepositoryCommand, RepositoryVisibility, SetupCommand,
-};
 
 #[tokio::main]
 async fn main() -> ExitCode {
-    let cli = match Cli::try_parse() {
-        Ok(cli) => cli,
-        Err(error) => {
-            let code = error.exit_code();
-            let _ = error.print();
-            return ExitCode::from(u8::try_from(code).unwrap_or(2));
-        }
-    };
-
-    if let Some(Command::Restore { archive, target }) = &cli.command {
-        return run_restore(archive, target);
-    }
-
-    match config::load(&cli) {
-        Ok(config) => match cli.command {
-            None => ExitCode::SUCCESS,
-            Some(Command::Serve) => match serve::run(&config).await {
-                Ok(()) => ExitCode::SUCCESS,
-                Err(error) => {
-                    eprintln!("tit: {error}");
-                    ExitCode::FAILURE
-                }
-            },
-            Some(Command::InviteCode) => {
-                match control::request_invitation(&config.instance_dir).await {
-                    Ok(code) => match writeln!(io::stdout().lock(), "Signup code: {code}") {
-                        Ok(()) => ExitCode::SUCCESS,
-                        Err(error) => {
-                            eprintln!("tit: cannot write the signup code: {error}");
-                            ExitCode::FAILURE
-                        }
-                    },
-                    Err(error) => {
-                        eprintln!("tit: {error}");
-                        ExitCode::FAILURE
-                    }
-                }
-            }
-            Some(Command::Doctor { backups }) => match diagnostics::doctor(&config, &backups) {
-                Ok(()) => ExitCode::SUCCESS,
-                Err(error) => {
-                    eprintln!("tit: {error}");
-                    ExitCode::FAILURE
-                }
-            },
-            Some(Command::Inspect { command }) => run_inspect_command(&config, command),
-            Some(Command::Dump) => run_dump_command(&config),
-            Some(Command::Repair { command }) => {
-                let result = match command {
-                    RepairCommand::Intents => repair::intents(&config.instance_dir),
-                    RepairCommand::Quarantine => repair::quarantine(&config.instance_dir),
-                };
-                match result {
-                    Ok(()) => ExitCode::SUCCESS,
-                    Err(error) => {
-                        eprintln!("tit: {error}");
-                        ExitCode::FAILURE
-                    }
-                }
-            }
-            Some(Command::Maintenance { retention_days }) => {
-                match admin::maintain(&config.instance_dir, retention_days) {
-                    Ok(result) => match writeln!(
-                        io::stdout().lock(),
-                        "Pruned {} terminal records.",
-                        result.deleted
-                    ) {
-                        Ok(()) => ExitCode::SUCCESS,
-                        Err(error) => {
-                            eprintln!("tit: cannot write maintenance information: {error}");
-                            ExitCode::FAILURE
-                        }
-                    },
-                    Err(error) => {
-                        eprintln!("tit: {error}");
-                        ExitCode::FAILURE
-                    }
-                }
-            }
-            Some(Command::Backup { output }) => run_backup(&config, &output).await,
-            Some(Command::Restore { .. }) => {
-                unreachable!("the restore command runs before configuration is loaded")
-            }
-            Some(Command::Setup {
-                command:
-                    SetupCommand::Admin {
-                        username,
-                        ssh_public_key,
-                    },
-            }) => match bootstrap::setup_administrator(
-                &config.instance_dir,
-                &username,
-                &ssh_public_key,
-            ) {
-                Ok(recovery_code) => {
-                    let mut output = io::stdout().lock();
-                    match writeln!(output, "Recovery code: {recovery_code}") {
-                        Ok(()) => ExitCode::SUCCESS,
-                        Err(error) => {
-                            eprintln!("tit: cannot write the recovery code: {error}");
-                            ExitCode::FAILURE
-                        }
-                    }
-                }
-                Err(error) => {
-                    eprintln!("tit: {error}");
-                    ExitCode::FAILURE
-                }
-            },
-            Some(Command::Admin {
-                command: AdminCommand::Repository { command },
-            }) => run_repository_command(&config.instance_dir, command),
-            Some(Command::Admin {
-                command: AdminCommand::Account { command },
-            }) => run_account_command(&config.instance_dir, command),
-            Some(Command::Admin {
-                command: AdminCommand::Audit { limit },
-            }) => run_audit_command(&config.instance_dir, limit),
-        },
-        Err(error) => {
-            eprintln!("tit: {error}");
-            ExitCode::FAILURE
-        }
-    }
-}
-
-fn run_inspect_command(config: &config::Config, command: InspectCommand) -> ExitCode {
-    let result = match command {
-        InspectCommand::Account { username } => {
-            serialize_inspection(diagnostics::inspect_account(config, &username))
-        }
-        InspectCommand::Repository { owner, slug } => {
-            serialize_inspection(diagnostics::inspect_repository(config, &owner, &slug))
-        }
-        InspectCommand::Intent { id } => {
-            serialize_inspection(diagnostics::inspect_intent(config, &id))
-        }
-    };
-    match result {
-        Ok(line) => match writeln!(io::stdout().lock(), "{line}") {
-            Ok(()) => ExitCode::SUCCESS,
-            Err(error) => {
-                eprintln!("tit: cannot write inspect information: {error}");
-                ExitCode::FAILURE
-            }
-        },
-        Err(error) => {
-            eprintln!("tit: {error}");
-            ExitCode::FAILURE
-        }
-    }
-}
-
-fn serialize_inspection(
-    result: Result<impl serde::Serialize, diagnostics::DiagnosticError>,
-) -> Result<String, Box<dyn std::error::Error>> {
-    Ok(serde_json::to_string(&result?)?)
-}
-
-fn run_dump_command(config: &config::Config) -> ExitCode {
-    let result = (|| -> Result<(), Box<dyn std::error::Error>> {
-        let mut output = io::stdout().lock();
-        let mut output_error = None;
-        diagnostics::dump(config, |row| {
-            let result = serde_json::to_writer(&mut output, &row)
-                .map_err(|error| Box::new(error) as Box<dyn std::error::Error>)
-                .and_then(|()| {
-                    writeln!(output).map_err(|error| Box::new(error) as Box<dyn std::error::Error>)
-                });
-            match result {
-                Ok(()) => true,
-                Err(error) => {
-                    output_error = Some(error);
-                    false
-                }
-            }
-        })?;
-        if let Some(error) = output_error {
-            return Err(error);
-        }
-        Ok(())
-    })();
-    match result {
-        Ok(()) => ExitCode::SUCCESS,
-        Err(error) => {
-            eprintln!("tit: {error}");
-            ExitCode::FAILURE
-        }
-    }
-}
-
-async fn run_backup(config: &config::Config, output: &std::path::Path) -> ExitCode {
-    let result = match backup::create_offline(&config.instance_dir, &config.config_path, output) {
-        Ok(()) => Ok(()),
-        Err(backup::BackupError::Instance(instance::InstanceError::Locked)) => {
-            control::request_backup(&config.instance_dir, output)
-                .await
-                .map_err(|error| Box::new(error) as Box<dyn std::error::Error>)
-        }
-        Err(error) => Err(Box::new(error) as Box<dyn std::error::Error>),
-    };
-    match result {
-        Ok(()) => match writeln!(
-            io::stdout().lock(),
-            "Backup: {}\nWarning: This backup contains credentials.",
-            output.display()
-        ) {
-            Ok(()) => ExitCode::SUCCESS,
-            Err(error) => {
-                eprintln!("tit: cannot write backup information: {error}");
-                ExitCode::FAILURE
-            }
-        },
-        Err(error) => {
-            eprintln!("tit: {error}");
-            ExitCode::FAILURE
-        }
-    }
-}
-
-fn run_restore(archive: &std::path::Path, target: &std::path::Path) -> ExitCode {
-    match backup::restore(archive, target) {
-        Ok(()) => match writeln!(
-            io::stdout().lock(),
-            "Restore: {}\nThe restored instance is not active.",
-            target.display()
-        ) {
-            Ok(()) => ExitCode::SUCCESS,
-            Err(error) => {
-                eprintln!("tit: cannot write restore information: {error}");
-                ExitCode::FAILURE
-            }
-        },
-        Err(error) => {
-            eprintln!("tit: {error}");
-            ExitCode::FAILURE
-        }
-    }
-}
-
-fn run_audit_command(instance_dir: &std::path::Path, limit: usize) -> ExitCode {
-    let result = (|| -> Result<(), Box<dyn std::error::Error>> {
-        let _lock = instance::InstanceLock::acquire(instance_dir)?;
-        let database = instance::prepare_database(instance_dir)?;
-        let events = store::Store::open(&database)?.audit_events(limit)?;
-        let mut output = io::stdout().lock();
-        for event in events {
-            writeln!(output, "id={}", event.id)?;
-            writeln!(output, "action={}", event.action)?;
-            writeln!(output, "actor={}", event.actor)?;
-            writeln!(output, "target={}", event.target)?;
-            writeln!(output, "outcome={}", event.outcome)?;
-            writeln!(output, "correlation-id={}", event.correlation_id)?;
-            writeln!(output, "created-at={}", event.created_at)?;
-            writeln!(output)?;
-        }
-        Ok(())
-    })();
-    match result {
-        Ok(()) => ExitCode::SUCCESS,
-        Err(error) => {
-            eprintln!("tit: {error}");
-            ExitCode::FAILURE
-        }
-    }
-}
-
-fn run_account_command(instance_dir: &std::path::Path, command: AccountCommand) -> ExitCode {
-    let result = (|| -> Result<Option<String>, Box<dyn std::error::Error>> {
-        let _lock = instance::InstanceLock::acquire(instance_dir)?;
-        let database = instance::prepare_database(instance_dir)?;
-        let accounts = account::AccountService::new(database);
-        let correlation_id = format!("{:032x}", rand::random::<u128>());
-        match command {
-            AccountCommand::KeyAdd {
-                username,
-                label,
-                ssh_public_key,
-            } => {
-                let fingerprint = accounts.add_key(
-                    &username,
-                    &label,
-                    &ssh_public_key,
-                    "admin-cli",
-                    &correlation_id,
-                )?;
-                Ok(Some(fingerprint))
-            }
-            AccountCommand::KeyRevoke {
-                username,
-                fingerprint,
-            } => {
-                accounts.revoke_key(&username, &fingerprint, "admin-cli", &correlation_id)?;
-                Ok(None)
-            }
-            AccountCommand::Suspend { username } => {
-                accounts.suspend(&username, true, "admin-cli", &correlation_id)?;
-                Ok(None)
-            }
-            AccountCommand::Resume { username } => {
-                accounts.suspend(&username, false, "admin-cli", &correlation_id)?;
-                Ok(None)
-            }
-        }
-    })();
-    match result {
-        Ok(Some(fingerprint)) => match writeln!(io::stdout().lock(), "fingerprint={fingerprint}") {
-            Ok(()) => ExitCode::SUCCESS,
-            Err(error) => {
-                eprintln!("tit: cannot write account information: {error}");
-                ExitCode::FAILURE
-            }
-        },
-        Ok(None) => ExitCode::SUCCESS,
-        Err(error) => {
-            eprintln!("tit: {error}");
-            ExitCode::FAILURE
-        }
-    }
-}
-
-fn run_repository_command(instance_dir: &std::path::Path, command: RepositoryCommand) -> ExitCode {
-    let result = match command {
-        RepositoryCommand::Create { owner, slug } => {
-            admin::create_repository(instance_dir, &owner, &slug, gix::hash::Kind::Sha1)
-        }
-        RepositoryCommand::Import {
-            owner,
-            slug,
-            source,
-        } => admin::import_repository(instance_dir, &owner, &slug, &source),
-        RepositoryCommand::Rename {
-            owner,
-            old_slug,
-            new_slug,
-        } => admin::rename_repository(instance_dir, &owner, &old_slug, &new_slug),
-        RepositoryCommand::Archive { owner, slug } => {
-            admin::archive_repository(instance_dir, &owner, &slug)
-        }
-        RepositoryCommand::Visibility {
-            owner,
-            slug,
-            visibility,
-        } => admin::set_repository_visibility(
-            instance_dir,
-            &owner,
-            &slug,
-            match visibility {
-                RepositoryVisibility::Public => "public",
-                RepositoryVisibility::Private => "private",
-            },
-        ),
-        RepositoryCommand::CollaboratorSet {
-            owner,
-            slug,
-            username,
-            role,
-        } => admin::set_repository_collaborator(
-            instance_dir,
-            &owner,
-            &slug,
-            &username,
-            match role {
-                CollaboratorRole::Maintainer => "maintainer",
-                CollaboratorRole::Writer => "writer",
-                CollaboratorRole::Reader => "reader",
-            },
-        ),
-        RepositoryCommand::CollaboratorRemove {
-            owner,
-            slug,
-            username,
-        } => admin::remove_repository_collaborator(instance_dir, &owner, &slug, &username),
-        RepositoryCommand::Inspect { owner, slug } => {
-            admin::inspect_repository(instance_dir, &owner, &slug)
-        }
-    };
-
-    match result {
-        Ok(repository) => {
-            let path = match admin::repository_path(instance_dir, &repository) {
-                Ok(path) => path,
-                Err(error) => {
-                    eprintln!("tit: {error}");
-                    return ExitCode::FAILURE;
-                }
-            };
-            let archived_at = repository
-                .archived_at
-                .map_or_else(|| "-".to_owned(), |value| value.to_string());
-            let mut output = io::stdout().lock();
-            let written = writeln!(output, "id={}", repository.id)
-                .and_then(|()| writeln!(output, "owner={}", repository.owner))
-                .and_then(|()| writeln!(output, "slug={}", repository.slug))
-                .and_then(|()| writeln!(output, "visibility={}", repository.visibility))
-                .and_then(|()| writeln!(output, "state={}", repository.state))
-                .and_then(|()| writeln!(output, "created-at={}", repository.created_at))
-                .and_then(|()| writeln!(output, "archived-at={archived_at}"))
-                .and_then(|()| writeln!(output, "path={}", path.display()));
-            match written {
-                Ok(()) => ExitCode::SUCCESS,
-                Err(error) => {
-                    eprintln!("tit: cannot write repository information: {error}");
-                    ExitCode::FAILURE
-                }
-            }
-        }
-        Err(error) => {
-            eprintln!("tit: {error}");
-            ExitCode::FAILURE
-        }
-    }
+    tit_cde::app::run().await
 }

src/pull_request.rs

Mode 100644100644; object 6bc529256155301265d5cb5a

@@ -1,10 +1,8 @@
 use std::fs;
 use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex};
-use std::time::{SystemTime, UNIX_EPOCH};
 
 use gix::hash::ObjectId;
-use rand::TryRng;
 use thiserror::Error;
 
 use crate::auth::{AuthError, validate_username};
@@ -20,6 +18,7 @@
     PullRequestDetail, PullRequestRecord, PullRequestRefIntentRecord, PullRequestRevisionRecord,
     RecordPage, RepositoryRecord, Store, StoreError,
 };
+use crate::system::{random_lower_hex, unix_timestamp};
 
 pub(crate) const MAX_TITLE_BYTES: usize = 200;
 pub(crate) const MAX_BODY_BYTES: usize = 256 * 1024;
@@ -815,20 +814,11 @@
 }
 
 fn random_id() -> Result<String, PullRequestError> {
-    let mut bytes = [0_u8; 16];
-    rand::rngs::SysRng
-        .try_fill_bytes(&mut bytes)
-        .map_err(|_| PullRequestError::Random)?;
-    Ok(bytes.iter().map(|byte| format!("{byte:02x}")).collect())
+    random_lower_hex::<16>().ok_or(PullRequestError::Random)
 }
 
 fn timestamp() -> Result<i64, PullRequestError> {
-    SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .map_err(|_| PullRequestError::Clock)?
-        .as_secs()
-        .try_into()
-        .map_err(|_| PullRequestError::Clock)
+    unix_timestamp().ok_or(PullRequestError::Clock)
 }
 
 #[cfg(test)]

src/repository.rs

Mode 100644100644; object 6295db17cd3d9779eb1d02c1

@@ -1,9 +1,7 @@
 use std::fs;
 use std::path::{Path, PathBuf};
-use std::time::{SystemTime, UNIX_EPOCH};
 
 use gix::hash::Kind;
-use rand::TryRng;
 use thiserror::Error;
 
 use crate::auth::{AuthError, validate_username};
@@ -11,9 +9,10 @@
 use crate::git::repository::{GitRepository, GitRepositoryError};
 use crate::maintenance::MaintenanceGate;
 use crate::store::{
-    HomeRepositoryRecord, NewAuditEvent, NewRepository, RepositoryOrigin, RepositoryRecord,
-    RepositorySettings, Store, StoreError,
+    HomeRepositoryRecord, NewAuditEvent, NewDefaultBranchIntent, NewRepository, RepositoryOrigin,
+    RepositoryRecord, RepositorySettings, Store, StoreError,
 };
+use crate::system::{random_lower_hex, unix_timestamp};
 
 const HOME_REPOSITORY_LIMIT: usize = 20;
 pub(crate) const MAX_DESCRIPTION_BYTES: usize = 512;
@@ -104,23 +103,49 @@
         validate_slug(repository)?;
         validate_username(actor)?;
         let _maintenance = self.maintenance.mutation();
+        let changed_at = timestamp()?;
+        let intent_id = random_id()?;
         let mut store = Store::open(&self.database)?;
         let settings = store.repository_settings(owner, repository, actor)?;
         let git = GitRepository::open(&self.root.join(format!("{}.git", settings.repository.id)))?;
         let previous = settings.default_branch;
-        git.set_default_branch(default_branch)?;
-        let result = store.update_repository_default_branch(
-            owner,
-            repository,
-            actor,
-            default_branch,
-            timestamp()?,
-            &random_id()?,
-        );
-        if result.is_err() {
-            let _ = git.set_default_branch(&previous);
+        git.resolve_branch(default_branch)?;
+        if git.default_branch()?.as_deref() != Some(previous.as_str()) {
+            return Err(RepositoryServiceError::DefaultBranchState);
         }
-        result?;
+        store.begin_repository_default_branch(&NewDefaultBranchIntent {
+            id: &intent_id,
+            owner,
+            slug: repository,
+            actor,
+            previous_branch: &previous,
+            default_branch,
+            changed_at,
+        })?;
+        if let Err(error) = git.set_default_branch(default_branch) {
+            store.abandon_repository_default_branch(&intent_id)?;
+            return Err(error.into());
+        }
+        store.complete_repository_default_branch(&intent_id)?;
+        Ok(())
+    }
+
+    pub(crate) fn recover(&self) -> Result<(), RepositoryServiceError> {
+        let _maintenance = self.maintenance.mutation();
+        let mut store = Store::open(&self.database)?;
+        for intent in store.incomplete_repository_default_branches()? {
+            let git =
+                GitRepository::open(&self.root.join(format!("{}.git", intent.repository_id)))?;
+            match git.default_branch()?.as_deref() {
+                Some(current) if current == intent.previous_branch => {
+                    store.abandon_repository_default_branch(&intent.id)?;
+                }
+                Some(current) if current == intent.proposed_branch => {
+                    store.complete_repository_default_branch(&intent.id)?;
+                }
+                _ => return Err(RepositoryServiceError::DefaultBranchState),
+            }
+        }
         Ok(())
     }
 
@@ -356,20 +381,11 @@
 }
 
 fn random_id() -> Result<String, RepositoryServiceError> {
-    let mut bytes = [0_u8; 16];
-    rand::rngs::SysRng
-        .try_fill_bytes(&mut bytes)
-        .map_err(|_| RepositoryServiceError::Random)?;
-    Ok(bytes.iter().map(|byte| format!("{byte:02x}")).collect())
+    random_lower_hex::<16>().ok_or(RepositoryServiceError::Random)
 }
 
 fn timestamp() -> Result<i64, RepositoryServiceError> {
-    SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .map_err(|_| RepositoryServiceError::Clock)?
-        .as_secs()
-        .try_into()
-        .map_err(|_| RepositoryServiceError::Clock)
+    unix_timestamp().ok_or(RepositoryServiceError::Clock)
 }
 
 fn validate_description(description: &str) -> Result<(), RepositoryServiceError> {
@@ -434,4 +450,6 @@
     Clock,
     #[error("repository object format is not supported")]
     UnsupportedObjectFormat,
+    #[error("repository default-branch state is not consistent")]
+    DefaultBranchState,
 }

src/serve.rs

Mode 100644100644; object f70be4937ee7241a9c2a9b79

@@ -19,6 +19,7 @@
 use crate::maintenance::MaintenanceGate;
 use crate::policy::PolicyError;
 use crate::pull_request::{PullRequestError, PullRequestService};
+use crate::repository::{RepositoryService, RepositoryServiceError};
 use crate::session::{SessionError, WebLoginService};
 use crate::ssh::{AuthorizedSshKeys, LoginApprover, RunningSshServer, SshServerError};
 use crate::store::{Store, StoreError};
@@ -33,6 +34,7 @@
     let database = prepare_database(&config.instance_dir)?;
     let repository_root = prepare_repository_root(&config.instance_dir)?;
     let maintenance = MaintenanceGate::default();
+    RepositoryService::new_with_gate(&database, &repository_root, maintenance.clone()).recover()?;
     PullRequestService::new_with_gate(&database, &repository_root, maintenance.clone())
         .recover()?;
     let store = Store::open(&database)?;
@@ -50,8 +52,12 @@
         config.config_path.clone(),
         maintenance.clone(),
     );
-    let control =
-        RunningControlServer::start_with_backup(&config.instance_dir, accounts.clone(), backup)?;
+    let control = RunningControlServer::start_with_backup_and_telemetry(
+        &config.instance_dir,
+        accounts.clone(),
+        backup,
+        telemetry.clone(),
+    )?;
     let authorized_keys = AuthorizedSshKeys::for_accounts(keys);
     let readiness = ListenerReadiness::default();
 
@@ -289,6 +295,8 @@
     Authentication(#[from] AuthError),
     #[error(transparent)]
     Repository(#[from] RepositoryPathError),
+    #[error(transparent)]
+    RepositoryService(#[from] RepositoryServiceError),
     #[error(transparent)]
     Configuration(#[from] ConfigError),
     #[error(transparent)]

src/session.rs

Mode 100644100644; object 96fec71f8170471c521c801f

@@ -1,9 +1,10 @@
 use std::path::PathBuf;
-use std::time::{SystemTime, UNIX_EPOCH};
 
 use rand::TryRng;
 use sha2::{Digest, Sha256};
 use thiserror::Error;
+
+use crate::codec::encode_lower_hex;
 use url::Url;
 
 use crate::auth::{
@@ -13,6 +14,7 @@
     ApproveLogin, NewApprovedWebSession, NewAuditEvent, NewLoginApproval, NewLoginNonce,
     NewWebSession, Store, StoreError, WebSessionRecord,
 };
+use crate::system::unix_timestamp;
 
 const CHALLENGE_LIFETIME_SECONDS: u64 = 5 * 60;
 const SESSION_LIFETIME_SECONDS: i64 = 7 * 24 * 60 * 60;
@@ -40,7 +42,7 @@
             .checked_add(CHALLENGE_LIFETIME_SECONDS)
             .ok_or(SessionError::Clock)?;
         let nonce = random_bytes()?;
-        let login_csrf = encode_hex(&random_bytes()?);
+        let login_csrf = encode_lower_hex(&random_bytes()?);
         Store::open(&self.database)?.create_login_nonce(&NewLoginNonce {
             nonce_hash: &hash(&nonce),
             csrf_hash: &hash(login_csrf.as_bytes()),
@@ -67,8 +69,8 @@
                 i64::try_from(CHALLENGE_LIFETIME_SECONDS).map_err(|_| SessionError::Clock)?,
             )
             .ok_or(SessionError::Clock)?;
-        let secret = encode_hex(&random_bytes()?);
-        let login_csrf = encode_hex(&random_bytes()?);
+        let secret = encode_lower_hex(&random_bytes()?);
+        let login_csrf = encode_lower_hex(&random_bytes()?);
         Store::open(&self.database)?.create_login_approval(&NewLoginApproval {
             secret_hash: &hash(secret.as_bytes()),
             csrf_hash: &hash(login_csrf.as_bytes()),
@@ -97,7 +99,7 @@
                 i64::try_from(CHALLENGE_LIFETIME_SECONDS).map_err(|_| SessionError::Clock)?,
             )
             .ok_or(SessionError::Clock)?;
-        let secret = encode_hex(&random_bytes()?);
+        let secret = encode_lower_hex(&random_bytes()?);
         Store::open(&self.database)?.create_login_approval(&NewLoginApproval {
             secret_hash: &hash(secret.as_bytes()),
             csrf_hash: &hash(csrf.as_bytes()),
@@ -140,8 +142,8 @@
         validate_token(secret)?;
         validate_token(login_csrf)?;
         let created_at = now()?;
-        let session = encode_hex(&random_bytes()?);
-        let csrf = encode_hex(&random_bytes()?);
+        let session = encode_lower_hex(&random_bytes()?);
+        let csrf = encode_lower_hex(&random_bytes()?);
         let expires_at = created_at
             .checked_add(SESSION_LIFETIME_SECONDS)
             .ok_or(SessionError::Clock)?;
@@ -178,8 +180,8 @@
                 username,
                 u64::try_from(created_at).map_err(|_| SessionError::Clock)?,
             )?;
-            let session = encode_hex(&random_bytes()?);
-            let csrf = encode_hex(&random_bytes()?);
+            let session = encode_lower_hex(&random_bytes()?);
+            let csrf = encode_lower_hex(&random_bytes()?);
             let expires_at = created_at
                 .checked_add(SESSION_LIFETIME_SECONDS)
                 .ok_or(SessionError::Clock)?;
@@ -298,15 +300,6 @@
     Sha256::digest(value).into()
 }
 
-fn encode_hex(value: &[u8]) -> String {
-    let mut result = String::with_capacity(value.len() * 2);
-    for byte in value {
-        use std::fmt::Write as _;
-        write!(result, "{byte:02x}").expect("writing to a string cannot fail");
-    }
-    result
-}
-
 fn validate_token(token: &str) -> Result<(), SessionError> {
     if token.len() != SECRET_BYTES * 2 || !token.bytes().all(|byte| byte.is_ascii_hexdigit()) {
         return Err(SessionError::InvalidToken);
@@ -315,12 +308,7 @@
 }
 
 fn now() -> Result<i64, SessionError> {
-    SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .map_err(|_| SessionError::Clock)?
-        .as_secs()
-        .try_into()
-        .map_err(|_| SessionError::Clock)
+    unix_timestamp().ok_or(SessionError::Clock)
 }
 
 #[derive(Debug, Error)]

src/ssh.rs

Mode 100644100644; object 79f7d01005278af371e0244a

@@ -18,7 +18,7 @@
 use crate::auth::SshPublicKey;
 use crate::git::packetline::{MAX_REQUEST_BYTES, Packet, decode, encode_data, first_flush_end};
 use crate::git::receive_pack::{ReceivePack, ReceivePackError};
-use crate::git::transport::{GitRepositories, GitSshService};
+use crate::git::transport::{GitRepositories, GitSshService, RepositoryPathError};
 use crate::git::upload_pack::{ProtocolVersion, UploadPack, UploadPackError};
 use crate::issue::{IssueError, IssueService, MAX_BODY_BYTES, MAX_TITLE_BYTES};
 use crate::policy::RepositoryOperation;
@@ -890,7 +890,17 @@
                 )?,
             }
         } else {
-            let service = self.open_git_service(command).await;
+            let service = match self.open_git_service(command).await {
+                Ok(service) => service,
+                Err(error) => {
+                    self.audit.rejected_exec.fetch_add(1, Ordering::Relaxed);
+                    self.telemetry
+                        .failure("ssh.git", Some(&operation_id), &error.to_string());
+                    session.channel_success(channel)?;
+                    fail_git_channel(channel, session)?;
+                    return Ok(());
+                }
+            };
             if let Some(service) = service {
                 self.audit.accepted_exec.fetch_add(1, Ordering::Relaxed);
                 session.channel_success(channel)?;
@@ -959,7 +969,7 @@
                     }
                 }
             } else {
-                self.audit.accepted_exec.fetch_add(1, Ordering::Relaxed);
+                self.audit.rejected_exec.fetch_add(1, Ordering::Relaxed);
                 session.channel_success(channel)?;
                 if requests_json(command) {
                     session.data(
@@ -2470,23 +2480,44 @@
         }
     }
 
-    async fn open_git_service(&mut self, command: &[u8]) -> Option<InitialGitService> {
+    async fn open_git_service(
+        &mut self,
+        command: &[u8],
+    ) -> Result<Option<InitialGitService>, GitServiceOpenError> {
         let active_channels = self
             .exec_channels
             .values()
             .filter(|channel| matches!(channel, ExecChannel::Upload(_) | ExecChannel::Receive(_)))
             .count();
-        let global_permit = reserve_git_channel(active_channels, &self.git_channels)?;
-        let repositories = self.repositories.as_ref()?;
-        let identity = self.active_identity()?;
-        let service = repositories
-            .resolve_ssh_service_for(Some(&identity.username), command)
-            .ok()?;
+        let Some(global_permit) = reserve_git_channel(active_channels, &self.git_channels) else {
+            return Ok(None);
+        };
+        let Some(repositories) = self.repositories.as_ref() else {
+            return Ok(None);
+        };
+        let Some(identity) = self.active_identity() else {
+            return Ok(None);
+        };
+        let service = match repositories.resolve_ssh_service_for(Some(&identity.username), command)
+        {
+            Ok(service) => service,
+            Err(
+                RepositoryPathError::InvalidName
+                | RepositoryPathError::InvalidCommand
+                | RepositoryPathError::Unauthorized,
+            ) => return Ok(None),
+            Err(RepositoryPathError::Repository { source, .. })
+                if source.kind() == std::io::ErrorKind::NotFound =>
+            {
+                return Ok(None);
+            }
+            Err(error) => return Err(error.into()),
+        };
         match service {
             GitSshService::Upload { path, .. } => {
-                let permit = repositories.blocking_permit().await.ok()?;
+                let permit = repositories.blocking_permit().await?;
                 let protocol = self.protocol;
-                tokio::task::spawn_blocking(move || {
+                let service = tokio::task::spawn_blocking(move || {
                     let _permit = permit;
                     let service = UploadPack::open(&path)?;
                     let advertisement = service.advertisement(protocol, false)?;
@@ -2496,9 +2527,8 @@
                         global_permit,
                     })
                 })
-                .await
-                .ok()?
-                .ok()
+                .await??;
+                Ok(Some(service))
             }
             GitSshService::Receive {
                 path,
@@ -2506,14 +2536,19 @@
                 repository,
             } => {
                 if !repositories.uses_policy() && !self.authenticated_writer {
-                    return None;
+                    return Ok(None);
                 }
-                let database = repositories.push_database()?.to_owned();
+                let database = repositories
+                    .push_database()
+                    .ok_or(GitServiceOpenError::PushDatabase)?
+                    .to_owned();
                 let actor = identity.username.clone();
-                let public_key = self.authenticated_key.clone()?;
+                let Some(public_key) = self.authenticated_key.clone() else {
+                    return Ok(None);
+                };
                 let uses_policy = repositories.uses_policy();
-                let permit = repositories.blocking_permit().await.ok()?;
-                tokio::task::spawn_blocking(move || {
+                let permit = repositories.blocking_permit().await?;
+                let service = tokio::task::spawn_blocking(move || {
                     let _permit = permit;
                     let service = if uses_policy {
                         ReceivePack::open_authorized(
@@ -2539,9 +2574,8 @@
                         },
                     )))
                 })
-                .await
-                .ok()?
-                .ok()
+                .await??;
+                Ok(Some(service))
             }
         }
     }
@@ -2552,6 +2586,22 @@
         let current = self.authorized_keys.identity(public_key)?;
         (current == *authenticated).then_some(current)
     }
+}
+
+#[derive(Debug, Error)]
+enum GitServiceOpenError {
+    #[error(transparent)]
+    Repository(#[from] RepositoryPathError),
+    #[error(transparent)]
+    Upload(#[from] UploadPackError),
+    #[error(transparent)]
+    Receive(#[from] ReceivePackError),
+    #[error("the Git worker stopped")]
+    Join(#[from] tokio::task::JoinError),
+    #[error("the Git work queue is closed")]
+    WorkQueue(#[from] tokio::sync::AcquireError),
+    #[error("the push database is unavailable")]
+    PushDatabase,
 }
 
 enum InitialGitService {

src/store/event.rs

Mode 100644100644; object ca8f480c077466e8fed3be22

@@ -1,5 +1,6 @@
 use serde_json::json;
 
+use crate::codec::encode_lower_hex;
 pub(super) const PAYLOAD_VERSION: i64 = 1;
 
 #[derive(Clone, Copy)]
@@ -147,7 +148,7 @@
             "revision": revision,
             "body": body,
             "commit_object_id": commit_object_id,
-            "path_hex": path.map(encode_hex),
+            "path_hex": path.map(encode_lower_hex),
             "side": side,
             "line": line,
         })
@@ -248,7 +249,7 @@
         kind,
         payload: json!({
             "version": PAYLOAD_VERSION,
-            "name_hex": encode_hex(name),
+            "name_hex": encode_lower_hex(name),
             "old_target": old_target,
             "new_target": new_target,
         })
@@ -321,13 +322,4 @@
         })
         .to_string(),
     }
-}
-
-fn encode_hex(bytes: &[u8]) -> String {
-    let mut encoded = String::with_capacity(bytes.len().saturating_mul(2));
-    for byte in bytes {
-        use std::fmt::Write;
-        write!(encoded, "{byte:02x}").expect("a string write cannot fail");
-    }
-    encoded
 }

src/store/migrations/024_default_branch_intents.sql

Mode 100644; object db9e83d5900b

@@ -1,0 +1,21 @@
+CREATE TABLE repository_default_branch_intent (
+    id TEXT PRIMARY KEY
+        CHECK (
+            length(id) = 32
+            AND id NOT GLOB '*[^0-9a-f]*'
+        ),
+    repository_id TEXT NOT NULL UNIQUE
+        REFERENCES repository(id) ON DELETE CASCADE,
+    actor TEXT NOT NULL,
+    previous_ref_name TEXT NOT NULL
+        CHECK (
+            length(previous_ref_name) BETWEEN 12 AND 1024
+            AND substr(previous_ref_name, 1, 11) = 'refs/heads/'
+        ),
+    proposed_ref_name TEXT NOT NULL
+        CHECK (
+            length(proposed_ref_name) BETWEEN 12 AND 1024
+            AND substr(proposed_ref_name, 1, 11) = 'refs/heads/'
+        ),
+    created_at INTEGER NOT NULL CHECK (created_at >= 0)
+) STRICT;

src/store/mod.rs

Mode 100644100644; object e597a1c32db3ef2edd91c364

@@ -10,12 +10,13 @@
 use serde::Serialize;
 use thiserror::Error;
 
+use crate::codec::encode_lower_hex;
 mod event;
 
 const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
 const BUSY_TIMEOUT_MILLISECONDS: i64 = 5_000;
 const MAX_ACTIVE_FEED_TOKENS: i64 = 1;
-const SCHEMA_VERSION: i64 = 23;
+const SCHEMA_VERSION: i64 = 24;
 #[allow(
     dead_code,
     reason = "the integration test imports this module without the CLI operation"
@@ -23,9 +24,9 @@
 pub(crate) const DATABASE_FILE: &str = "tit.sqlite3";
 #[allow(
     dead_code,
-    reason = "M1A proves migrations before the M2 server calls them"
+    reason = "some integration test crates import storage without migration operations"
 )]
-const MIGRATIONS: [&str; 23] = [
+const MIGRATIONS: [&str; 24] = [
     include_str!("migrations/001_initial.sql"),
     include_str!("migrations/002_state.sql"),
     include_str!("migrations/003_git_intents.sql"),
@@ -49,6 +50,7 @@
     include_str!("migrations/021_pull_request_lifecycle.sql"),
     include_str!("migrations/022_account_key_management.sql"),
     include_str!("migrations/023_default_branch.sql"),
+    include_str!("migrations/024_default_branch_intents.sql"),
 ];
 
 #[allow(
@@ -59,9 +61,14 @@
 pub(crate) enum StoreError {
     #[error("SQLite error: {0}")]
     Sqlite(#[from] rusqlite::Error),
+    #[error("cannot read migration path {path}: {source}")]
+    MigrationFilesystem {
+        path: PathBuf,
+        source: std::io::Error,
+    },
     #[allow(
         dead_code,
-        reason = "M1A proves migrations before the M2 server calls them"
+        reason = "some integration test crates import storage without schema-version rejection"
     )]
     #[error("database schema version {0} is newer than this executable")]
     NewerSchema(i64),
@@ -121,6 +128,8 @@
     InvalidRepositoryVisibility,
     #[error("repository default branch is not valid")]
     InvalidDefaultBranch,
+    #[error("repository default-branch intent {0} is not in the required state")]
+    DefaultBranchIntentState(String),
     #[error("collaborator role is not valid")]
     InvalidCollaboratorRole,
     #[error("repository owner cannot be a collaborator")]
@@ -214,7 +223,7 @@
 
     #[allow(
         dead_code,
-        reason = "M1A proves migrations before the M2 server calls them"
+        reason = "some integration test crates import storage without opening a database"
     )]
     pub(crate) fn open(path: &Path) -> Result<Self, StoreError> {
         let mut store = Self::open_unmigrated(path)?;
@@ -228,7 +237,7 @@
 
     #[allow(
         dead_code,
-        reason = "M1A proves migrations before the M2 server calls them"
+        reason = "some integration test crates import storage without migration setup"
     )]
     pub(crate) fn open_unmigrated(path: &Path) -> Result<Self, StoreError> {
         let connection = Connection::open(path)?;
@@ -248,7 +257,7 @@
 
     #[allow(
         dead_code,
-        reason = "M1A proves migrations before the M2 server calls them"
+        reason = "some integration test crates import storage without direct migration"
     )]
     pub(crate) fn migrate(&mut self) -> Result<(), StoreError> {
         self.migrate_with_hook(|_| {})
@@ -256,7 +265,7 @@
 
     #[allow(
         dead_code,
-        reason = "M1A proves migrations before the M2 server calls them"
+        reason = "some integration test crates import storage without migration hooks"
     )]
     pub(crate) fn migrate_with_hook(
         &mut self,
@@ -270,6 +279,23 @@
             return Ok(());
         }
 
+        let instance = if current < 23 {
+            let database = self.connection.path().ok_or_else(|| {
+                StoreError::Integrity("a database migration requires a filesystem path".to_owned())
+            })?;
+            Some(
+                Path::new(database)
+                    .parent()
+                    .ok_or_else(|| {
+                        StoreError::Integrity(
+                            "the database migration path has no parent".to_owned(),
+                        )
+                    })?
+                    .to_owned(),
+            )
+        } else {
+            None
+        };
         let transaction = self
             .connection
             .transaction_with_behavior(TransactionBehavior::Exclusive)?;
@@ -278,56 +304,11 @@
             transaction.pragma_update(None, "user_version", version)?;
             after_migration(version);
         }
+        if let Some(instance) = instance {
+            backfill_default_branches(&transaction, &instance)?;
+        }
         transaction.commit()?;
-        if current < 23 {
-            self.backfill_default_branches();
-        }
         Ok(())
-    }
-
-    fn backfill_default_branches(&self) {
-        let Some(database) = self.connection.path() else {
-            return;
-        };
-        let Some(instance) = Path::new(database).parent() else {
-            return;
-        };
-        let Ok(mut statement) = self.connection.prepare("SELECT id FROM repository") else {
-            return;
-        };
-        let Ok(ids) = statement.query_map([], |row| row.get::<_, String>(0)) else {
-            return;
-        };
-        for id in ids.flatten() {
-            let head = instance
-                .join("repositories")
-                .join(format!("{id}.git"))
-                .join("HEAD");
-            let Ok(contents) = fs::read(head) else {
-                continue;
-            };
-            let Some(name) = contents
-                .strip_suffix(b"\n")
-                .unwrap_or(&contents)
-                .strip_prefix(b"ref: ")
-            else {
-                continue;
-            };
-            let candidate = gix::bstr::BString::from(name);
-            if !name.starts_with(b"refs/heads/")
-                || gix::refs::FullName::try_from(candidate).is_err()
-            {
-                continue;
-            }
-            let Ok(name) = std::str::from_utf8(name) else {
-                continue;
-            };
-            let _ = self.connection.execute(
-                "UPDATE repository_default_branch SET ref_name = ?2
-                 WHERE repository_id = ?1",
-                rusqlite::params![id, name],
-            );
-        }
     }
 
     pub(crate) fn schema_version(&self) -> Result<i64, StoreError> {
@@ -355,7 +336,10 @@
         Ok(())
     }
 
-    #[allow(dead_code, reason = "M1A proves backup before the M2 server calls it")]
+    #[allow(
+        dead_code,
+        reason = "some integration test crates import storage without backup operations"
+    )]
     pub(crate) fn backup(&self, path: &Path) -> Result<(), StoreError> {
         let mut destination = Connection::open(path)?;
         let backup = Backup::new(&self.connection, &mut destination)?;
@@ -404,7 +388,7 @@
 
     #[allow(
         dead_code,
-        reason = "M1A tests storage behavior through this narrow test boundary"
+        reason = "integration storage tests use this database test boundary"
     )]
     pub(crate) fn connection(&self) -> &Connection {
         &self.connection
@@ -412,7 +396,7 @@
 
     #[allow(
         dead_code,
-        reason = "M1A tests storage behavior through this narrow test boundary"
+        reason = "integration storage tests use this mutable database test boundary"
     )]
     pub(crate) fn connection_mut(&mut self) -> &mut Connection {
         &mut self.connection
@@ -1890,26 +1874,86 @@
             .ok_or_else(|| StoreError::RepositoryNotFound(owner.to_owned(), slug.to_owned()))
     }
 
-    pub(crate) fn update_repository_default_branch(
+    pub(crate) fn begin_repository_default_branch(
         &mut self,
-        owner: &str,
-        slug: &str,
-        actor: &str,
-        default_branch: &str,
-        changed_at: i64,
-        correlation_id: &str,
+        intent: &NewDefaultBranchIntent<'_>,
     ) -> Result<(), StoreError> {
         let transaction = self
             .connection
             .transaction_with_behavior(TransactionBehavior::Immediate)?;
-        let access = repository_issue_access(&transaction, owner, slug, Some(actor))?;
+        let access =
+            repository_issue_access(&transaction, intent.owner, intent.slug, Some(intent.actor))?;
         if !access.can_maintain() {
             return Err(StoreError::PullRequestDenied);
         }
+        let stored: String = transaction.query_row(
+            "SELECT ref_name FROM repository_default_branch WHERE repository_id = ?1",
+            [&access.repository.id],
+            |row| row.get(0),
+        )?;
+        if stored != intent.previous_branch {
+            return Err(StoreError::InvalidDefaultBranch);
+        }
+        transaction.execute(
+            "INSERT INTO repository_default_branch_intent
+             (id, repository_id, actor, previous_ref_name, proposed_ref_name, created_at)
+             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
+            rusqlite::params![
+                intent.id,
+                access.repository.id,
+                intent.actor,
+                intent.previous_branch,
+                intent.default_branch,
+                intent.changed_at
+            ],
+        )?;
+        transaction.commit()?;
+        Ok(())
+    }
+
+    pub(crate) fn complete_repository_default_branch(
+        &mut self,
+        id: &str,
+    ) -> Result<(), StoreError> {
+        let transaction = self
+            .connection
+            .transaction_with_behavior(TransactionBehavior::Immediate)?;
+        let (repository_id, owner, slug, actor, default_branch, changed_at): (
+            String,
+            String,
+            String,
+            String,
+            String,
+            i64,
+        ) = transaction
+            .query_row(
+                "SELECT repository.id, account.username, repository.slug,
+                        repository_default_branch_intent.actor,
+                        repository_default_branch_intent.proposed_ref_name,
+                        repository_default_branch_intent.created_at
+                 FROM repository_default_branch_intent
+                 JOIN repository
+                   ON repository.id = repository_default_branch_intent.repository_id
+                 JOIN account ON account.id = repository.owner_account_id
+                 WHERE repository_default_branch_intent.id = ?1",
+                [id],
+                |row| {
+                    Ok((
+                        row.get(0)?,
+                        row.get(1)?,
+                        row.get(2)?,
+                        row.get(3)?,
+                        row.get(4)?,
+                        row.get(5)?,
+                    ))
+                },
+            )
+            .optional()?
+            .ok_or_else(|| StoreError::DefaultBranchIntentState(id.to_owned()))?;
         let changed = transaction.execute(
             "UPDATE repository_default_branch SET ref_name = ?2
              WHERE repository_id = ?1",
-            rusqlite::params![access.repository.id, default_branch],
+            rusqlite::params![repository_id, default_branch],
         )?;
         if changed != 1 {
             return Err(StoreError::InvalidDefaultBranch);
@@ -1919,15 +1963,58 @@
             &transaction,
             &NewAuditEvent {
                 action: "repository.default-branch",
-                actor,
+                actor: &actor,
                 target: &target,
                 outcome: "success",
-                correlation_id,
+                correlation_id: id,
                 created_at: changed_at,
             },
         )?;
+        let deleted = transaction.execute(
+            "DELETE FROM repository_default_branch_intent WHERE id = ?1",
+            [id],
+        )?;
+        if deleted != 1 {
+            return Err(StoreError::DefaultBranchIntentState(id.to_owned()));
+        }
         transaction.commit()?;
         Ok(())
+    }
+
+    pub(crate) fn abandon_repository_default_branch(&mut self, id: &str) -> Result<(), StoreError> {
+        let changed = self.connection.execute(
+            "DELETE FROM repository_default_branch_intent WHERE id = ?1",
+            [id],
+        )?;
+        if changed != 1 {
+            return Err(StoreError::DefaultBranchIntentState(id.to_owned()));
+        }
+        Ok(())
+    }
+
+    pub(crate) fn incomplete_repository_default_branches(
+        &self,
+    ) -> Result<Vec<DefaultBranchIntentRecord>, StoreError> {
+        let mut statement = self.connection.prepare(
+            "SELECT repository_default_branch_intent.id,
+                    repository_default_branch_intent.repository_id,
+                    repository_default_branch_intent.previous_ref_name,
+                    repository_default_branch_intent.proposed_ref_name
+             FROM repository_default_branch_intent
+             ORDER BY repository_default_branch_intent.created_at,
+                      repository_default_branch_intent.id",
+        )?;
+        statement
+            .query_map([], |row| {
+                Ok(DefaultBranchIntentRecord {
+                    id: row.get(0)?,
+                    repository_id: row.get(1)?,
+                    previous_branch: row.get(2)?,
+                    proposed_branch: row.get(3)?,
+                })
+            })?
+            .collect::<Result<Vec<_>, _>>()
+            .map_err(Into::into)
     }
 
     pub(crate) fn repository_description(&self, repository_id: &str) -> Result<String, StoreError> {
@@ -4894,6 +4981,23 @@
     pub(crate) pack_name: Option<String>,
 }
 
+pub(crate) struct DefaultBranchIntentRecord {
+    pub(crate) id: String,
+    pub(crate) repository_id: String,
+    pub(crate) previous_branch: String,
+    pub(crate) proposed_branch: String,
+}
+
+pub(crate) struct NewDefaultBranchIntent<'a> {
+    pub(crate) id: &'a str,
+    pub(crate) owner: &'a str,
+    pub(crate) slug: &'a str,
+    pub(crate) actor: &'a str,
+    pub(crate) previous_branch: &'a str,
+    pub(crate) default_branch: &'a str,
+    pub(crate) changed_at: i64,
+}
+
 #[allow(
     dead_code,
     reason = "some integration tests compile storage without accounts"
@@ -5791,20 +5895,10 @@
         ValueRef::Real(value) => DumpValue::Real(format!("{value:.17e}")),
         ValueRef::Text(value) => match std::str::from_utf8(value) {
             Ok(value) => DumpValue::TextUtf8(value.to_owned()),
-            Err(_) => DumpValue::TextHex(hex(value)),
+            Err(_) => DumpValue::TextHex(encode_lower_hex(value)),
         },
-        ValueRef::Blob(value) => DumpValue::BlobHex(hex(value)),
+        ValueRef::Blob(value) => DumpValue::BlobHex(encode_lower_hex(value)),
     }
-}
-
-fn hex(bytes: &[u8]) -> String {
-    const HEX: &[u8; 16] = b"0123456789abcdef";
-    let mut output = String::with_capacity(bytes.len() * 2);
-    for byte in bytes {
-        output.push(char::from(HEX[(byte >> 4) as usize]));
-        output.push(char::from(HEX[(byte & 0x0f) as usize]));
-    }
-    output
 }
 
 fn is_unique_constraint(error: &rusqlite::Error) -> bool {
@@ -5821,6 +5915,60 @@
         .and_then(|page| page.checked_mul(page_size))
         .and_then(|offset| i64::try_from(offset).ok())
         .ok_or(StoreError::EventLimit)
+}
+
+fn backfill_default_branches(
+    transaction: &rusqlite::Transaction<'_>,
+    instance: &Path,
+) -> Result<(), StoreError> {
+    let ids = {
+        let mut statement = transaction.prepare("SELECT id FROM repository")?;
+        statement
+            .query_map([], |row| row.get::<_, String>(0))?
+            .collect::<Result<Vec<_>, _>>()?
+    };
+    for id in ids {
+        let head = instance
+            .join("repositories")
+            .join(format!("{id}.git"))
+            .join("HEAD");
+        let contents = match fs::read(&head) {
+            Ok(contents) => contents,
+            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
+            Err(source) => return Err(StoreError::MigrationFilesystem { path: head, source }),
+        };
+        let name = contents
+            .strip_suffix(b"\n")
+            .unwrap_or(&contents)
+            .strip_prefix(b"ref: ")
+            .ok_or_else(|| {
+                StoreError::Integrity(format!(
+                    "repository {id} has a non-symbolic HEAD during migration"
+                ))
+            })?;
+        let candidate = gix::bstr::BString::from(name);
+        if !name.starts_with(b"refs/heads/") || gix::refs::FullName::try_from(candidate).is_err() {
+            return Err(StoreError::Integrity(format!(
+                "repository {id} has an invalid HEAD during migration"
+            )));
+        }
+        let name = std::str::from_utf8(name).map_err(|_| {
+            StoreError::Integrity(format!(
+                "repository {id} has a non-UTF-8 HEAD during migration"
+            ))
+        })?;
+        let changed = transaction.execute(
+            "UPDATE repository_default_branch SET ref_name = ?2
+             WHERE repository_id = ?1",
+            rusqlite::params![id, name],
+        )?;
+        if changed != 1 {
+            return Err(StoreError::Integrity(format!(
+                "repository {id} has no default-branch row during migration"
+            )));
+        }
+    }
+    Ok(())
 }
 
 #[allow(
@@ -5842,7 +5990,7 @@
 
 #[allow(
     dead_code,
-    reason = "M1A proves migrations before the M2 server calls them"
+    reason = "some integration test crates import storage without migration backup paths"
 )]
 fn migration_backup_path(path: &Path, version: i64) -> PathBuf {
     let mut backup = OsString::from(path.as_os_str());

src/system.rs

Mode 100644; object e199cfe70f74

@@ -1,0 +1,25 @@
+#![allow(
+    dead_code,
+    reason = "integration test crates use only the system operations required by their imported modules"
+)]
+
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use rand::TryRng;
+
+use crate::codec::encode_lower_hex;
+
+pub(crate) fn random_lower_hex<const N: usize>() -> Option<String> {
+    let mut bytes = [0_u8; N];
+    rand::rngs::SysRng.try_fill_bytes(&mut bytes).ok()?;
+    Some(encode_lower_hex(&bytes))
+}
+
+pub(crate) fn unix_timestamp() -> Option<i64> {
+    SystemTime::now()
+        .duration_since(UNIX_EPOCH)
+        .ok()?
+        .as_secs()
+        .try_into()
+        .ok()
+}

src/telemetry.rs

Mode 100644100644; object 4385dc68ebe68eecb5d9eaf3

@@ -71,6 +71,7 @@
             status: Some(status),
             duration_ms: Some(duration.as_millis().min(u128::from(u64::MAX)) as u64),
             outcome: None,
+            error: None,
         });
     }
 
@@ -114,6 +115,22 @@
             status: None,
             duration_ms: None,
             outcome: Some(outcome),
+            error: None,
+        });
+    }
+
+    pub(crate) fn failure(&self, event: &'static str, operation_id: Option<&str>, error: &str) {
+        self.write_event(&Event {
+            timestamp_ms: timestamp_ms(),
+            level: "error",
+            event,
+            request_id: None,
+            operation_id,
+            method: None,
+            status: None,
+            duration_ms: None,
+            outcome: Some("failure"),
+            error: Some(error),
         });
     }
 
@@ -147,6 +164,7 @@
             status: None,
             duration_ms: None,
             outcome: Some(outcome),
+            error: None,
         });
     }
 
@@ -187,6 +205,8 @@
     duration_ms: Option<u64>,
     #[serde(skip_serializing_if = "Option::is_none")]
     outcome: Option<&'static str>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    error: Option<&'a str>,
 }
 
 #[cfg(test)]

src/watch.rs

Mode 100644100644; object 0788253bde406d83643be499

@@ -1,11 +1,11 @@
 use std::path::{Path, PathBuf};
-use std::time::{SystemTime, UNIX_EPOCH};
 
 use thiserror::Error;
 
 use crate::auth::{AuthError, validate_username};
 use crate::domain::repository::{RepositoryNameError, validate_slug};
 use crate::store::{RepositoryRecord, Store, StoreError, WatchRecord};
+use crate::system::unix_timestamp;
 
 #[derive(Clone)]
 pub(crate) struct WatchService {
@@ -57,11 +57,7 @@
 }
 
 fn timestamp() -> Result<i64, WatchError> {
-    let seconds = SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .map_err(|_| WatchError::Clock)?
-        .as_secs();
-    i64::try_from(seconds).map_err(|_| WatchError::Clock)
+    unix_timestamp().ok_or(WatchError::Clock)
 }
 
 #[derive(Debug, Error)]

tests/account_lifecycle.rs

Mode 100644100644; object 4e21ed4a8718240f4738ec4b

@@ -1,17 +1,4 @@
-#[path = "../src/account.rs"]
-mod account;
-#[allow(
-    dead_code,
-    reason = "the account test uses only SSH public-key parsing"
-)]
-#[path = "../src/auth.rs"]
-mod auth;
-#[allow(
-    dead_code,
-    reason = "the account test does not use every store operation"
-)]
-#[path = "../src/store/mod.rs"]
-mod store;
+use crate::{account, auth, store};
 
 use rand::rng;
 use rusqlite::OptionalExtension;

tests/auth.rs

Mode 100644100644; object cc338bd31d0b3e372612f2e6

@@ -1,5 +1,4 @@
-#[path = "../src/auth.rs"]
-mod auth;
+use crate::auth;
 
 use std::fs;
 use std::path::{Path, PathBuf};

tests/cli.rs

Mode 100644100644; object 612eb342f7dde5eb4010bd82

@@ -32,7 +32,8 @@
     include_str!("../src/store/migrations/021_pull_request_lifecycle.sql"),
     include_str!("../src/store/migrations/022_account_key_management.sql"),
     include_str!("../src/store/migrations/023_default_branch.sql"),
-    "PRAGMA user_version = 23;\n",
+    include_str!("../src/store/migrations/024_default_branch_intents.sql"),
+    "PRAGMA user_version = 24;\n",
 );
 
 #[test]

tests/git_http.rs

Mode 100644100644; object 6c66c08e5a664952d1d924cc

@@ -1,35 +1,4 @@
-#[path = "../src/git/http.rs"]
-mod http;
-#[allow(dead_code, reason = "the HTTP test does not run maintenance")]
-#[path = "../src/maintenance.rs"]
-mod maintenance;
-#[allow(
-    dead_code,
-    reason = "the HTTP test does not use each shared protocol API"
-)]
-#[path = "../src/git/packetline.rs"]
-mod packetline;
-#[allow(dead_code, reason = "the HTTP test does not use repository policy")]
-#[path = "../src/policy.rs"]
-mod policy;
-#[allow(
-    dead_code,
-    reason = "the HTTP test does not inspect repository internals"
-)]
-#[path = "../src/git/repository.rs"]
-mod repository;
-#[allow(dead_code, reason = "the HTTP test does not use the intent store")]
-#[path = "../src/store/mod.rs"]
-mod store;
-#[allow(
-    dead_code,
-    reason = "the HTTP test uses transport resolution through HTTP"
-)]
-#[path = "../src/git/transport.rs"]
-mod transport;
-#[allow(dead_code, reason = "the HTTP test uses upload-pack through HTTP")]
-#[path = "../src/git/upload_pack.rs"]
-mod upload_pack;
+use crate::git::{http, packetline, transport, upload_pack};
 
 use std::fs;
 use std::io::{Read, Write};
@@ -314,7 +283,7 @@
     let output = Command::new(std::env::current_exe().expect("find the test executable"))
         .args([
             "--exact",
-            "server_process_does_not_invoke_git",
+            "git_http_tests::server_process_does_not_invoke_git",
             "--nocapture",
         ])
         .env(CHILD_VARIABLE, "1")

tests/git_push_ssh.rs

Mode 100644100644; object af2de5b2427a293f4bc0f399

@@ -1,46 +1,4 @@
-#[allow(
-    dead_code,
-    reason = "the SSH push test does not use each authentication API"
-)]
-#[path = "../src/auth.rs"]
-mod auth;
-#[allow(dead_code, reason = "the SSH push test does not use domain models")]
-#[path = "../src/domain/mod.rs"]
-mod domain;
-#[allow(
-    dead_code,
-    reason = "the SSH push test does not use each Git service API"
-)]
-#[path = "../src/git/mod.rs"]
-mod git;
-#[allow(dead_code, reason = "the SSH push test does not use issue commands")]
-#[path = "../src/issue.rs"]
-mod issue;
-#[allow(dead_code, reason = "the SSH push test does not run maintenance")]
-#[path = "../src/maintenance.rs"]
-mod maintenance;
-#[allow(dead_code, reason = "the SSH push test does not use repository policy")]
-#[path = "../src/policy.rs"]
-mod policy;
-#[allow(dead_code, reason = "the SSH push test does not use pull requests")]
-#[path = "../src/pull_request.rs"]
-mod pull_request;
-#[path = "../src/rate_limit.rs"]
-mod rate_limit;
-#[allow(dead_code, reason = "the SSH push test does not create repositories")]
-#[path = "../src/repository.rs"]
-mod repository;
-#[allow(
-    dead_code,
-    reason = "the SSH push test does not inspect the request audit"
-)]
-#[path = "../src/ssh.rs"]
-mod ssh;
-#[allow(dead_code, reason = "the SSH push test does not use each store API")]
-#[path = "../src/store/mod.rs"]
-mod store;
-#[path = "../src/telemetry.rs"]
-mod telemetry;
+use crate::{auth, git, ssh, store};
 
 use std::env;
 use std::fs;
@@ -402,7 +360,7 @@
     Command::new(env::current_exe().expect("find the integration test executable"))
         .args([
             "--exact",
-            "git_push_crash_child",
+            "git_push_ssh_tests::git_push_crash_child",
             "--nocapture",
             "--test-threads=1",
         ])

tests/git_reads.rs

Mode 100644100644; object 3919f6aca81fb08b72e0ca2b

@@ -1,11 +1,4 @@
-#[path = "../src/git/patch.rs"]
-mod patch;
-#[allow(
-    dead_code,
-    reason = "the test uses each public read contract selectively"
-)]
-#[path = "../src/git/read.rs"]
-mod read;
+use crate::git::{patch, read};
 
 use std::collections::BTreeSet;
 use std::fs;
@@ -62,6 +55,16 @@
         assert_eq!(
             history.iter().map(|commit| commit.id).collect::<Vec<_>>(),
             [fixture.second, fixture.first]
+        );
+        let history_prefix = service
+            .history_prefix(fixture.second, 1, &cancellation)
+            .expect("read a history prefix");
+        assert_eq!(
+            history_prefix
+                .iter()
+                .map(|commit| commit.id)
+                .collect::<Vec<_>>(),
+            [fixture.second]
         );
 
         let root = service

tests/git_repository.rs

Mode 100644100644; object 578d3175bb9c064f1da88ab2

@@ -1,6 +1,4 @@
-#[allow(dead_code, reason = "the repository test exercises selected Git APIs")]
-#[path = "../src/git/repository.rs"]
-mod repository;
+use crate::git::{repository, upload_pack};
 
 use std::fs;
 use std::io::Write;
@@ -11,6 +9,7 @@
 use gix::hash::ObjectId;
 use repository::{GitRepository, GitRepositoryError};
 use tempfile::TempDir;
+use upload_pack::advertised_ref;
 
 #[test]
 fn opens_empty_sha1_and_sha256_bare_repositories() {
@@ -92,6 +91,10 @@
         assert!(references.iter().any(|reference| {
             reference.name == b"refs/tags/v1" && reference.peeled == Some(first)
         }));
+        assert!(matches!(
+            source.references_with_limit(1),
+            Err(GitRepositoryError::ReferenceLimit)
+        ));
 
         let pack = source
             .make_pack(&[first], &[])
@@ -110,6 +113,24 @@
         index_pack(&destination, &incremental);
         assert_object(&destination, second, "commit");
     }
+}
+
+#[test]
+fn formats_reference_names_as_git_bytes() {
+    let directory = TempDir::new().expect("create a repository directory");
+    let repository_path = directory.path().join("source");
+    let commit = make_fixture(&repository_path, "sha1");
+    let name = b"topic-\xff".to_vec();
+    let advertisement = advertised_ref(commit, &name, Some(b"agent=tit"));
+    let commit_text = commit.to_string();
+    let expected = [
+        commit_text.as_bytes(),
+        b" ",
+        name.as_slice(),
+        b"\0agent=tit\n",
+    ]
+    .concat();
+    assert_eq!(advertisement, expected);
 }
 
 #[test]

tests/git_ssh.rs

Mode 100644100644; object 2bbdc070f0a24b206b369ccd

@@ -1,46 +1,4 @@
-#[allow(
-    dead_code,
-    reason = "the SSH Git test does not use each authentication API"
-)]
-#[path = "../src/auth.rs"]
-mod auth;
-#[allow(dead_code, reason = "the SSH Git test does not use domain models")]
-#[path = "../src/domain/mod.rs"]
-mod domain;
-#[allow(
-    dead_code,
-    reason = "the SSH Git test does not use each Git service API"
-)]
-#[path = "../src/git/mod.rs"]
-mod git;
-#[allow(dead_code, reason = "the SSH Git test does not use issue commands")]
-#[path = "../src/issue.rs"]
-mod issue;
-#[allow(dead_code, reason = "the SSH Git test does not run maintenance")]
-#[path = "../src/maintenance.rs"]
-mod maintenance;
-#[allow(dead_code, reason = "the SSH Git test does not use repository policy")]
-#[path = "../src/policy.rs"]
-mod policy;
-#[allow(dead_code, reason = "the SSH Git test does not use pull requests")]
-#[path = "../src/pull_request.rs"]
-mod pull_request;
-#[path = "../src/rate_limit.rs"]
-mod rate_limit;
-#[allow(dead_code, reason = "the SSH Git test does not create repositories")]
-#[path = "../src/repository.rs"]
-mod repository;
-#[allow(
-    dead_code,
-    reason = "the SSH Git test does not inspect the request audit"
-)]
-#[path = "../src/ssh.rs"]
-mod ssh;
-#[allow(dead_code, reason = "the SSH Git test does not use the intent store")]
-#[path = "../src/store/mod.rs"]
-mod store;
-#[path = "../src/telemetry.rs"]
-mod telemetry;
+use crate::{auth, git, ssh};
 
 use std::fs;
 use std::net::{Ipv4Addr, SocketAddr};

tests/metadata_search.rs

Mode 100644100644; object c05fefc46594f9e983c6ad13

@@ -1,13 +1,4 @@
-#[allow(dead_code, reason = "the search test uses only username validation")]
-#[path = "../src/auth.rs"]
-mod auth;
-#[path = "../src/domain/mod.rs"]
-mod domain;
-#[path = "../src/search.rs"]
-mod search;
-#[allow(dead_code, reason = "the search test uses only metadata storage")]
-#[path = "../src/store/mod.rs"]
-mod store;
+use crate::{search, store};
 
 use std::time::{Duration, Instant};
 

tests/public_routes.rs

Mode 100644100644; object 53d10b0cc8d92bc6f282dfac

@@ -1,80 +1,4 @@
-#[allow(
-    dead_code,
-    reason = "the public-route test does not use account mutations"
-)]
-#[path = "../src/account.rs"]
-mod account;
-#[allow(
-    dead_code,
-    reason = "the public-route test uses only username validation"
-)]
-#[path = "../src/auth.rs"]
-mod auth;
-#[path = "../src/domain/mod.rs"]
-mod domain;
-#[path = "../src/feed.rs"]
-mod feed;
-#[path = "../src/feed_token.rs"]
-mod feed_token;
-#[allow(
-    dead_code,
-    reason = "the public-route test does not use each shared Git API"
-)]
-#[path = "../src/git/mod.rs"]
-mod git;
-#[allow(
-    dead_code,
-    reason = "the public-route test uses the public Web server only"
-)]
-#[path = "../src/http/mod.rs"]
-mod http;
-#[allow(
-    dead_code,
-    reason = "the public-route test creates instance files directly"
-)]
-#[path = "../src/instance.rs"]
-mod instance;
-#[allow(dead_code, reason = "the public-route test does not mutate issues")]
-#[path = "../src/issue.rs"]
-mod issue;
-#[allow(dead_code, reason = "the public-route test does not run maintenance")]
-#[path = "../src/maintenance.rs"]
-mod maintenance;
-#[path = "../src/markdown.rs"]
-mod markdown;
-#[allow(dead_code, reason = "the public-route test uses anonymous policy only")]
-#[path = "../src/policy.rs"]
-mod policy;
-#[allow(
-    dead_code,
-    reason = "the public-route test does not mutate pull requests"
-)]
-#[path = "../src/pull_request.rs"]
-mod pull_request;
-#[path = "../src/rate_limit.rs"]
-mod rate_limit;
-#[allow(
-    dead_code,
-    reason = "the public route test does not create repositories through forms"
-)]
-#[path = "../src/repository.rs"]
-mod repository;
-#[path = "../src/search.rs"]
-mod search;
-#[allow(dead_code, reason = "the public-route test does not complete a login")]
-#[path = "../src/session.rs"]
-mod session;
-#[allow(
-    dead_code,
-    reason = "the public-route test does not use each store API"
-)]
-#[path = "../src/store/mod.rs"]
-mod store;
-#[path = "../src/telemetry.rs"]
-mod telemetry;
-#[allow(dead_code, reason = "the public-route test does not change watches")]
-#[path = "../src/watch.rs"]
-mod watch;
+use crate::{git, http, store};
 
 use std::collections::BTreeMap;
 use std::fs;
@@ -810,6 +734,17 @@
     assert!(anonymous.text().contains("<form class=\"filter-form\""));
     assert!(anonymous.text().contains("class=\"collection-action\""));
     assert!(!anonymous.text().contains("Create an issue</h2>"));
+    assert_eq!(
+        request(
+            server.address(),
+            "GET",
+            "/alice/example/issues?page=0",
+            &[],
+            &[],
+        )
+        .status,
+        400
+    );
 
     let cookie = format!("tit-session={token}; tit-csrf={csrf}");
     let headers = [
@@ -854,6 +789,17 @@
     assert!(!detail.text().contains("<script>"));
     assert!(detail.text().contains("Add a comment"));
     assert!(!detail.text().contains("Organize this issue"));
+    assert_eq!(
+        request(
+            server.address(),
+            "GET",
+            "/alice/example/issues/1?comments_page=0",
+            &[],
+            &[],
+        )
+        .status,
+        400
+    );
 
     let bad_csrf = form(&[("csrf", &"33".repeat(32)), ("state", "closed")]);
     assert_eq!(
@@ -923,6 +869,17 @@
     let anonymous_pull_requests =
         request(server.address(), "GET", "/alice/example/pulls", &[], &[]);
     assert_eq!(anonymous_pull_requests.status, 200);
+    assert_eq!(
+        request(
+            server.address(),
+            "GET",
+            "/alice/example/pulls?page=0",
+            &[],
+            &[],
+        )
+        .status,
+        400
+    );
     assert_repository_navigation(&anonymous_pull_requests, "alice", "example");
     assert!(
         anonymous_pull_requests

tests/pull_requests.rs

Mode 100644100644; object c02ebdd1b2b88818d416de28

@@ -1,31 +1,4 @@
-#[allow(
-    dead_code,
-    reason = "the pull-request test uses only identity validation"
-)]
-#[path = "../src/auth.rs"]
-mod auth;
-#[path = "../src/domain/mod.rs"]
-mod domain;
-#[allow(
-    dead_code,
-    reason = "the pull-request test uses part of the shared Git API"
-)]
-#[path = "../src/git/mod.rs"]
-mod git;
-#[allow(dead_code, reason = "the pull-request test does not run maintenance")]
-#[path = "../src/maintenance.rs"]
-mod maintenance;
-#[allow(
-    dead_code,
-    reason = "the pull-request test uses repository policy through Git"
-)]
-#[path = "../src/policy.rs"]
-mod policy;
-#[path = "../src/pull_request.rs"]
-mod pull_request;
-#[allow(dead_code, reason = "the pull-request test uses part of the store API")]
-#[path = "../src/store/mod.rs"]
-mod store;
+use crate::{git, pull_request, store};
 
 use std::fs;
 use std::os::unix::ffi::OsStringExt;

tests/repository_policy.rs

Mode 100644100644; object 99c8bd8bf9c65627b1e60d9e

@@ -1,11 +1,14 @@
-#[path = "../src/policy.rs"]
-mod policy;
-#[allow(dead_code, reason = "the policy test uses only repository storage")]
-#[path = "../src/store/mod.rs"]
-mod store;
+use crate::git::repository::GitRepository;
+use crate::repository::RepositoryService;
+use crate::{policy, store};
 
+use std::fs;
+
+use gix::hash::Kind;
 use policy::{PolicyError, RefChange, RepositoryOperation, RepositoryPolicy};
-use store::{AuditContext, NewRepository, RepositoryOrigin, Store, StoreError};
+use store::{
+    AuditContext, NewDefaultBranchIntent, NewRepository, RepositoryOrigin, Store, StoreError,
+};
 use tempfile::TempDir;
 
 #[test]
@@ -252,15 +255,19 @@
         )
         .expect("allow a writer topic branch");
     store
-        .update_repository_default_branch(
-            "owner",
-            "project",
-            "maintainer",
-            "refs/heads/trunk",
-            4,
-            "default-branch",
-        )
-        .expect("change the protected default branch");
+        .begin_repository_default_branch(&NewDefaultBranchIntent {
+            id: "00000000000000000000000000000004",
+            owner: "owner",
+            slug: "project",
+            actor: "maintainer",
+            previous_branch: "refs/heads/main",
+            default_branch: "refs/heads/trunk",
+            changed_at: 4,
+        })
+        .expect("begin the protected default-branch change");
+    store
+        .complete_repository_default_branch("00000000000000000000000000000004")
+        .expect("complete the protected default-branch change");
     policy
         .authorize_ref_change(
             "writer",
@@ -303,6 +310,72 @@
         policy.authorize_merge("writer", "owner", "project"),
         Err(PolicyError::Denied)
     ));
+}
+
+#[test]
+fn recovers_a_default_branch_change_after_git_moves_first() {
+    let directory = TempDir::new().expect("create a recovery fixture directory");
+    let database = directory.path().join("tit.sqlite3");
+    let repositories = directory.path().join("repositories");
+    let repository_id = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
+    let bare = repositories.join(format!("{repository_id}.git"));
+    fs::create_dir(&repositories).expect("create the repository directory");
+    let mut store = Store::open(&database).expect("create the recovery database");
+    store
+        .connection()
+        .execute(
+            "INSERT INTO account (id, username, is_administrator, state, created_at)
+             VALUES (1, 'owner', 0, 'active', 1)",
+            [],
+        )
+        .expect("create the repository owner");
+    store
+        .create_repository(&NewRepository {
+            id: repository_id,
+            owner: "owner",
+            slug: "project",
+            object_format: "sha1",
+            default_branch: "refs/heads/main",
+            created_at: 2,
+            origin: RepositoryOrigin::Created,
+            initial_references: &[],
+            actor: "admin-cli",
+            correlation_id: "test",
+        })
+        .expect("create the repository record");
+    GitRepository::create_bare(&bare, Kind::Sha1).expect("create the bare repository");
+    store
+        .begin_repository_default_branch(&NewDefaultBranchIntent {
+            id: "00000000000000000000000000000005",
+            owner: "owner",
+            slug: "project",
+            actor: "owner",
+            previous_branch: "refs/heads/main",
+            default_branch: "refs/heads/trunk",
+            changed_at: 3,
+        })
+        .expect("begin the default-branch change");
+    fs::write(bare.join("HEAD"), b"ref: refs/heads/trunk\n")
+        .expect("simulate the completed Git change");
+    drop(store);
+
+    RepositoryService::new(&database, &repositories)
+        .recover()
+        .expect("recover the default-branch change");
+
+    let store = Store::open(&database).expect("reopen the recovery database");
+    assert_eq!(
+        store
+            .repository_default_branch("owner", "project")
+            .expect("read the recovered default branch"),
+        "refs/heads/trunk"
+    );
+    assert!(
+        store
+            .incomplete_repository_default_branches()
+            .expect("read default-branch intents")
+            .is_empty()
+    );
 }
 
 fn operations() -> [RepositoryOperation; 4] {

tests/sqlite.rs

Mode 100644100644; object 23b75b03203dd98bbbef6772

@@ -1,6 +1,4 @@
-#[allow(dead_code, reason = "the storage test exercises selected store APIs")]
-#[path = "../src/store/mod.rs"]
-mod store;
+use crate::store;
 
 use std::process::{Child, Command};
 use std::sync::mpsc;
@@ -142,7 +140,12 @@
     ready_path: &std::path::Path,
 ) -> Child {
     Command::new(env::current_exe().expect("find the integration test executable"))
-        .args(["--exact", "crash_child", "--nocapture", "--test-threads=1"])
+        .args([
+            "--exact",
+            "sqlite_tests::crash_child",
+            "--nocapture",
+            "--test-threads=1",
+        ])
         .env("TIT_M1A_CHILD_MODE", mode)
         .env("TIT_M1A_DATABASE", database_path)
         .env("TIT_M1A_READY", ready_path)
@@ -235,7 +238,7 @@
     let directory = TempDir::new().expect("create a temporary directory");
     let store = Store::open(&database(&directory, "store.sqlite")).expect("open the store");
 
-    assert_eq!(store.schema_version().expect("read the schema version"), 23);
+    assert_eq!(store.schema_version().expect("read the schema version"), 24);
     assert_eq!(
         store
             .connection()
@@ -1631,7 +1634,7 @@
         create_fixture(&path, fixture);
 
         let store = Store::open(&path).expect("migrate the fixture");
-        assert_eq!(store.schema_version().expect("read the schema version"), 23);
+        assert_eq!(store.schema_version().expect("read the schema version"), 24);
         store.integrity_check().expect("check migrated integrity");
         let state: String = store
             .connection()
@@ -1692,6 +1695,10 @@
         .expect("remove the new table from the historical fixture");
     store
         .connection()
+        .execute("DROP TABLE repository_default_branch_intent", [])
+        .expect("remove the intent table from the historical fixture");
+    store
+        .connection()
         .pragma_update(None, "user_version", 22)
         .expect("set the historical schema version");
     drop(store);
@@ -1709,6 +1716,71 @@
             .repository_default_branch("alice", "project")
             .expect("read the migrated default branch"),
         "refs/heads/trunk"
+    );
+}
+
+#[test]
+fn migration_does_not_commit_when_a_repository_head_cannot_be_read() {
+    let directory = TempDir::new().expect("create a migration directory");
+    let path = database(&directory, "tit.sqlite3");
+    let mut store = Store::open(&path).expect("create the current database");
+    store
+        .connection()
+        .execute(
+            "INSERT INTO account
+             (id, username, is_administrator, state, created_at)
+             VALUES (1, 'alice', 1, 'active', 1)",
+            [],
+        )
+        .expect("create the migration account");
+    store
+        .create_repository(&NewRepository {
+            id: "00112233445566778899aabbccddeeff",
+            owner: "alice",
+            slug: "project",
+            object_format: "sha1",
+            default_branch: "refs/heads/main",
+            created_at: 2,
+            origin: RepositoryOrigin::Created,
+            initial_references: &[],
+            actor: "alice",
+            correlation_id: "migration-default-failure",
+        })
+        .expect("create the migration repository");
+    store
+        .connection()
+        .execute("DROP TABLE repository_default_branch", [])
+        .expect("remove the new table from the historical fixture");
+    store
+        .connection()
+        .execute("DROP TABLE repository_default_branch_intent", [])
+        .expect("remove the intent table from the historical fixture");
+    store
+        .connection()
+        .pragma_update(None, "user_version", 22)
+        .expect("set the historical schema version");
+    drop(store);
+    let head = directory
+        .path()
+        .join("repositories")
+        .join("00112233445566778899aabbccddeeff.git")
+        .join("HEAD");
+    fs::create_dir_all(&head).expect("create an unreadable historical HEAD");
+
+    let error = match Store::open(&path) {
+        Ok(_) => panic!("migration unexpectedly succeeded"),
+        Err(error) => error,
+    };
+    assert!(
+        matches!(&error, StoreError::MigrationFilesystem { .. }),
+        "{error}"
+    );
+    let unchanged = Store::open_unmigrated(&path).expect("open the rolled-back database");
+    assert_eq!(
+        unchanged
+            .schema_version()
+            .expect("read the rolled-back schema version"),
+        22
     );
 }
 
@@ -1758,7 +1830,7 @@
 
 #[test]
 fn recovers_complete_schema_versions_after_a_process_kill_during_migration() {
-    for (mode, expected_version) in [("migration-uncommitted", 1), ("migration-committed", 23)] {
+    for (mode, expected_version) in [("migration-uncommitted", 1), ("migration-committed", 24)] {
         let directory = TempDir::new().expect("create a temporary directory");
         let path = database(&directory, "fixture.sqlite");
         create_fixture(&path, V1_FIXTURE);

tests/sqlite_workload.rs

Mode 100644100644; object de4ed06f584737e646932592

@@ -1,9 +1,4 @@
-#[allow(
-    dead_code,
-    reason = "the M1A workload does not use Git operation intents"
-)]
-#[path = "../src/store/mod.rs"]
-mod store;
+use crate::store;
 
 use std::fs;
 use std::time::{Duration, Instant};

tests/ssh.rs

Mode 100644100644; object 552bf89103f405ea59378194

@@ -1,55 +1,4 @@
-#[allow(dead_code, reason = "the SSH test uses only the shared key boundary")]
-#[path = "../src/auth.rs"]
-mod auth;
-#[allow(dead_code, reason = "the SSH identity test does not use domain models")]
-#[path = "../src/domain/mod.rs"]
-mod domain;
-#[allow(
-    dead_code,
-    reason = "the SSH identity test does not use each Git service API"
-)]
-#[path = "../src/git/mod.rs"]
-mod git;
-#[allow(
-    dead_code,
-    reason = "the SSH identity test does not use issue commands"
-)]
-#[path = "../src/issue.rs"]
-mod issue;
-#[allow(dead_code, reason = "the SSH identity test does not run maintenance")]
-#[path = "../src/maintenance.rs"]
-mod maintenance;
-#[allow(
-    dead_code,
-    reason = "the SSH identity test does not use repository policy"
-)]
-#[path = "../src/policy.rs"]
-mod policy;
-#[allow(dead_code, reason = "the SSH identity test does not use pull requests")]
-#[path = "../src/pull_request.rs"]
-mod pull_request;
-#[path = "../src/rate_limit.rs"]
-mod rate_limit;
-#[allow(
-    dead_code,
-    reason = "the SSH identity test does not create repositories"
-)]
-#[path = "../src/repository.rs"]
-mod repository;
-#[allow(
-    dead_code,
-    reason = "the SSH identity test does not start a Git service"
-)]
-#[path = "../src/ssh.rs"]
-mod ssh;
-#[allow(
-    dead_code,
-    reason = "the SSH identity test does not use the intent store"
-)]
-#[path = "../src/store/mod.rs"]
-mod store;
-#[path = "../src/telemetry.rs"]
-mod telemetry;
+use crate::{auth, ssh};
 
 use std::fs;
 use std::net::{Ipv4Addr, SocketAddr};

tests/support/mod.rs

Mode 100644100644; object 5094ec042eead249db685df2

@@ -36,7 +36,11 @@
     }
 
     pub(crate) fn run(&self, arguments: &[&str]) -> Output {
-        Command::new(env!("CARGO_BIN_EXE_tit"))
+        let executable = option_env!("CARGO_BIN_EXE_tit")
+            .map(PathBuf::from)
+            .or_else(|| std::env::var_os("CARGO_BIN_EXE_tit").map(PathBuf::from))
+            .expect("find the tit test executable");
+        Command::new(executable)
             .args(arguments)
             .output()
             .expect("run tit")

tests/web_session.rs

Mode 100644100644; object 17eec5451fa63827ad13a9e3

@@ -1,23 +1,7 @@
+use crate::{account, auth, session, store};
+
 #[allow(dead_code, reason = "the Web session test uses one shared test helper")]
 mod support;
-
-#[allow(
-    dead_code,
-    reason = "the Web session test uses part of account management"
-)]
-#[path = "../src/account.rs"]
-mod account;
-#[allow(dead_code, reason = "the Web session test uses part of authentication")]
-#[path = "../src/auth.rs"]
-mod auth;
-#[path = "../src/session.rs"]
-mod session;
-#[allow(
-    dead_code,
-    reason = "the Web session test does not use each store operation"
-)]
-#[path = "../src/store/mod.rs"]
-mod store;
 
 use std::fs;
 use std::path::Path;

tests/web_shell.rs

Mode 100644100644; object 47eb9e815447cbc1fb38ada9

@@ -1,69 +1,4 @@
-#[allow(
-    dead_code,
-    reason = "the Web shell test does not use account mutations"
-)]
-#[path = "../src/account.rs"]
-mod account;
-#[allow(dead_code, reason = "the Web shell test uses only username validation")]
-#[path = "../src/auth.rs"]
-mod auth;
-#[allow(
-    dead_code,
-    reason = "the Web shell test uses only repository slug validation"
-)]
-#[path = "../src/domain/mod.rs"]
-mod domain;
-#[path = "../src/feed.rs"]
-mod feed;
-#[path = "../src/feed_token.rs"]
-mod feed_token;
-#[allow(dead_code, reason = "the shell test does not use each shared Git API")]
-#[path = "../src/git/mod.rs"]
-mod git;
-#[allow(
-    dead_code,
-    reason = "the shell test does not use public repository routes"
-)]
-#[path = "../src/http/mod.rs"]
-mod http;
-#[allow(
-    dead_code,
-    reason = "the shell test does not use instance administration"
-)]
-#[path = "../src/instance.rs"]
-mod instance;
-#[allow(dead_code, reason = "the Web shell test does not use issue workflows")]
-#[path = "../src/issue.rs"]
-mod issue;
-#[allow(dead_code, reason = "the Web shell test does not run maintenance")]
-#[path = "../src/maintenance.rs"]
-mod maintenance;
-#[path = "../src/markdown.rs"]
-mod markdown;
-#[allow(dead_code, reason = "the Web shell test has no repository catalog")]
-#[path = "../src/policy.rs"]
-mod policy;
-#[allow(dead_code, reason = "the Web shell test does not use pull requests")]
-#[path = "../src/pull_request.rs"]
-mod pull_request;
-#[path = "../src/rate_limit.rs"]
-mod rate_limit;
-#[allow(dead_code, reason = "the Web shell test does not create repositories")]
-#[path = "../src/repository.rs"]
-mod repository;
-#[path = "../src/search.rs"]
-mod search;
-#[allow(dead_code, reason = "the Web shell test does not complete a login")]
-#[path = "../src/session.rs"]
-mod session;
-#[allow(dead_code, reason = "the shell test does not use repository storage")]
-#[path = "../src/store/mod.rs"]
-mod store;
-#[path = "../src/telemetry.rs"]
-mod telemetry;
-#[allow(dead_code, reason = "the Web shell test does not change watches")]
-#[path = "../src/watch.rs"]
-mod watch;
+use crate::http;
 
 use std::collections::BTreeMap;
 use std::io::{Read, Write};