Merge pull request #5357 from gitbutlerapp/persist-forge-ids-on-series

Persist ForgeIdentifier on series (branches)
This commit is contained in:
Kiril Videlov 2024-10-30 10:41:54 +01:00 committed by GitHub
commit ad0a0a20d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 283 additions and 18 deletions

View File

@ -12,7 +12,7 @@
import Modal from '@gitbutler/ui/Modal.svelte';
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
import type { PullRequest } from '$lib/gitHost/interface/types';
import type { Branch } from '$lib/vbranches/types';
import type { Branch, ForgeIdentifier } from '$lib/vbranches/types';
import { goto } from '$app/navigation';
export let localBranch: Branch | undefined;
@ -111,7 +111,14 @@
remoteBranch?.name
);
} else {
await branchController.createvBranchFromBranch(remoteBranch!.name);
let forgeId: ForgeIdentifier | undefined = pr
? { type: 'GitHub', subject: { prNumber: pr.number } }
: undefined;
await branchController.createvBranchFromBranch(
remoteBranch!.name,
undefined,
forgeId
);
}
goto(`/${project.id}/board`);
} catch (e) {

View File

@ -67,7 +67,9 @@
await remotesService.addRemote(project.id, remoteName, remoteUrl);
await baseBranchService.fetchFromRemotes();
await branchController.createvBranchFromBranch(
`refs/remotes/${remoteName}/${pullrequest.sourceBranch}`
`refs/remotes/${remoteName}/${pullrequest.sourceBranch}`,
undefined,
{ type: 'GitHub', subject: { prNumber: pullrequest.number } }
);
await virtualBranchService.refresh();

View File

@ -4,7 +4,7 @@ import * as toasts from '$lib/utils/toasts';
import posthog from 'posthog-js';
import type { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
import type { BranchPushResult, Hunk, LocalFile } from './types';
import type { BranchPushResult, ForgeIdentifier, Hunk, LocalFile } from './types';
import type { VirtualBranchService } from './virtualBranch';
export type CommitIdOrChangeId = { CommitId: string } | { ChangeId: string };
@ -154,6 +154,30 @@ export class BranchController {
}
}
/**
* Updates the forge identifier for a branch/series.
* This is useful for storing for example the Pull Request Number for a branch.
* @param stackId The stack ID to update.
* @param headName The branch name to update.
* @param forgeId New forge id to be set for the branch (overrides current state). Setting to undefined will remove the forge id.
*/
async updateSeriesForgeId(
stackId: string,
headName: string,
forgeId: ForgeIdentifier | undefined
) {
try {
await invoke<void>('update_series_forge_ids', {
projectId: this.projectId,
stackId,
headName,
forgeId
});
} catch (err) {
showError('Failed to update branch forge ids', err);
}
}
/*
* Creates a new GitButler change reference associated with a branch.
* @param branchId
@ -389,12 +413,17 @@ export class BranchController {
* have a local branch, this should be the branch.
* @param remote Optionally sets another branch as the upstream.
*/
async createvBranchFromBranch(branch: string, remote: string | undefined = undefined) {
async createvBranchFromBranch(
branch: string,
remote: string | undefined = undefined,
forgeId: ForgeIdentifier | undefined = undefined
) {
try {
await invoke<string>('create_virtual_branch_from_branch', {
projectId: this.projectId,
branch,
remote
remote,
forgeId
});
} catch (err) {
showError('Failed to create virtual branch', err);

View File

@ -445,6 +445,12 @@ export class PatchSeries {
@Type(() => DetailedCommit)
upstreamPatches!: DetailedCommit[];
/**
* A list of identifiers for the review unit at possible forges (eg. Pull Request).
* The list is empty if there is no review units, eg. no Pull Request has been created.
*/
forgeId?: ForgeIdentifier | undefined;
get localCommits() {
return this.patches.filter((c) => c.status === 'local');
}
@ -465,3 +471,14 @@ export class PatchSeries {
return this.name?.replace('refs/remotes/origin/', '');
}
}
/**
* Represents a GitHub Pull Request identifier.
*/
export interface GitHubIdentifier {
prNumber: number;
}
/**
* Represents identifiers for the series at possible forges, eg. GitHub PR numbers.
*/
export type ForgeIdentifier = { type: 'GitHub'; subject: GitHubIdentifier };

View File

@ -25,6 +25,7 @@ use gitbutler_oplog::{
entry::{OperationKind, SnapshotDetails},
OplogExt, SnapshotExt,
};
use gitbutler_patch_reference::ForgeIdentifier;
use gitbutler_project::{FetchResult, Project};
use gitbutler_reference::{ReferenceName, Refname, RemoteRefname};
use gitbutler_repo::RepositoryExt;
@ -533,6 +534,7 @@ pub fn create_virtual_branch_from_branch(
project: &Project,
branch: &Refname,
remote: Option<RemoteRefname>,
forge_id: Option<ForgeIdentifier>,
) -> Result<StackId> {
let ctx = open_with_verify(project)?;
assure_open_workspace_mode(&ctx)
@ -540,7 +542,7 @@ pub fn create_virtual_branch_from_branch(
let branch_manager = ctx.branch_manager();
let mut guard = project.exclusive_worktree_access();
branch_manager
.create_virtual_branch_from_branch(branch, remote, guard.write_permission())
.create_virtual_branch_from_branch(branch, remote, forge_id, guard.write_permission())
.map_err(Into::into)
}

View File

@ -6,6 +6,7 @@ use gitbutler_cherry_pick::RepositoryExt as _;
use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders};
use gitbutler_error::error::Marker;
use gitbutler_oplog::SnapshotExt;
use gitbutler_patch_reference::ForgeIdentifier;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_reference::{Refname, RemoteRefname};
use gitbutler_repo::{
@ -122,6 +123,7 @@ impl BranchManager<'_> {
&self,
target: &Refname,
upstream_branch: Option<RemoteRefname>,
forge_id: Option<ForgeIdentifier>,
perm: &mut WorktreeWritePermission,
) -> Result<StackId> {
// only set upstream if it's not the default target
@ -247,6 +249,9 @@ impl BranchManager<'_> {
)
};
if let (Some(forge_id), Some(head)) = (forge_id, branch.heads().last()) {
branch.set_forge_id(self.ctx, head, Some(forge_id))?;
}
branch.set_stack_head(self.ctx, head_commit.id(), Some(head_commit_tree.id()))?;
self.ctx.add_branch_reference(&branch)?;

View File

@ -3,7 +3,7 @@ use std::collections::HashMap;
use anyhow::{Context, Result};
use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_patch_reference::{CommitOrChangeId, PatchReference};
use gitbutler_patch_reference::{CommitOrChangeId, ForgeIdentifier, PatchReference};
use gitbutler_project::Project;
use gitbutler_repo_actions::RepoActionsExt;
use gitbutler_stack::{PatchReferenceUpdate, Series};
@ -45,6 +45,7 @@ pub fn create_series(
target: target_patch,
name: req.name,
description: req.description,
forge_id: Default::default(),
},
req.preceding_head,
)
@ -121,6 +122,27 @@ pub fn update_series_description(
)
}
/// Sets the forge identifiers for a given series/branch. Existing values are overwritten.
///
/// # Errors
/// This method will return an error if:
/// - The series does not exist
/// - The stack cant be found
/// - The stack has not been initialized
/// - The project is not in workspace mode
/// - Persisting the changes failed
pub fn update_series_forge_ids(
project: &Project,
stack_id: StackId,
head_name: String,
forge_id: Option<ForgeIdentifier>,
) -> Result<()> {
let ctx = &open_with_verify(project)?;
assure_open_workspace_mode(ctx).context("Requires an open workspace mode")?;
let mut stack = ctx.project().virtual_branches().get_branch(stack_id)?;
stack.set_forge_id(ctx, &head_name, forge_id)
}
/// Pushes all series in the stack to the remote.
/// This operation will error out if the target has no push remote configured.
pub fn push_stack(project: &Project, branch_id: StackId, with_force: bool) -> Result<()> {
@ -260,6 +282,7 @@ pub(crate) fn stack_series(
upstream_reference,
patches,
upstream_patches,
forge_id: series.head.forge_id,
});
}
api_series.reverse();

