├── LICENSE ├── README.md └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Derek McGowan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git branch-status 2 | 3 | `branch-status` is a git utility to make managing large number of branches 4 | either across many remotes easier. Branch status allows comparing all branches 5 | against their upstream or any arbitrary branch to show the number of 6 | commit differences. This aids in discovering branches which are out of sync 7 | from their own remotes as well as branches that need to be rebased. 8 | 9 | The output of `branch-status` shows a comparison between a branch and either 10 | their upstream or another branch. The comparison shows the number of commits 11 | since the common ancestor in both the target branch (left) and 12 | upstream branch (right). 13 | 14 | ## Installation 15 | 16 | Install via `go get` 17 | 18 | ``` 19 | $ go get github.com/dmcgowan/git-branch-status 20 | ``` 21 | 22 | Don't have go? Install from [golang.org](http://golang.org/doc/install) 23 | 24 | ## Usage 25 | 26 | Ensure the `$GOPATH` `bin` directory is on `$PATH` or that the 27 | `git-branch-status` binary is on the path. Optionally each command 28 | invoked directly by running `git-branch-status`. 29 | 30 | ``` 31 | $ git branch-status 32 | ... 33 | ``` 34 | 35 | ### How to interpret output 36 | 37 | #### Rebase needed 38 | ``` 39 | some-branch 4|15 upstream/some-branch 40 | ``` 41 | This remote branch contains 15 commits 42 | which are not contained in the local branch. Likewise, the local 43 | branch has 4 commits which have been added locally and not pushed 44 | upstream. To rebase, the local branch will need to be rewinded and 45 | the 4 commits replayed on top of the upstream. Run `pull` with the 46 | `--rebase` flag to resolve `git pull --rebase upstream some-branch`. 47 | 48 | #### Local out of sync 49 | ``` 50 | some-branch 0|15 upstream/some-branch 51 | ``` 52 | The remote branch contains commits which are not part of the local 53 | branch. A simple pull will resolve `git pull upstream some-branch` 54 | by fast forwarding the local branch. 55 | 56 | #### Remote out of sync 57 | ``` 58 | some-branch 3|0 upstream/some-branch 59 | ``` 60 | The local branch contains changes which have not been pushed upstream. 61 | If the branch is ready to be pushed, then this can be resolved 62 | by pushing the branch `git push upstream some-branch`. 63 | 64 | ## Common use cases 65 | 66 | ### List out-of-sync branches 67 | List all branches which are out of sync from their upstream. 68 | 69 | ``` 70 | $ git branch-status 71 | v2-registry-command-header 191|26 stevvooe/v2-registry-command-header 72 | ``` 73 | 74 | Show all branches even those in sync with their upstream. 75 | ``` 76 | $ git branch-status -a 77 | 9468-cert-path 0|0 jfrazelle/9468-cert-path 78 | add_hostname_docker_info 0|0 vieux/add_hostname_docker_info 79 | address-digest-deadlock 0|0 stevvooe/address-digest-deadlock 80 | auth-option 0|0 bfirsh/auth-option 81 | concurrent-pull-fix 0|0 stevvooe/concurrent-pull-fix 82 | manifest-close-archive 0|0 rhvgoyal/manifest-close-archive 83 | master 0|0 origin/master 84 | parallel-load 0|0 dougm/parallel-load 85 | registry-info-refactor 0|0 lindenlab/registry-info-refactor 86 | registry_auth_refactor 0|0 jlhawn/registry_auth_refactor 87 | tls_libtrust_auth 0|0 origin/tls_libtrust_auth 88 | tls_libtrust_auth-documentation 0|0 bfirsh/tls_libtrust_auth-documentation 89 | v2-registry-auth 0|0 brianbland/v2-registry-auth 90 | v2-registry-command-header 191|26 stevvooe/v2-registry-command-header 91 | v2-registry-tests 0|0 icecrime/v2-registry-tests 92 | wip_provenance 0|0 origin/wip_provenance 93 | 94 | ``` 95 | 96 | Run `git fetch --all` to ensure all remote upstreams are in sync. 97 | 98 | ### Check rebases against master 99 | 100 | `branch-status` allows comparing branches against any other branch. 101 | This can be useful for checking when a branch was rebased against 102 | another, often useful when needing to keep a branch rebased off of 103 | master. The `-sort` flag can be used to sort by the most out of sync 104 | branches, use `-sort=right` in this case. 105 | 106 | Example 107 | ``` 108 | $ git branch-status -sort=right master 109 | distribution-refactor 5|10 master 110 | distribution-refactor-tibor 7|10 master 111 | concurrent-pull-fix 1|59 master 112 | trust-only-pull-by-digest 10|171 master 113 | trust-demo-tuf-push-pull 16|171 master 114 | vendor-distribution 1|558 master 115 | use-distribution-api 4|1270 master 116 | fix-official-image-management 1|2206 master 117 | v2-push-test 2|2494 master 118 | v2-error-handling 1|2495 master 119 | address-digest-deadlock 1|2556 master 120 | parallel-load 1|3201 master 121 | registry-split 4|3287 master 122 | manifest-close-archive 2|3325 master 123 | search-refactor-test 1|3492 master 124 | remote-client-signature 1|3511 master 125 | v2-registry-basic-auth-fix 1|3610 master 126 | registry-refactor 1|3674 master 127 | endpoint-creation-refactor 2|3674 master 128 | graph-unit-tests 4|3674 master 129 | v1-auth-fix 1|3755 master 130 | v2-registry-mirroring 1|3755 master 131 | registry-panic-fix 2|3755 master 132 | v2-registry-fallback 4|3790 master 133 | v2-registry-command-header 2|3791 master 134 | lk4d4-add_tests_for_registry 22|3980 master 135 | v2-registry-refactor 25|3980 master 136 | v2-registry-tag-fix 18|4145 master 137 | stevvooe-v2-registry 9|4288 master 138 | v2-registry-pushpull 12|4288 master 139 | push-official-v2 8|4308 master 140 | registry_auth_refactor 1|4381 master 141 | 9468-cert-path 1|4443 master 142 | v2-registry-auth 4|4624 master 143 | provenance 9|4624 master 144 | add_hostname_docker_info 2|4783 master 145 | libtrust_key 2|4785 master 146 | network_plugin 1|4992 master 147 | plugin_proposal 1|4992 master 148 | tls_libtrust_auth-documentation 6|5198 master 149 | auth-option 17|5198 master 150 | provenance_resumable 3|5467 master 151 | registry_v2_switch 1|5511 master 152 | wip_provenance 40|6220 master 153 | ``` 154 | 155 | ## License 156 | 157 | See LICENSE 158 | 159 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "sort" 11 | "strings" 12 | ) 13 | 14 | var sortFlag string 15 | var showAll bool 16 | 17 | func init() { 18 | flag.Usage = func() { 19 | name := os.Args[0] 20 | if name == "git-branch-status" { 21 | name = "git branch-status" 22 | } 23 | fmt.Fprintf(os.Stderr, "usage: %s [] []\n\noptions:\n", name) 24 | flag.PrintDefaults() 25 | } 26 | flag.StringVar(&sortFlag, "sort", "name", `How to sort branches (name | left | right) 27 | "name": Sort by name of branch (left side branch) 28 | "left": Sort by number of commits in left branch and not right 29 | "right": Sort by number of commits in right branch and not left`) 30 | flag.BoolVar(&showAll, "a", false, "Show all branches including branches with no commit differences") 31 | } 32 | 33 | func main() { 34 | flag.Parse() 35 | if flag.NArg() > 1 { 36 | flag.Usage() 37 | log.Fatalf("Too many arguments") 38 | } 39 | 40 | upstream := "%(upstream:short)" 41 | if flag.NArg() == 1 { 42 | upstream = flag.Arg(0) 43 | } 44 | 45 | sortT := SortName 46 | if sortFlag != "" { 47 | switch sortFlag { 48 | case "left": 49 | sortT = SortLeft 50 | case "right": 51 | sortT = SortRight 52 | case "name": 53 | default: 54 | flag.Usage() 55 | } 56 | } 57 | 58 | cmd := exec.Command("git", "for-each-ref", fmt.Sprintf("--format=%%(refname:short) %s", upstream), "refs/heads") 59 | cmd.Stderr = os.Stderr 60 | foreachOut, err := cmd.StdoutPipe() 61 | if err != nil { 62 | log.Fatalf("Error getting pipe: %s", err) 63 | } 64 | 65 | if err := cmd.Start(); err != nil { 66 | log.Fatalf("Error starting process: %s", err) 67 | } 68 | 69 | // Prepare output records 70 | var statuses []BranchStatus 71 | var maxLeftLen int 72 | 73 | scanner := bufio.NewScanner(foreachOut) 74 | for scanner.Scan() { 75 | line := scanner.Text() 76 | fields := strings.Fields(scanner.Text()) 77 | if len(fields) > 2 { 78 | log.Fatalf("Unexpected output line: %q", line) 79 | } 80 | if len(fields) == 1 { 81 | // No branch to compare, skip 82 | continue 83 | } 84 | 85 | status, err := GetBranchStatus(fields[0], fields[1]) 86 | if err != nil { 87 | log.Fatalf("Error getting branch status: %s", err) 88 | } 89 | if len(status.Left) > maxLeftLen { 90 | maxLeftLen = len(status.Left) 91 | } 92 | if !showAll && status.LeftCount == 0 && status.RightCount == 0 { 93 | continue 94 | } 95 | 96 | statuses = append(statuses, status) 97 | } 98 | 99 | if err := scanner.Err(); err != nil { 100 | log.Fatalf("Error starting process: %s", err) 101 | } 102 | 103 | if err := cmd.Wait(); err != nil { 104 | log.Fatalf("Error running process: %s", err) 105 | } 106 | 107 | l := NewBranchStatusList(statuses, sortT) 108 | sort.Sort(l) 109 | fmtStr := fmt.Sprintf("%%-%ds %%5d|%%-5d %%s\n", maxLeftLen) 110 | for _, status := range l.Statuses() { 111 | fmt.Printf(fmtStr, status.Left, status.LeftCount, status.RightCount, status.Right) 112 | } 113 | } 114 | 115 | type BranchStatus struct { 116 | Left string 117 | Right string 118 | LeftCount int 119 | RightCount int 120 | } 121 | 122 | func GetBranchStatus(left, right string) (BranchStatus, error) { 123 | args := []string{ 124 | "rev-list", 125 | "--left-right", 126 | left + "..." + right, 127 | "--", 128 | } 129 | cmd := exec.Command("git", args...) 130 | cmd.Stderr = os.Stderr 131 | 132 | p, err := cmd.StdoutPipe() 133 | if err != nil { 134 | return BranchStatus{}, err 135 | } 136 | if err := cmd.Start(); err != nil { 137 | return BranchStatus{}, err 138 | } 139 | 140 | status := BranchStatus{ 141 | Left: left, 142 | Right: right, 143 | } 144 | scanner := bufio.NewScanner(p) 145 | for scanner.Scan() { 146 | b := scanner.Bytes() 147 | switch b[0] { 148 | case '<': 149 | status.LeftCount++ 150 | case '>': 151 | status.RightCount++ 152 | default: 153 | return BranchStatus{}, fmt.Errorf("Unexpected revision line: %q", scanner.Text()) 154 | } 155 | } 156 | if err := scanner.Err(); err != nil { 157 | return BranchStatus{}, err 158 | } 159 | 160 | if err := cmd.Wait(); err != nil { 161 | return BranchStatus{}, err 162 | } 163 | 164 | return status, nil 165 | } 166 | 167 | type SortType int 168 | 169 | const ( 170 | SortName SortType = iota 171 | SortLeft 172 | SortRight 173 | ) 174 | 175 | func LeftLess(s1, s2 BranchStatus) bool { 176 | if s1.LeftCount == s2.LeftCount { 177 | if s1.RightCount == s2.RightCount { 178 | return s1.Left < s2.Left 179 | } 180 | return s1.RightCount < s2.RightCount 181 | } 182 | return s1.LeftCount < s2.LeftCount 183 | } 184 | 185 | func RightLess(s1, s2 BranchStatus) bool { 186 | if s1.RightCount == s2.RightCount { 187 | if s1.LeftCount == s2.LeftCount { 188 | return s1.Left < s2.Left 189 | } 190 | return s1.LeftCount < s2.LeftCount 191 | } 192 | return s1.RightCount < s2.RightCount 193 | } 194 | 195 | func NameLess(s1, s2 BranchStatus) bool { 196 | if s1.Left == s2.Left { 197 | if s1.LeftCount == s2.LeftCount { 198 | return s1.RightCount < s2.RightCount 199 | } 200 | return s1.LeftCount < s2.LeftCount 201 | } 202 | return s1.Left < s2.Left 203 | 204 | } 205 | 206 | type BranchStatusList struct { 207 | statuses []BranchStatus 208 | sort SortType 209 | } 210 | 211 | func NewBranchStatusList(statuses []BranchStatus, sort SortType) *BranchStatusList { 212 | return &BranchStatusList{ 213 | statuses: statuses, 214 | sort: sort, 215 | } 216 | } 217 | 218 | func (l *BranchStatusList) Statuses() []BranchStatus { 219 | return l.statuses 220 | } 221 | 222 | func (l *BranchStatusList) Len() int { 223 | return len(l.statuses) 224 | } 225 | 226 | func (l *BranchStatusList) Less(i, j int) bool { 227 | switch l.sort { 228 | case SortLeft: 229 | return LeftLess(l.statuses[i], l.statuses[j]) 230 | case SortRight: 231 | return RightLess(l.statuses[i], l.statuses[j]) 232 | default: 233 | return NameLess(l.statuses[i], l.statuses[j]) 234 | } 235 | } 236 | 237 | func (l *BranchStatusList) Swap(i, j int) { 238 | l.statuses[i], l.statuses[j] = l.statuses[j], l.statuses[i] 239 | } 240 | --------------------------------------------------------------------------------