huangcm
2025-02-24 69ed55dec4b2116a19e4cca4393cbc014fce5fb2
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// Copyright 2018 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 vcs provides helper functions for working with various repositories (e.g. git).
package vcs
 
import (
   "bytes"
   "fmt"
   "io"
   "regexp"
   "strings"
   "time"
 
   "github.com/google/syzkaller/pkg/osutil"
)
 
type Repo interface {
   // Poll checkouts the specified repository/branch.
   // This involves fetching/resetting/cloning as necessary to recover from all possible problems.
   // Returns hash of the HEAD commit in the specified branch.
   Poll(repo, branch string) (*Commit, error)
 
   // CheckoutBranch checkouts the specified repository/branch.
   CheckoutBranch(repo, branch string) (*Commit, error)
 
   // CheckoutCommit checkouts the specified repository on the specified commit.
   CheckoutCommit(repo, commit string) (*Commit, error)
 
   // SwitchCommit checkouts the specified commit without fetching.
   SwitchCommit(commit string) (*Commit, error)
 
   // HeadCommit returns info about the HEAD commit of the current branch of git repository.
   HeadCommit() (*Commit, error)
 
   // ListRecentCommits returns list of recent commit titles starting from baseCommit.
   ListRecentCommits(baseCommit string) ([]string, error)
 
   // ExtractFixTagsFromCommits extracts fixing tags for bugs from git log.
   // Given email = "user@domain.com", it searches for tags of the form "user+tag@domain.com"
   // and return pairs {tag, commit title}.
   ExtractFixTagsFromCommits(baseCommit, email string) ([]FixCommit, error)
 
   // PreviousReleaseTags returns list of preceding release tags that are reachable from the given commit.
   PreviousReleaseTags(commit string) ([]string, error)
 
   // Bisect bisects good..bad commit range against the provided predicate (wrapper around git bisect).
   // The predicate should return an error only if there is no way to proceed
   // (it will abort the process), if possible it should prefer to return BisectSkip.
   // Progress of the process is streamed to the provided trace.
   // Returns the first commit on which the predicate returns BisectBad.
   Bisect(bad, good string, trace io.Writer, pred func() (BisectResult, error)) (*Commit, error)
}
 
type Commit struct {
   Hash   string
   Title  string
   Author string
   CC     []string
   Date   time.Time
}
 
type FixCommit struct {
   Tag   string
   Title string
}
 
type BisectResult int
 
const (
   BisectBad BisectResult = iota
   BisectGood
   BisectSkip
)
 
func NewRepo(os, vm, dir string) (Repo, error) {
   switch os {
   case "linux":
       return newGit(os, vm, dir), nil
   case "akaros":
       return newAkaros(vm, dir), nil
   case "fuchsia":
       return newFuchsia(vm, dir), nil
   }
   return nil, fmt.Errorf("vcs is unsupported for %v", os)
}
 
func NewSyzkallerRepo(dir string) Repo {
   return newGit("syzkaller", "", dir)
}
 
func Patch(dir string, patch []byte) error {
   // Do --dry-run first to not mess with partially consistent state.
   cmd := osutil.Command("patch", "-p1", "--force", "--ignore-whitespace", "--dry-run")
   if err := osutil.Sandbox(cmd, true, true); err != nil {
       return err
   }
   cmd.Stdin = bytes.NewReader(patch)
   cmd.Dir = dir
   if output, err := cmd.CombinedOutput(); err != nil {
       // If it reverses clean, then it's already applied
       // (seems to be the easiest way to detect it).
       cmd = osutil.Command("patch", "-p1", "--force", "--ignore-whitespace", "--reverse", "--dry-run")
       if err := osutil.Sandbox(cmd, true, true); err != nil {
           return err
       }
       cmd.Stdin = bytes.NewReader(patch)
       cmd.Dir = dir
       if _, err := cmd.CombinedOutput(); err == nil {
           return fmt.Errorf("patch is already applied")
       }
       return fmt.Errorf("failed to apply patch:\n%s", output)
   }
   // Now apply for real.
   cmd = osutil.Command("patch", "-p1", "--force", "--ignore-whitespace")
   if err := osutil.Sandbox(cmd, true, true); err != nil {
       return err
   }
   cmd.Stdin = bytes.NewReader(patch)
   cmd.Dir = dir
   if output, err := cmd.CombinedOutput(); err != nil {
       return fmt.Errorf("failed to apply patch after dry run:\n%s", output)
   }
   return nil
}
 
// CheckRepoAddress does a best-effort approximate check of a git repo address.
func CheckRepoAddress(repo string) bool {
   return gitRepoRe.MatchString(repo)
}
 
// CheckBranch does a best-effort approximate check of a git branch name.
func CheckBranch(branch string) bool {
   return gitBranchRe.MatchString(branch)
}
 
func CheckCommitHash(hash string) bool {
   if !gitHashRe.MatchString(hash) {
       return false
   }
   ln := len(hash)
   return ln == 8 || ln == 10 || ln == 12 || ln == 16 || ln == 20 || ln == 40
}
 
func runSandboxed(dir, command string, args ...string) ([]byte, error) {
   cmd := osutil.Command(command, args...)
   cmd.Dir = dir
   if err := osutil.Sandbox(cmd, true, false); err != nil {
       return nil, err
   }
   return osutil.Run(time.Hour, cmd)
}
 
var (
   // nolint: lll
   gitRepoRe    = regexp.MustCompile(`^(git|ssh|http|https|ftp|ftps)://[a-zA-Z0-9-_]+(\.[a-zA-Z0-9-_]+)+(:[0-9]+)?/[a-zA-Z0-9-_./]+\.git(/)?$`)
   gitBranchRe  = regexp.MustCompile("^[a-zA-Z0-9-_/.]{2,200}$")
   gitHashRe    = regexp.MustCompile("^[a-f0-9]+$")
   releaseTagRe = regexp.MustCompile(`^v([0-9]+).([0-9]+)(?:\.([0-9]+))?$`)
   ccRes        = []*regexp.Regexp{
       regexp.MustCompile(`^Reviewed\-.*: (.*)$`),
       regexp.MustCompile(`^[A-Za-z-]+\-and\-[Rr]eviewed\-.*: (.*)$`),
       regexp.MustCompile(`^Acked\-.*: (.*)$`),
       regexp.MustCompile(`^[A-Za-z-]+\-and\-[Aa]cked\-.*: (.*)$`),
       regexp.MustCompile(`^Tested\-.*: (.*)$`),
       regexp.MustCompile(`^[A-Za-z-]+\-and\-[Tt]ested\-.*: (.*)$`),
   }
)
 
// CanonicalizeCommit returns commit title that can be used when checking
// if a particular commit is present in a git tree.
// Some trees add prefixes to commit titles during backporting,
// so we want e.g. commit "foo bar" match "BACKPORT: foo bar".
func CanonicalizeCommit(title string) string {
   for _, prefix := range commitPrefixes {
       if strings.HasPrefix(title, prefix) {
           title = title[len(prefix):]
           break
       }
   }
   return strings.TrimSpace(title)
}
 
var commitPrefixes = []string{
   "UPSTREAM:",
   "CHROMIUM:",
   "FROMLIST:",
   "BACKPORT:",
   "FROMGIT:",
   "net-backports:",
}