@@ -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,229 @@ 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+ /// Pick a commit, keeping it in the branch.
187+ Pick {
188+ /// The local commit to keep in the rewritten branch.
189+ commit_id : crate :: json:: HexHashString ,
190+ } ,
191+ /// Squash multiple commits into one.
192+ Squash {
193+ /// The ordered commits to squash together.
194+ commits : Vec < crate :: json:: HexHashString > ,
195+ /// Optional replacement message for the squash commit.
196+ message : Option < String > ,
197+ } ,
198+ /// Merge a commit into the previous one.
199+ Merge {
200+ /// The commit whose change range should be merged.
201+ commit_id : crate :: json:: HexHashString ,
202+ } ,
203+ }
204+ #[ cfg( feature = "export-schema" ) ]
205+ but_schemars:: register_sdk_type!( InteractiveIntegrationStep ) ;
206+
207+ impl TryFrom < InteractiveIntegrationStep >
208+ for but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep
209+ {
210+ type Error = anyhow:: Error ;
211+
212+ fn try_from ( value : InteractiveIntegrationStep ) -> Result < Self , Self :: Error > {
213+ Ok ( match value {
214+ InteractiveIntegrationStep :: Pick { commit_id } => Self :: Pick {
215+ commit_id : commit_id. try_into ( ) ?,
216+ } ,
217+ InteractiveIntegrationStep :: Squash { commits, message } => Self :: Squash {
218+ commits : commits
219+ . into_iter ( )
220+ . map ( TryInto :: try_into)
221+ . collect :: < Result < _ , _ > > ( ) ?,
222+ message,
223+ } ,
224+ InteractiveIntegrationStep :: Merge { commit_id } => Self :: Merge {
225+ commit_id : commit_id. try_into ( ) ?,
226+ } ,
227+ } )
228+ }
229+ }
230+
231+ /// JSON transport type describing an interactive branch integration plan.
232+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
233+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
234+ #[ serde( rename_all = "camelCase" ) ]
235+ pub struct InteractiveIntegration {
236+ /// Merge base between the upstream and the local reference.
237+ pub merge_base : crate :: json:: HexHashString ,
238+ /// The ordered integration steps to apply.
239+ pub steps : Vec < InteractiveIntegrationStep > ,
240+ }
241+ #[ cfg( feature = "export-schema" ) ]
242+ but_schemars:: register_sdk_type!( InteractiveIntegration ) ;
243+
244+ impl TryFrom < InteractiveIntegration >
245+ for but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegration
246+ {
247+ type Error = anyhow:: Error ;
248+
249+ fn try_from ( value : InteractiveIntegration ) -> Result < Self , Self :: Error > {
250+ let InteractiveIntegration { merge_base, steps } = value;
251+ Ok ( Self {
252+ merge_base : merge_base. try_into ( ) ?,
253+ steps : steps
254+ . into_iter ( )
255+ . map ( TryInto :: try_into)
256+ . collect :: < Result < _ , _ > > ( ) ?,
257+ } )
258+ }
259+ }
260+
261+ /// JSON transport type for the initial branch integration proposal.
262+ #[ derive( Debug , Serialize ) ]
263+ #[ cfg_attr( feature = "export-schema" , derive( schemars:: JsonSchema ) ) ]
264+ #[ serde( rename_all = "camelCase" ) ]
265+ pub struct InitialBranchIntegration {
266+ /// The editable execution plan for integrating the branch upstream.
267+ pub integration : InteractiveIntegration ,
268+ /// The current divergence between local branch and upstream for display.
269+ pub divergence : IntegrationDivergenceDisplay ,
270+ }
271+ #[ cfg( feature = "export-schema" ) ]
272+ but_schemars:: register_sdk_type!( InitialBranchIntegration ) ;
273+
274+ impl TryFrom < but_workspace:: branch:: InitialBranchIntegration > for InitialBranchIntegration {
275+ type Error = anyhow:: Error ;
276+
277+ fn try_from (
278+ value : but_workspace:: branch:: InitialBranchIntegration ,
279+ ) -> Result < Self , Self :: Error > {
280+ let but_workspace:: branch:: InitialBranchIntegration {
281+ integration,
282+ divergence,
283+ } = value;
284+ Ok ( Self {
285+ integration : InteractiveIntegration {
286+ merge_base : integration. merge_base . into ( ) ,
287+ steps : integration
288+ . steps
289+ . into_iter ( )
290+ . map ( |step| match step {
291+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Pick { commit_id } => {
292+ InteractiveIntegrationStep :: Pick {
293+ commit_id : commit_id. into ( ) ,
294+ }
295+ }
296+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Squash { commits, message } => {
297+ InteractiveIntegrationStep :: Squash {
298+ commits : commits. into_iter ( ) . map ( Into :: into) . collect ( ) ,
299+ message,
300+ }
301+ }
302+ but_workspace:: branch:: integrate_branch_upstream:: InteractiveIntegrationStep :: Merge { commit_id } => {
303+ InteractiveIntegrationStep :: Merge {
304+ commit_id : commit_id. into ( ) ,
305+ }
306+ }
307+ } )
308+ . collect ( ) ,
309+ } ,
310+ divergence : divergence. into ( ) ,
311+ } )
312+ }
313+ }
81314}
82315
83316/// Applies a branch using the behavior described by [`apply_only_with_perm()`].
@@ -187,6 +420,73 @@ pub fn branch_diff(ctx: &Context, branch: String) -> anyhow::Result<TreeChanges>
187420 but_workspace:: ui:: diff:: changes_in_branch ( & repo, & ws, branch. name ( ) )
188421}
189422
423+ /// Get the initial upstream integration script for `branch`.
424+ #[ but_api( napi, try_from = json:: InitialBranchIntegration ) ]
425+ #[ instrument( err( Debug ) ) ]
426+ pub fn get_initial_branch_integration (
427+ ctx : & Context ,
428+ branch : & gix:: refs:: FullNameRef ,
429+ ) -> anyhow:: Result < InitialBranchIntegration > {
430+ let repo = ctx. repo . get ( ) ?;
431+ but_workspace:: branch:: get_initial_integration_steps_for_branch ( branch, & repo)
432+ }
433+
434+ /// Apply `integration` to `branch`.
435+ ///
436+ /// This acquires exclusive worktree access from `ctx`, applies the integration
437+ /// steps to the branch, and records an oplog snapshot on success. When
438+ /// `dry_run` is enabled, the returned workspace previews the integration
439+ /// result and no oplog entry is persisted.
440+ #[ but_api( napi, try_from = json:: IntegrateBranchResult ) ]
441+ #[ instrument( err( Debug ) ) ]
442+ pub fn apply_branch_integration (
443+ ctx : & mut but_ctx:: Context ,
444+ branch : & gix:: refs:: FullNameRef ,
445+ integration : json:: InteractiveIntegration ,
446+ dry_run : DryRun ,
447+ ) -> anyhow:: Result < IntegrateBranchResult > {
448+ let integration = integration. try_into ( ) ?;
449+ let mut guard = ctx. exclusive_worktree_access ( ) ;
450+ apply_branch_integration_with_perm ( ctx, branch, integration, dry_run, guard. write_permission ( ) )
451+ }
452+
453+ /// Apply `integration` to `branch` under caller-held exclusive repository access.
454+ ///
455+ /// It prepares a best-effort oplog snapshot, runs the interactive branch
456+ /// integration, and commits the snapshot only if the operation succeeds. The
457+ /// returned [`IntegrateBranchResult`] contains the post-operation workspace
458+ /// view. When `dry_run` is enabled, it returns a preview of the resulting
459+ /// workspace state and skips oplog persistence.
460+ pub fn apply_branch_integration_with_perm (
461+ ctx : & mut but_ctx:: Context ,
462+ branch : & gix:: refs:: FullNameRef ,
463+ integration : InteractiveIntegration ,
464+ dry_run : DryRun ,
465+ perm : & mut RepoExclusive ,
466+ ) -> anyhow:: Result < IntegrateBranchResult > {
467+ branch_mutation_with_snapshot (
468+ ctx,
469+ perm,
470+ OperationKind :: GenericBranchUpdate ,
471+ dry_run,
472+ |ctx, perm| {
473+ let mut meta = ctx. meta ( ) ?;
474+ let ( repo, mut ws, _) = ctx. workspace_mut_and_db_with_perm ( perm) ?;
475+ let rebase = but_workspace:: branch:: integrate_branch_with_steps (
476+ branch,
477+ integration,
478+ & mut ws,
479+ & mut meta,
480+ & repo,
481+ ) ?;
482+
483+ Ok ( IntegrateBranchResult {
484+ workspace : WorkspaceState :: from_successful_rebase ( rebase, & repo, dry_run) ?,
485+ } )
486+ } ,
487+ )
488+ }
489+
190490/// Moves a branch using the behavior described by [`move_branch_with_perm()`].
191491///
192492/// This acquires exclusive worktree access from `ctx`, moves `subject_branch`
@@ -299,15 +599,15 @@ pub fn tear_off_branch_with_perm(
299599 )
300600}
301601
302- fn branch_mutation_with_snapshot < F > (
602+ fn branch_mutation_with_snapshot < T , F > (
303603 ctx : & mut but_ctx:: Context ,
304604 perm : & mut RepoExclusive ,
305605 operation_kind : OperationKind ,
306606 dry_run : DryRun ,
307607 operation : F ,
308- ) -> anyhow:: Result < MoveBranchResult >
608+ ) -> anyhow:: Result < T >
309609where
310- F : FnOnce ( & mut but_ctx:: Context , & mut RepoExclusive ) -> anyhow:: Result < MoveBranchResult > ,
610+ F : FnOnce ( & mut but_ctx:: Context , & mut RepoExclusive ) -> anyhow:: Result < T > ,
311611{
312612 let maybe_oplog_entry = but_oplog:: UnmaterializedOplogSnapshot :: from_details_with_perm (
313613 ctx,
0 commit comments