• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2021 The Tint Authors.
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
15// roll-release is a tool to roll changes in Tint release branches into Dawn,
16// and create new Tint release branches.
17//
18// See showUsage() for more information
19package main
20
21import (
22	"encoding/hex"
23	"flag"
24	"fmt"
25	"io/ioutil"
26	"log"
27	"net/http"
28	"os"
29	"os/exec"
30	"path/filepath"
31	"regexp"
32	"sort"
33	"strconv"
34	"strings"
35
36	"dawn.googlesource.com/tint/tools/src/gerrit"
37	"github.com/go-git/go-git/v5"
38	"github.com/go-git/go-git/v5/config"
39	"github.com/go-git/go-git/v5/plumbing"
40	"github.com/go-git/go-git/v5/plumbing/transport"
41	git_http "github.com/go-git/go-git/v5/plumbing/transport/http"
42	"github.com/go-git/go-git/v5/storage/memory"
43)
44
45const (
46	toolName            = "roll-release"
47	gitCommitMsgHookURL = "https://gerrit-review.googlesource.com/tools/hooks/commit-msg"
48	tintURL             = "https://dawn.googlesource.com/tint"
49	dawnURL             = "https://dawn.googlesource.com/dawn"
50	tintSubdirInDawn    = "third_party/tint"
51	branchPrefix        = "chromium/"
52	branchLegacyCutoff  = 4590 // Branch numbers < than this are ignored
53)
54
55type branches = map[string]plumbing.Hash
56
57func main() {
58	if err := run(); err != nil {
59		fmt.Println(err)
60		os.Exit(1)
61	}
62}
63
64func showUsage() {
65	fmt.Printf(`
66%[1]v is a tool to synchronize Dawn's release branches with Tint.
67
68%[1]v will scan the release branches of both Dawn and Tint, and will:
69* Create new Gerrit changes to roll new release branch changes from Tint into
70  Dawn.
71* Find and create missing Tint release branches, using the git hash of Tint in
72  the DEPS file of the Dawn release branch.
73
74%[1]v does not depend on the current state of the Tint checkout, nor will it
75make any changes to the local checkout.
76
77usage:
78  %[1]v
79`, toolName)
80	flag.PrintDefaults()
81	fmt.Println(``)
82	os.Exit(1)
83}
84
85func run() error {
86	dry := false
87	flag.BoolVar(&dry, "dry", false, "perform a dry run")
88	flag.Usage = showUsage
89	flag.Parse()
90
91	// This tool uses a mix of 'go-git' and the command line git.
92	// go-git has the benefit of keeping the git information entirely in-memory,
93	// but has issues working with chromiums tools and gerrit.
94	// To create new release branches in Tint, we use 'go-git', so we need to
95	// dig out the username and password.
96	var auth transport.AuthMethod
97	if user, pass := gerrit.LoadCredentials(); user != "" {
98		auth = &git_http.BasicAuth{Username: user, Password: pass}
99	} else {
100		return fmt.Errorf("failed to fetch git credentials")
101	}
102
103	// Using in-memory repos, find all the tint and dawn release branches
104	log.Println("Inspecting dawn and tint release branches...")
105	var tint, dawn *git.Repository
106	var tintBranches, dawnBranches branches
107	for _, r := range []struct {
108		name     string
109		url      string
110		repo     **git.Repository
111		branches *branches
112	}{
113		{"tint", tintURL, &tint, &tintBranches},
114		{"dawn", dawnURL, &dawn, &dawnBranches},
115	} {
116		repo, err := git.Init(memory.NewStorage(), nil)
117		if err != nil {
118			return fmt.Errorf("failed to create %v in-memory repo: %w", r.name, err)
119		}
120		remote, err := repo.CreateRemote(&config.RemoteConfig{
121			Name: "origin",
122			URLs: []string{r.url},
123		})
124		if err != nil {
125			return fmt.Errorf("failed to add %v remote: %w", r.name, err)
126		}
127		refs, err := remote.List(&git.ListOptions{})
128		if err != nil {
129			return fmt.Errorf("failed to fetch %v branches: %w", r.name, err)
130		}
131		branches := branches{}
132		for _, ref := range refs {
133			if !ref.Name().IsBranch() {
134				continue
135			}
136			name := ref.Name().Short()
137			if strings.HasPrefix(name, branchPrefix) {
138				branches[name] = ref.Hash()
139			}
140		}
141		*r.repo = repo
142		*r.branches = branches
143	}
144
145	// Find the release branches found in dawn, which are missing in tint.
146	// Find the release branches in dawn that are behind HEAD of the
147	// corresponding branch in tint.
148	log.Println("Scanning dawn DEPS...")
149	type roll struct {
150		from, to plumbing.Hash
151	}
152	tintBranchesToCreate := branches{}      // branch name -> tint hash
153	dawnBranchesToRoll := map[string]roll{} // branch name -> roll
154	for name := range dawnBranches {
155		if isBranchBefore(name, branchLegacyCutoff) {
156			continue // Branch is earlier than we're interested in
157		}
158		deps, err := getDEPS(dawn, name)
159		if err != nil {
160			return err
161		}
162		depsTintHash, err := parseTintFromDEPS(deps)
163		if err != nil {
164			return err
165		}
166
167		if tintBranchHash, found := tintBranches[name]; found {
168			if tintBranchHash != depsTintHash {
169				dawnBranchesToRoll[name] = roll{from: depsTintHash, to: tintBranchHash}
170			}
171		} else {
172			tintBranchesToCreate[name] = depsTintHash
173		}
174	}
175
176	if dry {
177		tasks := []string{}
178		for name, sha := range tintBranchesToCreate {
179			tasks = append(tasks, fmt.Sprintf("Create Tint release branch '%v' @ %v", name, sha))
180		}
181		for name, roll := range dawnBranchesToRoll {
182			tasks = append(tasks, fmt.Sprintf("Roll Dawn release branch '%v' from %v to %v", name, roll.from, roll.to))
183		}
184		sort.Strings(tasks)
185		fmt.Printf("%v was run with --dry. Run without --dry to:\n", toolName)
186		for _, task := range tasks {
187			fmt.Println(" >", task)
188		}
189		return nil
190	}
191
192	didSomething := false
193	if n := len(tintBranchesToCreate); n > 0 {
194		log.Println("Creating", n, "release branches in tint...")
195
196		// In order to create the branches, we need to know what the DEPS
197		// hashes are referring to. Perform an in-memory fetch of tint's main
198		// branch.
199		if _, err := fetch(tint, "main"); err != nil {
200			return err
201		}
202
203		for name, sha := range tintBranchesToCreate {
204			log.Println("Creating branch", name, "@", sha, "...")
205
206			// Pushing a branch by SHA does not work, so we need to create a
207			// local branch first. See https://github.com/go-git/go-git/issues/105
208			src := plumbing.NewHashReference(plumbing.NewBranchReferenceName(name), sha)
209			if err := tint.Storer.SetReference(src); err != nil {
210				return fmt.Errorf("failed to create temporary branch: %w", err)
211			}
212
213			dst := plumbing.NewBranchReferenceName(name)
214			refspec := config.RefSpec(src.Name() + ":" + dst)
215			err := tint.Push(&git.PushOptions{
216				RefSpecs: []config.RefSpec{refspec},
217				Progress: os.Stdout,
218				Auth:     auth,
219			})
220			if err != nil && err != git.NoErrAlreadyUpToDate {
221				return fmt.Errorf("failed to push branch: %w", err)
222			}
223		}
224		didSomething = true
225	}
226
227	if n := len(dawnBranchesToRoll); n > 0 {
228		log.Println("Rolling", n, "release branches in dawn...")
229
230		// Fetch the change-id hook script
231		commitMsgHookResp, err := http.Get(gitCommitMsgHookURL)
232		if err != nil {
233			return fmt.Errorf("failed to fetch the git commit message hook from '%v': %w", gitCommitMsgHookURL, err)
234		}
235		commitMsgHook, err := ioutil.ReadAll(commitMsgHookResp.Body)
236		if err != nil {
237			return fmt.Errorf("failed to fetch the git commit message hook from '%v': %w", gitCommitMsgHookURL, err)
238		}
239
240		for name, roll := range dawnBranchesToRoll {
241			log.Println("Rolling branch", name, "from tint", roll.from, "to", roll.to, "...")
242			dir, err := ioutil.TempDir("", "dawn-roll")
243			if err != nil {
244				return err
245			}
246			defer os.RemoveAll(dir)
247
248			// Clone dawn into dir
249			if err := call(dir, "git", "clone", "--depth", "1", "-b", name, dawnURL, "."); err != nil {
250				return fmt.Errorf("failed to clone dawn branch %v: %w", name, err)
251			}
252
253			// Copy the Change-Id hook into the dawn directory
254			gitHooksDir := filepath.Join(dir, ".git", "hooks")
255			if err := os.MkdirAll(gitHooksDir, 0777); err != nil {
256				return fmt.Errorf("failed create commit hooks directory: %w", err)
257			}
258			if err := ioutil.WriteFile(filepath.Join(gitHooksDir, "commit-msg"), commitMsgHook, 0777); err != nil {
259				return fmt.Errorf("failed install commit message hook: %w", err)
260			}
261
262			// Clone tint into third_party directory of dawn
263			tintDir := filepath.Join(dir, tintSubdirInDawn)
264			if err := os.MkdirAll(tintDir, 0777); err != nil {
265				return fmt.Errorf("failed to create directory %v: %w", tintDir, err)
266			}
267			if err := call(tintDir, "git", "clone", "-b", name, tintURL, "."); err != nil {
268				return fmt.Errorf("failed to clone tint hash %v: %w", roll.from, err)
269			}
270
271			// Checkout tint at roll.from
272			if err := call(tintDir, "git", "checkout", roll.from); err != nil {
273				return fmt.Errorf("failed to checkout tint at %v: %w", roll.from, err)
274			}
275
276			// Use roll-dep to roll tint to roll.to
277			if err := call(dir, "roll-dep", "--ignore-dirty-tree", fmt.Sprintf("--roll-to=%s", roll.to), tintSubdirInDawn); err != nil {
278				return err
279			}
280
281			// Push the change to gerrit
282			if err := call(dir, "git", "push", "origin", "HEAD:refs/for/"+name); err != nil {
283				return fmt.Errorf("failed to push roll to gerrit: %w", err)
284			}
285		}
286		didSomething = true
287	}
288
289	if !didSomething {
290		log.Println("Everything up to date")
291	} else {
292		log.Println("Done")
293	}
294	return nil
295}
296
297// returns true if the branch name contains a branch number less than 'version'
298func isBranchBefore(name string, version int) bool {
299	n, err := strconv.Atoi(strings.TrimPrefix(name, branchPrefix))
300	if err != nil {
301		return false
302	}
303	return n < version
304}
305
306// call invokes the executable 'exe' with the given arguments in the working
307// directory 'dir'.
308func call(dir, exe string, args ...interface{}) error {
309	s := make([]string, len(args))
310	for i, a := range args {
311		s[i] = fmt.Sprint(a)
312	}
313	cmd := exec.Command(exe, s...)
314	cmd.Dir = dir
315	cmd.Stdout = os.Stdout
316	cmd.Stderr = os.Stderr
317	if err := cmd.Run(); err != nil {
318		return fmt.Errorf("%v returned %v", cmd, err)
319	}
320	return nil
321}
322
323// getDEPS returns the content of the DEPS file for the given branch.
324func getDEPS(r *git.Repository, branch string) (string, error) {
325	hash, err := fetch(r, branch)
326	if err != nil {
327		return "", err
328	}
329	commit, err := r.CommitObject(hash)
330	if err != nil {
331		return "", fmt.Errorf("failed to fetch commit: %w", err)
332	}
333	tree, err := commit.Tree()
334	if err != nil {
335		return "", fmt.Errorf("failed to fetch tree: %w", err)
336	}
337	deps, err := tree.File("DEPS")
338	if err != nil {
339		return "", fmt.Errorf("failed to find DEPS: %w", err)
340	}
341	return deps.Contents()
342}
343
344// fetch performs a git-fetch of the given branch into 'r', returning the
345// fetched branch's hash.
346func fetch(r *git.Repository, branch string) (plumbing.Hash, error) {
347	src := plumbing.NewBranchReferenceName(branch)
348	dst := plumbing.NewRemoteReferenceName("origin", branch)
349	err := r.Fetch(&git.FetchOptions{
350		RefSpecs: []config.RefSpec{config.RefSpec("+" + src + ":" + dst)},
351	})
352	if err != nil {
353		return plumbing.Hash{}, fmt.Errorf("failed to fetch branch %v: %w", branch, err)
354	}
355	ref, err := r.Reference(plumbing.ReferenceName(dst), true)
356	if err != nil {
357		return plumbing.Hash{}, fmt.Errorf("failed to resolve branch %v: %w", branch, err)
358	}
359	return ref.Hash(), nil
360}
361
362var reDEPSTintVersion = regexp.MustCompile("tint@([0-9a-fA-F]*)")
363
364// parseTintFromDEPS returns the tint hash from the DEPS file content 'deps'
365func parseTintFromDEPS(deps string) (plumbing.Hash, error) {
366	m := reDEPSTintVersion.FindStringSubmatch(deps)
367	if len(m) != 2 {
368		return plumbing.Hash{}, fmt.Errorf("failed to find tint hash in DEPS")
369	}
370	b, err := hex.DecodeString(m[1])
371	if err != nil {
372		return plumbing.Hash{}, fmt.Errorf("failed to find parse tint hash in DEPS: %w", err)
373	}
374	var h plumbing.Hash
375	copy(h[:], b)
376	return h, nil
377}
378