Skip to content

Commit

Permalink
Add PR Generation and Creation (#1)
Browse files Browse the repository at this point in the history
* Add PR command

* Add PR generation and additional git helper functions

* Update pr command description and add Git requirement

* Add .env to .gitignore and improve pull request handling

* Updated diff function and added empty warnings

* Update copyright, remove login command, add signature

* Update docs, add otto config, remove otto login

* Update config, commit, and PR commands in README
  • Loading branch information
chand1012 authored Apr 24, 2023
1 parent 45428db commit c7c8941
Show file tree
Hide file tree
Showing 27 changed files with 575 additions and 112 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ bin/**
dist/**
*.bleve
**.DS_Store**
.env
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,23 @@ First, you need to create an OpenAI API Key. If you do not already have an OpenA
Once you have an API key, you can log in to ottodocs by running the following command:

```sh
otto login
otto config --apikey $OPENAI_API_KEY
```

Optionally you can pass the API key as an argument to the command:
You can set the model to use by running:

```sh
otto login --apikey $OPENAI_API_KEY
otto config --model $MODEL_NAME
```

You can add a GitHub Personal Access Token for opening PRs by running:

```sh
otto config --token $GITHUB_TOKEN
```

Make sure that your access token has the `repo` scope.

Once that is complete, you can start generating documentation by running the following command:

```sh
Expand All @@ -66,7 +74,14 @@ otto ask . -q "What does LoadFile do differently than ReadFile?"
Generate a commit message:

```sh
otto commit
otto commit # optionally add --push to push to remote
```

Generate a pull request:

```sh
# make sure you are creating the PR on the correct base branch
otto pr -b main # optionally add --publish to publish the Pull Request
```

Ask it about commands:
Expand All @@ -77,4 +92,4 @@ otto cmd -q "what is the command to add a remote?"

## Usage

For detailed usage instructions, please refer to the [documentation](docs/otto.md).
For detailed usage instructions, please refer to the [documentation](https://ottodocs.chand1012.dev/docs/usage/otto).
70 changes: 70 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
Copyright © 2023 Chandler <[email protected]>
*/
package cmd

import (
"os"

"github.com/chand1012/ottodocs/pkg/config"
"github.com/spf13/cobra"
)

// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
Short: "Configures ottodocs",
Long: `Configures ottodocs. Allows user to specify API Keys and the model with a single command.`,
Run: func(cmd *cobra.Command, args []string) {
// load the config
c, err := config.Load()
if err != nil {
log.Errorf("Error loading config: %s", err)
os.Exit(1)
}

// if none of the config options are provided, print a warning
if apiKey == "" && model == "" && ghToken == "" {
log.Warn("No configuration options provided")
return
}

// if the api key is provided, set it
if apiKey != "" {
log.Info("Setting API key...")
c.APIKey = apiKey
}

// if the model is provided, set it
if model != "" {
log.Info("Setting model...")
c.Model = model
}

// if the gh token is provided, set it
if ghToken != "" {
log.Info("Setting GitHub token...")
c.GHToken = ghToken
}

// save the config
err = c.Save()
if err != nil {
log.Errorf("Error saving config: %s", err)
os.Exit(1)
}

log.Info("Configuration saved successfully!")
},
}

func init() {
RootCmd.AddCommand(configCmd)

// get api key
configCmd.Flags().StringVarP(&apiKey, "apikey", "k", "", "API key to add to configuration")
// get model
configCmd.Flags().StringVarP(&model, "model", "m", "", "Model to use for documentation")
// set gh token
configCmd.Flags().StringVarP(&ghToken, "ghtoken", "t", "", "GitHub token to use for documentation")
}
48 changes: 0 additions & 48 deletions cmd/login.go

This file was deleted.

200 changes: 200 additions & 0 deletions cmd/pr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
Copyright © 2023 Chandler <[email protected]>
*/
package cmd

import (
"fmt"
"os"
"strings"

g "github.com/chand1012/git2gpt/prompt"
"github.com/chand1012/ottodocs/pkg/ai"
"github.com/chand1012/ottodocs/pkg/calc"
"github.com/chand1012/ottodocs/pkg/config"
"github.com/chand1012/ottodocs/pkg/gh"
"github.com/chand1012/ottodocs/pkg/git"
"github.com/chand1012/ottodocs/pkg/utils"
l "github.com/charmbracelet/log"
"github.com/spf13/cobra"
)

