github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/bobgit/add.go (about)

     1  package bobgit
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/Benchkram/bob/bobgit/pathspec"
    11  	"github.com/Benchkram/bob/pkg/bobutil"
    12  	"github.com/Benchkram/bob/pkg/cmdutil"
    13  	"github.com/Benchkram/bob/pkg/usererror"
    14  	"github.com/Benchkram/errz"
    15  )
    16  
    17  // pathspecItem stores a git pathspec with the repository
    18  // path relative to the bob root
    19  type pathspecItem struct {
    20  	pathspec string
    21  	repo     string
    22  }
    23  
    24  // Add run git add commands by travsersing all the repositories
    25  // inside the bob workspace.
    26  //
    27  // if target is provided "." it selects all modified files
    28  // in all repos by running `git add . --dry-run`, then run
    29  // `git add {filename}` for all selected files.
    30  //
    31  // else for subdirectory e.g. `subdir/.` it selectes all files
    32  // on that subdir and also all repository under that subtree
    33  // and run `git add --dry-run` followed by `git add {filename}`
    34  //
    35  // else for specific file under any repository on bob workspace
    36  // select the specific repository and run `git add --dry-run` followed
    37  // by `git add {filename}`.
    38  //
    39  // Run all the steps iterativley for multiple targets.
    40  func Add(targets ...string) (err error) {
    41  	pathlist := []pathspecItem{}
    42  
    43  	for _, target := range targets {
    44  		defer errz.Recover(&err)
    45  
    46  		bobRoot, err := bobutil.FindBobRoot()
    47  		errz.Fatal(err)
    48  
    49  		target, err = convertPathRelativeToRoot(bobRoot, target)
    50  		if err != nil {
    51  			return usererror.Wrap(err)
    52  		}
    53  
    54  		err = os.Chdir(bobRoot)
    55  		errz.Fatal(err)
    56  
    57  		// Assure toplevel is a git repo
    58  		isGit, err := isGitRepo(bobRoot)
    59  		errz.Fatal(err)
    60  		if !isGit {
    61  			return usererror.Wrap(ErrCouldNotFindGitDir)
    62  		}
    63  
    64  		ps := pathspec.New(target)
    65  
    66  		// search for git repos inside bobRoot/.
    67  		allRepos, err := findRepos(bobRoot)
    68  		errz.Fatal(err)
    69  
    70  		filteredRepos := ps.SelectReposByPath(allRepos)
    71  
    72  		for _, name := range filteredRepos {
    73  			thistarget, err := ps.GetRelativePathspec(name)
    74  			errz.Fatal(err)
    75  
    76  			if name == "." {
    77  				name = strings.TrimSuffix(name, ".")
    78  			}
    79  
    80  			output, err := cmdutil.GitAddDry(name, thistarget)
    81  			if err != nil {
    82  				return usererror.Wrapm(err, "Failed to Add files to git.")
    83  			}
    84  
    85  			filenames := parseAddDryOutput(output)
    86  
    87  			for _, f := range filenames {
    88  				pathspecItem := pathspecItem{
    89  					repo:     name,
    90  					pathspec: f,
    91  				}
    92  				pathlist = append(pathlist, pathspecItem)
    93  			}
    94  		}
    95  	}
    96  
    97  	for _, pi := range pathlist {
    98  		err = cmdutil.GitAdd(pi.repo, pi.pathspec)
    99  		if err != nil {
   100  			return usererror.Wrapm(err, "Failed to Add files to git.")
   101  		}
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  // parseAddDryOutput parse the output from `git add --dry-run`
   108  // and returns a list of filenames
   109  //
   110  // Example output:
   111  //   $ git add . --dry-run
   112  //   add 'bobgit/add.go'
   113  //   add 'bobgit/bobgit.go'
   114  //   add 'qq'
   115  //
   116  func parseAddDryOutput(buf []byte) (_ []string) {
   117  	fileNames := []string{}
   118  
   119  	scanner := bufio.NewScanner(bytes.NewBuffer(buf))
   120  	for scanner.Scan() {
   121  		line := scanner.Text()
   122  		nameStart := strings.Index(line, "'") + 1
   123  		nameEnd := strings.Index(line[nameStart:], "'") + nameStart
   124  		fileName := line[nameStart:nameEnd]
   125  		fileNames = append(fileNames, fileName)
   126  	}
   127  
   128  	return fileNames
   129  }
   130  
   131  // convertPathRelativeToRoot returns the relative targetpath from
   132  // the provided root.
   133  //
   134  // Example:  `../sample/path` => `bobroot/sample/path`.
   135  //
   136  // also handles git pathspec features like `.`
   137  //
   138  // Example: `sample/.` => `bobroot/sample/.`
   139  func convertPathRelativeToRoot(root string, target string) (string, error) {
   140  	dir, err := filepath.Abs(target)
   141  	errz.Fatal(err)
   142  
   143  	if dir == root {
   144  		return ".", nil
   145  	}
   146  
   147  	if !strings.HasPrefix(dir, root) {
   148  		return target, ErrOutsideBobWorkspace
   149  	}
   150  	relativepath, err := filepath.Rel(root, dir)
   151  	errz.Fatal(err)
   152  
   153  	if target == "." || target == "" || strings.HasSuffix(target, "/.") {
   154  		relativepath = relativepath + "/."
   155  	}
   156  
   157  	return relativepath, nil
   158  }