Skip to content

Commit

Permalink
Delete branches that correspond to accepted stories [#176488902]
Browse files Browse the repository at this point in the history
Co-authored-by: Charlie Nazario <cnazario@vmware.com>
  • Loading branch information
carpeliam and cnazario-vmw committed Jun 30, 2021
1 parent d097114 commit 0c98509
Show file tree
Hide file tree
Showing 14 changed files with 221 additions and 45 deletions.
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,39 @@ export TRACKER_API_TOKEN="YOUR PIVOTAL TRACKER API TOKEN" # https://www.pivotalt
2. Setup the homebrew tap and install git-story

```sh
$ brew tap git-story-branch/tap
$ brew install git-story
brew install git-story-branch/tap/git-story
```

## Usage

`git-story` is useful when your git branch name ends in the story ID of the Pivotal Tracker story you're working on:

```sh
$ git checkout WIP-some-story-1234567890
git checkout WIP-some-story-1234567890
```

### git story-view : view the details of a story

```sh
$ git story-view
State: delivered
Description goes here...
git story-view
# Example output:
# State: delivered
# Description goes here...
```

### git story-open : open the story URL in your default browser

```sh
$ git story-open
git story-open
```

### git story-sweep : delete all local branches corresponding to accepted stories, and report any errors in deleting

```sh
git story-sweep
# Example output:
# Could not delete 'WIP-accepted-branch-#987654321', error: Cannot delete branch 'WIP-accepted-branch-#987654321' checked out at '/path' # This is an expected error when you're deleting the current branch
# Deleted 'accepted-feature-branch-#123456789'
```

## Contributing to Git-Story
Expand All @@ -63,4 +72,4 @@ To run:
# assuming the library has been built and you're in a branch that has a story ID at the end...
./git-story-view
./git-story-open
```
```
10 changes: 10 additions & 0 deletions adapters/fixtures/git
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ args=$*
if [ "${args}" = "rev-parse --abbrev-ref HEAD" ]; then
echo "current-branch-123456789"
fi

if [ "${args}" = "--no-pager branch --format %(refname:short)" ]; then
echo "main"
echo "some-branch-#123"
fi

if [ "${args}" = "branch -D some-nonexistent-branch" ]; then
>&2 echo "error: branch 'some-nonexistent-branch' not found."
exit 1
fi
18 changes: 16 additions & 2 deletions adapters/git.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
package adapters

import (
"errors"
"os/exec"
"strings"
)

// GitRepository is an abstraction for git repositories
type GitRepository struct{}

// GetBranchName gets the current branch name of the repository.
func (repo GitRepository) GetBranchName() string {
// GetCurrentBranchName gets the current branch name of the repository.
func (repo GitRepository) GetCurrentBranchName() string {
output, _ := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output()
return strings.TrimSpace(string(output))
}

func (repo GitRepository) DeleteBranch(branchName string) error {
cmd := exec.Command("git", "branch", "-D", branchName)
output, error := cmd.CombinedOutput()
if error != nil {
return errors.New(strings.Trim(string(output), "\n"))
}
return nil
}
func (repo GitRepository) GetAllBranchNames() []string {
output, _ := exec.Command("git", "--no-pager", "branch", "--format", "%(refname:short)").Output()
return strings.Split(strings.TrimSpace(string(output)), "\n")
}

// NewRepository creates a new repository
func NewRepository() *GitRepository {
return &GitRepository{}
Expand Down
18 changes: 17 additions & 1 deletion adapters/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,23 @@ var _ = Describe("Git", func() {
})

It("should know the current branch", func() {
branchName := adapters.NewRepository().GetBranchName()
branchName := adapters.NewRepository().GetCurrentBranchName()
Expect(branchName).To(Equal("current-branch-123456789"))
})

It("should get all branch names", func() {
branchNames := adapters.NewRepository().GetAllBranchNames()
Expect(branchNames).To(Equal([]string{"main", "some-branch-#123"}))
})

It("should delete a branch", func() {
error := adapters.NewRepository().DeleteBranch("some-accepted-branch-123456789")
Expect(error).To(BeNil())
})

It("should fail to delete when there is a nonexistent branch", func() {
error := adapters.NewRepository().DeleteBranch("some-nonexistent-branch")
Expect(error).NotTo(BeNil())
Expect(error.Error()).To(Equal("error: branch 'some-nonexistent-branch' not found."))
})
})
2 changes: 1 addition & 1 deletion cli/git-story-open/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ func main() {
tracker := newTracker()
error := usecases.OpenStory(adapters.NewRepository(), tracker, adapters.NewBrowser())
if error != nil {
fmt.Print(error)
fmt.Println(error)
}
}
34 changes: 34 additions & 0 deletions cli/git-story-sweep/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package main

import (
"fmt"
"os"

"github.com/git-story-branch/git-story-branch/adapters"
"github.com/git-story-branch/git-story-branch/usecases"
"gopkg.in/salsita/go-pivotaltracker.v2/v5/pivotal"
)

func newTracker() usecases.Tracker {
apiToken := os.Getenv("TRACKER_API_TOKEN")
client := pivotal.NewClient(apiToken)
return adapters.NewPivotalTracker(client.Stories)
}

func main() {
tracker := newTracker()
results := usecases.SweepAcceptedStories(adapters.NewRepository(), tracker)

hasError := false
for branchName, error := range results {
if error != nil {
hasError = true
fmt.Printf("Could not delete '%s', %s\n", branchName, error)
} else {
fmt.Printf("Deleted '%s'\n", branchName)
}
}
if hasError {
os.Exit(1)
}
}
4 changes: 2 additions & 2 deletions cli/git-story-view/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ func main() {
tracker := newTracker()
story, error := usecases.GetStory(adapters.NewRepository(), tracker)
if error != nil {
fmt.Print(error)
fmt.Println(error)
} else {
fmt.Printf("State: %v\n%v", story.State, story.Description)
fmt.Printf("State: %v\n%v\n", story.State, story.Description)
}
}
18 changes: 18 additions & 0 deletions usecases/entities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package usecases

type Story struct {
ID int
Description string
State string
URL string
}

type Tracker interface {
GetStory(storyID int) (*Story, error)
}

type Repository interface {
GetCurrentBranchName() string
DeleteBranch(branchName string) error
GetAllBranchNames() []string
}
27 changes: 7 additions & 20 deletions usecases/get_story.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@ import (
"strconv"
)

type Story struct {
ID int
Description string
State string
URL string
}

func getPivotalTrackerTaskID(branchName string) (int, error) {
re := regexp.MustCompile(`\d+$`)
taskIDString := re.FindString(branchName)
Expand All @@ -22,22 +15,16 @@ func getPivotalTrackerTaskID(branchName string) (int, error) {

// GetStory comment
func GetStory(repo Repository, tracker Tracker) (*Story, error) {
currentBranchName := repo.GetBranchName()
storyID, branchError := getPivotalTrackerTaskID(currentBranchName)
currentBranchName := repo.GetCurrentBranchName()
return GetStoryByBranchName(currentBranchName, tracker)
}

func GetStoryByBranchName(branchName string, tracker Tracker) (*Story, error) {
storyID, branchError := getPivotalTrackerTaskID(branchName)

if branchError != nil {
return nil, errors.New("Please run in branch that contains a Pivotal Tracker Story ID")
return nil, errors.New("please run in branch that contains a Pivotal Tracker Story ID")
}

return tracker.GetStory(storyID)
}

// Tracker comment
type Tracker interface {
GetStory(storyID int) (*Story, error)
}

// Repository comment
type Repository interface {
GetBranchName() string
}
8 changes: 4 additions & 4 deletions usecases/get_story_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
var _ = Describe("Git Tracker name translator", func() {
Describe("when the current branch is a story ID", func() {
It("should retrieve a Pivotal Tracker Story based on the current git branch name", func() {
mockGitRepo := MockGitRepository{branchName: "Insert Branch Name Here-#1234567890"}
mockGitRepo := &MockGitRepository{currentBranchName: "Insert Branch Name Here-#1234567890"}
mockTrackerReader := MockPivotalTrackerReader{}

story, error := usecases.GetStory(mockGitRepo, mockTrackerReader)
Expand All @@ -21,19 +21,19 @@ var _ = Describe("Git Tracker name translator", func() {
})
Describe("when the current branch is not a story ID", func() {
It("should return an error", func() {
mockGitRepo := MockGitRepository{branchName: "main"}
mockGitRepo := &MockGitRepository{currentBranchName: "main"}
mockTrackerReader := MockPivotalTrackerReader{}

story, error := usecases.GetStory(mockGitRepo, mockTrackerReader)

Expect(story).To(BeNil())
Expect(error).NotTo(BeNil())
Expect(error.Error()).To(ContainSubstring("Please run in branch that contains a Pivotal Tracker Story ID"))
Expect(error.Error()).To(ContainSubstring("please run in branch that contains a Pivotal Tracker Story ID"))
})
})
Describe("when the Tracker API returns an error", func() {
It("should return an error", func() {
mockGitRepo := MockGitRepository{branchName: "Insert Branch Name Here-#1234567890"}
mockGitRepo := &MockGitRepository{currentBranchName: "Insert Branch Name Here-#1234567890"}
mockTrackerReader := MockPivotalTrackerReader{isBroken: true}

story, error := usecases.GetStory(mockGitRepo, mockTrackerReader)
Expand Down
40 changes: 37 additions & 3 deletions usecases/mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,29 @@ func (browserSpy *BrowserSpy) OpenURL(URL string) (*exec.Cmd, error) {
}

type MockGitRepository struct {
branchName string
currentBranchName string
branchNames []string
deletedBranches []string
erroredBranches []string
}

func (mockGitRepo MockGitRepository) GetBranchName() string {
return mockGitRepo.branchName
func (mockGitRepo MockGitRepository) GetCurrentBranchName() string {
return mockGitRepo.currentBranchName
}

func (mockGitRepo MockGitRepository) GetAllBranchNames() []string {
return mockGitRepo.branchNames
}

func (mockGitRepo *MockGitRepository) DeleteBranch(branchName string) error {
mockGitRepo.deletedBranches = append(mockGitRepo.deletedBranches, branchName)

for _, erroredBranch := range mockGitRepo.erroredBranches {
if branchName == erroredBranch {
return errors.New("Kabooooommmmm!")
}
}
return nil
}

type MockPivotalTrackerReader struct {
Expand All @@ -32,6 +50,22 @@ func (mockTrackerReader MockPivotalTrackerReader) GetStory(storyID int) (*usecas
if mockTrackerReader.isBroken {
return nil, errors.New("unable to find that story")
}
if storyID == 123 {
return &usecases.Story{
ID: 123,
Description: "Description",
State: "accepted",
URL: "https://story.com/123",
}, nil
}
if storyID == 234 {
return &usecases.Story{
ID: 234,
Description: "Description",
State: "started",
URL: "https://story.com/234",
}, nil
}
if storyID == 1234567890 {
return &usecases.Story{
ID: 1234567890,
Expand Down
8 changes: 4 additions & 4 deletions usecases/open_story_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
var _ = Describe("Browser Opener use case", func() {
Describe("when the current branch is a story ID", func() {
It("should open the browser to the URL of the story", func() {
mockGitRepo := MockGitRepository{branchName: "Insert Branch Name Here-#1234567890"}
mockGitRepo := &MockGitRepository{currentBranchName: "Insert Branch Name Here-#1234567890"}
browserSpy := &BrowserSpy{}
mockTrackerReader := MockPivotalTrackerReader{}

Expand All @@ -22,19 +22,19 @@ var _ = Describe("Browser Opener use case", func() {
})
Describe("when the current branch is not a story ID", func() {
It("should return an error", func() {
mockGitRepo := MockGitRepository{branchName: "main"}
mockGitRepo := &MockGitRepository{currentBranchName: "main"}
browserSpy := &BrowserSpy{}
mockTrackerReader := MockPivotalTrackerReader{}

error := usecases.OpenStory(mockGitRepo, mockTrackerReader, browserSpy)

Expect(error).NotTo(BeNil())
Expect(error.Error()).To(ContainSubstring("Please run in branch that contains a Pivotal Tracker Story ID"))
Expect(error.Error()).To(ContainSubstring("please run in branch that contains a Pivotal Tracker Story ID"))
})
})
Describe("when the Tracker API returns an error", func() {
It("should return an error", func() {
mockGitRepo := MockGitRepository{branchName: "Insert Branch Name Here-#1234567890"}
mockGitRepo := &MockGitRepository{currentBranchName: "Insert Branch Name Here-#1234567890"}
browserSpy := &BrowserSpy{}
mockTrackerReader := MockPivotalTrackerReader{isBroken: true}

Expand Down
20 changes: 20 additions & 0 deletions usecases/sweep_accepted_stories.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package usecases

type ResultMap map[string]error

// SweepAcceptedStories returns a map of the branches it attempted to delete and an error if that branch was unable to be deleted
func SweepAcceptedStories(repo Repository, tracker Tracker) ResultMap {
branchErrors := make(map[string]error)

branchNames := repo.GetAllBranchNames()

for _, branchName := range branchNames {
story, _ := GetStoryByBranchName(branchName, tracker)

if story != nil && story.State == "accepted" {
branchErrors[branchName] = repo.DeleteBranch(branchName)
}
}

return branchErrors
}
Loading

0 comments on commit 0c98509

Please sign in to comment.