Skip to content

Commit d738072

Browse files
authored
feat!: ref management and commit trailer handling (#53)
* 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
1 parent e0b979a commit d738072

File tree

15 files changed

+1334
-180
lines changed

15 files changed

+1334
-180
lines changed

README.md

Lines changed: 182 additions & 57 deletions
Large diffs are not rendered by default.

cmd/content.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,33 +25,33 @@ var contentCmd = &cobra.Command{
2525
}
2626

2727
func init() {
28-
contentCmd.PersistentFlags().Bool("create-branch", true, "create missing target branch")
29-
viper.BindPFlag("create-branch", contentCmd.PersistentFlags().Lookup("create-branch"))
28+
contentCmd.Flags().Bool("create-branch", true, "create missing target branch")
29+
viper.BindPFlag("create-branch", contentCmd.Flags().Lookup("create-branch"))
3030
viper.BindEnv("create-branch", "GHUP_CREATE_BRANCH")
3131

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

36-
contentCmd.PersistentFlags().String("pr-body", "", "pull request body")
37-
viper.BindPFlag("pr-body", contentCmd.PersistentFlags().Lookup("pr-body"))
36+
contentCmd.Flags().String("pr-body", "", "pull request body")
37+
viper.BindPFlag("pr-body", contentCmd.Flags().Lookup("pr-body"))
3838
viper.BindEnv("pr-body", "GHUP_PR_BODY")
3939

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

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

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

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

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

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

175-
_, commitUrl, err := client.CommitOnBranchV4(input)
175+
_, commitUrl, err := client.CreateCommitOnBranchV4(input)
176176
if err != nil {
177177
return errors.Wrap(err, "CommitOnBranchV4")
178178
}

cmd/info.go

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@ package cmd
33
import (
44
"fmt"
55

6+
"github.com/nexthink-oss/ghup/internal/remote"
67
"github.com/nexthink-oss/ghup/internal/util"
8+
"github.com/shurcooL/githubv4"
79
"github.com/spf13/cobra"
810
"github.com/spf13/viper"
9-
"gopkg.in/yaml.v3"
1011
)
1112

1213
type info struct {
13-
HasToken bool `yaml:"hasToken"`
14-
Trailer string `yaml:"trailer,omitempty"`
15-
Owner string
16-
Repository string
17-
Branch string
18-
Commit string
19-
IsClean bool `yaml:"isClean"`
14+
HasToken bool `yaml:"hasToken"`
15+
Trailers []string `yaml:"trailers,omitempty"`
16+
Owner string
17+
Repository string
18+
Branch string
19+
Commit string
20+
IsClean bool `yaml:"isClean"`
21+
CommitMessage githubv4.CommitMessage `yaml:"commitMessage"`
2022
}
2123

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

3436
func runInfoCmd(cmd *cobra.Command, args []string) (err error) {
3537
i := info{
36-
HasToken: len(viper.GetString("token")) > 0,
37-
Trailer: util.BuildTrailer(),
38-
Owner: owner,
39-
Repository: repo,
40-
Branch: branch,
38+
HasToken: len(viper.GetString("token")) > 0,
39+
Trailers: util.BuildTrailers(),
40+
Owner: owner,
41+
Repository: repo,
42+
Branch: branch,
43+
CommitMessage: remote.CommitMessage(util.BuildCommitMessage()),
4144
}
4245

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

52-
m, err := yaml.Marshal(i)
53-
if err != nil {
54-
return err
55-
}
56-
57-
fmt.Print(string(m))
55+
fmt.Print(util.EncodeYAML(&i))
5856
return
5957
}

cmd/root.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,42 +72,48 @@ func init() {
7272
viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token"))
7373
viper.BindEnv("token", "GHUP_TOKEN", "GITHUB_TOKEN")
7474

75-
rootCmd.PersistentFlags().StringP("owner", "o", defaultOwner, "repository owner")
75+
rootCmd.PersistentFlags().StringP("owner", "o", defaultOwner, "repository owner `name`")
7676
viper.BindPFlag("owner", rootCmd.PersistentFlags().Lookup("owner"))
7777
viper.BindEnv("owner", "GHUP_OWNER", "GITHUB_OWNER")
7878

79-
rootCmd.PersistentFlags().StringP("repo", "r", defaultRepo, "repository name")
79+
rootCmd.PersistentFlags().StringP("repo", "r", defaultRepo, "repository `name`")
8080
viper.BindPFlag("repo", rootCmd.PersistentFlags().Lookup("repo"))
8181

82-
rootCmd.PersistentFlags().StringP("branch", "b", defaultBranch, "target branch name")
82+
rootCmd.PersistentFlags().StringP("branch", "b", defaultBranch, "target branch `name`")
8383
viper.BindPFlag("branch", rootCmd.PersistentFlags().Lookup("branch"))
8484
viper.BindEnv("branch", "GHUP_BRANCH", "CHANGE_BRANCH", "BRANCH_NAME", "GIT_BRANCH")
8585

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

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

92-
rootCmd.PersistentFlags().String("trailer.name", defaultUserName, "name for commit trailer")
93-
viper.BindPFlag("trailer.name", rootCmd.PersistentFlags().Lookup("trailer.name"))
94-
viper.BindEnv("trailer.name", "GHUP_USER_NAME", "GIT_COMMITTER_NAME", "GIT_AUTHOR_NAME")
93+
rootCmd.PersistentFlags().String("user.name", defaultUserName, "`name` for commit author trailer")
94+
viper.BindPFlag("user.name", rootCmd.PersistentFlags().Lookup("user.name"))
95+
viper.BindEnv("user.name", "GHUP_TRAILER_NAME", "GIT_COMMITTER_NAME", "GIT_AUTHOR_NAME")
9596

96-
rootCmd.PersistentFlags().String("trailer.email", defaultUserEmail, "email for commit trailer")
97-
viper.BindPFlag("trailer.email", rootCmd.PersistentFlags().Lookup("trailer.email"))
98-
viper.BindEnv("trailer.email", "GHUP_USER_EMAIL", "GIT_COMMITTER_EMAIL", "GIT_AUTHOR_EMAIL")
97+
rootCmd.PersistentFlags().String("user.email", defaultUserEmail, "`email` for commit author trailer")
98+
viper.BindPFlag("user.email", rootCmd.PersistentFlags().Lookup("user.email"))
99+
viper.BindEnv("user.email", "GHUP_TRAILER_EMAIL", "GIT_COMMITTER_EMAIL", "GIT_AUTHOR_EMAIL")
100+
101+
rootCmd.PersistentFlags().StringToString("trailer", nil, "extra `key=value` commit trailers")
102+
viper.BindPFlag("trailer", rootCmd.PersistentFlags().Lookup("trailer"))
99103

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

103107
rootCmd.Flags().SortFlags = false
108+
rootCmd.PersistentFlags().SortFlags = false
104109
}
105110

106111
// initViper initializes Viper to load config from the environment
107112
func initViper() {
108113
viper.SetEnvPrefix("GHUP")
109114
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
110-
viper.AutomaticEnv() // read in environment variables that match bound variables
115+
viper.AutomaticEnv() // read in environment variables that match bound variables
116+
viper.AllowEmptyEnv(true) // respect empty environment variables
111117
}
112118

113119
// initLogger initializes the logger subsystem

cmd/tag.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import (
1515
"github.com/nexthink-oss/ghup/internal/util"
1616
)
1717

18-
var tagName string
19-
2018
var tagCmd = &cobra.Command{
2119
Use: "tag [flags] [<name>]",
2220
Short: "Manage tags via the GitHub V3 API",
@@ -29,7 +27,7 @@ func init() {
2927
tagCmd.Flags().String("tag", "", "tag name")
3028
viper.BindPFlag("tag", tagCmd.Flags().Lookup("tag"))
3129

32-
tagCmd.Flags().Bool("lightweight", false, "force lightweight tag")
30+
tagCmd.Flags().BoolP("lightweight", "l", false, "force lightweight tag")
3331
viper.BindPFlag("lightweight", tagCmd.Flags().Lookup("lightweight"))
3432

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

48-
tagName = viper.GetString("tag")
46+
tagName := viper.GetString("tag")
4947

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

6058
tagRefName := fmt.Sprintf("tags/%s", tagName)
59+
if err := util.IsValidRefName(tagRefName); err != nil {
60+
return errors.Wrapf(err, "Invalid tag reference: %s", tagRefName)
61+
}
62+
6163
var tagRefObject string
6264

6365
log.Infof("getting tag reference: %s", tagRefName)

cmd/updateref.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/apex/log"
8+
"github.com/google/go-github/v64/github"
9+
"github.com/pkg/errors"
10+
"github.com/spf13/cobra"
11+
"github.com/spf13/viper"
12+
13+
"github.com/nexthink-oss/ghup/internal/remote"
14+
"github.com/nexthink-oss/ghup/internal/util"
15+
"github.com/nexthink-oss/ghup/pkg/choiceflag"
16+
)
17+
18+
type sRef struct {
19+
Ref string `yaml:"ref"`
20+
SHA string `yaml:"sha"`
21+
}
22+
23+
type tRef struct {
24+
Ref string `yaml:"ref"`
25+
Updated bool `yaml:"updated"`
26+
OldSHA string `yaml:"old_sha,omitempty"`
27+
SHA string `yaml:"sha,omitempty"`
28+
Error string `yaml:"error,omitempty"`
29+
}
30+
31+
type report struct {
32+
Source sRef `yaml:"source"`
33+
Target []tRef `yaml:"target"`
34+
}
35+
36+
func (r report) String() string {
37+
return util.EncodeYAML(&r)
38+
}
39+
40+
var updateRefCmd = &cobra.Command{
41+
Use: "update-ref [flags] -s <source> <target> ...",
42+
Short: "Update target refs to match source",
43+
Args: cobra.MinimumNArgs(1),
44+
PreRunE: validateFlags,
45+
RunE: runUpdateRefCmd,
46+
}
47+
48+
func init() {
49+
updateRefCmd.Flags().StringP("source", "s", "", "source `ref-or-commit`")
50+
updateRefCmd.MarkFlagRequired("source")
51+
viper.BindPFlag("source", updateRefCmd.Flags().Lookup("source"))
52+
53+
refTypes := []string{"heads", "tags"}
54+
55+
defaultSourceType := choiceflag.NewChoiceFlag(refTypes)
56+
_ = defaultSourceType.Set("heads")
57+
updateRefCmd.Flags().VarP(defaultSourceType, "source-type", "S", "unqualified source ref type")
58+
viper.BindPFlag("source-type", updateRefCmd.Flags().Lookup("source-type"))
59+
60+
defaultTargetType := choiceflag.NewChoiceFlag(refTypes)
61+
_ = defaultTargetType.Set("tags")
62+
updateRefCmd.Flags().VarP(defaultTargetType, "target-type", "T", "unqualified target ref type")
63+
viper.BindPFlag("target-type", updateRefCmd.Flags().Lookup("target-type"))
64+
65+
updateRefCmd.Flags().SortFlags = false
66+
67+
rootCmd.AddCommand(updateRefCmd)
68+
}
69+
70+
func runUpdateRefCmd(cmd *cobra.Command, args []string) (err error) {
71+
ctx := context.Background()
72+
73+
client, err := remote.NewTokenClient(ctx, viper.GetString("token"))
74+
if err != nil {
75+
return errors.Wrap(err, "NewTokenClient")
76+
}
77+
78+
sourceRefName := viper.GetString("source")
79+
var sourceObject string
80+
81+
if util.IsCommitHash(sourceRefName) {
82+
sourceCommit, _, err := client.GetCommitSHA(ctx, owner, repo, sourceRefName)
83+
if err != nil {
84+
return errors.Wrapf(err, "GetCommitSHA(%s, %s, %s)", owner, repo, sourceRefName)
85+
}
86+
sourceObject = *sourceCommit
87+
} else {
88+
sourceRefName, err = util.NormalizeRefName(sourceRefName, viper.GetString("source-type"))
89+
if err != nil {
90+
return errors.Wrapf(err, "NormalizeRefName(%s, %s)", sourceRefName, viper.GetString("source-type"))
91+
}
92+
93+
log.Infof("resolving source ref: %s", sourceRefName)
94+
sourceRef, _, err := client.V3.Git.GetRef(ctx, owner, repo, sourceRefName)
95+
if err != nil {
96+
return errors.Wrapf(err, "GetSourceRef(%s, %s, %s)", owner, repo, sourceRefName)
97+
}
98+
99+
sourceObject = sourceRef.Object.GetSHA()
100+
}
101+
102+
targetRefNames := args
103+
104+
// ensure all target refs are properly qualified
105+
for i, targetRefName := range targetRefNames {
106+
targetRefName, err = util.NormalizeRefName(targetRefName, viper.GetString("target-type"))
107+
if err != nil {
108+
return errors.Wrapf(err, "NormalizeRefName(%s, %s)", targetRefName, viper.GetString("target-type"))
109+
}
110+
111+
targetRefNames[i] = targetRefName
112+
}
113+
114+
report := report{
115+
Source: sRef{
116+
Ref: sourceRefName,
117+
SHA: sourceObject,
118+
},
119+
Target: make([]tRef, 0, len(targetRefNames)),
120+
}
121+
122+
for _, targetRefName := range targetRefNames {
123+
targetReport := tRef{
124+
Ref: targetRefName,
125+
}
126+
127+
targetRef := &github.Reference{
128+
Ref: &targetRefName,
129+
Object: &github.GitObject{
130+
SHA: github.String(sourceObject),
131+
},
132+
}
133+
134+
oldHash, newHash, err := client.UpdateRefName(ctx, owner, repo, targetRefName, targetRef, viper.GetBool("force"))
135+
if err != nil {
136+
targetReport.Error = errors.Wrapf(err, "UpdateRefName").Error()
137+
report.Target = append(report.Target, targetReport)
138+
continue
139+
}
140+
targetReport.SHA = newHash
141+
if oldHash != newHash {
142+
targetReport.OldSHA = oldHash
143+
targetReport.Updated = true
144+
}
145+
report.Target = append(report.Target, targetReport)
146+
}
147+
148+
fmt.Print(report)
149+
return
150+
}

internal/local/testdata/testfile.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test content

internal/local/utils.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@ func GetLocalFileContent(arg string, separator string) (target string, content [
1616
case len(files) < 1:
1717
err = fmt.Errorf("invalid file parameter")
1818
return
19+
case files[0] == "":
20+
err = fmt.Errorf("no source file specified")
21+
return
1922
case len(files) == 1:
2023
source = files[0]
2124
target = files[0]
25+
case files[1] == "":
26+
err = fmt.Errorf("no target file specified")
27+
return
2228
default:
2329
source = files[0]
2430
target = files[1]

0 commit comments

Comments
 (0)