• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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