1// Copyright 2022 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package local 16 17// 18// Command line implementation of Git interface 19// 20 21import ( 22 "bufio" 23 "bytes" 24 "context" 25 "errors" 26 "fmt" 27 "strconv" 28 "strings" 29 "time" 30 31 "tools/treble/build/report/app" 32) 33 34// Separate out the executable to allow tests to override the results 35type gitExec interface { 36 ProjectInfo(ctx context.Context, gitDir, workDir string) (out *bytes.Buffer, err error) 37 RemoteUrl(ctx context.Context, gitDir, workDir, remote string) (*bytes.Buffer, error) 38 Tree(ctx context.Context, gitDir, workDir, revision string) (*bytes.Buffer, error) 39 CommitInfo(ctx context.Context, gitDir, workDir, revision string) (*bytes.Buffer, error) 40 DiffBranches(ctx context.Context, gitDir, workDir, upstream, sha string) (*bytes.Buffer, error) 41} 42 43type gitCli struct { 44 git gitExec // Git executable 45} 46 47// Create GIT project based on input parameters 48func (cli gitCli) Project(ctx context.Context, path, gitDir, remote, revision string) (*app.GitProject, error) { 49 workDir := path 50 // Set defaults 51 if remote == "" { 52 remote = "origin" 53 } 54 if gitDir == "" { 55 gitDir = ".git" 56 } 57 58 if raw, err := cli.git.ProjectInfo(ctx, gitDir, workDir); err == nil { 59 topLevel, projRevision, err := parseProjectInfo(raw) 60 if err == nil { 61 // Update work dir to use absolute path 62 workDir = topLevel 63 if revision == "" { 64 revision = projRevision 65 } 66 } 67 } 68 // Create project to use to run commands 69 out := &app.GitProject{ 70 RepoDir: path, 71 WorkDir: workDir, 72 GitDir: gitDir, 73 Remote: remote, 74 Revision: revision, 75 Files: make(map[string]*app.GitTreeObj)} 76 77 // Remote URL 78 if raw, err := cli.git.RemoteUrl(ctx, gitDir, workDir, remote); err == nil { 79 url, err := parseRemoteUrl(raw) 80 if err == nil { 81 out.RemoteUrl = url 82 } 83 } 84 85 return out, nil 86} 87 88// Get all files in the repository if, upstream branch is provided mark which files differ from upstream 89func (cli gitCli) PopulateFiles(ctx context.Context, proj *app.GitProject, upstream string) error { 90 if raw, err := cli.git.Tree(ctx, proj.GitDir, proj.WorkDir, proj.Revision); err == nil { 91 lsFiles, err := parseLsTree(raw) 92 if err == nil { 93 for _, file := range lsFiles { 94 proj.Files[file.Filename] = file 95 } 96 } 97 if upstream != "" { 98 99 if diff, err := cli.git.DiffBranches(ctx, proj.GitDir, proj.WorkDir, upstream, proj.Revision); err == nil { 100 if diffFiles, err := parseBranchDiff(diff); err == nil { 101 for f, d := range diffFiles { 102 if file, exists := proj.Files[f]; exists { 103 file.BranchDiff = d 104 } 105 } 106 } 107 } 108 109 } 110 } 111 return nil 112} 113 114// Get the commit information associated with the input sha 115func (cli gitCli) CommitInfo(ctx context.Context, proj *app.GitProject, sha string) (*app.GitCommit, error) { 116 if sha == "" { 117 sha = "HEAD" 118 } 119 raw, err := cli.git.CommitInfo(ctx, proj.GitDir, proj.WorkDir, sha) 120 121 if err != nil { 122 return nil, err 123 } 124 return parseCommitInfo(raw) 125} 126 127// parse rev-parse 128func parseProjectInfo(data *bytes.Buffer) (topLevel string, revision string, err error) { 129 s := bufio.NewScanner(data) 130 scanner := newLineScanner(2) 131 if err = scanner.Parse(s); err != nil { 132 return "", "", err 133 } 134 return scanner.Lines[0], scanner.Lines[1], nil 135 136} 137 138// parse remote get-url 139func parseRemoteUrl(data *bytes.Buffer) (url string, err error) { 140 s := bufio.NewScanner(data) 141 scanner := newLineScanner(1) 142 if err = scanner.Parse(s); err != nil { 143 return "", err 144 } 145 return scanner.Lines[0], nil 146 147} 148 149// parse ls-tree 150func parseLsTree(data *bytes.Buffer) ([]*app.GitTreeObj, error) { 151 out := []*app.GitTreeObj{} 152 s := bufio.NewScanner(data) 153 for s.Scan() { 154 obj := &app.GitTreeObj{} 155 // TODO 156 // Filename could contain a <space> as quotepath is turned off, truncating the name here 157 fmt.Sscanf(s.Text(), "%s %s %s %s", &obj.Permissions, &obj.Type, &obj.Sha, &obj.Filename) 158 out = append(out, obj) 159 } 160 return out, nil 161} 162 163// parse branch diff (diff --num-stat) 164func parseBranchDiff(data *bytes.Buffer) (map[string]*app.GitDiff, error) { 165 out := make(map[string]*app.GitDiff) 166 s := bufio.NewScanner(data) 167 for s.Scan() { 168 d := &app.GitDiff{} 169 var fname, added, deleted string 170 _, err := fmt.Sscanf(s.Text(), "%s %s %s", &added, &deleted, &fname) 171 if err == nil { 172 if added == "-" || deleted == "-" { 173 d.BinaryDiff = true 174 } else { 175 d.AddedLines, _ = strconv.Atoi(added) 176 d.DeletedLines, _ = strconv.Atoi(deleted) 177 } 178 } 179 out[fname] = d 180 } 181 return out, nil 182} 183 184// parse commit diff-tree 185func parseCommitInfo(data *bytes.Buffer) (*app.GitCommit, error) { 186 out := &app.GitCommit{Files: []app.GitCommitFile{}} 187 s := bufio.NewScanner(data) 188 first := true 189 for s.Scan() { 190 if first { 191 out.Sha = s.Text() 192 } else { 193 file := app.GitCommitFile{} 194 t := "" 195 fmt.Sscanf(s.Text(), "%s %s", &t, &file.Filename) 196 switch t { 197 case "M": 198 file.Type = app.GitFileModified 199 case "A": 200 file.Type = app.GitFileAdded 201 case "R": 202 file.Type = app.GitFileRemoved 203 } 204 out.Files = append(out.Files, file) 205 } 206 first = false 207 } 208 return out, nil 209} 210 211// Command line git 212type gitCmd struct { 213 cmd string // GIT executable 214 timeout time.Duration // Timeout for commands 215} 216 217// Run git command in working directory 218func (git *gitCmd) runDirCmd(ctx context.Context, gitDir string, workDir string, args []string) (*bytes.Buffer, error) { 219 gitArgs := append([]string{"--git-dir", gitDir, "-C", workDir}, args...) 220 out, err, _ := run(ctx, git.timeout, git.cmd, gitArgs) 221 if err != nil { 222 return nil, errors.New(fmt.Sprintf("Failed to run %s %s [error %s]", git.cmd, strings.Join(gitArgs, " "))) 223 } 224 return out, nil 225} 226 227func (git *gitCmd) ProjectInfo(ctx context.Context, gitDir, workDir string) (*bytes.Buffer, error) { 228 return git.runDirCmd(ctx, gitDir, workDir, []string{"rev-parse", "--show-toplevel", "HEAD"}) 229} 230func (git *gitCmd) RemoteUrl(ctx context.Context, gitDir, workDir, remote string) (*bytes.Buffer, error) { 231 return git.runDirCmd(ctx, gitDir, workDir, []string{"remote", "get-url", remote}) 232} 233func (git *gitCmd) Tree(ctx context.Context, gitDir, workDir, revision string) (*bytes.Buffer, error) { 234 cmdArgs := []string{"-c", "core.quotepath=off", "ls-tree", "--full-name", revision, "-r", "-t"} 235 return git.runDirCmd(ctx, gitDir, workDir, cmdArgs) 236} 237func (git *gitCmd) CommitInfo(ctx context.Context, gitDir, workDir, sha string) (*bytes.Buffer, error) { 238 cmdArgs := []string{"diff-tree", "-r", "-m", "--name-status", "--root", sha} 239 return git.runDirCmd(ctx, gitDir, workDir, cmdArgs) 240} 241func (git *gitCmd) DiffBranches(ctx context.Context, gitDir, workDir, upstream, sha string) (*bytes.Buffer, error) { 242 cmdArgs := []string{"diff", "--numstat", fmt.Sprintf("%s...%s", upstream, sha)} 243 return git.runDirCmd(ctx, gitDir, workDir, cmdArgs) 244} 245func NewGitCli() *gitCli { 246 cli := &gitCli{git: &gitCmd{cmd: "git", timeout: 100000 * time.Millisecond}} 247 return cli 248} 249