aboutsummaryrefslogtreecommitdiffstats
path: root/syz-cluster/pkg/triage/commit.go
blob: 6d8644623c6950441a8469c867213fd0bbaa03b1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// Copyright 2024 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package triage

import (
	"time"

	"github.com/google/syzkaller/pkg/debugtracer"
	"github.com/google/syzkaller/pkg/vcs"
	"github.com/google/syzkaller/syz-cluster/pkg/api"
)

// TODO: Some further improvements:
//   1. Add support for experimental sessions: these may be way behind the current HEAD.

type TreeOps interface {
	HeadCommit(tree *api.Tree) (*vcs.Commit, error)
	ApplySeries(commit string, patches [][]byte) error
}

type CommitSelector struct {
	ops    TreeOps
	tracer debugtracer.DebugTracer
}

func NewCommitSelector(ops TreeOps, tracer debugtracer.DebugTracer) *CommitSelector {
	return &CommitSelector{ops: ops, tracer: tracer}
}

type SelectResult struct {
	Commit string
	Reason string // Set if Commit is empty.
}

const (
	reasonSeriesTooOld = "series lags behind the current HEAD too much"
	reasonNotApplies   = "series does not apply"
)

// Select returns the best matching commit hash.
func (cs *CommitSelector) Select(series *api.Series, tree *api.Tree, lastBuild *api.Build) (SelectResult, error) {
	head, err := cs.ops.HeadCommit(tree)
	if err != nil || head == nil {
		return SelectResult{}, err
	}
	cs.tracer.Log("current HEAD: %q (commit date: %v)", head.Hash, head.CommitDate)
	// If the series is already too old, it may be incompatible even if it applies cleanly.
	const seriesLagsBehind = time.Hour * 24 * 7
	if diff := head.CommitDate.Sub(series.PublishedAt); series.PublishedAt.Before(head.CommitDate) &&
		diff > seriesLagsBehind {
		cs.tracer.Log("the series is too old: %v before the HEAD", diff)
		return SelectResult{Reason: reasonSeriesTooOld}, nil
	}

	// Algorithm:
	// 1. If the last successful build is sufficiently new, prefer it over the last master.
	// We should it be renewing it regularly, so the commit should be quite up to date.
	// 2. If the last build is too old / the series does not apply, give a chance to the
	// current HEAD.

	var hashes []string
	if lastBuild != nil {
		// Check if the commit is still good enough.
		if diff := head.CommitDate.Sub(lastBuild.CommitDate); diff > seriesLagsBehind {
			cs.tracer.Log("the last successful build is already too old: %v, skipping", diff)
		} else {
			hashes = append(hashes, lastBuild.CommitHash)
		}
	}
	for _, hash := range append(hashes, head.Hash) {
		cs.tracer.Log("considering %q", hash)
		err := cs.ops.ApplySeries(hash, series.PatchBodies())
		if err == nil {
			cs.tracer.Log("series can be applied to %q", hash)
			return SelectResult{Commit: hash}, nil
		} else {
			cs.tracer.Log("failed to apply to %q: %v", hash, err)
		}
	}
	return SelectResult{Reason: reasonNotApplies}, nil
}

func FromBaseCommits(series *api.Series, baseCommits []*vcs.BaseCommit, trees []*api.Tree) (*api.Tree, string) {
	// Technically, any one of baseCommits could be a good match.
	// However, the developers have their own expectations regarding
	// what tree and what branch are actually preferred there.
	// So, among baseCommits, we still give preference to those that
	// align with the mailing lists Cc'd by the patch series.
	tree, commit := bestCommit(baseCommits, SelectTrees(series, trees))
	if tree != nil {
		return tree, commit
	}
	return bestCommit(baseCommits, trees)
}

func bestCommit(baseCommits []*vcs.BaseCommit, trees []*api.Tree) (*api.Tree, string) {
	retTreeIdx, retSameBranch, retCommit := -1, false, ""
	for _, commit := range baseCommits {
		for _, commitBranch := range commit.Branches {
			treeIdx, branch := FindTree(trees, commitBranch)
			if treeIdx < 0 {
				continue
			}
			sameBranch := branch == trees[treeIdx].Branch
			// If, for the same tree, we also have matched the branch, even better.
			if retTreeIdx < 0 || treeIdx < retTreeIdx ||
				treeIdx == retTreeIdx && !retSameBranch && sameBranch {
				retTreeIdx = treeIdx
				retSameBranch = sameBranch
				retCommit = commit.Hash
			}
		}
	}
	if retTreeIdx < 0 {
		return nil, ""
	}
	return trees[retTreeIdx], retCommit
}