55use std:: {
66 borrow:: Cow ,
77 ops:: { Deref , DerefMut } ,
8+ str:: FromStr ,
89} ;
910
1011use bstr:: BString ;
@@ -45,6 +46,8 @@ pub struct Commit {
4546 /// if it's not stored in the Commit. Use [`Self::change_id()`] to always get the change id,
4647 /// if necessary, by deriving it from the commit hash itself.
4748 pub change_id : Option < but_core:: ChangeId > ,
49+ /// Optional URL to the Gerrit review for this commit, if applicable.
50+ pub gerrit_review_url : Option < String > ,
4851}
4952
5053impl std:: fmt:: Debug for Commit {
@@ -80,6 +83,7 @@ impl From<but_core::Commit<'_>> for Commit {
8083 change_id,
8184 refs : Vec :: new ( ) ,
8285 flags : StackCommitFlags :: empty ( ) ,
86+ gerrit_review_url : None ,
8387 }
8488 }
8589}
@@ -114,6 +118,7 @@ impl Commit {
114118 refs : graph_commit. refs . clone ( ) ,
115119 flags : graph_commit. flags . into ( ) ,
116120 change_id : hdr. and_then ( |hdr| hdr. change_id ) ,
121+ gerrit_review_url : None ,
117122 }
118123 }
119124}
@@ -222,15 +227,48 @@ impl WorkspaceExt for but_graph::Workspace {
222227 }
223228}
224229
230+ /// Controls whether [`RefInfo`] should be interpreted with Gerrit push metadata.
231+ ///
232+ /// Standard `head_info()` derives commit relation and push status from the Git
233+ /// graph: local branch tips, remote-tracking refs, target reachability, and
234+ /// similarity checks. Gerrit mode has an extra source of truth: after a push,
235+ /// GitButler records the Gerrit Change-Id, the patchset commit id accepted by
236+ /// Gerrit, and the review URL in the cache database.
237+ ///
238+ /// When enabled, that recorded metadata is applied after the standard graph and
239+ /// similarity pass. This lets `RefInfo` report commits as already present on
240+ /// Gerrit even when there is no normal remote-tracking branch update for
241+ /// `refs/for/*` pushes, and lets the UI link commits back to their Gerrit
242+ /// reviews.
243+ #[ derive( Default ) ]
244+ pub enum GerritMode < ' db > {
245+ /// Use only the standard graph-derived `head_info()` data.
246+ #[ default]
247+ Disabled ,
248+ /// Apply Gerrit metadata from the cache database to commits and push status.
249+ Enabled ( but_db:: GerritMetadataHandle < ' db > ) ,
250+ }
251+
252+ impl std:: fmt:: Debug for GerritMode < ' _ > {
253+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
254+ match self {
255+ GerritMode :: Disabled => f. write_str ( "Disabled" ) ,
256+ GerritMode :: Enabled ( _) => f. write_str ( "Enabled(..)" ) ,
257+ }
258+ }
259+ }
260+
225261/// Options for the [`ref_info()`](crate::ref_info()) call.
226- #[ derive( Default , Debug , Clone ) ]
227- pub struct Options {
262+ #[ derive( Default , Debug ) ]
263+ pub struct Options < ' db > {
228264 /// Control how to traverse the commit-graph as the basis for the workspace conversion.
229265 pub traversal : but_graph:: init:: Options ,
230266 /// Perform expensive computations on a per-commit basis.
231267 ///
232268 /// Note that less expensive checks are still performed.
233269 pub expensive_commit_info : bool ,
270+ /// Configure whether Gerrit metadata should augment the standard graph-derived result.
271+ pub gerrit_mode : GerritMode < ' db > ,
234272}
235273
236274/// A segment of a commit graph, representing a set of commits exclusively.
@@ -358,7 +396,7 @@ impl std::fmt::Debug for Segment {
358396 }
359397}
360398
361- use anyhow:: bail;
399+ use anyhow:: { Context as _ , bail} ;
362400use but_core:: { is_workspace_ref_name, ref_metadata:: ValueInfo } ;
363401use but_graph:: {
364402 Graph ,
@@ -377,7 +415,7 @@ use crate::{AncestorWorkspaceCommit, RefInfo, WorkspaceCommit, branch, ui::PushS
377415pub fn head_info (
378416 repo : & gix:: Repository ,
379417 meta : & impl but_core:: RefMetadata ,
380- opts : Options ,
418+ opts : Options < ' _ > ,
381419) -> anyhow:: Result < RefInfo > {
382420 let graph = Graph :: from_head ( repo, meta, opts. traversal . clone ( ) ) ?;
383421 graph_to_ref_info ( & graph. into_workspace ( ) ?, repo, opts)
@@ -395,7 +433,7 @@ pub fn head_info(
395433pub fn ref_info (
396434 mut existing_ref : gix:: Reference < ' _ > ,
397435 meta : & impl but_core:: RefMetadata ,
398- opts : Options ,
436+ opts : Options < ' _ > ,
399437) -> anyhow:: Result < RefInfo > {
400438 let id = existing_ref. peel_to_id ( ) ?;
401439 let repo = id. repo ;
@@ -451,7 +489,7 @@ pub(crate) fn find_ancestor_workspace_commit(
451489pub fn graph_to_ref_info (
452490 workspace : & but_graph:: Workspace ,
453491 repo : & gix:: Repository ,
454- opts : Options ,
492+ opts : Options < ' _ > ,
455493) -> anyhow:: Result < RefInfo > {
456494 if workspace. graph . hard_limit_hit ( ) {
457495 tracing:: warn!( hard_limit=?opts. traversal. hard_limit,
@@ -518,9 +556,90 @@ pub fn graph_to_ref_info(
518556 bail ! ( "{msg}" ) ;
519557 }
520558 info. compute_similarity ( graph, repo, opts. expensive_commit_info ) ?;
559+ if let GerritMode :: Enabled ( metadata) = opts. gerrit_mode {
560+ info. apply_gerrit_metadata ( metadata) ?;
561+ }
521562 Ok ( info)
522563}
523564
565+ impl RefInfo {
566+ /// Enrich standard `RefInfo` output with Gerrit review metadata.
567+ ///
568+ /// The regular construction path has already computed stack shape, commit
569+ /// similarity, integration state, and ordinary push status from refs and
570+ /// graph reachability. Gerrit pushes do not update a branch remote-tracking
571+ /// ref in the same way a normal Git push does, so those graph-only signals
572+ /// are not enough to tell whether a local commit has already been accepted
573+ /// by Gerrit.
574+ ///
575+ /// For each local commit, this pass looks up its GitButler Change-Id in the
576+ /// Gerrit metadata table populated after successful pushes. A hit attaches
577+ /// the review URL and, unless the commit is already integrated, marks the
578+ /// commit as `LocalAndRemote(recorded_patchset_commit_id)`. If the recorded
579+ /// patchset id differs from the local commit id, the commit is treated as a
580+ /// rewritten patchset that would require another Gerrit push.
581+ fn apply_gerrit_metadata (
582+ & mut self ,
583+ metadata : but_db:: GerritMetadataHandle < ' _ > ,
584+ ) -> anyhow:: Result < ( ) > {
585+ for segment in self
586+ . stacks
587+ . iter_mut ( )
588+ . flat_map ( |stack| stack. segments . iter_mut ( ) )
589+ {
590+ for commit in & mut segment. commits {
591+ let Some ( meta) = metadata. get ( & commit. change_id ( ) . to_string ( ) ) ? else {
592+ continue ;
593+ } ;
594+ commit. inner . gerrit_review_url = Some ( meta. review_url ) ;
595+ if !matches ! ( commit. relation, LocalCommitRelation :: Integrated ( _) ) {
596+ let remote_id =
597+ gix:: ObjectId :: from_str ( & meta. commit_id ) . with_context ( || {
598+ format ! (
599+ "Gerrit metadata for change-id {} has invalid commit id" ,
600+ meta. change_id
601+ )
602+ } ) ?;
603+ commit. relation = LocalCommitRelation :: LocalAndRemote ( remote_id) ;
604+ }
605+ }
606+ segment. push_status = gerrit_push_status ( segment) ;
607+ }
608+ Ok ( ( ) )
609+ }
610+ }
611+
612+ /// Derive push status for a Gerrit-enriched segment.
613+ ///
614+ /// Standard push status compares local branches with their remote-tracking
615+ /// branches. Gerrit mode instead compares local commits with the patchset
616+ /// commit ids recorded in Gerrit metadata. A matching id means the current
617+ /// local commit is already pushed as-is, a different recorded id means the
618+ /// local commit has been rewritten since the last Gerrit patchset, and a
619+ /// local-only commit still needs to be pushed.
620+ fn gerrit_push_status ( segment : & crate :: ref_info:: Segment ) -> PushStatus {
621+ let has_local_only = segment
622+ . commits
623+ . iter ( )
624+ . any ( |commit| matches ! ( commit. relation, LocalCommitRelation :: LocalOnly ) ) ;
625+ let has_diverged = segment. commits . iter ( ) . any ( |commit| {
626+ matches ! ( commit. relation, LocalCommitRelation :: LocalAndRemote ( remote_id) if commit. id != remote_id)
627+ } ) ;
628+ let all_pushed = segment. commits . iter ( ) . all ( |commit| {
629+ matches ! ( commit. relation, LocalCommitRelation :: LocalAndRemote ( remote_id) if commit. id == remote_id)
630+ } ) ;
631+
632+ if has_diverged {
633+ PushStatus :: UnpushedCommitsRequiringForce
634+ } else if has_local_only {
635+ PushStatus :: UnpushedCommits
636+ } else if all_pushed {
637+ PushStatus :: NothingToPush
638+ } else {
639+ segment. push_status
640+ }
641+ }
642+
524643impl branch:: Stack {
525644 fn try_from_graph_stack (
526645 stack : & but_graph:: workspace:: Stack ,
0 commit comments