// prCmd represents the pr command
var prCmd = &cobra.Command{
Use: "pr",
Short: "Generate a pull request",
Long: `The "pr" command generates a pull request by combining commit messages, a title, and the git diff between branches.
Requires Git to be installed on the system. If a title is not provided, one will be generated.`,
PreRun: func(cmd *cobra.Command, args []string) {
if verbose {
log.SetLevel(l.DebugLevel)
}
},
Run: func(cmd *cobra.Command, args []string) {
c, err := config.Load()
if err != nil {
log.Errorf("Error loading config: %s", err)
os.Exit(1)
}

log.Info("Generating PR...")

currentBranch, err := git.GetBranch()
if err != nil {
log.Errorf("Error getting current branch: %s", err)
os.Exit(1)
}

log.Debugf("Current branch: %s", currentBranch)

if base == "" {
// ask them for the base branch
fmt.Print("Please provide a base branch: ")
fmt.Scanln(&base)
}

logs, err := git.LogBetween(base, currentBranch)
if err != nil {
log.Errorf("Error getting logs: %s", err)
os.Exit(1)
}

log.Debugf("Got %d logs", len(strings.Split(logs, "\n")))

if title == "" {
// generate the title
log.Debug("Generating title...")
title, err = ai.PRTitle(logs, c)
if err != nil {
log.Errorf("Error generating title: %s", err)
os.Exit(1)
}
}

log.Debugf("Title: %s", title)
// get the diff
diff, err := git.GetBranchDiff(base, currentBranch)
if err != nil {
log.Errorf("Error getting diff: %s", err)
os.Exit(1)
}

log.Debug("Calculating Diff Tokens...")
// count the diff tokens
diffTokens, err := calc.PreciseTokens(diff)
if err != nil {
log.Errorf("Error counting diff tokens: %s", err)
os.Exit(1)
}

log.Debug("Calculating Title Tokens...")
titleTokens, err := calc.PreciseTokens(title)
if err != nil {
log.Errorf("Error counting title tokens: %s", err)
os.Exit(1)
}

if diffTokens == 0 {
log.Warn("Diff is empty!")
}

if titleTokens == 0 {
log.Warn("Title is empty!")
}

log.Debugf("Diff tokens: %d", diffTokens)
log.Debugf("Title tokens: %d", titleTokens)
var prompt string
if diffTokens+titleTokens > calc.GetMaxTokens(c.Model) {
log.Debug("Diff is large, creating compressed diff and using logs and title")
prompt = "Title: " + title + "\n\nGit logs: " + logs
// get a list of the changed files
files, err := git.GetChangedFilesBranches(base, currentBranch)
if err != nil {
log.Errorf("Error getting changed files: %s", err)
os.Exit(1)
}
ignoreFiles := g.GenerateIgnoreList(".", ".gptignore", false)
for _, file := range files {
if utils.Contains(ignoreFiles, file) {
log.Debugf("Ignoring file: %s", file)
continue
}
log.Debug("Compressing diff for file: " + file)
// get the file's diff
fileDiff, err := git.GetFileDiffBranches(base, currentBranch, file)
if err != nil {
log.Errorf("Error getting file diff: %s", err)
continue
}
// compress the diff with ChatGPT
compressedDiff, err := ai.CompressDiff(fileDiff, c)
if err != nil {
log.Errorf("Error compressing diff: %s", err)
continue
}
prompt += "\n\n" + file + ":\n" + compressedDiff
}
} else {
log.Debug("Diff is small enough, using logs, title, and diff")
prompt = "Title: " + title + "\n\nGit logs: " + logs + "\n\nGit diff: " + diff
}

body, err := ai.PRBody(prompt, c)
if err != nil {
log.Errorf("Error generating PR body: %s", err)
os.Exit(1)
}

if !push {
fmt.Println("Title: ", title)
fmt.Println("Body: ", body)
os.Exit(0)
}

// get the origin remote
origin, err := git.GetRemote(remote)
if err != nil {
log.Errorf("Error getting remote: %s", err)
os.Exit(1)
}

owner, repo, err := git.ExtractOriginInfo(origin)
if err != nil {
log.Errorf("Error extracting origin info: %s", err)
os.Exit(1)
}

// print the origin and repo if debug is enabled
log.Debugf("Origin: %s", origin)
log.Debugf("Owner: %s", owner)
log.Debugf("Repo: %s", repo)

data := make(map[string]string)
data["title"] = title
data["body"] = body + "\n\n" + c.Signature
data["head"] = currentBranch
data["base"] = base

log.Info("Opening pull request...")
prNumber, err := gh.OpenPullRequest(data, owner, repo, c)
if err != nil {
log.Errorf("Error opening pull request: %s", err)
os.Exit(1)
}

fmt.Printf("Successfully opened pull request: %s\n", title)
// link to the pull request
fmt.Printf("https://github.com/%s/%s/pull/%d\n", owner, repo, prNumber)
},
}

func init() {
RootCmd.AddCommand(prCmd)

prCmd.Flags().StringVarP(&base, "base", "b", "", "Base branch to create the pull request against")
prCmd.Flags().StringVarP(&title, "title", "t", "", "Title of the pull request")
prCmd.Flags().StringVarP(&remote, "remote", "r", "origin", "Remote for creating the pull request. Only works with GitHub.")
prCmd.Flags().BoolVarP(&push, "publish", "p", false, "Create the pull request. Must have a remote named \"origin\"")
prCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output")
}
3 changes: 3 additions & 0 deletions cmd/setModel.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Copyright © 2023 Chandler <[email protected]>
package cmd

import (
"os"

"github.com/spf13/cobra"

"github.com/chand1012/ottodocs/pkg/config"
Expand All @@ -25,6 +27,7 @@ See here for more information: https://platform.openai.com/docs/models/model-end
c, err := config.Load()
if err != nil {
log.Errorf("Error loading config: %s", err)
os.Exit(1)
}
if !utils.Contains(VALID_MODELS, model) {
log.Errorf("Invalid model: %s", model)
Expand Down
Loading

0 comments on commit c7c8941

Please sign in to comment.