github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/bobgit/commit.go (about) 1 package bobgit 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "strings" 8 9 "github.com/Benchkram/bob/pkg/bobutil" 10 "github.com/Benchkram/bob/pkg/cmdutil" 11 "github.com/Benchkram/bob/pkg/usererror" 12 "github.com/Benchkram/errz" 13 git "github.com/go-git/go-git/v5" 14 "github.com/logrusorgru/aurora" 15 ) 16 17 var ErrEmptyCommitMessage = fmt.Errorf("aaaaaa") 18 19 var CleanWorkingDirMessage = "nothing to commit, working trees are clean." 20 21 func UntrackedRepoMessage(repolist []string) string { 22 formattedRepos := []string{} 23 24 for _, repo := range repolist { 25 formattedRepos = append(formattedRepos, formatRepoNameForOutput(repo)) 26 } 27 28 return fmt.Sprint("nothing to commit but untracked files present in repositories [", strings.Join(formattedRepos, ", "), "]") 29 } 30 31 // Commit executes `git commit -m ${message}` in all repositories. 32 // 33 // indifferent of the subdirectories and subrepositories, 34 // it walks through all the repositories starting from bobroot 35 // and run `git commit -m {message}` command. 36 // 37 // Only returns user messages in case of nothing to commit. 38 func Commit(message string) (s string, err error) { 39 defer errz.Recover(&err) 40 41 if message == "" { 42 return "", ErrEmptyCommitMessage 43 } 44 45 bobRoot, err := bobutil.FindBobRoot() 46 errz.Fatal(err) 47 48 err = os.Chdir(bobRoot) 49 errz.Fatal(err) 50 51 // Assure toplevel is a git repo 52 isGit, err := isGitRepo(bobRoot) 53 errz.Fatal(err) 54 if !isGit { 55 return "", usererror.Wrap(ErrCouldNotFindGitDir) 56 } 57 58 // search for git repos inside bobRoot/. 59 repoNames, err := findRepos(bobRoot) 60 errz.Fatal(err) 61 62 // repos with no changes, throws exit status 1 error 63 // while executing `git commit --dry-run`. 64 // 65 // only repos with changes filtered out 66 filteredRepo, untrackedRepo, err := filterModifiedRepos(repoNames) 67 errz.Fatal(err) 68 69 // throw some user message for untracked repositories 70 if len(filteredRepo) == 0 { 71 s := CleanWorkingDirMessage 72 if len(untrackedRepo) > 0 { 73 s = UntrackedRepoMessage(untrackedRepo) 74 } 75 return s, nil 76 } 77 78 maxRepoLen := 0 79 // execute dry-run first on all repositories 80 // to check for errors 81 for _, name := range filteredRepo { 82 _, err := cmdutil.GitDryCommit(name, message) 83 84 // set maximum repository name length for indentation 85 if len(name) > maxRepoLen { 86 maxRepoLen = len(name) 87 } 88 89 if err != nil { 90 return "", usererror.Wrapm(err, "Failed to commit to git in repo \""+name+"\"") 91 } 92 } 93 94 for _, name := range filteredRepo { 95 output, err := cmdutil.GitCommit(name, message) 96 buf := FprintCommitOutput(name, output, maxRepoLen, err == nil) 97 // instead of returnting the git output prints it here 98 if len(output) > 0 { 99 fmt.Println(buf.String()) 100 } 101 } 102 103 return "", nil 104 } 105 106 // filterModifiedRepos filters the repositories with changes 107 // by running `git status --porcelain` command on each repository and 108 // look for tracked files in staging. 109 // 110 // returns a list of repository which consist tracked files, 111 // also returns a list of untracked but modified repositories. 112 func filterModifiedRepos(repolist []string) ([]string, []string, error) { 113 updatedRepo := []string{} 114 untrackedRepo := []string{} 115 116 for _, name := range repolist { 117 output, err := cmdutil.GitStatus(name) 118 if err != nil { 119 return updatedRepo, untrackedRepo, err 120 } 121 122 status, err := parse(output) 123 if err != nil { 124 return updatedRepo, untrackedRepo, err 125 } 126 127 var tracked bool = false 128 var untracked bool = false 129 for _, filestatus := range status { 130 if filestatus.Staging != git.Untracked { 131 tracked = true 132 break 133 } 134 135 if !untracked && filestatus.Worktree != git.Unmodified { 136 untracked = true 137 } 138 } 139 140 if tracked { 141 updatedRepo = append(updatedRepo, name) 142 } else if untracked { 143 untrackedRepo = append(untrackedRepo, name) 144 } 145 } 146 147 return updatedRepo, untrackedRepo, nil 148 } 149 150 // FprintCommitOutput formats output buffer from every repository commit output 151 func FprintCommitOutput(reponame string, output []byte, maxlen int, success bool) *bytes.Buffer { 152 buf := bytes.NewBuffer(nil) 153 154 // format the reponame for output 155 repopath := reponame 156 if reponame == "." { 157 repopath = "/" 158 } else if repopath[len(repopath)-1:] != "/" { 159 repopath = repopath + "/" 160 } 161 162 spacing := "%-" + fmt.Sprint(maxlen) + "s" 163 repopath = fmt.Sprintf(spacing, repopath) 164 title := fmt.Sprint(repopath, "\t", aurora.Green("success")) 165 if !success { 166 title = fmt.Sprint(repopath, "\t", aurora.Red("failed!!")) 167 } 168 fmt.Fprint(buf, title) 169 fmt.Fprintln(buf) 170 171 if len(output) > 0 { 172 for _, line := range outputLines(output) { 173 modified := fmt.Sprint(" ", aurora.Gray(12, line)) 174 fmt.Fprintln(buf, modified) 175 } 176 } 177 return buf 178 } 179 180 func outputLines(output []byte) []string { 181 lines := strings.TrimSuffix(string(output), "\n") 182 return strings.Split(lines, "\n") 183 }