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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"context"
"fmt"
"os/exec"
"go.chromium.org/luci/common/errors"
)
// fileStatus is a type of file change in a git commit.
// Doc: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt-git-diff-filesltpatterngt82308203
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: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt-git-diff-filesltpatterngt82308203
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,
"log",
"--format=format:%H",
"--raw",
"--numstat",
"-z",
"--reverse",
"--first-parent",
}
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() {
cancel()
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)
}