Home | History | Annotate | Download | only in vcs
      1 // Copyright 2017 syzkaller project authors. All rights reserved.
      2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
      3 
      4 package vcs
      5 
      6 import (
      7 	"bufio"
      8 	"bytes"
      9 	"fmt"
     10 	"io"
     11 	"net/mail"
     12 	"os"
     13 	"os/exec"
     14 	"sort"
     15 	"strconv"
     16 	"strings"
     17 	"time"
     18 
     19 	"github.com/google/syzkaller/pkg/osutil"
     20 )
     21 
     22 type git struct {
     23 	os  string
     24 	vm  string
     25 	dir string
     26 }
     27 
     28 func newGit(os, vm, dir string) *git {
     29 	return &git{
     30 		os:  os,
     31 		vm:  vm,
     32 		dir: dir,
     33 	}
     34 }
     35 
     36 func (git *git) Poll(repo, branch string) (*Commit, error) {
     37 	dir := git.dir
     38 	runSandboxed(dir, "git", "bisect", "reset")
     39 	runSandboxed(dir, "git", "reset", "--hard")
     40 	origin, err := runSandboxed(dir, "git", "remote", "get-url", "origin")
     41 	if err != nil || strings.TrimSpace(string(origin)) != repo {
     42 		// The repo is here, but it has wrong origin (e.g. repo in config has changed), re-clone.
     43 		if err := git.clone(repo, branch); err != nil {
     44 			return nil, err
     45 		}
     46 	}
     47 	// Use origin/branch for the case the branch was force-pushed,
     48 	// in such case branch is not the same is origin/branch and we will
     49 	// stuck with the local version forever (git checkout won't fail).
     50 	if _, err := runSandboxed(dir, "git", "checkout", "origin/"+branch); err != nil {
     51 		// No such branch (e.g. branch in config has changed), re-clone.
     52 		if err := git.clone(repo, branch); err != nil {
     53 			return nil, err
     54 		}
     55 	}
     56 	if _, err := runSandboxed(dir, "git", "fetch", "--no-tags"); err != nil {
     57 		// Something else is wrong, re-clone.
     58 		if err := git.clone(repo, branch); err != nil {
     59 			return nil, err
     60 		}
     61 	}
     62 	if _, err := runSandboxed(dir, "git", "checkout", "origin/"+branch); err != nil {
     63 		return nil, err
     64 	}
     65 	return git.HeadCommit()
     66 }
     67 
     68 func (git *git) CheckoutBranch(repo, branch string) (*Commit, error) {
     69 	dir := git.dir
     70 	runSandboxed(dir, "git", "bisect", "reset")
     71 	if _, err := runSandboxed(dir, "git", "reset", "--hard"); err != nil {
     72 		if err := git.initRepo(); err != nil {
     73 			return nil, err
     74 		}
     75 	}
     76 	_, err := runSandboxed(dir, "git", "fetch", repo, branch)
     77 	if err != nil {
     78 		return nil, err
     79 	}
     80 	if _, err := runSandboxed(dir, "git", "checkout", "FETCH_HEAD"); err != nil {
     81 		return nil, err
     82 	}
     83 	return git.HeadCommit()
     84 }
     85 
     86 func (git *git) CheckoutCommit(repo, commit string) (*Commit, error) {
     87 	dir := git.dir
     88 	runSandboxed(dir, "git", "bisect", "reset")
     89 	if _, err := runSandboxed(dir, "git", "reset", "--hard"); err != nil {
     90 		if err := git.initRepo(); err != nil {
     91 			return nil, err
     92 		}
     93 	}
     94 	_, err := runSandboxed(dir, "git", "fetch", repo)
     95 	if err != nil {
     96 		return nil, err
     97 	}
     98 	return git.SwitchCommit(commit)
     99 }
    100 
    101 func (git *git) SwitchCommit(commit string) (*Commit, error) {
    102 	dir := git.dir
    103 	if _, err := runSandboxed(dir, "git", "checkout", commit); err != nil {
    104 		return nil, err
    105 	}
    106 	return git.HeadCommit()
    107 }
    108 
    109 func (git *git) clone(repo, branch string) error {
    110 	if err := git.initRepo(); err != nil {
    111 		return err
    112 	}
    113 	if _, err := runSandboxed(git.dir, "git", "remote", "add", "origin", repo); err != nil {
    114 		return err
    115 	}
    116 	if _, err := runSandboxed(git.dir, "git", "fetch", "origin", branch); err != nil {
    117 		return err
    118 	}
    119 	return nil
    120 }
    121 
    122 func (git *git) initRepo() error {
    123 	if err := os.RemoveAll(git.dir); err != nil {
    124 		return fmt.Errorf("failed to remove repo dir: %v", err)
    125 	}
    126 	if err := osutil.MkdirAll(git.dir); err != nil {
    127 		return fmt.Errorf("failed to create repo dir: %v", err)
    128 	}
    129 	if err := osutil.SandboxChown(git.dir); err != nil {
    130 		return err
    131 	}
    132 	if _, err := runSandboxed(git.dir, "git", "init"); err != nil {
    133 		return err
    134 	}
    135 	return nil
    136 }
    137 
    138 func (git *git) HeadCommit() (*Commit, error) {
    139 	return git.getCommit("HEAD")
    140 }
    141 
    142 func (git *git) getCommit(commit string) (*Commit, error) {
    143 	output, err := runSandboxed(git.dir, "git", "log", "--format=%H%n%s%n%ae%n%ad%n%b", "-n", "1", commit)
    144 	if err != nil {
    145 		return nil, err
    146 	}
    147 	return gitParseCommit(output)
    148 }
    149 
    150 func gitParseCommit(output []byte) (*Commit, error) {
    151 	lines := bytes.Split(output, []byte{'\n'})
    152 	if len(lines) < 4 || len(lines[0]) != 40 {
    153 		return nil, fmt.Errorf("unexpected git log output: %q", output)
    154 	}
    155 	const dateFormat = "Mon Jan 2 15:04:05 2006 -0700"
    156 	date, err := time.Parse(dateFormat, string(lines[3]))
    157 	if err != nil {
    158 		return nil, fmt.Errorf("failed to parse date in git log output: %v\n%q", err, output)
    159 	}
    160 	cc := make(map[string]bool)
    161 	cc[strings.ToLower(string(lines[2]))] = true
    162 	for _, line := range lines[4:] {
    163 		for _, re := range ccRes {
    164 			matches := re.FindSubmatchIndex(line)
    165 			if matches == nil {
    166 				continue
    167 			}
    168 			addr, err := mail.ParseAddress(string(line[matches[2]:matches[3]]))
    169 			if err != nil {
    170 				break
    171 			}
    172 			cc[strings.ToLower(addr.Address)] = true
    173 			break
    174 		}
    175 	}
    176 	sortedCC := make([]string, 0, len(cc))
    177 	for addr := range cc {
    178 		sortedCC = append(sortedCC, addr)
    179 	}
    180 	sort.Strings(sortedCC)
    181 	com := &Commit{
    182 		Hash:   string(lines[0]),
    183 		Title:  string(lines[1]),
    184 		Author: string(lines[2]),
    185 		CC:     sortedCC,
    186 		Date:   date,
    187 	}
    188 	return com, nil
    189 }
    190 
    191 func (git *git) ListRecentCommits(baseCommit string) ([]string, error) {
    192 	// On upstream kernel this produces ~11MB of output.
    193 	// Somewhat inefficient to collect whole output in a slice
    194 	// and then convert to string, but should be bearable.
    195 	output, err := runSandboxed(git.dir, "git", "log",
    196 		"--pretty=format:%s", "--no-merges", "-n", "200000", baseCommit)
    197 	if err != nil {
    198 		return nil, err
    199 	}
    200 	return strings.Split(string(output), "\n"), nil
    201 }
    202 
    203 func (git *git) ExtractFixTagsFromCommits(baseCommit, email string) ([]FixCommit, error) {
    204 	since := time.Now().Add(-time.Hour * 24 * 365).Format("01-02-2006")
    205 	cmd := exec.Command("git", "log", "--no-merges", "--since", since, baseCommit)
    206 	cmd.Dir = git.dir
    207 	stdout, err := cmd.StdoutPipe()
    208 	if err != nil {
    209 		return nil, err
    210 	}
    211 	if err := cmd.Start(); err != nil {
    212 		return nil, err
    213 	}
    214 	defer cmd.Wait()
    215 	defer cmd.Process.Kill()
    216 	return gitExtractFixTags(stdout, email)
    217 }
    218 
    219 func gitExtractFixTags(r io.Reader, email string) ([]FixCommit, error) {
    220 	user, domain, err := splitEmail(email)
    221 	if err != nil {
    222 		return nil, fmt.Errorf("failed to parse email %q: %v", email, err)
    223 	}
    224 	var (
    225 		s           = bufio.NewScanner(r)
    226 		commits     []FixCommit
    227 		commitTitle = ""
    228 		commitStart = []byte("commit ")
    229 		bodyPrefix  = []byte("    ")
    230 		userBytes   = []byte(user + "+")
    231 		domainBytes = []byte(domain)
    232 	)
    233 	for s.Scan() {
    234 		ln := s.Bytes()
    235 		if bytes.HasPrefix(ln, commitStart) {
    236 			commitTitle = ""
    237 			continue
    238 		}
    239 		if !bytes.HasPrefix(ln, bodyPrefix) {
    240 			continue
    241 		}
    242 		ln = ln[len(bodyPrefix):]
    243 		if len(ln) == 0 {
    244 			continue
    245 		}
    246 		if commitTitle == "" {
    247 			commitTitle = string(ln)
    248 			continue
    249 		}
    250 		userPos := bytes.Index(ln, userBytes)
    251 		if userPos == -1 {
    252 			continue
    253 		}
    254 		domainPos := bytes.Index(ln[userPos+len(userBytes)+1:], domainBytes)
    255 		if domainPos == -1 {
    256 			continue
    257 		}
    258 		startPos := userPos + len(userBytes)
    259 		endPos := userPos + len(userBytes) + domainPos + 1
    260 		tag := string(ln[startPos:endPos])
    261 		commits = append(commits, FixCommit{tag, commitTitle})
    262 	}
    263 	return commits, s.Err()
    264 }
    265 
    266 func splitEmail(email string) (user, domain string, err error) {
    267 	addr, err := mail.ParseAddress(email)
    268 	if err != nil {
    269 		return "", "", err
    270 	}
    271 	at := strings.IndexByte(addr.Address, '@')
    272 	if at == -1 {
    273 		return "", "", fmt.Errorf("no @ in email address")
    274 	}
    275 	user = addr.Address[:at]
    276 	domain = addr.Address[at:]
    277 	if plus := strings.IndexByte(user, '+'); plus != -1 {
    278 		user = user[:plus]
    279 	}
    280 	return
    281 }
    282 
    283 func (git *git) Bisect(bad, good string, trace io.Writer, pred func() (BisectResult, error)) (*Commit, error) {
    284 	dir := git.dir
    285 	runSandboxed(dir, "git", "bisect", "reset")
    286 	runSandboxed(dir, "git", "reset", "--hard")
    287 	firstBad, err := git.getCommit(bad)
    288 	if err != nil {
    289 		return nil, err
    290 	}
    291 	output, err := runSandboxed(dir, "git", "bisect", "start", bad, good)
    292 	if err != nil {
    293 		return nil, err
    294 	}
    295 	defer runSandboxed(dir, "git", "bisect", "reset")
    296 	fmt.Fprintf(trace, "# git bisect start %v %v\n%s", bad, good, output)
    297 	current, err := git.HeadCommit()
    298 	if err != nil {
    299 		return nil, err
    300 	}
    301 	var bisectTerms = [...]string{
    302 		BisectBad:  "bad",
    303 		BisectGood: "good",
    304 		BisectSkip: "skip",
    305 	}
    306 	for {
    307 		res, err := pred()
    308 		if err != nil {
    309 			return nil, err
    310 		}
    311 		if res == BisectBad {
    312 			firstBad = current
    313 		}
    314 		output, err = runSandboxed(dir, "git", "bisect", bisectTerms[res])
    315 		if err != nil {
    316 			return nil, err
    317 		}
    318 		fmt.Fprintf(trace, "# git bisect %v %v\n%s", bisectTerms[res], current.Hash, output)
    319 		next, err := git.HeadCommit()
    320 		if err != nil {
    321 			return nil, err
    322 		}
    323 		if current.Hash == next.Hash {
    324 			return firstBad, nil
    325 		}
    326 		current = next
    327 	}
    328 }
    329 
    330 // Note: linux-specific.
    331 func (git *git) PreviousReleaseTags(commit string) ([]string, error) {
    332 	output, err := runSandboxed(git.dir, "git", "tag", "--no-contains", commit, "--merged", commit, "v*.*")
    333 	if err != nil {
    334 		return nil, err
    335 	}
    336 	return gitParseReleaseTags(output)
    337 }
    338 
    339 func gitParseReleaseTags(output []byte) ([]string, error) {
    340 	var tags []string
    341 	for _, tag := range bytes.Split(output, []byte{'\n'}) {
    342 		if releaseTagRe.Match(tag) && gitReleaseTagToInt(string(tag)) != 0 {
    343 			tags = append(tags, string(tag))
    344 		}
    345 	}
    346 	sort.Slice(tags, func(i, j int) bool {
    347 		return gitReleaseTagToInt(tags[i]) > gitReleaseTagToInt(tags[j])
    348 	})
    349 	return tags, nil
    350 }
    351 
    352 func gitReleaseTagToInt(tag string) uint64 {
    353 	matches := releaseTagRe.FindStringSubmatchIndex(tag)
    354 	v1, err := strconv.ParseUint(tag[matches[2]:matches[3]], 10, 64)
    355 	if err != nil {
    356 		return 0
    357 	}
    358 	v2, err := strconv.ParseUint(tag[matches[4]:matches[5]], 10, 64)
    359 	if err != nil {
    360 		return 0
    361 	}
    362 	var v3 uint64
    363 	if matches[6] != -1 {
    364 		v3, err = strconv.ParseUint(tag[matches[6]:matches[7]], 10, 64)
    365 		if err != nil {
    366 			return 0
    367 		}
    368 	}
    369 	return v1*1e6 + v2*1e3 + v3
    370 }
    371