This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

knot2: create fork pull request links on push

Signed-off-by: Will <did:plc:dadhhalkfcq3gucaq25hjqon>

author did:plc:dadhhalkfcq3gucaq25hjq… committer
Tangled
date (Jul 30, 2026, 4:10 PM UTC) commit 7ee7f4b2 parent 1a044ae3 change-id spzluzkw
+99 -17
+66 -15
knot2/crates/knot-postreceive/src/lib.rs
··· 8 8 use knot_messages::{CiLogsKey, PushMessages, UrlKey}; 9 9 use knot_types::{ 10 10 AccountDid, AppviewEndpoint, BranchName, ChangedFiles, CiLogsAddr, Email, Handle, Listing, Oid, 11 - OwnerDid, PushOptions, RefName, RefTransition, RepoDid, RepoPath, RepoRkey, 11 + OriginUrl, OwnerDid, PushOptions, RefName, RefTransition, RepoDid, RepoPath, RepoRkey, 12 12 }; 13 13 use knot_workflow::{Compiled, RawWorkflow, Trigger, WorkflowName}; 14 14 use url::Url; ··· 125 125 126 126 let pull_link = match (transition, context.pull) { 127 127 (RefTransition::Create { .. }, Some(link)) => { 128 - pull_request_message(repo, link, name, context.messages).unwrap_or_default() 128 + pull_request_message(repo, link, name, context.messages, &context.actor.repo) 129 + .unwrap_or_default() 129 130 } 130 131 _ => Vec::new(), 131 132 }; ··· 166 167 link: &PullLink, 167 168 name: &RefName, 168 169 messages: &PushMessages, 170 + repo_did: &RepoDid, 169 171 ) -> Option<Vec<String>> { 170 172 let branch = branch_short(name)?; 171 173 let default_ref = repo.default_branch()?; ··· 174 176 return None; 175 177 } 176 178 repo.find_ref(&default_ref).ok().flatten()?; 177 - if repo.origin_url().is_some() { 178 - return None; 179 - } 180 - let url = pull_url( 181 - &link.appview, 182 - &link.owner, 183 - &link.rkey, 184 - &SourceBranch(branch), 185 - &TargetBranch(default), 186 - )?; 179 + 180 + let url = match repo.origin_url() { 181 + Some(remote) => fork_pull_url( 182 + &link.appview, 183 + &SourceBranch(branch), 184 + &TargetBranch(default), 185 + remote, 186 + repo_did, 187 + )?, 188 + None => branch_pull_url( 189 + &link.appview, 190 + &link.owner, 191 + &link.rkey, 192 + &SourceBranch(branch), 193 + &TargetBranch(default), 194 + )?, 195 + }; 196 + 187 197 Some(messages.pull_request.lines(|UrlKey::Url| url.to_string())) 188 198 } 189 199 190 - fn pull_url( 200 + fn branch_pull_url( 191 201 appview: &AppviewEndpoint, 192 202 owner: &OwnerLabel, 193 - repo: &RepoRkey, 203 + repo_rkey: &RepoRkey, 194 204 source: &SourceBranch, 195 205 target: &TargetBranch, 196 206 ) -> Option<Url> { 197 207 let mut url = Url::parse(appview.as_str()).ok()?; 208 + 198 209 url.path_segments_mut().ok()?.pop_if_empty().extend([ 199 210 owner.as_str(), 200 - repo.as_str(), 211 + repo_rkey.as_str(), 201 212 "pulls", 202 213 "new", 203 214 ]); 215 + 204 216 url.query_pairs_mut() 205 217 .append_pair("source", "branch") 206 218 .append_pair("sourceBranch", source.0.as_str()) 207 219 .append_pair("targetBranch", target.0.as_str()); 220 + 221 + Some(url) 222 + } 223 + 224 + fn fork_pull_url( 225 + appview: &AppviewEndpoint, 226 + source: &SourceBranch, 227 + target: &TargetBranch, 228 + remote: OriginUrl, 229 + repo_did: &RepoDid, 230 + ) -> Option<Url> { 231 + let remote_url = Url::parse(remote.as_str()).ok()?; 232 + 233 + // TODO: We need to handle file schemes. For now though if the remote is a 234 + // file scheme a fork PR link won't be created. 235 + match remote_url.scheme() { 236 + "http" | "https" => (), 237 + _ => return None, 238 + } 239 + 240 + let paths: Vec<&str> = remote_url 241 + .path_segments() 242 + .map(|segments| segments.collect()) 243 + .unwrap_or_default(); 244 + 245 + let mut url = Url::parse(appview.as_str()).ok()?; 246 + 247 + url.path_segments_mut() 248 + .ok()? 249 + .pop_if_empty() 250 + .extend(paths) 251 + .extend(["pulls", "new"]); 252 + 253 + url.query_pairs_mut() 254 + .append_pair("source", "fork") 255 + .append_pair("sourceBranch", source.0.as_str()) 256 + .append_pair("targetBranch", target.0.as_str()) 257 + .append_pair("fork", repo_did.as_str()); 258 + 208 259 Some(url) 209 260 } 210 261
+33 -2
knot2/crates/knot-postreceive/tests/post_receive.rs
··· 611 611 ); 612 612 assert!(link.contains("sourceBranch=feature"), "{link}"); 613 613 assert!(link.contains("targetBranch=main"), "{link}"); 614 + assert!(link.contains("source=branch"), "{link}"); 615 + } 616 + 617 + #[test] 618 + fn a_new_branch_with_an_origin_remote_yields_a_pull_request_link() { 619 + let world = world(); 620 + world 621 + .repo 622 + .set_origin_url(&OriginUrl::new("https://oyster.cafe/did:plc:squid/anemone")) 623 + .unwrap(); 624 + let log = log(); 625 + let head = create_feature(&world); 626 + 627 + let applied = created("feature", head); 628 + let messages = run(&world, &log, &applied, &Ci::Skip, Some(&pull())); 629 + 630 + let link = messages 631 + .iter() 632 + .find(|line| line.contains("/pulls/new")) 633 + .expect("pull-request link is offered for a new branch on a fork with origin remote"); 634 + 635 + assert!( 636 + link.contains("https://tangled.test/did:plc:squid/anemone/pulls/new"), 637 + "{link}" 638 + ); 639 + assert!(link.contains("sourceBranch=feature"), "{link}"); 640 + assert!(link.contains("targetBranch=main"), "{link}"); 641 + assert!(link.contains("source=fork"), "{link}"); 642 + assert!(link.contains("fork=did%3Aplc%3Alimpet"), "{link}"); 614 643 } 615 644 616 645 #[test] ··· 633 662 new: oid(w, "HEAD"), 634 663 }] 635 664 }), 636 - ("new branch on a fork with an origin remote", |w| { 665 + ("new branch with a file origin remote", |w| { 637 666 let head = create_feature(w); 638 667 w.repo 639 - .set_origin_url(&OriginUrl::new("https://oyster.cafe/did:plc:squid/anemone")) 668 + .set_origin_url(&OriginUrl::new( 669 + "file://git/repos/oyster.cafe/did:plc:squid/anemone", 670 + )) 640 671 .unwrap(); 641 672 created("feature", head) 642 673 }),