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