Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Hooks. git-open-pull provides the ability to modify an issue template (preProces
git-open-pull will also look for the following environment variables, which will take precendence over values found in the config file.

```
GITOPENPULL_TOKEN
GITOPENPULL_TOKEN || GITHUB_TOKEN
GITOPENPULL_USER
GITOPENPULL_BASE_ACCOUNT
GITOPENPULL_BASE_REPO
Expand Down
99 changes: 99 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
name: git-open-pull
description: "Create a GitHub Pull Request using git-open-pull. Use when: opening a PR, submitting code for review, creating a draft PR, publishing a branch as a pull request. git-open-pull creates an issue, renames the branch to include the issue number, pushes the branch, and converts the issue to a PR."
argument-hint: "Optionally specify a title, labels, description file, or whether to create as a draft"
---

# git-open-pull Skill

`git-open-pull` converts the current git branch into a GitHub pull request by:
1. Opening or creating a GitHub issue (with title, description, and labels)
2. Renaming the local branch to embed the issue number (e.g. `my-feature` → `my-feature_42`)
3. Pushing the renamed branch and converting the issue into a pull request

Use `git-open-pull` instead of `gh pr create` when working in a repository that tracks work via GitHub Issues and encodes the issue number in the branch name.

## When to Use

- The user wants to open a PR for their current branch
- The user has finished a feature or fix and wants to submit it for review
- The user wants to create a draft PR to share work in progress
- The repository uses the convention of embedding issue numbers in branch names

## Procedure

### 1. Check Configuration

Run `git-open-pull --help` first. If required configuration is missing, the help output will list any pre-req information needed (e.g. GitHub username, token, destination account/repo). Set these values via `git config` or environment variables as described in the help output before proceeding.

### 2. Check for Uncommitted Changes

Before running, ensure all changes are committed. `git-open-pull` does not commit changes — it only renames the branch, pushes it, and opens the PR.

### 3. Do Not Push the Branch First

**Do not run `git push` before `git-open-pull`.** The tool renames the local branch to include the issue number before pushing. If you push manually beforehand the remote branch name will not match and the tool will fail.

### 4. Inspect Labels (if using --labels)

Run `git-open-pull --list-labels` to see the exact label names valid for this repository before passing `--labels`.

### 5. Prepare PR Details

Write a good title and description:

**Title**: Use imperative mood, keep it under 72 characters, and describe *what* the PR does (e.g. `Add retry logic for failed API requests`).

**Description file**: Write to a temp file and pass via `--description-file`. Include:
- A short summary of what changed and why
- Any relevant issue references (e.g. `Closes #123` — note: `git-open-pull` converts the issue to a PR, so the issue number is already linked)
- Notable implementation decisions useful for the reviewer

### 6. Run git-open-pull

```sh
git-open-pull \
--interactive=false \
--title "Fix null pointer in login flow" \
--description-file /tmp/pr-description.txt \
--labels bug \
--draft
```

### 7. Confirm Result

On success the tool prints the GitHub issue/PR URL:
```
https://github.com/owner/repo/issues/42
```

Report this URL to the user.

## Flags

| Flag | Description |
|------|-------------|
| `--title` | PR / issue title (required with `--interactive=false`) |
| `--description-file` | Path to a file whose contents become the PR description |
| `--labels` | Comma-separated label names (use `--list-labels` to enumerate valid values) |
| `--draft` | Open the PR in draft mode (default: true) |
| `--interactive` | Set to `false` for non-interactive/agent use |
| `--list-labels` | Print all repository labels and exit |
| `--skill` | Print this skill document and exit |
| `--version` | Print the version and exit |

## Best Practices

### Titles
- Use the imperative mood: `Fix`, `Add`, `Update`, `Remove`, `Refactor` — not `Fixed`, `Adding`, etc.
- Be specific: `Fix null pointer in user login flow` beats `Fix bug`.
- Keep it under 72 characters so it displays cleanly in GitHub and email notifications.

### Descriptions
- Start with a one-sentence summary.
- Explain *why* the change is needed, not just *what* it does — reviewers benefit from context.
- If the change is large, add a brief list of the main files or components touched.

### Draft PRs
- Use `--draft` when the code is not yet ready for formal review (e.g. work in progress, awaiting feedback on approach, CI not yet passing).
- Draft PRs are visible to collaborators but will not show as review-requested until marked ready.
92 changes: 83 additions & 9 deletions git-open-pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
_ "embed"
"flag"
"fmt"
"log"
Expand All @@ -13,10 +14,14 @@ import (
"time"

"github.com/google/go-github/v60/github"
"github.com/jehiah/agentdetection"
"github.com/jehiah/git-open-pull/internal/input"
"golang.org/x/oauth2"
)

//go:embed SKILL.md
var skillDoc string

func RenameBranch(ctx context.Context, branch string, issueNumber int) (string, error) {
branch = fmt.Sprintf("%s_%d", branch, issueNumber)
_, err := RunGit(ctx, "branch", "-m", branch)
Expand Down Expand Up @@ -73,13 +78,49 @@ func GetIssueNumber(ctx context.Context, client *github.Client, settings *Settin
return detected, nil
}

func printUsage(settings *Settings) {
out := flag.CommandLine.Output()
fmt.Fprintln(out, "git-open-pull opens a or creates an issue, renames the local branch to include that issue number, pushes the renamed branch and finally converts the issue into a pull request.")
fmt.Fprintln(out, "Equivalent to 'gh pr create'.")
if settings != nil && settings.User != "" && settings.BaseAccount != "" && settings.BaseRepo != "" {
fmt.Fprintf(out, "By default, code is pushed to %s/%s and the pull request targets %s/%s branch %s.\n", settings.User, settings.BaseRepo, settings.BaseAccount, settings.BaseRepo, settings.BaseBranch)
}
fmt.Fprintln(out)
if agentdetection.IsAgent() {
fmt.Fprintln(out, "Hint: run --list-labels first to inspect the valid repository labels before passing --labels.")
fmt.Fprintln(out, "Do not push code prior to running this command, as the branch may be renamed to include the issue number.")
fmt.Fprintln(out, "Run --skill for full agent usage documentation, or redirect to a file: git-open-pull --skill > SKILL.md")
if settings != nil {
if hints := settings.RequiredHints(); len(hints) > 0 {
fmt.Fprintln(out)
fmt.Fprintln(out, "Required configuration missing:")
for _, h := range hints {
fmt.Fprintf(out, " - %s\n", h)
}
}
}
fmt.Fprintln(out)
}
fmt.Fprintf(out, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
}

func main() {
ctx := context.Background()
preSettings, _ := readSettingsConfig(ctx)

flag.Usage = func() {
printUsage(preSettings)
}

description := flag.String("description-file", "", "Path to PR description file")
listLabels := flag.Bool("list-labels", false, "List available issue labels and exit")
skill := flag.Bool("skill", false, "Print agent skill documentation and exit")
labels := flag.String("labels", "", "Comma separated PR Labels")
title := flag.String("title", "", "PR Title")
interactive := flag.Bool("interactive", true, "Toggles interactive mode")
version := flag.Bool("version", false, "Prints current version")
draft := flag.Bool("draft", false, "Open PR in draft mode")
draft := flag.Bool("draft", true, "Open PR in draft mode")

flag.Parse()

Expand All @@ -88,12 +129,42 @@ func main() {
os.Exit(0)
}

ctx := context.Background()
if *skill {
fmt.Print(skillDoc)
return
}

// Load and initialize settings
settings, err := LoadSettings(ctx)
if err != nil {
log.Fatal(err)
var settings *Settings
var err error
if *interactive {
settings, err = LoadSettings(ctx)
if err != nil {
log.Fatal(err)
}
} else {
settings, err = readSettingsConfig(ctx)
if err != nil {
log.Fatal(err)
}
if hints := settings.RequiredHints(); len(hints) > 0 {
for _, h := range hints {
fmt.Fprintln(os.Stderr, h)
}
os.Exit(1)
}
}

client := SetupClient(ctx, settings)

if *listLabels {
labels, err := Labels(ctx, client, settings)
if err != nil {
log.Fatal(err)
}
for _, label := range labels {
fmt.Println(label)
}
return
}

var labelSlice []string
Expand Down Expand Up @@ -134,8 +205,6 @@ func main() {
}
detectedIssueNumber := DetectIssueNumber(branch)

client := SetupClient(ctx, settings)

// create issue if needed
issueNumber, err := GetIssueNumber(ctx, client, settings, detectedIssueNumber, *interactive, *title, descriptionString, labelSlice)
if err != nil {
Expand Down Expand Up @@ -226,13 +295,18 @@ func main() {
if strings.ToLower(yn) != "y" {
log.Fatal("exiting")
}
yn, err = input.Ask("Open as draft [Y/n]", "")
}

if *interactive {
yn, err := input.Ask("Open as draft [Y/n]", "")
if err != nil {
log.Fatal(err)
}
switch yn {
case "", "Y", "y":
*draft = true
case "n", "N":
*draft = false
}
}

Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module github.com/jehiah/git-open-pull

go 1.23.0
go 1.26.2

require (
github.com/google/go-github/v60 v60.0.0
github.com/jehiah/agentdetection v0.0.0-20260504175341-4747d1e40782
golang.org/x/oauth2 v0.30.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCy
github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/jehiah/agentdetection v0.0.0-20260504175341-4747d1e40782 h1:EV4iRaivD9whutiTtYjj3EAYAyojBPMXdskIcEpKeYk=
github.com/jehiah/agentdetection v0.0.0-20260504175341-4747d1e40782/go.mod h1:Mc3j1GxXS1r1HJ42lBPejePFX5Xjz+5mVo/7DYXyhDc=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Loading
Loading