package main import ( "bytes" "errors" "fmt" "io" "os/exec" "strconv" "time" "os" ) type GitDir string func (dir GitDir) cmd(args ...string) *exec.Cmd { cmd := exec.Command(args[0], args[1:]...) cmd.Dir = string(dir) return cmd } func (dir GitDir) output(args ...string) ([]byte, error) { return dir.cmd(args...).Output() } func (dir GitDir) run(args ...string) error { // Use .Output() instead of .Run() so that it populates // ExitError.Stderr. _, err := dir.output(args...) return err } func (dir GitDir) Pull() error { return dir.run("git", "fetch") } func (dir GitDir) Push() error { return dir.run("git", "push") } func (dir GitDir) Commit(commit GitCommit) error { cmd := dir.cmd("git", "fast-import", "--done") // stdin pw, err := cmd.StdinPipe() if err != nil { return err } // stderr var buf bytes.Buffer cmd.Stderr = &buf // run if err = cmd.Start(); err != nil { return err } werr := commit.FastExport(pw) if werr != nil { goto end; } _, werr = io.WriteString(pw, "done\n") if werr != nil { goto end; } end: cerr := pw.Close() if err = cmd.Wait(); err != nil { if ee, ok := err.(*exec.ExitError); ok { ee.Stderr = buf.Bytes() } } else if werr != nil { err = werr } else { err = cerr } return err } type GitCommit struct { UserName string UserEmail string Time time.Time Message string Files map[string][]byte } func gitTime(t time.Time) string { return fmt.Sprintf("%d %s", t.Unix(), t.Format("-0700")) } func (edit GitCommit) FastExport(w io.Writer) error { var err error commit := make([]string, len(edit.Files)) i := 0 for name, content := range edit.Files { commit[i] = name if content != nil { if _, err = fmt.Fprintf(w, "blob\nmark :%d\ndata %d\n%s", i+1, len(content), content); err != nil { return err } } } _, err = fmt.Fprintf(w, `commit refs/heads/master author %s <%s> %s committer %s <%s> %s data %d %sfrom HEAD `, edit.UserName, edit.UserEmail, gitTime(edit.Time), edit.UserName, edit.UserEmail, gitTime(edit.Time), len(edit.Message), edit.Message) if err != nil { return err } for i, filename := range commit { if edit.Files[filename] != nil { if _, err = fmt.Fprintf(w, "M 100644 :%d %s\n", i+1, filename); err != nil { return err } } else { if _, err = fmt.Fprintf(w, "D %s\n", filename); err != nil { return err } } } return nil } type GitFile struct { Dir GitDir Mode int32 // 18 bits unsigned Type string Hash string Size int64 } func (f GitFile) Cat() ([]byte, error) { return f.Dir.output("git", "cat-file", "blob", f.Hash) } type GitTree map[string]GitFile var ( ParseErrorSpaces = errors.New("parse: git ls-tree: not enough spaces") ParseErrorTab = errors.New("parse: git ls-tree: no tab") ParseErrorNull = errors.New("parse: git ls-tree: no trailing null") ) func parseGitTreeLine(line []byte) (rname string, rfile GitFile, err error) { // There is probably a better, shorter way of doing this // line = mode SP type SP hash SP size TAB name a := bytes.SplitN(line, []byte{' '}, 4) if len(a) != 4 { err = ParseErrorSpaces return } b := bytes.SplitN(a[3], []byte{'\t'}, 2) if len(b) != 2 { err = ParseErrorTab return } fmode := a[0] ftype := a[1] fhash := a[2] fsize := b[0] fname := b[1] // convert datatypes fmodeN, err := strconv.ParseInt(string(fmode), 10, 19) if err != nil { return } fsizeN := int64(-1) fsizeS := string(bytes.TrimLeft(fsize, " ")) if fsizeS != "-" { fsizeN, err = strconv.ParseInt(fsizeS, 10, 64) if err != nil { return } } rname = string(fname) rfile = GitFile{ Mode: int32(fmodeN), Type: string(ftype), Hash: string(fhash), Size: fsizeN, } return } func (dir GitDir) LsTree() (GitTree, error) { // Get the root tree-ish data, err := dir.output("git", "rev-parse", "HEAD:") if err != nil { return nil, err } treeish := string(bytes.TrimSuffix(data, []byte{'\n'})) // Recursively read said tree-ish data, err = dir.output("git", "ls-tree", "-trlz", treeish) if err != nil { return nil, err } lines := bytes.Split(data, []byte{0}) // The last line is empty line so se can ignore it // (`len(lines)-1`), but because it doesn't include a line for // the root tree, we have to +1. ret := make(GitTree, len(lines)) // Add the root tree ret["/"] = GitFile{ Mode: int32(40000), Type: "tree", Hash: treeish, Size: -1, } // Parse the lines from git ls-tree for _, line := range lines { if len(line) == 0 { continue } name, file, err := parseGitTreeLine(line) if err != nil { panic(err) return nil, err } file.Dir = dir ret["/"+name] = file } return ret, nil }