Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ capture) through F6 (acceptance on the first deployment); F0, F1 (server
skeleton, users, organizations, tokens, `admin` CLI), F2a (repositories API, lifecycle
and reconciler, collaborators, branches) and F2b (smart-HTTP git transport via `git http-backend`,
`pre-receive` ref policy, admission control, threat-model pass #1) and F3a (content reads: refs, tags, commits,
trees, contents, archives; writes are F3b) are built. Hosted repos are
trees, contents, archives) and F3b (contents writes: create, update, delete as server-made commits) are built. Hosted repos are
bare Git and get none of the `.sc/` features.

Other follow-up work is tracked as GitHub issues and listed under Deferred
Expand Down
32 changes: 0 additions & 32 deletions crates/forge/conformance/slice-pending.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,6 @@
]
]
},
"E": {
"stop": [
42,
"KeyError: 'sha'"
],
"defer": [
[
36,
"POST /api/v1/repos/acme/hist/contents/new.txt"
],
[
37,
"POST /api/v1/repos/acme/hist/contents/new.txt"
],
[
38,
"PUT /api/v1/repos/acme/hist/contents/README.md"
],
[
39,
"PUT /api/v1/repos/acme/hist/contents/README.md"
],
[
40,
"PUT /api/v1/repos/acme/hist/contents/README.md"
],
[
41,
"POST /api/v1/repos/acme/hist/contents/branch-file.txt"
]
]
},
"K": {
"defer": [
[
Expand Down
23 changes: 20 additions & 3 deletions crates/forge/src/cgi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ impl Transports {
}
}

/// Reserve `need` bytes of disk for a server-made write against the same floor and pool as pushes
/// (`None`: the disk cannot promise it).
pub fn reserve_disk(
&self,
data_dir: &std::path::Path,
need: u64,
) -> Option<crate::transport::Held> {
let free = crate::transport::free_bytes(data_dir).unwrap_or(0);
self.reserve.reserve(free, self.cfg.min_free, need)
}

fn repo_slots(&self, repo: i64) -> Arc<Semaphore> {
let mut m = self.per_repo.lock().unwrap_or_else(|e| e.into_inner());
if m.len() > 256 {
Expand Down Expand Up @@ -182,6 +193,11 @@ fn buffered(r: crate::http::Resp) -> Response<RespBody> {
.unwrap_or_else(|_| fallback(500))
}

/// The answer to a request that meets the server's shutdown.
fn shutting_down() -> crate::http::Resp {
text(503, "Service Unavailable\n").with("retry-after", "5".into())
}

fn fallback(status: u16) -> Response<RespBody> {
let mut r = Response::new(
http_body_util::Full::new(Bytes::from_static(b"internal server error\n"))
Expand Down Expand Up @@ -840,6 +856,8 @@ pub async fn handle(app: Arc<App>, req: Req, body: Incoming, path: GitPath) -> R
match tokio::task::spawn_blocking(move || transport::authorize(&app, &req, path)).await {
Ok(Ok(p)) => p,
Ok(Err(r)) => return buffered(r),
// A cancelled blocking task means the runtime is shutting down; a panic is an internal error.
Err(e) if e.is_cancelled() => return buffered(shutting_down()),
Err(_) => return buffered(text(500, "internal server error\n")),
}
};
Expand Down Expand Up @@ -936,13 +954,12 @@ pub async fn handle(app: Arc<App>, req: Req, body: Incoming, path: GitPath) -> R
let started = match tokio::task::spawn_blocking(move || launch(launch_args)).await {
Ok(Ok(s)) => s,
Ok(Err(LaunchError::Gone)) => return buffered(transport::repo_not_found()),
Ok(Err(LaunchError::Stopped)) => {
return buffered(text(503, "Service Unavailable\n").with("retry-after", "5".into()))
}
Ok(Err(LaunchError::Stopped)) => return buffered(shutting_down()),
Ok(Err(LaunchError::Io(e))) => {
eprintln!("sc-forge: could not start git: {e}");
return buffered(text(500, "internal server error\n"));
}
Err(e) if e.is_cancelled() => return buffered(shutting_down()),
Err(_) => return buffered(text(500, "internal server error\n")),
};
let Started {
Expand Down
83 changes: 54 additions & 29 deletions crates/forge/src/content_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ use serde_json::{json, Value};
use std::collections::HashMap;
use std::path::PathBuf;

fn dir(ctx: &Ctx, a: &Access) -> PathBuf {
pub(crate) fn dir(ctx: &Ctx, a: &Access) -> PathBuf {
ctx.app.life.repo_dir(a.repo.id)
}

/// `{base}/api/v1/repos/{owner}/{repo}`.
fn api_base(ctx: &Ctx, a: &Access) -> String {
pub(crate) fn api_base(ctx: &Ctx, a: &Access) -> String {
format!(
"{}/api/v1/repos/{}/{}",
ctx.app.base, a.repo.owner, a.repo.name
)
}

/// `{base}/{owner}/{repo}`: the web URLs Gitea puts in `html_url`.
fn web_base(ctx: &Ctx, a: &Access) -> String {
pub(crate) fn web_base(ctx: &Ctx, a: &Access) -> String {
format!("{}/{}/{}", ctx.app.base, a.repo.owner, a.repo.name)
}

Expand All @@ -55,7 +55,7 @@ fn flag(ctx: &Ctx, k: &str, default: bool) -> bool {
}

/// A git failure that is the request's doing, as opposed to the repository's.
fn read_err(ctx: &Ctx, a: &Access, e: GitError) -> Resp {
pub(crate) fn read_err(ctx: &Ctx, a: &Access, e: GitError) -> Resp {
match e {
GitError::OutputTooLarge => error(&ctx.app.base, 413, "the result is too large to serve"),
GitError::Timeout { .. } => error(&ctx.app.base, 504, "git timed out"),
Expand Down Expand Up @@ -123,7 +123,7 @@ impl<'a> Users<'a> {
}
}

fn verification() -> Value {
pub(crate) fn verification() -> Value {
json!({"verified": false, "reason": "gpg.error.not_signed_commit", "signature": "", "signer": null, "payload": ""})
}

Expand Down Expand Up @@ -588,33 +588,12 @@ fn content_json(
exact_last: bool,
) -> Result<Value, Resp> {
let (git, d) = (&ctx.app.life.git, dir(ctx, a));
let api = api_base(ctx, a);
let web = web_base(ctx, a);
let enc_path = e
.path
.split('/')
.map(pct_encode)
.collect::<Vec<_>>()
.join("/");
let enc_ref = refname
.split('/')
.map(pct_encode)
.collect::<Vec<_>>()
.join("/");
let query_ref = pct_encode(refname);
let last = if exact_last {
git_err(ctx, a, gitobj::last_commit(git, &d, commit, &e.path))?.unwrap_or_default()
} else {
commit.to_string()
};
let name = e.path.rsplit('/').next().unwrap_or(&e.path);
let seg = kind.segment();
let ty = match (e.kind.as_str(), e.mode.as_str()) {
("tree", _) => "dir",
("commit", _) => "submodule",
(_, "120000") => "symlink",
_ => "file",
};
let ty = entry_type(e);
let size = e.size.unwrap_or(0);
let (mut encoding, mut content, mut target) = (Value::Null, Value::Null, Value::Null);
// A link target is a path (PATH_MAX); a bigger blob is not read, and a listing reads at most
Expand All @@ -627,11 +606,57 @@ fn content_json(
encoding = json!("base64");
content = Value::String(b64::encode(&b));
}
Ok(content_value(
ctx, a, refname, kind, e, &last, encoding, content, target,
))
}

/// The type string of a tree entry in a contents object.
fn entry_type(e: &TreeEntry) -> &'static str {
match (e.kind.as_str(), e.mode.as_str()) {
("tree", _) => "dir",
("commit", _) => "submodule",
(_, "120000") => "symlink",
_ => "file",
}
}

/// The contents object of `e` as of ref `refname` (also what a contents write answers with).
#[allow(clippy::too_many_arguments)]
pub(crate) fn content_value(
ctx: &Ctx,
a: &Access,
refname: &str,
kind: RefKind,
e: &TreeEntry,
last: &str,
encoding: Value,
content: Value,
target: Value,
) -> Value {
let api = api_base(ctx, a);
let web = web_base(ctx, a);
let enc_path = e
.path
.split('/')
.map(pct_encode)
.collect::<Vec<_>>()
.join("/");
let enc_ref = refname
.split('/')
.map(pct_encode)
.collect::<Vec<_>>()
.join("/");
let query_ref = pct_encode(refname);
let name = e.path.rsplit('/').next().unwrap_or(&e.path);
let seg = kind.segment();
let ty = entry_type(e);
let size = e.size.unwrap_or(0);
let self_url = format!("{api}/contents/{enc_path}?ref={query_ref}");
// Recorded: directories point at `git/blobs/<tree id>` too.
let git_url = format!("{api}/git/blobs/{}", e.oid);
let html = format!("{web}/src/{seg}/{enc_ref}/{enc_path}");
Ok(json!({
json!({
"name": name,
"path": e.path,
"sha": e.oid,
Expand All @@ -647,7 +672,7 @@ fn content_json(
"download_url": if ty == "file" { json!(format!("{web}/raw/{seg}/{enc_ref}/{enc_path}")) } else { Value::Null },
"submodule_git_url": null,
"_links": {"self": self_url, "git": git_url, "html": html},
}))
})
}

/// Entries of one listing whose `last_commit_sha` is computed exactly.
Expand Down
Loading
Loading