Expand 10 hidden lines 11 11 12 12 13 13 14 + use std:: collections:: HashMap ; 14 15 use std:: path:: Path as FsPath ; 15 16 16 17 use axum:: extract:: { Path , Query , State } ; Expand 41 hidden lines 58 59 # [ derive ( Clone , Copy , PartialEq , Eq ) ] 59 60 enum Tab { 60 61 Code , 62 + Commits , 61 63 Branches , 62 64 Tags , 63 65 Search , Expand 28 hidden lines 92 94 . map_err ( AppError :: from) 93 95 } 94 96 97 + 98 + 99 + async fn branch_names ( state : & AppState , repo_id : & str ) -> Vec < String > { 100 + git_read ( state, repo_id, git:: Repo :: branch_names) 101 + . await 102 + . unwrap_or_default ( ) 103 + } 104 + 95 105 96 106 97 107 Expand 169 hidden lines 267 277 "branches" => branches_view ( state, viewer, jar, uri, ctx) . await , 268 278 "tags" => tags_view ( state, viewer, jar, uri, ctx) . await , 269 279 "search" => { 270 - let header = repo_header ( state, & ctx, Tab :: Search , & ctx. repo. default_branch) ; 280 + let branches = branch_names ( state, & ctx. repo. id) . await ; 281 + let header = repo_header ( 282 + state, 283 + & ctx, 284 + Tab :: Search , 285 + & ctx. repo. default_branch, 286 + & branches, 287 + ) ; 271 288 let raw_query = dq. q. clone ( ) . unwrap_or_default ( ) ; 272 289 crate :: search:: in_repo ( state, viewer, jar, uri, & ctx, & raw_query, header) . await 273 290 } Expand 48 hidden lines 322 339 } ) 323 340 . await ?; 324 341 let readme = load_readme ( state, & ctx. repo. id, & rev, & entries) . await ; 342 + let branches = branch_names ( state, & ctx. repo. id) . await ; 325 343 let base = format ! ( "/{}/{}" , ctx. owner. username, ctx. repo. path) ; 326 344 html ! { 327 - ( repo_header ( state, & ctx, Tab :: Code , & rev_name) ) 345 + ( repo_header ( state, & ctx, Tab :: Code , & rev_name, & branches) ) 328 346 form class="search-row repo-codesearch" method="get" action=( format ! ( "{base}/-/search" ) ) { 329 347 input type ="search" name="q" placeholder="Search this repository…" 330 348 aria-label="Search this repository" ; Expand 42 hidden lines 373 391 374 392 fn empty_repo_body ( state : & AppState , ctx : & RepoCtx ) -> Markup { 375 393 html ! { 376 - ( repo_header ( state, ctx, Tab :: Code , & ctx. repo. default_branch) ) 394 + ( repo_header ( state, ctx, Tab :: Code , & ctx. repo. default_branch, & [ ] ) ) 377 395 div class="card" { 378 396 h2 { "This repository is empty" } 379 397 p class="muted" { "Push a commit to get started:" } Expand 28 hidden lines 408 426 } ) 409 427 . await ?; 410 428 429 + let branches = branch_names ( state, & ctx. repo. id) . await ; 411 430 let body = html ! { 412 - ( repo_header ( state, & ctx, Tab :: Code , & rev_name) ) 431 + ( repo_header ( state, & ctx, Tab :: Code , & rev_name, & branches) ) 413 432 ( breadcrumb ( & ctx. owner. username, & ctx. repo. path, & rev_name, & path) ) 414 433 ( tree_table ( & ctx. owner. username, & ctx. repo. path, & rev_name, & path, & entries) ) 415 434 } ; Expand 46 hidden lines 462 481 attr_lang. as_deref ( ) , 463 482 source_view, 464 483 ) ; 484 + let branches = branch_names ( state, & ctx. repo. id) . await ; 465 485 let body = html ! { 466 - ( repo_header ( state, & ctx, Tab :: Code , & rev_name) ) 486 + ( repo_header ( state, & ctx, Tab :: Code , & rev_name, & branches) ) 467 487 ( breadcrumb ( & ctx. owner. username, & ctx. repo. path, & rev_name, & path) ) 468 488 ( content_body) 469 489 } ; Expand 140 hidden lines 610 630 ctx : RepoCtx , 611 631 ) -> AppResult < Response > { 612 632 let branches = git_read ( state, & ctx. repo. id, git:: Repo :: branches) . await ?; 633 + let branch_list: Vec < String > = branches. iter ( ) . map ( |b| b. name. clone ( ) ) . collect ( ) ; 613 634 let body = html ! { 614 - ( repo_header ( state, & ctx, Tab :: Branches , & ctx. repo. default_branch) ) 635 + ( repo_header ( state, & ctx, Tab :: Branches , & ctx. repo. default_branch, & branch_list) ) 615 636 h2 { "Branches" } 616 637 table class="list" { 617 638 thead { tr { th { "Branch" } th { "Ahead" } th { "Behind" } th { "Updated" } } } Expand 27 hidden lines 645 666 ctx : RepoCtx , 646 667 ) -> AppResult < Response > { 647 668 let tags = git_read ( state, & ctx. repo. id, git:: Repo :: tags) . await ?; 669 + let branches = branch_names ( state, & ctx. repo. id) . await ; 648 670 let body = html ! { 649 - ( repo_header ( state, & ctx, Tab :: Tags , & ctx. repo. default_branch) ) 671 + ( repo_header ( state, & ctx, Tab :: Tags , & ctx. repo. default_branch, & branches) ) 650 672 h2 { "Tags" } 651 673 table class="list" { 652 674 thead { tr { th { " Tag " } th { "Commit" } th { "Kind" } } } Expand 50 hidden lines 703 725 } ) 704 726 . await ?; 705 727 728 + 729 + let mut signers: HashMap < String , String > = HashMap :: new ( ) ; 730 + for s in & states { 731 + if let git:: SignatureState :: Verified { user_id, .. } = s 732 + && !signers. contains_key ( user_id) 733 + && let Ok ( Some ( u) ) = state. store. user_by_id ( user_id) . await 734 + { 735 + signers. insert ( user_id. clone ( ) , u. username) ; 736 + } 737 + } 738 + 706 739 let base = format ! ( "/{}/{}" , ctx. owner. username, ctx. repo. path) ; 707 740 let now = crate :: now_ms ( ) ; 741 + let branches = branch_names ( state, & ctx. repo. id) . await ; 708 742 let body = html ! { 709 - ( repo_header ( state, & ctx, Tab :: Code , & rev_name) ) 743 + ( repo_header ( state, & ctx, Tab :: Commits , & rev_name, & branches) ) 710 744 h2 { "Commits" } 711 745 ul class="commit-list card" { 712 746 @for ( commit, sig) in commits. iter ( ) . zip ( states. iter ( ) ) { 713 747 @let commit_url = format ! ( "{base}/-/commit/{}" , commit. oid) ; 748 + @let signer = match sig { 749 + git:: SignatureState :: Verified { user_id, .. } => signers. get ( user_id) . map ( String :: as_str) , 750 + _ => None , 751 + } ; 714 752 li class="commit-row" { 715 753 div class="commit-row-body" { 716 754 a class="commit-row-msg" href=( commit_url) { ( commit. summary) } Expand 4 hidden lines 721 759 } 722 760 } 723 761 div class="commit-row-actions" { 724 - ( signature_badge ( sig) ) 762 + ( commit_sig_cell ( sig, signer, commit. author. time_ms) ) 725 763 a class="mono commit-sha-pill" href=( commit_url) { ( commit. oid. short ( ) ) } 726 764 button class="icon-btn commit-copy" type ="button" 727 765 data-clipboard=( commit. oid. to_string ( ) ) aria-label="Copy commit id" { Expand 46 hidden lines 774 812 ( a + fa, d + fd) 775 813 } ) ; 776 814 let repo_base = format ! ( "/{}/{}" , ctx. owner. username, ctx. repo. path) ; 815 + let branches = branch_names ( state, & ctx. repo. id) . await ; 777 816 778 817 let body = html ! { 779 - ( repo_header ( state, & ctx, Tab :: Code , & ctx. repo. default_branch) ) 780 - ( commit_meta ( & detail, & sig) ) 818 + ( repo_header ( state, & ctx, Tab :: Code , & ctx. repo. default_branch, & branches) ) 819 + ( commit_meta ( & detail) ) 781 820 ( signature_detail ( & sig, signer. as_deref ( ) ) ) 782 821 div class="diff-toolbar" { 783 822 span { Expand 126 hidden lines 910 949 } 911 950 } 912 951 913 - 914 - fn commit_meta ( detail : & git:: CommitDetail , sig : & git:: SignatureState ) -> Markup { 952 + 953 + 954 + fn commit_meta ( detail : & git:: CommitDetail ) -> Markup { 915 955 let ( subject, body) = detail 916 956 . message 917 957 . split_once ( " \n \n " ) 918 958 . map_or ( ( detail. message. as_str ( ) , "" ) , |( s, b) | ( s. trim ( ) , b) ) ; 919 959 html ! { 920 960 div class="commit-meta" { 921 - h2 { ( subject. lines ( ) . next ( ) . unwrap_or ( "" ) ) " " ( signature_badge ( sig ) ) } 922 - p class= " muted" { 923 - span class="mono" { ( detail. oid. short ( ) ) } 924 - " · " ( detail. author. name) 925 - " committed on " ( fmt_date ( detail. committer. time_ms) ) 961 + div class="commit-title-row" { 962 + h2 class="commit-title" { ( subject. lines ( ) . next ( ) . unwrap_or ( "" ) ) } 963 + p class="commit-title-meta muted" { 964 + span class="mono" { ( detail. oid. short ( ) ) } 965 + " · " ( detail. author. name) 966 + " committed on " ( fmt_date ( detail. committer. time_ms) ) 967 + } 926 968 } 927 969 @if !body. trim ( ) . is_empty ( ) { 928 970 pre class="commit-body" { ( body. trim_end ( ) ) } Expand 311 hidden lines 1240 1282 } 1241 1283 1242 1284 1243 - 1244 - fn repo_header ( state : & AppState , ctx : & RepoCtx , active : Tab , rev : & str ) -> Markup { 1285 + 1286 + fn repo_header ( 1287 + state : & AppState , 1288 + ctx : & RepoCtx , 1289 + active : Tab , 1290 + rev : & str , 1291 + branches : & [ String ] , 1292 + ) -> Markup { 1245 1293 let base = format ! ( "/{}/{}" , ctx. owner. username, ctx. repo. path) ; 1246 1294 let tab = |t : Tab , label : & str , glyph : Icon , href : String | { 1247 1295 html ! { Expand 14 hidden lines 1262 1310 div class="repo-nav" { 1263 1311 nav class="tabs repo-tabs" { 1264 1312 ( tab ( Tab :: Code , "Code" , Icon :: File , base. clone ( ) ) ) 1313 + ( tab ( Tab :: Commits , "Commits" , Icon :: History , format ! ( "{base}/-/commits/{rev}" ) ) ) 1265 1314 ( tab ( Tab :: Branches , "Branches" , Icon :: GitBranch , format ! ( "{base}/-/branches" ) ) ) 1266 1315 ( tab ( Tab :: Tags , "Tags" , Icon :: Box , format ! ( "{base}/-/tags" ) ) ) 1267 1316 } 1268 1317 div class="repo-actions" { 1269 - a class="btn" href=( format ! ( "{base}/-/branches" ) ) { 1270 - ( icon ( Icon :: GitBranch ) ) span { ( rev) } 1271 - } 1272 - a class="btn" href=( format ! ( "{base}/-/commits/{rev}" ) ) { "Commits" } 1318 + ( branch_menu ( & base, rev, branches) ) 1273 1319 ( clone_menu ( state, ctx) ) 1274 1320 } 1275 1321 } Expand 1 hidden lines 1277 1323 } 1278 1324 } 1279 1325 1326 + 1327 + 1328 + fn branch_menu ( base : & str , rev : & str , branches : & [ String ] ) -> Markup { 1329 + html ! { 1330 + @if branches. is_empty ( ) { 1331 + a class="btn" href=( format ! ( "{base}/-/branches" ) ) { 1332 + ( icon ( Icon :: GitBranch ) ) span { ( rev) } 1333 + } 1334 + } @else { 1335 + details class="branch-menu" { 1336 + summary class="btn" { ( icon ( Icon :: GitBranch ) ) span { ( rev) } ( icon ( Icon :: ChevronDown ) ) } 1337 + div class="branch-pop card" { 1338 + div class="branch-pop-head muted" { "Switch branch" } 1339 + @for name in branches { 1340 + a href=( format ! ( "{base}/-/tree/{name}" ) ) 1341 + aria-current=[ ( name == rev) . then_some ( "page" ) ] { ( name) } 1342 + } 1343 + a class="branch-pop-all" href=( format ! ( "{base}/-/branches" ) ) { "View all branches" } 1344 + } 1345 + } 1346 + } 1347 + } 1348 + } 1349 + 1280 1350 1281 1351 fn clone_menu ( state : & AppState , ctx : & RepoCtx ) -> Markup { 1282 1352 let https = clone_https ( state, ctx) ; Expand 123 hidden lines 1406 1476 } 1407 1477 } 1408 1478 1479 + 1480 + fn key_kind_label ( kind : model:: KeyKind ) -> & ' static str { 1481 + match kind { 1482 + model:: KeyKind :: Ssh => "SSH" , 1483 + model:: KeyKind :: Gpg => "GPG" , 1484 + } 1485 + } 1486 + 1487 + 1488 + 1489 + 1490 + fn commit_sig_cell ( state : & git:: SignatureState , signer : Option < & str > , time_ms : i64 ) -> Markup { 1491 + match state { 1492 + git:: SignatureState :: Verified { 1493 + kind, 1494 + fingerprint, 1495 + user_id, 1496 + .. 1497 + } => html ! { 1498 + span class="sig-pop-wrap" tabindex="0" { 1499 + ( signature_badge ( state) ) 1500 + div class="sig-pop card" { 1501 + div class="sig-pop-title" { 1502 + "This commit was signed with a verified signature." 1503 + } 1504 + div class="sig-pop-signer" { 1505 + "Signed by " strong { ( signer. unwrap_or ( user_id) ) } 1506 + } 1507 + div class="sig-pop-fp" { 1508 + ( key_kind_label ( * kind) ) " key fingerprint:" 1509 + span class="mono" { ( fingerprint) } 1510 + } 1511 + div class="muted sig-pop-when" { "Verified on " ( fmt_joined ( time_ms) ) } 1512 + } 1513 + } 1514 + } , 1515 + _ => signature_badge ( state) , 1516 + } 1517 + } 1518 + 1409 1519 1410 1520 fn signature_badge ( state : & git:: SignatureState ) -> Markup { 1411 1521 match state { Expand 17 hidden lines 1429 1539 1430 1540 1431 1541 fn signature_detail ( state : & git:: SignatureState , signer : Option < & str > ) -> Markup { 1432 - let kind_label = |k : model:: KeyKind | match k { 1433 - model:: KeyKind :: Ssh => "SSH" , 1434 - model:: KeyKind :: Gpg => "GPG" , 1435 - } ; 1436 1542 match state { 1437 1543 git:: SignatureState :: Unsigned => html ! { } , 1438 1544 git:: SignatureState :: Verified { Expand 7 hidden lines 1446 1552 "Signed by " strong { ( signer. unwrap_or ( user_id) ) } 1447 1553 } 1448 1554 span class="sig-fp mono" { 1449 - ( kind_label ( * kind) ) " key fingerprint: " ( fingerprint) 1555 + ( key_kind_label ( * kind) ) " key fingerprint: " ( fingerprint) 1450 1556 } 1451 1557 } 1452 1558 } , 1453 1559 git:: SignatureState :: UnknownKey { kind, fingerprint } => html ! { 1454 1560 div class="sig-detail sig-unverified" { 1455 - span { "Signed by an unregistered " ( kind_label ( * kind) ) " key" } 1561 + span { "Signed by an unregistered " ( key_kind_label ( * kind) ) " key" } 1456 1562 span class="sig-fp mono" { ( fingerprint) } 1457 1563 } 1458 1564 } ,