blob: 54943f7524db1fe43068d8101a4b27c42ab2cba5 [file] [log] [blame]
// Copyright 2019 The LUCI Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
// fileStatus is a type of file change in a git commit.
// Doc:
type fileStatus byte
const (
added fileStatus = 'A'
deleted fileStatus = 'D'
modified fileStatus = 'M'
copied fileStatus = 'C'
renamed fileStatus = 'R'
typeChanged fileStatus = 'T'
unmerged fileStatus = 'U'
unknownChange fileStatus = 'X'
// fileChange is one file change in a git commit.
// Doc:
type fileChange struct {
Status fileStatus
// Meaning depends on Status.
// - copied and renamed: percentage of similarity between the source and
// target of the move or copy
// - modified: the percentage of dissimilarity
Score int
// Path to the "src" file.
Src Path
// Path to the "dst" file, i.e. the new path of the file.
Dst Path
// Number of added lines.
// For binary files, it is -1.
AddedLines int
// Number of deleted lines.
// For binary files, it is -1.
DeletedLines int
type commit struct {
Hash string
Files []*fileChange
// errStop returned by a callback indicates that the iteration must stop.
var errStop = fmt.Errorf("stop the iteration")
// readCommits calls fn for each commit in the git repository in the order
// from parents to children, which is roughly the chrological order.
// If fn returns errStop, readCommits stops and returns nil.
// If exclude is non-empty, commits reachable from exclude are excluded.
// This is similar to "exclude.." revision range in git-log.
// It follows only first parents.
// TODO(nodir): follow all parents and include all parents in commit.
func readCommits(ctx context.Context, dir, exclude string, fn func(c commit) error) (err error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
args := []string{
"-C", dir,
if exclude != "" {
args = append(args, exclude+"..")
cmd := exec.CommandContext(ctx, "git", args...)
stderr := &bytes.Buffer{}
cmd.Stderr = stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
if err := cmd.Start(); err != nil {
return errors.Annotate(err, "git failed to start").Err()
defer func() {
werr := cmd.Wait()
if err == nil && werr != nil {
err = errors.Reason("git log failed: %s. stderr: %s", werr, stderr).Err()
return newLogParser(stdout).ReadCommits(fn)