// ReadAllHeaders reads the file headers and extended headers (if any) // from a file unified diff. It does not read hunks, and the returned // FileDiff's Hunks field is nil. To read the hunks, call the // (*FileDiffReader).HunksReader() method to get a HunksReader and // read hunks from that. func (r *FileDiffReader) ReadAllHeaders() (*FileDiff, error) { var err error fd := &FileDiff{} fd.Extended, err = r.ReadExtendedHeaders() if err != nil { return nil, err } var origTime, newTime *time.Time fd.OrigName, fd.NewName, origTime, newTime, err = r.ReadFileHeaders() if err != nil { return nil, err } if origTime != nil { ts := pbtypes.NewTimestamp(*origTime) fd.OrigTime = &ts } if newTime != nil { ts := pbtypes.NewTimestamp(*newTime) fd.NewTime = &ts } return fd, nil }
func (r *Repository) makeCommit(rec *hg_revlog.Rec) (*vcs.Commit, error) { fb := hg_revlog.NewFileBuilder() ce, err := hg_changelog.BuildEntry(rec, fb) if err != nil { return nil, err } addr, err := mail.ParseAddress(ce.Committer) if err != nil { // This occurs when the commit author specifier is // malformed. Fall back to just using the whole committer // string as the name. addr = &mail.Address{ Name: ce.Committer, Address: "", } } var parents []vcs.CommitID if !rec.IsStartOfBranch() { if p := rec.Parent(); p != nil { parents = append(parents, vcs.CommitID(hex.EncodeToString(rec.Parent().Id()))) } if rec.Parent2Present() { parents = append(parents, vcs.CommitID(hex.EncodeToString(rec.Parent2().Id()))) } } return &vcs.Commit{ ID: vcs.CommitID(ce.Id), Author: vcs.Signature{addr.Name, addr.Address, pbtypes.NewTimestamp(ce.Date)}, Message: ce.Comment, Parents: parents, }, nil }
func TestServeRepoTreeEntry_File(t *testing.T) { setupHandlerTest() defer teardownHandlerTest() commitID := vcs.CommitID(strings.Repeat("a", 40)) repoPath := "a.b/c" rm := &mockFileSystem{ t: t, at: commitID, fs: mapFS(map[string]string{"myfile": "mydata"}), } sm := &mockServiceForExistingRepo{ t: t, repoPath: repoPath, repo: rm, } testHandler.Service = sm resp, err := http.Get(server.URL + testHandler.router.URLToRepoTreeEntry(repoPath, commitID, "myfile").String()) if err != nil { t.Fatal(err) } defer resp.Body.Close() if got, want := resp.StatusCode, http.StatusOK; got != want { t.Errorf("got status code %d, want %d", got, want) } if !sm.opened { t.Errorf("!opened") } if !rm.called { t.Errorf("!called") } var e *vcsclient.TreeEntry if err := json.NewDecoder(resp.Body).Decode(&e); err != nil { t.Fatal(err) } wantEntry := &vcsclient.TreeEntry{ Name: "myfile", Type: vcsclient.FileEntry, Size: 6, ModTime: pbtypes.NewTimestamp(time.Time{}), Contents: []byte("mydata"), } if !reflect.DeepEqual(e, wantEntry) { t.Errorf("got tree entry %+v, want %+v", e, wantEntry) } // used canonical commit ID, so should be long-cached if cc := resp.Header.Get("cache-control"); cc != longCacheControl { t.Errorf("got cache-control %q, want %q", cc, longCacheControl) } }
func (r *Repository) makeCommit(c *git2go.Commit) *vcs.Commit { var parents []vcs.CommitID if pc := c.ParentCount(); pc > 0 { parents = make([]vcs.CommitID, pc) for i := 0; i < int(pc); i++ { parents[i] = vcs.CommitID(c.ParentId(uint(i)).String()) } } au, cm := c.Author(), c.Committer() return &vcs.Commit{ ID: vcs.CommitID(c.Id().String()), Author: vcs.Signature{au.Name, au.Email, pbtypes.NewTimestamp(au.When)}, Committer: &vcs.Signature{cm.Name, cm.Email, pbtypes.NewTimestamp(cm.When)}, Message: strings.TrimSuffix(c.Message(), "\n"), Parents: parents, } }
// Convert a git.Commit to a vcs.Commit func (r *Repository) vcsCommit(commit *git.Commit) *vcs.Commit { var committer *vcs.Signature if commit.Committer != nil { committer = &vcs.Signature{ Name: commit.Committer.Name, Email: commit.Committer.Email, Date: pbtypes.NewTimestamp(commit.Committer.When), } } n := commit.ParentCount() parentIds := commit.ParentIds() parents := make([]vcs.CommitID, 0, len(parentIds)) for _, id := range parentIds { parents = append(parents, vcs.CommitID(id.String())) } if n == 0 { // Required to make reflect.DeepEqual tests pass. :/ parents = nil } var author vcs.Signature if commit.Author != nil { author.Name = commit.Author.Name author.Email = commit.Author.Email author.Date = pbtypes.NewTimestamp(commit.Author.When) } return &vcs.Commit{ ID: vcs.CommitID(commit.Id.String()), Author: author, Committer: committer, Message: strings.TrimSuffix(commit.Message(), "\n"), Parents: parents, } }
func newTreeEntry(fi os.FileInfo) *TreeEntry { e := &TreeEntry{ Name: fi.Name(), Size: fi.Size(), ModTime: pbtypes.NewTimestamp(fi.ModTime()), } if fi.Mode().IsDir() { e.Type = DirEntry } else if fi.Mode().IsRegular() { e.Type = FileEntry } else if fi.Mode()&os.ModeSymlink != 0 { e.Type = SymlinkEntry } return e }
func (r *Repository) BlameFile(path string, opt *vcs.BlameOptions) ([]*vcs.Hunk, error) { if opt == nil { opt = &vcs.BlameOptions{} } // TODO(sqs): implement OldestCommit cmd := exec.Command("python", "-", r.Dir, string(opt.NewestCommit), path) cmd.Dir = r.Dir cmd.Stdin = strings.NewReader(hgRepoAnnotatePy) stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } stderr, err := cmd.StderrPipe() if err != nil { return nil, err } in := bufio.NewReader(stdout) if err := cmd.Start(); err != nil { return nil, err } var data struct { Commits map[string]struct { Author struct{ Name, Email string } AuthorDate time.Time } Hunks map[string][]struct { CommitID string StartLine, EndLine int StartByte, EndByte int } } jsonErr := json.NewDecoder(in).Decode(&data) errOut, _ := ioutil.ReadAll(stderr) if jsonErr != nil { cmd.Wait() return nil, fmt.Errorf("%s (stderr: %s)", jsonErr, errOut) } if err := cmd.Wait(); err != nil { return nil, fmt.Errorf("%s (stderr: %s)", err, errOut) } hunks := make([]*vcs.Hunk, len(data.Hunks[path])) for i, hunk := range data.Hunks[path] { c := data.Commits[hunk.CommitID] hunks[i] = &vcs.Hunk{ StartLine: hunk.StartLine, EndLine: hunk.EndLine, StartByte: hunk.StartByte, EndByte: hunk.EndByte, CommitID: vcs.CommitID(hunk.CommitID), Author: vcs.Signature{ Name: c.Author.Name, Email: c.Author.Email, Date: pbtypes.NewTimestamp(c.AuthorDate.In(time.UTC)), }, } } return hunks, nil }
func (r *Repository) commitLog(opt vcs.CommitsOptions) ([]*vcs.Commit, uint, error) { revSpec := string(opt.Head) if opt.Skip != 0 { revSpec += "~" + strconv.FormatUint(uint64(opt.N), 10) } args := []string{"log", `--template={node}\x00{author|person}\x00{author|email}\x00{date|rfc3339date}\x00{desc}\x00{p1node}\x00{p2node}\x00`} if opt.N != 0 { args = append(args, "--limit", strconv.FormatUint(uint64(opt.N), 10)) } args = append(args, "--rev="+revSpec+":0") cmd := exec.Command("hg", args...) cmd.Dir = r.Dir out, err := cmd.CombinedOutput() if err != nil { out = bytes.TrimSpace(out) if isUnknownRevisionError(string(out), revSpec) { return nil, 0, vcs.ErrCommitNotFound } return nil, 0, fmt.Errorf("exec `hg log` failed: %s. Output was:\n\n%s", err, out) } const partsPerCommit = 7 // number of \x00-separated fields per commit allParts := bytes.Split(out, []byte{'\x00'}) numCommits := len(allParts) / partsPerCommit commits := make([]*vcs.Commit, numCommits) for i := 0; i < numCommits; i++ { parts := allParts[partsPerCommit*i : partsPerCommit*(i+1)] id := vcs.CommitID(parts[0]) authorTime, err := time.Parse(time.RFC3339, string(parts[3])) if err != nil { log.Println(err) //return nil, 0, err } parents, err := r.getParents(id) if err != nil { return nil, 0, fmt.Errorf("r.GetParents failed: %s. Output was:\n\n%s", err, out) } commits[i] = &vcs.Commit{ ID: id, Author: vcs.Signature{string(parts[1]), string(parts[2]), pbtypes.NewTimestamp(authorTime)}, Message: string(parts[4]), Parents: parents, } } // Count commits. var total uint if !opt.NoTotal { cmd = exec.Command("hg", "id", "--num", "--rev="+revSpec) cmd.Dir = r.Dir out, err = cmd.CombinedOutput() if err != nil { return nil, 0, fmt.Errorf("exec `hg id --num` failed: %s. Output was:\n\n%s", err, out) } out = bytes.TrimSpace(out) total, err = parseUint(string(out)) if err != nil { return nil, 0, err } total++ // sequence number is 1 less than total number of commits // Add back however many we skipped. total += opt.Skip } return commits, total, nil }
func (r *Repository) BlameFile(path string, opt *vcs.BlameOptions) ([]*vcs.Hunk, error) { r.editLock.RLock() defer r.editLock.RUnlock() if opt == nil { opt = &vcs.BlameOptions{} } if opt.OldestCommit != "" { return nil, fmt.Errorf("OldestCommit not implemented") } if err := checkSpecArgSafety(string(opt.NewestCommit)); err != nil { return nil, err } if err := checkSpecArgSafety(string(opt.OldestCommit)); err != nil { return nil, err } args := []string{"blame", "-w", "--porcelain"} if opt.StartLine != 0 || opt.EndLine != 0 { args = append(args, fmt.Sprintf("-L%d,%d", opt.StartLine, opt.EndLine)) } args = append(args, string(opt.NewestCommit), "--", filepath.ToSlash(path)) cmd := exec.Command("git", args...) cmd.Dir = r.Dir out, err := cmd.CombinedOutput() if err != nil { return nil, fmt.Errorf("exec `git blame` failed: %s. Output was:\n\n%s", err, out) } if len(out) < 1 { // go 1.8.5 changed the behavior of `git blame` on empty files. // previously, it returned a boundary commit. now, it returns nothing. // TODO(sqs) TODO(beyang): make `git blame` return the boundary commit // on an empty file somehow, or come up with some other workaround. st, err := os.Stat(filepath.Join(r.Dir, path)) if err == nil && st.Size() == 0 { return nil, nil } return nil, fmt.Errorf("Expected git output of length at least 1") } commits := make(map[string]vcs.Commit) hunks := make([]*vcs.Hunk, 0) remainingLines := strings.Split(string(out[:len(out)-1]), "\n") byteOffset := 0 for len(remainingLines) > 0 { // Consume hunk hunkHeader := strings.Split(remainingLines[0], " ") if len(hunkHeader) != 4 { fmt.Printf("Remaining lines: %+v, %d, '%s'\n", remainingLines, len(remainingLines), remainingLines[0]) return nil, fmt.Errorf("Expected at least 4 parts to hunkHeader, but got: '%s'", hunkHeader) } commitID := hunkHeader[0] lineNoCur, _ := strconv.Atoi(hunkHeader[2]) nLines, _ := strconv.Atoi(hunkHeader[3]) hunk := &vcs.Hunk{ CommitID: vcs.CommitID(commitID), StartLine: int(lineNoCur), EndLine: int(lineNoCur + nLines), StartByte: byteOffset, } if _, in := commits[commitID]; in { // Already seen commit byteOffset += len(remainingLines[1]) remainingLines = remainingLines[2:] } else { // New commit author := strings.Join(strings.Split(remainingLines[1], " ")[1:], " ") email := strings.Join(strings.Split(remainingLines[2], " ")[1:], " ") if len(email) >= 2 && email[0] == '<' && email[len(email)-1] == '>' { email = email[1 : len(email)-1] } authorTime, err := strconv.ParseInt(strings.Join(strings.Split(remainingLines[3], " ")[1:], " "), 10, 64) if err != nil { return nil, fmt.Errorf("Failed to parse author-time %q", remainingLines[3]) } summary := strings.Join(strings.Split(remainingLines[9], " ")[1:], " ") commit := vcs.Commit{ ID: vcs.CommitID(commitID), Message: summary, Author: vcs.Signature{ Name: author, Email: email, Date: pbtypes.NewTimestamp(time.Unix(authorTime, 0).In(time.UTC)), }, } if len(remainingLines) >= 13 && strings.HasPrefix(remainingLines[10], "previous ") { byteOffset += len(remainingLines[12]) remainingLines = remainingLines[13:] } else if len(remainingLines) >= 13 && remainingLines[10] == "boundary" { byteOffset += len(remainingLines[12]) remainingLines = remainingLines[13:] } else if len(remainingLines) >= 12 { byteOffset += len(remainingLines[11]) remainingLines = remainingLines[12:] } else if len(remainingLines) == 11 { // Empty file remainingLines = remainingLines[11:] } else { return nil, fmt.Errorf("Unexpected number of remaining lines (%d):\n%s", len(remainingLines), " "+strings.Join(remainingLines, "\n ")) } commits[commitID] = commit } if commit, present := commits[commitID]; present { // Should always be present, but check just to avoid // panicking in case of a (somewhat likely) bug in our // git-blame parser above. hunk.CommitID = commit.ID hunk.Author = commit.Author } // Consume remaining lines in hunk for i := 1; i < nLines; i++ { byteOffset += len(remainingLines[1]) remainingLines = remainingLines[2:] } hunk.EndByte = byteOffset hunks = append(hunks, hunk) } return hunks, nil }
// commitLog returns a list of commits, and total number of commits // starting from Head until Base or beginning of branch (unless NoTotal is true). // // The caller is responsible for doing checkSpecArgSafety on opt.Head and opt.Base. func (r *Repository) commitLog(opt vcs.CommitsOptions) ([]*vcs.Commit, uint, error) { args := []string{"log", `--format=format:%H%x00%aN%x00%aE%x00%at%x00%cN%x00%cE%x00%ct%x00%B%x00%P%x00`} if opt.N != 0 { args = append(args, "-n", strconv.FormatUint(uint64(opt.N), 10)) } if opt.Skip != 0 { args = append(args, "--skip="+strconv.FormatUint(uint64(opt.Skip), 10)) } if opt.Path != "" { args = append(args, "--follow") } // Range rng := string(opt.Head) if opt.Base != "" { rng += "..." + string(opt.Base) } args = append(args, rng) if opt.Path != "" { args = append(args, "--", opt.Path) } cmd := exec.Command("git", args...) cmd.Dir = r.Dir out, err := cmd.CombinedOutput() if err != nil { out = bytes.TrimSpace(out) if isBadObjectErr(string(out), string(opt.Head)) { return nil, 0, vcs.ErrCommitNotFound } return nil, 0, fmt.Errorf("exec `git log` failed: %s. Output was:\n\n%s", err, out) } const partsPerCommit = 9 // number of \x00-separated fields per commit allParts := bytes.Split(out, []byte{'\x00'}) numCommits := len(allParts) / partsPerCommit commits := make([]*vcs.Commit, numCommits) for i := 0; i < numCommits; i++ { parts := allParts[partsPerCommit*i : partsPerCommit*(i+1)] // log outputs are newline separated, so all but the 1st commit ID part // has an erroneous leading newline. parts[0] = bytes.TrimPrefix(parts[0], []byte{'\n'}) authorTime, err := strconv.ParseInt(string(parts[3]), 10, 64) if err != nil { return nil, 0, fmt.Errorf("parsing git commit author time: %s", err) } committerTime, err := strconv.ParseInt(string(parts[6]), 10, 64) if err != nil { return nil, 0, fmt.Errorf("parsing git commit committer time: %s", err) } var parents []vcs.CommitID if parentPart := parts[8]; len(parentPart) > 0 { parentIDs := bytes.Split(parentPart, []byte{' '}) parents = make([]vcs.CommitID, len(parentIDs)) for i, id := range parentIDs { parents[i] = vcs.CommitID(id) } } commits[i] = &vcs.Commit{ ID: vcs.CommitID(parts[0]), Author: vcs.Signature{string(parts[1]), string(parts[2]), pbtypes.NewTimestamp(time.Unix(authorTime, 0))}, Committer: &vcs.Signature{string(parts[4]), string(parts[5]), pbtypes.NewTimestamp(time.Unix(committerTime, 0))}, Message: string(bytes.TrimSuffix(parts[7], []byte{'\n'})), Parents: parents, } } // Count commits. var total uint if !opt.NoTotal { cmd = exec.Command("git", "rev-list", "--count", rng) if opt.Path != "" { // This doesn't include --follow flag because rev-list doesn't support it, so the number may be slightly off. cmd.Args = append(cmd.Args, "--", opt.Path) } cmd.Dir = r.Dir out, err = cmd.CombinedOutput() if err != nil { return nil, 0, fmt.Errorf("exec `git rev-list --count` failed: %s. Output was:\n\n%s", err, out) } out = bytes.TrimSpace(out) total, err = parseUint(string(out)) if err != nil { return nil, 0, err } } return commits, total, nil }
func TestServeRepoTreeEntry_Dir(t *testing.T) { setupHandlerTest() defer teardownHandlerTest() repoPath := "a.b/c" rm := &mockFileSystem{ t: t, at: "abcd", fs: mapFS(map[string]string{"myfile": "mydata", "mydir/f": ""}), } sm := &mockServiceForExistingRepo{ t: t, repoPath: repoPath, repo: rm, } testHandler.Service = sm resp, err := http.Get(server.URL + testHandler.router.URLToRepoTreeEntry(repoPath, "abcd", ".").String()) if err != nil { t.Fatal(err) } defer resp.Body.Close() if got, want := resp.StatusCode, http.StatusOK; got != want { t.Errorf("got status code %d, want %d", got, want) } if !sm.opened { t.Errorf("!opened") } if !rm.called { t.Errorf("!called") } var e *vcsclient.TreeEntry if err := json.NewDecoder(resp.Body).Decode(&e); err != nil { t.Fatal(err) } wantEntry := &vcsclient.TreeEntry{ Name: ".", Type: vcsclient.DirEntry, ModTime: pbtypes.NewTimestamp(time.Time{}), Entries: []*vcsclient.TreeEntry{ { Name: "myfile", Type: vcsclient.FileEntry, Size: 6, ModTime: pbtypes.NewTimestamp(time.Time{}), }, { Name: "mydir", Type: vcsclient.DirEntry, ModTime: pbtypes.NewTimestamp(time.Time{}), }, }, } sort.Sort(vcsclient.TreeEntriesByTypeByName(e.Entries)) sort.Sort(vcsclient.TreeEntriesByTypeByName(wantEntry.Entries)) if !reflect.DeepEqual(e, wantEntry) { t.Errorf("got tree entry %+v, want %+v", e, wantEntry) } // used short commit ID, so should not be long-cached if cc := resp.Header.Get("cache-control"); cc != shortCacheControl { t.Errorf("got cache-control %q, want %q", cc, shortCacheControl) } }
func (r *Repository) BlameFile(path string, opt *vcs.BlameOptions) ([]*vcs.Hunk, error) { r.editLock.RLock() defer r.editLock.RUnlock() gopt := git2go.BlameOptions{} if opt != nil { var err error if opt.NewestCommit != "" { gopt.NewestCommit, err = git2go.NewOid(string(opt.NewestCommit)) if err != nil { return nil, err } } if opt.OldestCommit != "" { gopt.OldestCommit, err = git2go.NewOid(string(opt.OldestCommit)) if err != nil { return nil, err } } gopt.MinLine = uint32(opt.StartLine) gopt.MaxLine = uint32(opt.EndLine) } blame, err := r.u.BlameFile(path, &gopt) if err != nil { return nil, err } defer blame.Free() // Read file contents so we can set hunk byte start and end. fs, err := r.FileSystem(vcs.CommitID(gopt.NewestCommit.String())) if err != nil { return nil, err } b, err := fs.(*gitFSLibGit2).readFileBytes(path) if err != nil { return nil, err } lines := bytes.SplitAfter(b, []byte{'\n'}) byteOffset := 0 hunks := make([]*vcs.Hunk, blame.HunkCount()) for i := 0; i < len(hunks); i++ { hunk, err := blame.HunkByIndex(i) if err != nil { return nil, err } hunkBytes := 0 for j := uint16(0); j < hunk.LinesInHunk; j++ { hunkBytes += len(lines[j]) } endByteOffset := byteOffset + hunkBytes hunks[i] = &vcs.Hunk{ StartLine: int(hunk.FinalStartLineNumber), EndLine: int(hunk.FinalStartLineNumber + hunk.LinesInHunk), StartByte: byteOffset, EndByte: endByteOffset, CommitID: vcs.CommitID(hunk.FinalCommitId.String()), Author: vcs.Signature{ Name: hunk.FinalSignature.Name, Email: hunk.FinalSignature.Email, Date: pbtypes.NewTimestamp(hunk.FinalSignature.When.In(time.UTC)), }, } byteOffset = endByteOffset lines = lines[hunk.LinesInHunk:] } return hunks, nil }