Support specifying a branching point even if the parent is trunk (#623)(#622)(#621)(#620)(#619)(#618)

This allows us to handle a case where a newly adopted remote branch has
an already merged branch as a parent. In the next PR, we will specify
the branching point for those branches.


Check if the default branch is set when opening a repository
We can check if the default branch at remote tracking branch
(refs/remotes/origin/HEAD) exists when opening a repository. By doing
so, we don't have to take context and do not have to do an error
handling in many situations.


Closes #622

Allow specifying the branching point even for trunk parents
This relax the semantics of the branch metadata. In the branch metadata,
we track `trunk: bool` and `head: string`, but only one of them could be
specified in the current semantics. This update allows them to specify
both. That is, even if the parent branch is a trunk branch, a branch
metadata can specify the branching point. Accordingly, we update the
internal name of that branching point as BranchingPointCommitHash.

Newly added av branch --remote motivates this change. In that command,
we might see a partially merged stack. In that case, we will have a case
where a topic branch would have a trunk branch as a parent (because its
original parent has been merged), but at the same time, such topic
branches would need to have an explicit branching point in order to
avoid already merged commits to be considered as a part of that topic
branch.

Behavior updates would be necessary in the sequencer, but we will do
that in the following PRs.


Closes #621

Deprecate reference based metadata store
It's been a while since we migrated to a file-base metadata storage. We
can drop this refmeta code.



Closes #620

Extract MergeCommit for remote branches




Closes #619

Handle a case where no branch was chosen




Closes #618
This commit is contained in:
Masaya Suzuki
2025-10-31 11:02:36 +09:00
committed by GitHub
parent eb3e5b5b97
commit e3a22062a5
38 changed files with 201 additions and 370 deletions

View File

@@ -105,16 +105,11 @@ func adoptForceAdoption(
return errors.New("cannot adopt the current branch as its parent")
}
if isCurrentBranchTrunk, err := repo.IsTrunkBranch(ctx, currentBranch); err != nil {
return errors.Wrap(err, "failed to check if the current branch is trunk")
} else if isCurrentBranchTrunk {
if repo.IsTrunkBranch(currentBranch) {
return errors.New("cannot adopt the default branch")
}
isParentBranchTrunk, err := repo.IsTrunkBranch(ctx, parent)
if err != nil {
return errors.Wrap(err, "failed to check if the parent branch is trunk")
}
isParentBranchTrunk := repo.IsTrunkBranch(parent)
if isParentBranchTrunk {
branch.Parent = meta.BranchState{
Name: parent,
@@ -131,9 +126,9 @@ func adoptForceAdoption(
return err
}
branch.Parent = meta.BranchState{
Name: parent,
Trunk: false,
Head: mergeBase,
Name: parent,
Trunk: false,
BranchingPointCommitHash: mergeBase,
}
tx.SetBranch(branch)
}
@@ -242,7 +237,7 @@ func (vm *adoptViewModel) initAdoption(chosenTargets []plumbing.ReferenceName) t
},
}
if !piece.ParentIsTrunk {
ab.Parent.Head = piece.ParentMergeBase.String()
ab.Parent.BranchingPointCommitHash = piece.ParentMergeBase.String()
}
branches = append(branches, ab)
}
@@ -350,6 +345,12 @@ func (vm *remoteAdoptViewModel) initTreeSelector(prs []actions.RemotePRInfo) tea
}
func (vm *remoteAdoptViewModel) initGitFetch(prs []actions.RemotePRInfo, chosenTargets []plumbing.ReferenceName) tea.Cmd {
if len(chosenTargets) == 0 {
return tea.Batch(
vm.AddView(uiutils.SimpleMessageView{Message: colors.SuccessStyle.Render("✓ No branch adopted")}),
tea.Quit,
)
}
refspecs := []string{}
for _, target := range chosenTargets {
// Directly clone as a local branch.

View File

@@ -268,10 +268,7 @@ func createBranch(
) (reterr error) {
// Determine important contextual information from Git
// or if a parent branch is provided, check it allows as a default branch
defaultBranch, err := repo.DefaultBranch(ctx)
if err != nil {
return errors.WrapIf(err, "failed to determine repository default branch")
}
defaultBranch := repo.DefaultBranch()
tx := db.WriteTx()
cu := cleanup.New(func() {
@@ -283,7 +280,7 @@ func createBranch(
// Determine the parent branch and make sure it's checked out
if parentBranchName == "" {
var err error
parentBranchName, err = repo.CurrentBranchName(ctx)
parentBranchName, err = repo.CurrentBranchName()
if err != nil {
return errors.WrapIff(err, "failed to get current branch name")
}
@@ -295,10 +292,7 @@ func createBranch(
}
parentBranchName = strings.TrimPrefix(parentBranchName, remoteName+"/")
isBranchFromTrunk, err := repo.IsTrunkBranch(ctx, parentBranchName)
if err != nil {
return errors.WrapIf(err, "failed to determine if branch is a trunk")
}
isBranchFromTrunk := repo.IsTrunkBranch(parentBranchName)
checkoutStartingPoint := parentBranchName
var parentHead string
if isBranchFromTrunk {
@@ -373,9 +367,9 @@ func createBranch(
tx.SetBranch(meta.Branch{
Name: branchName,
Parent: meta.BranchState{
Name: parentBranchName,
Trunk: isBranchFromTrunk,
Head: parentHead,
Name: parentBranchName,
Trunk: isBranchFromTrunk,
BranchingPointCommitHash: parentHead,
},
})
@@ -403,7 +397,7 @@ func branchMove(
oldBranch, newBranch, _ = strings.Cut(newBranch, ":")
} else {
var err error
oldBranch, err = repo.CurrentBranchName(ctx)
oldBranch, err = repo.CurrentBranchName()
if err != nil {
return err
}
@@ -422,10 +416,7 @@ func branchMove(
currentMeta, ok := tx.Branch(oldBranch)
if !ok {
defaultBranch, err := repo.DefaultBranch(ctx)
if err != nil {
return errors.WrapIf(err, "failed to determine repository default branch")
}
defaultBranch := repo.DefaultBranch()
currentMeta.Parent = meta.BranchState{
Name: defaultBranch,
Trunk: true,

View File

@@ -108,9 +108,9 @@ var branchMetaSetCmd = &cobra.Command{
}
}
br.Parent = meta.BranchState{
Name: branchMetaFlags.parent,
Trunk: branchMetaFlags.trunk,
Head: parentHead,
Name: branchMetaFlags.parent,
Trunk: branchMetaFlags.trunk,
BranchingPointCommitHash: parentHead,
}
}
tx.SetBranch(br)

View File

@@ -103,7 +103,7 @@ var commitCmd = &cobra.Command{
}
func runCreate(ctx context.Context, repo *git.Repo, db meta.DB) error {
currentBranch, err := repo.CurrentBranchName(ctx)
currentBranch, err := repo.CurrentBranchName()
if err != nil {
return errors.WrapIf(err, "failed to determine current branch")
}
@@ -182,7 +182,7 @@ func runAmend(
edit bool,
all bool,
) error {
currentBranch, err := repo.CurrentBranchName(ctx)
currentBranch, err := repo.CurrentBranchName()
if err != nil {
return errors.WrapIf(err, "failed to determine current branch")
}

View File

@@ -1,7 +1,6 @@
package main
import (
"context"
"strings"
"emperror.dev/errors"
@@ -114,7 +113,7 @@ func (vm *postCommitRestackViewModel) writeState(state *sequencerui.RestackState
}
func (vm *postCommitRestackViewModel) createState() (*sequencerui.RestackState, error) {
currentBranch, err := vm.repo.CurrentBranchName(context.Background())
currentBranch, err := vm.repo.CurrentBranchName()
if err != nil {
return nil, err
}

View File

@@ -33,7 +33,7 @@ Generates the diff between the working tree and the parent branch
return err
}
currentBranchName, err := repo.CurrentBranchName(ctx)
currentBranchName, err := repo.CurrentBranchName()
if err != nil {
return err
}
@@ -41,10 +41,7 @@ Generates the diff between the working tree and the parent branch
tx := db.ReadTx()
branch, exists := tx.Branch(currentBranchName)
if !exists {
defaultBranch, err := repo.DefaultBranch(ctx)
if err != nil {
return err
}
defaultBranch := repo.DefaultBranch()
branch.Parent = meta.BranchState{
Name: defaultBranch,
Trunk: true,
@@ -90,7 +87,7 @@ Generates the diff between the working tree and the parent branch
// have those changes). Instead, we want to compute the diff between
// 1a and 2a. We can't just use merge-base here to account for the
// fact that one might be amended (not just advanced).
diffArgs = append(diffArgs, branch.Parent.Head)
diffArgs = append(diffArgs, branch.Parent.BranchingPointCommitHash)
// Determine if the branch is up-to-date with the parent and warn if
// not.
@@ -98,7 +95,7 @@ Generates the diff between the working tree and the parent branch
if err != nil {
return err
}
notUpToDate = currentParentHead != branch.Parent.Head
notUpToDate = currentParentHead != branch.Parent.BranchingPointCommitHash
}
// NOTE:

View File

@@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
@@ -12,7 +11,6 @@ import (
"github.com/aviator-co/av/internal/git"
"github.com/aviator-co/av/internal/meta"
"github.com/aviator-co/av/internal/meta/jsonfiledb"
"github.com/aviator-co/av/internal/meta/refmeta"
"github.com/aviator-co/av/internal/utils/colors"
"github.com/spf13/cobra"
)
@@ -59,7 +57,7 @@ var ErrRepoNotInitialized = errors.Sentinel(
)
func getDB(ctx context.Context, repo *git.Repo) (meta.DB, error) {
db, exists, err := getOrCreateDB(ctx, repo)
db, exists, err := getOrCreateDB(repo)
if err != nil {
return nil, err
}
@@ -69,23 +67,8 @@ func getDB(ctx context.Context, repo *git.Repo) (meta.DB, error) {
return db, nil
}
func getOrCreateDB(ctx context.Context, repo *git.Repo) (meta.DB, bool, error) {
func getOrCreateDB(repo *git.Repo) (meta.DB, bool, error) {
dbPath := filepath.Join(repo.AvDir(), "av.db")
oldDBPathPath := filepath.Join(repo.AvDir(), "repo-metadata.json")
dbPathStat, _ := os.Stat(dbPath)
oldDBPathStat, _ := os.Stat(oldDBPathPath)
if dbPathStat == nil && oldDBPathStat != nil {
// Migrate old db to new db
db, exists, err := jsonfiledb.OpenPath(dbPath)
if err != nil {
return nil, false, err
}
if err := refmeta.Import(ctx, repo, db); err != nil {
return nil, false, errors.WrapIff(err, "failed to import ref metadata into av database")
}
return db, exists, nil
}
return jsonfiledb.OpenPath(dbPath)
}
@@ -99,11 +82,7 @@ func allBranches(ctx context.Context) ([]string, error) {
return nil, err
}
defaultBranch, err := repo.DefaultBranch(ctx)
if err != nil {
return nil, err
}
defaultBranch := repo.DefaultBranch()
tx := db.ReadTx()
branches := []string{defaultBranch}

View File

@@ -21,7 +21,7 @@ var initCmd = &cobra.Command{
return err
}
db, _, err := getOrCreateDB(ctx, repo)
db, _, err := getOrCreateDB(repo)
if err != nil {
return err
}

View File

@@ -80,7 +80,7 @@ func newNextModel(ctx context.Context, lastInStack bool, nInStack int) (stackNex
return stackNextModel{}, err
}
currentBranch, err := repo.CurrentBranchName(ctx)
currentBranch, err := repo.CurrentBranchName()
if err != nil {
return stackNextModel{}, err
}

View File

@@ -28,7 +28,7 @@ var orphanCmd = &cobra.Command{
tx := db.WriteTx()
defer tx.Abort()
currentBranch, err := repo.CurrentBranchName(ctx)
currentBranch, err := repo.CurrentBranchName()
if err != nil {
return errors.WrapIf(err, "failed to determine current branch")
}

View File

@@ -93,7 +93,7 @@ Examples:
if err != nil {
return err
}
branchName, err := repo.CurrentBranchName(ctx)
branchName, err := repo.CurrentBranchName()
if err != nil {
return errors.WrapIf(err, "failed to determine current branch")
}
@@ -183,7 +183,7 @@ func submitAll(ctx context.Context, current bool, draft bool) error {
cu := cleanup.New(func() { tx.Abort() })
defer cu.Cleanup()
currentBranch, err := repo.CurrentBranchName(ctx)
currentBranch, err := repo.CurrentBranchName()
if err != nil {
return err
}
@@ -288,7 +288,7 @@ func queue(ctx context.Context) error {
}
tx := db.ReadTx()
currentBranchName, err := repo.CurrentBranchName(ctx)
currentBranchName, err := repo.CurrentBranchName()
if err != nil {
return err
}

View File

@@ -211,7 +211,7 @@ func getQueryVariables(ctx context.Context) (map[string]any, error) {
tx := db.ReadTx()
currentBranchName, err := repo.CurrentBranchName(ctx)
currentBranchName, err := repo.CurrentBranchName()
if err != nil {
return nil, err
}

View File

@@ -33,14 +33,11 @@ var prevCmd = &cobra.Command{
return err
}
tx := db.ReadTx()
currentBranch, err := repo.CurrentBranchName(ctx)
currentBranch, err := repo.CurrentBranchName()
if err != nil {
return err
}
isCurrentBranchTrunk, err := repo.IsTrunkBranch(ctx, currentBranch)
if err != nil {
return err
} else if isCurrentBranchTrunk {
if repo.IsTrunkBranch(currentBranch) {
fmt.Fprint(os.Stderr, "already on trunk branch (", colors.UserInput(currentBranch), ")\n")
return nil
}

View File

@@ -87,7 +87,7 @@ squashed, dropped, or moved within the stack.
return actions.ErrExitSilently{ExitCode: 127}
}
tx := db.ReadTx()
currentBranch, err := repo.CurrentBranchName(ctx)
currentBranch, err := repo.CurrentBranchName()
if err != nil {
return err
}

View File

@@ -145,22 +145,18 @@ func (vm *reparentViewModel) writeState(state *sequencerui.RestackState) error {
func (vm *reparentViewModel) createState() (*sequencerui.RestackState, error) {
ctx := context.Background()
currentBranch, err := vm.repo.CurrentBranchName(ctx)
currentBranch, err := vm.repo.CurrentBranchName()
if err != nil {
return nil, err
}
if isCurrentBranchTrunk, err := vm.repo.IsTrunkBranch(ctx, currentBranch); err != nil {
return nil, err
} else if isCurrentBranchTrunk {
if vm.repo.IsTrunkBranch(currentBranch) {
return nil, errors.New("current branch is a trunk branch")
}
if _, exist := vm.db.ReadTx().Branch(currentBranch); !exist {
return nil, errors.New("current branch is not adopted to av")
}
if isParentBranchTrunk, err := vm.repo.IsTrunkBranch(ctx, reparentFlags.Parent); err != nil {
return nil, err
} else if !isParentBranchTrunk {
if !vm.repo.IsTrunkBranch(reparentFlags.Parent) {
if _, exist := vm.db.ReadTx().Branch(reparentFlags.Parent); !exist {
return nil, errors.New("parent branch is not adopted to av")
}

View File

@@ -52,7 +52,7 @@ func runSquash(ctx context.Context, repo *git.Repo, db meta.DB) error {
)
}
currentBranchName, err := repo.CurrentBranchName(ctx)
currentBranchName, err := repo.CurrentBranchName()
if err != nil {
return err
}

View File

@@ -52,7 +52,7 @@ Examples:
return err
}
tx := db.ReadTx()
currentBranch, err := repo.CurrentBranchName(ctx)
currentBranch, err := repo.CurrentBranchName()
if err != nil {
return err
}

View File

@@ -145,7 +145,7 @@ func (vm *syncViewModel) initSync() tea.Cmd {
return uiutils.ErrCmd(errors.New("no restack in progress"))
}
isTrunkBranch, err := vm.repo.IsCurrentBranchTrunk(context.Background())
isTrunkBranch, err := vm.repo.IsCurrentBranchTrunk()
if err != nil {
return uiutils.ErrCmd(err)
}

View File

@@ -149,16 +149,16 @@ func TestRestack(t *testing.T) {
Trunk: true,
}, GetStoredParentBranchState(t, repo, "stack-1"))
require.Equal(t, meta.BranchState{
Name: "stack-1",
Head: stack1Commit.String(),
Name: "stack-1",
BranchingPointCommitHash: stack1Commit.String(),
}, GetStoredParentBranchState(t, repo, "stack-2"))
require.Equal(t, meta.BranchState{
Name: "stack-2",
Head: stack2Commit.String(),
Name: "stack-2",
BranchingPointCommitHash: stack2Commit.String(),
}, GetStoredParentBranchState(t, repo, "stack-3"))
require.Equal(t, meta.BranchState{
Name: "stack-1",
Head: stack1Commit.String(),
Name: "stack-1",
BranchingPointCommitHash: stack1Commit.String(),
}, GetStoredParentBranchState(t, repo, "stack-4"))
}
@@ -214,8 +214,8 @@ func TestStackRestackAbort(t *testing.T) {
// Because we aborted the sync, the stack-2 parent HEAD must stay at the original stack-1
// HEAD.
require.Equal(t, meta.BranchState{
Name: "stack-1",
Head: origStack1Commit.String(),
Name: "stack-1",
BranchingPointCommitHash: origStack1Commit.String(),
}, GetStoredParentBranchState(t, repo, "stack-2"))
}

View File

@@ -155,16 +155,16 @@ func TestSync(t *testing.T) {
Trunk: true,
}, GetStoredParentBranchState(t, repo, "stack-1"))
require.Equal(t, meta.BranchState{
Name: "stack-1",
Head: stack1Commit.String(),
Name: "stack-1",
BranchingPointCommitHash: stack1Commit.String(),
}, GetStoredParentBranchState(t, repo, "stack-2"))
require.Equal(t, meta.BranchState{
Name: "stack-2",
Head: stack2Commit.String(),
Name: "stack-2",
BranchingPointCommitHash: stack2Commit.String(),
}, GetStoredParentBranchState(t, repo, "stack-3"))
require.Equal(t, meta.BranchState{
Name: "stack-1",
Head: stack1Commit.String(),
Name: "stack-1",
BranchingPointCommitHash: stack1Commit.String(),
}, GetStoredParentBranchState(t, repo, "stack-4"))
}
@@ -222,8 +222,8 @@ func TestStackSyncAbort(t *testing.T) {
// Because we aborted the sync, the stack-2 parent HEAD must stay at the original stack-1
// HEAD.
require.Equal(t, meta.BranchState{
Name: "stack-1",
Head: origStack1Commit.String(),
Name: "stack-1",
BranchingPointCommitHash: origStack1Commit.String(),
}, GetStoredParentBranchState(t, repo, "stack-2"))
}

View File

@@ -33,6 +33,7 @@ type RemotePRInfo struct {
Name string
Parent meta.BranchState
PullRequest meta.PullRequest
MergeCommit string
Title string
}
@@ -94,9 +95,9 @@ func (m *GetRemoteStackedPRModel) Init() tea.Cmd {
remotePRInfo := RemotePRInfo{
Name: strings.TrimPrefix(pr.HeadRefName, "refs/heads/"),
Parent: meta.BranchState{
Name: prMeta.Parent,
Head: prMeta.ParentHead,
Trunk: prMeta.ParentHead == "",
Name: prMeta.Parent,
Trunk: prMeta.Trunk == prMeta.Parent,
BranchingPointCommitHash: prMeta.ParentHead,
},
PullRequest: meta.PullRequest{
ID: pr.ID,
@@ -104,7 +105,8 @@ func (m *GetRemoteStackedPRModel) Init() tea.Cmd {
Permalink: pr.Permalink,
State: pr.State,
},
Title: pr.Title,
MergeCommit: pr.GetMergeCommit(),
Title: pr.Title,
}
m.prs = append(m.prs, remotePRInfo)
if remotePRInfo.Parent.Trunk {

View File

@@ -73,7 +73,7 @@ func getPRMetadata(
trunk, _ := meta.Trunk(tx, branch.Name)
prMeta := PRMetadata{
Parent: branch.Parent.Name,
ParentHead: branch.Parent.Head,
ParentHead: branch.Parent.BranchingPointCommitHash,
Trunk: trunk,
}
if parent == nil && branch.Parent.Name != "" {
@@ -224,10 +224,7 @@ func CreatePullRequest(
// figure this out based on whether or not we're on a stacked branch
parentState := branchMeta.Parent
if parentState.Name == "" {
defaultBranch, err := repo.DefaultBranch(ctx)
if err != nil {
return nil, errors.WrapIf(err, "failed to determine default branch")
}
defaultBranch := repo.DefaultBranch()
parentState = meta.BranchState{
Name: defaultBranch,
Trunk: true,

View File

@@ -566,7 +566,7 @@ func (vm *GitHubPushModel) createPRMetadata(branch meta.Branch) actions.PRMetada
metadata := actions.PRMetadata{
Parent: branch.Parent.Name,
ParentHead: branch.Parent.Head,
ParentHead: branch.Parent.BranchingPointCommitHash,
Trunk: trunk,
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/aviator-co/av/internal/config"
giturls "github.com/chainguard-dev/git-urls"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
@@ -25,10 +26,11 @@ var ErrRemoteNotFound = errors.Sentinel("this repository doesn't have a remote o
const DEFAULT_REMOTE_NAME = "origin"
type Repo struct {
repoDir string
gitDir string
gitRepo *git.Repository
log logrus.FieldLogger
repoDir string
gitDir string
gitRepo *git.Repository
log logrus.FieldLogger
defaultBranch plumbing.ReferenceName
}
func OpenRepo(repoDir string, gitDir string) (*Repo, error) {
@@ -40,11 +42,29 @@ func OpenRepo(repoDir string, gitDir string) (*Repo, error) {
return nil, errors.Errorf("failed to open git repo: %v", err)
}
r := &Repo{
repoDir,
gitDir,
repo,
logrus.WithFields(logrus.Fields{"repo": filepath.Base(repoDir)}),
repoDir: repoDir,
gitDir: gitDir,
gitRepo: repo,
log: logrus.WithFields(logrus.Fields{"repo": filepath.Base(repoDir)}),
defaultBranch: "",
}
// Fill the default branch now so that we can error early if it can't be
// determined.
remoteName := r.GetRemoteName()
ref, err := r.GoGitRepo().Reference(plumbing.NewRemoteHEADReferenceName(remoteName), false)
if err != nil {
logrus.WithError(err).Debug("failed to determine remote HEAD")
// This `git remote set-head --auto origin` communicates with
// the remote, so we probably don't want to run it here inline,
// but we suggest it to the user in order to fix this situation.
logrus.Warn(
"Failed to determine repository default branch. " +
"Ensure you have a remote named origin and try running `git remote set-head --auto origin` to fix this.",
)
return nil, fmt.Errorf("failed to determine remote HEAD: %v", err)
}
r.defaultBranch = plumbing.NewBranchReferenceName(strings.TrimPrefix(ref.Target().String(), fmt.Sprintf("refs/remotes/%s/", remoteName)))
return r, nil
}
@@ -72,51 +92,24 @@ func (r *Repo) AvTmpDir() string {
return dir
}
func (r *Repo) DefaultBranch(ctx context.Context) (string, error) {
remoteName := r.GetRemoteName()
ref, err := r.Git(ctx, "symbolic-ref", fmt.Sprintf("refs/remotes/%s/HEAD", remoteName))
if err != nil {
logrus.WithError(err).Debug("failed to determine remote HEAD")
// this communicates with the remote, so we probably don't want to run
// it by default, but we helpfully suggest it to the user. :shrug:
logrus.Warn(
"Failed to determine repository default branch. " +
"Ensure you have a remote named origin and try running `git remote set-head --auto origin` to fix this.",
)
return "", errors.New("failed to determine remote HEAD")
}
return strings.TrimPrefix(ref, fmt.Sprintf("refs/remotes/%s/", remoteName)), nil
func (r *Repo) DefaultBranch() string {
return r.defaultBranch.Short()
}
func (r *Repo) IsTrunkBranch(ctx context.Context, name string) (bool, error) {
branches, err := r.TrunkBranches(ctx)
func (r *Repo) IsTrunkBranch(name string) bool {
return slices.Contains(r.TrunkBranches(), name)
}
func (r *Repo) IsCurrentBranchTrunk() (bool, error) {
currentBranch, err := r.CurrentBranchName()
if err != nil {
return false, err
}
if slices.Contains(branches, name) {
return true, nil
}
return false, nil
return r.IsTrunkBranch(currentBranch), nil
}
func (r *Repo) IsCurrentBranchTrunk(ctx context.Context) (bool, error) {
currentBranch, err := r.CurrentBranchName(ctx)
if err != nil {
return false, err
}
return r.IsTrunkBranch(ctx, currentBranch)
}
func (r *Repo) TrunkBranches(ctx context.Context) ([]string, error) {
defaultBranch, err := r.DefaultBranch(ctx)
if err != nil {
return nil, err
}
branches := []string{defaultBranch}
branches = append(branches, config.Av.AdditionalTrunkBranches...)
return branches, nil
func (r *Repo) TrunkBranches() []string {
return append([]string{r.DefaultBranch()}, config.Av.AdditionalTrunkBranches...)
}
func (r *Repo) GetRemoteName() string {
@@ -228,15 +221,19 @@ func (r *Repo) Run(ctx context.Context, opts *RunOpts) (*Output, error) {
// The name is return in "short" format -- i.e., without the "refs/heads/" prefix.
// IMPORTANT: This function will return an error if the repository is currently
// in a detached-head state (e.g., during a rebase conflict).
func (r *Repo) CurrentBranchName(ctx context.Context) (string, error) {
branch, err := r.Git(ctx, "symbolic-ref", "--short", "HEAD")
func (r *Repo) CurrentBranchName() (string, error) {
ref, err := r.GoGitRepo().Reference(plumbing.HEAD, false)
if err != nil {
return "", errors.Wrap(
err,
"failed to determine current branch (are you in detached HEAD or is a rebase in progress?)",
)
}
return branch, nil
if ref.Type() == plumbing.SymbolicReference {
return ref.Target().Short(), nil
}
// Detached HEAD
return "", errors.New("repository is in detached HEAD state")
}
func (r *Repo) DoesBranchExist(ctx context.Context, branch string) (bool, error) {
@@ -295,7 +292,7 @@ type CheckoutBranch struct {
// branch if necessary). The returned previous branch name may be empty if the
// repo is currently not checked out to a branch (i.e., in detached HEAD state).
func (r *Repo) CheckoutBranch(ctx context.Context, opts *CheckoutBranch) (string, error) {
previousBranchName, err := r.CurrentBranchName(ctx)
previousBranchName, err := r.CurrentBranchName()
if err != nil {
r.log.WithError(err).
Debug("failed to get current branch name, repo is probably in detached HEAD")

View File

@@ -26,21 +26,23 @@ func TestOrigin(t *testing.T) {
func TestTrunkBranches(t *testing.T) {
repo := gittest.NewTempRepo(t)
branches, err := repo.AsAvGitRepo().TrunkBranches(t.Context())
require.NoError(t, err)
branches := repo.AsAvGitRepo().TrunkBranches()
require.Equal(t, branches, []string{"main"})
// add some branches to AdditionalTrunkBranches
config.Av.AdditionalTrunkBranches = []string{"develop", "staging"}
branches, err = repo.AsAvGitRepo().TrunkBranches(t.Context())
require.NoError(t, err)
branches = repo.AsAvGitRepo().TrunkBranches()
require.Equal(t, branches, []string{"main", "develop", "staging"})
}
func TestGetRemoteName(t *testing.T) {
repo := gittest.NewTempRepo(t)
require.Equal(t, repo.AsAvGitRepo().GetRemoteName(), git.DEFAULT_REMOTE_NAME)
avGitRepo := repo.AsAvGitRepo()
require.Equal(t, avGitRepo.GetRemoteName(), git.DEFAULT_REMOTE_NAME)
// This is a global config, so changing it here affects other tests. Be
// sure to reset it.
config.Av.Remote = "new-remote"
require.Equal(t, repo.AsAvGitRepo().GetRemoteName(), "new-remote")
require.Equal(t, avGitRepo.GetRemoteName(), "new-remote")
config.Av.Remote = ""
}

View File

@@ -80,6 +80,9 @@ func NewTempRepoWithGitHubServer(t *testing.T, serverURL string) *GitTestRepo {
repo.Git(t, "commit", "-m", "Initial commit")
repo.Git(t, "push", "origin", "main")
repo.Git(t, "remote", "set-head", "--auto", "origin")
repo.Git(t, "branch", "--all")
// Write metadata because some commands expect it to be there.
// This repository obviously doesn't exist so tests still need to be careful
// not to invoke operations that would communicate with GitHub (e.g.,
@@ -117,7 +120,10 @@ type GitTestRepo struct {
}
func (r *GitTestRepo) AsAvGitRepo() *avgit.Repo {
repo, _ := avgit.OpenRepo(r.RepoDir, r.GitDir)
repo, err := avgit.OpenRepo(r.RepoDir, r.GitDir)
if err != nil {
panic(fmt.Sprintf("failed to open av git repo: %v", err))
}
return repo
}

View File

@@ -254,10 +254,7 @@ func (vm *PruneBranchModel) CheckoutInitialState() error {
}
// The branch is deleted. Let's checkout the default branch.
defaultBranch, err := vm.repo.DefaultBranch(context.Background())
if err != nil {
return err
}
defaultBranch := vm.repo.DefaultBranch()
defaultBranchRef := plumbing.NewBranchReferenceName(defaultBranch)
ref, err := vm.repo.GoGitRepo().Reference(defaultBranchRef, true)
if err == nil {

View File

@@ -18,11 +18,24 @@ type BranchState struct {
// master.
Trunk bool `json:"trunk,omitempty"`
// The commit SHA of the parent's latest commit. This is used when syncing
// the branch with the parent to identify the commits that belong to the
// child branch (since the HEAD of the parent branch may change).
// This will be unset if Trunk is true.
Head string `json:"head,omitempty"`
// The branching point commit hash.
//
// When we start a new branch off of a parent branch, we record the
// commit SHA of the parent's latest commit at that time as the
// branching point commit hash. This allows us to later identify which
// commits belong to the child branch when syncing with the parent
// branch.
//
// NOTE: This field is named "head" in the JSON for historical reasons.
//
// This field may be empty if the branching off from a trunk branch. In
// that case, we will find the branching point commit hash based on `git
// merge-base`. Note that this will only work if the trunk branch has
// not been rebased since the branch was created, which typically
// stands. On the other hand, when branching off of a non-trunk branch,
// we should almost always set this field as the non-trunk branches are
// tend to be rebased.
BranchingPointCommitHash string `json:"head,omitempty"`
}
// unmarshalBranchState unmarshals a BranchState from JSON (which can either be
@@ -42,7 +55,7 @@ func unmarshalBranchState(data []byte) (BranchState, error) {
if s == "" {
return BranchState{}, nil
}
return BranchState{Name: s, Head: ""}, nil
return BranchState{Name: s, BranchingPointCommitHash: ""}, nil
}
// Define a type alias here so that it doesn't have the UnmarshalJSON

View File

@@ -1,79 +0,0 @@
package refmeta
import (
"context"
"encoding/json"
"strings"
"emperror.dev/errors"
"github.com/aviator-co/av/internal/git"
"github.com/aviator-co/av/internal/meta"
"github.com/sirupsen/logrus"
)
// ReadAllBranches fetches all branch metadata stored in the git repository.
// It returns a map where the key is the name of the branch.
func ReadAllBranches(ctx context.Context, repo *git.Repo) (map[string]meta.Branch, error) {
// Find all branch metadata ref names
// Note: need `**` here (not just `*`) because Git seems to only match one
// level of nesting in the ref pattern with just a single `*` (even though
// the docs seem to suggest this to not be the case). With a single star,
// we won't match branch names like `feature/add-xyz` or `travis/fix-123`.
refs, err := repo.ListRefs(ctx, &git.ListRefs{
Patterns: []string{branchMetaRefPrefix + "**"},
})
if err != nil {
return nil, err
}
logrus.WithField("refs", refs).Debug("found branch metadata refs")
// Read the contents of each ref to get the associated metadata blob...
refNames := make([]string, len(refs))
for i, ref := range refs {
refNames[i] = ref.Name
}
refContents, err := repo.GetRefs(ctx, &git.GetRefs{
Revisions: refNames,
})
if err != nil {
return nil, err
}
// ...and for each metadata blob, parse it from JSON into a Branch
branches := make(map[string]meta.Branch, len(refs))
for _, ref := range refContents {
name := strings.TrimPrefix(ref.Revision, branchMetaRefPrefix)
branch, _ := unmarshalBranch(ctx, repo, name, ref.Revision, string(ref.Contents))
branches[name] = branch
}
return branches, nil
}
const branchMetaRefPrefix = "refs/av/branch-metadata/"
func unmarshalBranch(
ctx context.Context,
repo *git.Repo,
name string,
refName string,
blob string,
) (meta.Branch, bool) {
branch := meta.Branch{Name: name}
if err := json.Unmarshal([]byte(blob), &branch); err != nil {
logrus.WithError(err).WithField("ref", refName).Error("corrupt stack metadata, deleting...")
_ = repo.UpdateRef(ctx, &git.UpdateRef{Ref: refName, New: git.Missing})
return branch, false
}
if branch.Parent.Name == "" {
// COMPAT: assume parent branch is the default/mainline branch
defaultBranch, err := repo.DefaultBranch(ctx)
if err != nil {
// panic isn't great, but plumbing through the error is more effort
// that it's worth here
panic(errors.Wrap(err, "failed to determine repository default branch"))
}
branch.Parent.Name = defaultBranch
branch.Parent.Trunk = true
}
return branch, true
}

View File

@@ -1,40 +0,0 @@
package refmeta
import (
"context"
"github.com/aviator-co/av/internal/git"
"github.com/aviator-co/av/internal/meta"
"github.com/aviator-co/av/internal/utils/cleanup"
"github.com/sirupsen/logrus"
)
// Import imports all ref metadata from the git repo into the database.
func Import(ctx context.Context, repo *git.Repo, db meta.DB) error {
tx := db.WriteTx()
cu := cleanup.New(func() { tx.Abort() })
defer cu.Cleanup()
repoMeta, err := ReadRepository(repo)
if err != nil {
return err
}
tx.SetRepository(repoMeta)
allBranchMetas, err := ReadAllBranches(ctx, repo)
if err != nil {
return err
}
for _, branchMeta := range allBranchMetas {
tx.SetBranch(branchMeta)
}
logrus.
WithField("branches", len(allBranchMetas)).
Debug("Imported branches into av database from ref metadata")
cu.Cancel()
if err := tx.Commit(); err != nil {
return err
}
return nil
}

View File

@@ -1,26 +0,0 @@
package refmeta
import (
"encoding/json"
"os"
"path/filepath"
"emperror.dev/errors"
"github.com/aviator-co/av/internal/git"
"github.com/aviator-co/av/internal/meta"
)
// ReadRepository reads repository metadata from the git repo.
// Returns the metadata and a boolean indicating if the metadata was found.
func ReadRepository(repo *git.Repo) (meta.Repository, error) {
metaPath := filepath.Join(repo.Dir(), ".git", "av", "repo-metadata.json")
data, err := os.ReadFile(metaPath)
if err != nil {
return meta.Repository{}, errors.WrapIf(err, "failed to read the repository metadata")
}
var repository meta.Repository
if err := json.Unmarshal(data, &repository); err != nil {
return meta.Repository{}, errors.WrapIf(err, "failed to unmarshal the repository metadata")
}
return repository, nil
}

View File

@@ -31,9 +31,9 @@ func CreatePlan(
var upstreamCommit string
// TODO: would be nice to show the user whether or not the branch is
// already up-to-date with the parent.
if branch.Parent.Head != "" {
if branch.Parent.BranchingPointCommitHash != "" {
branchCmd.Parent = branch.Parent.Name
upstreamCommit = branch.Parent.Head
upstreamCommit = branch.Parent.BranchingPointCommitHash
} else {
trunkCommit, err := repo.MergeBase(ctx, branchName, "origin/"+branch.Parent.Name)
if err != nil {

View File

@@ -38,9 +38,9 @@ func TestCreatePlan(t *testing.T) {
tx.SetBranch(meta.Branch{
Name: "two",
Parent: meta.BranchState{
Name: "one",
Trunk: false,
Head: c1b.String(),
Name: "one",
Trunk: false,
BranchingPointCommitHash: c1b.String(),
},
})
require.NoError(t, tx.Commit())

View File

@@ -64,7 +64,7 @@ func (b StackBranchCmd) Execute(ctx *Context) error {
if err != nil {
return err
}
parentState.Head = headCommit
parentState.BranchingPointCommitHash = headCommit
}
branch.Parent = parentState
tx.SetBranch(branch)

View File

@@ -120,10 +120,7 @@ func PlanForReparent(
if slices.Contains(children, newParentBranch.Short()) {
return nil, errors.New("cannot re-parent to a child branch")
}
isParentTrunk, err := repo.IsTrunkBranch(ctx, newParentBranch.Short())
if err != nil {
return nil, err
}
isParentTrunk := repo.IsTrunkBranch(newParentBranch.Short())
var ret []sequencer.RestackOp
ret = append(ret, sequencer.RestackOp{
Name: currentBranch,

View File

@@ -48,7 +48,7 @@ func GetTargetBranches(
return ret, nil
}
if mode == CurrentAndParents {
curr, err := repo.CurrentBranchName(ctx)
curr, err := repo.CurrentBranchName()
if err != nil {
return nil, err
}
@@ -69,7 +69,7 @@ func GetTargetBranches(
return ret, nil
}
if mode == CurrentAndChildren {
curr, err := repo.CurrentBranchName(ctx)
curr, err := repo.CurrentBranchName()
if err != nil {
return nil, err
}
@@ -83,7 +83,7 @@ func GetTargetBranches(
}
return ret, nil
}
curr, err := repo.CurrentBranchName(ctx)
curr, err := repo.CurrentBranchName()
if err != nil {
return nil, err
}

View File

@@ -36,9 +36,10 @@ type branchSnapshot struct {
ParentBranch plumbing.ReferenceName
// True if the parent branch is the trunk branch (refs/heads/master etc.).
IsParentTrunk bool
// Commit hash that the parent branch was previously at last time this was synced.
// This is plumbing.ZeroHash if the parent branch is a trunk.
PreviouslySyncedParentBranchHash plumbing.Hash
// Commit hash that the branch was branched off from the parent. This
// can be empty when the parent branch is trunk, but this can be
// specified in some circumstances even if the parent is trunk.
BranchingPointCommitHash plumbing.Hash
}
// Sequencer re-stacks the specified branches.
@@ -80,10 +81,9 @@ func getBranchSnapshots(db meta.DB) map[plumbing.ReferenceName]*branchSnapshot {
ParentBranch: plumbing.ReferenceName("refs/heads/" + avbr.Parent.Name),
}
ret[snapshot.Name] = snapshot
if avbr.Parent.Trunk {
snapshot.IsParentTrunk = true
} else {
snapshot.PreviouslySyncedParentBranchHash = plumbing.NewHash(avbr.Parent.Head)
snapshot.IsParentTrunk = avbr.Parent.Trunk
if avbr.Parent.BranchingPointCommitHash != "" {
snapshot.BranchingPointCommitHash = plumbing.NewHash(avbr.Parent.BranchingPointCommitHash)
}
}
return ret
@@ -171,16 +171,20 @@ func (seq *Sequencer) rebaseBranch(
panic(fmt.Sprintf("branch %q not found in original branch infos", op.Name))
}
var previousParentHash plumbing.Hash
if snapshot.IsParentTrunk {
// Use the current remote tracking branch hash as the previous parent hash.
var err error
previousParentHash, err = seq.getRemoteTrackingBranchCommit(repo, snapshot.ParentBranch)
var branchingPoint plumbing.Hash
if snapshot.BranchingPointCommitHash.IsZero() {
// If the branching point is not specified, find the merge-base with the parent's remote-tracking branch.
rtb, err := seq.getRemoteTrackingBranch(repo, snapshot.ParentBranch)
if err != nil {
return nil, err
}
mb, err := repo.MergeBase(ctx, rtb.String(), op.Name.String())
if err != nil {
return nil, err
}
branchingPoint = plumbing.NewHash(mb)
} else {
previousParentHash = snapshot.PreviouslySyncedParentBranchHash
branchingPoint = snapshot.BranchingPointCommitHash
}
var newParentHash plumbing.Hash
@@ -213,7 +217,7 @@ func (seq *Sequencer) rebaseBranch(
// conflicts.
skipGitRebase := false
if b1, err := repo.IsAncestor(ctx, newParentHash.String(), op.Name.String()); err == nil && b1 {
if b2, err := repo.IsAncestor(ctx, previousParentHash.String(), newParentHash.String()); err == nil &&
if b2, err := repo.IsAncestor(ctx, branchingPoint.String(), newParentHash.String()); err == nil &&
b2 {
logrus.Debug("Skipping rebase since branch is already based on new parent")
skipGitRebase = true
@@ -225,7 +229,7 @@ func (seq *Sequencer) rebaseBranch(
// The commits from `rebaseFrom` to `snapshot.Name` should be rebased onto `rebaseOnto`.
opts := git.RebaseOpts{
Branch: op.Name.Short(),
Upstream: previousParentHash.String(),
Upstream: branchingPoint.String(),
Onto: newParentHash.String(),
}
var err error
@@ -238,7 +242,7 @@ func (seq *Sequencer) rebaseBranch(
"Failed to rebase %q onto %q (merge base is %q)\n",
op.Name,
op.NewParent,
previousParentHash.String()[:7],
branchingPoint.String()[:7],
) + result.ErrorHeadline
seq.SequenceInterruptedNewParentHash = newParentHash
return result, nil
@@ -274,7 +278,7 @@ func (seq *Sequencer) postRebaseBranchUpdate(db meta.DB, newParentHash plumbing.
Trunk: op.NewParentIsTrunk,
}
if !op.NewParentIsTrunk {
newParentBranchState.Head = newParentHash.String()
newParentBranchState.BranchingPointCommitHash = newParentHash.String()
}
tx := db.WriteTx()
@@ -311,19 +315,27 @@ func (seq *Sequencer) getRemoteTrackingBranchCommit(
repo *git.Repo,
ref plumbing.ReferenceName,
) (plumbing.Hash, error) {
rtb, err := seq.getRemoteTrackingBranch(repo, ref)
if err != nil {
return plumbing.ZeroHash, err
}
return seq.getBranchCommit(repo, *rtb)
}
func (seq *Sequencer) getRemoteTrackingBranch(repo *git.Repo, ref plumbing.ReferenceName) (*plumbing.ReferenceName, error) {
remote, err := repo.GoGitRepo().Remote(seq.RemoteName)
if err != nil {
return plumbing.ZeroHash, errors.Errorf("failed to get remote %q: %v", seq.RemoteName, err)
return nil, errors.Errorf("failed to get remote %q: %v", seq.RemoteName, err)
}
rtb := mapToRemoteTrackingBranch(remote.Config(), ref)
if rtb == nil {
return plumbing.ZeroHash, errors.Errorf(
return nil, errors.Errorf(
"failed to get remote tracking branch in %q for %q",
seq.RemoteName,
ref,
)
}
return seq.getBranchCommit(repo, *rtb)
return rtb, nil
}
func (seq *Sequencer) getBranchCommit(

View File

@@ -49,7 +49,7 @@ func DetectBranches(
// don't have to adopt it.
continue
}
bp, err := traverseUntilTrunk(ctx, repo, bn, nearestTrunkCommit, hashToRefMap, refToHashMap)
bp, err := traverseUntilTrunk(repo, bn, nearestTrunkCommit, hashToRefMap, refToHashMap)
if err != nil {
return nil, err
}
@@ -59,7 +59,6 @@ func DetectBranches(
}
func traverseUntilTrunk(
ctx context.Context,
repo *avgit.Repo,
branch plumbing.ReferenceName,
nearestTrunkCommit plumbing.Hash,
@@ -77,10 +76,7 @@ func traverseUntilTrunk(
// commit that has multiple parents.
err = object.NewCommitPreorderIter(commit, nil, nil).ForEach(func(c *object.Commit) error {
if c.Hash == nearestTrunkCommit {
trunk, err := repo.DefaultBranch(ctx)
if err != nil {
return err
}
trunk := repo.DefaultBranch()
ret.Parent = plumbing.NewBranchReferenceName(trunk)
ret.ParentIsTrunk = true
ret.ParentMergeBase = c.Hash
@@ -116,10 +112,7 @@ func getNearestTrunkCommit(
repo *avgit.Repo,
ref plumbing.ReferenceName,
) (plumbing.Hash, error) {
trunk, err := repo.DefaultBranch(ctx)
if err != nil {
return plumbing.ZeroHash, err
}
trunk := repo.DefaultBranch()
rtb := fmt.Sprintf("refs/remotes/%s/%s", repo.GetRemoteName(), trunk)
mbArgs := []string{rtb, ref.String()}