🐸 Frogue(Fed)

~ololduck/Frogue

chore: setup rustfmt more tightly

d4c836a2026-10-25T13:36:19+01:00Ololduck

106 files changed, +663, −502
.rustfmt.toml+13−0
@@ -0,0 +1,13 @@
+use_field_init_shorthand = true
+hex_literal_case = "Lower"
+imports_indent = "Block"
+imports_layout = "Mixed"
+imports_granularity = "Module"
+group_imports = "StdExternalCrate"
+reorder_imports = true
+reorder_modules = true
+reorder_impl_items = true
+merge_derives = true
+
+format_code_in_doc_comments = true
+wrap_comments = true
Makefile+1−0
@@ -2,6 +2,7 @@
.PHONY: install clean build
+# Generates target/release/frogue
build:
cargo build --release
frogue/src/activitypub/actor.rs+8−6
@@ -6,9 +6,8 @@
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set};
use crate::activitypub::ApUrls;
−use crate::entities::ap_actors;
use crate::entities::prelude::{ApActors, Repositories, Users};
−use crate::entities::{repositories, users};
+use crate::entities::{ap_actors, repositories, users};
use crate::repositories::RESERVED_SLUGS;
/// Fetch or create an actor for the given user, generating a keypair if needed.
@@ -41,7 +40,8 @@ pub async fn get_or_create(
Ok(inserted)
}
−/// Fetch or create an actor for the given repository, generating a keypair if needed.
+/// Fetch or create an actor for the given repository, generating a keypair if
+/// needed.
pub async fn get_or_create_for_repo(
db: &DatabaseConnection,
repo_id: i64,
@@ -73,7 +73,8 @@ pub async fn get_or_create_for_repo(
/// Get the actor URL for a given actor model.
///
−/// Returns the appropriate URL based on whether this is a user actor or repository actor.
+/// Returns the appropriate URL based on whether this is a user actor or
+/// repository actor.
pub async fn actor_url_for(
db: &DatabaseConnection,
public_url: &str,
@@ -104,8 +105,9 @@ pub async fn actor_url_for(
/// Fetch a local actor by their ActivityPub URL.
///
−/// Returns None if the URL doesn't match a local actor pattern or if the user doesn't exist.
−/// Matches both user actors at `{base}/~{user}` and repository actors at `{base}/~{owner}/{slug}`.
+/// Returns None if the URL doesn't match a local actor pattern or if the user
+/// doesn't exist. Matches both user actors at `{base}/~{user}` and repository
+/// actors at `{base}/~{owner}/{slug}`.
pub async fn get_by_url(
db: &DatabaseConnection,
actor_url: &str,
frogue/src/activitypub/delivery.rs+7−6
@@ -1,6 +1,6 @@
−//! Delivery worker: polls pending deliveries and sends them to follower inboxes.
−//! Also provides the shared `enqueue_delivery` helper for inserting activities
−//! and creating delivery rows.
+//! Delivery worker: polls pending deliveries and sends them to follower
+//! inboxes. Also provides the shared `enqueue_delivery` helper for inserting
+//! activities and creating delivery rows.
use std::sync::Arc;
use std::time::Duration;
@@ -19,7 +19,8 @@ use crate::entities::prelude::*;
/// Spawns the delivery worker background task.
///
−/// Polls every 2 seconds for pending deliveries and sends them with HTTP signatures.
+/// Polls every 2 seconds for pending deliveries and sends them with HTTP
+/// signatures.
pub fn spawn(state: Arc<RwLock<AppState>>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
loop {
@@ -40,8 +41,8 @@ pub fn spawn(state: Arc<RwLock<AppState>>) -> tokio::task::JoinHandle<()> {
/// Insert an activity and create a pending delivery row for it.
///
−/// Shared helper used by inbox (Accept/Follow), following module (Follow, Undo),
−/// and notes (Create). Returns the activity ID.
+/// Shared helper used by inbox (Accept/Follow), following module (Follow,
+/// Undo), and notes (Create). Returns the activity ID.
#[tracing::instrument(skip(db, activity_json), fields(object_url = %object_url, inbox_url = %inbox_url))]
pub async fn enqueue_delivery(
db: &DatabaseConnection,
frogue/src/activitypub/feed.rs+8−4
@@ -82,7 +82,8 @@ impl FeedEntry {
/// Fetch the home feed for a user.
///
/// Returns up to `limit` entries, ordered by published date descending.
−/// The `page` parameter (0-indexed) determines which slice of entries to return.
+/// The `page` parameter (0-indexed) determines which slice of entries to
+/// return.
#[tracing::instrument(skip(db))]
pub async fn get_home_feed(
db: &DatabaseConnection,
@@ -208,16 +209,18 @@ pub async fn get_home_feed(
});
}
− // Query viewer's reactions to determine which notes they've liked or boosted
+ // Query viewer's reactions to determine which notes they've liked or
+ // boosted
let mut reacted_note_ids = std::collections::HashSet::new();
let mut reacted_remote_note_ids = std::collections::HashSet::new();
let mut boosted_note_ids = std::collections::HashSet::new();
let mut boosted_remote_note_ids = std::collections::HashSet::new();
if !local_note_ids.is_empty() || !remote_note_ids.is_empty() {
− use crate::entities::ap_reactions;
use sea_orm::Condition;
+ use crate::entities::ap_reactions;
+
// Query both like and announce reactions
let mut query =
ApReactions::find().filter(ap_reactions::Column::LocalUserId.eq(Some(user_id)));
@@ -282,9 +285,10 @@ pub async fn get_home_feed(
// Query reaction counts for all notes
if !local_note_ids.is_empty() || !remote_note_ids.is_empty() {
− use crate::entities::ap_reactions;
use sea_orm::Condition;
+ use crate::entities::ap_reactions;
+
// Build OR condition for note_id IN or remote_note_id IN
let mut or_condition = Condition::any();
if !local_note_ids.is_empty() {
frogue/src/activitypub/forge/activity.rs+11−8
@@ -81,16 +81,19 @@ pub async fn publish_activity(
Ok(activity.id)
}
−/// Publish an activity signed by one actor but delivered to another actor's followers.
+/// Publish an activity signed by one actor but delivered to another actor's
+/// followers.
///
−/// This is used for comments: the comment is signed by the user actor but delivered
−/// to the repository's followers (since the user may have zero followers while the
−/// repo has many).
+/// This is used for comments: the comment is signed by the user actor but
+/// delivered to the repository's followers (since the user may have zero
+/// followers while the repo has many).
///
/// # Arguments
/// * `db` - Database connection
−/// * `signer_actor_id` - The actor who signs the activity (user actor for comments)
−/// * `follower_source_actor_id` - The actor whose followers receive the delivery (repo actor)
+/// * `signer_actor_id` - The actor who signs the activity (user actor for
+/// comments)
+/// * `follower_source_actor_id` - The actor whose followers receive the
+/// delivery (repo actor)
/// * `activity_json` - The serialized activity JSON
/// * `object_url` - The URL of the object being acted upon
///
@@ -160,8 +163,8 @@ pub async fn publish_activity_with_separate_signer(
/// Announce acceptance of an Offer (for both local and remote Offers).
///
/// This builds an Accept activity, records it in the repository's outbox,
−/// and fans it out to all followers. If offerer_inbox is provided (remote Offer),
−/// also enqueues delivery to the offerer.
+/// and fans it out to all followers. If offerer_inbox is provided (remote
+/// Offer), also enqueues delivery to the offerer.
///
/// # Arguments
/// * `db` - Database connection
frogue/src/activitypub/forge/inbound.rs+7−4
@@ -1,7 +1,8 @@
//! ForgeFed inbound activity handlers for repository inboxes.
//!
−//! Handles Offer (ticket creation), Resolve (ticket closure), Edit (ticket updates),
−//! Create (comments), and Add (patch sets) activities from remote actors.
+//! Handles Offer (ticket creation), Resolve (ticket closure), Edit (ticket
+//! updates), Create (comments), and Add (patch sets) activities from remote
+//! actors.
use actix_web::HttpResponse;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
@@ -622,7 +623,8 @@ pub async fn handle_create(
/// Handle an incoming Add activity (patch set update).
///
−/// Per decision 7, inline patches are rejected. Origin-only patches are also rejected.
+/// Per decision 7, inline patches are rejected. Origin-only patches are also
+/// rejected.
#[instrument(skip(db, add))]
pub async fn handle_add(
db: &DatabaseConnection,
@@ -674,7 +676,8 @@ pub async fn handle_add(
);
// TODO: send Reject activity with explanation
− // Reject{object: add.id, summary: "inline patch merge requests are not supported yet"}
+ // Reject{object: add.id, summary: "inline patch merge requests are not
+ // supported yet"}
HttpResponse::Accepted().finish()
}
frogue/src/activitypub/inbox.rs+4−2
@@ -63,7 +63,8 @@ pub async fn handle(
}
};
− // Verify actor attribution: the activity's actor field must match the verified actor URL
+ // Verify actor attribution: the activity's actor field must match the
+ // verified actor URL
let activity_actor: Option<&str> = match &activity {
InboxActivity::Follow(f) => Some(f.actor.as_str()),
InboxActivity::Undo(u) => Some(u.actor.as_str()),
@@ -211,7 +212,8 @@ pub async fn handle(
.await
}
InboxActivity::Announce(announce) => {
− // Check if the Announce contains an embedded Note from an accepted followee
+ // Check if the Announce contains an embedded Note from an accepted
+ // followee
if let crate::activitypub::types::AnnounceObject::Embedded(embedded) = &announce.object
{
// Only store if it's a Note
frogue/src/activitypub/mod.rs+4−2
@@ -141,7 +141,8 @@ impl ApUrls {
format!("{}/patches/{}", self.repo_actor(owner, slug), number)
}
− /// Ticket comment: `{base}/~{owner}/{slug}/tickets/{number}/comments/{uuid}`.
+ /// Ticket comment:
+ /// `{base}/~{owner}/{slug}/tickets/{number}/comments/{uuid}`.
pub fn repo_ticket_comment(
&self,
owner: &str,
@@ -156,7 +157,8 @@ impl ApUrls {
)
}
− /// Patch comment: `{base}/~{owner}/{slug}/patches/{number}/comments/{uuid}`.
+ /// Patch comment:
+ /// `{base}/~{owner}/{slug}/patches/{number}/comments/{uuid}`.
pub fn repo_patch_comment(
&self,
owner: &str,
frogue/src/activitypub/notes.rs+10−5
@@ -12,7 +12,8 @@ use crate::entities::{ap_activities, ap_actors, ap_notes};
/// Publishes a new note from markdown content.
///
−/// Renders the markdown to HTML, stores the note, and creates a Create activity.
+/// Renders the markdown to HTML, stores the note, and creates a Create
+/// activity.
pub async fn publish(
db: &DatabaseConnection,
actor_id: i64,
@@ -127,9 +128,10 @@ pub async fn get_note(
// Get the user to build the actor URL
let user = match actor.user_id {
Some(user_id) => {
+ use sea_orm::QueryFilter;
+
use crate::entities::prelude::Users;
use crate::entities::users;
− use sea_orm::QueryFilter;
Users::find()
.filter(users::Column::Id.eq(user_id))
@@ -232,16 +234,19 @@ pub async fn get_reactions(
/// Deliver an activity to all followers of an actor.
///
−/// Groups followers by sharedInbox when available to avoid duplicate deliveries.
+/// Groups followers by sharedInbox when available to avoid duplicate
+/// deliveries.
async fn deliver_to_followers(
db: &DatabaseConnection,
actor_id: i64,
activity_id: i64,
) -> Result<(), sea_orm::DbErr> {
+ use std::collections::HashSet;
+
+ use sea_orm::QueryFilter;
+
use crate::entities::prelude::{ApFollowers, ApRemoteActors};
use crate::entities::{ap_deliveries, ap_followers};
− use sea_orm::QueryFilter;
− use std::collections::HashSet;
// Find all followers of this actor
let followers = ApFollowers::find()
frogue/src/activitypub/remote/mod.rs+10−6
@@ -37,14 +37,16 @@ struct RemoteActor {
icon: Option<String>,
}
−/// Deserialize the icon field, which can be either a URL string or an Image object
+/// Deserialize the icon field, which can be either a URL string or an Image
+/// object
fn deserialize_icon<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
− use serde::de::{self, Visitor};
use std::fmt;
+ use serde::de::{self, Visitor};
+
struct IconVisitor;
impl<'de> Visitor<'de> for IconVisitor {
@@ -267,7 +269,8 @@ async fn webfinger_lookup(
)
})?;
− // Validate the actor URL's host matches the webfinger host (prevent JRD redirect attacks)
+ // Validate the actor URL's host matches the webfinger host (prevent JRD
+ // redirect attacks)
let url = Url::parse(&actor_url)
.map_err(|e| ResolveError::WebfingerFailed(format!("Invalid actor URL in JRD: {}", e)))?;
@@ -291,8 +294,8 @@ async fn webfinger_lookup(
/// Fetch any URL with HTTP signature and return parsed JSON
///
−/// This is a generic helper for fetching ActivityPub objects with proper signing.
−/// Used for both actor documents and note objects.
+/// This is a generic helper for fetching ActivityPub objects with proper
+/// signing. Used for both actor documents and note objects.
pub async fn signed_get(
http_client: &reqwest_middleware::ClientWithMiddleware,
url_str: &str,
@@ -416,7 +419,8 @@ async fn fetch_actor_signed(
///
/// First checks the cache. If not found or if the key_id doesn't match,
/// fetches the actor document from the remote server.
−/// If the cache entry is older than 24 hours, re-fetches and updates it in place.
+/// If the cache entry is older than 24 hours, re-fetches and updates it in
+/// place.
#[tracing::instrument(skip(db, http_client), fields(actor_url = %actor_url))]
pub async fn resolve_actor(
db: &DatabaseConnection,
frogue/src/activitypub/remote/notes.rs+2−1
@@ -182,7 +182,8 @@ pub async fn handle_create(
/// Handle an incoming Update activity for a Note.
///
−/// Only updates notes that we've previously stored. The content is re-sanitized.
+/// Only updates notes that we've previously stored. The content is
+/// re-sanitized.
#[instrument(skip(db, http_client, update, public_url, signer))]
pub async fn handle_update(
db: &DatabaseConnection,
frogue/src/activitypub/signatures/mod.rs+4−2
@@ -1,6 +1,7 @@
//! HTTP Signatures for ActivityPub.
//!
−//! Supports both legacy draft-cavage signatures and RFC 9421 HTTP Message Signatures.
+//! Supports both legacy draft-cavage signatures and RFC 9421 HTTP Message
+//! Signatures.
mod rfc9421;
@@ -332,9 +333,10 @@ fn parse_http_date(date_str: &str) -> Option<chrono::DateTime<Utc>> {
#[cfg(test)]
mod tests {
+ use chrono::Datelike;
+
use super::*;
use crate::activitypub::keys;
− use chrono::Datelike;
#[test]
fn sign_and_verify_round_trip() {
frogue/src/activitypub/signatures/rfc9421.rs → frogue/src/activitypub/signatures/rfc9421/mod.rs+11−6
@@ -5,15 +5,18 @@
mod sf;
−use crate::activitypub::keys;
−use crate::activitypub::signatures::SignatureError;
use actix_web::http::header::HeaderMap;
−use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
+use base64::Engine as _;
+use base64::engine::general_purpose::STANDARD as BASE64;
use chrono::Utc;
use sf::{SfParam, SfValue};
use sha2::{Digest, Sha256};
−/// Verify an inbound request's signature using RFC 9421 HTTP Message Signatures.
+use crate::activitypub::keys;
+use crate::activitypub::signatures::SignatureError;
+
+/// Verify an inbound request's signature using RFC 9421 HTTP Message
+/// Signatures.
///
/// Returns the verified actor URL on success.
///
@@ -31,7 +34,8 @@ use sha2::{Digest, Sha256};
/// 1. Signature-Input header present and parseable
/// 2. Signature header present and parseable
/// 3. Exactly one signature label present in both headers
−/// 4. Parameters extracted: keyid (required), created (required), alg (optional)
+/// 4. Parameters extracted: keyid (required), created (required), alg
+/// (optional)
/// 5. Algorithm must be rsa-v1_5-sha256 (only RSA keys supported)
/// 6. Created timestamp within 300 seconds of now
/// 7. Expires timestamp (if present) must not be in the past
@@ -317,9 +321,10 @@ fn build_component_line(
#[cfg(test)]
mod tests {
+ use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue};
+
use super::*;
use crate::activitypub::keys;
− use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue};
#[tokio::test]
async fn test_rfc9421_round_trip() {
frogue/src/activitypub/types.rs+10−5
@@ -49,16 +49,20 @@ pub struct Actor {
/// ForgeFed: creation timestamp in RFC3339 format (for Repository actors).
#[serde(skip_serializing_if = "Option::is_none")]
pub published: Option<String>,
− /// ForgeFed: ticket tracker actor URL (self-referential for repos that track their own tickets).
+ /// ForgeFed: ticket tracker actor URL (self-referential for repos that
+ /// track their own tickets).
#[serde(skip_serializing_if = "Option::is_none")]
pub tickets_tracked_by: Option<String>,
− /// ForgeFed: patch tracker actor URL (self-referential for repos that track their own patches).
+ /// ForgeFed: patch tracker actor URL (self-referential for repos that track
+ /// their own patches).
#[serde(skip_serializing_if = "Option::is_none")]
pub send_patches_to: Option<String>,
− /// ForgeFed: repositories whose tickets this actor tracks (bidirectional back-reference).
+ /// ForgeFed: repositories whose tickets this actor tracks (bidirectional
+ /// back-reference).
#[serde(skip_serializing_if = "Option::is_none")]
pub tracks_tickets_for: Option<Vec<String>>,
− /// ForgeFed: repositories whose patches this actor tracks (bidirectional back-reference).
+ /// ForgeFed: repositories whose patches this actor tracks (bidirectional
+ /// back-reference).
#[serde(skip_serializing_if = "Option::is_none")]
pub tracks_patches_for: Option<Vec<String>>,
}
@@ -529,7 +533,8 @@ pub struct OfferActivity {
pub target: Option<ObjectRef>,
}
−/// Offer.object can be a URL (fetch via signed_get) or an embedded Ticket/Patch.
+/// Offer.object can be a URL (fetch via signed_get) or an embedded
+/// Ticket/Patch.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum OfferObject {
frogue/src/activitypub/webfinger.rs+1−2
@@ -7,11 +7,10 @@ use actix_web::HttpResponse;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use serde::Serialize;
+use super::ApUrls;
use crate::entities::prelude::Users;
use crate::entities::users;
−use super::ApUrls;
−
/// JRD (JSON Resource Descriptor) for webfinger.
#[derive(Serialize)]
struct Jrd {
frogue/src/auth.rs+8−7
@@ -5,15 +5,16 @@
//! looking anything up. [`CurrentUser`] is that payload, extracted from the
//! request.
−use actix_session::Session;
−use actix_session::SessionExt;
−use actix_web::{FromRequest, HttpRequest, dev::Payload, error::ErrorUnauthorized};
−use argon2::Argon2;
−use argon2::password_hash::{
− PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng,
−};
use std::future::{Ready, ready};
+use actix_session::{Session, SessionExt};
+use actix_web::dev::Payload;
+use actix_web::error::ErrorUnauthorized;
+use actix_web::{FromRequest, HttpRequest};
+use argon2::Argon2;
+use argon2::password_hash::rand_core::OsRng;
+use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
+
use crate::config::SESSION_SECRET_VAR_KEY;
use crate::entities::users;
frogue/src/comments/mod.rs+2−1
@@ -87,7 +87,8 @@ mod tests {
assert!(may_change(Some(3), 7, 3), "the author may change it");
assert!(may_change(Some(3), 7, 7), "the owner may change it");
assert!(!may_change(Some(3), 7, 9), "somebody else may not");
− // Remote-created comments (author is None) can only be changed by the owner
+ // Remote-created comments (author is None) can only be changed by the
+ // owner
assert!(
may_change(None, 7, 7),
"the owner may change remote-created comments"
frogue/src/comments/store.rs+4−2
@@ -194,7 +194,8 @@ pub struct CommentAuthor {
pub is_remote: bool,
}
−/// The authors who wrote comments, looking up both local users and remote actors.
+/// The authors who wrote comments, looking up both local users and remote
+/// actors.
#[tracing::instrument(level = "debug", skip(db, author_ids, remote_actor_ids))]
pub async fn comment_authors(
db: &DatabaseConnection,
@@ -229,7 +230,8 @@ pub async fn comment_authors(
.all(db)
.await?
{
− // Use preferred_username if available, otherwise extract from actor_url
+ // Use preferred_username if available, otherwise extract from
+ // actor_url
let name = actor
.preferred_username
.or_else(|| actor.actor_url.rsplit('/').next().map(|s| s.to_string()))
frogue/src/entities/ap_following.rs+2−1
@@ -1,7 +1,8 @@
//! Entity representing a following relationship.
//!
//! Tracks which local actors follow which remote actors. The follow_activity_id
−//! is the ActivityPub Follow activity's id we sent, used for matching Accept/Reject.
+//! is the ActivityPub Follow activity's id we sent, used for matching
+//! Accept/Reject.
use sea_orm::entity::prelude::*;
frogue/src/entities/ap_remote_tickets.rs+2−1
@@ -2,7 +2,8 @@
//!
//! When a local user opens a ticket or merge request on a remote forge,
//! we track it here: the Offer activity we sent, the tracker we sent it to,
−//! and the status (pending/accepted/rejected) with the assigned URL if accepted.
+//! and the status (pending/accepted/rejected) with the assigned URL if
+//! accepted.
use sea_orm::entity::prelude::*;
use serde::Serialize;
frogue/src/entities/comments.rs+4−2
@@ -11,7 +11,8 @@ pub struct Model {
pub id: i64,
#[sea_orm(unique)]
pub uuid: Uuid,
− /// The local user who wrote this comment (null for remote-authored comments).
+ /// The local user who wrote this comment (null for remote-authored
+ /// comments).
pub author_id: Option<i64>,
/// The remote actor who wrote this comment (for federated comments).
/// Exactly one of author_id or remote_actor_id is set.
@@ -24,7 +25,8 @@ pub struct Model {
pub patch_proposal_id: Option<i64>,
/// The comment content in markdown or HTML.
pub content: String,
− /// The format of the content: 'markdown' (default) or 'html' (for remote content).
+ /// The format of the content: 'markdown' (default) or 'html' (for remote
+ /// content).
pub content_format: String,
pub created_at: DateTimeWithTimeZone,
pub updated_at: Option<DateTimeWithTimeZone>,
frogue/src/entities/patch_proposals.rs+4−2
@@ -48,14 +48,16 @@ pub struct Model {
#[sea_orm(unique)]
pub uuid: Uuid,
pub repository_id: i64,
− /// Local user who created this patch proposal (null for remote-created proposals).
+ /// Local user who created this patch proposal (null for remote-created
+ /// proposals).
pub creator_id: Option<i64>,
/// Remote actor who created this patch proposal (for federated proposals).
/// Exactly one of creator_id or remote_actor_id is set.
pub remote_actor_id: Option<i64>,
/// The inbound Offer activity id, for idempotency on federation.
pub remote_offer_id: Option<String>,
− /// The activity URL of the last emitted/accepted Resolve, for Undo(Resolve) correlation.
+ /// The activity URL of the last emitted/accepted Resolve, for Undo(Resolve)
+ /// correlation.
pub resolve_activity_url: Option<String>,
pub number: i64,
pub summary: String,
frogue/src/entities/tickets.rs+2−1
@@ -33,7 +33,8 @@ pub struct Model {
pub remote_actor_id: Option<i64>,
/// The inbound Offer activity id, for idempotency on federation.
pub remote_offer_id: Option<String>,
− /// The activity URL of the last emitted/accepted Resolve, for Undo(Resolve) correlation.
+ /// The activity URL of the last emitted/accepted Resolve, for Undo(Resolve)
+ /// correlation.
pub resolve_activity_url: Option<String>,
/// The ticket's number within its repository, which is what its address
/// holds. Counted by `repositories.next_ticket_number` rather than by
frogue/src/git_hook.rs+20−12
@@ -329,7 +329,8 @@ async fn process_push_command(
}
if cmd.ref_name == "refs/patches" {
− // Creating a new patch proposal OR updating an existing one by change-id
+ // Creating a new patch proposal OR updating an existing one by
+ // change-id
if cmd.new_oid == ZERO_OID {
return Err("cannot delete refs/patches".to_string());
}
@@ -337,7 +338,8 @@ async fn process_push_command(
// Determine the target branch
let target_branch = opt_target.as_deref().unwrap_or(&repository.default_branch);
− // Check if there are any differences between the patch and target branch
+ // Check if there are any differences between the patch and target
+ // branch
if !has_differences(&cmd.new_oid, target_branch)
.await
.map_err(|e| format!("failed to check differences: {}", e))?
@@ -404,7 +406,8 @@ async fn process_push_command(
(pp, false)
};
− // If ready option was provided and we just created, open the patch proposal
+ // If ready option was provided and we just created, open the patch
+ // proposal
let pp = if opt_ready
&& !is_update
&& pp.status == crate::entities::patch_proposals::Status::Draft
@@ -569,15 +572,16 @@ async fn run_post_receive(
repository_id: i64,
user_id: i64,
) -> Result<(), Box<dyn std::error::Error>> {
− use crate::activitypub::ApUrls;
− use crate::activitypub::types;
− use crate::entities::prelude::{ApFollowers, ApRemoteActors};
− use crate::entities::{ap_followers, ap_remote_actors, users};
+ use std::collections::HashSet;
+
use chrono::Utc;
use sea_orm::QueryFilter;
− use std::collections::HashSet;
use uuid::Uuid;
+ use crate::activitypub::{ApUrls, types};
+ use crate::entities::prelude::{ApFollowers, ApRemoteActors};
+ use crate::entities::{ap_followers, ap_remote_actors, users};
+
// Get the repository
let repository = repositories::Entity::find_by_id(repository_id)
.one(db)
@@ -936,7 +940,8 @@ async fn has_differences(commit1: &str, commit2: &str) -> Result<bool, Box<dyn s
Ok(output.status.code() == Some(1))
}
−/// Emits Offer+Accept activities when a patch is opened via push with ready option
+/// Emits Offer+Accept activities when a patch is opened via push with ready
+/// option
async fn emit_patch_open_activity(
db: &sea_orm::DatabaseConnection,
repository_id: i64,
@@ -945,9 +950,10 @@ async fn emit_patch_open_activity(
patch: &crate::entities::patch_proposals::Model,
public_url: &str,
) -> Result<(), Box<dyn std::error::Error>> {
+ use uuid::Uuid;
+
use crate::activitypub::ApUrls;
use crate::entities::users;
− use uuid::Uuid;
let urls = ApUrls::new(public_url);
@@ -1045,9 +1051,10 @@ async fn emit_patch_update_activity(
patch: &crate::entities::patch_proposals::Model,
public_url: &str,
) -> Result<(), Box<dyn std::error::Error>> {
+ use uuid::Uuid;
+
use crate::activitypub::ApUrls;
use crate::entities::users;
− use uuid::Uuid;
let urls = ApUrls::new(public_url);
@@ -1069,7 +1076,8 @@ async fn emit_patch_update_activity(
// Build patch collection (the new patch set)
// For now, we'll create a simple collection with the current patch state
− // In a full implementation, this would use git format-patch to generate the actual patches
+ // In a full implementation, this would use git format-patch to generate the
+ // actual patches
let activity_id = urls.repo_activity(&owner.name, &repository.slug, Uuid::new_v4());
let add = crate::activitypub::types::Add {
frogue/src/lib.rs+17−9
@@ -2,7 +2,8 @@ use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
−use actix_session::{SessionMiddleware, storage::CookieSessionStore};
+use actix_session::SessionMiddleware;
+use actix_session::storage::CookieSessionStore;
use actix_web::cookie::SameSite;
use actix_web::{App, HttpServer, middleware, web};
use miette::{IntoDiagnostic, WrapErr};
@@ -17,20 +18,24 @@ use crate::config::*;
pub mod config {
// === Federation ===
− /// The environment variable key that enables public https url configuration and federation. No default value.
+ /// The environment variable key that enables public https url configuration
+ /// and federation. No default value.
pub const FROGUE_PUBLIC_URL_VAR_KEY: &str = "FROGUE_PUBLIC_URL";
// === HTTP ===
/// Environment variable holding the socket to listen on, as `host:port`.
pub const HTTP_BIND_ADDR_VAR_KEY: &str = "FROGUE_BIND_ADDR";
pub const DEFAULT_HTTP_BIND_ADDR: &str = "127.0.0.1:6565";
− /// A 64 character-long string used to sign/verify the session cookie. Default: randomly generated, so a server restart will invalidate all current sessions.
+ /// A 64 character-long string used to sign/verify the session cookie.
+ /// Default: randomly generated, so a server restart will invalidate all
+ /// current sessions.
pub const SESSION_SECRET_VAR_KEY: &str = "FROGUE_SESSION_SECRET";
/// Mainly for testing purpose, this will allow public urls as localhost
pub const FROGUE_ALLOW_PRIVATE_HOSTS_VAR_KEY: &str = "FROGUE_ALLOW_PRIVATE_HOSTS";
// === Storage ===
− /// Environment variable holding the directory bare git repositories live under.
+ /// Environment variable holding the directory bare git repositories live
+ /// under.
pub const REPO_ROOT_VAR_KEY: &str = "FROGUE_REPO_ROOT";
pub const DEFAULT_REPO_ROOT: &str = "/tmp/frogue-repos";
/// Where the database url should be found in the env
@@ -38,11 +43,12 @@ pub mod config {
pub const DEFAULT_DATABASE_URL: &str = "sqlite:///tmp/frogue-db.sqlite?mode=rwc";
// === SSH ===t
− /// Environment variable holding the name the SSH server should be advertised
− /// under in clone URLs.
+ /// Environment variable holding the name the SSH server should be
+ /// advertised under in clone URLs.
pub const SSH_HOST_VAR_KEY: &str = "FROGUE_SSH_HOST";
pub const DEFAULT_SSH_HOST: &str = "localhost:2222";
− /// Environment variable holding the SSH user Frogue should advertise in clone URLs.
+ /// Environment variable holding the SSH user Frogue should advertise in
+ /// clone URLs.
pub const SSH_USER_VAR_KEY: &str = "FROGUE_SSH_USER";
pub const DEFAULT_SSH_USER: &str = "git";
/// Environment variable holding the socket the SSH server listens on, as
@@ -61,13 +67,15 @@ pub mod config {
/// Timeout in seconds for completing the SSH TUI registration form.
pub const SSH_TUI_TIMEOUT_SECONDS_VAR_KEY: &str = "FROGUE_SSH_TUI_TIMEOUT_SECONDS";
pub const DEFAULT_SSH_TUI_TIMEOUT_SECONDS: u64 = 60;
− /// Timeout in seconds for a guest that authenticated but never opened the TUI.
+ /// Timeout in seconds for a guest that authenticated but never opened the
+ /// TUI.
pub const SSH_GUEST_IDLE_TIMEOUT_SECONDS_VAR_KEY: &str =
"FROGUE_SSH_GUEST_IDLE_TIMEOUT_SECONDS";
pub const DEFAULT_SSH_GUEST_IDLE_TIMEOUT_SECONDS: u64 = 60;
// === Registration Mode ===
− /// Environment variable for registration mode. See [`registration::RegistrationMode`]. default: open
+ /// Environment variable for registration mode. See
+ /// [`registration::RegistrationMode`]. default: open
pub const REGISTRATION_MODE_VAR_KEY: &str = "FROGUE_REGISTRATION_MODE";
/// Environment variable for invite validity in seconds.
pub const INVITES_VALIDITY_SECONDS_VAR_KEY: &str = "FROGUE_INVITES_VALIDITY_SECONDS";
frogue/src/markdown.rs+13−8
@@ -9,9 +9,10 @@
//! [`render`] is the only way a page embeds content it did not write itself,
//! which is why it lives on its own rather than inside a view.
−use pulldown_cmark::{Event, Options, Parser, Tag, html};
use std::path::Path;
+use pulldown_cmark::{Event, Options, Parser, Tag, html};
+
/// The markdown extensions the forge shows, on top of CommonMark.
///
/// Tables and footnotes are ordinary in a readme; strikethrough and task lists
@@ -27,9 +28,10 @@ fn options() -> Options {
options
}
−/// The sanitizer permits only checkbox inputs — no text fields, no hidden inputs,
−/// no way to submit a form. The `type` attribute is gated by value rather than
−/// by name, so a non-checkbox input cannot be injected via raw HTML.
+/// The sanitizer permits only checkbox inputs — no text fields, no hidden
+/// inputs, no way to submit a form. The `type` attribute is gated by value
+/// rather than by name, so a non-checkbox input cannot be injected via raw
+/// HTML.
fn sanitizer() -> ammonia::Builder<'static> {
let mut builder = ammonia::Builder::default();
builder
@@ -57,10 +59,12 @@ pub fn render(source: &str) -> String {
sanitizer().clean(&rendered).to_string()
}
−/// Renders `source` from markdown to HTML, rewriting relative file links to tree URLs.
+/// Renders `source` from markdown to HTML, rewriting relative file links to
+/// tree URLs.
///
−/// This is used when rendering markdown files in the repository browser, so that
−/// links like `[docs](./docs/guide.md)` become navigable links to the tree view.
+/// This is used when rendering markdown files in the repository browser, so
+/// that links like `[docs](./docs/guide.md)` become navigable links to the tree
+/// view.
pub fn render_with_repo_context(
source: &str,
owner: &str,
@@ -299,7 +303,8 @@ mod tests {
#[test]
fn raw_input_is_stripped() {
− // Only checkbox inputs from task lists should survive; raw HTML inputs are not allowed.
+ // Only checkbox inputs from task lists should survive; raw HTML inputs
+ // are not allowed.
for (source, forbidden) in [
("<input type=\"text\">", "type=\"text\""),
("<input type=\"hidden\" name=\"x\">", "type=\"hidden\""),
frogue/src/patches/change_id.rs+3−3
@@ -16,9 +16,9 @@ const CHANGE_ID_LEN: usize = 32;
/// Validates whether a string is a well-formed jj change-id.
///
−/// A valid change-id is exactly 32 characters long and uses only the reverse-hex
−/// alphabet: z, y, x, w, v, u, t, s, r, q, p, o, n, m, l, k (16 letters from
−/// 'k' to 'z' inclusive).
+/// A valid change-id is exactly 32 characters long and uses only the
+/// reverse-hex alphabet: z, y, x, w, v, u, t, s, r, q, p, o, n, m, l, k (16
+/// letters from 'k' to 'z' inclusive).
fn is_valid_change_id(s: &str) -> bool {
if s.len() != CHANGE_ID_LEN {
return false;
frogue/src/patches/merge.rs+4−2
@@ -26,7 +26,8 @@ pub enum MergeResult {
Refused { reason: String },
}
−/// Attempts to merge the source ref into the target branch according to the policy.
+/// Attempts to merge the source ref into the target branch according to the
+/// policy.
///
/// Returns a `MergeResult` indicating success, conflict, or policy refusal.
#[tracing::instrument(level = "debug", skip(bare), fields(bare = %bare.display()))]
@@ -163,7 +164,8 @@ async fn try_fast_forward(
})
}
−/// Creates a merge commit with target as the first parent and source as the second.
+/// Creates a merge commit with target as the first parent and source as the
+/// second.
async fn create_merge_commit(
bare: &Path,
merge_base: &str,
frogue/src/patches/mod.rs+6−5
@@ -9,8 +9,7 @@ pub mod change_id;
pub mod merge;
pub mod store;
−use crate::entities::patch_proposals::Model as PatchProposal;
−use crate::entities::patch_proposals::{MergePolicy, Status};
+use crate::entities::patch_proposals::{MergePolicy, Model as PatchProposal, Status};
use crate::entities::repositories::MergePolicy as RepoMergePolicy;
/// How long a summary may be, which is the width of the column it is kept in.
@@ -89,8 +88,9 @@ pub fn repo_merge_policy_to_pp(policy: RepoMergePolicy) -> MergePolicy {
}
}
−/// Whether `user` may change the merge policy of a patch proposal in a repository
−/// owned by `owner`. Only the repository owner may change the merge policy.
+/// Whether `user` may change the merge policy of a patch proposal in a
+/// repository owned by `owner`. Only the repository owner may change the merge
+/// policy.
pub fn may_change_merge_policy(owner: i64, user: i64) -> bool {
user == owner
}
@@ -145,7 +145,8 @@ mod tests {
assert!(may_revise(owner, Some(creator), creator));
assert!(may_revise(owner, Some(creator), owner));
assert!(!may_revise(owner, Some(creator), stranger));
− // Remote-created patches (creator is None) can only be revised by the owner
+ // Remote-created patches (creator is None) can only be revised by the
+ // owner
assert!(
may_revise(owner, None, owner),
"the owner may revise remote-created patches"
frogue/src/patches/store.rs+6−4
@@ -1,15 +1,15 @@
//! The patch proposal store: database operations for creating, updating, and
//! querying patch proposals.
+use chrono::Utc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, Condition, DatabaseConnection, DbErr, EntityTrait,
IntoActiveModel, QueryFilter, Set,
};
+use uuid::Uuid;
use crate::entities::patch_proposals::{self, MergePolicy, Status};
use crate::entities::repositories;
−use chrono::Utc;
−use uuid::Uuid;
/// Creates a new draft patch proposal for the given repository.
///
@@ -243,7 +243,8 @@ pub async fn update(
pp.update(db).await
}
−/// Updates a patch proposal with the given fields, including the resolve_activity_url.
+/// Updates a patch proposal with the given fields, including the
+/// resolve_activity_url.
#[allow(clippy::too_many_arguments)]
pub async fn update_with_resolve_url(
db: &DatabaseConnection,
@@ -332,7 +333,8 @@ pub async fn update_with_merge_snapshot(
}
/// Closes a patch proposal by setting its status to Closed.
−/// If snapshot OIDs are provided, they are stored to preserve the diff state at close time.
+/// If snapshot OIDs are provided, they are stored to preserve the diff state at
+/// close time.
pub async fn close(
db: &DatabaseConnection,
patch_id: i64,
frogue/src/protocol/pktline.rs+1−1
@@ -15,7 +15,7 @@ pub const FLUSH: &[u8] = b"0000";
/// The length includes the 4-byte length prefix itself.
pub fn encode(payload: &[u8], out: &mut impl Write) -> io::Result<()> {
let len = payload.len() + 4;
− if len > 0xFFFF {
+ if len > 0xffff {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"pkt-line payload too large",
frogue/src/registration.rs+8−5
@@ -1,13 +1,13 @@
//! Shared registration logic between web and SSH TUI.
−use crate::auth::hash_password;
−use crate::entities::{invites, users};
use chrono::{Duration, Utc};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
use tracing::info;
use uuid::Uuid;
+use crate::auth::hash_password;
use crate::config::*;
+use crate::entities::{invites, users};
/// Errors that can occur during registration.
#[derive(Debug, thiserror::Error)]
@@ -51,7 +51,8 @@ pub fn registration_mode() -> RegistrationMode {
}
}
−/// Returns the invite validity duration, read from the environment or the default.
+/// Returns the invite validity duration, read from the environment or the
+/// default.
pub fn invites_validity() -> Duration {
std::env::var(INVITES_VALIDITY_SECONDS_VAR_KEY)
.ok()
@@ -60,7 +61,8 @@ pub fn invites_validity() -> Duration {
.unwrap_or_else(|| Duration::seconds(DEFAULT_INVITES_VALIDITY_SECONDS))
}
−/// Returns the number of invites allowed per day, read from the environment or the default.
+/// Returns the number of invites allowed per day, read from the environment or
+/// the default.
pub fn invites_allowed_per_day() -> u64 {
std::env::var(INVITES_ALLOWED_PER_DAY_VAR_KEY)
.ok()
@@ -80,7 +82,8 @@ pub struct Registration {
/// Checks whether registration is allowed given the current mode and invite.
///
−/// Returns Ok(()) if registration should proceed, or an error if it should be blocked.
+/// Returns Ok(()) if registration should proceed, or an error if it should be
+/// blocked.
#[tracing::instrument(skip(db))]
pub async fn check_mode_allows_registration(
db: &sea_orm::DatabaseConnection,
frogue/src/repositories/git.rs+21−14
@@ -274,9 +274,9 @@ pub async fn history(bare: &Path, rev: &str) -> std::io::Result<Option<Vec<Commi
fields.next(),
fields.next(),
) else {
− // A row git could not shape into a commit is dropped rather than
− // failing the whole listing, the same way `refs` treats a ref
− // that is not a commit.
+ // A row git could not shape into a commit is dropped rather
+ // than failing the whole listing, the same way
+ // `refs` treats a ref that is not a commit.
continue;
};
@@ -632,11 +632,13 @@ fn parse_patch(patch: &str, numstat_files: Vec<(String, String, u64, u64, bool)>
.map(|(old, new, added, deleted, binary)| {
let status = if old == new {
if added == 0 && !binary {
− // Could be a mode change or an empty diff; treat as Modified.
+ // Could be a mode change or an empty diff; treat as
+ // Modified.
FileStatus::Modified
} else if added > 0 && deleted == 0 {
// Pure addition: could be a new file or just additions.
− // We can't tell from paths alone; the diff header clarifies.
+ // We can't tell from paths alone; the diff header
+ // clarifies.
FileStatus::Added
} else {
FileStatus::Modified
@@ -698,7 +700,8 @@ fn parse_patch(patch: &str, numstat_files: Vec<(String, String, u64, u64, bool)>
text: rest.to_string(),
});
}
− // `\ No newline at end of file` and other metadata lines are ignored.
+ // `\ No newline at end of file` and other metadata lines are
+ // ignored.
}
}
@@ -831,9 +834,10 @@ pub fn looks_binary(bytes: &[u8]) -> bool {
/// The readme in `names` to show a visitor who reads `languages`.
///
/// A localised readme wins over the plain one. The language is matched both as
−/// the visitor sent it and as its primary subtag, so `fr-CA` finds `README.fr.md`
−/// as well as `README.fr-CA.md`, and both the `.` and `_` separators are tried
−/// because repositories use both. Only the top level of the tree is looked at.
+/// the visitor sent it and as its primary subtag, so `fr-CA` finds
+/// `README.fr.md` as well as `README.fr-CA.md`, and both the `.` and `_`
+/// separators are tried because repositories use both. Only the top level of
+/// the tree is looked at.
fn choose_readme<'a>(names: &[&'a str], languages: &[String]) -> Option<&'a str> {
let mut candidates: Vec<String> = Vec::new();
@@ -1272,7 +1276,7 @@ mod tests {
async fn a_blob_returns_the_bytes_git_stored() {
let (scratch, bare, work) = scaffold("blob").await;
− let odd = [0x00u8, 0x01, 0xFF, b'a', b'b'];
+ let odd = [0x00u8, 0x01, 0xff, b'a', b'b'];
fs::write(work.join("odd.bin"), odd).expect("writing the file");
git_in(&work, &[], &["add", "-A"]);
git_in(&work, &[], &["commit", "-q", "-m", "Add the file"]);
@@ -1747,8 +1751,9 @@ index 1234567..abcdefg 100644
assert_eq!(commits.len(), 2);
− // Newest commit: modify file1.txt (2 additions, 1 deletion) + add file3.txt (3 additions)
− // Total: 2 files, 5 insertions, 1 deletion
+ // Newest commit: modify file1.txt (2 additions, 1 deletion) + add
+ // file3.txt (3 additions) Total: 2 files, 5 insertions, 1
+ // deletion
assert_eq!(commits[0].subject, "Modify and add");
assert_eq!(commits[0].files_changed, 2);
assert_eq!(commits[0].insertions, 5);
@@ -1788,7 +1793,8 @@ index 1234567..abcdefg 100644
assert_eq!(commits.len(), 1);
assert_eq!(commits[0].subject, "Add binary");
assert_eq!(commits[0].files_changed, 1);
− // Binary files show as "-" in numstat, which we should handle gracefully
+ // Binary files show as "-" in numstat, which we should handle
+ // gracefully
assert_eq!(commits[0].insertions, 0);
assert_eq!(commits[0].deletions, 0);
@@ -1842,7 +1848,8 @@ index 1234567..abcdefg 100644
"main's history excludes the branch's commit"
);
− // Topic's history should include all three: the branch's commit plus its ancestors.
+ // Topic's history should include all three: the branch's commit plus
+ // its ancestors.
let topic_history = history(&bare, "topic")
.await
.expect("git should answer")
frogue/src/ssh/handler.rs+21−16
@@ -23,13 +23,14 @@ use tracing::{Instrument, Span, debug, error, info, warn};
use crate::entities::{ssh_keys, users};
use crate::repositories::store::find_repository_by_path;
use crate::repositories::{GitService, RepositoryPath, may_read, parse_git_command};
−use crate::state_read;
−use crate::{AppState, FROGUE_PUBLIC_URL_VAR_KEY, SshThrottleConfig};
+use crate::{AppState, FROGUE_PUBLIC_URL_VAR_KEY, SshThrottleConfig, state_read};
−/// Buffer size for child communication. Needs to be small-ish because it stays on the child's stack
+/// Buffer size for child communication. Needs to be small-ish because it stays
+/// on the child's stack
const BUFFER: usize = 32 * 1024;
−/// Yeah, so this one (pun intended) is weiiiird. It has only one use but i was feeling weird just puting "1" as arg. 1 is the file descriptor of stderr.
+/// Yeah, so this one (pun intended) is weiiiird. It has only one use but i was
+/// feeling weird just puting "1" as arg. 1 is the file descriptor of stderr.
const STDERR: u32 = 1;
/// A connection that has been authenticated by a key belonging to a user.
@@ -68,7 +69,8 @@ enum Refusal {
/// An SSH public key that was offered but not registered yet.
///
/// Remembered so the registration TUI can pre-fill the key field with proof
−/// of possession: the client signed with it, so we know they hold the private half.
+/// of possession: the client signed with it, so we know they hold the private
+/// half.
#[derive(Clone)]
struct OfferedKey {
/// The key itself, in the form russh understands.
@@ -96,8 +98,8 @@ pub struct SessionHandler {
/// Keys that were offered but not registered yet. Remembered so the
/// registration TUI can show them pre-filled with proof of possession.
offered_keys: Vec<OfferedKey>,
− /// A key that was verified (the client signed it) but belongs to no user yet.
− /// This is the registration candidate.
+ /// A key that was verified (the client signed it) but belongs to no user
+ /// yet. This is the registration candidate.
verified_key: Option<OfferedKey>,
/// Whether this is a guest session (no authenticated user yet).
guest: bool,
@@ -215,7 +217,8 @@ impl russh::server::Handler for SessionHandler {
let fingerprint = public_key.fingerprint(ssh_key::HashAlg::Sha256).to_string();
// Remember this key for potential registration, regardless of whether
− // it's already registered. The auth_publickey step will verify possession.
+ // it's already registered. The auth_publickey step will verify
+ // possession.
if !self
.offered_keys
.iter()
@@ -541,7 +544,8 @@ impl russh::server::Handler for SessionHandler {
}
}
− /// Route incoming data to either git or the TUI, depending on what's active.
+ /// Route incoming data to either git or the TUI, depending on what's
+ /// active.
async fn data(
&mut self,
_channel: ChannelId,
@@ -570,9 +574,9 @@ impl russh::server::Handler for SessionHandler {
/// `upload-pack` and `receive-pack` the client has said all it intends to.
///
/// The process is left alone: it still has an answer to write, and its exit
− /// status is what the client waits for. Dropping the whole handle here would
− /// drop the kill sender too, which the waiting task reads as "stop" - and
− /// that would cut the answer off mid-flight.
+ /// status is what the client waits for. Dropping the whole handle here
+ /// would drop the kill sender too, which the waiting task reads as
+ /// "stop" - and that would cut the answer off mid-flight.
async fn channel_eof(
&mut self,
_channel: ChannelId,
@@ -649,7 +653,8 @@ impl russh::server::Handler for SessionHandler {
info!(peer = ?self.peer, "launching registration TUI for guest");
// Notify the idle watchdog that the TUI is starting.
− // TODO: check that it really cuts the connection if we're still on the login after SSH_TUI_TIMEOUT_SECONDS_VAR_KEY
+ // TODO: check that it really cuts the connection if we're still on the
+ // login after SSH_TUI_TIMEOUT_SECONDS_VAR_KEY
self.tui_started.notify_one();
// Create channels for TUI communication
@@ -862,9 +867,9 @@ async fn authorise(
may_read(repository.visibility, repository.owner_id, Some(user_id))
}
GitService::ReceivePack => {
− // Owner can always push. For public repos, any authenticated user can
− // run receive-pack; the pre-receive hook will restrict what refs they
− // may touch.
+ // Owner can always push. For public repos, any authenticated user
+ // can run receive-pack; the pre-receive hook will
+ // restrict what refs they may touch.
repository.owner_id == user_id
|| repository.visibility == crate::entities::repositories::Visibility::Public
}
frogue/src/ssh/mod.rs+9−9
@@ -3,10 +3,10 @@
//! A git client reaches a repository by opening an SSH connection and asking
//! to run one command: `git-upload-pack '/owner/slug.git'` to fetch,
//! `git-receive-pack '/owner/slug.git'` to push. There is no shell in the
−//! picture - the command is parsed by [`crate::repositories::parse_git_command`],
−//! which accepts exactly those two words and nothing else - so the server runs
−//! `git` as a child process on a single channel and shuttles its standard
−//! streams back and forth.
+//! picture - the command is parsed by
+//! [`crate::repositories::parse_git_command`], which accepts exactly those two
+//! words and nothing else - so the server runs `git` as a child process on a
+//! single channel and shuttles its standard streams back and forth.
//!
//! Connections are authenticated by public key against the keys users have
//! registered, not by password, and never anonymously. Authorisation is per
@@ -22,22 +22,22 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use miette::{IntoDiagnostic, WrapErr, bail};
−use russh::keys::ssh_key;
−use russh::keys::{PrivateKey, load_secret_key};
+use russh::keys::{PrivateKey, load_secret_key, ssh_key};
use russh::server::{Config, Server};
use tokio::net::TcpListener;
use tokio::sync::{RwLock, Semaphore};
use tracing::{Instrument, Span, error, info, warn};
−use crate::{AppState, SshThrottleConfig, config::*};
+use crate::config::*;
+use crate::{AppState, SshThrottleConfig};
pub mod handler;
mod tui;
pub use handler::SessionHandler;
−/// KINDA DEPRECIATED How to reach the SSH server, for the clone URLs the webui shows.
−/// TODO: Remove fully
+/// KINDA DEPRECIATED How to reach the SSH server, for the clone URLs the webui
+/// shows. TODO: Remove fully
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Settings {
pub host: String,
frogue/src/ssh/tui/mod.rs+3−1
@@ -4,6 +4,7 @@ pub mod register;
use std::io::{self, Write};
use std::sync::Arc;
+
use tokio::sync::mpsc;
use tracing::debug;
@@ -36,7 +37,8 @@ impl Write for ChannelWriter {
/// Runs the registration TUI over SSH.
///
−/// Spawns the registration form and handles input/output through the provided channels.
+/// Spawns the registration form and handles input/output through the provided
+/// channels.
pub async fn run_registration(
input_rx: &mut mpsc::UnboundedReceiver<Vec<u8>>,
output_tx: mpsc::UnboundedSender<Vec<u8>>,
frogue/src/ssh/tui/register.rs+4−5
@@ -1,14 +1,13 @@
//! The SSH registration form.
//!
−//! A simple terminal UI that allows unauthenticated users to register an account
−//! by providing username, email, and password. The SSH public key is taken from
−//! the verified connection and displayed as read-only.
+//! A simple terminal UI that allows unauthenticated users to register an
+//! account by providing username, email, and password. The SSH public key is
+//! taken from the verified connection and displayed as read-only.
use std::sync::Arc;
−use crossterm::ExecutableCommand;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
−use crossterm::terminal;
+use crossterm::{ExecutableCommand, terminal};
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Style};
frogue/src/telemetry.rs+15−12
@@ -35,13 +35,13 @@
use std::env;
+use actix_web::Error;
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
+use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
−use actix_web::{Error, error::ResponseError};
−use opentelemetry::global;
use opentelemetry::trace::TracerProvider as _;
−use opentelemetry::{KeyValue, Value};
+use opentelemetry::{KeyValue, Value, global};
use opentelemetry_otlp::{SpanExporter, WithExportConfig};
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::error::OTelSdkResult;
@@ -98,7 +98,8 @@ fn note(message: impl std::fmt::Display) {
eprintln!("frogue telemetry: {message}");
}
−/// Installs the global subscriber and returns the handle that owns the pipeline.
+/// Installs the global subscriber and returns the handle that owns the
+/// pipeline.
///
/// The returned [`Telemetry`] must outlive the server: dropping it shuts the
/// tracer provider down, which is what flushes the spans still sitting in the
@@ -142,7 +143,8 @@ pub fn init() -> Telemetry {
/// Kept alive by `main` so that spans recorded while the server is shutting
/// down still have somewhere to go.
pub struct Telemetry {
− /// Held for its `Drop`, which drains the exporter. `None` when export is off.
+ /// Held for its `Drop`, which drains the exporter. `None` when export is
+ /// off.
_otlp: Option<OtlpExporter>,
request_logger: TracingLogger<ErrorAwareRootSpanBuilder>,
}
@@ -158,15 +160,16 @@ impl Telemetry {
}
}
−/// The root span per request, like [`tracing_actix_web::DefaultRootSpanBuilder`]
−/// but with one correction: a `5xx` built by a handler is an error.
+/// The root span per request, like
+/// [`tracing_actix_web::DefaultRootSpanBuilder`] but with one correction: a
+/// `5xx` built by a handler is an error.
///
/// The default only considers a response failed when it carries an attached
−/// error, so a handler that answers a bare `HttpResponse::InternalServerError()`
−/// (which is how every failure path in this crate reports a database problem)
−/// would be recorded as `otel.status_code=OK`. Marking the status from the
−/// response code makes those traces come out red in Jaeger, where the operator
−/// looks for them.
+/// error, so a handler that answers a bare
+/// `HttpResponse::InternalServerError()` (which is how every failure path in
+/// this crate reports a database problem) would be recorded as
+/// `otel.status_code=OK`. Marking the status from the response code makes those
+/// traces come out red in Jaeger, where the operator looks for them.
pub struct ErrorAwareRootSpanBuilder;
impl RootSpanBuilder for ErrorAwareRootSpanBuilder {
frogue/src/tickets/mod.rs+4−2
@@ -283,7 +283,8 @@ mod tests {
assert!(may_change(7, Some(3), 3), "the creator may change it");
assert!(may_change(7, Some(3), 7), "the owner may change it");
assert!(!may_change(7, Some(3), 9), "somebody else may not");
− // Remote-created tickets (creator is None) can only be changed by the owner
+ // Remote-created tickets (creator is None) can only be changed by the
+ // owner
assert!(
may_change(7, None, 7),
"the owner may change remote-created tickets"
@@ -305,7 +306,8 @@ mod tests {
selected_labels(Some("bug,improvement")),
vec!["bug", "improvement"]
);
− // Space around the names is what writing a label's name by hand leaves behind.
+ // Space around the names is what writing a label's name by hand leaves
+ // behind.
assert_eq!(
selected_labels(Some(" bug , improvement ")),
vec!["bug", "improvement"]
frogue/src/tickets/store.rs+4−3
@@ -228,8 +228,8 @@ pub async fn list_tickets(
.insert(assignment.label_id);
}
− // Every one of the labels, not any of them: a filter of `bug,improvement` is a
− // ticket that is both a bug and an improvement.
+ // Every one of the labels, not any of them: a filter of `bug,improvement`
+ // is a ticket that is both a bug and an improvement.
listed.retain(|ticket| {
wanted.iter().all(|label| {
held.get(&ticket.id)
@@ -361,7 +361,8 @@ pub async fn creator_names(
Ok(names)
}
−/// The authors who created tickets, looking up both local users and remote actors.
+/// The authors who created tickets, looking up both local users and remote
+/// actors.
#[tracing::instrument(level = "debug", skip(db, creator_ids, remote_actor_ids))]
pub async fn creator_authors(
db: &DatabaseConnection,
frogue/src/views/accounts.rs+4−5
@@ -1,10 +1,8 @@
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
−use super::{
− Page,
− components::{Complaint, PasswordField, TextField},
−};
+use super::Page;
+use super::components::{Complaint, PasswordField, TextField};
/// What the registration form shows when it is (re)rendered: the values to
/// put back in the fields, and any complaint about the last attempt.
@@ -126,7 +124,8 @@ pub fn login(viewer: Option<&str>, form: &LoginForm) -> HttpResponse {
.into_response()
}
−/// The registration page, showing `message` instead of the form (for closed mode).
+/// The registration page, showing `message` instead of the form (for closed
+/// mode).
pub fn register_closed(viewer: Option<&str>, message: &str) -> HttpResponse {
Page {
title: "Frogue - Register".to_string(),
frogue/src/views/comments.rs+4−2
@@ -8,7 +8,8 @@ use crate::comments::CommentAuthor;
/// A comment as displayed in the UI.
pub struct CommentView<'a> {
− /// The base URL we can find this comment at. MUST contain the "/comments" part.
+ /// The base URL we can find this comment at. MUST contain the "/comments"
+ /// part.
pub base_url: &'a str,
/// The comment's UUID for edit/delete forms.
pub uuid: String,
@@ -29,7 +30,8 @@ pub struct CommentView<'a> {
impl Render for CommentView<'_> {
fn render(&self) -> Markup {
// let edit_action = format!("{}/{}", base_action, comment.uuid);
− // let delete_action = format!("{}/{}/delete", base_action, comment.uuid);
+ // let delete_action = format!("{}/{}/delete", base_action,
+ // comment.uuid);
html! {
article class="comment" id=(format!("comment-{}", self.uuid)) {
frogue/src/views/commit.rs+3−5
@@ -8,11 +8,9 @@ use actix_web::HttpResponse;
use maud::{Markup, Render, html};
use crate::repositories::git;
−use crate::views::{
− Page,
− components::{RepositoryChrome, RepositoryTab},
− repositories::CommitSummary,
−};
+use crate::views::Page;
+use crate::views::components::{RepositoryChrome, RepositoryTab};
+use crate::views::repositories::CommitSummary;
pub const MAX_DIFF_LINES_TO_AUTOPEN: usize = 100;
frogue/src/views/commits.rs+3−5
@@ -1,11 +1,9 @@
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
−use super::{
− Page,
− components::{RefSelect, RepositoryChrome, RepositoryTab},
− repositories::CommitSummary,
−};
+use super::Page;
+use super::components::{RefSelect, RepositoryChrome, RepositoryTab};
+use super::repositories::CommitSummary;
pub(super) struct CommitsBody<'a> {
owner: &'a str,
frogue/src/views/compare.rs+5−7
@@ -2,13 +2,11 @@ use actix_web::HttpResponse;
use maud::{Markup, Render, html};
use crate::repositories::git;
−use crate::views::{
− Page, RefSummary,
− commit::{diff_stats_summary, file_diffs},
− commits::CommitLine,
− components::{Complaint, RefSelect, RepositoryChrome, RepositoryTab},
− repositories::CommitSummary,
−};
+use crate::views::commit::{diff_stats_summary, file_diffs};
+use crate::views::commits::CommitLine;
+use crate::views::components::{Complaint, RefSelect, RepositoryChrome, RepositoryTab};
+use crate::views::repositories::CommitSummary;
+use crate::views::{Page, RefSummary};
/// The body of the compare page, showing the diff between two revisions.
pub(super) struct CompareBody<'a> {
frogue/src/views/components.rs+6−5
@@ -38,7 +38,8 @@ impl Render for Complaint<'_> {
}
}
−/// A repository heading: the owner, a slash, the slug, and the visibility indicator.
+/// A repository heading: the owner, a slash, the slug, and the visibility
+/// indicator.
pub(super) struct RepositoryHeading<'a> {
pub(super) owner: &'a str,
pub(super) slug: &'a str,
@@ -138,8 +139,8 @@ impl Render for FrogueUsername<'_> {
}
}
−/// A comment author, local or remote: local gets a `~username` link, remote gets
−/// a link to their actor URL plus a badge saying so.
+/// A comment author, local or remote: local gets a `~username` link, remote
+/// gets a link to their actor URL plus a badge saying so.
pub(super) struct AuthorName<'a>(pub(super) &'a crate::comments::CommentAuthor);
impl Render for AuthorName<'_> {
@@ -421,8 +422,8 @@ impl Render for FilterLink<'_> {
/// A reusable pagination component.
///
−/// Renders Previous/Next links based on the current page and whether more items exist.
−/// The base_url should include the path but not the query string.
+/// Renders Previous/Next links based on the current page and whether more items
+/// exist. The base_url should include the path but not the query string.
pub(super) struct Pagination {
pub current_page: u64,
pub has_next: bool,
frogue/src/views/file.rs+3−5
@@ -1,11 +1,9 @@
use actix_web::HttpResponse;
use maud::{Markup, PreEscaped, Render, html};
−use super::{
− Page,
− components::{RepositoryChrome, RepositoryTab},
− tree::Breadcrumb,
−};
+use super::Page;
+use super::components::{RepositoryChrome, RepositoryTab};
+use super::tree::Breadcrumb;
/// One file of one ref, as its page shows it.
pub struct FileDetail {
frogue/src/views/index.rs+1−2
@@ -1,9 +1,8 @@
use maud::{Markup, Render, html};
use tracing::instrument;
−use super::Page;
use super::components::Pagination;
−use super::{ActionTarget, Note, NoteActions, NoteAuthor, NoteAuthorHandle};
+use super::{ActionTarget, Note, NoteActions, NoteAuthor, NoteAuthorHandle, Page};
use crate::activitypub::feed::FeedEntry;
use crate::activitypub::notes::NoteReactions;
use crate::registration::RegistrationMode;
frogue/src/views/invites.rs+2−1
@@ -1,7 +1,8 @@
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
−use super::{Page, components::Complaint};
+use super::Page;
+use super::components::Complaint;
/// An invite as the list renders it.
pub struct InviteSummary {
frogue/src/views/notes.rs+7−4
@@ -1,8 +1,9 @@
−use super::components;
−use crate::activitypub::notes::NoteReactions;
use maud::{Markup, PreEscaped, Render, html};
use url::Url;
+use super::components;
+use crate::activitypub::notes::NoteReactions;
+
/// The target for note actions (like/announce endpoints).
#[derive(Debug, Clone)]
pub enum ActionTarget {
@@ -143,7 +144,8 @@ impl Render for NoteAuthorHandle {
}
}
−/// A unified note component with author, content, permalink, counts, and actions.
+/// A unified note component with author, content, permalink, counts, and
+/// actions.
pub struct Note {
/// The author display info.
pub author: NoteAuthor,
@@ -230,9 +232,10 @@ impl Render for Note {
#[cfg(test)]
mod tests {
− use super::*;
use chrono::Utc;
+ use super::*;
+
#[test]
fn test_note_renders_basic_structure() {
let note = Note {
frogue/src/views/patches.rs+7−8
@@ -2,14 +2,12 @@ use actix_web::HttpResponse;
use maud::{Markup, PreEscaped, Render, html};
use tracing::instrument;
−use super::{
− Page,
− commit::{diff_stats_summary, file_diffs},
− components::{
− AuthorName, Complaint, FilterLink, RepositoryChrome, RepositoryTab, TextArea, TextField,
− },
− repositories::CommitSummary,
+use super::Page;
+use super::commit::{diff_stats_summary, file_diffs};
+use super::components::{
+ AuthorName, Complaint, FilterLink, RepositoryChrome, RepositoryTab, TextArea, TextField,
};
+use super::repositories::CommitSummary;
/// A repository's patches, as the list page shows them.
pub struct PatchesDetail {
@@ -145,7 +143,8 @@ pub struct PatchDetail<'a> {
pub commits: Vec<CommitSummary>,
/// The files changed in this patch.
pub files: Vec<crate::repositories::git::DiffFile>,
− /// If the diff cannot be shown, the reason (e.g. merged before snapshots existed).
+ /// If the diff cannot be shown, the reason (e.g. merged before snapshots
+ /// existed).
pub diff_unavailable_reason: Option<String>,
/// The comments on this patch, in chronological order.
pub comments: Vec<super::comments::CommentView<'a>>,
frogue/src/views/profile.rs+4−5
@@ -2,11 +2,10 @@ use actix_web::HttpResponse;
use maud::{Markup, Render, html};
use tracing::instrument;
−use super::{
− ActionTarget, Note, NoteActions, NoteAuthor, NoteAuthorHandle, Page,
− components::{Complaint, TextArea},
−};
−use crate::{entities::ap_notes, views::components};
+use super::components::{Complaint, TextArea};
+use super::{ActionTarget, Note, NoteActions, NoteAuthor, NoteAuthorHandle, Page};
+use crate::entities::ap_notes;
+use crate::views::components;
/// What can be displayed about a user, for instance to tell who made this note
pub struct ActorProfile {
frogue/src/views/refs.rs+3−5
@@ -1,11 +1,9 @@
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
−use super::{
− Page,
− components::{RepositoryChrome, RepositoryTab},
− repositories::RefSummary,
−};
+use super::Page;
+use super::components::{RepositoryChrome, RepositoryTab};
+use super::repositories::RefSummary;
pub(super) struct RefsBody<'a> {
owner: &'a str,
frogue/src/views/remote_forge.rs+2−1
@@ -6,7 +6,8 @@
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
−use super::{Page, components::Complaint};
+use super::Page;
+use super::components::Complaint;
/// List of remote tickets/MRs a user has opened
pub struct RemoteTicketsList {
frogue/src/views/repositories.rs+4−4
@@ -1,11 +1,11 @@
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
−use super::{
− Page,
− components::{Complaint, DeleteForm, RepositoryChrome, RepositoryTab, TextArea, TextField},
− form::{MergePolicySelect, VisibilityRadios},
+use super::Page;
+use super::components::{
+ Complaint, DeleteForm, RepositoryChrome, RepositoryTab, TextArea, TextField,
};
+use super::form::{MergePolicySelect, VisibilityRadios};
use crate::entities::ticket_labels::Model as Label;
/// A repository as its own page shows it.
frogue/src/views/ssh_keys.rs+2−4
@@ -1,10 +1,8 @@
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
−use super::{
− Page,
− components::{Complaint, DeleteForm, TextArea},
−};
+use super::Page;
+use super::components::{Complaint, DeleteForm, TextArea};
/// A key as the list of them shows it.
pub struct SshKeySummary {
frogue/src/views/tickets.rs+4−6
@@ -1,12 +1,10 @@
use actix_web::HttpResponse;
use maud::{Markup, PreEscaped, Render, html};
−use super::{
− Page,
− components::{
− AuthorName, Complaint, DeleteForm, FilterLink, RepositoryChrome, RepositoryTab, TextArea,
− TextField,
− },
+use super::Page;
+use super::components::{
+ AuthorName, Complaint, DeleteForm, FilterLink, RepositoryChrome, RepositoryTab, TextArea,
+ TextField,
};
/// A summary as a ticket's heading shows it: markdown, when what the
frogue/src/views/tree.rs+2−4
@@ -1,10 +1,8 @@
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
−use super::{
− Page,
− components::{RepositoryChrome, RepositoryTab},
−};
+use super::Page;
+use super::components::{RepositoryChrome, RepositoryTab};
/// A directory entry as the tree browser shows it.
pub struct TreeEntry {
frogue/src/webui/accounts.rs+3−4
@@ -15,13 +15,12 @@ use tokio::sync::RwLock;
use tracing::error;
use uuid::Uuid;
−use crate::AppState;
use crate::auth::{CurrentUser, log_in, log_out, verify_password, viewer_name};
−use crate::entities::{prelude::Users, users};
+use crate::entities::prelude::Users;
+use crate::entities::users;
use crate::registration::{RegisterError, Registration};
−use crate::state_read;
−use crate::views;
use crate::views::{LoginForm, RegisterForm};
+use crate::{AppState, state_read, views};
/// What the registration form posts.
#[derive(Debug, Deserialize)]
frogue/src/webui/federation.rs+25−23
@@ -12,15 +12,11 @@ use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::error;
−use crate::AppState;
use crate::activitypub::{self, ApUrls, types};
use crate::auth::CurrentUser;
use crate::entities::prelude::{ApFollowers, ApRemoteActors, Repositories, Users};
−use crate::entities::repositories;
−use crate::entities::users;
−use crate::entities::{ap_followers, ap_remote_actors};
−
−use crate::state_read;
+use crate::entities::{ap_followers, ap_remote_actors, repositories, users};
+use crate::{AppState, state_read};
/// Guard function for ActivityPub content negotiation.
///
@@ -679,9 +675,9 @@ pub async fn post_react(
}
};
+ use sea_orm::{ActiveModelTrait, Set};
+
use crate::entities::ap_reactions;
− use sea_orm::ActiveModelTrait;
− use sea_orm::Set;
let reaction = ap_reactions::ActiveModel {
note_id: Set(Some(note.id)),
@@ -791,9 +787,10 @@ pub async fn post_unreact(
}
};
− use crate::entities::ap_reactions;
use sea_orm::ModelTrait;
+ use crate::entities::ap_reactions;
+
let reaction = match crate::entities::prelude::ApReactions::find()
.filter(ap_reactions::Column::NoteId.eq(note.id))
.filter(ap_reactions::Column::LocalUserId.eq(Some(viewer.id)))
@@ -906,9 +903,9 @@ pub async fn post_announce(
}
};
+ use sea_orm::{ActiveModelTrait, Set};
+
use crate::entities::ap_reactions;
− use sea_orm::ActiveModelTrait;
− use sea_orm::Set;
let reaction = ap_reactions::ActiveModel {
note_id: Set(Some(note.id)),
@@ -1150,9 +1147,9 @@ pub async fn post_react_remote(
}
// Store the reaction locally
+ use sea_orm::{ActiveModelTrait, Set};
+
use crate::entities::ap_reactions;
− use sea_orm::ActiveModelTrait;
− use sea_orm::Set;
let reaction = ap_reactions::ActiveModel {
note_id: Set(None),
@@ -1260,9 +1257,10 @@ pub async fn post_unreact_remote(
}
};
− use crate::entities::ap_reactions;
use sea_orm::ModelTrait;
+ use crate::entities::ap_reactions;
+
// Find the reaction
let reaction = match crate::entities::prelude::ApReactions::find()
.filter(ap_reactions::Column::RemoteNoteId.eq(remote_note_id))
@@ -1444,9 +1442,9 @@ pub async fn post_announce_remote(
}
// Store the reaction locally
+ use sea_orm::{ActiveModelTrait, Set};
+
use crate::entities::ap_reactions;
− use sea_orm::ActiveModelTrait;
− use sea_orm::Set;
let reaction = ap_reactions::ActiveModel {
note_id: Set(None),
@@ -1482,7 +1480,8 @@ pub async fn post_announce_remote(
/// POST /remote-notes/{id}/unannounce
///
−/// Remove an announce (unboost) from a remote note by sending an Undo(Announce) activity.
+/// Remove an announce (unboost) from a remote note by sending an Undo(Announce)
+/// activity.
#[post("/remote-notes/{id}/unannounce")]
#[tracing::instrument(skip_all, fields(remote_note_id = %id))]
pub async fn post_unannounce_remote(
@@ -1554,9 +1553,10 @@ pub async fn post_unannounce_remote(
}
};
− use crate::entities::ap_reactions;
use sea_orm::ModelTrait;
+ use crate::entities::ap_reactions;
+
// Find the reaction
let reaction = match crate::entities::prelude::ApReactions::find()
.filter(ap_reactions::Column::RemoteNoteId.eq(remote_note_id))
@@ -1732,10 +1732,10 @@ pub async fn post_follow(
};
// Check if already following (idempotent)
+ use sea_orm::{ColumnTrait, QueryFilter};
+
use crate::entities::ap_following;
use crate::entities::prelude::ApFollowing;
− use sea_orm::ColumnTrait;
− use sea_orm::QueryFilter;
let existing = ApFollowing::find()
.filter(ap_following::Column::LocalActorId.eq(actor.id))
@@ -2288,8 +2288,9 @@ pub async fn get_repo_commit(
let urls = ApUrls::new(state.public_url());
− // Note: We don't have author email in the Commit struct, so we omit attributed_to
− // In the future, this could be matched by author name or extended to include email
+ // Note: We don't have author email in the Commit struct, so we omit
+ // attributed_to In the future, this could be matched by author name or
+ // extended to include email
let attributed_to = None;
let commit_doc = types::Commit {
@@ -2362,7 +2363,8 @@ pub async fn get_ticket_ap(
}
};
− // Look up the creator (local tickets have creator_id, remote tickets have remote_actor_id)
+ // Look up the creator (local tickets have creator_id, remote tickets have
+ // remote_actor_id)
let creator_name = match ticket.creator_id {
Some(creator_id) => {
match Users::find()
frogue/src/webui/git.rs+1−2
@@ -23,9 +23,8 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web};
use tokio::sync::RwLock;
use tracing::{error, info};
−use crate::AppState;
use crate::repositories::{self, RepositoryPath, store};
−use crate::state_read;
+use crate::{AppState, state_read};
/// The only service worth advertising: fetching. `git-receive-pack` is
/// refused, since pushing over HTTP is not something this forge offers.
frogue/src/webui/invites.rs+1−2
@@ -11,11 +11,10 @@ use chrono::{DateTime, FixedOffset, Utc};
use tokio::sync::RwLock;
use tracing::error;
−use crate::AppState;
use crate::auth::CurrentUser;
−use crate::state_read;
use crate::views::{self, InviteSummary};
use crate::webui::repositories::acting_user;
+use crate::{AppState, state_read};
#[get("/~{user_or_team}/invites")]
#[tracing::instrument(skip_all, fields(user_or_team = %user_or_team.as_str()))]
frogue/src/webui/mod.rs+1−2
@@ -4,11 +4,10 @@ use actix_web::{HttpResponse, get, web};
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter};
use tokio::sync::RwLock;
−use crate::AppState;
use crate::auth::CurrentUser;
use crate::entities::prelude::*;
use crate::entities::users::Column as UserColumn;
−use crate::views;
+use crate::{AppState, views};
pub mod accounts;
pub mod federation;
frogue/src/webui/nodeinfo.rs+1−2
@@ -7,8 +7,7 @@ use tokio::sync::RwLock;
use tracing::instrument;
use crate::entities::{ap_notes, comments, patch_proposals, repositories, tickets, users};
−use crate::state_read;
−use crate::{AppState, activitypub, registration};
+use crate::{AppState, activitypub, registration, state_read};
/// NodeInfo 2.1 JSON document structure
#[derive(Serialize)]
frogue/src/webui/remote_forge.rs+14−9
@@ -9,14 +9,14 @@
//! 5. Delivers to the tracker's inbox
//! 6. Later, Accept/Reject correlation updates the status
+use std::sync::Arc;
+
use actix_web::{HttpResponse, get, post, web};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
−use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info, instrument};
use uuid::Uuid;
−use crate::AppState;
use crate::activitypub::actor::{actor_url_for, get_or_create};
use crate::activitypub::delivery::enqueue_delivery;
use crate::activitypub::remote;
@@ -24,10 +24,10 @@ use crate::auth::CurrentUser;
use crate::entities::ap_remote_tickets;
use crate::repositories::RepositoryPath;
use crate::repositories::store::find_repository_by_path;
−use crate::state_read;
use crate::views::{
NewRemoteTicket, RemoteTicketRow, RemoteTicketsList, new_remote_ticket, remote_tickets,
};
+use crate::{AppState, state_read};
/// Parses a repository URL like `http://host/~owner/slug` into (owner, slug).
fn parse_repository_url(url: &str, public_url: &str) -> Option<(String, String)> {
@@ -48,7 +48,8 @@ fn parse_repository_url(url: &str, public_url: &str) -> Option<(String, String)>
Some((owner.to_string(), slug.to_string()))
}
−/// Generates patch files using git format-patch for the range default_branch..branch
+/// Generates patch files using git format-patch for the range
+/// default_branch..branch
async fn generate_patches(
bare_path: &std::path::Path,
branch: &str,
@@ -87,7 +88,8 @@ async fn generate_patches(
Ok(patches)
}
−/// GET `/~{user}/remote-tickets` - list of remote tickets/MRs this user has opened
+/// GET `/~{user}/remote-tickets` - list of remote tickets/MRs this user has
+/// opened
#[get("/~{user}/remote-tickets")]
#[instrument(skip(state, current))]
pub async fn get_remote_tickets(
@@ -137,7 +139,8 @@ pub async fn get_remote_tickets(
remote_tickets(Some(&current.name), &view)
}
−/// GET `/~{user}/remote-tickets/new` - form to open a ticket/MR on a remote tracker
+/// GET `/~{user}/remote-tickets/new` - form to open a ticket/MR on a remote
+/// tracker
#[get("/~{user}/remote-tickets/new")]
#[instrument(skip(_state, current))]
pub async fn get_new_remote_ticket(
@@ -295,8 +298,9 @@ pub async fn post_remote_ticket(
}
};
− // Verify the bidirectional reference: tracker must list the repo in tracksTicketsFor/tracksPatchesFor
− // Exception: be lenient when tracker == repo actor (self-tracking case)
+ // Verify the bidirectional reference: tracker must list the repo in
+ // tracksTicketsFor/tracksPatchesFor Exception: be lenient when tracker
+ // == repo actor (self-tracking case)
if tracker_url != target_repo_url {
let back_ref_field = match form.kind.as_str() {
"ticket" => "tracksTicketsFor",
@@ -338,7 +342,8 @@ pub async fn post_remote_ticket(
// Build the appropriate object based on kind
let object = if form.kind == "mr" {
− // For patches, we need to build a Patch object with attachment and collection
+ // For patches, we need to build a Patch object with attachment and
+ // collection
let origin_repo = form.origin_repo.as_deref().unwrap_or("");
let origin_branch = form.origin_branch.as_deref().unwrap_or("");
frogue/src/webui/repositories/mod.rs+18−20
@@ -13,22 +13,19 @@ pub mod tickets;
use std::path::{Path, PathBuf};
use std::sync::Arc;
−use actix_web::{HttpRequest, HttpResponse, get, http::header, post, web};
+use actix_web::http::header;
+use actix_web::{HttpRequest, HttpResponse, get, post, web};
use sea_orm::DatabaseConnection;
use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::error;
−use crate::AppState;
use crate::auth::{CurrentUser, viewer_name};
use crate::entities::repositories::Model as Repository;
use crate::entities::users;
−use crate::highlight;
−use crate::markdown;
−use crate::repositories::store;
−use crate::repositories::{self, RepositoryPath};
−use crate::state_read;
+use crate::repositories::{self, RepositoryPath, store};
use crate::views::{self, RepositoryForm, RepositorySummary};
+use crate::{AppState, highlight, markdown, state_read};
/// What the new repository form posts.
#[derive(Debug, Deserialize)]
@@ -468,9 +465,10 @@ async fn get_commit(
/// A single commit's diff, read and rendered.
///
−/// The `rev` must resolve to a commit via [`repositories::git::resolves_to_commit`];
−/// otherwise the page is `404`. A `rev` starting with `-` is refused to avoid
−/// argument injection, the same guard [`commits_address`] uses.
+/// The `rev` must resolve to a commit via
+/// [`repositories::git::resolves_to_commit`]; otherwise the page is `404`. A
+/// `rev` starting with `-` is refused to avoid argument injection, the same
+/// guard [`commits_address`] uses.
async fn commit_address(
_data: &web::Data<Arc<RwLock<AppState>>>,
viewer: Option<CurrentUser>,
@@ -526,7 +524,8 @@ async fn commit_address(
let files = match repositories::git::commit_diff(&bare, &rev).await {
Ok(Some(files)) => files,
Ok(None) => {
− // Should not happen if resolves_to_commit returned true, but handle it.
+ // Should not happen if resolves_to_commit returned true, but handle
+ // it.
return HttpResponse::NotFound().body(format!(
"`{rev}` names no revision of `{}/{}`",
owner.name, repository.slug
@@ -1140,7 +1139,8 @@ async fn commits_address(
let commits = match repositories::git::history(&bare, &tail).await {
Ok(Some(commits)) => commits,
Ok(None) => {
− // Should not happen if resolves_to_commit returned true, but handle it.
+ // Should not happen if resolves_to_commit returned true, but handle
+ // it.
return HttpResponse::NotFound().body(format!(
"`{tail}` names no revision of `{}/{}`",
owner.name, repository.slug
@@ -1429,9 +1429,9 @@ async fn tree_address(
})
.collect();
− // Directories first, the shape of a file tree everywhere, and then by
− // name: what a reader scanning a page wants, rather than git's own
− // byte order.
+ // Directories first, the shape of a file tree everywhere, and then
+ // by name: what a reader scanning a page wants, rather
+ // than git's own byte order.
entries.sort_by(|left, right| {
right
.is_directory
@@ -1983,11 +1983,9 @@ pub(crate) async fn summaries(
#[cfg(test)]
mod tests {
− use super::accept_languages;
− use super::is_markdown;
− use super::resolve_tail;
− use super::split_compare_range;
− use super::usable_compare_end;
+ use super::{
+ accept_languages, is_markdown, resolve_tail, split_compare_range, usable_compare_end,
+ };
fn available(names: &[&str]) -> Vec<String> {
names.iter().map(|name| name.to_string()).collect()
frogue/src/webui/repositories/patches.rs+43−38
@@ -12,24 +12,20 @@ use tokio::sync::RwLock;
use tracing::error;
use uuid::Uuid;
−use crate::AppState;
+use super::readable_repository;
use crate::auth::{CurrentUser, viewer_name};
−use crate::comments;
use crate::entities::patch_proposals::{MergePolicy, Status};
use crate::entities::repositories::Model as Repository;
use crate::entities::users;
−use crate::markdown;
use crate::patches::{self, merge, store};
use crate::repositories::git;
−use crate::state_read;
−use crate::views;
−
−use super::readable_repository;
+use crate::{AppState, comments, markdown, state_read, views};
/// What the list page is asked to show.
#[derive(Debug, Default, Deserialize)]
struct PatchesQuery {
− /// The state filter: "all", "draft", "open", "closed", "merged", or empty for default.
+ /// The state filter: "all", "draft", "open", "closed", "merged", or empty
+ /// for default.
#[serde(default)]
state: String,
}
@@ -345,7 +341,8 @@ pub async fn get_patch(
}
.bare_path(&repo_root);
− // Check if the diff is unavailable (merged/closed before snapshots were implemented)
+ // Check if the diff is unavailable (merged/closed before snapshots were
+ // implemented)
let diff_unavailable_reason = if matches!(patch.status, Status::Merged | Status::Closed)
&& patch.snapshot_base_oid.is_none()
&& patch.snapshot_head_oid.is_none()
@@ -429,7 +426,8 @@ pub async fn get_patch(
is_remote: false,
});
views::CommentView {
− base_url: "", // filled later on in [`crate::views::comments::CommentListView`]
+ base_url: "", /* filled later on in
+ * [`crate::views::comments::CommentListView`] */
uuid: c.uuid.to_string(),
author,
content: c.content.clone(),
@@ -697,28 +695,29 @@ pub async fn post_edit_patch(
let old_resolve_url = patch.resolve_activity_url.clone();
// Determine what the new resolve_activity_url should be
− let new_resolve_url =
− if old_status != status && status == crate::entities::patch_proposals::Status::Closed {
− // Open→Closed: will emit Resolve, need to generate and store the URL
− let state_guard = state_read(&data).await;
− let federation_enabled = crate::activitypub::enabled(&state_guard);
− let is_public = repository.visibility.label() == "public";
− drop(state_guard);
+ let new_resolve_url = if old_status != status
+ && status == crate::entities::patch_proposals::Status::Closed
+ {
+ // Open→Closed: will emit Resolve, need to generate and store the URL
+ let state_guard = state_read(&data).await;
+ let federation_enabled = crate::activitypub::enabled(&state_guard);
+ let is_public = repository.visibility.label() == "public";
+ drop(state_guard);
− if federation_enabled && is_public {
− let state_guard = state_read(&data).await;
− let urls = crate::activitypub::ApUrls::new(state_guard.public_url());
− Some(urls.activity(&owner.name, Uuid::new_v4()))
− } else {
− None
− }
− } else if old_status != status && status == crate::entities::patch_proposals::Status::Open {
− // Closed→Open: will emit Undo(Resolve), clear the URL
− None
+ if federation_enabled && is_public {
+ let state_guard = state_read(&data).await;
+ let urls = crate::activitypub::ApUrls::new(state_guard.public_url());
+ Some(urls.activity(&owner.name, Uuid::new_v4()))
} else {
− // No status change or same status: keep existing value
− old_resolve_url.clone()
− };
+ None
+ }
+ } else if old_status != status && status == crate::entities::patch_proposals::Status::Open {
+ // Closed→Open: will emit Undo(Resolve), clear the URL
+ None
+ } else {
+ // No status change or same status: keep existing value
+ old_resolve_url.clone()
+ };
// Compute snapshot OIDs if closing the patch
let snapshot_oids =
@@ -1058,7 +1057,8 @@ pub async fn post_edit_patch(
}
};
− // Insert Offer into user's activities (no delivery - tracker is local)
+ // Insert Offer into user's activities (no delivery - tracker is
+ // local)
if let Err(e) = crate::activitypub::forge::activity::publish_activity(
&db,
user_actor.id,
@@ -1389,8 +1389,8 @@ pub async fn post_merge_patch(
}
merge::MergeResult::Conflict { reason } => {
// Redirect back to the patch page with an error message
− // For now, we'll just log and redirect. A better UX would pass the error
− // through flash messages or query parameters.
+ // For now, we'll just log and redirect. A better UX would pass the
+ // error through flash messages or query parameters.
tracing::warn!(
%reason,
owner = %owner.name,
@@ -1549,7 +1549,8 @@ pub async fn post_comment(
let create_json =
serde_json::to_string(&create_activity).unwrap_or_default();
− // Publish with user as signer, repo followers as delivery target
+ // Publish with user as signer, repo followers as
+ // delivery target
let comment_url = urls.repo_patch_comment(
&owner.name,
&repository.slug,
@@ -1735,7 +1736,8 @@ pub async fn post_edit_comment(
let edit_json = serde_json::to_string(&edit_activity).unwrap_or_default();
− // Publish with user as signer, repo followers as delivery target
+ // Publish with user as signer, repo followers as
+ // delivery target
if let Err(e) = crate::activitypub::forge::activity::publish_activity_with_separate_signer(
&db,
current.id, // signer = user actor
@@ -1864,7 +1866,8 @@ pub async fn post_delete_comment(
let delete_json = serde_json::to_string(&delete_activity).unwrap_or_default();
− // Publish with user as signer, repo followers as delivery target
+ // Publish with user as signer, repo followers as delivery
+ // target
if let Err(e) =
crate::activitypub::forge::activity::publish_activity_with_separate_signer(
&db,
@@ -1963,7 +1966,8 @@ async fn patch_with_comment_error(
}
.bare_path(&repo_root);
− // Check if the diff is unavailable (merged/closed before snapshots were implemented)
+ // Check if the diff is unavailable (merged/closed before snapshots were
+ // implemented)
let diff_unavailable_reason = if matches!(patch.status, Status::Merged | Status::Closed)
&& patch.snapshot_base_oid.is_none()
&& patch.snapshot_head_oid.is_none()
@@ -2045,7 +2049,8 @@ async fn patch_with_comment_error(
is_remote: false,
});
views::CommentView {
− base_url: "", // filled later on in [`crate::views::comments::CommentListView`]
+ base_url: "", /* filled later on in
+ * [`crate::views::comments::CommentListView`] */
uuid: c.uuid.to_string(),
author,
content: c.content.clone(),
frogue/src/webui/repositories/settings.rs+4−7
@@ -12,14 +12,10 @@ use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::error;
−use crate::AppState;
+use super::readable_repository;
use crate::auth::CurrentUser;
−use crate::repositories;
use crate::repositories::store;
−use crate::tickets;
−use crate::views;
−
−use super::readable_repository;
+use crate::{AppState, repositories, tickets, views};
/// What the settings form posts.
#[derive(Debug, Deserialize)]
@@ -418,7 +414,8 @@ pub async fn post_delete_label(
// Look up the label to verify it exists and belongs to this repository.
match tickets::store::find_label_by_id(&db, label_id).await {
Ok(Some(label)) if label.repository_id == repository.id => {
− // Label exists and belongs to this repository, proceed with deletion
+ // Label exists and belongs to this repository, proceed with
+ // deletion
}
Ok(_) => {
// The label does not exist or belongs to another repository:
frogue/src/webui/repositories/tickets.rs+14−13
@@ -17,23 +17,19 @@ use tokio::sync::RwLock;
use tracing::error;
use uuid::Uuid;
−use crate::AppState;
+use super::readable_repository;
use crate::auth::{CurrentUser, viewer_name};
−use crate::comments;
use crate::entities::repositories::Model as Repository;
use crate::entities::ticket_labels::Model as Label;
use crate::entities::users;
−use crate::markdown;
−use crate::state_read;
use crate::tickets::{self, store};
−use crate::views;
−
−use super::readable_repository;
+use crate::{AppState, comments, markdown, state_read, views};
/// What the list page is asked to show.
#[derive(Debug, Default, Deserialize)]
struct TicketsQuery {
− /// The state filter: "open", "closed", or absent for open tickets (the default).
+ /// The state filter: "open", "closed", or absent for open tickets (the
+ /// default).
state: Option<String>,
/// The labels to filter by, absent when none are selected.
labels: Option<String>,
@@ -820,7 +816,8 @@ pub async fn get_ticket(
is_remote: false,
});
views::CommentView {
− base_url: "", // filled later on in [`crate::views::comments::CommentListView`]
+ base_url: "", /* filled later on in
+ * [`crate::views::comments::CommentListView`] */
uuid: c.uuid.to_string(),
author,
content: c.content.clone(),
@@ -1782,7 +1779,8 @@ pub async fn post_comment(
let create_json =
serde_json::to_string(&create_activity).unwrap_or_default();
− // Publish with user as signer, repo followers as delivery target
+ // Publish with user as signer, repo followers as
+ // delivery target
let comment_url = urls.repo_ticket_comment(
&owner.name,
&repository.slug,
@@ -1968,7 +1966,8 @@ pub async fn post_edit_comment(
let edit_json = serde_json::to_string(&edit_activity).unwrap_or_default();
− // Publish with user as signer, repo followers as delivery target
+ // Publish with user as signer, repo followers as
+ // delivery target
if let Err(e) = crate::activitypub::forge::activity::publish_activity_with_separate_signer(
&db,
current.id, // signer = user actor
@@ -2097,7 +2096,8 @@ pub async fn post_delete_comment(
let delete_json = serde_json::to_string(&delete_activity).unwrap_or_default();
− // Publish with user as signer, repo followers as delivery target
+ // Publish with user as signer, repo followers as delivery
+ // target
if let Err(e) =
crate::activitypub::forge::activity::publish_activity_with_separate_signer(
&db,
@@ -2240,7 +2240,8 @@ async fn ticket_with_comment_error(
is_remote: false,
});
views::CommentView {
− base_url: "", // filled later on in [`crate::views::comments::CommentListView`]
+ base_url: "", /* filled later on in
+ * [`crate::views::comments::CommentListView`] */
uuid: c.uuid.to_string(),
author,
content: c.content.clone(),
frogue/src/webui/ssh_keys.rs+1−2
@@ -16,12 +16,11 @@ use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::error;
−use crate::AppState;
use crate::auth::CurrentUser;
use crate::entities::ssh_keys;
−use crate::state_read;
use crate::views::{self, SshKeyForm, SshKeySummary};
use crate::webui::repositories::acting_user;
+use crate::{AppState, state_read};
/// What the add-key form posts.
#[derive(Debug, Deserialize)]
frogue/src/webui/user.rs+5−5
@@ -2,20 +2,20 @@ use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, get, post, web};
use sea_orm::ActiveValue::Set;
−use sea_orm::{ActiveModelTrait, DatabaseConnection, DbErr, EntityTrait, PaginatorTrait};
−use sea_orm::{ColumnTrait, QueryFilter};
+use sea_orm::{
+ ActiveModelTrait, ColumnTrait, DatabaseConnection, DbErr, EntityTrait, PaginatorTrait,
+ QueryFilter,
+};
use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::error;
−use crate::AppState;
use crate::auth::{CurrentUser, viewer_name};
use crate::entities::prelude::*;
use crate::entities::users;
−use crate::state_read;
−use crate::views;
use crate::views::profile::ActorProfile;
use crate::webui::repositories;
+use crate::{AppState, state_read, views};
/// What the profile editing form posts.
#[derive(Debug, Deserialize)]
frogue/tests/activitypub.rs+6−3
@@ -1,7 +1,8 @@
//! ActivityPub actor and webfinger integration tests.
//!
//! These tests verify the Phase 1 deliverables: actor JSON documents, webfinger
−//! discovery, lazy keypair generation, and the federation gate (disabled → 404).
+//! discovery, lazy keypair generation, and the federation gate (disabled →
+//! 404).
mod support;
@@ -112,7 +113,8 @@ async fn actor_json_has_correct_fields() -> miette::Result<()> {
Ok(())
}
−/// Test 2: Same request without the Accept header → HTML profile (existing behaviour).
+/// Test 2: Same request without the Accept header → HTML profile (existing
+/// behaviour).
#[tokio::test]
async fn actor_without_accept_header_returns_html() -> miette::Result<()> {
let port = free_port();
@@ -256,7 +258,8 @@ async fn unknown_user_actor_returns_404() -> miette::Result<()> {
Ok(())
}
−/// Test 6: Non-federated spawn (no FROGUE_PUBLIC_URL) → actor and webfinger → 404.
+/// Test 6: Non-federated spawn (no FROGUE_PUBLIC_URL) → actor and webfinger →
+/// 404.
#[tokio::test]
async fn non_federated_spawn_returns_404() -> miette::Result<()> {
let port = free_port();
frogue/tests/comments.rs+4−2
@@ -2,7 +2,8 @@
//!
//! Tests the full stack: HTTP endpoints, database, views.
//! Comments can be created, edited, and deleted on both tickets and patches.
−//! Permissions: author or repo owner can edit/delete, anyone logged in can comment.
+//! Permissions: author or repo owner can edit/delete, anyone logged in can
+//! comment.
mod support;
@@ -455,7 +456,8 @@ async fn empty_comment_is_refused() -> miette::Result<()> {
&format!("/~alice/test-repo/tickets/{ticket_num}/comments"),
&[("content", "")],
);
− // Refused comments re-render the detail page with the error shown (200 status)
+ // Refused comments re-render the detail page with the error shown (200
+ // status)
assert_eq!(
response.status, 200,
"empty comment should be refused with page re-rendered\n{}",
frogue/tests/commit_diff.rs+2−1
@@ -108,7 +108,8 @@ fn assert_ok(output: &Output, what: &str) {
);
}
−/// Fills `owner/slug`'s bare repository with commits that have actual file changes.
+/// Fills `owner/slug`'s bare repository with commits that have actual file
+/// changes.
fn populate_with_diffs(server: &Server, scratch: &Scratch, owner: &str, slug: &str) {
let bare = server.repo_root().join(owner).join(format!("{slug}.git"));
frogue/tests/compare.rs+4−2
@@ -1,4 +1,5 @@
−//! The compare view: shows the diff between two revisions (branches, tags, or commits).
+//! The compare view: shows the diff between two revisions (branches, tags, or
+//! commits).
mod support;
@@ -111,7 +112,8 @@ fn assert_ok(output: &Output, what: &str) {
/// Fills `owner/slug`'s bare repository with a diverged history:
/// - c1 "Add readme": readme.txt = "hello\n" (main)
/// - c2 "Add a line": append "world\n" (main); lightweight tag `v1` at c2
−/// - branch `feat` at c2: c3 "Fork change" (append "from feat\n" to readme.txt), c4 "Add feature file" (feature.txt = "feature\n")
+/// - branch `feat` at c2: c3 "Fork change" (append "from feat\n" to
+/// readme.txt), c4 "Add feature file" (feature.txt = "feature\n")
/// - back on main: c5 "Main moves on" (main_only.txt = "only on main\n")
/// - branch `feature/x` at c5: c6 "Slashed branch change" (slash.txt)
/// - `push --all` + `push --tags`
frogue/tests/diff_snapshot.rs+2−1
@@ -664,7 +664,8 @@ fn snapshot_captures_exact_state() {
"bob pushing updated to refs/patches",
);
− // The patch page should still show the original merged content, not the new changes
+ // The patch page should still show the original merged content, not the new
+ // changes
let patch_page = alice.get("/~alice/test-repo/patches/1");
assert_eq!(patch_page.status, 200);
assert!(patch_page.body.contains("original content"));
frogue/tests/discovery.rs+4−2
@@ -1,4 +1,5 @@
−//! Phase 1 tests: Remote actor discovery via webfinger (now integrated with follow)
+//! Phase 1 tests: Remote actor discovery via webfinger (now integrated with
+//! follow)
mod support;
@@ -37,7 +38,8 @@ async fn discover_by_handle_succeeds() -> miette::Result<()> {
let recorder_port = free_port();
let _recorder = RemoteActor::serve(recorder_port);
− // Alice follows bob@127.0.0.1:recorder_port (discovery happens automatically)
+ // Alice follows bob@127.0.0.1:recorder_port (discovery happens
+ // automatically)
let handle = format!("remote@127.0.0.1:{recorder_port}");
// POST to follow endpoint with handle (discovery + follow in one step)
frogue/tests/federation.rs+2−1
@@ -968,7 +968,8 @@ async fn remote_actor_cache_populates_name() -> miette::Result<()> {
"should have received Accept(Follow) within deadline"
);
− // Connect to the database and verify the remote actor was cached with name and preferred_username
+ // Connect to the database and verify the remote actor was cached with name
+ // and preferred_username
let db = Database::connect(server.database_url())
.await
.expect("should connect to database");
frogue/tests/file_view.rs+3−2
@@ -136,7 +136,7 @@ fn populate(server: &Server, scratch: &Scratch, owner: &str, slug: &str) -> (Str
// Bytes no text file would hold, which is what makes it binary.
fs::write(
work.join("blob.bin"),
− [0x00u8, 0x01, 0x02, 0xFF, 0x00, 0x7F],
+ [0x00u8, 0x01, 0x02, 0xff, 0x00, 0x7f],
)
.expect("writing a file");
@@ -331,7 +331,8 @@ async fn a_binary_file_is_said_to_be_binary() -> miette::Result<()> {
"the page should say the file is binary, got:\n{}",
response.body
);
− // Check that the empty file message section doesn't have syntax highlighting spans
+ // Check that the empty file message section doesn't have syntax
+ // highlighting spans
let empty_section = response
.body
.split(r#"<p class="empty">"#)
frogue/tests/home_feed.rs+4−2
@@ -208,7 +208,8 @@ async fn anonymous_user_sees_empty_home_page() -> miette::Result<()> {
let response = anon_client.get("/");
assert_eq!(response.status, 200, "home page should load for anonymous");
// Should not contain feed-related markup
− // (We can't check the absence of everything, but we can check it doesn't crash)
+ // (We can't check the absence of everything, but we can check it doesn't
+ // crash)
Ok(())
}
@@ -709,7 +710,8 @@ fn extract_remote_note_id_from_feed(html: &str, _object_url_suffix: &str) -> Opt
None
}
−/// Test: Feed pagination - 21 notes shows 20 on page 0 with next link, 21st on page 1 with prev link
+/// Test: Feed pagination - 21 notes shows 20 on page 0 with next link, 21st on
+/// page 1 with prev link
#[tokio::test]
async fn feed_pagination_works() -> miette::Result<()> {
let port = free_port();
frogue/tests/nodeinfo.rs+3−4
@@ -7,13 +7,12 @@
mod support;
use chrono::Utc;
−use miette::IntoDiagnostic;
−use sea_orm::{ActiveModelTrait, Database, Set};
−use support::{Client, Server, free_port};
−
use frogue::entities::patch_proposals::{MergePolicy, Status};
use frogue::entities::tickets::State;
use frogue::entities::{comments, patch_proposals, repositories, tickets, users};
+use miette::IntoDiagnostic;
+use sea_orm::{ActiveModelTrait, Database, Set};
+use support::{Client, Server, free_port};
/// Test 1: JDD at /.well-known/nodeinfo has correct structure
#[tokio::test]
frogue/tests/patch_federation.rs+2−2
@@ -1,7 +1,7 @@
//! Tests for ForgeFed patch proposal federation.
//!
−//! These tests verify that patch proposal activities (Offer, Add, Resolve, Push)
−//! are correctly emitted to repository followers.
+//! These tests verify that patch proposal activities (Offer, Add, Resolve,
+//! Push) are correctly emitted to repository followers.
mod support;
frogue/tests/patches.rs+12−6
@@ -1001,7 +1001,8 @@ async fn creator_and_owner_can_force_push_patch() -> miette::Result<()> {
Ok(())
}
−/// Test 5: Creator deletes refs/patches/1 → PP closed. Stranger deletes → refused.
+/// Test 5: Creator deletes refs/patches/1 → PP closed. Stranger deletes →
+/// refused.
#[tokio::test]
async fn creator_can_delete_patch_stranger_cannot() -> miette::Result<()> {
skip_without_tooling!();
@@ -1294,7 +1295,8 @@ async fn owner_can_push_branches_normally() -> miette::Result<()> {
Ok(())
}
−/// Test 7: Private repo, stranger → refused at connection level (existing gate).
+/// Test 7: Private repo, stranger → refused at connection level (existing
+/// gate).
#[tokio::test]
async fn stranger_cannot_push_to_private_repo() -> miette::Result<()> {
skip_without_tooling!();
@@ -1337,7 +1339,8 @@ async fn stranger_cannot_push_to_private_repo() -> miette::Result<()> {
Ok(())
}
−/// Test 8: Mixed push (branch + patch ref) by a stranger → whole push refused by pre-receive.
+/// Test 8: Mixed push (branch + patch ref) by a stranger → whole push refused
+/// by pre-receive.
#[tokio::test]
async fn stranger_cannot_mix_branch_and_patch_push() -> miette::Result<()> {
skip_without_tooling!();
@@ -1802,7 +1805,8 @@ fn commit_with_change_id(
commit_hash
}
−/// Test 1: Pushing a commit with a change-id to refs/patches creates a PP with that change-id.
+/// Test 1: Pushing a commit with a change-id to refs/patches creates a PP with
+/// that change-id.
#[tokio::test]
async fn push_with_change_id_creates_pp_with_change_id() -> miette::Result<()> {
skip_without_tooling!();
@@ -1940,7 +1944,8 @@ async fn push_with_change_id_creates_pp_with_change_id() -> miette::Result<()> {
Ok(())
}
−/// Test 2: Amending with the same change-id updates the existing PP instead of creating a new one.
+/// Test 2: Amending with the same change-id updates the existing PP instead of
+/// creating a new one.
#[tokio::test]
async fn amend_with_same_change_id_updates_existing_pp() -> miette::Result<()> {
skip_without_tooling!();
@@ -2262,7 +2267,8 @@ async fn different_change_id_creates_independent_pp() -> miette::Result<()> {
Ok(())
}
−/// Test 4: Pushing a commit with a mismatched change-id to refs/patches/{n} is refused.
+/// Test 4: Pushing a commit with a mismatched change-id to refs/patches/{n} is
+/// refused.
#[tokio::test]
async fn push_mismatched_change_id_to_numbered_ref_is_refused() -> miette::Result<()> {
skip_without_tooling!();
frogue/tests/patches_web.rs+7−5
@@ -447,8 +447,9 @@ async fn only_owner_can_merge_patch() -> miette::Result<()> {
response.body
);
− // Alice (owner) can merge (will fail because no actual git refs, but should be 403 for others)
− // For now, just test that the endpoint exists and rejects non-owners
+ // Alice (owner) can merge (will fail because no actual git refs, but should
+ // be 403 for others) For now, just test that the endpoint exists and
+ // rejects non-owners
Ok(())
}
@@ -490,10 +491,11 @@ async fn owner_can_merge_a_patch() -> miette::Result<()> {
)
.await;
− // Alice (owner) can attempt to merge (will fail because no actual git refs exist)
− // but should get a redirect or error, not 403
+ // Alice (owner) can attempt to merge (will fail because no actual git refs
+ // exist) but should get a redirect or error, not 403
let response = alice.post_form(&format!("/~alice/test-repo/patches/1/merge"), &[]);
− // The merge will fail because there are no actual git refs, but it should not be 403
+ // The merge will fail because there are no actual git refs, but it should
+ // not be 403
assert_ne!(
response.status, 403,
"owner should be allowed to merge, got: {}",
frogue/tests/remote_notes.rs+2−2
@@ -249,8 +249,8 @@ async fn create_note_xss_is_sanitized() -> miette::Result<()> {
"Create(Note) with XSS should be accepted. Server log:"
);
− // We can't directly query the DB, but the test verifies the endpoint accepts it
− // The actual sanitization is tested in the markdown module
+ // We can't directly query the DB, but the test verifies the endpoint
+ // accepts it The actual sanitization is tested in the markdown module
Ok(())
}
frogue/tests/remote_reactions.rs+4−3
@@ -347,7 +347,8 @@ async fn announce_returns_404_when_federation_disabled() -> miette::Result<()> {
// In a real scenario, we'd need to test with federation disabled
// For now, we'll just verify the endpoint exists
let response = alice_client.post_form("/remote-notes/1/announce", &[]);
− // Should get 404 (not found) or 303 (redirect) depending on whether note exists
+ // Should get 404 (not found) or 303 (redirect) depending on whether note
+ // exists
assert!(
response.status == 404 || response.status == 303,
"endpoint should exist. Body:\n{}",
@@ -582,8 +583,8 @@ async fn non_owner_cannot_react_to_remote_note() -> miette::Result<()> {
let remote_note_id = post_remote_note(&mut alice_client, port, &recorder, recorder_port);
// Bob tries to react to Alice's remote note (should fail)
− // Note: The endpoint is /remote-notes/{id}/react, which doesn't have a user prefix
− // So Bob can try to react to it
+ // Note: The endpoint is /remote-notes/{id}/react, which doesn't have a user
+ // prefix So Bob can try to react to it
let response = bob_client.post_form(&format!("/remote-notes/{remote_note_id}/react"), &[]);
// Bob should be able to react (it's his own reaction)
frogue/tests/repo_federation.rs+7−5
@@ -403,8 +403,9 @@ async fn commit_document_is_served() -> miette::Result<()> {
register(&mut alice_client, "alice");
create_repository(&mut alice_client, "alice", "commits", "public");
− // For now, we'll just test that the endpoint exists and returns 404 for unknown commits
− // In the future, we'll push a real commit and verify it's served
+ // For now, we'll just test that the endpoint exists and returns 404 for
+ // unknown commits In the future, we'll push a real commit and verify
+ // it's served
let response = alice_client.get_with(
"/~alice/commits/commits/abc123def456",
&[("Accept", "application/activity+json")],
@@ -679,7 +680,8 @@ fn assert_ok(output: &std::process::Output, what: &str) {
}
/// Test 9: HTML content negotiation regression test
−/// Verifies that requesting a repository page with Accept: text/html returns HTML, not JSON.
+/// Verifies that requesting a repository page with Accept: text/html returns
+/// HTML, not JSON.
#[tokio::test]
async fn repo_page_still_html_for_browsers() -> miette::Result<()> {
let port = free_port();
@@ -716,8 +718,8 @@ async fn repo_page_still_html_for_browsers() -> miette::Result<()> {
}
/// Test 10: Federation-disabled regression test
−/// Verifies that when federation is disabled (no FROGUE_PUBLIC_URL), all repository
−/// ActivityPub endpoints return 404.
+/// Verifies that when federation is disabled (no FROGUE_PUBLIC_URL), all
+/// repository ActivityPub endpoints return 404.
#[tokio::test]
async fn federation_disabled_repo_endpoints_404() -> miette::Result<()> {
let port = free_port();
frogue/tests/rfc9421.rs+2−1
@@ -24,7 +24,8 @@ fn register(client: &mut Client, name: &str) {
);
}
−/// Test: RemoteActor follows alice using RFC 9421 signature → Accept(Follow) is delivered
+/// Test: RemoteActor follows alice using RFC 9421 signature → Accept(Follow) is
+/// delivered
#[tokio::test]
async fn rfc9421_follow_is_accepted() -> miette::Result<()> {
let port = free_port();
frogue/tests/settings.rs+2−1
@@ -104,7 +104,8 @@ async fn the_settings_page_needs_an_owner() -> miette::Result<()> {
"the owner should reach the settings page"
);
− // The settings page has the repository tab bar, with Settings marked as current.
+ // The settings page has the repository tab bar, with Settings marked as
+ // current.
let body = &response.body;
assert!(
body.contains(r#"<a href="/~alice/test-repo">Summary</a>"#),
frogue/tests/ssh_throttling.rs+2−1
@@ -3,7 +3,8 @@
//! These tests verify that connection limits and timeouts work correctly.
//!
//! TODO: write an helper to check if stream is closed.
−//! TODO: still need to write more accurate tests, but my guess is that we would need to hook into russh. Maybe improve the tests with some timers?
+//! TODO: still need to write more accurate tests, but my guess is that we would
+//! need to hook into russh. Maybe improve the tests with some timers?
mod support;
frogue/tests/support/ap.rs+3−3
@@ -12,11 +12,10 @@ use std::thread;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use chrono::Utc;
+use frogue::activitypub::keys;
use serde_json::json;
use sha2::{Digest, Sha256};
−use frogue::activitypub::keys;
−
/// A recorded inbox delivery.
#[derive(Debug, Clone)]
pub struct Delivery {
@@ -854,7 +853,8 @@ fn handle_connection(
let _ = stream.write_all(response.as_bytes());
}
−/// Handle a connection with configurable failures for the first N POST requests.
+/// Handle a connection with configurable failures for the first N POST
+/// requests.
fn handle_connection_with_failures(
mut stream: TcpStream,
actor_json: &str,
frogue/tests/support/mod.rs+14−13
@@ -7,19 +7,17 @@
#![allow(dead_code)]
pub mod ap;
−use miette::IntoDiagnostic;
−
−use frogue::config::*;
−
use std::collections::HashSet;
−use std::fs;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::{Mutex, OnceLock};
−use std::thread;
use std::time::{Duration, Instant};
+use std::{fs, thread};
+
+use frogue::config::*;
+use miette::IntoDiagnostic;
const STARTUP_TIMEOUT: Duration = Duration::from_secs(15);
@@ -80,8 +78,8 @@ impl Server {
Self::spawn_in(bind_addr, None, None, &[])
}
− /// Like [`Server::spawn`], but sets additional environment variables for the
− /// spawned process.
+ /// Like [`Server::spawn`], but sets additional environment variables for
+ /// the spawned process.
pub fn spawn_with_env(bind_addr: &str, env: &[(&str, &str)]) -> Self {
Self::spawn_in(bind_addr, None, None, env)
}
@@ -116,8 +114,8 @@ impl Server {
public_url: Option<String>,
env: &[(&str, &str)],
) -> Self {
− // A database of its own, so the tests never touch a running dev server's
− // database.
+ // A database of its own, so the tests never touch a running dev
+ // server's database.
let db = std::env::temp_dir().join(format!("frogue-test-{}.sqlite", free_port()));
let database_url = format!("sqlite://{}?mode=rwc", db.display());
@@ -311,12 +309,14 @@ impl Client {
self.send(&format!("GET {path} HTTP/1.1"), None, headers)
}
− /// `GET`s `path` with `Accept: application/activity+json` for ActivityPub content negotiation.
+ /// `GET`s `path` with `Accept: application/activity+json` for ActivityPub
+ /// content negotiation.
pub fn get_with_accept(&mut self, path: &str) -> Response {
self.get_with(path, &[("Accept", "application/activity+json")])
}
− /// `GET`s `path` with `Accept: application/activity+json` and parses the JSON response.
+ /// `GET`s `path` with `Accept: application/activity+json` and parses the
+ /// JSON response.
pub fn get_ap(&mut self, path: &str) -> miette::Result<serde_json::Value> {
let response = self.get_with_accept(path);
let body = &response.body;
@@ -349,7 +349,8 @@ impl Client {
)
}
− /// `POST`s `body` as JSON with fully custom headers (no default Content-Type).
+ /// `POST`s `body` as JSON with fully custom headers (no default
+ /// Content-Type).
pub fn post_json_with_headers(
&mut self,
path: &str,
frogue/tests/ticket_federation.rs+31−17
@@ -5,6 +5,7 @@ mod support;
use std::fs;
use std::path::Path;
use std::process::{Command, Output};
+
use support::{Server, free_port};
/// Register a user through the web form.
@@ -226,7 +227,8 @@ fn closed_ticket_is_resolved() -> miette::Result<()> {
ticket["isResolved"], true,
"isResolved should be true for closed tickets"
);
− // resolvedBy is set to the repo owner when closed (we don't track who specifically closed it)
+ // resolvedBy is set to the repo owner when closed (we don't track who
+ // specifically closed it)
assert!(
ticket["resolvedBy"].is_string(),
"resolvedBy should be present for closed tickets"
@@ -301,8 +303,9 @@ fn patch_document_has_correct_fields() -> miette::Result<()> {
let response = client.get_with_accept("/~alice/testrepo/patches/1");
assert_eq!(response.status, 404, "non-existent patch should return 404");
− // TODO: Once we have a way to create patches via the API, test the full document structure
− // For now, this test just verifies the route exists and handles 404 correctly
+ // TODO: Once we have a way to create patches via the API, test the full
+ // document structure For now, this test just verifies the route exists
+ // and handles 404 correctly
Ok(())
}
@@ -352,8 +355,8 @@ fn ticket_html_vs_json_ld_content_negotiation() -> miette::Result<()> {
// Phase 3: Outbound federation tests
// ============================================================================
−/// Test that creating a ticket emits an Accept activity to repository followers.
−/// This verifies the Offer+Accept flow for local ticket creation.
+/// Test that creating a ticket emits an Accept activity to repository
+/// followers. This verifies the Offer+Accept flow for local ticket creation.
#[test]
fn ticket_creation_emits_accept_to_followers() -> miette::Result<()> {
use support::ap::RemoteActor;
@@ -582,7 +585,8 @@ fn fetch_first_comment_url(
.to_string()
}
−/// Helper to fetch a comment Note document with ActivityPub content negotiation.
+/// Helper to fetch a comment Note document with ActivityPub content
+/// negotiation.
fn fetch_comment_ap(
client: &mut support::Client,
port: u16,
@@ -1284,7 +1288,8 @@ fn remote_ticket_submission_creates_pending_offer() -> miette::Result<()> {
let remote_port = free_port();
let remote_repo = RemoteActor::serve(remote_port);
− // Serve a Repository actor document with ticketsTrackedBy pointing to itself
+ // Serve a Repository actor document with ticketsTrackedBy pointing to
+ // itself
let repo_url = format!("http://127.0.0.1:{remote_port}/repo");
let tracker_url = format!("http://127.0.0.1:{remote_port}/tracker");
@@ -1375,7 +1380,8 @@ fn remote_ticket_submission_creates_pending_offer() -> miette::Result<()> {
Ok(())
}
−/// Test that when a remote tracker Accepts our Offer, the pending ticket is marked accepted.
+/// Test that when a remote tracker Accepts our Offer, the pending ticket is
+/// marked accepted.
#[test]
fn remote_ticket_acceptance_updates_status() -> miette::Result<()> {
use support::ap::RemoteActor;
@@ -1487,7 +1493,8 @@ fn remote_ticket_acceptance_updates_status() -> miette::Result<()> {
Ok(())
}
−/// Test that when a remote tracker Rejects our Offer, the pending ticket is marked rejected.
+/// Test that when a remote tracker Rejects our Offer, the pending ticket is
+/// marked rejected.
#[test]
fn remote_ticket_rejection_updates_status() -> miette::Result<()> {
use support::ap::RemoteActor;
@@ -1588,7 +1595,8 @@ fn remote_ticket_rejection_updates_status() -> miette::Result<()> {
Ok(())
}
−/// Test that our own repository actor document includes tracksTicketsFor and tracksPatchesFor = [self].
+/// Test that our own repository actor document includes tracksTicketsFor and
+/// tracksPatchesFor = [self].
#[test]
fn repo_actor_includes_tracks_fields() -> miette::Result<()> {
let port = free_port();
@@ -1655,7 +1663,8 @@ fn repo_actor_includes_tracks_fields() -> miette::Result<()> {
Ok(())
}
−/// Test that remote ticket submission is rejected when the tracker doesn't advertise tracksTicketsFor.
+/// Test that remote ticket submission is rejected when the tracker doesn't
+/// advertise tracksTicketsFor.
#[test]
fn remote_ticket_rejected_when_tracker_lacks_back_reference() -> miette::Result<()> {
use support::ap::RemoteActor;
@@ -1667,7 +1676,8 @@ fn remote_ticket_rejected_when_tracker_lacks_back_reference() -> miette::Result<
let mut client = support::Client::new(port);
register(&mut client, "alice");
− // Set up a remote repository with a separate tracker that doesn't advertise tracksTicketsFor
+ // Set up a remote repository with a separate tracker that doesn't advertise
+ // tracksTicketsFor
let remote_port = free_port();
let remote_repo = RemoteActor::serve(remote_port);
@@ -1807,7 +1817,8 @@ fn remote_patch_requires_origin_fields() -> miette::Result<()> {
Ok(())
}
−/// Test that outbound patch submission generates a Patch object with patch collection.
+/// Test that outbound patch submission generates a Patch object with patch
+/// collection.
#[test]
fn remote_patch_submission_generates_patch_object() -> miette::Result<()> {
let port = free_port();
@@ -2013,7 +2024,8 @@ fn assert_ok(output: &Output, what: &str) {
);
}
−/// Test that remote ticket creators are displayed with remote badge and link in WebUI.
+/// Test that remote ticket creators are displayed with remote badge and link in
+/// WebUI.
#[test]
fn remote_ticket_author_displayed_with_badge() -> miette::Result<()> {
use support::ap::RemoteActor;
@@ -2055,8 +2067,9 @@ fn remote_ticket_author_displayed_with_badge() -> miette::Result<()> {
response.body
);
− // Verify the author name is present (should be the remote actor's preferred_username or display name)
− // The RemoteActor serves a default actor with name "Remote User"
+ // Verify the author name is present (should be the remote actor's
+ // preferred_username or display name) The RemoteActor serves a default
+ // actor with name "Remote User"
assert!(
response.body.contains("Remote User") || response.body.contains("remote"),
"ticket list should show remote author name: {}",
@@ -2077,7 +2090,8 @@ fn remote_ticket_author_displayed_with_badge() -> miette::Result<()> {
Ok(())
}
−/// Test that remote comment authors are displayed with remote badge and link in WebUI.
+/// Test that remote comment authors are displayed with remote badge and link in
+/// WebUI.
#[test]
fn remote_comment_author_displayed_with_badge() -> miette::Result<()> {
use support::ap::RemoteActor;
frogue/tests/tickets.rs+2−2
@@ -50,8 +50,8 @@ fn create(client: &mut Client, owner: &str, slug: &str, visibility: &str) {
);
}
−/// Opens a ticket with the state and labels given, and returns the answer to read the
−/// address it landed on out of.
+/// Opens a ticket with the state and labels given, and returns the answer to
+/// read the address it landed on out of.
fn open(
client: &mut Client,
owner: &str,
frogue/tests/tree_browser.rs+4−2
@@ -122,7 +122,8 @@ fn populate(server: &Server, scratch: &Scratch, owner: &str, slug: &str) -> Stri
fs::write(work.join("README.md"), "# Notes\n").expect("writing a file");
fs::write(work.join("notes.txt"), "plain notes\n").expect("writing a file");
fs::write(work.join("docs/guide.md"), "# Guide\n").expect("writing a file");
− // A markdown file with relative links to verify they get rewritten to tree URLs
+ // A markdown file with relative links to verify they get rewritten to tree
+ // URLs
fs::write(
work.join("docs/index.md"),
"# Index\n\nSee [the guide](./guide.md) and [the readme](../README.md).\n",
@@ -506,7 +507,8 @@ async fn relative_links_in_markdown_are_rewritten_to_tree_urls() -> miette::Resu
create(&mut alice, "alice", "test-repo", "public");
populate(&server, &scratch, "alice", "test-repo");
− // When viewing a markdown file, relative links should be rewritten to tree URLs
+ // When viewing a markdown file, relative links should be rewritten to tree
+ // URLs
let response = alice.get("/~alice/test-repo/tree/main/docs/index.md");
assert_eq!(response.status, 200, "markdown file");
let body = &response.body;
migration/src/m20261019_000007_create_patch_proposals.rs+4−2
@@ -92,7 +92,8 @@ impl MigrationTrait for Migration {
)
.await?;
− // Add unique constraint for (repository_id, number) to ensure patch numbers are unique per repository
+ // Add unique constraint for (repository_id, number) to ensure patch
+ // numbers are unique per repository
manager
.create_index(
Index::create()
@@ -105,7 +106,8 @@ impl MigrationTrait for Migration {
)
.await?;
− // Add unique constraint for (repository_id, source_ref) to ensure source refs are unique per repository
+ // Add unique constraint for (repository_id, source_ref) to ensure
+ // source refs are unique per repository
manager
.create_index(
Index::create()
migration/src/m20261021_000009_add_change_id_to_patch_proposals.rs+6−4
@@ -8,15 +8,17 @@ impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Add the `change_id` column to `patch_proposals`: a jj change-id that
// survives rewrites, so pushes to `refs/patches` for the same logical
− // change update the existing patch proposal rather than creating a new one.
+ // change update the existing patch proposal rather than creating a new
+ // one.
manager
.get_connection()
.execute_unprepared("ALTER TABLE patch_proposals ADD COLUMN change_id varchar(32)")
.await?;
− // Unique per (repository_id, change_id) — NULLs never collide in SQLite's
− // partial unique index (only non-NULL rows participate), so plain-git
− // patch proposals with no change-id remain unaffected.
+ // Unique per (repository_id, change_id) — NULLs never collide in
+ // SQLite's partial unique index (only non-NULL rows
+ // participate), so plain-git patch proposals with no change-id
+ // remain unaffected.
manager
.get_connection()
.execute_unprepared(
migration/src/m20261022_000010_add_state_to_tickets.rs+3−2
@@ -6,8 +6,9 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
− // Add the `state` column to `tickets`: Open or Closed, defaults to Open.
− // This replaces the old system where "open" and "closed" were labels.
+ // Add the `state` column to `tickets`: Open or Closed, defaults to
+ // Open. This replaces the old system where "open" and "closed"
+ // were labels.
manager
.get_connection()
.execute_unprepared(
migration/src/m20261023_000012_create_ap_remote_actors.rs+3−2
@@ -25,8 +25,9 @@ impl MigrationTrait for Migration {
)
.await?;
− // Follower relationships: which remote actors follow which local actors.
− // follow_activity_id is the ActivityPub Follow activity's id, for Undo matching.
+ // Follower relationships: which remote actors follow which local
+ // actors. follow_activity_id is the ActivityPub Follow
+ // activity's id, for Undo matching.
manager
.get_connection()
.execute_unprepared(
migration/src/m20261025_000016_create_invites_table.rs+2−1
@@ -61,7 +61,8 @@ impl MigrationTrait for Migration {
)
.await?;
− // Add invited_by column to users table (without FK constraint - SQLite limitation)
+ // Add invited_by column to users table (without FK constraint - SQLite
+ // limitation)
manager
.alter_table(
Table::alter()
migration/src/m20261025_000017_create_ap_following.rs+5−4
@@ -6,10 +6,11 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
− // Following relationships: which local actors follow which remote actors.
− // This is the opposite direction of ap_followers.
− // follow_activity_id is our Follow activity's id, for matching Accept/Reject.
− // state tracks whether the follow is pending, accepted, or rejected.
+ // Following relationships: which local actors follow which remote
+ // actors. This is the opposite direction of ap_followers.
+ // follow_activity_id is our Follow activity's id, for matching
+ // Accept/Reject. state tracks whether the follow is pending,
+ // accepted, or rejected.
manager
.get_connection()
.execute_unprepared(
migration/src/m20261025_000018_create_ap_remote_notes.rs+2−1
@@ -7,7 +7,8 @@ pub struct Migration;
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Store remote notes from actors we follow
− // object_url is the canonical ActivityPub ID (unique to prevent duplicates)
+ // object_url is the canonical ActivityPub ID (unique to prevent
+ // duplicates)
manager
.get_connection()
.execute_unprepared(
migration/src/m20261025_000020_federate_tickets.rs+7−4
@@ -7,7 +7,8 @@ pub struct Migration;
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// SQLite cannot drop NOT NULL constraints, so we rebuild the table.
− // This follows the pattern from migration 000019 (ap_reactions rebuild).
+ // This follows the pattern from migration 000019 (ap_reactions
+ // rebuild).
//
// Key changes:
// - creator_id becomes nullable (for remote-created tickets)
@@ -16,8 +17,9 @@ impl MigrationTrait for Migration {
// - Add resolve_activity_url (for Undo(Resolve) correlation)
// - CHECK constraint: exactly one of creator_id or remote_actor_id
//
− // The FTS5 triggers must be dropped before the rebuild and recreated after.
− // The ticket_search FTS index is rebuilt from the new table.
+ // The FTS5 triggers must be dropped before the rebuild and recreated
+ // after. The ticket_search FTS index is rebuilt from the new
+ // table.
manager
.get_connection()
.execute_unprepared(
@@ -101,7 +103,8 @@ INSERT INTO ticket_search(ticket_search) VALUES('rebuild');
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
− // Revert to original schema (creator_id NOT NULL, no federation columns)
+ // Revert to original schema (creator_id NOT NULL, no federation
+ // columns)
manager
.get_connection()
.execute_unprepared(
migration/src/m20261025_000022_federate_comments.rs+2−1
@@ -12,7 +12,8 @@ impl MigrationTrait for Migration {
// - author_id becomes nullable (for remote-authored comments)
// - Add remote_actor_id (FK to ap_remote_actors, nullable)
// - Add remote_object_id (unique, for idempotency on inbound Notes)
− // - Add content_format (default 'markdown', can be 'html' for remote content)
+ // - Add content_format (default 'markdown', can be 'html' for remote
+ // content)
// - CHECK constraint: exactly one of author_id or remote_actor_id
manager
.get_connection()
migration/src/m20261025_000023_create_ap_remote_tickets.rs+6−4
@@ -6,12 +6,14 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
− // Table for tracking outbound Offers to remote trackers (round-trip client side).
+ // Table for tracking outbound Offers to remote trackers (round-trip
+ // client side).
//
// When a local user opens a ticket or merge request on a remote forge,
− // we create a row here with status 'pending', record the Offer activity,
− // and deliver it. When the remote tracker Accepts or Rejects, we update
− // the status and (for Accept) the assigned_url from the Accept.result field.
+ // we create a row here with status 'pending', record the Offer
+ // activity, and deliver it. When the remote tracker Accepts or
+ // Rejects, we update the status and (for Accept) the
+ // assigned_url from the Accept.result field.
manager
.create_table(
Table::create()