| // Copyright 2022 The Tint Authors. | 
 | // | 
 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | // you may not use this file except in compliance with the License. | 
 | // You may obtain a copy of the License at | 
 | // | 
 | //     http://www.apache.org/licenses/LICENSE-2.0 | 
 | // | 
 | // Unless required by applicable law or agreed to in writing, software | 
 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | // See the License for the specific language governing permissions and | 
 | // limitations under the License. | 
 |  | 
 | // git-stats gathers statistics about changes made to a git repo. | 
 | package main | 
 |  | 
 | import ( | 
 | 	"flag" | 
 | 	"fmt" | 
 | 	"os" | 
 | 	"os/exec" | 
 | 	"regexp" | 
 | 	"runtime" | 
 | 	"sort" | 
 | 	"strings" | 
 | 	"sync" | 
 | 	"text/tabwriter" | 
 | 	"time" | 
 |  | 
 | 	"dawn.googlesource.com/dawn/tools/src/container" | 
 | 	"dawn.googlesource.com/dawn/tools/src/git" | 
 | ) | 
 |  | 
 | // Flags | 
 | var ( | 
 | 	repo       = flag.String("repo", ".", "path to git directory") | 
 | 	afterFlag  = flag.String("after", "", "start date") | 
 | 	beforeFlag = flag.String("before", "", "end date") | 
 | 	daysFlag   = flag.Int("days", 182, "interval in days (used if --after is not specified)") | 
 | ) | 
 |  | 
 | // main entry point | 
 | func main() { | 
 | 	flag.Parse() | 
 | 	if err := run(); err != nil { | 
 | 		fmt.Fprintln(os.Stderr, err) | 
 | 		os.Exit(1) | 
 | 	} | 
 | } | 
 |  | 
 | // Date format strings | 
 | const ( | 
 | 	yyyymmdd = "2006-01-02" | 
 | 	yyyymm   = "2006-01" | 
 | ) | 
 |  | 
 | // Returns true if the file with the given path should be included for addition / deletion stats. | 
 | func shouldConsiderLinesOfFile(path string) bool { | 
 | 	for _, ignore := range []string{ | 
 | 		"Doxyfile", | 
 | 		"package-lock.json", | 
 | 		"src/tint/builtin_table.inl", | 
 | 		"src/tint/resolver/intrinsic_table.inl", | 
 | 		"test/tint/", | 
 | 		"third_party/gn/webgpu-cts/test_list.txt", | 
 | 		"third_party/khronos/", | 
 | 		"webgpu-cts/", | 
 | 		"src/external/petamoriken", | 
 | 	} { | 
 | 		if strings.HasPrefix(path, ignore) { | 
 | 			return false | 
 | 		} | 
 | 	} | 
 | 	return true | 
 | } | 
 |  | 
 | // Returns true if the commit with the given hash should be included for addition / deletion stats. | 
 | func shouldConsiderLinesOfCommit(hash string) bool { | 
 | 	for _, ignore := range []string{ | 
 | 		"41e4d9a34c1d9dcb2eef3ff39ff9c1f987bfa02a", // Consistent formatting for Dawn/Tint. | 
 | 		"e87ac76f7ddf9237f3022cda90224bd0691fb318", // Merge tint -> dawn | 
 | 		"b0acbd436dbd499505a3fa8bf89e69231ec4d1e0", // Fix build/namespaces issues | 
 | 	} { | 
 | 		if hash == ignore { | 
 | 			return false | 
 | 		} | 
 | 	} | 
 | 	return true | 
 | } | 
 |  | 
 | // Regular expression used to parse the email from an author string. Example: | 
 | // Bob Bobson <bob@bobmail.com> | 
 | // ____________^^^^^^^^^^^^^^^_ | 
 | var reEmail = regexp.MustCompile(`<([^>]+)>`) | 
 |  | 
 | func run() error { | 
 | 	// Parse the --after and --before flags | 
 | 	var after, before time.Time | 
 | 	var err error | 
 | 	if *beforeFlag != "" { | 
 | 		before, err = time.Parse(yyyymmdd, *beforeFlag) | 
 | 		if err != nil { | 
 | 			return fmt.Errorf("Couldn't parse before date: %w", err) | 
 | 		} | 
 | 	} else { | 
 | 		before = time.Now() | 
 | 	} | 
 | 	if *afterFlag != "" { | 
 | 		after, err = time.Parse(yyyymmdd, *afterFlag) | 
 | 		if err != nil { | 
 | 			return fmt.Errorf("Couldn't parse after date: %w", err) | 
 | 		} | 
 | 	} else { | 
 | 		after = before.Add(-time.Hour * time.Duration(24**daysFlag)) | 
 | 	} | 
 |  | 
 | 	// Find 'git' | 
 | 	gitExe, err := exec.LookPath("git") | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	// Create the git.Git wrapper | 
 | 	g, err := git.New(gitExe) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	// Open the repo | 
 | 	r, err := g.Open(*repo) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	// Information obtained about a single commit | 
 | 	type CommitStat struct { | 
 | 		author     string | 
 | 		commit     *git.CommitInfo | 
 | 		insertions int | 
 | 		deletions  int | 
 | 		fileDeltas container.Map[string, int] | 
 | 	} | 
 |  | 
 | 	// Kick a goroutine to gather all the commits in the git log between | 
 | 	// 'after' and 'before', streaming the commits to the 'commits' chan. | 
 | 	// This chan will be closed by the goroutine when all commits have been | 
 | 	// gathered. | 
 | 	commits := make(chan git.CommitInfo, 256) | 
 | 	go func() { | 
 | 		log, err := r.LogBetween(after, before, &git.LogBetweenOptions{}) | 
 | 		if err != nil { | 
 | 			panic(fmt.Errorf("failed to gather commits: %w", err)) | 
 | 		} | 
 | 		for _, commit := range log { | 
 | 			commits <- commit | 
 | 		} | 
 | 		close(commits) | 
 | 	}() | 
 |  | 
 | 	// Kick 'numWorkers' goroutines to gather the commit statistics of the | 
 | 	// commits in the 'commits' chan, streaming the commit statistics to the | 
 | 	// 'commitStats' chan. | 
 | 	commitStats := make(chan CommitStat, 256) | 
 | 	numWorkers := runtime.NumCPU() | 
 | 	wg := sync.WaitGroup{} | 
 | 	wg.Add(numWorkers) | 
 | 	for worker := 0; worker < numWorkers; worker++ { | 
 | 		go func() { | 
 | 			defer wg.Done() | 
 | 			for commit := range commits { | 
 | 				commit := commit | 
 | 				email := reEmail.FindStringSubmatch(commit.Author)[1] | 
 | 				stats, err := r.Stats(commit, nil) | 
 | 				if err != nil { | 
 | 					panic(fmt.Errorf("failed to get stats for commit '%v': %w", commit.Hash, err)) | 
 | 				} | 
 |  | 
 | 				s := CommitStat{ | 
 | 					author:     email, | 
 | 					commit:     &commit, | 
 | 					fileDeltas: container.NewMap[string, int](), | 
 | 				} | 
 | 				if shouldConsiderLinesOfCommit(commit.Hash.String()) { | 
 | 					for file, stats := range stats { | 
 | 						if shouldConsiderLinesOfFile(file) { | 
 | 							s.insertions += stats.Insertions | 
 | 							s.deletions += stats.Deletions | 
 | 							s.fileDeltas[file] = stats.Insertions + stats.Deletions | 
 | 						} | 
 | 					} | 
 | 				} | 
 | 				commitStats <- s | 
 | 			} | 
 | 		}() | 
 | 	} | 
 |  | 
 | 	// Kick a helper goroutine that waits for all the goroutines that feed the | 
 | 	// 'commitStats' chan to complete, and then closes the 'commitStats' chan. | 
 | 	go func() { | 
 | 		wg.Wait() | 
 | 		close(commitStats) | 
 | 	}() | 
 |  | 
 | 	// CommitDelta holds the sum of line additions and deletions for a given | 
 | 	// commit. | 
 | 	type CommitDelta struct { | 
 | 		commit *git.CommitInfo | 
 | 		delta  int | 
 | 	} | 
 |  | 
 | 	// Stream in the commit statistics from the 'commitStats' chan, and collect | 
 | 	// statistics by author and by file. | 
 | 	statsByAuthor := container.NewMap[string, AuthorStats]() | 
 | 	fileDeltas := container.NewMap[string, int]() | 
 | 	commitDeltas := []CommitDelta{} | 
 | 	for cs := range commitStats { | 
 | 		as := statsByAuthor[cs.author] | 
 | 		as.insertions += cs.insertions | 
 | 		as.deletions += cs.deletions | 
 | 		as.commits++ | 
 | 		if as.commitsByMonth == nil { | 
 | 			as.commitsByMonth = container.NewMap[string, int]() | 
 | 		} | 
 | 		month := cs.commit.Date.Format(yyyymm) | 
 | 		as.commitsByMonth[month] = as.commitsByMonth[month] + 1 | 
 | 		statsByAuthor[cs.author] = as | 
 |  | 
 | 		commitDelta := 0 | 
 | 		for path, delta := range cs.fileDeltas { | 
 | 			fileDeltas[path] = fileDeltas[path] + delta | 
 | 			commitDelta += delta | 
 | 		} | 
 | 		commitDeltas = append(commitDeltas, CommitDelta{cs.commit, commitDelta}) | 
 | 	} | 
 |  | 
 | 	// Transform the 'statsByAuthor' map, so that authors that have statistics | 
 | 	// for both a @google.com and @chromium.org account have all their | 
 | 	// statistics merged into the @google.com account. | 
 | 	for google, g := range statsByAuthor { | 
 | 		if strings.HasSuffix(google, "@google.com") { | 
 | 			combined := strings.TrimSuffix(google, "@google.com") | 
 | 			chromium := combined + "@chromium.org" | 
 | 			if c, hasChromium := statsByAuthor[chromium]; hasChromium { | 
 | 				statsByAuthor[google] = combine(g, c) | 
 | 				delete(statsByAuthor, chromium) | 
 | 			} | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// Print those stats! | 
 |  | 
 | 	fmt.Printf("Between %v and %v:\n", after, before) | 
 |  | 
 | 	// Print the top 10 most modified files. | 
 | 	// This is helpful to identify files that are automatically generated, which | 
 | 	// we should exclude from the statistics. | 
 | 	{ | 
 | 		type FileDelta struct { | 
 | 			file  string | 
 | 			delta int | 
 | 		} | 
 | 		l := make([]FileDelta, 0, len(fileDeltas)) | 
 | 		for file, delta := range fileDeltas { | 
 | 			l = append(l, FileDelta{file, delta}) | 
 | 		} | 
 | 		sort.Slice(l, func(i, j int) bool { return l[i].delta > l[j].delta }) | 
 | 		n := len(l) | 
 | 		if n > 10 { | 
 | 			n = 10 | 
 | 		} | 
 | 		fmt.Println() | 
 | 		fmt.Printf("Top %v most modified files:\n", n) | 
 | 		fmt.Println() | 
 | 		tw := tabwriter.NewWriter(os.Stdout, 0, 0, 0, ' ', 0) | 
 | 		fmt.Fprintln(tw, "  delta\t | file") | 
 | 		for _, fd := range l[:n] { | 
 | 			fmt.Fprintln(tw, | 
 | 				" ", fd.delta, | 
 | 				"\t |", fd.file) | 
 | 		} | 
 | 		tw.Flush() | 
 | 	} | 
 |  | 
 | 	// Print the top 10 largest commits. | 
 | 	// This is helpful to identify commits that may contain a large bulk | 
 | 	// refactor, which we should exclude from the statistics. | 
 | 	{ | 
 | 		sort.Slice(commitDeltas, func(i, j int) bool { | 
 | 			return commitDeltas[i].delta > commitDeltas[j].delta | 
 | 		}) | 
 | 		n := len(commitDeltas) | 
 | 		if n > 10 { | 
 | 			n = 10 | 
 | 		} | 
 | 		fmt.Println() | 
 | 		fmt.Printf("Top %v largest commits:\n", n) | 
 | 		fmt.Println() | 
 | 		tw := tabwriter.NewWriter(os.Stdout, 0, 0, 0, ' ', 0) | 
 | 		fmt.Fprintln(tw, | 
 | 			"  delta\t | author\t | hash\t | description") | 
 | 		for _, fd := range commitDeltas[:n] { | 
 | 			fmt.Fprintln(tw, | 
 | 				" ", fd.delta, | 
 | 				"\t |", fd.commit.Author, | 
 | 				"\t |", fd.commit.Hash.String()[:6], | 
 | 				"\t |", fd.commit.Subject) | 
 | 		} | 
 | 		tw.Flush() | 
 | 	} | 
 |  | 
 | 	// Print the contributions by author. | 
 | 	{ | 
 | 		fmt.Println() | 
 | 		fmt.Println("Total contributions by author:") | 
 | 		tw := tabwriter.NewWriter(os.Stdout, 0, 0, 0, ' ', 0) | 
 | 		fmt.Println() | 
 | 		fmt.Fprintln(tw, "  author\t | commits\t | added\t | removed") | 
 | 		for _, author := range statsByAuthor.Keys() { | 
 | 			s := statsByAuthor[author] | 
 | 			fmt.Fprintln(tw, | 
 | 				"  "+author, | 
 | 				"\t |", s.commits, | 
 | 				"\t |", s.insertions, | 
 | 				"\t |", s.deletions) | 
 | 		} | 
 | 		tw.Flush() | 
 | 	} | 
 |  | 
 | 	// Print the per-author contributions by month. | 
 | 	{ | 
 | 		allMonths := container.NewSet[string]() | 
 | 		for _, author := range statsByAuthor { | 
 | 			for month := range author.commitsByMonth { | 
 | 				allMonths.Add(month) | 
 | 			} | 
 | 		} | 
 |  | 
 | 		months := allMonths.List() | 
 |  | 
 | 		fmt.Println() | 
 | 		fmt.Println("Commits by author by month:") | 
 | 		tw := tabwriter.NewWriter(os.Stdout, 0, 0, 0, ' ', 0) | 
 | 		fmt.Println() | 
 | 		fmt.Fprintf(tw, "  author") | 
 | 		for _, month := range months { | 
 | 			fmt.Fprint(tw, "\t | ", month) | 
 | 		} | 
 | 		fmt.Fprintln(tw) | 
 |  | 
 | 		for _, author := range statsByAuthor.Keys() { | 
 | 			fmt.Fprint(tw, "  ", author) | 
 | 			cbm := statsByAuthor[author].commitsByMonth | 
 | 			for _, month := range months { | 
 | 				fmt.Fprint(tw, "\t | ", cbm[month]) | 
 | 			} | 
 | 			fmt.Fprintln(tw) | 
 | 		} | 
 | 		tw.Flush() | 
 | 	} | 
 |  | 
 | 	return nil | 
 | } | 
 |  | 
 | type AuthorStats struct { | 
 | 	commits        int | 
 | 	commitsByMonth container.Map[string, int] | 
 | 	insertions     int | 
 | 	deletions      int | 
 | } | 
 |  | 
 | // combine returns a new AuthorStats, with the summed statistics of 'a' and 'b'. | 
 | func combine(a, b AuthorStats) AuthorStats { | 
 | 	out := AuthorStats{ | 
 | 		commits:    a.commits + b.commits, | 
 | 		insertions: a.insertions + b.insertions, | 
 | 		deletions:  a.deletions + b.deletions, | 
 | 	} | 
 | 	out.commitsByMonth = container.NewMap[string, int]() | 
 | 	for month, commits := range a.commitsByMonth { | 
 | 		out.commitsByMonth[month] = commits | 
 | 	} | 
 | 	for month, commits := range b.commitsByMonth { | 
 | 		out.commitsByMonth[month] = out.commitsByMonth[month] + commits | 
 | 	} | 
 | 	return out | 
 | } | 
 |  | 
 | func today() time.Time { | 
 | 	return time.Now() | 
 | } | 
 |  | 
 | func date(t time.Time) string { | 
 | 	return t.Format(yyyymmdd) | 
 | } |