Skip to content

Commit

Permalink
New hub sync command to update local branches
Browse files Browse the repository at this point in the history
  • Loading branch information
mislav committed Sep 12, 2016
1 parent b5f257a commit ce24b1c
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 22 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ HELP_CMD = \
share/man/man1/hub-pull-request.1 \
share/man/man1/hub-release.1 \
share/man/man1/hub-issue.1 \
share/man/man1/hub-sync.1 \

HELP_EXT = \
share/man/man1/hub-am.1 \
Expand Down
5 changes: 4 additions & 1 deletion commands/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ func (r *Runner) Execute() ExecError {
return execErr
}

err = git.Run(args.Command, args.Params...)
gitArgs := []string{args.Command}
gitArgs = append(gitArgs, args.Params...)

err = git.Run(gitArgs...)
return newExecError(err)
}

Expand Down
132 changes: 132 additions & 0 deletions commands/sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package commands

import (
"fmt"
"os"
"regexp"
"strings"

"github.com/github/hub/git"
"github.com/github/hub/github"
"github.com/github/hub/ui"
"github.com/github/hub/utils"
)

var cmdSync = &Command{
Run: sync,
Usage: "sync",
Long: `Fetch git objects from upstream and update branches.
- If the local branch is outdated, fast-forward it;
- If the local branch contains unpushed work, warn about it;
- If the branch seems merged and its upstream branch was deleted, delete it.
If a local branch doesn't have any upstream configuration, but has a
same-named branch on the remote, treat that as its upstream branch.
## See also:
hub(1), git-fetch(1)
`,
}

func init() {
CmdRunner.Use(cmdSync)
}

func sync(cmd *Command, args *Args) {
localRepo, err := github.LocalRepo()
utils.Check(err)

remote, err := localRepo.MainRemote()
utils.Check(err)

defaultBranch := localRepo.MasterBranch().ShortName()
fullDefaultBranch := fmt.Sprintf("refs/remotes/%s/%s", remote.Name, defaultBranch)
currentBranch := ""
if curBranch, err := localRepo.CurrentBranch(); err == nil {
currentBranch = curBranch.ShortName()
}

err = git.Spawn("fetch", "--prune", "--quiet", "--progress", remote.Name)
utils.Check(err)

branchToRemote := map[string]string{}
if lines, err := git.ConfigAll("branch.*.remote"); err == nil {
configRe := regexp.MustCompile(`^branch\.(.+?)\.remote (.+)`)

for _, line := range lines {
if matches := configRe.FindStringSubmatch(line); len(matches) > 0 {
branchToRemote[matches[1]] = matches[2]
}
}
}

branches, err := git.LocalBranches()
utils.Check(err)

var green,
lightGreen,
red,
lightRed,
resetColor string

if ui.IsTerminal(os.Stdout) {
green = "\033[32m"
lightGreen = "\033[32;1m"
red = "\033[31m"
lightRed = "\033[31;1m"
resetColor = "\033[0m"
}

for _, branch := range branches {
fullBranch := fmt.Sprintf("refs/heads/%s", branch)
remoteBranch := fmt.Sprintf("refs/remotes/%s/%s", remote.Name, branch)
gone := false

if branchToRemote[branch] == remote.Name {
if upstream, err := git.SymbolicFullName(fmt.Sprintf("%s@{upstream}", branch)); err == nil {
remoteBranch = upstream
} else {
remoteBranch = ""
gone = true
}
} else if !git.HasFile(strings.Split(remoteBranch, "/")...) {
remoteBranch = ""
}

if remoteBranch != "" {
diff, err := git.NewRange(fullBranch, remoteBranch)
utils.Check(err)

if diff.IsIdentical() {
continue
} else if diff.IsAncestor() {
if branch == currentBranch {
git.Quiet("merge", "--ff-only", "--quiet", remoteBranch)
} else {
git.Quiet("update-ref", fullBranch, remoteBranch)
}
ui.Printf("%sUpdated branch %s%s%s (was %s).\n", green, lightGreen, branch, resetColor, diff.A[0:7])
} else {
ui.Errorf("warning: `%s' seems to contain unpushed commits\n", branch)
}
} else if gone {
diff, err := git.NewRange(fullBranch, fullDefaultBranch)
utils.Check(err)

if diff.IsAncestor() {
if branch == currentBranch {
git.Quiet("checkout", "--quiet", defaultBranch)
currentBranch = defaultBranch
}
git.Quiet("branch", "-D", branch)
ui.Printf("%sDeleted branch %s%s%s (was %s).\n", red, lightRed, branch, resetColor, diff.A[0:7])
} else {
ui.Errorf("warning: `%s' was deleted on %s, but appears not merged into %s\n", branch, remote.Name, defaultBranch)
}
}
}

args.NoForward()
}
85 changes: 64 additions & 21 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,29 @@ func RefList(a, b string) ([]string, error) {
return output, nil
}

