- reorder if checks: validate --enable-squash-merge is set before checking the value, and error when --enable-squash-merge=false - use validSquashMsgValues directly in interactive prompt instead of duplicating the slice - use slices.Contains in validateSquashMergeCommitMsg - interpolate const values in Long description instead of hardcoding - add default clause in transformSquashMergeOpts to avoid mutating title/message on unknown input - move optionDiscussions to end of const block with TODO comment - add test for unknown input and --enable-squash-merge=false case
719 lines
24 KiB
Go
719 lines
24 KiB
Go
package edit
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/api"
|
|
fd "github.com/cli/cli/v2/internal/featuredetection"
|
|
"github.com/cli/cli/v2/internal/ghinstance"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
"github.com/cli/cli/v2/internal/text"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/cli/cli/v2/pkg/set"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
type iprompter interface {
|
|
MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
|
|
Input(string, string) (string, error)
|
|
Confirm(string, bool) (bool, error)
|
|
Select(string, string, []string) (int, error)
|
|
}
|
|
|
|
const (
|
|
allowMergeCommits = "Allow Merge Commits"
|
|
allowSquashMerge = "Allow Squash Merging"
|
|
allowRebaseMerge = "Allow Rebase Merging"
|
|
|
|
squashMsgDefault = "default"
|
|
squashMsgPRTitle = "pr-title"
|
|
squashMsgPRTitleCommits = "pr-title-commits"
|
|
squashMsgPRTitleDescription = "pr-title-description"
|
|
|
|
optionAllowForking = "Allow Forking"
|
|
optionDefaultBranchName = "Default Branch Name"
|
|
optionDescription = "Description"
|
|
optionHomePageURL = "Home Page URL"
|
|
optionIssues = "Issues"
|
|
optionMergeOptions = "Merge Options"
|
|
optionProjects = "Projects"
|
|
optionTemplateRepo = "Template Repository"
|
|
optionTopics = "Topics"
|
|
optionVisibility = "Visibility"
|
|
optionWikis = "Wikis"
|
|
|
|
// TODO: GitHub Enterprise Server does not support has_discussions yet
|
|
// optionDiscussions = "Discussions"
|
|
)
|
|
|
|
type EditOptions struct {
|
|
HTTPClient *http.Client
|
|
Repository ghrepo.Interface
|
|
IO *iostreams.IOStreams
|
|
Edits EditRepositoryInput
|
|
AddTopics []string
|
|
RemoveTopics []string
|
|
AcceptVisibilityChangeConsequences bool
|
|
InteractiveMode bool
|
|
Detector fd.Detector
|
|
Prompter iprompter
|
|
// Cache of current repo topics to avoid retrieving them
|
|
// in multiple flows.
|
|
topicsCache []string
|
|
}
|
|
|
|
type EditRepositoryInput struct {
|
|
enableAdvancedSecurity *bool
|
|
enableSecretScanning *bool
|
|
enableSecretScanningPushProtection *bool
|
|
squashMergeCommitMsg *string
|
|
|
|
AllowForking *bool `json:"allow_forking,omitempty"`
|
|
AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"`
|
|
DefaultBranch *string `json:"default_branch,omitempty"`
|
|
DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"`
|
|
EnableIssues *bool `json:"has_issues,omitempty"`
|
|
EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"`
|
|
EnableProjects *bool `json:"has_projects,omitempty"`
|
|
EnableDiscussions *bool `json:"has_discussions,omitempty"`
|
|
EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"`
|
|
EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"`
|
|
EnableWiki *bool `json:"has_wiki,omitempty"`
|
|
Homepage *string `json:"homepage,omitempty"`
|
|
IsTemplate *bool `json:"is_template,omitempty"`
|
|
SecurityAndAnalysis *SecurityAndAnalysisInput `json:"security_and_analysis,omitempty"`
|
|
SquashMergeCommitTitle *string `json:"squash_merge_commit_title,omitempty"`
|
|
SquashMergeCommitMessage *string `json:"squash_merge_commit_message,omitempty"`
|
|
Visibility *string `json:"visibility,omitempty"`
|
|
}
|
|
|
|
func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command {
|
|
opts := &EditOptions{
|
|
IO: f.IOStreams,
|
|
Prompter: f.Prompter,
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "edit [<repository>]",
|
|
Short: "Edit repository settings",
|
|
Annotations: map[string]string{
|
|
"help:arguments": heredoc.Doc(`
|
|
A repository can be supplied as an argument in any of the following formats:
|
|
- "OWNER/REPO"
|
|
- by URL, e.g. "https://github.com/OWNER/REPO"
|
|
`),
|
|
},
|
|
Long: heredoc.Docf(`
|
|
Edit repository settings.
|
|
|
|
To toggle a setting off, use the %[1]s--<flag>=false%[1]s syntax.
|
|
|
|
Changing repository visibility can have unexpected consequences including but not limited to:
|
|
|
|
- Losing stars and watchers, affecting repository ranking
|
|
- Detaching public forks from the network
|
|
- Disabling push rulesets
|
|
- Allowing access to GitHub Actions history and logs
|
|
|
|
When the %[1]s--visibility%[1]s flag is used, %[1]s--accept-visibility-change-consequences%[1]s flag is required.
|
|
|
|
For information on all the potential consequences, see <https://gh.io/setting-repository-visibility>.
|
|
|
|
When the %[1]s--enable-squash-merge%[1]s flag is used, %[1]s--squash-merge-commit-message%[1]s
|
|
can be used to change the default squash merge commit message behavior:
|
|
|
|
- %[1]s%[2]s%[1]s: uses commit title and message for 1 commit, or pull request title and list of commits for 2 or more
|
|
- %[1]s%[3]s%[1]s: uses pull request title
|
|
- %[1]s%[4]s%[1]s: uses pull request title and list of commits
|
|
- %[1]s%[5]s%[1]s: uses pull request title and description
|
|
`, "`", squashMsgDefault, squashMsgPRTitle, squashMsgPRTitleCommits, squashMsgPRTitleDescription),
|
|
Args: cobra.MaximumNArgs(1),
|
|
Example: heredoc.Doc(`
|
|
# Enable issues and wiki
|
|
$ gh repo edit --enable-issues --enable-wiki
|
|
|
|
# Disable projects
|
|
$ gh repo edit --enable-projects=false
|
|
`),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if len(args) > 0 {
|
|
var err error
|
|
opts.Repository, err = ghrepo.FromFullName(args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
var err error
|
|
opts.Repository, err = f.BaseRepo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if httpClient, err := f.HttpClient(); err == nil {
|
|
opts.HTTPClient = httpClient
|
|
} else {
|
|
return err
|
|
}
|
|
|
|
if cmd.Flags().NFlag() == 0 {
|
|
opts.InteractiveMode = true
|
|
}
|
|
|
|
if opts.InteractiveMode && !opts.IO.CanPrompt() {
|
|
return cmdutil.FlagErrorf("specify properties to edit when not running interactively")
|
|
}
|
|
|
|
if opts.Edits.Visibility != nil && !opts.AcceptVisibilityChangeConsequences {
|
|
return cmdutil.FlagErrorf("use of --visibility flag requires --accept-visibility-change-consequences flag")
|
|
}
|
|
|
|
if opts.Edits.squashMergeCommitMsg != nil {
|
|
if opts.Edits.EnableSquashMerge == nil {
|
|
return cmdutil.FlagErrorf("--squash-merge-commit-message requires --enable-squash-merge")
|
|
}
|
|
if !*opts.Edits.EnableSquashMerge {
|
|
return cmdutil.FlagErrorf("--squash-merge-commit-message cannot be used when --enable-squash-merge=false")
|
|
}
|
|
if err := validateSquashMergeCommitMsg(*opts.Edits.squashMergeCommitMsg); err != nil {
|
|
return err
|
|
}
|
|
transformSquashMergeOpts(&opts.Edits)
|
|
}
|
|
|
|
if hasSecurityEdits(opts.Edits) {
|
|
opts.Edits.SecurityAndAnalysis = transformSecurityAndAnalysisOpts(opts)
|
|
}
|
|
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
return editRun(cmd.Context(), opts)
|
|
},
|
|
}
|
|
|
|
cmdutil.NilStringFlag(cmd, &opts.Edits.Description, "description", "d", "Description of the repository")
|
|
cmdutil.NilStringFlag(cmd, &opts.Edits.Homepage, "homepage", "h", "Repository home page `URL`")
|
|
cmdutil.NilStringFlag(cmd, &opts.Edits.DefaultBranch, "default-branch", "", "Set the default branch `name` for the repository")
|
|
cmdutil.NilStringFlag(cmd, &opts.Edits.Visibility, "visibility", "", "Change the visibility of the repository to {public,private,internal}")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.IsTemplate, "template", "", "Make the repository available as a template repository")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableIssues, "enable-issues", "", "Enable issues in the repository")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableProjects, "enable-projects", "", "Enable projects in the repository")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableWiki, "enable-wiki", "", "Enable wiki in the repository")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableDiscussions, "enable-discussions", "", "Enable discussions in the repository")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableMergeCommit, "enable-merge-commit", "", "Enable merging pull requests via merge commit")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableSquashMerge, "enable-squash-merge", "", "Enable merging pull requests via squashed commit")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableRebaseMerge, "enable-rebase-merge", "", "Enable merging pull requests via rebase")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableAutoMerge, "enable-auto-merge", "", "Enable auto-merge functionality")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.enableAdvancedSecurity, "enable-advanced-security", "", "Enable advanced security in the repository")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.enableSecretScanning, "enable-secret-scanning", "", "Enable secret scanning in the repository")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.enableSecretScanningPushProtection, "enable-secret-scanning-push-protection", "", "Enable secret scanning push protection in the repository. Secret scanning must be enabled first")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.DeleteBranchOnMerge, "delete-branch-on-merge", "", "Delete head branch when pull requests are merged")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowForking, "allow-forking", "", "Allow forking of an organization repository")
|
|
cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowUpdateBranch, "allow-update-branch", "", "Allow a pull request head branch that is behind its base branch to be updated")
|
|
cmdutil.NilStringFlag(cmd, &opts.Edits.squashMergeCommitMsg, "squash-merge-commit-message", "", "The default value for a squash merge commit message: {default|pr-title|pr-title-commits|pr-title-description}")
|
|
cmd.Flags().StringSliceVar(&opts.AddTopics, "add-topic", nil, "Add repository topic")
|
|
cmd.Flags().StringSliceVar(&opts.RemoveTopics, "remove-topic", nil, "Remove repository topic")
|
|
cmd.Flags().BoolVar(&opts.AcceptVisibilityChangeConsequences, "accept-visibility-change-consequences", false, "Accept the consequences of changing the repository visibility")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func editRun(ctx context.Context, opts *EditOptions) error {
|
|
repo := opts.Repository
|
|
|
|
if opts.InteractiveMode {
|
|
detector := opts.Detector
|
|
if detector == nil {
|
|
cachedClient := api.NewCachedHTTPClient(opts.HTTPClient, time.Hour*24)
|
|
detector = fd.NewDetector(cachedClient, repo.RepoHost())
|
|
}
|
|
repoFeatures, err := detector.RepositoryFeatures()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
apiClient := api.NewClientFromHTTP(opts.HTTPClient)
|
|
fieldsToRetrieve := []string{
|
|
"defaultBranchRef",
|
|
"deleteBranchOnMerge",
|
|
"description",
|
|
"hasIssuesEnabled",
|
|
"hasProjectsEnabled",
|
|
"hasWikiEnabled",
|
|
// TODO: GitHub Enterprise Server does not support has_discussions yet
|
|
// "hasDiscussionsEnabled",
|
|
"homepageUrl",
|
|
"isInOrganization",
|
|
"isTemplate",
|
|
"mergeCommitAllowed",
|
|
"rebaseMergeAllowed",
|
|
"repositoryTopics",
|
|
"stargazerCount",
|
|
"squashMergeAllowed",
|
|
"watchers",
|
|
}
|
|
// TODO repoFeaturesCleanup
|
|
if repoFeatures.VisibilityField {
|
|
fieldsToRetrieve = append(fieldsToRetrieve, "visibility")
|
|
}
|
|
// TODO repoFeaturesCleanup
|
|
if repoFeatures.AutoMerge {
|
|
fieldsToRetrieve = append(fieldsToRetrieve, "autoMergeAllowed")
|
|
}
|
|
|
|
opts.IO.StartProgressIndicator()
|
|
fetchedRepo, err := api.FetchRepository(apiClient, opts.Repository, fieldsToRetrieve)
|
|
opts.IO.StopProgressIndicator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = interactiveRepoEdit(opts, fetchedRepo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if opts.Edits.SecurityAndAnalysis != nil {
|
|
apiClient := api.NewClientFromHTTP(opts.HTTPClient)
|
|
repo, err := api.FetchRepository(apiClient, opts.Repository, []string{"viewerCanAdminister"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !repo.ViewerCanAdminister {
|
|
return fmt.Errorf("you do not have sufficient permissions to edit repository security and analysis features")
|
|
}
|
|
}
|
|
|
|
apiPath := fmt.Sprintf("repos/%s/%s", repo.RepoOwner(), repo.RepoName())
|
|
|
|
body := &bytes.Buffer{}
|
|
enc := json.NewEncoder(body)
|
|
if err := enc.Encode(opts.Edits); err != nil {
|
|
return err
|
|
}
|
|
|
|
g := errgroup.Group{}
|
|
|
|
if body.Len() > 3 {
|
|
g.Go(func() error {
|
|
apiClient := api.NewClientFromHTTP(opts.HTTPClient)
|
|
_, err := api.CreateRepoTransformToV4(apiClient, repo.RepoHost(), "PATCH", apiPath, body)
|
|
return err
|
|
})
|
|
}
|
|
|
|
if len(opts.AddTopics) > 0 || len(opts.RemoveTopics) > 0 {
|
|
g.Go(func() error {
|
|
// opts.topicsCache gets populated in interactive mode
|
|
if !opts.InteractiveMode {
|
|
var err error
|
|
opts.topicsCache, err = getTopics(ctx, opts.HTTPClient, repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
oldTopics := set.NewStringSet()
|
|
oldTopics.AddValues(opts.topicsCache)
|
|
|
|
newTopics := set.NewStringSet()
|
|
newTopics.AddValues(opts.topicsCache)
|
|
newTopics.AddValues(opts.AddTopics)
|
|
newTopics.RemoveValues(opts.RemoveTopics)
|
|
|
|
if oldTopics.Equal(newTopics) {
|
|
return nil
|
|
}
|
|
return setTopics(ctx, opts.HTTPClient, repo, newTopics.ToSlice())
|
|
})
|
|
}
|
|
|
|
err := g.Wait()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.IO.IsStdoutTTY() {
|
|
cs := opts.IO.ColorScheme()
|
|
fmt.Fprintf(opts.IO.Out,
|
|
"%s Edited repository %s\n",
|
|
cs.SuccessIcon(),
|
|
ghrepo.FullName(repo))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func interactiveChoice(p iprompter, r *api.Repository) ([]string, error) {
|
|
options := []string{
|
|
optionDefaultBranchName,
|
|
optionDescription,
|
|
optionHomePageURL,
|
|
optionIssues,
|
|
optionMergeOptions,
|
|
optionProjects,
|
|
// TODO: GitHub Enterprise Server does not support has_discussions yet
|
|
// optionDiscussions,
|
|
optionTemplateRepo,
|
|
optionTopics,
|
|
optionVisibility,
|
|
optionWikis,
|
|
}
|
|
if r.IsInOrganization {
|
|
options = append(options, optionAllowForking)
|
|
}
|
|
var answers []string
|
|
selected, err := p.MultiSelect("What do you want to edit?", nil, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, i := range selected {
|
|
answers = append(answers, options[i])
|
|
}
|
|
|
|
return answers, err
|
|
}
|
|
|
|
func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
|
|
for _, v := range r.RepositoryTopics.Nodes {
|
|
opts.topicsCache = append(opts.topicsCache, v.Topic.Name)
|
|
}
|
|
p := opts.Prompter
|
|
choices, err := interactiveChoice(p, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, c := range choices {
|
|
switch c {
|
|
case optionDescription:
|
|
answer, err := p.Input("Description of the repository", r.Description)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.Description = &answer
|
|
case optionHomePageURL:
|
|
a, err := p.Input("Repository home page URL", r.HomepageURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.Homepage = &a
|
|
case optionTopics:
|
|
addTopics, err := p.Input("Add topics?(csv format)", "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(strings.TrimSpace(addTopics)) > 0 {
|
|
opts.AddTopics = parseTopics(addTopics)
|
|
}
|
|
|
|
if len(opts.topicsCache) > 0 {
|
|
selected, err := p.MultiSelect("Remove Topics", nil, opts.topicsCache)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, i := range selected {
|
|
opts.RemoveTopics = append(opts.RemoveTopics, opts.topicsCache[i])
|
|
}
|
|
}
|
|
case optionDefaultBranchName:
|
|
name, err := p.Input("Default branch name", r.DefaultBranchRef.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.DefaultBranch = &name
|
|
case optionWikis:
|
|
c, err := p.Confirm("Enable Wikis?", r.HasWikiEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.EnableWiki = &c
|
|
case optionIssues:
|
|
a, err := p.Confirm("Enable Issues?", r.HasIssuesEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.EnableIssues = &a
|
|
case optionProjects:
|
|
a, err := p.Confirm("Enable Projects?", r.HasProjectsEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.EnableProjects = &a
|
|
case optionVisibility:
|
|
cs := opts.IO.ColorScheme()
|
|
fmt.Fprintf(opts.IO.ErrOut, "%s Danger zone: changing repository visibility can have unexpected consequences; consult https://gh.io/setting-repository-visibility before continuing.\n", cs.WarningIcon())
|
|
|
|
visibilityOptions := []string{"public", "private", "internal"}
|
|
selected, err := p.Select("Visibility", strings.ToLower(r.Visibility), visibilityOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
selectedVisibility := visibilityOptions[selected]
|
|
|
|
if selectedVisibility != r.Visibility && (r.StargazerCount > 0 || r.Watchers.TotalCount > 0) {
|
|
fmt.Fprintf(opts.IO.ErrOut, "%s Changing the repository visibility to %s will cause permanent loss of %s and %s.\n", cs.WarningIcon(), selectedVisibility, text.Pluralize(r.StargazerCount, "star"), text.Pluralize(r.Watchers.TotalCount, "watcher"))
|
|
}
|
|
|
|
confirmed, err := p.Confirm(fmt.Sprintf("Do you want to change visibility to %s?", selectedVisibility), false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if confirmed {
|
|
opts.Edits.Visibility = &selectedVisibility
|
|
}
|
|
case optionMergeOptions:
|
|
var defaultMergeOptions []string
|
|
var selectedMergeOptions []string
|
|
if r.MergeCommitAllowed {
|
|
defaultMergeOptions = append(defaultMergeOptions, allowMergeCommits)
|
|
}
|
|
if r.SquashMergeAllowed {
|
|
defaultMergeOptions = append(defaultMergeOptions, allowSquashMerge)
|
|
}
|
|
if r.RebaseMergeAllowed {
|
|
defaultMergeOptions = append(defaultMergeOptions, allowRebaseMerge)
|
|
}
|
|
mergeOpts := []string{allowMergeCommits, allowSquashMerge, allowRebaseMerge}
|
|
selected, err := p.MultiSelect(
|
|
"Allowed merge strategies",
|
|
defaultMergeOptions,
|
|
mergeOpts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, i := range selected {
|
|
selectedMergeOptions = append(selectedMergeOptions, mergeOpts[i])
|
|
}
|
|
enableMergeCommit := isIncluded(allowMergeCommits, selectedMergeOptions)
|
|
opts.Edits.EnableMergeCommit = &enableMergeCommit
|
|
enableSquashMerge := isIncluded(allowSquashMerge, selectedMergeOptions)
|
|
opts.Edits.EnableSquashMerge = &enableSquashMerge
|
|
enableRebaseMerge := isIncluded(allowRebaseMerge, selectedMergeOptions)
|
|
opts.Edits.EnableRebaseMerge = &enableRebaseMerge
|
|
if !enableMergeCommit && !enableSquashMerge && !enableRebaseMerge {
|
|
return fmt.Errorf("you need to allow at least one merge strategy")
|
|
}
|
|
|
|
if enableSquashMerge {
|
|
squashMsgOptions := validSquashMsgValues
|
|
idx, err := p.Select(
|
|
"Default squash merge commit message",
|
|
squashMsgDefault,
|
|
squashMsgOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
selected := squashMsgOptions[idx]
|
|
opts.Edits.squashMergeCommitMsg = &selected
|
|
transformSquashMergeOpts(&opts.Edits)
|
|
}
|
|
|
|
opts.Edits.EnableAutoMerge = &r.AutoMergeAllowed
|
|
c, err := p.Confirm("Enable Auto Merge?", r.AutoMergeAllowed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.EnableAutoMerge = &c
|
|
|
|
opts.Edits.DeleteBranchOnMerge = &r.DeleteBranchOnMerge
|
|
c, err = p.Confirm(
|
|
"Automatically delete head branches after merging?", r.DeleteBranchOnMerge)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.DeleteBranchOnMerge = &c
|
|
case optionTemplateRepo:
|
|
c, err := p.Confirm("Convert into a template repository?", r.IsTemplate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.IsTemplate = &c
|
|
case optionAllowForking:
|
|
c, err := p.Confirm(
|
|
"Allow forking (of an organization repository)?",
|
|
r.ForkingAllowed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Edits.AllowForking = &c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseTopics(s string) []string {
|
|
topics := strings.Split(s, ",")
|
|
for i, topic := range topics {
|
|
topics[i] = strings.TrimSpace(topic)
|
|
}
|
|
return topics
|
|
}
|
|
|
|
func getTopics(ctx context.Context, httpClient *http.Client, repo ghrepo.Interface) ([]string, error) {
|
|
apiPath := fmt.Sprintf("repos/%s/%s/topics", repo.RepoOwner(), repo.RepoName())
|
|
req, err := http.NewRequestWithContext(ctx, "GET", ghinstance.RESTPrefix(repo.RepoHost())+apiPath, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// "mercy-preview" is still needed for some GitHub Enterprise versions
|
|
req.Header.Set("Accept", "application/vnd.github.mercy-preview+json")
|
|
res, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, api.HandleHTTPError(res)
|
|
}
|
|
|
|
var responseData struct {
|
|
Names []string `json:"names"`
|
|
}
|
|
dec := json.NewDecoder(res.Body)
|
|
err = dec.Decode(&responseData)
|
|
return responseData.Names, err
|
|
}
|
|
|
|
func setTopics(ctx context.Context, httpClient *http.Client, repo ghrepo.Interface, topics []string) error {
|
|
payload := struct {
|
|
Names []string `json:"names"`
|
|
}{
|
|
Names: topics,
|
|
}
|
|
body := &bytes.Buffer{}
|
|
dec := json.NewEncoder(body)
|
|
if err := dec.Encode(&payload); err != nil {
|
|
return err
|
|
}
|
|
|
|
apiPath := fmt.Sprintf("repos/%s/%s/topics", repo.RepoOwner(), repo.RepoName())
|
|
req, err := http.NewRequestWithContext(ctx, "PUT", ghinstance.RESTPrefix(repo.RepoHost())+apiPath, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("Content-type", "application/json")
|
|
// "mercy-preview" is still needed for some GitHub Enterprise versions
|
|
req.Header.Set("Accept", "application/vnd.github.mercy-preview+json")
|
|
res, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return api.HandleHTTPError(res)
|
|
}
|
|
|
|
if res.Body != nil {
|
|
_, _ = io.Copy(io.Discard, res.Body)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isIncluded(value string, opts []string) bool {
|
|
for _, opt := range opts {
|
|
if strings.EqualFold(opt, value) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func boolToStatus(status bool) *string {
|
|
var result string
|
|
if status {
|
|
result = "enabled"
|
|
} else {
|
|
result = "disabled"
|
|
}
|
|
return &result
|
|
}
|
|
|
|
func hasSecurityEdits(edits EditRepositoryInput) bool {
|
|
return edits.enableAdvancedSecurity != nil || edits.enableSecretScanning != nil || edits.enableSecretScanningPushProtection != nil
|
|
}
|
|
|
|
type SecurityAndAnalysisInput struct {
|
|
EnableAdvancedSecurity *SecurityAndAnalysisStatus `json:"advanced_security,omitempty"`
|
|
EnableSecretScanning *SecurityAndAnalysisStatus `json:"secret_scanning,omitempty"`
|
|
EnableSecretScanningPushProtection *SecurityAndAnalysisStatus `json:"secret_scanning_push_protection,omitempty"`
|
|
}
|
|
|
|
type SecurityAndAnalysisStatus struct {
|
|
Status *string `json:"status,omitempty"`
|
|
}
|
|
|
|
// Transform security and analysis parameters to properly serialize EditRepositoryInput
|
|
// See API Docs: https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#update-a-repository
|
|
func transformSecurityAndAnalysisOpts(opts *EditOptions) *SecurityAndAnalysisInput {
|
|
securityOptions := &SecurityAndAnalysisInput{}
|
|
if opts.Edits.enableAdvancedSecurity != nil {
|
|
securityOptions.EnableAdvancedSecurity = &SecurityAndAnalysisStatus{
|
|
Status: boolToStatus(*opts.Edits.enableAdvancedSecurity),
|
|
}
|
|
}
|
|
if opts.Edits.enableSecretScanning != nil {
|
|
securityOptions.EnableSecretScanning = &SecurityAndAnalysisStatus{
|
|
Status: boolToStatus(*opts.Edits.enableSecretScanning),
|
|
}
|
|
}
|
|
if opts.Edits.enableSecretScanningPushProtection != nil {
|
|
securityOptions.EnableSecretScanningPushProtection = &SecurityAndAnalysisStatus{
|
|
Status: boolToStatus(*opts.Edits.enableSecretScanningPushProtection),
|
|
}
|
|
}
|
|
return securityOptions
|
|
}
|
|
|
|
var validSquashMsgValues = []string{squashMsgDefault, squashMsgPRTitle, squashMsgPRTitleCommits, squashMsgPRTitleDescription}
|
|
|
|
func validateSquashMergeCommitMsg(value string) error {
|
|
if slices.Contains(validSquashMsgValues, value) {
|
|
return nil
|
|
}
|
|
return cmdutil.FlagErrorf("invalid value for --squash-merge-commit-message: %q. Valid values are: %s", value, strings.Join(validSquashMsgValues, ", "))
|
|
}
|
|
|
|
// transformSquashMergeOpts maps the user-facing squash merge commit message option
|
|
// to the two API fields: squash_merge_commit_title and squash_merge_commit_message.
|
|
func transformSquashMergeOpts(edits *EditRepositoryInput) {
|
|
if edits.squashMergeCommitMsg == nil {
|
|
return
|
|
}
|
|
var title, message string
|
|
switch *edits.squashMergeCommitMsg {
|
|
case squashMsgDefault:
|
|
title = "COMMIT_OR_PR_TITLE"
|
|
message = "COMMIT_MESSAGES"
|
|
case squashMsgPRTitle:
|
|
title = "PR_TITLE"
|
|
message = "BLANK"
|
|
case squashMsgPRTitleCommits:
|
|
title = "PR_TITLE"
|
|
message = "COMMIT_MESSAGES"
|
|
case squashMsgPRTitleDescription:
|
|
title = "PR_TITLE"
|
|
message = "PR_BODY"
|
|
default:
|
|
return
|
|
}
|
|
edits.SquashMergeCommitTitle = &title
|
|
edits.SquashMergeCommitMessage = &message
|
|
}
|