michal/tit
Browse tree · Show commit · Download archive
Blob: src/pull_request.rs
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};
use crate::domain::repository::{RepositoryNameError, validate_slug};
use crate::git::read::{
Comparison, ReadCancellation, ReadError, ReadLimits, RepositoryReadService,
};
use crate::git::repository::{GitRepository, GitRepositoryError};
use crate::maintenance::MaintenanceGate;
use crate::policy::{PolicyError, RepositoryPolicy};
use crate::store::{
GitOperationIntent, NewPullRequestMerge, NewPullRequestRefIntent, NewPullRequestReview,
PullRequestDetail, PullRequestRecord, PullRequestRefIntentRecord, PullRequestRevisionRecord,
RecordPage, RepositoryRecord, Store, StoreError,
};
pub(crate) const MAX_TITLE_BYTES: usize = 200;
pub(crate) const MAX_BODY_BYTES: usize = 256 * 1024;
pub(crate) const MAX_REVIEW_BODY_BYTES: usize = 256 * 1024;
pub(crate) const PAGE_SIZE: usize = 50;
#[derive(Clone)]
pub(crate) struct PullRequestService {
database: PathBuf,
repositories: PathBuf,
operations: Arc<Mutex<()>>,
maintenance: MaintenanceGate,
}
pub(crate) struct PullRequestComparison {
pub(crate) detail: PullRequestDetail,
pub(crate) revision: PullRequestRevisionRecord,
pub(crate) comparison: Comparison,
}
impl PullRequestService {
pub(crate) fn new(database: &Path, repositories: &Path) -> Self {
Self::new_with_gate(database, repositories, MaintenanceGate::default())
}
pub(crate) fn new_with_gate(
database: &Path,
repositories: &Path,
maintenance: MaintenanceGate,
) -> Self {
Self {
database: database.to_owned(),
repositories: repositories.to_owned(),
operations: Arc::new(Mutex::new(())),
maintenance,
}
}
#[allow(
clippy::too_many_arguments,
reason = "opening a pull request requires its repository, content, and two refs"
)]
pub(crate) fn open(
&self,
owner: &str,
repository: &str,
actor: &str,
title: &str,
body: &str,
base_ref: &str,
head_ref: &str,
) -> Result<PullRequestRecord, PullRequestError> {
validate_context(owner, repository, actor)?;
validate_content(title, body)?;
validate_branch(base_ref)?;
validate_branch(head_ref)?;
let _maintenance = self.maintenance.mutation();
let _operation = self
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.recover_inner()?;
let authorization = Store::open(&self.database)?.repository_authorization(
owner,
repository,
Some(actor),
)?;
let path = self.repository_path(&authorization.repository.id)?;
let git = GitRepository::open(&path)?;
let base = git.resolve_branch(base_ref)?;
let head = git.resolve_branch(head_ref)?;
let intent_id = random_id()?;
let pull_request_id = random_id()?;
let created_at = timestamp()?;
let mut store = Store::open(&self.database)?;
let intent = store.begin_pull_request_open(&NewPullRequestRefIntent {
id: &intent_id,
pull_request_id: &pull_request_id,
owner,
repository,
actor,
title,
body,
base_ref,
head_ref,
base_object_id: &base.to_string(),
head_object_id: &head.to_string(),
created_at,
})?;
crash_point("intent");
self.apply_intent(&mut store, &git, &intent)?;
crash_point("completed");
Ok(store
.pull_request(owner, repository, intent.pull_request_number, Some(actor))?
.pull_request)
}
pub(crate) fn revise(
&self,
owner: &str,
repository: &str,
number: i64,
actor: &str,
) -> Result<PullRequestRecord, PullRequestError> {
validate_context(owner, repository, actor)?;
if number < 1 {
return Err(PullRequestError::Number);
}
let _maintenance = self.maintenance.mutation();
let _operation = self
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.recover_inner()?;
let current = Store::open(&self.database)?
.pull_request(owner, repository, number, Some(actor))?
.pull_request;
if current.state != "open" {
return Err(StoreError::PullRequestState.into());
}
let authorization = Store::open(&self.database)?.repository_authorization(
owner,
repository,
Some(actor),
)?;
let path = self.repository_path(&authorization.repository.id)?;
let git = GitRepository::open(&path)?;
let base = git.resolve_branch(¤t.base_ref)?;
let head = git.resolve_branch(¤t.head_ref)?;
if head.to_string() == current.head_object_id && base.to_string() == current.base_object_id
{
return Err(PullRequestError::Unchanged);
}
let intent_id = random_id()?;
let created_at = timestamp()?;
let mut store = Store::open(&self.database)?;
let intent = store.begin_pull_request_revision(
number,
&NewPullRequestRefIntent {
id: &intent_id,
pull_request_id: ¤t.id,
owner,
repository,
actor,
title: ¤t.title,
body: ¤t.body,
base_ref: ¤t.base_ref,
head_ref: ¤t.head_ref,
base_object_id: &base.to_string(),
head_object_id: &head.to_string(),
created_at,
},
)?;
crash_point("intent");
self.apply_intent(&mut store, &git, &intent)?;
crash_point("completed");
Ok(store
.pull_request(owner, repository, number, Some(actor))?
.pull_request)
}
#[allow(
dead_code,
reason = "the pull-request integration test imports this module without the Web list route"
)]
pub(crate) fn list_page(
&self,
owner: &str,
repository: &str,
actor: Option<&str>,
state: &str,
page: usize,
) -> Result<(RepositoryRecord, RecordPage<PullRequestRecord>, bool), PullRequestError> {
validate_username(owner)?;
validate_slug(repository)?;
if let Some(actor) = actor {
validate_username(actor)?;
}
if !matches!(state, "all" | "open" | "closed" | "merged") || page == 0 || page > 10_000 {
return Err(PullRequestError::State);
}
let _maintenance = self.maintenance.mutation();
let _operation = self
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.recover_inner()?;
let result = Store::open(&self.database)?
.pull_request_page(owner, repository, actor, state, page, PAGE_SIZE)
.map_err(PullRequestError::from)?;
if page > 1 && result.1.items.is_empty() {
return Err(PullRequestError::State);
}
Ok(result)
}
#[allow(
dead_code,
reason = "the pull-request integration test imports this module without the Web edit route"
)]
pub(crate) fn edit(
&self,
owner: &str,
repository: &str,
number: i64,
actor: &str,
title: &str,
body: &str,
) -> Result<(), PullRequestError> {
validate_context(owner, repository, actor)?;
if number < 1 {
return Err(PullRequestError::Number);
}
validate_content(title, body)?;
Store::open(&self.database)?
.edit_pull_request(owner, repository, number, actor, title, body, timestamp()?)
.map_err(Into::into)
}
#[allow(
dead_code,
reason = "the pull-request integration test imports this module without the Web state route"
)]
pub(crate) fn set_state(
&self,
owner: &str,
repository: &str,
number: i64,
actor: &str,
state: &str,
) -> Result<(), PullRequestError> {
validate_context(owner, repository, actor)?;
if number < 1 {
return Err(PullRequestError::Number);
}
if !matches!(state, "open" | "closed") {
return Err(PullRequestError::State);
}
Store::open(&self.database)?
.set_pull_request_state(owner, repository, number, actor, state, timestamp()?)
.map_err(Into::into)
}
#[allow(
dead_code,
reason = "integration tests and later non-Web callers read pull requests without comparison"
)]
pub(crate) fn get(
&self,
owner: &str,
repository: &str,
number: i64,
actor: Option<&str>,
) -> Result<PullRequestDetail, PullRequestError> {
validate_username(owner)?;
validate_slug(repository)?;
if let Some(actor) = actor {
validate_username(actor)?;
}
if number < 1 {
return Err(PullRequestError::Number);
}
let _maintenance = self.maintenance.mutation();
let _operation = self
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.recover_inner()?;
Store::open(&self.database)?
.pull_request(owner, repository, number, actor)
.map_err(Into::into)
}
#[allow(
dead_code,
reason = "the binary uses the paged Web comparison and integration tests use the full comparison"
)]
pub(crate) fn compare(
&self,
owner: &str,
repository: &str,
number: i64,
revision: Option<i64>,
actor: Option<&str>,
) -> Result<PullRequestComparison, PullRequestError> {
self.compare_with_activity(owner, repository, number, revision, actor, None)
}
#[allow(
clippy::too_many_arguments,
dead_code,
reason = "the pull-request integration test imports this module without the Web detail route"
)]
pub(crate) fn compare_page(
&self,
owner: &str,
repository: &str,
number: i64,
revision: Option<i64>,
actor: Option<&str>,
reviews_page: usize,
timeline_page: usize,
) -> Result<PullRequestComparison, PullRequestError> {
self.compare_with_activity(
owner,
repository,
number,
revision,
actor,
Some((reviews_page, timeline_page)),
)
}
#[allow(
clippy::too_many_arguments,
reason = "the paged comparison includes repository identity and independent activity pages"
)]
fn compare_with_activity(
&self,
owner: &str,
repository: &str,
number: i64,
revision: Option<i64>,
actor: Option<&str>,
activity: Option<(usize, usize)>,
) -> Result<PullRequestComparison, PullRequestError> {
validate_username(owner)?;
validate_slug(repository)?;
if let Some(actor) = actor {
validate_username(actor)?;
}
if number < 1
|| revision.is_some_and(|number| number < 1)
|| activity.is_some_and(|(reviews, timeline)| {
reviews == 0 || reviews > 10_000 || timeline == 0 || timeline > 10_000
})
{
return Err(PullRequestError::Number);
}
let _maintenance = self.maintenance.mutation();
let _operation = self
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.recover_inner()?;
let store = Store::open(&self.database)?;
let detail = match activity {
Some((reviews_page, timeline_page)) => store.pull_request_detail_page(
owner,
repository,
number,
actor,
reviews_page,
timeline_page,
PAGE_SIZE,
)?,
None => store.pull_request(owner, repository, number, actor)?,
};
if activity.is_some_and(|(reviews_page, timeline_page)| {
(reviews_page > 1 && detail.reviews.is_empty())
|| (timeline_page > 1 && detail.timeline.is_empty())
}) {
return Err(PullRequestError::Number);
}
let revision = match revision {
Some(number) => detail
.revisions
.iter()
.find(|revision| revision.number == number),
None => detail.revisions.last(),
}
.cloned()
.ok_or(PullRequestError::Revision)?;
let path = self.repository_path(&detail.repository.id)?;
let reader = RepositoryReadService::open(&path, ReadLimits::default())?;
let comparison = reader.comparison(
parse_id(&revision.base_object_id)?,
parse_id(&revision.head_object_id)?,
&ReadCancellation::default(),
)?;
Ok(PullRequestComparison {
detail,
revision,
comparison,
})
}
#[allow(
clippy::too_many_arguments,
reason = "a review action includes its repository, revision, content, and optional line anchor"
)]
pub(crate) fn review(
&self,
owner: &str,
repository: &str,
number: i64,
revision_number: i64,
actor: &str,
kind: &str,
body: &str,
path: Option<&[u8]>,
side: Option<&str>,
line: Option<i64>,
) -> Result<String, PullRequestError> {
validate_context(owner, repository, actor)?;
if number < 1 || revision_number < 1 {
return Err(PullRequestError::Number);
}
validate_review(kind, body, path, side, line)?;
let _maintenance = self.maintenance.mutation();
let _operation = self
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.recover_inner()?;
let detail =
Store::open(&self.database)?.pull_request(owner, repository, number, Some(actor))?;
if detail.pull_request.state != "open" {
return Err(StoreError::PullRequestState.into());
}
let revision = detail
.revisions
.iter()
.find(|revision| revision.number == revision_number)
.ok_or(PullRequestError::Revision)?;
let commit_object_id = if kind == "line-comment" {
let path = path.ok_or(PullRequestError::ReviewAnchor)?;
let side = side.ok_or(PullRequestError::ReviewAnchor)?;
let line = line.ok_or(PullRequestError::ReviewAnchor)?;
let path_to_repository = self.repository_path(&detail.repository.id)?;
let reader = RepositoryReadService::open(&path_to_repository, ReadLimits::default())?;
let base = parse_id(&revision.base_object_id)?;
let head = parse_id(&revision.head_object_id)?;
let comparison = reader.comparison(base, head, &ReadCancellation::default())?;
let file = comparison
.files
.iter()
.find(|file| file.path == path)
.filter(|file| !file.binary)
.ok_or(PullRequestError::ReviewAnchor)?;
let (commit, exists) = match side {
"base" => (base, file.old_id.is_some()),
"head" => (head, file.new_id.is_some()),
_ => return Err(PullRequestError::ReviewAnchor),
};
if !exists {
return Err(PullRequestError::ReviewAnchor);
}
let blob = reader.blob(commit, path, &ReadCancellation::default())?;
let line_count = if blob.data.is_empty() {
0
} else {
blob.data.split(|byte| *byte == b'\n').count()
- usize::from(blob.data.ends_with(b"\n"))
};
let line = usize::try_from(line).map_err(|_| PullRequestError::ReviewAnchor)?;
if line == 0 || line > line_count {
return Err(PullRequestError::ReviewAnchor);
}
Some(commit.to_string())
} else {
None
};
Store::open(&self.database)?
.create_pull_request_review(&NewPullRequestReview {
owner,
repository,
number,
revision: revision_number,
actor,
kind,
body,
commit_object_id: commit_object_id.as_deref(),
path,
side,
line,
created_at: timestamp()?,
})
.map_err(Into::into)
}
pub(crate) fn merge(
&self,
owner: &str,
repository: &str,
number: i64,
actor: &str,
method: &str,
) -> Result<PullRequestRecord, PullRequestError> {
validate_context(owner, repository, actor)?;
if number < 1 || !matches!(method, "fast-forward" | "merge-commit") {
return Err(PullRequestError::MergeMethod);
}
let _maintenance = self.maintenance.mutation();
let _operation = self
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.recover_inner()?;
let detail =
Store::open(&self.database)?.pull_request(owner, repository, number, Some(actor))?;
if detail.pull_request.state != "open" {
return Err(StoreError::PullRequestState.into());
}
RepositoryPolicy::new(&self.database)
.authorize_merge(actor, owner, repository)
.map_err(|error| match error {
PolicyError::Denied => PullRequestError::Store(StoreError::PullRequestDenied),
other => PullRequestError::Policy(other),
})?;
let revision = detail.revisions.last().ok_or(PullRequestError::Revision)?;
let path = self.repository_path(&detail.repository.id)?;
let git = GitRepository::open(&path)?;
let base = git.resolve_branch(&detail.pull_request.base_ref)?;
let head = git.resolve_branch(&detail.pull_request.head_ref)?;
if base.to_string() != revision.base_object_id
|| head.to_string() != revision.head_object_id
{
return Err(PullRequestError::StaleRevision);
}
let reader = RepositoryReadService::open(&path, ReadLimits::default())?;
let comparison = reader.comparison(base, head, &ReadCancellation::default())?;
let intent_id = random_id()?;
let created_at = timestamp()?;
let merge_message = format!(
"Merge pull request #{number} from {}\n\n{}",
detail.pull_request.head_ref, detail.pull_request.title
);
let new_target = match (method, comparison.mergeability) {
("fast-forward", crate::git::read::Mergeability::FastForward) => head,
("merge-commit", crate::git::read::Mergeability::Clean) => {
git.prepare_merge_commit(base, head, actor, created_at, &merge_message)?
}
_ => return Err(PullRequestError::Mergeability),
};
let repository_text = path.to_str().ok_or(PullRequestError::RepositoryPath)?;
let quarantine = path.join("objects").join("tit-quarantine").join(&intent_id);
let quarantine_text = quarantine
.to_str()
.ok_or(PullRequestError::RepositoryPath)?;
let initial = serialize_ref(base, &detail.pull_request.base_ref);
let proposed = serialize_ref(new_target, &detail.pull_request.base_ref);
let base_text = base.to_string();
let head_text = head.to_string();
let new_target_text = new_target.to_string();
let mut store = Store::open(&self.database)?;
store.begin_pull_request_merge(
&NewPullRequestMerge {
owner,
repository,
number,
revision: revision.number,
actor,
method,
base_ref: &detail.pull_request.base_ref,
old_target: &base_text,
head_target: &head_text,
new_target: &new_target_text,
created_at,
},
&GitOperationIntent {
id: &intent_id,
repository_path: repository_text,
actor,
initial_refs: &initial,
proposed_refs: &proposed,
event_payload: &proposed,
quarantine_path: quarantine_text,
created_at,
},
)?;
crash_point("merge-intent");
if method == "merge-commit" {
match git.write_merge_commit(base, head, actor, created_at, &merge_message) {
Ok(written) if written == new_target => {}
Ok(_) => {
store.abandon_git_intent(&intent_id)?;
return Err(PullRequestError::NonDeterministicMerge);
}
Err(error) => {
store.abandon_git_intent(&intent_id)?;
return Err(error.into());
}
}
}
store.mark_git_objects_promoted(&intent_id, None)?;
crash_point("merge-objects");
if let Err(error) = git.update_reference_with_log(
&detail.pull_request.base_ref,
Some(base),
new_target,
"merge pull request",
) {
match git.reference_target(&detail.pull_request.base_ref)? {
Some(current) if current == new_target => store.complete_git_intent(&intent_id)?,
_ => store.abandon_git_intent(&intent_id)?,
}
return Err(error.into());
}
crash_point("merge-ref");
store.complete_git_intent(&intent_id)?;
crash_point("merge-completed");
Ok(store
.pull_request(owner, repository, number, Some(actor))?
.pull_request)
}
#[allow(
dead_code,
reason = "some integration tests compile the service without the Web list route"
)]
pub(crate) fn list(
&self,
owner: &str,
repository: &str,
actor: Option<&str>,
) -> Result<(crate::store::RepositoryRecord, Vec<PullRequestRecord>, bool), PullRequestError>
{
validate_username(owner)?;
validate_slug(repository)?;
if let Some(actor) = actor {
validate_username(actor)?;
}
let _maintenance = self.maintenance.mutation();
let _operation = self
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.recover_inner()?;
Store::open(&self.database)?
.pull_requests(owner, repository, actor)
.map_err(Into::into)
}
pub(crate) fn recover(&self) -> Result<(), PullRequestError> {
let _maintenance = self.maintenance.mutation();
let _operation = self
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
crate::git::receive_pack::recover_incomplete_pushes(&self.database)?;
self.recover_inner()
}
fn recover_inner(&self) -> Result<(), PullRequestError> {
let mut store = Store::open(&self.database)?;
for intent in store.incomplete_pull_request_ref_intents()? {
let path = self.repository_path(&intent.repository_id)?;
let git = GitRepository::open(&path)?;
self.recover_intent(&mut store, &git, &intent)?;
}
Ok(())
}
fn apply_intent(
&self,
store: &mut Store,
git: &GitRepository,
intent: &PullRequestRefIntentRecord,
) -> Result<(), PullRequestError> {
let name = pull_request_ref(intent.pull_request_number);
let old = parse_optional_id(intent.old_head_object_id.as_deref())?;
let head = parse_id(&intent.head_object_id)?;
if let Err(error) = git.update_reference(&name, old, head) {
let current = git.reference_target(&name)?;
if current == Some(head) {
store.complete_pull_request_ref_intent(&intent.id)?;
return Ok(());
}
if current == old {
store.abandon_pull_request_ref_intent(&intent.id)?;
return Err(error.into());
}
return Err(PullRequestError::MixedRecovery(intent.id.clone()));
}
crash_point("ref");
store.complete_pull_request_ref_intent(&intent.id)?;
Ok(())
}
fn recover_intent(
&self,
store: &mut Store,
git: &GitRepository,
intent: &PullRequestRefIntentRecord,
) -> Result<(), PullRequestError> {
let name = pull_request_ref(intent.pull_request_number);
let old = parse_optional_id(intent.old_head_object_id.as_deref())?;
let head = parse_id(&intent.head_object_id)?;
match git.reference_target(&name)? {
Some(current) if current == head => {
store.complete_pull_request_ref_intent(&intent.id)?;
}
current if current == old => {
git.update_reference(&name, old, head)?;
store.complete_pull_request_ref_intent(&intent.id)?;
}
_ => return Err(PullRequestError::MixedRecovery(intent.id.clone())),
}
Ok(())
}
fn repository_path(&self, repository_id: &str) -> Result<PathBuf, PullRequestError> {
let path = fs::canonicalize(self.repositories.join(format!("{repository_id}.git")))?;
if path.parent() != Some(self.repositories.as_path()) {
return Err(PullRequestError::RepositoryPath);
}
Ok(path)
}
}
fn validate_context(owner: &str, repository: &str, actor: &str) -> Result<(), PullRequestError> {
validate_username(owner)?;
validate_slug(repository)?;
validate_username(actor)?;
Ok(())
}
fn validate_content(title: &str, body: &str) -> Result<(), PullRequestError> {
if title.is_empty() || title.len() > MAX_TITLE_BYTES || title.contains(['\r', '\n']) {
return Err(PullRequestError::Title);
}
if body.len() > MAX_BODY_BYTES {
return Err(PullRequestError::Body);
}
Ok(())
}
fn validate_branch(name: &str) -> Result<(), PullRequestError> {
if !name.starts_with("refs/heads/") || name.len() > 1024 || !name.is_ascii() {
return Err(PullRequestError::Branch);
}
Ok(())
}
fn validate_review(
kind: &str,
body: &str,
path: Option<&[u8]>,
side: Option<&str>,
line: Option<i64>,
) -> Result<(), PullRequestError> {
if body.len() > MAX_REVIEW_BODY_BYTES
|| body
.chars()
.any(|character| character.is_control() && !matches!(character, '\n' | '\r' | '\t'))
{
return Err(PullRequestError::ReviewBody);
}
match kind {
"comment" | "changes-requested" if !body.trim().is_empty() => {
if path.is_none() && side.is_none() && line.is_none() {
Ok(())
} else {
Err(PullRequestError::ReviewAnchor)
}
}
"approved" if path.is_none() && side.is_none() && line.is_none() => Ok(()),
"line-comment" if !body.trim().is_empty() => {
let path = path.ok_or(PullRequestError::ReviewAnchor)?;
if path.is_empty()
|| path.len() > ReadLimits::default().max_path_bytes
|| path.contains(&0)
|| !matches!(side, Some("base" | "head"))
|| line.is_none_or(|line| line < 1)
{
return Err(PullRequestError::ReviewAnchor);
}
Ok(())
}
"comment" | "line-comment" | "approved" | "changes-requested" => {
Err(PullRequestError::ReviewBody)
}
_ => Err(PullRequestError::ReviewKind),
}
}
fn pull_request_ref(number: i64) -> String {
format!("refs/pull/{number}/head")
}
fn serialize_ref(id: ObjectId, name: &str) -> Vec<u8> {
format!("{id} {name}\n").into_bytes()
}
fn parse_id(value: &str) -> Result<ObjectId, PullRequestError> {
ObjectId::from_hex(value.as_bytes()).map_err(|_| PullRequestError::StoredObjectId)
}
fn parse_optional_id(value: Option<&str>) -> Result<Option<ObjectId>, PullRequestError> {
value.map(parse_id).transpose()
}
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())
}
fn timestamp() -> Result<i64, PullRequestError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| PullRequestError::Clock)?
.as_secs()
.try_into()
.map_err(|_| PullRequestError::Clock)
}
#[cfg(test)]
fn crash_point(point: &str) {
if std::env::var("TIT_M5_1_CRASH_AFTER").as_deref() != Ok(point) {
return;
}
let ready = std::env::var_os("TIT_M5_1_READY").expect("read the M5.1 ready path");
fs::write(ready, point.as_bytes()).expect("write the M5.1 ready file");
loop {
std::thread::park();
}
}
#[cfg(not(test))]
fn crash_point(_point: &str) {}
#[derive(Debug, Error)]
pub(crate) enum PullRequestError {
#[error(transparent)]
Auth(#[from] AuthError),
#[error(transparent)]
RepositoryName(#[from] RepositoryNameError),
#[error(transparent)]
Store(#[from] StoreError),
#[error(transparent)]
Git(#[from] GitRepositoryError),
#[error("pull-request title is not valid")]
Title,
#[error("pull-request body is too large")]
Body,
#[error("pull-request branch name is not valid")]
Branch,
#[error("pull-request number is not valid")]
Number,
#[error("pull-request list state is not valid")]
#[allow(
dead_code,
reason = "the pull-request integration test imports this module without Web list state"
)]
State,
#[error("pull-request revision does not exist")]
Revision,
#[error("pull-request review kind is not valid")]
ReviewKind,
#[error("pull-request review body is not valid")]
ReviewBody,
#[error("pull-request review line anchor is not valid")]
ReviewAnchor,
#[error("pull-request merge method is not valid")]
MergeMethod,
#[error("pull-request revision is not the current branch state")]
StaleRevision,
#[error("pull request cannot use the requested merge method")]
Mergeability,
#[error("pull-request merge did not produce a deterministic commit")]
NonDeterministicMerge,
#[error("pull-request refs have not changed")]
Unchanged,
#[error("stored pull-request object ID is not valid")]
StoredObjectId,
#[error("pull-request ref intent {0} has mixed Git and metadata state")]
MixedRecovery(String),
#[error("pull-request repository path is not canonical")]
RepositoryPath,
#[error("cannot access a pull-request repository: {0}")]
Io(#[from] std::io::Error),
#[error(transparent)]
Read(#[from] ReadError),
#[error(transparent)]
Policy(#[from] PolicyError),
#[error(transparent)]
ReceivePack(#[from] crate::git::receive_pack::ReceivePackError),
#[error("cannot create a random pull-request ID")]
Random,
#[error("the system clock is before the Unix epoch")]
Clock,
}