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