package libfastimport import ( "bufio" "fmt" "io" "strconv" "strings" "git.lukeshu.com/go/libfastimport/textproto" ) type UnsupportedCommand string func (e UnsupportedCommand) Error() string { return "Unsupported command: " + string(e) } func trimLinePrefix(line string, prefix string) string { if !strings.HasPrefix(line, prefix) { panic("line didn't have prefix") } if !strings.HasSuffix(line, "\n") { panic("line didn't have prefix") } return strings.TrimSuffix(strings.TrimPrefix(line, prefix), "\n") } // A Frontend is something that produces a fast-import stream; the // Frontend object provides methods for reading from it. type Frontend struct { fir *textproto.FIReader cbw *textproto.CatBlobWriter w *bufio.Writer cmd chan Cmd err error } func NewFrontend(fastImport io.Reader, catBlob io.Writer) *Frontend { ret := &Frontend{} ret.fir = textproto.NewFIReader(fastImport) if catBlob != nil { ret.w = bufio.NewWriter(catBlob) ret.cbw = textproto.NewCatBlobWriter(ret.w) } ret.cmd = make(chan Cmd) go func() { ret.err = ret.parse() close(ret.cmd) }() return ret } func (f *Frontend) nextLine() (line string, err error) { for { line, err = f.fir.ReadLine() if err != nil { return } switch { case strings.HasPrefix(line, "#"): f.cmd <- CmdComment{Comment: line[1:]} case strings.HasPrefix(line, "cat-blob "): // 'cat-blob' SP LF f.cmd <- CmdCatBlob{DataRef: trimLinePrefix(line, "cat-blob ")} case strings.HasPrefix(line, "get-mark :"): // 'get-mark' SP ':' LF c := CmdGetMark{} c.Mark, err = strconv.Atoi(trimLinePrefix(line, "get-mark :")) if err != nil { line = "" err = fmt.Errorf("get-mark: %v", err) return } f.cmd <- c default: return } } } func parse_data(line string) (data string, err error) { nl := strings.IndexByte(line, '\n') if nl < 0 { return "", fmt.Errorf("data: expected newline: %v", data) } head := line[:nl+1] rest := line[nl+1:] if !strings.HasPrefix(head, "data ") { return "", fmt.Errorf("data: could not parse: %v", data) } if strings.HasPrefix(head, "data <<") { // Delimited format delim := trimLinePrefix(head, "data <<") suffix := "\n" + delim + "\n" if !strings.HasSuffix(rest, suffix) { return "", fmt.Errorf("data: did not find suffix: %v", suffix) } data = strings.TrimSuffix(rest, suffix) } else { // Exact byte count format size, err := strconv.Atoi(trimLinePrefix(head, "data ")) if err != nil { return "", err } if size != len(rest) { panic("FIReader should not have let this happen") } data = rest } return } func (f *Frontend) parse() error { line, err := f.nextLine() if err != nil { return err } for { switch { case line == "blob\n": // 'blob' LF // mark? // data c := CmdBlob{} line, err = f.nextLine() if err != nil { return err } if strings.HasPrefix(line, "mark :") { c.Mark, err = strconv.Atoi(trimLinePrefix(line, "mark :")) if err != nil { return err } line, err = f.nextLine() if err != nil { return err } } if !strings.HasPrefix(line, "data ") { return fmt.Errorf("Unexpected command in blob: %q", line) } c.Data, err = parse_data(line) if err != nil { return err } f.cmd <- c case line == "checkpoint\n": f.cmd <- CmdCheckpoint{} case line == "done\n": f.cmd <- CmdDone{} case strings.HasPrefix(line, "commit "): // 'commit' SP LF // mark? // ('author' (SP )? SP LT GT SP LF)? // 'committer' (SP )? SP LT GT SP LF // data // ('from' SP LF)? // ('merge' SP LF)* // (filemodify | filedelete | filecopy | filerename | filedeleteall | notemodify)* c := CmdCommit{Ref: trimLinePrefix(line, "commit ")} line, err = f.nextLine() if err != nil { return err } if strings.HasPrefix(line, "mark :") { c.Mark, err = strconv.Atoi(trimLinePrefix(line, "mark :")) if err != nil { return err } line, err = f.nextLine() if err != nil { return err } } if strings.HasPrefix(line, "author ") { author, err := textproto.ParseIdent(trimLinePrefix(line, "author ")) if err != nil { return err } c.Author = &author line, err = f.nextLine() if err != nil { return err } } if !strings.HasPrefix(line, "committer ") { return fmt.Errorf("commit: expected committer command: %v", line) } c.Committer, err = textproto.ParseIdent(trimLinePrefix(line, "committer ")) if err != nil { return err } line, err = f.nextLine() if err != nil { return err } if !strings.HasPrefix(line, "data ") { return fmt.Errorf("commit: expected data command: %v", line) } c.Msg, err = parse_data(line) if err != nil { return err } line, err = f.nextLine() if err != nil { return err } if strings.HasPrefix(line, "from ") { c.From = trimLinePrefix(line, "from ") line, err = f.nextLine() if err != nil { return err } } for strings.HasPrefix(line, "merge ") { c.Merge = append(c.Merge, trimLinePrefix(line, "merge ")) line, err = f.nextLine() if err != nil { return err } } for { switch { case strings.HasPrefix(line, "M "): str := trimLinePrefix(line, "M ") sp1 := strings.IndexByte(str, ' ') sp2 := strings.IndexByte(str, ' ') if sp1 < 0 || sp2 < 0 { return fmt.Errorf("commit: malformed modify command: %v", line) } nMode, err := strconv.ParseUint(str[:sp1], 8, 18) if err != nil { return err } ref := str[sp1+1 : sp2] path := textproto.PathUnescape(str[sp2+1:]) if ref == "inline" { line, err = f.nextLine() if err != nil { return err } if !strings.HasPrefix(line, "data ") { return fmt.Errorf("commit: modify: expected data command: %v", line) } data, err := parse_data(line) if err != nil { return err } c.Tree = append(c.Tree, FileModifyInline{Mode: textproto.Mode(nMode), Path: path, Data: data}) } else { c.Tree = append(c.Tree, FileModify{Mode: textproto.Mode(nMode), Path: path, DataRef: ref}) } case strings.HasPrefix(line, "D "): c.Tree = append(c.Tree, FileDelete{Path: textproto.PathUnescape(trimLinePrefix(line, "D "))}) case strings.HasPrefix(line, "C "): panic("C not implemented") // TODO case strings.HasPrefix(line, "R "): panic("R not implemented") // TODO case strings.HasPrefix(line, "N "): str := trimLinePrefix(line, "N ") sp := strings.IndexByte(str, ' ') if sp < 0 { return fmt.Errorf("commit: malformed notemodify command: %v", line) } ref := str[:sp] commitish := str[sp+1:] if ref == "inline" { line, err = f.nextLine() if err != nil { return err } if !strings.HasPrefix(line, "data ") { return fmt.Errorf("commit: notemodify: expected data command: %v", line) } data, err := parse_data(line) if err != nil { return err } c.Tree = append(c.Tree, NoteModifyInline{CommitIsh: commitish, Data: data}) } else { c.Tree = append(c.Tree, NoteModify{CommitIsh: commitish, DataRef: ref}) } case strings.HasPrefix(line, "ls "): panic("ls not implemented") // TODO case line == "deleteall\n": c.Tree = append(c.Tree, FileDeleteAll{}) default: break } line, err = f.nextLine() if err != nil { return err } } f.cmd <- c continue case strings.HasPrefix(line, "feature "): // 'feature' SP ('=' )? LF str := trimLinePrefix(line, "feature ") eq := strings.IndexByte(str, '=') if eq < 0 { f.cmd <- CmdFeature{ Feature: str, } } else { f.cmd <- CmdFeature{ Feature: str[:eq], Argument: str[eq+1:], } } case strings.HasPrefix(line, "ls "): // 'ls' SP SP LF sp1 := strings.IndexByte(line, ' ') sp2 := strings.IndexByte(line[sp1+1:], ' ') lf := strings.IndexByte(line[sp2+1:], '\n') if sp1 < 0 || sp2 < 0 || lf < 0 { return fmt.Errorf("ls: outside of a commit both and are required: %v", line) } f.cmd <- CmdLs{ DataRef: line[sp1+1 : sp2], Path: textproto.PathUnescape(line[sp2+1 : lf]), } case strings.HasPrefix(line, "option "): // 'option' SP