// Copyright 2022 The Android Open Source Project // // 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 local // // Command line implementation of Git interface // import ( "bufio" "bytes" "context" "errors" "fmt" "strconv" "strings" "time" "tools/treble/build/report/app" ) // Separate out the executable to allow tests to override the results type gitExec interface { ProjectInfo(ctx context.Context, gitDir, workDir string) (out *bytes.Buffer, err error) RemoteUrl(ctx context.Context, gitDir, workDir, remote string) (*bytes.Buffer, error) Tree(ctx context.Context, gitDir, workDir, revision string) (*bytes.Buffer, error) CommitInfo(ctx context.Context, gitDir, workDir, revision string) (*bytes.Buffer, error) DiffBranches(ctx context.Context, gitDir, workDir, upstream, sha string) (*bytes.Buffer, error) } type gitCli struct { git gitExec // Git executable } // Create GIT project based on input parameters func (cli gitCli) Project(ctx context.Context, path, gitDir, remote, revision string) (*app.GitProject, error) { workDir := path // Set defaults if remote == "" { remote = "origin" } if gitDir == "" { gitDir = ".git" } if raw, err := cli.git.ProjectInfo(ctx, gitDir, workDir); err == nil { topLevel, projRevision, err := parseProjectInfo(raw) if err == nil { // Update work dir to use absolute path workDir = topLevel if revision == "" { revision = projRevision } } } // Create project to use to run commands out := &app.GitProject{ RepoDir: path, WorkDir: workDir, GitDir: gitDir, Remote: remote, Revision: revision, Files: make(map[string]*app.GitTreeObj)} // Remote URL if raw, err := cli.git.RemoteUrl(ctx, gitDir, workDir, remote); err == nil { url, err := parseRemoteUrl(raw) if err == nil { out.RemoteUrl = url } } return out, nil } // Get all files in the repository if, upstream branch is provided mark which files differ from upstream func (cli gitCli) PopulateFiles(ctx context.Context, proj *app.GitProject, upstream string) error { if raw, err := cli.git.Tree(ctx, proj.GitDir, proj.WorkDir, proj.Revision); err == nil { lsFiles, err := parseLsTree(raw) if err == nil { for _, file := range lsFiles { proj.Files[file.Filename] = file } } if upstream != "" { if diff, err := cli.git.DiffBranches(ctx, proj.GitDir, proj.WorkDir, upstream, proj.Revision); err == nil { if diffFiles, err := parseBranchDiff(diff); err == nil { for f, d := range diffFiles { if file, exists := proj.Files[f]; exists { file.BranchDiff = d } } } } } } return nil } // Get the commit information associated with the input sha func (cli gitCli) CommitInfo(ctx context.Context, proj *app.GitProject, sha string) (*app.GitCommit, error) { if sha == "" { sha = "HEAD" } raw, err := cli.git.CommitInfo(ctx, proj.GitDir, proj.WorkDir, sha) if err != nil { return nil, err } return parseCommitInfo(raw) } // parse rev-parse func parseProjectInfo(data *bytes.Buffer) (topLevel string, revision string, err error) { s := bufio.NewScanner(data) scanner := newLineScanner(2) if err = scanner.Parse(s); err != nil { return "", "", err } return scanner.Lines[0], scanner.Lines[1], nil } // parse remote get-url func parseRemoteUrl(data *bytes.Buffer) (url string, err error) { s := bufio.NewScanner(data) scanner := newLineScanner(1) if err = scanner.Parse(s); err != nil { return "", err } return scanner.Lines[0], nil } // parse ls-tree func parseLsTree(data *bytes.Buffer) ([]*app.GitTreeObj, error) { out := []*app.GitTreeObj{} s := bufio.NewScanner(data) for s.Scan() { obj := &app.GitTreeObj{} // TODO // Filename could contain a as quotepath is turned off, truncating the name here fmt.Sscanf(s.Text(), "%s %s %s %s", &obj.Permissions, &obj.Type, &obj.Sha, &obj.Filename) out = append(out, obj) } return out, nil } // parse branch diff (diff --num-stat) func parseBranchDiff(data *bytes.Buffer) (map[string]*app.GitDiff, error) { out := make(map[string]*app.GitDiff) s := bufio.NewScanner(data) for s.Scan() { d := &app.GitDiff{} var fname, added, deleted string _, err := fmt.Sscanf(s.Text(), "%s %s %s", &added, &deleted, &fname) if err == nil { if added == "-" || deleted == "-" { d.BinaryDiff = true } else { d.AddedLines, _ = strconv.Atoi(added) d.DeletedLines, _ = strconv.Atoi(deleted) } } out[fname] = d } return out, nil } // parse commit diff-tree func parseCommitInfo(data *bytes.Buffer) (*app.GitCommit, error) { out := &app.GitCommit{Files: []app.GitCommitFile{}} s := bufio.NewScanner(data) first := true for s.Scan() { if first { out.Sha = s.Text() } else { file := app.GitCommitFile{} t := "" fmt.Sscanf(s.Text(), "%s %s", &t, &file.Filename) switch t { case "M": file.Type = app.GitFileModified case "A": file.Type = app.GitFileAdded case "R": file.Type = app.GitFileRemoved } out.Files = append(out.Files, file) } first = false } return out, nil } // Command line git type gitCmd struct { cmd string // GIT executable timeout time.Duration // Timeout for commands } // Run git command in working directory func (git *gitCmd) runDirCmd(ctx context.Context, gitDir string, workDir string, args []string) (*bytes.Buffer, error) { gitArgs := append([]string{"--git-dir", gitDir, "-C", workDir}, args...) out, err, _ := run(ctx, git.timeout, git.cmd, gitArgs) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to run %s %s [error %s]", git.cmd, strings.Join(gitArgs, " "))) } return out, nil } func (git *gitCmd) ProjectInfo(ctx context.Context, gitDir, workDir string) (*bytes.Buffer, error) { return git.runDirCmd(ctx, gitDir, workDir, []string{"rev-parse", "--show-toplevel", "HEAD"}) } func (git *gitCmd) RemoteUrl(ctx context.Context, gitDir, workDir, remote string) (*bytes.Buffer, error) { return git.runDirCmd(ctx, gitDir, workDir, []string{"remote", "get-url", remote}) } func (git *gitCmd) Tree(ctx context.Context, gitDir, workDir, revision string) (*bytes.Buffer, error) { cmdArgs := []string{"-c", "core.quotepath=off", "ls-tree", "--full-name", revision, "-r", "-t"} return git.runDirCmd(ctx, gitDir, workDir, cmdArgs) } func (git *gitCmd) CommitInfo(ctx context.Context, gitDir, workDir, sha string) (*bytes.Buffer, error) { cmdArgs := []string{"diff-tree", "-r", "-m", "--name-status", "--root", sha} return git.runDirCmd(ctx, gitDir, workDir, cmdArgs) } func (git *gitCmd) DiffBranches(ctx context.Context, gitDir, workDir, upstream, sha string) (*bytes.Buffer, error) { cmdArgs := []string{"diff", "--numstat", fmt.Sprintf("%s...%s", upstream, sha)} return git.runDirCmd(ctx, gitDir, workDir, cmdArgs) } func NewGitCli() *gitCli { cli := &gitCli{git: &gitCmd{cmd: "git", timeout: 100000 * time.Millisecond}} return cli }