@@ -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,253 @@ 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+ /// Pick an upstream commit.
197+ PickUpstream {
198+ /// The upstream commit to integrate into the branch.
199+ commit_id : crate :: json:: HexHashString ,
200+ } ,
201+ /// Squash multiple commits into one.
202+ Squash {
203+ /// The ordered commits to squash together.
204+ commits : Vec < crate :: json:: HexHashString > ,
205+ /// Optional replacement message for the squash commit.
206+ message : Option < String > ,
207+ } ,
208+ /// Merge a commit into the previous one.
209+ Merge {
210+ /// The commit whose change range should be merged.
211+ commit_id : crate :: json:: HexHashString ,
212+ } ,
213+ }
214+ #[ cfg( feature = "export-schema" ) ]
215+ but_schemars:: register_sdk_type!( InteractiveIntegrationStep ) ;
216+
217+ impl TryFrom < InteractiveIntegrationStep >
218+ for but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep
219+ {
220+ type Error = anyhow:: Error ;
221+
222+ fn try_from ( value : InteractiveIntegrationStep ) -> Result < Self , Self :: Error > {
223+ Ok ( match value {
224+ InteractiveIntegrationStep :: Skip { commit_id } => Self :: Skip {
225+ commit_id : commit_id. try_into ( ) ?,
226+ } ,
227+ InteractiveIntegrationStep :: Pick { commit_id } => Self :: Pick {
228+ commit_id : commit_id. try_into ( ) ?,
229+ } ,
230+ InteractiveIntegrationStep :: PickUpstream { commit_id } => Self :: PickUpstream {
231+ commit_id : commit_id. try_into ( ) ?,
232+ } ,
233+ InteractiveIntegrationStep :: Squash { commits, message } => Self :: Squash {
234+ commits : commits
235+ . into_iter ( )
236+ . map ( TryInto :: try_into)
237+ . collect :: < Result < _ , _ > > ( ) ?,
238+ message,
239+ } ,
240+ InteractiveIntegrationStep :: Merge { commit_id } => Self :: Merge {
241+ commit_id : commit_id. try_into ( ) ?,
242+ } ,
243+ } )
244+ }
245+ }
246+
247+ /// JSON transport type describing an interactive branch integration plan.
248+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
249+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
250+ #[ serde( rename_all = "camelCase" ) ]
251+ pub struct InteractiveIntegration {
252+ /// Merge base between the upstream and the local reference.
253+ pub merge_base : crate :: json:: HexHashString ,
254+ /// The ordered integration steps to apply.
255+ pub steps : Vec < InteractiveIntegrationStep > ,
256+ }
257+ #[ cfg( feature = "export-schema" ) ]
258+ but_schemars:: register_sdk_type!( InteractiveIntegration ) ;
259+
260+ impl TryFrom < InteractiveIntegration >
261+ for but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegration
262+ {
263+ type Error = anyhow:: Error ;
264+
265+ fn try_from ( value : InteractiveIntegration ) -> Result < Self , Self :: Error > {
266+ let InteractiveIntegration { merge_base, steps } = value;
267+ Ok ( Self {
268+ merge_base : merge_base. try_into ( ) ?,
269+ steps : steps
270+ . into_iter ( )
271+ . map ( TryInto :: try_into)
272+ . collect :: < Result < _ , _ > > ( ) ?,
273+ } )
274+ }
275+ }
276+
277+ /// JSON transport type for the initial branch integration proposal.
278+ #[ derive( Debug , Serialize ) ]
279+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
280+ #[ serde( rename_all = "camelCase" ) ]
281+ pub struct InitialBranchIntegration {
282+ /// The editable execution plan for integrating the branch upstream.
283+ pub integration : InteractiveIntegration ,
284+ /// The current divergence between local branch and upstream for display.
285+ pub divergence : IntegrationDivergenceDisplay ,
286+ }
287+ #[ cfg( feature = "export-schema" ) ]
288+ but_schemars:: register_sdk_type!( InitialBranchIntegration ) ;
289+
290+ impl TryFrom < but_workspace:: branch:: InitialBranchIntegration > for InitialBranchIntegration {
291+ type Error = anyhow:: Error ;
292+
293+ fn try_from ( value : but_workspace:: branch:: InitialBranchIntegration ) -> Result < Self , Self :: Error > {
294+ let but_workspace:: branch:: InitialBranchIntegration {
295+ integration,
296+ divergence,
297+ } = value;
298+ Ok ( Self {
299+ integration : InteractiveIntegration {
300+ merge_base : integration. merge_base . into ( ) ,
301+ steps : integration
302+ . steps
303+ . into_iter ( )
304+ . map ( |step| match step {
305+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Skip { commit_id } => {
306+ InteractiveIntegrationStep :: Skip {
307+ commit_id : commit_id. into ( ) ,
308+ }
309+ }
310+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Pick { commit_id } => {
311+ InteractiveIntegrationStep :: Pick {
312+ commit_id : commit_id. into ( ) ,
313+ }
314+ }
315+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: PickUpstream { commit_id } => {
316+ InteractiveIntegrationStep :: PickUpstream {
317+ commit_id : commit_id. into ( ) ,
318+ }
319+ }
320+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Squash { commits, message } => {
321+ InteractiveIntegrationStep :: Squash {
322+ commits : commits. into_iter ( ) . map ( Into :: into) . collect ( ) ,
323+ message,
324+ }
325+ }
326+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Merge { commit_id } => {
327+ InteractiveIntegrationStep :: Merge {
328+ commit_id : commit_id. into ( ) ,
329+ }
330+ }
331+ } )
332+ . collect ( ) ,
333+ } ,
334+ divergence : divergence. into ( ) ,
335+ } )
336+ }
337+ }
81338}
82339
83340/// Applies a branch using the behavior described by [`apply_only_with_perm()`].
@@ -187,6 +444,73 @@ pub fn branch_diff(ctx: &Context, branch: String) -> anyhow::Result<TreeChanges>
187444 but_workspace:: ui:: diff:: changes_in_branch ( & repo, & ws, branch. name ( ) )
188445}
189446
447+ /// Get the initial upstream integration script for `branch`.
448+ #[ but_api( napi, try_from = json:: InitialBranchIntegration ) ]
449+ #[ instrument( err( Debug ) ) ]
450+ pub fn get_initial_branch_integration (
451+ ctx : & Context ,
452+ branch : & gix:: refs:: FullNameRef ,
453+ ) -> anyhow:: Result < InitialBranchIntegration > {
454+ let repo = ctx. repo . get ( ) ?;
455+ but_workspace:: branch:: get_initial_integration_steps_for_branch ( branch, & repo)
456+ }
457+
458+ /// Apply `integration` to `branch`.
459+ ///
460+ /// This acquires exclusive worktree access from `ctx`, applies the integration
461+ /// steps to the branch, and records an oplog snapshot on success. When
462+ /// `dry_run` is enabled, the returned workspace previews the integration
463+ /// result and no oplog entry is persisted.
464+ #[ but_api( napi, try_from = json:: IntegrateBranchResult ) ]
465+ #[ instrument( err( Debug ) ) ]
466+ pub fn apply_branch_integration (
467+ ctx : & mut but_ctx:: Context ,
468+ branch : & gix:: refs:: FullNameRef ,
469+ integration : json:: InteractiveIntegration ,
470+ dry_run : DryRun ,
471+ ) -> anyhow:: Result < IntegrateBranchResult > {
472+ let integration = integration. try_into ( ) ?;
473+ let mut guard = ctx. exclusive_worktree_access ( ) ;
474+ apply_branch_integration_with_perm ( ctx, branch, integration, dry_run, guard. write_permission ( ) )
475+ }
476+
477+ /// Apply `integration` to `branch` under caller-held exclusive repository access.
478+ ///
479+ /// It prepares a best-effort oplog snapshot, runs the interactive branch
480+ /// integration, and commits the snapshot only if the operation succeeds. The
481+ /// returned [`IntegrateBranchResult`] contains the post-operation workspace
482+ /// view. When `dry_run` is enabled, it returns a preview of the resulting
483+ /// workspace state and skips oplog persistence.
484+ pub fn apply_branch_integration_with_perm (
485+ ctx : & mut but_ctx:: Context ,
486+ branch : & gix:: refs:: FullNameRef ,
487+ integration : InteractiveIntegration ,
488+ dry_run : DryRun ,
489+ perm : & mut RepoExclusive ,
490+ ) -> anyhow:: Result < IntegrateBranchResult > {
491+ branch_mutation_with_snapshot (
492+ ctx,
493+ perm,
494+ OperationKind :: GenericBranchUpdate ,
495+ dry_run,
496+ |ctx, perm| {
497+ let mut meta = ctx. meta ( ) ?;
498+ let ( repo, mut ws, _) = ctx. workspace_mut_and_db_with_perm ( perm) ?;
499+ let rebase = but_workspace:: branch:: integrate_branch_with_steps (
500+ branch,
501+ integration,
502+ & mut ws,
503+ & mut meta,
504+ & repo,
505+ ) ?;
506+
507+ Ok ( IntegrateBranchResult {
508+ workspace : WorkspaceState :: from_successful_rebase ( rebase, & repo, dry_run) ?,
509+ } )
510+ } ,
511+ )
512+ }
513+
190514/// Moves a branch using the behavior described by [`move_branch_with_perm()`].
191515///
192516/// This acquires exclusive worktree access from `ctx`, moves `subject_branch`
@@ -299,15 +623,15 @@ pub fn tear_off_branch_with_perm(
299623 )
300624}
301625
302- fn branch_mutation_with_snapshot < F > (
626+ fn branch_mutation_with_snapshot < T , F > (
303627 ctx : & mut but_ctx:: Context ,
304628 perm : & mut RepoExclusive ,
305629 operation_kind : OperationKind ,
306630 dry_run : DryRun ,
307631 operation : F ,
308- ) -> anyhow:: Result < MoveBranchResult >
632+ ) -> anyhow:: Result < T >
309633where
310- F : FnOnce ( & mut but_ctx:: Context , & mut RepoExclusive ) -> anyhow:: Result < MoveBranchResult > ,
634+ F : FnOnce ( & mut but_ctx:: Context , & mut RepoExclusive ) -> anyhow:: Result < T > ,
311635{
312636 let maybe_oplog_entry = but_oplog:: UnmaterializedOplogSnapshot :: from_details_with_perm (
313637 ctx,
0 commit comments