func NewRange(a, b string) (*Range, error) {
output, err := gitOutput("rev-parse", "-q", a, b)
if err != nil {
return nil, err
}

return &Range{output[0], output[1]}, nil
}

type Range struct {
A string
B string
}

func (r *Range) IsIdentical() bool {
return strings.EqualFold(r.A, r.B)
}

func (r *Range) IsAncestor() bool {
cmd := gitCmd("merge-base", "--is-ancestor", r.A, r.B)
return cmd.Success()
}

func CommentChar() string {
char, err := Config("core.commentchar")
if err != nil {
Expand Down Expand Up @@ -209,7 +232,12 @@ func Config(name string) (string, error) {
}

func ConfigAll(name string) ([]string, error) {
lines, err := gitOutput(gitConfigCommand([]string{"--get-all", name})...)
mode := "--get-all"
if strings.Contains(name, "*") {
mode = "--get-regexp"
}

lines, err := gitOutput(gitConfigCommand([]string{mode, name})...)
if err != nil {
err = fmt.Errorf("Unknown config %s", name)
}
Expand Down Expand Up @@ -251,20 +279,19 @@ func Alias(name string) (string, error) {
return Config(fmt.Sprintf("alias.%s", name))
}

func Run(command string, args ...string) error {
cmd := cmd.New("git")

for _, v := range GlobalFlags {
cmd.WithArg(v)
}

cmd.WithArg(command)
func Run(args ...string) error {
cmd := gitCmd(args...)
return cmd.Run()
}

for _, a := range args {
cmd.WithArg(a)
}
func Spawn(args ...string) error {
cmd := gitCmd(args...)
return cmd.Spawn()
}

return cmd.Run()
func Quiet(args ...string) bool {
cmd := gitCmd(args...)
return cmd.Success()
}

func IsGitDir(dir string) bool {
Expand All @@ -273,16 +300,18 @@ func IsGitDir(dir string) bool {
return cmd.Success()
}

func gitOutput(input ...string) (outputs []string, err error) {
cmd := cmd.New("git")

for _, v := range GlobalFlags {
cmd.WithArg(v)
func LocalBranches() ([]string, error) {
lines, err := gitOutput("branch", "--list")
if err == nil {
for i, line := range lines {
lines[i] = strings.TrimPrefix(line, "* ")
}
}
return lines, err
}

for _, i := range input {
cmd.WithArg(i)
}
func gitOutput(input ...string) (outputs []string, err error) {
cmd := gitCmd(input...)

out, err := cmd.CombinedOutput()
for _, line := range strings.Split(out, "\n") {
Expand All @@ -294,3 +323,17 @@ func gitOutput(input ...string) (outputs []string, err error) {

return outputs, err
}

func gitCmd(args ...string) *cmd.Cmd {
cmd := cmd.New("git")

for _, v := range GlobalFlags {
cmd.WithArg(v)
}

for _, a := range args {
cmd.WithArg(a)
}

return cmd
}
3 changes: 3 additions & 0 deletions share/man/man1/hub.1.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ git but that are extended through hub, and custom ones that hub provides.
* hub-release(1):
List and create GitHub releases.

* hub-sync(1):
Fetch from upstream and update local branches.

## CONFIGURATION

### GitHub OAuth authentication
Expand Down

0 comments on commit ce24b1c

Please sign in to comment.