Skip to content

Commit

Permalink
feat!: ref management and commit trailer handling (#53)
Browse files Browse the repository at this point in the history
* feat: add `ref` verb supporting reference copying

* docs: update README

* feat!: refactor commit trailers

* enable support for setting multiple arbitrary commit trailers via the
  `--trailer` flag: `--trailer "Key=Value" --trailer "Key2=Value2"`, or
  `env GHUP_TRAILER=$(jo Key=Value Key2=Value2)`.
* BREAKING CHANGE: the `--trailer.key`, `--trailer.user` and
  `--trailer.email` flags have been renamed to `--author.trailer`,
  `--user.name` and `--user.email` respectively.
  Environment-based overrides still support the old names.

* fix: document and extend `info` verb
  • Loading branch information
isometry authored Sep 23, 2024
1 parent e0b979a commit d738072
Show file tree
Hide file tree
Showing 15 changed files with 1,334 additions and 180 deletions.
239 changes: 182 additions & 57 deletions README.md

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions cmd/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,33 @@ var contentCmd = &cobra.Command{
}

func init() {
contentCmd.PersistentFlags().Bool("create-branch", true, "create missing target branch")
viper.BindPFlag("create-branch", contentCmd.PersistentFlags().Lookup("create-branch"))
contentCmd.Flags().Bool("create-branch", true, "create missing target branch")
viper.BindPFlag("create-branch", contentCmd.Flags().Lookup("create-branch"))
viper.BindEnv("create-branch", "GHUP_CREATE_BRANCH")

contentCmd.PersistentFlags().String("pr-title", "", "create pull request iff target branch is created and title is specified")
viper.BindPFlag("pr-title", contentCmd.PersistentFlags().Lookup("pr-title"))
contentCmd.Flags().String("pr-title", "", "create pull request iff target branch is created and title is specified")
viper.BindPFlag("pr-title", contentCmd.Flags().Lookup("pr-title"))
viper.BindEnv("pr-title", "GHUP_PR_TITLE")

contentCmd.PersistentFlags().String("pr-body", "", "pull request body")
viper.BindPFlag("pr-body", contentCmd.PersistentFlags().Lookup("pr-body"))
contentCmd.Flags().String("pr-body", "", "pull request body")
viper.BindPFlag("pr-body", contentCmd.Flags().Lookup("pr-body"))
viper.BindEnv("pr-body", "GHUP_PR_BODY")

contentCmd.PersistentFlags().Bool("pr-draft", false, "create pull request in draft mode")
viper.BindPFlag("pr-draft", contentCmd.PersistentFlags().Lookup("pr-draft"))
contentCmd.Flags().Bool("pr-draft", false, "create pull request in draft mode")
viper.BindPFlag("pr-draft", contentCmd.Flags().Lookup("pr-draft"))
viper.BindEnv("pr-draft", "GHUP_PR_DRAFT")

contentCmd.PersistentFlags().String("base-branch", "", `base branch name (default: "[remote-default-branch])"`)
viper.BindPFlag("base-branch", contentCmd.PersistentFlags().Lookup("base-branch"))
contentCmd.Flags().String("base-branch", "", `base branch `+"`name`"+` (default: "[remote-default-branch])"`)
viper.BindPFlag("base-branch", contentCmd.Flags().Lookup("base-branch"))
viper.BindEnv("base-branch", "GHUP_BASE_BRANCH")

contentCmd.Flags().StringP("separator", "s", ":", "file-spec separator")
viper.BindPFlag("separator", contentCmd.Flags().Lookup("separator"))

contentCmd.Flags().StringSliceP("update", "u", []string{}, "file-spec to update")
contentCmd.Flags().StringSliceP("update", "u", []string{}, "`file-spec` to update")
viper.BindPFlag("update", contentCmd.Flags().Lookup("update"))

contentCmd.Flags().StringSliceP("delete", "d", []string{}, "file-path to delete")
contentCmd.Flags().StringSliceP("delete", "d", []string{}, "`file-path` to delete")
viper.BindPFlag("delete", contentCmd.Flags().Lookup("delete"))

contentCmd.Flags().SortFlags = false
Expand Down Expand Up @@ -172,7 +172,7 @@ func runContentCmd(cmd *cobra.Command, args []string) (err error) {
}
log.Debugf("CreateCommitOnBranchInput: %+v", input)

_, commitUrl, err := client.CommitOnBranchV4(input)
_, commitUrl, err := client.CreateCommitOnBranchV4(input)
if err != nil {
return errors.Wrap(err, "CommitOnBranchV4")
}
Expand Down
36 changes: 17 additions & 19 deletions cmd/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ package cmd
import (
"fmt"

"github.com/nexthink-oss/ghup/internal/remote"
"github.com/nexthink-oss/ghup/internal/util"
"github.com/shurcooL/githubv4"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)

type info struct {
HasToken bool `yaml:"hasToken"`
Trailer string `yaml:"trailer,omitempty"`
Owner string
Repository string
Branch string
Commit string
IsClean bool `yaml:"isClean"`
HasToken bool `yaml:"hasToken"`
Trailers []string `yaml:"trailers,omitempty"`
Owner string
Repository string
Branch string
Commit string
IsClean bool `yaml:"isClean"`
CommitMessage githubv4.CommitMessage `yaml:"commitMessage"`
}

var infoCmd = &cobra.Command{
Expand All @@ -33,11 +35,12 @@ func init() {

func runInfoCmd(cmd *cobra.Command, args []string) (err error) {
i := info{
HasToken: len(viper.GetString("token")) > 0,
Trailer: util.BuildTrailer(),
Owner: owner,
Repository: repo,
Branch: branch,
HasToken: len(viper.GetString("token")) > 0,
Trailers: util.BuildTrailers(),
Owner: owner,
Repository: repo,
Branch: branch,
CommitMessage: remote.CommitMessage(util.BuildCommitMessage()),
}

if localRepo != nil {
Expand All @@ -49,11 +52,6 @@ func runInfoCmd(cmd *cobra.Command, args []string) (err error) {
}
}

m, err := yaml.Marshal(i)
if err != nil {
return err
}

fmt.Print(string(m))
fmt.Print(util.EncodeYAML(&i))
return
}
30 changes: 18 additions & 12 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,42 +72,48 @@ func init() {
viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token"))
viper.BindEnv("token", "GHUP_TOKEN", "GITHUB_TOKEN")

rootCmd.PersistentFlags().StringP("owner", "o", defaultOwner, "repository owner")
rootCmd.PersistentFlags().StringP("owner", "o", defaultOwner, "repository owner `name`")
viper.BindPFlag("owner", rootCmd.PersistentFlags().Lookup("owner"))
viper.BindEnv("owner", "GHUP_OWNER", "GITHUB_OWNER")

rootCmd.PersistentFlags().StringP("repo", "r", defaultRepo, "repository name")
rootCmd.PersistentFlags().StringP("repo", "r", defaultRepo, "repository `name`")
viper.BindPFlag("repo", rootCmd.PersistentFlags().Lookup("repo"))

rootCmd.PersistentFlags().StringP("branch", "b", defaultBranch, "target branch name")
rootCmd.PersistentFlags().StringP("branch", "b", defaultBranch, "target branch `name`")
viper.BindPFlag("branch", rootCmd.PersistentFlags().Lookup("branch"))
viper.BindEnv("branch", "GHUP_BRANCH", "CHANGE_BRANCH", "BRANCH_NAME", "GIT_BRANCH")

rootCmd.PersistentFlags().StringP("message", "m", "Commit via API", "message")
viper.BindPFlag("message", rootCmd.PersistentFlags().Lookup("message"))

rootCmd.PersistentFlags().String("trailer.key", "Co-Authored-By", "key for commit trailer (blank to disable)")
viper.BindPFlag("trailer.key", rootCmd.PersistentFlags().Lookup("trailer.key"))
rootCmd.PersistentFlags().String("author.trailer", "Co-Authored-By", "`key` for commit author trailer (blank to disable)")
viper.BindPFlag("author.trailer", rootCmd.PersistentFlags().Lookup("author.trailer"))
viper.BindEnv("author.trailer", "GHUP_TRAILER_KEY")

rootCmd.PersistentFlags().String("trailer.name", defaultUserName, "name for commit trailer")
viper.BindPFlag("trailer.name", rootCmd.PersistentFlags().Lookup("trailer.name"))
viper.BindEnv("trailer.name", "GHUP_USER_NAME", "GIT_COMMITTER_NAME", "GIT_AUTHOR_NAME")
rootCmd.PersistentFlags().String("user.name", defaultUserName, "`name` for commit author trailer")
viper.BindPFlag("user.name", rootCmd.PersistentFlags().Lookup("user.name"))
viper.BindEnv("user.name", "GHUP_TRAILER_NAME", "GIT_COMMITTER_NAME", "GIT_AUTHOR_NAME")

rootCmd.PersistentFlags().String("trailer.email", defaultUserEmail, "email for commit trailer")
viper.BindPFlag("trailer.email", rootCmd.PersistentFlags().Lookup("trailer.email"))
viper.BindEnv("trailer.email", "GHUP_USER_EMAIL", "GIT_COMMITTER_EMAIL", "GIT_AUTHOR_EMAIL")
rootCmd.PersistentFlags().String("user.email", defaultUserEmail, "`email` for commit author trailer")
viper.BindPFlag("user.email", rootCmd.PersistentFlags().Lookup("user.email"))
viper.BindEnv("user.email", "GHUP_TRAILER_EMAIL", "GIT_COMMITTER_EMAIL", "GIT_AUTHOR_EMAIL")

rootCmd.PersistentFlags().StringToString("trailer", nil, "extra `key=value` commit trailers")
viper.BindPFlag("trailer", rootCmd.PersistentFlags().Lookup("trailer"))

rootCmd.PersistentFlags().BoolVarP(&force, "force", "f", false, "force action")
viper.BindPFlag("force", rootCmd.PersistentFlags().Lookup("force"))

rootCmd.Flags().SortFlags = false
rootCmd.PersistentFlags().SortFlags = false
}

// initViper initializes Viper to load config from the environment
func initViper() {
viper.SetEnvPrefix("GHUP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv() // read in environment variables that match bound variables
viper.AutomaticEnv() // read in environment variables that match bound variables
viper.AllowEmptyEnv(true) // respect empty environment variables
}

// initLogger initializes the logger subsystem
Expand Down
10 changes: 6 additions & 4 deletions cmd/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import (
"github.com/nexthink-oss/ghup/internal/util"
)

var tagName string

var tagCmd = &cobra.Command{
Use: "tag [flags] [<name>]",
Short: "Manage tags via the GitHub V3 API",
Expand All @@ -29,7 +27,7 @@ func init() {
tagCmd.Flags().String("tag", "", "tag name")
viper.BindPFlag("tag", tagCmd.Flags().Lookup("tag"))

tagCmd.Flags().Bool("lightweight", false, "force lightweight tag")
tagCmd.Flags().BoolP("lightweight", "l", false, "force lightweight tag")
viper.BindPFlag("lightweight", tagCmd.Flags().Lookup("lightweight"))

tagCmd.Flags().SortFlags = false
Expand All @@ -45,7 +43,7 @@ func runTagCmd(cmd *cobra.Command, args []string) (err error) {
return errors.Wrap(err, "NewTokenClient")
}

tagName = viper.GetString("tag")
tagName := viper.GetString("tag")

if len(args) == 1 {
tagName = args[0]
Expand All @@ -58,6 +56,10 @@ func runTagCmd(cmd *cobra.Command, args []string) (err error) {
branchRefName := fmt.Sprintf("heads/%s", branch)

tagRefName := fmt.Sprintf("tags/%s", tagName)
if err := util.IsValidRefName(tagRefName); err != nil {
return errors.Wrapf(err, "Invalid tag reference: %s", tagRefName)
}

var tagRefObject string

log.Infof("getting tag reference: %s", tagRefName)
Expand Down
150 changes: 150 additions & 0 deletions cmd/updateref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package cmd

import (
"context"
"fmt"

"github.com/apex/log"
"github.com/google/go-github/v64/github"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/nexthink-oss/ghup/internal/remote"
"github.com/nexthink-oss/ghup/internal/util"
"github.com/nexthink-oss/ghup/pkg/choiceflag"
)

type sRef struct {
Ref string `yaml:"ref"`
SHA string `yaml:"sha"`
}

type tRef struct {
Ref string `yaml:"ref"`
Updated bool `yaml:"updated"`
OldSHA string `yaml:"old_sha,omitempty"`
SHA string `yaml:"sha,omitempty"`
Error string `yaml:"error,omitempty"`
}

type report struct {
Source sRef `yaml:"source"`
Target []tRef `yaml:"target"`
}

func (r report) String() string {
return util.EncodeYAML(&r)
}

var updateRefCmd = &cobra.Command{
Use: "update-ref [flags] -s <source> <target> ...",
Short: "Update target refs to match source",
Args: cobra.MinimumNArgs(1),
PreRunE: validateFlags,
RunE: runUpdateRefCmd,
}

func init() {
updateRefCmd.Flags().StringP("source", "s", "", "source `ref-or-commit`")
updateRefCmd.MarkFlagRequired("source")
viper.BindPFlag("source", updateRefCmd.Flags().Lookup("source"))

refTypes := []string{"heads", "tags"}

defaultSourceType := choiceflag.NewChoiceFlag(refTypes)
_ = defaultSourceType.Set("heads")
updateRefCmd.Flags().VarP(defaultSourceType, "source-type", "S", "unqualified source ref type")
viper.BindPFlag("source-type", updateRefCmd.Flags().Lookup("source-type"))

defaultTargetType := choiceflag.NewChoiceFlag(refTypes)
_ = defaultTargetType.Set("tags")
updateRefCmd.Flags().VarP(defaultTargetType, "target-type", "T", "unqualified target ref type")
viper.BindPFlag("target-type", updateRefCmd.Flags().Lookup("target-type"))

updateRefCmd.Flags().SortFlags = false

rootCmd.AddCommand(updateRefCmd)
}

func runUpdateRefCmd(cmd *cobra.Command, args []string) (err error) {
ctx := context.Background()

client, err := remote.NewTokenClient(ctx, viper.GetString("token"))
if err != nil {
return errors.Wrap(err, "NewTokenClient")
}

sourceRefName := viper.GetString("source")
var sourceObject string

if util.IsCommitHash(sourceRefName) {
sourceCommit, _, err := client.GetCommitSHA(ctx, owner, repo, sourceRefName)
if err != nil {
return errors.Wrapf(err, "GetCommitSHA(%s, %s, %s)", owner, repo, sourceRefName)
}
sourceObject = *sourceCommit
} else {
sourceRefName, err = util.NormalizeRefName(sourceRefName, viper.GetString("source-type"))
if err != nil {
return errors.Wrapf(err, "NormalizeRefName(%s, %s)", sourceRefName, viper.GetString("source-type"))
}

log.Infof("resolving source ref: %s", sourceRefName)
sourceRef, _, err := client.V3.Git.GetRef(ctx, owner, repo, sourceRefName)
if err != nil {
return errors.Wrapf(err, "GetSourceRef(%s, %s, %s)", owner, repo, sourceRefName)
}

sourceObject = sourceRef.Object.GetSHA()
}

targetRefNames := args

// ensure all target refs are properly qualified
for i, targetRefName := range targetRefNames {
targetRefName, err = util.NormalizeRefName(targetRefName, viper.GetString("target-type"))
if err != nil {
return errors.Wrapf(err, "NormalizeRefName(%s, %s)", targetRefName, viper.GetString("target-type"))
}

targetRefNames[i] = targetRefName
}

report := report{
Source: sRef{
Ref: sourceRefName,
SHA: sourceObject,
},
Target: make([]tRef, 0, len(targetRefNames)),
}

for _, targetRefName := range targetRefNames {
targetReport := tRef{
Ref: targetRefName,
}

targetRef := &github.Reference{
Ref: &targetRefName,
Object: &github.GitObject{
SHA: github.String(sourceObject),
},
}

oldHash, newHash, err := client.UpdateRefName(ctx, owner, repo, targetRefName, targetRef, viper.GetBool("force"))
if err != nil {
targetReport.Error = errors.Wrapf(err, "UpdateRefName").Error()
report.Target = append(report.Target, targetReport)
continue
}
targetReport.SHA = newHash
if oldHash != newHash {
targetReport.OldSHA = oldHash
targetReport.Updated = true
}
report.Target = append(report.Target, targetReport)
}

fmt.Print(report)
return
}
1 change: 1 addition & 0 deletions internal/local/testdata/testfile.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test content
6 changes: 6 additions & 0 deletions internal/local/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ func GetLocalFileContent(arg string, separator string) (target string, content [
case len(files) < 1:
err = fmt.Errorf("invalid file parameter")
return
case files[0] == "":
err = fmt.Errorf("no source file specified")
return
case len(files) == 1:
source = files[0]
target = files[0]
case files[1] == "":
err = fmt.Errorf("no target file specified")
return
default:
source = files[0]
target = files[1]
Expand Down
Loading

0 comments on commit d738072

Please sign in to comment.