Skip to content

Commit c7c8941

Browse files
authored
Add PR Generation and Creation (#1)
* 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
1 parent 45428db commit c7c8941

27 files changed

+575
-112
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ bin/**
33
dist/**
44
*.bleve
55
**.DS_Store**
6+
.env

README.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,23 @@ First, you need to create an OpenAI API Key. If you do not already have an OpenA
3636
Once you have an API key, you can log in to ottodocs by running the following command:
3737

3838
```sh
39-
otto login
39+
otto config --apikey $OPENAI_API_KEY
4040
```
4141

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

4444
```sh
45-
otto login --apikey $OPENAI_API_KEY
45+
otto config --model $MODEL_NAME
4646
```
4747

48+
You can add a GitHub Personal Access Token for opening PRs by running:
49+
50+
```sh
51+
otto config --token $GITHUB_TOKEN
52+
```
53+
54+
Make sure that your access token has the `repo` scope.
55+
4856
Once that is complete, you can start generating documentation by running the following command:
4957

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

6876
```sh
69-
otto commit
77+
otto commit # optionally add --push to push to remote
78+
```
79+
80+
Generate a pull request:
81+
82+
```sh
83+
# make sure you are creating the PR on the correct base branch
84+
otto pr -b main # optionally add --publish to publish the Pull Request
7085
```
7186

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

7893
## Usage
7994

80-
For detailed usage instructions, please refer to the [documentation](docs/otto.md).
95+
For detailed usage instructions, please refer to the [documentation](https://ottodocs.chand1012.dev/docs/usage/otto).

cmd/config.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright © 2023 Chandler <chandler@chand1012.dev>
3+
*/
4+
package cmd
5+
6+
import (
7+
"os"
8+
9+
"github.com/chand1012/ottodocs/pkg/config"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
// configCmd represents the config command
14+
var configCmd = &cobra.Command{
15+
Use: "config",
16+
Short: "Configures ottodocs",
17+
Long: `Configures ottodocs. Allows user to specify API Keys and the model with a single command.`,
18+
Run: func(cmd *cobra.Command, args []string) {
19+
// load the config
20+
c, err := config.Load()
21+
if err != nil {
22+
log.Errorf("Error loading config: %s", err)
23+
os.Exit(1)
24+
}
25+
26+
// if none of the config options are provided, print a warning
27+
if apiKey == "" && model == "" && ghToken == "" {
28+
log.Warn("No configuration options provided")
29+
return
30+
}
31+
32+
// if the api key is provided, set it
33+
if apiKey != "" {
34+
log.Info("Setting API key...")
35+
c.APIKey = apiKey
36+
}
37+
38+
// if the model is provided, set it
39+
if model != "" {
40+
log.Info("Setting model...")
41+
c.Model = model
42+
}
43+
44+
// if the gh token is provided, set it
45+
if ghToken != "" {
46+
log.Info("Setting GitHub token...")
47+
c.GHToken = ghToken
48+
}
49+
50+
// save the config
51+
err = c.Save()
52+
if err != nil {
53+
log.Errorf("Error saving config: %s", err)
54+
os.Exit(1)
55+
}
56+
57+
log.Info("Configuration saved successfully!")
58+
},
59+
}
60+
61+
func init() {
62+
RootCmd.AddCommand(configCmd)
63+
64+
// get api key
65+
configCmd.Flags().StringVarP(&apiKey, "apikey", "k", "", "API key to add to configuration")
66+
// get model
67+
configCmd.Flags().StringVarP(&model, "model", "m", "", "Model to use for documentation")
68+
// set gh token
69+
configCmd.Flags().StringVarP(&ghToken, "ghtoken", "t", "", "GitHub token to use for documentation")
70+
}

cmd/login.go

Lines changed: 0 additions & 48 deletions
This file was deleted.

cmd/pr.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
Copyright © 2023 Chandler <chandler@chand1012.dev>
3+
*/
4+
package cmd
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
g "github.com/chand1012/git2gpt/prompt"
12+
"github.com/chand1012/ottodocs/pkg/ai"
13+
"github.com/chand1012/ottodocs/pkg/calc"
14+
"github.com/chand1012/ottodocs/pkg/config"
15+
"github.com/chand1012/ottodocs/pkg/gh"
16+
"github.com/chand1012/ottodocs/pkg/git"
17+
"github.com/chand1012/ottodocs/pkg/utils"
18+
l "github.com/charmbracelet/log"
19+
"github.com/spf13/cobra"
20+
)
21+
22+
// prCmd represents the pr command
23+
var prCmd = &cobra.Command{
24+
Use: "pr",
25+
Short: "Generate a pull request",
26+
Long: `The "pr" command generates a pull request by combining commit messages, a title, and the git diff between branches.
27+
Requires Git to be installed on the system. If a title is not provided, one will be generated.`,
28+
PreRun: func(cmd *cobra.Command, args []string) {
29+
if verbose {
30+
log.SetLevel(l.DebugLevel)
31+
}
32+
},
33+
Run: func(cmd *cobra.Command, args []string) {
34+
c, err := config.Load()
35+
if err != nil {
36+
log.Errorf("Error loading config: %s", err)
37+
os.Exit(1)
38+
}
39+
40+
log.Info("Generating PR...")
41+
42+
currentBranch, err := git.GetBranch()
43+
if err != nil {
44+
log.Errorf("Error getting current branch: %s", err)
45+
os.Exit(1)
46+
}
47+
48+
log.Debugf("Current branch: %s", currentBranch)
49+
50+
if base == "" {
51+
// ask them for the base branch
52+
fmt.Print("Please provide a base branch: ")
53+
fmt.Scanln(&base)
54+
}
55+
56+
logs, err := git.LogBetween(base, currentBranch)
57+
if err != nil {
58+
log.Errorf("Error getting logs: %s", err)
59+
os.Exit(1)
60+
}
61+
62+
log.Debugf("Got %d logs", len(strings.Split(logs, "\n")))
63+
64+
if title == "" {
65+
// generate the title
66+
log.Debug("Generating title...")
67+
title, err = ai.PRTitle(logs, c)
68+
if err != nil {
69+
log.Errorf("Error generating title: %s", err)
70+
os.Exit(1)
71+
}
72+
}
73+
74+
log.Debugf("Title: %s", title)
75+
// get the diff
76+
diff, err := git.GetBranchDiff(base, currentBranch)
77+
if err != nil {
78+
log.Errorf("Error getting diff: %s", err)
79+
os.Exit(1)
80+
}
81+
82+
log.Debug("Calculating Diff Tokens...")
83+
// count the diff tokens
84+
diffTokens, err := calc.PreciseTokens(diff)
85+
if err != nil {
86+
log.Errorf("Error counting diff tokens: %s", err)
87+
os.Exit(1)
88+
}
89+
90+
log.Debug("Calculating Title Tokens...")
91+
titleTokens, err := calc.PreciseTokens(title)
92+
if err != nil {
93+
log.Errorf("Error counting title tokens: %s", err)
94+
os.Exit(1)
95+
}
96+
97+
if diffTokens == 0 {
98+
log.Warn("Diff is empty!")
99+
}
100+
101+
if titleTokens == 0 {
102+
log.Warn("Title is empty!")
103+
}
104+
105+
log.Debugf("Diff tokens: %d", diffTokens)
106+
log.Debugf("Title tokens: %d", titleTokens)
107+
var prompt string
108+
if diffTokens+titleTokens > calc.GetMaxTokens(c.Model) {
109+
log.Debug("Diff is large, creating compressed diff and using logs and title")
110+
prompt = "Title: " + title + "\n\nGit logs: " + logs
111+
// get a list of the changed files
112+
files, err := git.GetChangedFilesBranches(base, currentBranch)
113+
if err != nil {
114+
log.Errorf("Error getting changed files: %s", err)
115+
os.Exit(1)
116+
}
117+
ignoreFiles := g.GenerateIgnoreList(".", ".gptignore", false)
118+
for _, file := range files {
119+
if utils.Contains(ignoreFiles, file) {
120+
log.Debugf("Ignoring file: %s", file)
121+
continue
122+
}
123+
log.Debug("Compressing diff for file: " + file)
124+
// get the file's diff
125+
fileDiff, err := git.GetFileDiffBranches(base, currentBranch, file)
126+
if err != nil {
127+
log.Errorf("Error getting file diff: %s", err)
128+
continue
129+
}
130+
// compress the diff with ChatGPT
131+
compressedDiff, err := ai.CompressDiff(fileDiff, c)
132+
if err != nil {
133+
log.Errorf("Error compressing diff: %s", err)
134+
continue
135+
}
136+
prompt += "\n\n" + file + ":\n" + compressedDiff
137+
}
138+
} else {
139+
log.Debug("Diff is small enough, using logs, title, and diff")
140+
prompt = "Title: " + title + "\n\nGit logs: " + logs + "\n\nGit diff: " + diff
141+
}
142+
143+
body, err := ai.PRBody(prompt, c)
144+
if err != nil {
145+
log.Errorf("Error generating PR body: %s", err)
146+
os.Exit(1)
147+
}
148+
149+
if !push {
150+
fmt.Println("Title: ", title)
151+
fmt.Println("Body: ", body)
152+
os.Exit(0)
153+
}
154+
155+
// get the origin remote
156+
origin, err := git.GetRemote(remote)
157+
if err != nil {
158+
log.Errorf("Error getting remote: %s", err)
159+
os.Exit(1)
160+
}
161+
162+
owner, repo, err := git.ExtractOriginInfo(origin)
163+
if err != nil {
164+
log.Errorf("Error extracting origin info: %s", err)
165+
os.Exit(1)
166+
}
167+
168+
// print the origin and repo if debug is enabled
169+
log.Debugf("Origin: %s", origin)
170+
log.Debugf("Owner: %s", owner)
171+
log.Debugf("Repo: %s", repo)
172+
173+
data := make(map[string]string)
174+
data["title"] = title
175+
data["body"] = body + "\n\n" + c.Signature
176+
data["head"] = currentBranch
177+
data["base"] = base
178+
179+
log.Info("Opening pull request...")
180+
prNumber, err := gh.OpenPullRequest(data, owner, repo, c)
181+
if err != nil {
182+
log.Errorf("Error opening pull request: %s", err)
183+
os.Exit(1)
184+
}
185+
186+
fmt.Printf("Successfully opened pull request: %s\n", title)
187+
// link to the pull request
188+
fmt.Printf("https://github.com/%s/%s/pull/%d\n", owner, repo, prNumber)
189+
},
190+
}
191+
192+
func init() {
193+
RootCmd.AddCommand(prCmd)
194+
195+
prCmd.Flags().StringVarP(&base, "base", "b", "", "Base branch to create the pull request against")
196+
prCmd.Flags().StringVarP(&title, "title", "t", "", "Title of the pull request")
197+
prCmd.Flags().StringVarP(&remote, "remote", "r", "origin", "Remote for creating the pull request. Only works with GitHub.")
198+
prCmd.Flags().BoolVarP(&push, "publish", "p", false, "Create the pull request. Must have a remote named \"origin\"")
199+
prCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output")
200+
}

cmd/setModel.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Copyright © 2023 Chandler <chandler@chand1012.dev>
44
package cmd
55

66
import (
7+
"os"
8+
79
"github.com/spf13/cobra"
810

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

0 commit comments

Comments
 (0)