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  }