#[cfg(feature = "read")]
use crate::http_backend;
use crate::{Request, Response, route::Version};
use git_zero_core::{Error, Repository, Result, io::Write, repository::uses_only_head};
#[cfg(feature = "read")]
use git_zero_core::{ObjectId, SelectionOptions, ValidatedRepository, pack};
pub(crate) mod advertisement;
pub(crate) mod extensions;
pub(crate) mod negotiation;
pub(crate) mod policy;
#[cfg(feature = "read")]
mod storage;
#[cfg(feature = "read")]
pub use negotiation::{Acknowledgments, ShallowUpdates};
pub use negotiation::{PackOutput, write_upload_error};
pub(crate) use negotiation::{UploadRequest, capabilities, id};
pub use policy::{WantOptions, WantPolicy};
#[cfg(feature = "read")]
pub use storage::{RevisionBuffers, RevisionStorage, SelectionBuffers, SelectionStorage};
#[cfg(feature = "read")]
pub fn serve_with_selection<R: Repository + ?Sized>(
repository: &ValidatedRepository<'_, '_, R>,
request: Request<'_>,
storage: &mut SelectionStorage<'_>,
output: &mut impl Write,
) -> Result<Response> {
serve_with_selection_policy(repository, request, storage, WantOptions::STRICT, output)
}
#[cfg(feature = "read")]
pub fn serve_with_selection_policy<R: Repository + ?Sized>(
repository: &ValidatedRepository<'_, '_, R>,
request: Request<'_>,
storage: &mut SelectionStorage<'_>,
options: WantOptions,
output: &mut impl Write,
) -> Result<Response> {
serve_with_selected_pack_policy(
repository,
request,
storage,
options,
output,
|indices, read, packed| {
pack::write_selected_with_scratch(repository, indices, read, packed).map(|_| ())
},
)
}
#[cfg(feature = "read")]
pub fn serve_with_selected_pack<R: Repository + ?Sized, W: Write>(
repository: &ValidatedRepository<'_, '_, R>,
request: Request<'_>,
storage: &mut SelectionStorage<'_>,
output: &mut W,
write_selected: impl FnOnce(&[usize], &mut [u8], &mut PackOutput<'_, W>) -> Result<()>,
) -> Result<Response> {
serve_with_selected_pack_policy(
repository,
request,
storage,
WantOptions::STRICT,
output,
write_selected,
)
}
#[cfg(feature = "read")]
pub fn serve_with_selected_pack_policy<R: Repository + ?Sized, W: Write>(
repository: &ValidatedRepository<'_, '_, R>,
request: Request<'_>,
storage: &mut SelectionStorage<'_>,
options: WantOptions,
output: &mut W,
write_selected: impl FnOnce(&[usize], &mut [u8], &mut PackOutput<'_, W>) -> Result<()>,
) -> Result<Response> {
serve_selected_request(
repository,
request,
storage.reborrow(),
options,
None,
output,
write_selected,
)
}
#[cfg(feature = "read")]
pub fn serve_with_revisions<R: Repository + ?Sized>(
repository: &ValidatedRepository<'_, '_, R>,
request: Request<'_>,
storage: &mut RevisionStorage<'_>,
output: &mut impl Write,
) -> Result<Response> {
let storage = storage.reborrow();
serve_selected_request(
repository,
request,
storage.selection,
WantOptions::STRICT,
Some((storage.state, storage.limits)),
output,
|indices, read, packed| {
pack::write_selected_with_scratch(repository, indices, read, packed).map(|_| ())
},
)
}
#[cfg(feature = "read")]
fn serve_selected_request<R: Repository + ?Sized, W: Write>(
repository: &ValidatedRepository<'_, '_, R>,
request: Request<'_>,
storage: SelectionStorage<'_>,
options: WantOptions,
revision: Option<(&mut [u8], git_zero_core::RevisionLimits)>,
output: &mut W,
write_selected: impl FnOnce(&[usize], &mut [u8], &mut PackOutput<'_, W>) -> Result<()>,
) -> Result<Response> {
let version = match prepare_route(repository, request, options, revision.is_some(), output)? {
http_backend::Plan::Buffered(response) => return Ok(response),
http_backend::Plan::Loose(index) => {
return http_backend::write_loose(repository, index, storage.read, output);
}
http_backend::Plan::Upload(version) => version,
};
let selected = prepare_fetch(
repository, request, version, storage, options, revision, output,
)?;
if let Some(indices) = selected.indices {
let mut packed = selected.upload.output(output);
write_selected(indices, selected.read, &mut packed)?;
packed.finish()?;
}
Ok(response())
}
#[cfg(feature = "read")]
pub(crate) fn prepare_route(
repository: &impl Repository,
request: Request<'_>,
options: WantOptions,
revisions: bool,
output: &mut impl Write,
) -> Result<http_backend::Plan> {
http_backend::prepare_with_refs(repository, request, output, |route, output| {
if revisions {
advertisement::serve_revision_info_refs(repository, route, output)
} else {
policy::info_refs(repository, route, output, options)
}
})
}
#[cfg(feature = "read")]
pub(crate) struct SelectedFetch<'storage, 'body> {
pub(crate) upload: UploadRequest<'body>,
pub(crate) indices: Option<&'storage [usize]>,
read: &'storage mut [u8],
}
#[cfg(feature = "read")]
fn select_fetch<'indices, R: Repository + ?Sized>(
repository: &ValidatedRepository<'_, '_, R>,
upload: &UploadRequest<'_>,
wants: &[ObjectId],
work: &mut [Option<git_zero_core::ValidationMetadata>],
indices: &'indices mut [usize],
scratch: (
&mut [u8],
&mut [u8],
Option<&mut git_zero_core::RevisionState<'_>>,
),
options: SelectionOptions,
) -> Result<&'indices [usize]> {
if upload.extensions.revision() {
repository.select_revision_fetch(
wants,
(
upload.common(repository),
upload.shallow(),
upload.exclusion_ids(repository),
),
work,
indices,
(scratch.0, scratch.1, scratch.2.ok_or(Error::Unsupported)?),
(
options,
upload.extensions.options,
git_zero_core::RevisionOptions {
since: upload.extensions.since,
},
),
)
} else {
repository.select_bounded_fetch(
wants,
(upload.common(repository), upload.shallow()),
work,
indices,
(scratch.0, scratch.1),
(options, upload.extensions.options),
)
}
}
#[cfg(feature = "read")]
pub(crate) fn prepare_fetch<'storage, 'body, R: Repository + ?Sized>(
repository: &ValidatedRepository<'_, '_, R>,
request: Request<'body>,
version: Version,
storage: SelectionStorage<'storage>,
options: WantOptions,
revision: Option<(&mut [u8], git_zero_core::RevisionLimits)>,
output: &mut impl Write,
) -> Result<SelectedFetch<'storage, 'body>> {
let (upload, count) = UploadRequest::parse_selected(
repository,
request.body,
version,
storage.wants,
options,
revision.is_some(),
)?;
let mut revision =
revision.map(|(state, limits)| git_zero_core::RevisionState::new(state, limits));
if upload.extensions.exclusions > revision.as_ref().map_or(0, |state| state.max_exclusions()) {
return Err(Error::LimitExceeded);
}
let boundary_probe = version != Version::V2 && !upload.done && upload.extensions.history();
let select_boundaries = boundary_probe
&& (upload.extensions.revision()
|| !upload.extensions.options.deepen_relative
|| upload.shallow().next().is_some());
let indices = if upload.done || select_boundaries {
let options = SelectionOptions {
graph_read_limit: storage.graph_read_limit,
include_tags: upload.include_tags(),
};
Some(select_fetch(
repository,
&upload,
&storage.wants[..count],
storage.objects,
storage.indices,
(storage.read, storage.parse, revision.as_mut()),
options,
)?)
} else {
upload.validate_shallow(repository)?;
if upload.extensions.revision() {
for id in upload.exclusion_ids(repository) {
id?;
}
}
None
};
if indices.is_some() {
if upload.extensions.revision() {
upload.revision_info(
repository.object_format(),
revision
.as_ref()
.ok_or(Error::Unsupported)?
.changes(storage.objects),
output,
)?;
} else {
upload.shallow_info(
repository.object_format(),
&storage.objects[..repository.object_count()],
output,
)?;
}
} else if boundary_probe {
output.write_all(b"0000")?;
}
upload.acknowledge(repository, output)?;
Ok(SelectedFetch {
indices: indices.filter(|_| upload.done),
upload,
read: storage.read,
})
}
pub(crate) fn response() -> Response {
Response {
status: 200,
content_type: "application/x-git-upload-pack-result",
}
}
#[cfg(feature = "read")]
pub(super) fn advertised(repository: &impl Repository, id: ObjectId) -> bool {
repository.head_state().id() == Some(id)
|| (0..repository.reference_count()).any(|index| {
repository.reference_id(index) == Some(id)
|| repository.reference_peeled(index) == Some(id)
})
}
pub(crate) fn prepare_complete<'a>(
repository: &impl Repository,
body: &'a [u8],
version: Version,
) -> Result<UploadRequest<'a>> {
if !uses_only_head(repository) {
return Err(Error::Unsupported);
}
let head = repository.head_state().id().ok_or(Error::InvalidRequest)?;
let upload = UploadRequest::parse(body, repository.object_format(), version, |id| {
(id == head).then_some(()).ok_or(Error::InvalidRequest)
})?;
upload.require_complete()?;
Ok(upload)
}
pub(crate) fn write_upload<W: Write>(
repository: &impl Repository,
request: Request<'_>,
version: Version,
scratch: &mut [u8],
output: &mut W,
write_pack: impl FnOnce(&mut [u8], &mut PackOutput<'_, W>) -> Result<()>,
) -> Result<()> {
let upload = prepare_complete(repository, request.body, version)?;
upload.acknowledge(repository, output)?;
if upload.done {
let mut packed = upload.output(output);
write_pack(scratch, &mut packed)?;
packed.finish()?;
}
Ok(())
}