├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .notes ├── git-pair-gif.gif ├── notes.md ├── talk │ ├── example-git-log.png │ ├── funny-error-message.png │ ├── github-example.png │ └── talk.md └── test ├── actions ├── actions.go ├── modifyCommitMessage.go └── prepareCommit_test.go ├── arrays └── arrays.go ├── autocomplete └── zsh_autocomplete ├── gitconfig ├── gitconfig.go └── helpers.go ├── gitpair.go └── readme.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 🐞 3 | about: Something isn't working as expected? Here is the right place to report. 4 | --- 5 | 6 | ## Description 7 | 8 | Describe the issue that you're seeing. 9 | 10 | ### Steps to reproduce 11 | 12 | Clear steps describing how to reproduce the issue. Please please please link to a demo project if possible, this makes your issue _much_ easier to diagnose (seriously). 13 | 14 | ### Expected result 15 | 16 | What should happen? 17 | 18 | ### Actual result 19 | 20 | What happened. 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 💡 3 | about: Suggest a new idea for the project. 4 | --- 5 | 6 | ## Summary 7 | 8 | Brief explanation of the feature. 9 | 10 | ### Basic example 11 | 12 | ### Motivation 13 | 14 | Why are we doing this? What use cases does it support? What is the expected outcome? 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thechutrain/git-pair/9c6dc6afc9620ea0ae1615ed28ab91fcd9921010/.gitignore -------------------------------------------------------------------------------- /.notes/git-pair-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thechutrain/git-pair/9c6dc6afc9620ea0ae1615ed28ab91fcd9921010/.notes/git-pair-gif.gif -------------------------------------------------------------------------------- /.notes/notes.md: -------------------------------------------------------------------------------- 1 | git commit message after pair amends should look like this: 2 | 3 | ``` 4 | ... message 5 | 6 | Co-authored-by: brandon 7 | Co-authored-by: alan 8 | # git comments 9 | # 10 | ``` 11 | 12 | ```sh 13 | #!/bin/sh 14 | set -e 15 | 16 | CURR_PAIR_FILE=$HOME/.pear/$(basename $PWD)/current_pairs.txt 17 | cat $CURR_PAIR_FILE | awk 'BEGIN{FS=" "} {print "Co-authored-by: " $1 " <" $2 ">"}' >> $1 18 | ``` 19 | 20 | prepare-commit-msg hook: 21 | 22 | ```sh 23 | #!/bin/sh 24 | set -e 25 | 26 | CURR_PAIR_FILE=$HOME/.pear/$(basename $PWD)/current_pairs.txt 27 | TEMP="$HOME/.pear/.default-commit" 28 | 29 | # Save the old content. 30 | cat $1 > $TEMP 31 | 32 | # Prepend two newlines. 33 | printf "\n\n" > $1 34 | 35 | # Create the Co-authored-by tags. 36 | cat $CURR_PAIR_FILE | awk 'BEGIN{FS=" "} {print "Co-authored-by: " $1 " <" $2 ">"}' >> $1 37 | 38 | # Append the old content. 39 | cat $TEMP | sed "/^Co-authored-by/d" >> $1 40 | 41 | rm $TEMP 42 | ``` 43 | 44 | ## Saving current pairs 45 | 46 | - you can use the `.git/config` file 47 | - use `git config` commands to modify these commands: 48 | - http://craig-russell.co.uk/2011/08/24/git-tip-custom-config-parameters.html#.XPaUBtNKiV4 49 | 50 | ``` 51 | // Note throws an error that "key does not contain a section" if you try to get something that doesnt exist! 52 | 53 | 54 | git config --add pair.coauthor namehere // you need the --add flag in order to not overwrite prev val 55 | git config --get-all pair.coauthor // gets all the coauthors 56 | git config --unset pair.coauthor nameofpair // removes a single user 57 | git config --unset-all pair.coauthor // removes all users 58 | ``` 59 | 60 | ## Misc reading arguments from a shell script 61 | 62 | ```sh 63 | #!/bin/sh 64 | 65 | if [ -z $1 ]; then 66 | echo empty 67 | else 68 | echo not empty 69 | fi 70 | ``` 71 | 72 | ## Set up 73 | 74 | Add this to your `.git/hooks/prepare-commit-msg` file 75 | 76 | ```sh 77 | #!/bin/sh 78 | 79 | set -e 80 | 81 | # cannot alias this? 82 | gitpair _prepare-commit-msg $@ #adds all of the arguments in bash 83 | ``` 84 | -------------------------------------------------------------------------------- /.notes/talk/example-git-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thechutrain/git-pair/9c6dc6afc9620ea0ae1615ed28ab91fcd9921010/.notes/talk/example-git-log.png -------------------------------------------------------------------------------- /.notes/talk/funny-error-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thechutrain/git-pair/9c6dc6afc9620ea0ae1615ed28ab91fcd9921010/.notes/talk/funny-error-message.png -------------------------------------------------------------------------------- /.notes/talk/github-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thechutrain/git-pair/9c6dc6afc9620ea0ae1615ed28ab91fcd9921010/.notes/talk/github-example.png -------------------------------------------------------------------------------- /.notes/talk/talk.md: -------------------------------------------------------------------------------- 1 | ## Git-Pair 🍐🍐 2 | 3 | --- 4 | 5 | ## Background 6 | 7 | - pairing at RC 8 | - manually adding "Co-authored-by:" to credit others 9 | - Gonazalo (Recurser) made [gonzalo/pair](https://github.com/gonzalo-bulnes/pair) 10 | 11 | --- 12 | 13 | ## Specs 14 | 15 | - stores current pairs in `.git/config` 16 | - uses `prepare-commit-msg` hook 17 | - uses `urfave/cli` for autocompletion of cli cmds 18 | 19 | --- 20 | 21 | ## Demo 22 | 23 | --- 24 | 25 | ## Future Features 26 | 27 | - alias for github names 28 | - validating github names (http requests) 29 | - autocomplete the `git pair add` command with previous collaborators 30 | - ps1 variable output 31 | 32 | --- 33 | 34 | ## Fun Moments 35 | 36 | #### Testing on the repo itself 37 | 38 | ![funny error](funny-error-message.png) 39 | 40 | --- 41 | 42 | ## More Fun Moments 43 | 44 | ## ![fun messages](example-git-log.png) 45 | 46 | --- 47 | 48 | ## 🙏 Thanks 49 | 50 |
51 |
    52 |
  • Brandon
  • 53 |
  • Blinry
  • 54 |
  • Austin
  • 55 |
  • Nick
  • 56 |
  • Anushri
  • 57 |
  • Maren
  • 58 |
  • Ian
  • 59 |
