github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/bobgit/status/status.go (about)

     1  package status
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"path/filepath"
     7  	"sort"
     8  	"strings"
     9  
    10  	git "github.com/go-git/go-git/v5"
    11  	"github.com/logrusorgru/aurora"
    12  )
    13  
    14  type MultiRepoStatus map[string]git.Status
    15  
    16  type S struct {
    17  	Staging   MultiRepoStatus
    18  	Unstaged  MultiRepoStatus
    19  	Untracked MultiRepoStatus
    20  	Conflicts MultiRepoStatus
    21  
    22  	Repos []string
    23  }
    24  
    25  func New() *S {
    26  	s := &S{
    27  		Staging:   make(MultiRepoStatus),
    28  		Unstaged:  make(MultiRepoStatus),
    29  		Untracked: make(MultiRepoStatus),
    30  		Conflicts: make(MultiRepoStatus),
    31  	}
    32  	return s
    33  }
    34  
    35  // AddRepo iniitializes the state maps with a new repo
    36  func (s *S) AddRepo(repoName string) {
    37  	s.Staging[repoName] = make(git.Status)
    38  	s.Unstaged[repoName] = make(git.Status)
    39  	s.Untracked[repoName] = make(git.Status)
    40  	s.Conflicts[repoName] = make(git.Status)
    41  }
    42  
    43  func (s *S) String() string {
    44  	const spacing = "        "
    45  
    46  	buf := bytes.NewBuffer(nil)
    47  
    48  	{
    49  		b := bytes.NewBuffer(nil)
    50  		keys := sortedKeys(s.Conflicts)
    51  		var conflictingRepos []string
    52  		for _, repoName := range keys {
    53  			repoStatus := s.Conflicts[repoName]
    54  			repoStatusKeys := sortedKeysStatus(repoStatus)
    55  			for _, path := range repoStatusKeys {
    56  				fmt.Fprint(b, spacing)
    57  				fprintChanges(b, repoName, path, repoStatus[path], aurora.Red)
    58  				conflictingRepos = append(conflictingRepos, repoName)
    59  			}
    60  		}
    61  
    62  		if len(conflictingRepos) > 0 {
    63  			fmt.Fprintln(buf, "On at least one repository")
    64  			fmt.Fprintln(buf, "You have unmerged paths.")
    65  			fmt.Fprintln(buf, "  (fix conflicts using plain git)")
    66  			fmt.Fprintln(buf, "\nUnmerged paths:")
    67  			fmt.Fprint(buf, b)
    68  		}
    69  	}
    70  
    71  	{
    72  		fmt.Fprintln(buf, "Changes to be committed:")
    73  
    74  		// keys := sortedKeys(s.Staging)
    75  		// for _, repoName := range keys {
    76  		// 	repoStatus := s.Staging[repoName]
    77  		// 	for path, status := range repoStatus {
    78  		// 		fmt.Fprint(buf, spacing)
    79  		// 		//fmt.Fprintf(buf, "s%cw%c", status.Staging, status.Worktree) // debugging
    80  		// 		//fmt.Fprintf(buf, " (%s) ", aurora.Green(repoName))
    81  		// 		fprintChanges(buf, repoName, path, status, aurora.Green)
    82  		// 	}
    83  		// }
    84  		FPrintMultirepoStatus(buf, spacing, s.Staging, aurora.Green)
    85  		fmt.Fprintln(buf)
    86  	}
    87  
    88  	{
    89  		fmt.Fprintln(buf, "Changes not staged for commit:")
    90  
    91  		// keys := sortedKeys(s.Unstaged)
    92  		// for _, repoName := range keys {
    93  		// 	repoStatus := s.Unstaged[repoName]
    94  		// 	for path, status := range repoStatus {
    95  		// 		fmt.Fprint(buf, spacing)
    96  		// 		//fmt.Fprintf(buf, "s%cw%c", status.Staging, status.Worktree) // debugging
    97  		// 		//fmt.Fprintf(buf, " (%s) ", aurora.Red(repoName))
    98  		// 		fprintChanges(buf, repoName, path, status, aurora.Red)
    99  		// 	}
   100  		// }
   101  		FPrintMultirepoStatus(buf, spacing, s.Unstaged, aurora.Red)
   102  		fmt.Fprintln(buf)
   103  	}
   104  
   105  	{
   106  		fmt.Fprintln(buf, "Untracked files:")
   107  
   108  		keys := sortedKeys(s.Untracked)
   109  		for _, repoName := range keys {
   110  			repoStatus := s.Untracked[repoName]
   111  			repoStatusKeys := sortedKeysStatus(repoStatus)
   112  
   113  			for _, path := range repoStatusKeys {
   114  				fmt.Fprint(buf, spacing)
   115  				fprintChanges(buf, repoName, path, &git.FileStatus{}, aurora.Red)
   116  			}
   117  		}
   118  	}
   119  
   120  	return buf.String()
   121  }
   122  
   123  func FPrintMultirepoStatus(buf *bytes.Buffer, spacing string, repos MultiRepoStatus, color func(interface{}) aurora.Value) {
   124  	keys := sortedKeys(repos)
   125  	for _, repoName := range keys {
   126  		repoStatus := repos[repoName]
   127  		repoStatusKeys := sortedKeysStatus(repoStatus)
   128  		for _, path := range repoStatusKeys {
   129  			fmt.Fprint(buf, spacing)
   130  			fprintChanges(buf, repoName, path, repoStatus[path], color)
   131  		}
   132  	}
   133  }
   134  
   135  // fprintChanges helper to color & highlight the output based on status
   136  func fprintChanges(
   137  	buf *bytes.Buffer,
   138  	repoPath, localPath string,
   139  	status *git.FileStatus,
   140  	color func(interface{}) aurora.Value,
   141  ) {
   142  	if repoPath == "." {
   143  		repoPath = ""
   144  	}
   145  
   146  	dir, basename := splitDirAndBasename(repoPath)
   147  	// fmt.Printf("prefix: [%s] path: [%s]\n", dir, basename)
   148  
   149  	// fmt.Println(string(status.Staging) + " " + string(status.Worktree))
   150  
   151  	// check for the conflicts first
   152  	if (status.Staging == git.UpdatedButUnmerged || status.Worktree == git.UpdatedButUnmerged) ||
   153  		(status.Staging == git.Deleted && status.Worktree == git.Deleted) ||
   154  		(status.Staging == git.Added && status.Worktree == git.Added) {
   155  		conflictText := getConflictText(status)
   156  		fmt.Fprint(buf, color(conflictText+dir+aurora.Bold(withSlash(basename)).String()).String())
   157  		fmt.Fprintln(buf, color(localPath).String())
   158  	} else if status.Staging == git.Renamed {
   159  		//fmt.Fprint(buf, color("renamed:   "+aurora.Bold(withSlash(repoName)).String()).String())
   160  		fmt.Fprint(buf, color("renamed:    "))
   161  		fmt.Fprint(buf, color(dir).String())
   162  		fmt.Fprint(buf, color(aurora.Bold(withSlash(basename))).String())
   163  		fmt.Fprint(buf, color(status.Extra).String())
   164  		fmt.Fprint(buf, color(" -> ").String())
   165  		fmt.Fprint(buf, color(dir).String())
   166  		fmt.Fprint(buf, color(aurora.Bold(withSlash(basename))).String())
   167  		fmt.Fprintln(buf, color(localPath).String())
   168  	} else if status.Staging == git.Modified || status.Worktree == git.Modified {
   169  		fmt.Fprint(buf, color("modified:   "+dir+aurora.Bold(withSlash(basename)).String()).String())
   170  		fmt.Fprintln(buf, color(localPath).String())
   171  	} else if status.Staging == git.Added || status.Worktree == git.Added {
   172  		fmt.Fprint(buf, color("new file:   "+dir+aurora.Bold(withSlash(basename)).String()).String())
   173  		fmt.Fprintln(buf, color(localPath).String())
   174  	} else if status.Staging == git.Deleted || status.Worktree == git.Deleted {
   175  		fmt.Fprint(buf, color("deleted:    "+dir+aurora.Bold(withSlash(basename)).String()).String())
   176  		fmt.Fprintln(buf, color(localPath).String())
   177  	} else {
   178  		fmt.Fprint(buf, color(dir).String())
   179  		fmt.Fprint(buf, color(aurora.Bold(withSlash(basename)).String()).String())
   180  		fmt.Fprintln(buf, color(localPath).String())
   181  	}
   182  }
   183  
   184  // sortedKeys returns the keys of `MultiRepoStatus` sorted by `sort.Strings`
   185  func sortedKeys(m MultiRepoStatus) (keys []string) {
   186  	keys = make([]string, 0, len(m))
   187  	for key := range m {
   188  		keys = append(keys, key)
   189  	}
   190  	sort.Strings(keys)
   191  	return keys
   192  }
   193  
   194  func sortedKeysStatus(m git.Status) (keys []string) {
   195  	keys = make([]string, 0, len(m))
   196  	for key := range m {
   197  		keys = append(keys, key)
   198  	}
   199  	sort.Strings(keys)
   200  	return keys
   201  }
   202  
   203  func withSlash(s string) string {
   204  	if strings.HasSuffix(s, "/") || s == "" {
   205  		return s
   206  	}
   207  	return s + "/"
   208  }
   209  
   210  // split repoPath in dir and basename
   211  // with some additional sanitising.
   212  func splitDirAndBasename(path string) (prefix, name string) {
   213  	if path != "" {
   214  		prefix = filepath.Dir(path) + "/"
   215  		if prefix == "./" {
   216  			prefix = ""
   217  		}
   218  
   219  		name = filepath.Base(path)
   220  		if name == ".." {
   221  			name = ""
   222  		}
   223  	}
   224  	return prefix, name
   225  }
   226  
   227  // return the merge conflict text for the file
   228  // depending on the conflicting status
   229  // on merge, delete, etc
   230  func getConflictText(status *git.FileStatus) string {
   231  	conflictText := "both modified:\t"
   232  
   233  	if status.Worktree == git.UpdatedButUnmerged && status.Staging == git.Deleted {
   234  		conflictText = "deleted by us:\t"
   235  	}
   236  	if status.Staging == git.UpdatedButUnmerged && status.Worktree == git.Deleted {
   237  		conflictText = "deleted by them:\t"
   238  	}
   239  	if status.Staging == git.Deleted && status.Worktree == git.Deleted {
   240  		conflictText = "both deleted:\t"
   241  	}
   242  	if status.Worktree == git.UpdatedButUnmerged && status.Staging == git.Added {
   243  		conflictText = "added by us:\t"
   244  	}
   245  	if status.Staging == git.UpdatedButUnmerged && status.Worktree == git.Added {
   246  		conflictText = "added by them:\t"
   247  	}
   248  	if status.Staging == git.Added && status.Worktree == git.Added {
   249  		conflictText = "both added:\t"
   250  	}
   251  
   252  	return conflictText
   253  }