View File

@ -21,6 +21,7 @@ use gitbutler_diff::{trees, GitHunk, Hunk};
use gitbutler_error::error::Code;
use gitbutler_operating_modes::assure_open_workspace_mode;
use gitbutler_oxidize::git2_signature_to_gix_signature;
use gitbutler_patch_reference::ForgeIdentifier;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname};
use gitbutler_repo::{
@ -96,6 +97,9 @@ pub struct PatchSeries {
pub patches: Vec<VirtualBranchCommit>,
/// List of patches that only exist on the upstream branch
pub upstream_patches: Vec<VirtualBranchCommit>,
/// A list of identifiers for the review unit at possible forges (eg. Pull Request).
/// The list is empty if there is no review units, eg. no Pull Request has been created.
pub forge_id: Option<ForgeIdentifier>,
}
#[derive(Debug, PartialEq, Clone, Serialize)]

View File

@ -1137,6 +1137,7 @@ fn unapply_branch() -> Result<()> {
let branch1_id = branch_manager.create_virtual_branch_from_branch(
&Refname::from_str(&real_branch)?,
None,
None,
guard.write_permission(),
)?;
let contents = std::fs::read(Path::new(&project.path).join(file_path))?;
@ -1219,6 +1220,7 @@ fn apply_unapply_added_deleted_files() -> Result<()> {
.create_virtual_branch_from_branch(
&Refname::from_str(&real_branch_2).unwrap(),
None,
None,
guard.write_permission(),
)
.unwrap();
@ -1230,6 +1232,7 @@ fn apply_unapply_added_deleted_files() -> Result<()> {
.create_virtual_branch_from_branch(
&Refname::from_str(&real_branch_3).unwrap(),
None,
None,
guard.write_permission(),
)
.unwrap();

View File

@ -94,6 +94,7 @@ fn rebase_commit() {
project,
&unapplied_branch,
None,
None,
)
.unwrap();
@ -192,6 +193,7 @@ fn rebase_work() {
project,
&unapplied_branch,
None,
None,
)
.unwrap();

View File

@ -1,4 +1,5 @@
use gitbutler_branch::BranchCreateRequest;
use gitbutler_patch_reference::{ForgeIdentifier, GitHubIdentifier};
use gitbutler_reference::LocalRefname;
use super::*;
@ -46,9 +47,13 @@ fn integration() {
};
// checkout a existing remote branch
let branch_id =
gitbutler_branch_actions::create_virtual_branch_from_branch(project, &branch_name, None)
.unwrap();
let branch_id = gitbutler_branch_actions::create_virtual_branch_from_branch(
project,
&branch_name,
None,
Some(ForgeIdentifier::GitHub(GitHubIdentifier { pr_number: 123 })),
)
.unwrap();
{
// add a commit
@ -96,6 +101,11 @@ fn integration() {
.find(|branch| branch.id == branch_id)
.unwrap();
assert_eq!(
branch.series.first().unwrap().forge_id,
Some(ForgeIdentifier::GitHub(GitHubIdentifier { pr_number: 123 }))
);
assert!(branch.commits[0].is_remote);
assert!(branch.commits[0].is_integrated);
assert!(branch.commits[1].is_remote);
@ -134,6 +144,7 @@ fn no_conflicts() {
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None,
None,
)
.unwrap();
@ -182,6 +193,7 @@ fn conflicts_with_uncommited() {
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None,
None,
)
.unwrap();
let new_branch = gitbutler_branch_actions::list_virtual_branches(project)
@ -236,6 +248,7 @@ fn conflicts_with_commited() {
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None,
None,
)
.unwrap();
let new_branch = gitbutler_branch_actions::list_virtual_branches(project)
@ -265,7 +278,8 @@ fn from_default_target() {
gitbutler_branch_actions::create_virtual_branch_from_branch(
project,
&"refs/remotes/origin/master".parse().unwrap(),
None
None,
None,
)
.unwrap_err()
.to_string(),
@ -289,7 +303,8 @@ fn from_non_existent_branch() {
gitbutler_branch_actions::create_virtual_branch_from_branch(
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None
None,
None,
)
.unwrap_err()
.to_string(),
@ -330,6 +345,7 @@ fn from_state_remote_branch() {
project,
&"refs/remotes/origin/branch".parse().unwrap(),
None,
None,
)
.unwrap();
@ -420,6 +436,7 @@ mod conflict_cases {
project,
&Refname::from_str(&branch_refname).unwrap(),
None,
None,
)
.unwrap();

View File

@ -316,8 +316,13 @@ fn applying_first_branch() {
let unapplied_branch =
gitbutler_branch_actions::save_and_unapply_virutal_branch(project, branches[0].id).unwrap();
let unapplied_branch = Refname::from_str(&unapplied_branch).unwrap();
gitbutler_branch_actions::create_virtual_branch_from_branch(project, &unapplied_branch, None)
.unwrap();
gitbutler_branch_actions::create_virtual_branch_from_branch(
project,
&unapplied_branch,
None,
None,
)
.unwrap();
let (branches, _) = gitbutler_branch_actions::list_virtual_branches(project).unwrap();
assert_eq!(branches.len(), 1);

View File

@ -17,6 +17,25 @@ pub struct PatchReference {
pub name: String,
/// Optional description of the series. This could be markdown or anything our hearts desire.
pub description: Option<String>,
/// An identifier for a review unit at a forge (eg. GitHub Pull Request number).
/// None if is no review unit, eg. no Pull Request has been created.
#[serde(default)]
pub forge_id: Option<ForgeIdentifier>,
}
/// Represents identifiers for the series at possible forges, eg. GitHub PR numbers.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "type", content = "subject")]
pub enum ForgeIdentifier {
GitHub(GitHubIdentifier),
}
/// Represents a GitHub Pull Request identifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitHubIdentifier {
/// Pull Request number.
pub pr_number: usize,
}
/// A patch identifier which is either `CommitId` or a `ChangeId`.

View File

@ -11,6 +11,7 @@ use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_commit::commit_ext::CommitVecExt;
use gitbutler_id::id::Id;
use gitbutler_patch_reference::ForgeIdentifier;
use gitbutler_patch_reference::{CommitOrChangeId, PatchReference};
use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname, VirtualRefname};
use gitbutler_repo::{LogUntil, RepositoryExt};
@ -229,6 +230,7 @@ impl Stack {
generate_branch_name(author)?
},
description: None,
forge_id: Default::default(),
};
let state = branch_state(ctx);
@ -302,6 +304,7 @@ impl Stack {
target: current_top_head.target.clone(),
name,
description,
forge_id: Default::default(),
};
self.add_series(ctx, new_head, Some(current_top_head.name.clone()))
}
@ -655,6 +658,34 @@ impl Stack {
Ok(())
}
/// Sets the forge identifier for a given series/branch.
/// Existing value is overwritten - passing `None` sets the forge identifier to `None`.
///
/// # Errors
/// If the series does not exist, this method will return an error.
/// If the stack has not been initialized, this method will return an error.
pub fn set_forge_id(
&mut self,
ctx: &CommandContext,
series_name: &str,
new_forge_id: Option<ForgeIdentifier>,
) -> Result<()> {
if !self.initialized() {
return Err(anyhow!("Stack has not been initialized"));
}
match self.heads.iter_mut().find(|r| r.name == series_name) {
Some(head) => {
head.forge_id = new_forge_id;
branch_state(ctx).set_branch(self.clone())
}
None => bail!(
"Series {} does not exist on stack {}",
series_name,
self.name
),
}
}
pub fn set_legacy_compatible_stack_reference(&mut self, ctx: &CommandContext) -> Result<()> {
// self.upstream is only set if this is a branch that was created & manipulated by the legacy flow
let legacy_refname = match self.upstream.clone().map(|r| r.branch().to_owned()) {
@ -687,6 +718,10 @@ impl Stack {
}
Ok(())
}
pub fn heads(&self) -> Vec<String> {
self.heads.iter().map(|h| h.name.clone()).collect()
}
}
/// Request to update a PatchReference.

View File

@ -4,7 +4,9 @@ pub mod ownership;
use anyhow::Result;
use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_patch_reference::{CommitOrChangeId, PatchReference};
use gitbutler_patch_reference::{
CommitOrChangeId, ForgeIdentifier, GitHubIdentifier, PatchReference,
};
use gitbutler_reference::RemoteRefname;
use gitbutler_repo::{LogUntil, RepositoryExt as _};
use gitbutler_repo_actions::RepoActionsExt;
@ -58,6 +60,7 @@ fn add_series_success() -> Result<()> {
name: "asdf".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: Some("my description".into()),
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference, None);
assert!(result.is_ok());
@ -112,6 +115,7 @@ fn add_series_top_base() -> Result<()> {
name: "asdf".into(),
target: CommitOrChangeId::CommitId(merge_base.id().to_string()),
description: Some("my description".into()),
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference, None);
println!("{:?}", result);
@ -137,6 +141,7 @@ fn add_multiple_series() -> Result<()> {
name: "head_4".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
let result = test_ctx
.branch
@ -148,6 +153,7 @@ fn add_multiple_series() -> Result<()> {
name: "head_2".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, head_2, None);
assert!(result.is_ok());
@ -160,6 +166,7 @@ fn add_multiple_series() -> Result<()> {
name: "head_1".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, head_1, None);
@ -185,6 +192,7 @@ fn add_series_commit_id_when_change_id_available() -> Result<()> {
name: "asdf".into(),
target: CommitOrChangeId::CommitId(test_ctx.commits[1].id().to_string()),
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference, None);
assert_eq!(
@ -206,6 +214,7 @@ fn add_series_invalid_name_fails() -> Result<()> {
name: "name with spaces".into(),
target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()),
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference, None);
assert_eq!(result.err().unwrap().to_string(), "Invalid branch name");
@ -221,6 +230,7 @@ fn add_series_duplicate_name_fails() -> Result<()> {
name: "asdf".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
assert!(result.is_ok());
@ -241,6 +251,7 @@ fn add_series_matching_git_ref_is_ok() -> Result<()> {
name: "existing-branch".into(),
target: test_ctx.commits[0].clone().into(),
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
assert!(result.is_ok()); // allow this
@ -256,6 +267,7 @@ fn add_series_including_refs_head_fails() -> Result<()> {
name: "refs/heads/my-branch".into(),
target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()),
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
assert_eq!(
@ -274,6 +286,7 @@ fn add_series_target_commit_doesnt_exist() -> Result<()> {
name: "my-branch".into(),
target: CommitOrChangeId::CommitId("30696678319e0fa3a20e54f22d47fc8cf1ceaade".into()), // does not exist
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
assert!(result
@ -293,6 +306,7 @@ fn add_series_target_change_id_doesnt_exist() -> Result<()> {
name: "my-branch".into(),
target: CommitOrChangeId::ChangeId("does-not-exist".into()), // does not exist
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
assert_eq!(
@ -312,6 +326,7 @@ fn add_series_target_commit_not_in_stack() -> Result<()> {
name: "my-branch".into(),
target: CommitOrChangeId::CommitId(other_commit_id.clone()), // does not exist
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
assert_eq!(
@ -368,6 +383,7 @@ fn remove_series_with_multiple_last_heads() -> Result<()> {
name: "to_stay".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, to_stay.clone(), None);
assert!(result.is_ok());
@ -399,6 +415,7 @@ fn remove_series_no_orphan_commits() -> Result<()> {
name: "to_stay".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()),
description: None,
forge_id: Default::default(),
}; // references the oldest commit
let result = test_ctx.branch.add_series(&ctx, to_stay.clone(), None);
assert!(result.is_ok());
@ -561,6 +578,7 @@ fn update_series_target_success() -> Result<()> {
name: "series_1".into(),
target: commit_0_change_id.clone(),
description: None,
forge_id: Default::default(),
};
let result = test_ctx.branch.add_series(&ctx, series_1, None);
assert!(result.is_ok());
@ -662,6 +680,7 @@ fn list_series_two_heads_same_commit() -> Result<()> {
name: "head_before".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
// add `head_before` before the initial head
let result = test_ctx.branch.add_series(&ctx, head_before, None);
@ -697,6 +716,7 @@ fn list_series_two_heads_different_commit() -> Result<()> {
// point to the first commit
target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
// add `head_before` before the initial head
let result = test_ctx.branch.add_series(&ctx, head_before, None);
@ -761,6 +781,7 @@ fn replace_head_single() -> Result<()> {
name: "from_head".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
test_ctx.branch.add_series(&ctx, from_head, None)?;
// replace with previous head
@ -792,6 +813,7 @@ fn replace_head_single_with_merge_base() -> Result<()> {
name: "from_head".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
test_ctx.branch.add_series(&ctx, from_head, None)?;
// replace with merge base
@ -827,6 +849,7 @@ fn replace_head_with_invalid_commit_error() -> Result<()> {
name: "from_head".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
test_ctx.branch.add_series(&ctx, from_head, None)?;
let stack = test_ctx.branch.clone();
@ -853,6 +876,7 @@ fn replace_head_with_same_noop() -> Result<()> {
name: "from_head".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
test_ctx.branch.add_series(&ctx, from_head, None)?;
let stack = test_ctx.branch.clone();
@ -938,11 +962,13 @@ fn replace_head_multiple() -> Result<()> {
name: "from_head_1".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
let from_head_2 = PatchReference {
name: "from_head_2".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
// both references point to the same commit
test_ctx.branch.add_series(&ctx, from_head_1, None)?;
@ -982,6 +1008,7 @@ fn replace_head_top_of_stack_multiple() -> Result<()> {
name: "extra_head".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
// an extra head just beneath the top of the stack
test_ctx.branch.add_series(&ctx, extra_head, None)?;
@ -1047,6 +1074,7 @@ fn set_legacy_refname_multiple_heads() -> Result<()> {
name: "extra_head".into(),
target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
description: None,
forge_id: Default::default(),
};
// an extra head just beneath the top of the stack
test_ctx.branch.add_series(&ctx, extra_head, None)?;
@ -1117,6 +1145,7 @@ fn prune_heads_success() -> Result<()> {
target: test_ctx.other_commits.first().cloned().unwrap().into(),
name: "foo".to_string(),
description: None,
forge_id: Default::default(),
},
);
assert_eq!(test_ctx.branch.heads.len(), 2);
@ -1146,6 +1175,7 @@ fn does_not_prune_head_on_merge_base() -> Result<()> {
target: merge_base.into(),
name: "bottom".to_string(),
description: None,
forge_id: Default::default(),
},
None,
)?;
@ -1160,6 +1190,48 @@ fn does_not_prune_head_on_merge_base() -> Result<()> {
Ok(())
}
#[test]
fn set_forge_identifiers_success() -> Result<()> {
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
let mut test_ctx = test_ctx(&ctx)?;
test_ctx.branch.initialize(&ctx)?;
let result = test_ctx.branch.set_forge_id(
&ctx,
"a-branch-2",
Some(ForgeIdentifier::GitHub(GitHubIdentifier { pr_number: 123 })),
);
assert!(result.is_ok());
assert_eq!(
test_ctx.branch.heads[0].forge_id,
Some(ForgeIdentifier::GitHub(GitHubIdentifier { pr_number: 123 }))
);
// Assert persisted
assert_eq!(
test_ctx.branch,
test_ctx.handle.get_branch(test_ctx.branch.id)?
);
Ok(())
}
#[test]
fn set_forge_identifiers_series_not_found_fails() -> Result<()> {
let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
let mut test_ctx = test_ctx(&ctx)?;
test_ctx.branch.initialize(&ctx)?;
let result = test_ctx.branch.set_forge_id(
&ctx,
"does-not-exist",
Some(ForgeIdentifier::GitHub(GitHubIdentifier { pr_number: 123 })),
);
assert_eq!(
result.err().unwrap().to_string(),
format!(
"Series does-not-exist does not exist on stack {}",
test_ctx.branch.name
)
);
Ok(())
}
fn command_ctx(name: &str) -> Result<(CommandContext, TempDir)> {
gitbutler_testsupport::writable::fixture("stacking.sh", name)
}

View File

@ -195,6 +195,7 @@ fn main() {
stack::remove_series,
stack::update_series_name,
stack::update_series_description,
stack::update_series_forge_ids,
stack::push_stack,
secret::secret_get_global,
secret::secret_set_global,

View File

@ -1,4 +1,5 @@
use gitbutler_branch_actions::stack::CreateSeriesRequest;
use gitbutler_patch_reference::ForgeIdentifier;
use gitbutler_project as projects;
use gitbutler_project::ProjectId;
use gitbutler_stack::StackId;
@ -80,6 +81,24 @@ pub fn update_series_description(
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(projects, windows), err(Debug))]
pub fn update_series_forge_ids(
windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
stack_id: StackId,
head_name: String,
forge_id: Option<ForgeIdentifier>,
) -> Result<(), Error> {
let project = projects.get(project_id)?;
gitbutler_branch_actions::stack::update_series_forge_ids(
&project, stack_id, head_name, forge_id,
)?;
emit_vbranches(&windows, project_id);
Ok(())
}
#[tauri::command(async)]
#[instrument(skip(projects, windows), err(Debug))]
pub fn push_stack(

View File

@ -10,6 +10,7 @@ pub mod commands {
RemoteBranchData, RemoteBranchFile, RemoteCommit, StackOrder, VirtualBranches,
};
use gitbutler_command_context::CommandContext;
use gitbutler_patch_reference::ForgeIdentifier;
use gitbutler_project as projects;
use gitbutler_project::{FetchResult, ProjectId};
use gitbutler_reference::{normalize_branch_name as normalize_name, Refname, RemoteRefname};
@ -101,10 +102,12 @@ pub mod commands {
project_id: ProjectId,
branch: Refname,
remote: Option<RemoteRefname>,
forge_id: Option<ForgeIdentifier>,
) -> Result<StackId, Error> {
let project = projects.get(project_id)?;
let branch_id =
gitbutler_branch_actions::create_virtual_branch_from_branch(&project, &branch, remote)?;
let branch_id = gitbutler_branch_actions::create_virtual_branch_from_branch(
&project, &branch, remote, forge_id,
)?;
emit_vbranches(&windows, project_id);
Ok(branch_id)
}