예제 #1
0
// 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
}
예제 #2
0
파일: repo.go 프로젝트: shazow/go-vcs
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
}
예제 #3
0
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)
	}
}
예제 #4
0
파일: repo.go 프로젝트: shazow/go-vcs
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,
	}
}
예제 #5
0
파일: repo.go 프로젝트: emil2k/go-vcs
// 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,
	}
}
예제 #6
0
파일: fs.go 프로젝트: alexsaveliev/vcsstore
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
}
예제 #7
0
파일: repo.go 프로젝트: alexsaveliev/go-vcs
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
}
예제 #8
0
파일: repo.go 프로젝트: alexsaveliev/go-vcs
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
}
예제 #9
0
파일: repo.go 프로젝트: emil2k/go-vcs
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
}
예제 #10
0
파일: repo.go 프로젝트: emil2k/go-vcs
// 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
}
예제 #11
0
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)
	}
}
예제 #12
0
파일: repo.go 프로젝트: shazow/go-vcs
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
}