Skip to content

Commit

Permalink
branch create: --[no-]commit flag, branchCreate.commit option (#347)
Browse files Browse the repository at this point in the history
The flag defaults to `--commit`.
Users can use `--no-commit` on a one-off basis to create a branch
without committing changes.

Alternatively, users can set `spice.branchCreate.commit` to `false`
to make `--no-commit` the default behavior.
They may then use `--commit` on a one-off basis to commit changes.

Resolves #332
  • Loading branch information
abhinav authored Aug 15, 2024
1 parent 73ab60b commit 4fb0588
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 44 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/Added-20240815-053353.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Added
body: >-
branch create: Add `--[no-]commit` flag
and accompayning `spice.branchCreate.commit` configuration
to create stacked branches without committing changes.
time: 2024-08-15T05:33:53.799135-07:00
97 changes: 54 additions & 43 deletions branch_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type branchCreateCmd struct {

All bool `short:"a" help:"Automatically stage modified and deleted files"`
Message string `short:"m" placeholder:"MSG" help:"Commit message"`

Commit bool `negatable:"" default:"true" config:"branchCreate.commit" help:"Commit staged changes to the new branch, or create an empty commit"`
}

func (*branchCreateCmd) Help() string {
Expand All @@ -28,6 +30,7 @@ func (*branchCreateCmd) Help() string {
If there are no staged changes, an empty commit will be created.
Use -a/--all to automatically stage modified and deleted files,
just like 'git commit -a'.
Use --no-commit to create the branch without committing.
If a branch name is not provided,
it will be generated from the commit message.
Expand Down Expand Up @@ -75,6 +78,10 @@ func (*branchCreateCmd) Help() string {
}

func (cmd *branchCreateCmd) Run(ctx context.Context, log *log.Logger, opts *globalOptions) (err error) {
if cmd.Name == "" && !cmd.Commit {
return fmt.Errorf("a branch name is required with --no-commit")
}

repo, store, svc, err := openRepo(ctx, log, opts)
if err != nil {
return err
Expand Down Expand Up @@ -136,58 +143,62 @@ func (cmd *branchCreateCmd) Run(ctx context.Context, log *log.Logger, opts *glob
}
}

commitHash, restore, err := cmd.commit(ctx, repo, baseName)
if err != nil {
return err
}

// Staged changes are committed to commitHash.
// From this point on, to prevent data loss,
// we'll want to revert to original branch while keeping the changes
// if we failed to create the new branch for any reason.
//
// The condition for this is not whether an error is returned,
// but whether the new branch was successfully created.
var branchCreated bool // set only after CreateBranch
defer func() {
if branchCreated {
return
}

log.Warn("Unable to create branch. Rolling back.",
"branch", cmd.Target)

if restoreErr := restore(); restoreErr != nil {
log.Error("Could not roll back. You may need to reset manually.", "error", restoreErr)
log.Errorf("Get your changes from: %s", commitHash)
}
}()

if cmd.Name == "" {
// Branch name was not specified.
// Generate one from the commit message.
subject, err := repo.CommitSubject(ctx, commitHash.String())
branchAt := baseHash
if cmd.Commit {
commitHash, restore, err := cmd.commit(ctx, repo, baseName)
if err != nil {
return fmt.Errorf("get commit subject: %w", err)
return err
}

name := spice.GenerateBranchName(subject)
current := name

// If the auto-generated branch name already exists,
// append a number to it until we find an unused name.
_, err = repo.PeelToCommit(ctx, current)
for num := 2; err == nil; num++ {
current = fmt.Sprintf("%s-%d", name, num)
branchAt = commitHash

// Staged changes are committed to commitHash.
// From this point on, to prevent data loss,
// we'll want to revert to original branch while keeping the changes
// if we failed to create the new branch for any reason.
//
// The condition for this is not whether an error is returned,
// but whether the new branch was successfully created.
defer func() {
if branchCreated {
return
}

log.Warn("Unable to create branch. Rolling back.",
"branch", cmd.Target)

if restoreErr := restore(); restoreErr != nil {
log.Error("Could not roll back. You may need to reset manually.", "error", restoreErr)
log.Errorf("Get your changes from: %s", commitHash)
}
}()

if cmd.Name == "" {
// Branch name was not specified.
// Generate one from the commit message.
subject, err := repo.CommitSubject(ctx, commitHash.String())
if err != nil {
return fmt.Errorf("get commit subject: %w", err)
}

name := spice.GenerateBranchName(subject)
current := name

// If the auto-generated branch name already exists,
// append a number to it until we find an unused name.
_, err = repo.PeelToCommit(ctx, current)
}
for num := 2; err == nil; num++ {
current = fmt.Sprintf("%s-%d", name, num)
_, err = repo.PeelToCommit(ctx, current)
}

cmd.Name = current
cmd.Name = current
}
}

if err := repo.CreateBranch(ctx, git.CreateBranchRequest{
Name: cmd.Name,
Head: commitHash.String(),
Head: branchAt.String(),
}); err != nil {
return fmt.Errorf("create branch: %w", err)
}
Expand Down
4 changes: 4 additions & 0 deletions doc/includes/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ Staged changes will be committed to the new branch.
If there are no staged changes, an empty commit will be created.
Use -a/--all to automatically stage modified and deleted files,
just like 'git commit -a'.
Use --no-commit to create the branch without committing.

If a branch name is not provided,
it will be generated from the commit message.
Expand Down Expand Up @@ -531,6 +532,9 @@ target (A) to the specified branch:
* `-t`, `--target=BRANCH`: Branch to create the new branch above/below
* `-a`, `--all`: Automatically stage modified and deleted files
* `-m`, `--message=MSG`: Commit message
* `--[no-]commit` ([:material-wrench:{ .middle title="spice.branchCreate.commit" }](/cli/config.md#spicebranchcreatecommit)): Commit staged changes to the new branch, or create an empty commit

**Configuration**: [spice.branchCreate.commit](/cli/config.md#spicebranchcreatecommit)

### gs branch delete

Expand Down
11 changes: 11 additions & 0 deletions doc/src/cli/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ This option controls whether untracked branches are shown in the prompt.
- `true`
- `false` (default)

### spice.branchCreate.commit

<!-- gs:version unreleased -->

Whether $$gs branch create$$ should commit staged changes to the new branch.
Set this to `false` to default to creating new branches without committing,
and use the `--commit` flag to commit changes when needed.

- `true` (default)
- `false`

### spice.forge.github.apiUrl

URL at which the GitHub API is available.
Expand Down
9 changes: 8 additions & 1 deletion doc/src/guide/branch.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,20 @@ commit the staged files to it, and track it with the current branch as base.
An editor will open to let you write a commit message
if one was not provided with the `-m`/`--message` flag.

!!! tip "But I use `git commit -a`"
??? tip "But I use `git commit -a`"

If you prefer to use `git commit -a` to automatically stage files
before committing, use `gs branch create -a` to do the same with git-spice.

Explore the full list of options at $$gs branch create$$.

??? info "Creating branches without committing"

If you prefer a workflow where you create branches first
and then work on them,
you can configure git-spice to never commit by default.
See [Create branches without committing](../recipes.md#create-branches-without-committing).

### Manual stacking

git-spice does not require to change your workflow too drastically.
Expand Down
37 changes: 37 additions & 0 deletions doc/src/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@ description: >-

# Recipes

## Workflows

### Create branches without committing

<!-- gs:version unreleased -->

The default workflow for git-spice forces you to commit immediately
to new branches: $$gs branch create$$ will create a new branch,
and commit staged changes to it immediately,
or if there are no staged changes, it will create an empty commit.

If you have a workflow where you prefer to create branches first,
and then work on them, you can use the following to adjust the workflow:

- Configure $$gs branch create$$ to never commit by default
by setting $$spice.branchCreate.commit$$ to false.

```bash
git config --global spice.branchCreate.commit false
```

- Use $$gs branch create$$ as usual to create branches.
Changes will not be committed automatically anymore.

```bash
gs branch create my-branch
```

- If, for some branches, you do want to commit staged changes upon creation,
use the `--commit` flag.

```bash
gs branch create my-branch --commit
```

## Tasks

### Import a Pull Request from GitHub

git-spice can recognize and manage GitHub Pull Requests
Expand Down
75 changes: 75 additions & 0 deletions testdata/script/branch_create_no_commit.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# branch create --no-commit stacks branches
# without committing staged changes.

as 'Test <[email protected]>'
at '2024-08-15T05:26:12Z'

cd repo
git init
git commit --allow-empty -m 'Initial commit'
gs repo init

git add foo.txt
gs bc --no-commit feat1

git status --porcelain
cmp stdout $WORK/golden/foo-staged.txt
gs ls
cmp stderr $WORK/golden/ls-feat1.txt

# make it the default, try again
git config spice.branchCreate.commit false
git add bar.txt
gs bc feat2

git status --porcelain
cmp stdout $WORK/golden/both-staged.txt
gs ls
cmp stderr $WORK/golden/ls-feat2.txt

# commit opt-in overrides the config
gs bc --commit feat3 -m 'Add foo and bar'
git status --porcelain
cmp stdout $WORK/golden/clean.txt
gs ls
cmp stderr $WORK/golden/ls-feat3.txt

git graph --branches
cmp stdout $WORK/golden/final-graph.txt

gs ll
cmp stderr $WORK/golden/ll-feat3.txt

-- repo/foo.txt --
foo
-- repo/bar.txt --
bar

-- golden/clean.txt --
-- golden/foo-staged.txt --
A foo.txt
?? bar.txt
-- golden/both-staged.txt --
A bar.txt
A foo.txt
-- golden/ls-feat1.txt --
┏━■ feat1 ◀
main
-- golden/ls-feat2.txt --
┏━■ feat2 ◀
┏━┻□ feat1
main
-- golden/ls-feat3.txt --
┏━■ feat3 ◀
┏━┻□ feat2
┏━┻□ feat1
main
-- golden/ll-feat3.txt --
┏━■ feat3 ◀
┃ 2a17718 Add foo and bar (now)
┏━┻□ feat2
┏━┻□ feat1
main
-- golden/final-graph.txt --
* 2a17718 (HEAD -> feat3) Add foo and bar
* 356f4ce (main, feat2, feat1) Initial commit

0 comments on commit 4fb0588

Please sign in to comment.