branch: main
settings.rs
8464 bytesRaw
use super::*;
struct SettingsPage {
owner: String,
repo_name: String,
commits: i64,
blobs: i64,
db_bytes: u64,
default_branch: String,
}
impl SettingsPage {
fn settings_path(&self) -> String {
format!("/{}/{}/settings", self.owner, self.repo_name)
}
fn action_path(&self, sub: &str) -> String {
format!("{}/{}", self.settings_path(), sub)
}
fn db_mb(&self) -> f64 {
self.db_bytes as f64 / 1_048_576.0
}
}
fn build_settings_page(sql: &SqlStorage, owner: &str, repo_name: &str) -> Result<SettingsPage> {
#[derive(serde::Deserialize)]
struct CountRow {
n: i64,
}
let commits = sql
.exec("SELECT COUNT(*) AS n FROM commits", None)?
.to_array::<CountRow>()?
.first()
.map(|row| row.n)
.unwrap_or(0);
let blobs = sql
.exec("SELECT COUNT(*) AS n FROM blobs", None)?
.to_array::<CountRow>()?
.first()
.map(|row| row.n)
.unwrap_or(0);
Ok(SettingsPage {
owner: owner.to_string(),
repo_name: repo_name.to_string(),
commits,
blobs,
db_bytes: sql.database_size() as u64,
default_branch: store::get_config(sql, "default_branch")?
.unwrap_or_else(|| "refs/heads/main".to_string()),
})
}
fn render_settings_html(page: &SettingsPage, actor_name: Option<&str>) -> String {
let mut html = String::new();
html.push_str(&format!(
r#"
<section class="settings-section">
<h2>Repository stats</h2>
<div class="stats-grid">
<div class="stat-box"><div class="stat-val">{commits}</div><div class="stat-lbl">commits</div></div>
<div class="stat-box"><div class="stat-val">{blobs}</div><div class="stat-lbl">blobs</div></div>
<div class="stat-box"><div class="stat-val">{db_mb:.1} MB</div><div class="stat-lbl">database size</div></div>
</div>
</section>"#,
commits = page.commits,
blobs = page.blobs,
db_mb = page.db_mb(),
));
html.push_str(&format!(
r#"
<section class="settings-section">
<h2>Search indexes</h2>
<p class="settings-hint">Rebuild after a bulk push or if search results look stale.</p>
<div class="action-row">
<form method="POST" action="{commit_graph}">
<button class="btn-action" type="submit">Rebuild commit graph</button>
<span class="action-hint">Required for commit history and log</span>
</form>
<form method="POST" action="{fts_commits}">
<button class="btn-action" type="submit">Rebuild commit search</button>
<span class="action-hint">Full-text search over commit messages</span>
</form>
<form method="POST" action="{fts_head}">
<button class="btn-action" type="submit">Rebuild code search</button>
<span class="action-hint">Full-text search over file contents (slow on large repos)</span>
</form>
</div>
</section>"#,
commit_graph = page.action_path("rebuild-graph"),
fts_commits = page.action_path("rebuild-fts-commits"),
fts_head = page.action_path("rebuild-fts"),
));
html.push_str(&format!(
r#"
<section class="settings-section">
<h2>Default branch</h2>
<form method="POST" action="{action}" class="inline-form">
<input type="text" name="branch" value="{branch}" class="branch-input" placeholder="refs/heads/main">
<button class="btn-action" type="submit">Save</button>
</form>
</section>"#,
action = page.action_path("default-branch"),
branch = html_escape(&page.default_branch),
));
html.push_str(&format!(
r#"
<section class="settings-section settings-danger">
<h2>Danger zone</h2>
<p class="settings-hint">This will permanently delete all data for <strong>{owner}/{repo}</strong>. There is no undo.</p>
<form method="POST" action="{action}" class="inline-form">
<input type="text" name="confirm" placeholder='Type "{owner}/{repo}" to confirm' class="branch-input danger-confirm">
<button class="btn-danger-action" type="submit">Delete repository</button>
</form>
</section>"#,
owner = html_escape(&page.owner),
repo = html_escape(&page.repo_name),
action = page.action_path("delete"),
));
layout(
"Settings",
&page.owner,
&page.repo_name,
&page.default_branch,
actor_name,
&html,
)
}
fn render_settings_markdown(page: &SettingsPage, selection: &NegotiatedRepresentation) -> String {
let mut markdown = format!(
"# {}/{} settings\n\nOwner-only repository maintenance page.\n\n## Repository Stats\n- Commits: `{}`\n- Blobs: `{}`\n- Database size: `{:.1} MB` (`{}` bytes)\n\n## Current Configuration\n- Settings page: `{}`\n- Default branch: `{}`\n- Code search rebuilds index the current default branch only.\n",
page.owner,
page.repo_name,
page.commits,
page.blobs,
page.db_mb(),
page.db_bytes,
page.settings_path(),
page.default_branch,
);
let actions = vec![
Action::post(
page.action_path("rebuild-graph"),
"rebuild the commit ancestry graph used by history and log traversal",
)
.with_requires("repo owner")
.with_effect("deletes existing `commit_graph` rows, regenerates them from `commit_parents`, then redirects back to settings"),
Action::post(
page.action_path("rebuild-fts-commits"),
"rebuild the commit search index over commit messages and authors",
)
.with_requires("repo owner")
.with_effect("clears `fts_commits`, re-inserts every commit, then redirects back to settings"),
Action::post(
page.action_path("rebuild-fts"),
"rebuild the code search index for the saved default branch",
)
.with_requires("repo owner")
.with_effect("looks up the current `default_branch`, rebuilds the HEAD file-content index from that ref when it exists, then redirects back to settings"),
Action::post(
page.action_path("default-branch"),
"save the repository default branch used by the UI and code-search rebuilds",
)
.with_requires("repo owner")
.with_fields(vec![presentation::ActionField::required(
"branch",
"full ref name to store, for example `refs/heads/main`; empty or whitespace-only input leaves the current value unchanged",
)])
.with_effect("stores `default_branch` exactly as submitted after trimming outer whitespace, then redirects back to settings"),
Action::post(
page.action_path("delete"),
"permanently delete this repository",
)
.with_requires("repo owner")
.with_fields(vec![presentation::ActionField::required(
"confirm",
&format!(
"must exactly match `{}/{}` to proceed",
page.owner, page.repo_name
),
)])
.with_effect(&format!(
"danger: on an exact match, deletes all Durable Object storage for `{}/{}` and redirects to `/{}/`; any other value leaves the repository intact and redirects back to settings",
page.owner, page.repo_name, page.owner
)),
];
let hints = vec![
presentation::text_navigation_hint(*selection),
Hint::new("All settings mutations here are POST-only and owner-only."),
Hint::new("Use fully qualified refs like `refs/heads/main` for the default branch; this form does not verify that the ref exists before saving."),
Hint::new("Danger: repository deletion is irreversible because it clears the repository Durable Object storage."),
];
markdown.push_str(&presentation::render_actions_section(&actions));
markdown.push_str(&presentation::render_hints_section(&hints));
markdown
}
pub fn page_settings(
sql: &SqlStorage,
owner: &str,
repo_name: &str,
actor_name: Option<&str>,
) -> Result<Response> {
let page = build_settings_page(sql, owner, repo_name)?;
html_response(&render_settings_html(&page, actor_name))
}
pub fn page_settings_markdown(
sql: &SqlStorage,
owner: &str,
repo_name: &str,
selection: &NegotiatedRepresentation,
) -> Result<Response> {
let page = build_settings_page(sql, owner, repo_name)?;
presentation::markdown_response(&render_settings_markdown(&page, selection), selection)
}