// scan given a path crawls it and its subfolders // searching for Git repositories func scan(path string) { print("scan") }
// stats generates a nice graph of your Git contributions func stats(email string) { print("stats") }
func main() { var folder string var email string flag.StringVar(&folder, "add", "", "add a new folder to scan for Git repositories") flag.StringVar(&email, "email", "[[email protected]](/cdn-cgi/l/email-protection)", "the email to scan") flag.Parse()
// scanGitFolders returns a list of subfolders of `folder` ending with `.git`. // Returns the base folder of the repo, the .git folder parent. // Recursively searches in the subfolders by passing an existing `folders` slice. func scanGitFolders(folders []string, folder string) []string { // trim the last `/` folder = strings.TrimSuffix(folder, "/")
// recursiveScanFolder starts the recursive search of git repositories // living in the `folder` subtree func recursiveScanFolder(folder string) []string { return scanGitFolders(make([]string, 0), folder) }
工作流的第 2 部分是獲取包含存儲庫路徑數據庫的點文件的路徑:
1 2 3 4 5 6 7 8 9 10 11 12
// getDotFilePath returns the dot file for the repos list. // Creates it and the enclosing folder if it does not exist. func getDotFilePath() string { usr, err := user.Current() if err != nil { log.Fatal(err) }
// User represents a user account. type User struct { // Uid is the user ID. // On POSIX systems, this is a decimal number representing the uid. // On Windows, this is a security identifier (SID) in a string format. // On Plan 9, this is the contents of /dev/user. Uid string // Gid is the primary group ID. // On POSIX systems, this is a decimal number representing the gid. // On Windows, this is a SID in a string format. // On Plan 9, this is the contents of /dev/user. Gid string // Username is the login name. Username string // Name is the user's real or display name. // It might be blank. // On POSIX systems, this is the first (or only) entry in the GECOS field // list. // On Windows, this is the user's display name. // On Plan 9, this is the contents of /dev/user. Name string // HomeDir is the path to the user's home directory (if they have one). HomeDir string }
// addNewSliceElementsToFile given a slice of strings representing paths, stores them // to the filesystem // addNewSliceElementsToFile given a slice of strings representing paths, stores them // to the filesystem func addNewSliceElementsToFile(filePath string, newRepos []string) { existingRepos := parseFileLinesToSlice(filePath) repos := joinSlices(newRepos, existingRepos) dumpStringsSliceToFile(repos, filePath) }
// parseFileLinesToSlice given a file path string, gets the content // of each line and parses it to a slice of strings. func parseFileLinesToSlice(filePath string) []string { f := openFile(filePath) defer f.Close()
var lines []string scanner := bufio.NewScanner(f) for scanner.Scan() { lines = append(lines, scanner.Text()) } if err := scanner.Err(); err != nil { if err != io.EOF { panic(err) } }
return lines }
它調用 openFile(),根據文件路徑字符串打開文件並返回它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// openFile opens the file located at `filePath`. Creates it if not existing. func openFile(filePath string) \*os.File { f, err := os.OpenFile(filePath, os.O\_APPEND|os.O\_WRONLY, 0755) if err != nil { if os.IsNotExist(err) { // file does not exist \_, err = os.Create(filePath) if err != nil { panic(err) } } else { // other error panic(err) } }
// joinSlices adds the element of the `new` slice // into the `existing` slice, only if not already there func joinSlices(new []string, existing []string) []string { for \_, i := range new { if !sliceContains(existing, i) { existing = append(existing, i) } } return existing }
// sliceContains returns true if `slice` contains `value` func sliceContains(slice []string, value string) bool { for \_, v := range slice { if v == value { return true } } return false }
// stats calculates and prints the stats. func stats(email string) { commits := processRepositories(email) printCommitsStats(commits) }
1.獲取提交列表 2.根據提交生成圖表
看起來很簡單。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// processRepositories given a user email, returns the // commits made in the last 6 months func processRepositories(email string) map[int]int { filePath := getDotFilePath() repos := parseFileLinesToSlice(filePath) daysInMap := daysInLastSixMonths
commits := make(map[int]int, daysInMap) for i := daysInMap; i > 0; i-- { commits[i] = 0 }
for \_, path := range repos { commits = fillCommits(email, path, commits) }
// fillCommits given a repository found in `path`, gets the commits and // puts them in the `commits` map, returning it when completed func fillCommits(email string, path string, commits map[int]int) map[int]int { // instantiate a git repo object from path repo, err := git.PlainOpen(path) if err != nil { panic(err) } // get the HEAD reference ref, err := repo.Head() if err != nil { panic(err) } // get the commits history starting from HEAD iterator, err := repo.Log(&git.LogOptions{From: ref.Hash()}) if err != nil { panic(err) } // iterate the commits offset := calcOffset() err = iterator.ForEach(func(c \*object.Commit) error { daysAgo := countDaysSinceDate(c.Author.When) + offset
// getBeginningOfDay given a time.Time calculates the start time of that day func getBeginningOfDay(t time.Time) time.Time { year, month, day := t.Date() startOfDay := time.Date(year, month, day, 0, 0, 0, 0, t.Location()) return startOfDay }
// countDaysSinceDate counts how many days passed since the passed `date` func countDaysSinceDate(date time.Time) int { days := 0 now := getBeginningOfDay(time.Now()) for date.Before(now) { date = date.Add(time.Hour \* 24) days++ if days > daysInLastSixMonths { return outOfRange } } return days }
// calcOffset determines and returns the amount of days missing to fill // the last row of the stats graph func calcOffset() int { var offset int weekday := time.Now().Weekday()
switch weekday { case time.Sunday: offset = 7 case time.Monday: offset = 6 case time.Tuesday: offset = 5 case time.Wednesday: offset = 4 case time.Thursday: offset = 3 case time.Friday: offset = 2 case time.Saturday: offset = 1 }
return offset }
現在我們處理完了提交。我們現在有了一個提交映射,我們可以打印它。這是操作中心:
1 2 3 4 5 6
// printCommitsStats prints the commits stats func printCommitsStats(commits map[int]int) { keys := sortMapIntoSlice(commits) cols := buildCols(keys, commits) printCells(cols) }
1.對映射排序 2.生成列 3.打印每列
對映射排序
1 2 3 4 5 6 7 8 9 10 11 12
// sortMapIntoSlice returns a slice of indexes of a map, ordered func sortMapIntoSlice(m map[int]int) []int { // order map // To store the keys in slice in sorted order var keys []int for k := range m { keys = append(keys, k) } sort.Ints(keys)
// buildCols generates a map with rows and columns ready to be printed to screen func buildCols(keys []int, commits map[int]int) map[int]column { cols := make(map[int]column) col := column{}
for \_, k := range keys { week := int(k / 7) //26,25...1 dayinweek := k % 7 // 0,1,2,3,4,5,6
// printCells prints the cells of the graph func printCells(cols map[int]column) { printMonths() for j := 6; j >= 0; j-- { for i := weeksInLastSixMonths + 1; i >= 0; i-- { if i == weeksInLastSixMonths+1 { printDayCol(j) } if col, ok := cols[i]; ok { //special case today if i == 0 && j == calcOffset()-1 { printCell(col[j], true) continue } else { if len(col) > j { printCell(col[j], false) continue } } } printCell(0, false) } fmt.Printf("\n") } }
// printDayCol given the day number (0 is Sunday) prints the day name, // alternating the rows (prints just 2,4,6) func printDayCol(day int) { out := “ “ switch day { case 1: out = “ Mon “ case 3: out = “ Wed “ case 5: out = “ Fri “ }
// printCell given a cell value prints it with a different format // based on the value amount, and on the today flag. func printCell(val int, today bool) { escape := “\033[0;37;30m” switch { case val > 0 && val < 5: escape = “\033[1;30;47m” case val >= 5 && val < 10: escape = “\033[1;30;43m” case val >= 10: escape = “\033[1;30;42m” }
if today { escape = “\033[1;37;45m” }
if val == 0 { fmt.Printf(escape + “ - “ + “\033[0m”) return }
str := “ %d “ switch { case val >= 10: str = “ %d “ case val >= 100: str = “%d “ }