Skip to content

Commit

Permalink
Switch to porcelain v2
Browse files Browse the repository at this point in the history
Drop regex-based parsing in favor of
bufio.Scanner based approach

This needs at least git v2.13.2
  • Loading branch information
robertgzr committed Jul 3, 2017
1 parent 0c8ca3c commit e366704
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 320 deletions.
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
porcelain
============

Parses `git status --porcelain` and outputs nicely formatted strings.
Parses `git status --porcelain=v2 --branch` and outputs nicely formatted strings for your shell.

**Formatted output with `porcelain -fmt`**
<img width="646" alt="screen_shot" src="https://user-images.githubusercontent.com/3930615/27802035-9c1b92d2-6021-11e7-9289-7b8a17164bf4.png">

![formatted output screenshot](http://i.imgur.com/d3Ckvbj.png)
The minimum git version for porcelain v2 with `--branch` is `v2.13.2`.
Otherwise you can use the old porcelain v1 based parser on the [`legacy` branch](/~https://github.com/robertgzr/porcelain/tree/legacy).

![formatted output screenshot 2](http://i.imgur.com/xAnGH7C.png)
With a working Go environment do: `go get -u github.com/robertgzr/porcelain`

**Basic output with `porcelain -basic`**
Binaries can be found [here](/~https://github.com/robertgzr/porcelain/releases).

![basic output screenshot](http://i.imgur.com/F1DnTOA.png)
## Output explained:

```
commit,branch,tracked_branch,ahead,behind,untracked,added,modified,deleted,renamed,copied
```
` <branch>@<commit> [↑/↓ <ahead/behind count>][untracked][unmerged][modified][dirty/clean]`

With a working Go environment do: `go get -u github.com/robertgzr/porcelain`
- `?` : untracked files
- `` : unmerged : merge in process
- `Δ` : modified : unstaged changes

Binaries can be found [here](/~https://github.com/robertgzr/porcelain/releases).
Definitions taken from: https://www.kernel.org/pub/software/scm/git/docs/gitglossary.html#def_dirty
- `` : dirty : working tree contains uncommited but staged changes
- `` : clean : working tree corresponds to the revision referenced by HEAD

---

Expand Down
40 changes: 40 additions & 0 deletions git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

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

const notRepoStatus string = "exit status 128"

var ErrNotAGitRepo error = errors.New("not a git repo")

func GetGitOutput(cwd string) (io.Reader, error) {
var buf = new(bytes.Buffer)

cmd := exec.Command("git", "status", "--porcelain=v2", "--branch")
cmd.Stderr = buf
cmd.Stdout = buf

if err := cmd.Run(); err != nil {
if cmd.ProcessState.String() == notRepoStatus {
return nil, ErrNotAGitRepo
}
return nil, err
}

return buf, nil
}

func PathToGitDir(cwd string) (string, error) {
cmd := exec.Command("git", "rev-parse", "--absolute-git-dir")
cmd.Dir = cwd
out, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
154 changes: 154 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package main

import (
"bufio"
"io"
"strconv"
"strings"
)

func consumeNext(s *bufio.Scanner) string {
if s.Scan() {
return s.Text()
}
return ""
}

func (pi *PorcInfo) ParsePorcInfo(r io.Reader) error {
var err error
var s = bufio.NewScanner(r)

for s.Scan() {
if len(s.Text()) < 1 {
continue
}

pi.ParseLine(s.Text())
}

return err
}

func (pi *PorcInfo) ParseLine(line string) error {
s := bufio.NewScanner(strings.NewReader(line))
// switch to a word based scanner
s.Split(bufio.ScanWords)

for s.Scan() {
switch s.Text() {
case "#":
pi.parseBranchInfo(s)
case "1":
pi.parseTrackedFile(s)
case "2":
pi.parseRenamedFile(s)
case "u":
pi.unmerged++
case "?":
pi.untracked++
}
}
return nil
}

func (pi *PorcInfo) parseBranchInfo(s *bufio.Scanner) (err error) {
// uses the word based scanner from ParseLine
for s.Scan() {
switch s.Text() {
case "branch.oid":
pi.commit = consumeNext(s)
case "branch.head":
pi.branch = consumeNext(s)
case "branch.upstream":
pi.upstream = consumeNext(s)
case "branch.ab":
err = pi.parseAheadBehind(s)
}
}
return err
}

func (pi *PorcInfo) parseAheadBehind(s *bufio.Scanner) error {
// uses the word based scanner from ParseLine
for s.Scan() {
i, err := strconv.Atoi(s.Text()[1:])
if err != nil {
return err
}

switch s.Text()[:1] {
case "+":
pi.ahead = i
case "-":
pi.behind = i
}
}
return nil
}

// parseTrackedFile parses the porcelain v2 output for tracked entries
// doc: https://git-scm.com/docs/git-status#_changed_tracked_entries
//
func (pi *PorcInfo) parseTrackedFile(s *bufio.Scanner) error {
// uses the word based scanner from ParseLine
var index int
for s.Scan() {
switch index {
case 0: // xy
pi.parseXY(s.Text())
default:
continue
// case 1: // sub
// if s.Text() != "N..." {
// log.Println("is submodule!!!")
// }
// case 2: // mH - octal file mode in HEAD
// log.Println(index, s.Text())
// case 3: // mI - octal file mode in index
// log.Println(index, s.Text())
// case 4: // mW - octal file mode in worktree
// log.Println(index, s.Text())
// case 5: // hH - object name in HEAD
// log.Println(index, s.Text())
// case 6: // hI - object name in index
// log.Println(index, s.Text())
// case 7: // path
// log.Println(index, s.Text())
}
index++
}
return nil
}

func (pi *PorcInfo) parseXY(xy string) error {
switch xy[:1] { // parse staged
case "M":
pi.Staged.modified++
case "A":
pi.Staged.added++
case "D":
pi.Staged.deleted++
case "R":
pi.Staged.renamed++
case "C":
pi.Staged.copied++
}

switch xy[1:] { // parse unstaged
case "M":
pi.Unstaged.modified++
case "A":
pi.Unstaged.added++
case "D":
pi.Unstaged.deleted++
case "R":
pi.Unstaged.renamed++
case "C":
pi.Unstaged.copied++
}
return nil
}

func (pi *PorcInfo) parseRenamedFile(s *bufio.Scanner) error {
return pi.parseTrackedFile(s)
}
61 changes: 61 additions & 0 deletions parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"reflect"
"strings"
"testing"
)

const gitoutput string = `
# branch.oid 51c9c58e2175b768137c1e38865f394c76a7d49d
# branch.head master
# branch.upstream origin/master
# branch.ab +1 -10
1 .M N... 100644 100644 100644 3e2ceb914cf9be46bf235432781840f4145363fd 3e2ceb914cf9be46bf235432781840f4145363fd Gopkg.lock
1 .M N... 100644 100644 100644 cecb683e6e626bcba909ddd36d3357d49f0cfd09 cecb683e6e626bcba909ddd36d3357d49f0cfd09 Gopkg.toml
1 .M N... 100644 100644 100644 aea984b7df090ce3a5826a854f3e5364cd8f2ccd aea984b7df090ce3a5826a854f3e5364cd8f2ccd porcelain.go
1 .D N... 100644 100644 000000 6d9532ba55b84ec4faf214f9cdb9ce70ec8f4f5b 6d9532ba55b84ec4faf214f9cdb9ce70ec8f4f5b porcelain_test.go
2 R. N... 100644 100644 100644 44d0a25072ee3706a8015bef72bdd2c4ab6da76d 44d0a25072ee3706a8015bef72bdd2c4ab6da76d R100 hm.rb hw.rb
u UU N... 100644 100644 100644 100644 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 36c06c8752c78d2aff89571132f3bf7841a7b5c3 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd hw.rb
? _porcelain_test.go
? git.go
? git_test.go
? goreleaser.yml
? vendor/
`

var expectedPorcInfo = PorcInfo{
branch: "master",
commit: "51c9c58e2175b768137c1e38865f394c76a7d49d",
remote: "",
upstream: "origin/master",
ahead: 1,
behind: 10,
untracked: 5,
unmerged: 1,
Unstaged: GitArea{
modified: 3,
added: 0,
deleted: 1,
renamed: 0,
copied: 0,
},
Staged: GitArea{
modified: 0,
added: 0,
deleted: 0,
renamed: 1,
copied: 0,
},
}

func TestParsePorcInfo(t *testing.T) {
var pi = new(PorcInfo)
if err := pi.ParsePorcInfo(strings.NewReader(gitoutput)); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(&expectedPorcInfo, pi) {
t.Logf("%#+v\n", pi)
t.FailNow()
}
}
Loading

0 comments on commit e366704

Please sign in to comment.