60 |
61 | -------------------------------------------------------------------------------- /.notes/test: -------------------------------------------------------------------------------- 1 | Stuff here 2 | 3 | Co-authored-by: cookie monster 4 | Co-authored-by: elmo 5 | 6 | # asdfads 7 | # asdfasdfasd 8 | 9 | Changes: 10 | 11 | + Co-authored-by: someone 12 | + IMPORTANT="Co-authored-by" -------------------------------------------------------------------------------- /actions/actions.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/thechutrain/git-pair/gitconfig" 11 | "gopkg.in/urfave/cli.v1" 12 | ) 13 | 14 | // Init - creates the prepare-commit-msg hooks 15 | func Init(args cli.Args) error { 16 | if !isInitialized() { 17 | fmt.Printf("Initializing git commit message hook ...\n") 18 | _ = makeCommitMessageHook() 19 | } else if len(args) > 0 && args[0] == "force" { 20 | fmt.Printf("Reinitializing git commit message hook ... \n") 21 | _ = makeCommitMessageHook() 22 | } else { 23 | fmt.Printf("Already initialized. To remake the commit msg hook, type: pair init force") 24 | } 25 | 26 | return nil 27 | } 28 | 29 | // isInitialized checks to see if the prepare-commit-msg hook exists 30 | func isInitialized() bool { 31 | gitDir, err := gitconfig.GitDir() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | hookFile := gitDir + "/hooks/commit-msg" 37 | 38 | if _, err := os.Stat(hookFile); err == nil { 39 | return true 40 | } else if os.IsNotExist(err) { 41 | return false 42 | } else if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | return false 47 | } 48 | 49 | // mustBeInitialized - makes sures the prepare commit msg hook exists 50 | func mustBeInitialized() { 51 | if !isInitialized() { 52 | // Question: Panic here? or return error? 53 | // log.Fatalf("You must first initialize this repo for pairing\nType: git pair init\n") 54 | fmt.Printf("You must first initialize this repo for pairing\nType: pair init\n") 55 | os.Exit(2) // TODO: have a list of exit codes? 56 | } 57 | } 58 | 59 | func makeCommitMessageHook() error { 60 | // Note: all the file permissions are 755 61 | gitDir, err := gitconfig.GitDir() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | hookFile := gitDir + "/hooks/commit-msg" 67 | 68 | hookScript := []byte(`#!/bin/sh 69 | set -e 70 | 71 | # Hook from git-pair 🍐 72 | gitpair _modify-commit-msg $@ #adds all of the arguments in bash 73 | `) 74 | 75 | err = ioutil.WriteFile(hookFile, hookScript, 0755) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // Add - adds a new pair 84 | func Add(args cli.Args) error { 85 | mustBeInitialized() 86 | 87 | return gitconfig.AddPair(args) 88 | } 89 | 90 | // Remove - remove 91 | func Remove(args cli.Args) error { 92 | mustBeInitialized() 93 | 94 | _, err := gitconfig.RemovePair(strings.Join(args, " ")) 95 | 96 | return err 97 | } 98 | 99 | // RemoveAll - removes all 100 | func RemoveAll(args cli.Args) error { 101 | mustBeInitialized() 102 | 103 | err := gitconfig.RemoveAllPairs() 104 | 105 | return err 106 | } 107 | 108 | // Status - status status status status status status status status status status status 109 | func Status(args cli.Args) error { 110 | mustBeInitialized() 111 | 112 | pairs, _ := gitconfig.CurrPairs() 113 | 114 | if len(pairs) > 0 { 115 | // TODO: feature - print with column headers etc 116 | fmt.Printf("Pairing with: \n\t" + strings.Join(pairs, "\n\t")) 117 | fmt.Printf("\n\nType: \"pair stop\" ") 118 | } else { 119 | fmt.Printf("You are not currently pairing with anyone\nTo begin pairing with a new person type:\n\t\"pair add [GitHub_Handle]\"\n") 120 | } 121 | 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /actions/modifyCommitMessage.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "os" 7 | "regexp" 8 | 9 | "github.com/thechutrain/git-pair/gitconfig" 10 | 11 | "github.com/thechutrain/git-pair/arrays" 12 | ) 13 | 14 | // PrepareCommitMsg prepares the commit message 15 | // gets called by git arg [command, "preparecommitmsg", $1, $2, $3] 16 | // where $1 is the filename of the temp git commit file? 17 | func ModifyCommitMsg(args []string) { 18 | fileName := args[2] 19 | 20 | lines := readLines(fileName) 21 | 22 | pairs, _ := gitconfig.CurrPairs() 23 | coauthors := arrays.Map(pairs, func(str string) string { 24 | return "Co-authored-by: " + str + " <" + str + "@users.noreply.github.com>" 25 | }) 26 | updateCommitMsg := addCoAuthors(lines, coauthors) 27 | writeLines(fileName, updateCommitMsg) 28 | 29 | // TODO: Print out possible co-authors so end-user knows who was added 30 | } 31 | 32 | // readLines - given the path to .git/COMMIT_EDITMSG, reads contents into a string slice 33 | func readLines(fileName string) []string { 34 | file, err := os.Open(fileName) 35 | 36 | if err != nil { 37 | log.Fatalf("Failed opening file: %s", err) 38 | } 39 | 40 | scanner := bufio.NewScanner(file) 41 | scanner.Split(bufio.ScanLines) 42 | var lines []string 43 | 44 | for scanner.Scan() { 45 | lines = append(lines, scanner.Text()) 46 | } 47 | 48 | defer file.Close() 49 | 50 | return lines 51 | } 52 | 53 | // writeLines - given the path to .git/COMMIT_EDITMSG, will rewrite the contents of the commit msg 54 | func writeLines(fileName string, lines []string) { 55 | fileWrite, err := os.OpenFile(fileName, os.O_WRONLY, 0644) 56 | 57 | if err != nil { 58 | log.Fatalf("Failed opening file: %s", err) 59 | } 60 | 61 | for _, line := range lines { 62 | fileWrite.WriteString(line + "\n") 63 | 64 | // fmt.Println(line) 65 | } 66 | defer fileWrite.Close() 67 | } 68 | 69 | // addCoAuthors - adds coauthors to the commit message above the first commented block 70 | func addCoAuthors(lines []string, coauthors []string) []string { 71 | // Note: for gc --amend case. Remove possible stale data 72 | re := regexp.MustCompile("(^Co-authored-by:)|(^# Added by git-pair)") 73 | lines = arrays.Filter(lines, func(str string) bool { 74 | return !re.MatchString(str) 75 | }) 76 | 77 | // No co-authors case 78 | if len(coauthors) == 0 { 79 | return lines 80 | } 81 | 82 | // Read from top and find first line that is a comment 83 | firstEmptyLine := len(lines) - 1 84 | for i := 0; i < len(lines); i++ { 85 | match, _ := regexp.MatchString("^#||^\\s", lines[i]) 86 | if match { 87 | firstEmptyLine = i + 1 88 | break 89 | } 90 | } 91 | 92 | updateCommitMsg := make([]string, 0) 93 | updateCommitMsg = append(updateCommitMsg, lines[:firstEmptyLine]...) 94 | updateCommitMsg = append(updateCommitMsg, "") 95 | updateCommitMsg = append(updateCommitMsg, coauthors...) 96 | // TODO: adding the #added by git-pair 🍐 breaks it haha 97 | // updateCommitMsg = append(updateCommitMsg, "# Added by git-pair 🍐") 98 | updateCommitMsg = append(updateCommitMsg, lines[firstEmptyLine:]...) 99 | 100 | return updateCommitMsg 101 | } 102 | -------------------------------------------------------------------------------- /actions/prepareCommit_test.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | // func TestContainsCoAuthors(t *testing.T) { 9 | // lines := []string{"This does not", "contain Co-authored-by"} 10 | // if containsCoAuthor(lines) { 11 | // t.Error("Lines without the co-author tag were reported to contain it") 12 | // } 13 | 14 | // lines2 := []string{"This does contain", "Co-authored-by: cookie monster"} 15 | // if !containsCoAuthor(lines2) { 16 | // t.Error("Lines with the co-author tag were not reported to contain it") 17 | // } 18 | 19 | // lines3 := []string{"This does contain", "Co-AuthoRED-By: cookie monster"} 20 | // if !containsCoAuthor(lines3) { 21 | // t.Error("Lines with the co-author tag were not reported to contain it") 22 | // } 23 | // } 24 | 25 | func TestDeepEqual(t *testing.T) { 26 | lines := []string{"A", "B", "C"} 27 | lines2 := []string{"A", "B", "D"} 28 | lines3 := []string{"A", "B", "C"} 29 | 30 | if reflect.DeepEqual(lines, lines2) { 31 | t.Error("Unequal lines were reported to be deepequal") 32 | } 33 | 34 | if !reflect.DeepEqual(lines, lines3) { 35 | t.Error("Equal lines were reported to be notdeepequal") 36 | } 37 | } 38 | 39 | func TestAddCoAuthors(t *testing.T) { 40 | lines := []string{"A", "B", "# C"} 41 | coauthors := []string{"D", "E"} 42 | expectedResult := []string{"A", "B", "# Added by 🐙", "D", "E", "", "# C"} 43 | actualResult := addCoAuthors(lines, coauthors) 44 | if !reflect.DeepEqual(expectedResult, actualResult) { 45 | t.Error("Coauthors were not inserted correctly") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /arrays/arrays.go: -------------------------------------------------------------------------------- 1 | package arrays 2 | 3 | func Filter(vs []string, f func(string) bool) []string { 4 | vsf := make([]string, 0) 5 | for _, v := range vs { 6 | if f(v) { 7 | vsf = append(vsf, v) 8 | } 9 | } 10 | return vsf 11 | } 12 | 13 | func Map(vs []string, f func(string) string) []string { 14 | vsm := make([]string, len(vs)) 15 | for i, v := range vs { 16 | vsm[i] = f(v) 17 | } 18 | return vsm 19 | } 20 | -------------------------------------------------------------------------------- /autocomplete/zsh_autocomplete: -------------------------------------------------------------------------------- 1 | _cli_zsh_autocomplete() { 2 | 3 | local -a opts 4 | opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") 5 | 6 | _describe 'values' opts 7 | 8 | return 9 | } 10 | 11 | compdef _cli_zsh_autocomplete $PROG 12 | -------------------------------------------------------------------------------- /gitconfig/gitconfig.go: -------------------------------------------------------------------------------- 1 | package gitconfig 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/thechutrain/git-pair/arrays" 10 | ) 11 | 12 | // Coauthor represents a coauthor 13 | type Coauthor struct { 14 | Name string 15 | Email string 16 | } 17 | 18 | // CmdError - represents an error from making a bash command 19 | type CmdError struct { 20 | Message string // error description 21 | ExitCode int // exit code 22 | } 23 | 24 | func (e CmdError) Error() string { 25 | return fmt.Sprintf("CmdError: %s", e.Message) 26 | } 27 | 28 | // CurrPairs gets the current co-authors you are pairing with 29 | func CurrPairs() ([]string, error) { 30 | // TODO: return struct of curr pairs? 31 | coauthors := []string{} 32 | 33 | exists, err := ContainsSection() 34 | if err != nil { 35 | return coauthors, err 36 | } 37 | 38 | if !exists { 39 | return coauthors, nil 40 | } 41 | 42 | output, err := RunGitConfigCmd("--get-all", "") 43 | if err != nil { 44 | return coauthors, err 45 | } 46 | 47 | splitOutput := strings.Split(output, "\n") 48 | splitOutput = arrays.Filter(splitOutput, func(str string) bool { 49 | return str != "" 50 | }) 51 | 52 | return splitOutput, nil 53 | } 54 | 55 | func validatePairStr(rawArgs []string) error { 56 | // TODO: validate the pairStr 57 | // IDEA: could validate github handle via http request? 58 | /* Valid inputs: 59 | - githubhandle 60 | - name 61 | */ 62 | 63 | switch len(rawArgs) { 64 | case 0: 65 | return errors.New("Must provide at least one argument for adding a pair") 66 | case 1: 67 | if strings.TrimSpace(rawArgs[0]) == "" { 68 | return errors.New("Must provide at least one argument for adding a pair") 69 | } 70 | rawUserName := rawArgs[0] 71 | // TODO: look up this github username 72 | // case 2: // Note: for username and email entered manually 73 | 74 | default: 75 | 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // AddPair adds a new coauthor line only if its unique 82 | // pairStr is in the format of "name " 83 | func AddPair(rawArgs []string) error { 84 | // Validate user input 85 | err := validatePairStr(rawArgs) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | pairStr := strings.Join(rawArgs, " ") 91 | _, cmdErr := RunGitConfigCmd("--unset-all", pairStr) // Note: prevents the addition of duplicate keys 92 | if cmdErr != nil { 93 | return cmdErr 94 | } 95 | 96 | _, cmdErr = RunGitConfigCmd("--add", pairStr) 97 | if CheckCmdError(cmdErr) != nil { 98 | return cmdErr 99 | } 100 | 101 | return nil 102 | } 103 | 104 | // RemovePair removes a single coauthor 105 | func RemovePair(pairStr string) (bool, error) { 106 | pairsBefore, cmdErr := CurrPairs() 107 | if CheckCmdError(cmdErr) != nil { 108 | return false, cmdErr 109 | } 110 | 111 | numWords := len(strings.Split(pairStr, " ")) 112 | if numWords == 1 { 113 | pairStr = "^" + pairStr + " " 114 | } else { 115 | pairStr = "^" + pairStr + "$" 116 | } 117 | 118 | _, cmdErr = RunGitConfigCmd("--unset-all", pairStr) 119 | CheckCmdError(cmdErr) 120 | pairsAfter, _ := CurrPairs() 121 | 122 | // fmt.Printf("%s\n%s", (pairsAfter), (pairsBefore)) 123 | return bool(len(pairsBefore) > len(pairsAfter)), CmdError{} 124 | } 125 | 126 | // RemoveAllPairs removes all the coauthors 127 | func RemoveAllPairs() error { 128 | _, cmdErr := RunGitConfigCmd("--unset-all", "") 129 | return cmdErr 130 | } 131 | 132 | // CheckError returns a boolean of whether there was an error that should prevent you from proceeding or not 133 | // QUESTION: should we exit the program here? 134 | // Using this function as middleware 135 | func CheckCmdError(err error) error { 136 | if err == nil { 137 | return nil 138 | } 139 | 140 | if CmdError, ok := err.(CmdError); ok { 141 | switch CmdError.ExitCode { 142 | case 0: 143 | return nil 144 | case 1: 145 | // Note - Git errors, there is a section, but no keys. 146 | return nil 147 | case 5: 148 | // Note - Ignore this error: Git config exit code if you try to access a section.key that does not exist 149 | return nil 150 | case 128: 151 | // fmt.Printf("Exited with code: %d\n", CmdError.ExitCode) 152 | log.Fatal(`Cannot run pair command outside of a git repository`) 153 | default: 154 | log.Fatalf("Unknown exit code of: %d\n Error message: %s", CmdError.ExitCode, CmdError.Message) 155 | } 156 | } 157 | 158 | return err 159 | } 160 | -------------------------------------------------------------------------------- /gitconfig/helpers.go: -------------------------------------------------------------------------------- 1 | package gitconfig 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "os/exec" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // SectionName will be the section header in the .git/config file 15 | const sectionName = "pair" 16 | const sectionKey = sectionName + "." + "coauthor" 17 | 18 | // TODO: make a helper that converts a string of usernames & emails to coauthor structs 19 | // also should validate the strings 20 | // for _, line := range splitOutput { 21 | // lineSlice := strings.Split(line, " ") 22 | // coauthor := Coauthor{Name: lineSlice[0], Email: lineSlice[1]} 23 | // coauthors = append(coauthors, &coauthor) 24 | // } 25 | 26 | // ContainsSection checks if pair section exists 27 | func ContainsSection() (bool, error) { 28 | filepath, cmderr := GitDir() 29 | // Question: now that Im return error instead of cmderrors 30 | // can't just pass a regular error, need to pass a CmdError type 31 | if CheckCmdError(cmderr) != nil { 32 | return false, cmderr 33 | } 34 | 35 | filepath = filepath + "/config" 36 | 37 | file, err := os.Open(filepath) 38 | if err != nil { 39 | errMsg := "Could not open file/filepath: " + filepath 40 | return false, errors.Wrap(err, errMsg) 41 | } 42 | scanner := bufio.NewScanner(file) 43 | re := regexp.MustCompile("^\\[" + sectionName + "\\]") 44 | for scanner.Scan() { 45 | line := scanner.Text() 46 | match := re.MatchString(line) 47 | if match { 48 | return true, nil 49 | } 50 | } 51 | return false, nil 52 | } 53 | 54 | // GitDir gets the file path to the where the git dir is located 55 | func GitDir() (string, error) { 56 | out, cmderr := RootDir() 57 | if CheckCmdError(cmderr) != nil { 58 | return "", cmderr 59 | } 60 | out = out + "/.git" 61 | 62 | return out, nil 63 | } 64 | 65 | func RootDir() (string, error) { 66 | //TODO: change name to getGitDirectory 67 | // Note:You can use: git rev-parse --git-dir 68 | out, err := RunCmd([]string{"git", "rev-parse", "--git-dir"}) 69 | isRelativePath := (out == ".git") 70 | 71 | if isRelativePath { 72 | // TODO: can use the filepath package to get dir instead 73 | out, err = RunCmd([]string{"pwd"}) 74 | } 75 | 76 | return out, err 77 | } 78 | 79 | // RunGitConfigCmd - Executes "git config" related commands 80 | func RunGitConfigCmd(flags string, val string) (string, error) { 81 | return RunCmd([]string{"git", "config", flags, sectionKey, val}) 82 | } 83 | 84 | // RunCmd a wrapper for exec 85 | func RunCmd(cmdArgs []string) (string, error) { 86 | if len(cmdArgs) == 0 { 87 | return "", CmdError{Message: "Need at least one argument to run cmd"} 88 | } 89 | 90 | var out bytes.Buffer 91 | var stderr bytes.Buffer 92 | cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) 93 | cmd.Stderr = &stderr 94 | cmd.Stdout = &out 95 | err := cmd.Run() 96 | 97 | if err != nil { 98 | cmdErr := CmdError{ 99 | Message: "Failed to run \"RunCmd()\" with arguments " + strings.Join(cmdArgs, ", "), 100 | ExitCode: exitCode(err)} 101 | 102 | return "", cmdErr 103 | } 104 | 105 | return strings.Trim(out.String(), "\n"), nil 106 | } 107 | 108 | // gets the exit code from a exec.Cmd 109 | func exitCode(err error) int { 110 | if exitError, ok := err.(*exec.ExitError); ok { 111 | return exitError.ExitCode() 112 | } 113 | return 0 114 | } 115 | -------------------------------------------------------------------------------- /gitpair.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/thechutrain/git-pair/actions" 9 | "gopkg.in/urfave/cli.v1" 10 | ) 11 | 12 | func main() { 13 | // Note: prepare-commit-msg is a func called internally so we don't want to expose it to the cli 14 | prepareCommit := len(os.Args) > 1 && os.Args[1] == "_modify-commit-msg" 15 | if prepareCommit { 16 | actions.ModifyCommitMsg(os.Args) 17 | return 18 | } 19 | 20 | // Registers the cli package for autocompletion of bash commands 21 | app := cli.NewApp() 22 | app.EnableBashCompletion = true 23 | app.Commands = []cli.Command{ 24 | { 25 | Name: "init", 26 | Action: func(c *cli.Context) error { 27 | actions.Init(c.Args()) 28 | 29 | return nil 30 | }, 31 | }, 32 | { 33 | Name: "add", 34 | Aliases: []string{"a"}, 35 | Usage: "GitHubHandle", 36 | Action: func(c *cli.Context) error { 37 | err := actions.Add(c.Args()) 38 | if err != nil { 39 | log.Fatal(errors.Cause(err)) 40 | } 41 | 42 | return nil 43 | }, 44 | BashComplete: func(c *cli.Context) { 45 | if c.NArg() > 0 { 46 | return 47 | } 48 | 49 | // TODO: get a list of past collaborators! to populate the script 50 | // Get 51 | // for _, user := range getCollaborators() { 52 | // fmt.Println(user) 53 | // } 54 | }, 55 | }, 56 | { 57 | Name: "remove", 58 | Aliases: []string{"rm"}, 59 | Action: func(c *cli.Context) error { 60 | actions.Remove(c.Args()) 61 | return nil 62 | }, 63 | }, 64 | { 65 | Name: "stop", 66 | Aliases: []string{"reset"}, 67 | Action: func(c *cli.Context) error { 68 | actions.RemoveAll(c.Args()) 69 | return nil 70 | }, 71 | }, 72 | { 73 | Name: "status", 74 | Aliases: []string{"s"}, 75 | Action: func(c *cli.Context) error { 76 | actions.Status(c.Args()) 77 | 78 | return nil 79 | }, 80 | }, 81 | } 82 | 83 | err := app.Run(os.Args) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## git-pair 🍐🍐 2 | 3 | 4 | 5 | > Makes it easy to add co-authors to your project 6 | 7 | # Demo 8 | 9 | ![demo](.notes/git-pair-gif.gif) 10 | 11 | # Installing 12 | 13 | ``` 14 | $ go install git-pair.go 15 | ``` 16 | 17 | Make sure that your `go/bin` directory is in your path. You can check this by `echo $PATH`. If the go directory is not there then go into your zsh or bash file and add that. 18 | 19 | Add the following to your `.zshrc` file: 20 | 21 | ``` 22 | PROG=pair source $PATH/to/autocomplete/bash_autocomplete 23 | ``` 24 | 25 | # Usage 26 | 27 | To initiate a pairing session just type: 28 | 29 | ``` 30 | pair add GITHUB_USERNAME GITHUB_EMAIL 31 | ``` 32 | 33 | To stop pairing with everyone: 34 | 35 | ``` 36 | pair stop 37 | ``` 38 | 39 | To see if you are currently pairing with anyone: 40 | 41 | ``` 42 | pair status 43 | ``` 44 | 45 | # Description 46 | 47 | This cli uses the `prepare-commit-msg` git hook to append co-authors to your commit messages for you. 48 | 49 | Example: 50 | 51 | ``` 52 | wip - git commit message, pairing is fun! 53 | 54 | # Added by 🍐 55 | Co-authored-by: pairpal 56 | 57 | # Please enter the commit message for your changes. Lines starting 58 | # with '#' will be ignored, and an empty message aborts the commit. 59 | # 60 | # Date: Tue Jun 4 17:22:45 2019 -0400 61 | # 62 | # On branch master 63 | # Your branch is up-to-date with 'origin/master'. 64 | #¬ 65 | # Changes to be committed: 66 | # new file: .gitignore¬ 67 | # modified: actions/remove.go¬ 68 | # modified: notes.md¬ 69 | # modified: readme.md 70 | # 71 | # Changes not staged for commit: 72 | # modified: readme.md 73 | ``` 74 | --------------------------------------------------------------------------------