@@ -7,8 +7,9 @@ use but_ctx::Context;
77use but_oplog:: legacy:: { OperationKind , SnapshotDetails , Trailer } ;
88use but_rebase:: graph_rebase:: { Editor , SuccessfulRebase } ;
99use but_workspace:: branch:: {
10- OnWorkspaceMergeConflict ,
10+ InitialBranchIntegration , OnWorkspaceMergeConflict ,
1111 apply:: { WorkspaceMerge , WorkspaceReferenceNaming } ,
12+ integrate_branch_upstream:: InteractiveIntegration ,
1213} ;
1314use tracing:: instrument;
1415
@@ -18,11 +19,20 @@ pub struct MoveBranchResult {
1819 pub workspace : WorkspaceState ,
1920}
2021
22+ /// Outcome after integrating a branch with an interactive integration plan.
23+ pub struct IntegrateBranchResult {
24+ /// Workspace state after applying or previewing the integration.
25+ pub workspace : WorkspaceState ,
26+ }
27+
2128/// JSON transport types for branch APIs.
2229pub mod json {
23- use serde:: Serialize ;
30+ use serde:: { Deserialize , Serialize } ;
2431
25- use crate :: branch:: MoveBranchResult as InternalMoveBranchResult ;
32+ use crate :: branch:: {
33+ IntegrateBranchResult as InternalIntegrateBranchResult ,
34+ MoveBranchResult as InternalMoveBranchResult ,
35+ } ;
2636
2737 /// JSON sibling of [`but_workspace::branch::apply::Outcome`].
2838 #[ derive( Debug , Serialize ) ]
@@ -78,6 +88,242 @@ pub mod json {
7888 } )
7989 }
8090 }
91+
92+ #[ derive( Debug , Serialize ) ]
93+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
94+ #[ serde( rename_all = "camelCase" ) ]
95+ /// JSON transport type for integrating a branch.
96+ pub struct IntegrateBranchResult {
97+ /// Workspace state after applying or previewing the integration.
98+ pub workspace : crate :: json:: WorkspaceState ,
99+ }
100+ #[ cfg( feature = "export-schema" ) ]
101+ but_schemars:: register_sdk_type!( IntegrateBranchResult ) ;
102+
103+ impl TryFrom < InternalIntegrateBranchResult > for IntegrateBranchResult {
104+ type Error = anyhow:: Error ;
105+
106+ fn try_from ( value : InternalIntegrateBranchResult ) -> Result < Self , Self :: Error > {
107+ Ok ( Self {
108+ workspace : value. workspace . try_into ( ) ?,
109+ } )
110+ }
111+ }
112+
113+ /// JSON transport type for a divergence commit row.
114+ #[ derive( Debug , Serialize ) ]
115+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
116+ #[ serde( rename_all = "camelCase" ) ]
117+ pub struct IntegrationDivergenceCommit {
118+ /// The commit shown in the graph row.
119+ pub id : crate :: json:: HexHashString ,
120+ /// The first-line subject shown for the commit.
121+ pub subject : String ,
122+ /// Human-facing ref labels rendered inline on the commit row.
123+ pub refs : Vec < String > ,
124+ }
125+ #[ cfg( feature = "export-schema" ) ]
126+ but_schemars:: register_sdk_type!( IntegrationDivergenceCommit ) ;
127+
128+ impl From < but_workspace:: branch:: IntegrationDivergenceCommit > for IntegrationDivergenceCommit {
129+ fn from ( value : but_workspace:: branch:: IntegrationDivergenceCommit ) -> Self {
130+ let but_workspace:: branch:: IntegrationDivergenceCommit { id, subject, refs } = value;
131+ Self {
132+ id : id. into ( ) ,
133+ subject,
134+ refs,
135+ }
136+ }
137+ }
138+
139+ /// JSON transport type for current branch/upstream divergence information.
140+ #[ derive( Debug , Serialize ) ]
141+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
142+ #[ serde( rename_all = "camelCase" ) ]
143+ pub struct IntegrationDivergenceDisplay {
144+ /// The local branch being integrated.
145+ pub branch_ref_name : crate :: json:: FullRefName ,
146+ /// The upstream branch this local branch integrates with.
147+ pub upstream_ref_name : crate :: json:: FullRefName ,
148+ /// Commits only reachable from the local branch tip down to the shared section.
149+ pub local_only : Vec < IntegrationDivergenceCommit > ,
150+ /// Commits only reachable from the upstream branch tip down to the shared section.
151+ pub upstream_only : Vec < IntegrationDivergenceCommit > ,
152+ /// Commits shared or matched between local and upstream above the merge-base.
153+ pub matched : Vec < IntegrationDivergenceCommit > ,
154+ /// The merge-base row shown once at the bottom.
155+ pub merge_base : IntegrationDivergenceCommit ,
156+ }
157+ #[ cfg( feature = "export-schema" ) ]
158+ but_schemars:: register_sdk_type!( IntegrationDivergenceDisplay ) ;
159+
160+ impl From < but_workspace:: branch:: IntegrationDivergenceDisplay > for IntegrationDivergenceDisplay {
161+ fn from ( value : but_workspace:: branch:: IntegrationDivergenceDisplay ) -> Self {
162+ let but_workspace:: branch:: IntegrationDivergenceDisplay {
163+ branch_ref_name,
164+ upstream_ref_name,
165+ local_only,
166+ upstream_only,
167+ matched,
168+ merge_base,
169+ } = value;
170+ Self {
171+ branch_ref_name : branch_ref_name. into ( ) ,
172+ upstream_ref_name : upstream_ref_name. into ( ) ,
173+ local_only : local_only. into_iter ( ) . map ( Into :: into) . collect ( ) ,
174+ upstream_only : upstream_only. into_iter ( ) . map ( Into :: into) . collect ( ) ,
175+ matched : matched. into_iter ( ) . map ( Into :: into) . collect ( ) ,
176+ merge_base : merge_base. into ( ) ,
177+ }
178+ }
179+ }
180+
181+ /// JSON transport type for a branch integration step.
182+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
183+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
184+ #[ serde( rename_all = "camelCase" , tag = "kind" ) ]
185+ pub enum InteractiveIntegrationStep {
186+ /// Skip a given commit, effectively removing it from the branch.
187+ Skip {
188+ /// The commit to omit from the rewritten branch.
189+ commit_id : crate :: json:: HexHashString ,
190+ } ,
191+ /// Pick a commit, keeping it in the branch.
192+ Pick {
193+ /// The local commit to keep in the rewritten branch.
194+ commit_id : crate :: json:: HexHashString ,
195+ } ,
196+ /// Squash multiple commits into one.
197+ Squash {
198+ /// The ordered commits to squash together.
199+ commits : Vec < crate :: json:: HexHashString > ,
200+ /// Optional replacement message for the squash commit.
201+ message : Option < String > ,
202+ } ,
203+ /// Merge a commit into the previous one.
204+ Merge {
205+ /// The commit whose change range should be merged.
206+ commit_id : crate :: json:: HexHashString ,
207+ } ,
208+ }
209+ #[ cfg( feature = "export-schema" ) ]
210+ but_schemars:: register_sdk_type!( InteractiveIntegrationStep ) ;
211+
212+ impl TryFrom < InteractiveIntegrationStep >
213+ for but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep
214+ {
215+ type Error = anyhow:: Error ;
216+
217+ fn try_from ( value : InteractiveIntegrationStep ) -> Result < Self , Self :: Error > {
218+ Ok ( match value {
219+ InteractiveIntegrationStep :: Skip { commit_id } => Self :: Skip {
220+ commit_id : commit_id. try_into ( ) ?,
221+ } ,
222+ InteractiveIntegrationStep :: Pick { commit_id } => Self :: Pick {
223+ commit_id : commit_id. try_into ( ) ?,
224+ } ,
225+ InteractiveIntegrationStep :: Squash { commits, message } => Self :: Squash {
226+ commits : commits
227+ . into_iter ( )
228+ . map ( TryInto :: try_into)
229+ . collect :: < Result < _ , _ > > ( ) ?,
230+ message,
231+ } ,
232+ InteractiveIntegrationStep :: Merge { commit_id } => Self :: Merge {
233+ commit_id : commit_id. try_into ( ) ?,
234+ } ,
235+ } )
236+ }
237+ }
238+
239+ /// JSON transport type describing an interactive branch integration plan.
240+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
241+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
242+ #[ serde( rename_all = "camelCase" ) ]
243+ pub struct InteractiveIntegration {
244+ /// Merge base between the upstream and the local reference.
245+ pub merge_base : crate :: json:: HexHashString ,
246+ /// The ordered integration steps to apply.
247+ pub steps : Vec < InteractiveIntegrationStep > ,
248+ }
249+ #[ cfg( feature = "export-schema" ) ]
250+ but_schemars:: register_sdk_type!( InteractiveIntegration ) ;
251+
252+ impl TryFrom < InteractiveIntegration >
253+ for but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegration
254+ {
255+ type Error = anyhow:: Error ;
256+
257+ fn try_from ( value : InteractiveIntegration ) -> Result < Self , Self :: Error > {
258+ let InteractiveIntegration { merge_base, steps } = value;
259+ Ok ( Self {
260+ merge_base : merge_base. try_into ( ) ?,
261+ steps : steps
262+ . into_iter ( )
263+ . map ( TryInto :: try_into)
264+ . collect :: < Result < _ , _ > > ( ) ?,
265+ } )
266+ }
267+ }
268+
269+ /// JSON transport type for the initial branch integration proposal.
270+ #[ derive( Debug , Serialize ) ]
271+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
272+ #[ serde( rename_all = "camelCase" ) ]
273+ pub struct InitialBranchIntegration {
274+ /// The editable execution plan for integrating the branch upstream.
275+ pub integration : InteractiveIntegration ,
276+ /// The current divergence between local branch and upstream for display.
277+ pub divergence : IntegrationDivergenceDisplay ,
278+ }
279+ #[ cfg( feature = "export-schema" ) ]
280+ but_schemars:: register_sdk_type!( InitialBranchIntegration ) ;
281+
282+ impl TryFrom < but_workspace:: branch:: InitialBranchIntegration > for InitialBranchIntegration {
283+ type Error = anyhow:: Error ;
284+
285+ fn try_from (
286+ value : but_workspace:: branch:: InitialBranchIntegration ,
287+ ) -> Result < Self , Self :: Error > {
288+ let but_workspace:: branch:: InitialBranchIntegration {
289+ integration,
290+ divergence,
291+ } = value;
292+ Ok ( Self {
293+ integration : InteractiveIntegration {
294+ merge_base : integration. merge_base . into ( ) ,
295+ steps : integration
296+ . steps
297+ . into_iter ( )
298+ . map ( |step| match step {
299+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Skip { commit_id } => {
300+ InteractiveIntegrationStep :: Skip {
301+ commit_id : commit_id. into ( ) ,
302+ }
303+ }
304+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Pick { commit_id } => {
305+ InteractiveIntegrationStep :: Pick {
306+ commit_id : commit_id. into ( ) ,
307+ }
308+ }
309+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Squash { commits, message } => {
310+ InteractiveIntegrationStep :: Squash {
311+ commits : commits. into_iter ( ) . map ( Into :: into) . collect ( ) ,
312+ message,
313+ }
314+ }
315+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Merge { commit_id } => {
316+ InteractiveIntegrationStep :: Merge {
317+ commit_id : commit_id. into ( ) ,
318+ }
319+ }
320+ } )
321+ . collect ( ) ,
322+ } ,
323+ divergence : divergence. into ( ) ,
324+ } )
325+ }
326+ }
81327}
82328
83329/// Applies a branch using the behavior described by [`apply_only_with_perm()`].
@@ -187,6 +433,73 @@ pub fn branch_diff(ctx: &Context, branch: String) -> anyhow::Result<TreeChanges>
187433 but_workspace:: ui:: diff:: changes_in_branch ( & repo, & ws, branch. name ( ) )
188434}
189435
436+ /// Get the initial upstream integration script for `branch`.
437+ #[ but_api( napi, try_from = json:: InitialBranchIntegration ) ]
438+ #[ instrument( err( Debug ) ) ]
439+ pub fn get_initial_branch_integration (
440+ ctx : & Context ,
441+ branch : & gix:: refs:: FullNameRef ,
442+ ) -> anyhow:: Result < InitialBranchIntegration > {
443+ let repo = ctx. repo . get ( ) ?;
444+ but_workspace:: branch:: get_initial_integration_steps_for_branch ( branch, & repo)
445+ }
446+
447+ /// Apply `integration` to `branch`.
448+ ///
449+ /// This acquires exclusive worktree access from `ctx`, applies the integration
450+ /// steps to the branch, and records an oplog snapshot on success. When
451+ /// `dry_run` is enabled, the returned workspace previews the integration
452+ /// result and no oplog entry is persisted.
453+ #[ but_api( napi, try_from = json:: IntegrateBranchResult ) ]
454+ #[ instrument( err( Debug ) ) ]
455+ pub fn apply_branch_integration (
456+ ctx : & mut but_ctx:: Context ,
457+ branch : & gix:: refs:: FullNameRef ,
458+ integration : json:: InteractiveIntegration ,
459+ dry_run : DryRun ,
460+ ) -> anyhow:: Result < IntegrateBranchResult > {
461+ let integration = integration. try_into ( ) ?;
462+ let mut guard = ctx. exclusive_worktree_access ( ) ;
463+ apply_branch_integration_with_perm ( ctx, branch, integration, dry_run, guard. write_permission ( ) )
464+ }
465+
466+ /// Apply `integration` to `branch` under caller-held exclusive repository access.
467+ ///
468+ /// It prepares a best-effort oplog snapshot, runs the interactive branch
469+ /// integration, and commits the snapshot only if the operation succeeds. The
470+ /// returned [`IntegrateBranchResult`] contains the post-operation workspace
471+ /// view. When `dry_run` is enabled, it returns a preview of the resulting
472+ /// workspace state and skips oplog persistence.
473+ pub fn apply_branch_integration_with_perm (
474+ ctx : & mut but_ctx:: Context ,
475+ branch : & gix:: refs:: FullNameRef ,
476+ integration : InteractiveIntegration ,
477+ dry_run : DryRun ,
478+ perm : & mut RepoExclusive ,
479+ ) -> anyhow:: Result < IntegrateBranchResult > {
480+ branch_mutation_with_snapshot (
481+ ctx,
482+ perm,
483+ OperationKind :: GenericBranchUpdate ,
484+ dry_run,
485+ |ctx, perm| {
486+ let mut meta = ctx. meta ( ) ?;
487+ let ( repo, mut ws, _) = ctx. workspace_mut_and_db_with_perm ( perm) ?;
488+ let rebase = but_workspace:: branch:: integrate_branch_with_steps (
489+ branch,
490+ integration,
491+ & mut ws,
492+ & mut meta,
493+ & repo,
494+ ) ?;
495+
496+ Ok ( IntegrateBranchResult {
497+ workspace : WorkspaceState :: from_successful_rebase ( rebase, & repo, dry_run) ?,
498+ } )
499+ } ,
500+ )
501+ }
502+
190503/// Moves a branch using the behavior described by [`move_branch_with_perm()`].
191504///
192505/// This acquires exclusive worktree access from `ctx`, moves `subject_branch`
@@ -299,15 +612,15 @@ pub fn tear_off_branch_with_perm(
299612 )
300613}
301614
302- fn branch_mutation_with_snapshot < F > (
615+ fn branch_mutation_with_snapshot < T , F > (
303616 ctx : & mut but_ctx:: Context ,
304617 perm : & mut RepoExclusive ,
305618 operation_kind : OperationKind ,
306619 dry_run : DryRun ,
307620 operation : F ,
308- ) -> anyhow:: Result < MoveBranchResult >
621+ ) -> anyhow:: Result < T >
309622where
310- F : FnOnce ( & mut but_ctx:: Context , & mut RepoExclusive ) -> anyhow:: Result < MoveBranchResult > ,
623+ F : FnOnce ( & mut but_ctx:: Context , & mut RepoExclusive ) -> anyhow:: Result < T > ,
311624{
312625 let maybe_oplog_entry = but_oplog:: UnmaterializedOplogSnapshot :: from_details_with_perm (
313626 ctx,
0 commit comments