diff --git a/README.md b/README.md index 6390262..77bce2a 100644 --- a/README.md +++ b/README.md @@ -94,12 +94,12 @@ The `--base-commit` flag allows you to specify a specific commit SHA to use as t **Basic base commit usage:** ```bash -gh commit -B main -m "fix: update configs" -b a1b2c3d4e5f6789012345678901234567890abcd file.txt +gh commit -B main -m "fix: update configs" -b a1b2c3d4e5f6789012345678901234567890abcd -f file.txt ``` **With fast-forward (simulates force push):** ```bash -gh commit -B main -m "fix: update configs" -b a1b2c3d4e5f6789012345678901234567890abcd -f file.txt +gh commit -B main -m "fix: update configs" -b a1b2c3d4e5f6789012345678901234567890abcd --allow-fast-forward -f file.txt ``` ### How It Works diff --git a/cmd/args.go b/cmd/args.go index e720627..0b1881a 100644 --- a/cmd/args.go +++ b/cmd/args.go @@ -53,13 +53,26 @@ var ( Type: "bool", } - HeadRefFlag = Flag{Short: "H", Long: "head-ref", Description: "The name of the branch created with the base ref being `--branch`. Only relevant if used in conjunction with the --use-pr flag.", Type: "string"} - PrTitleFlag = Flag{Short: "T", Long: "title", Description: "The title of the PR created. Only relevant if used in conjunction with the --use-pr flag. If not specified, the PR title will be the commit message.", Type: "string"} - PrDescFlag = Flag{Short: "D", Long: "pr-description", Description: "The description of the PR created. Only relevant if used in conjunction with the --use-pr flag. If not specified, the PR title will be the commit message.", Type: "string"} - PrLabelFlag = Flag{Short: "l", Long: "label", Description: "A list of labels to add to the PR created. Only relevant if used in conjunction with the --use-pr flag. Labels can be added recursively -- i.e. -l feature -l blocked.", Type: "stringSlice"} - AllFlag = Flag{Short: "A", Long: "all", Description: "Commit all tracked files that have changed. Only relevant if the target branch is the same as the local branch.", Type: "bool", Default: "false"} - Untracked = Flag{Short: "U", Long: "untracked", Description: "Include untracked files in the commit. Only relevant if used in conjunction with the --all flag.", Type: "bool", Default: "false"} - DryRun = Flag{Short: "d", Long: "dry-run", Description: "Show which files would be committed.", Type: "bool", Default: "false"} + HeadRefFlag = Flag{Short: "H", Long: "head-ref", Description: "The name of the branch created with the base ref being `--branch`. Only relevant if used in conjunction with the --use-pr flag.", Type: "string"} + PrTitleFlag = Flag{Short: "T", Long: "title", Description: "The title of the PR created. Only relevant if used in conjunction with the --use-pr flag. If not specified, the PR title will be the commit message.", Type: "string"} + PrDescFlag = Flag{Short: "D", Long: "pr-description", Description: "The description of the PR created. Only relevant if used in conjunction with the --use-pr flag. If not specified, the PR title will be the commit message.", Type: "string"} + PrLabelFlag = Flag{Short: "l", Long: "label", Description: "A list of labels to add to the PR created. Only relevant if used in conjunction with the --use-pr flag. Labels can be added recursively -- i.e. -l feature -l blocked.", Type: "stringSlice"} + AllFlag = Flag{Short: "A", Long: "all", Description: "Commit all tracked files that have changed. Only relevant if the target branch is the same as the local branch.", Type: "bool", Default: "false"} + Untracked = Flag{Short: "U", Long: "untracked", Description: "Include untracked files in the commit. Only relevant if used in conjunction with the --all flag.", Type: "bool", Default: "false"} + DryRun = Flag{Short: "d", Long: "dry-run", Description: "Show which files would be committed.", Type: "bool", Default: "false"} + BaseCommitFlag = Flag{ + Short: "b", + Long: "base-commit", + Description: "The base commit SHA to use as the base for your new commit", + Required: false, + Type: "string", + } + AllowFastForwardFlag = Flag{ + Long: "allow-fast-forward", + Description: "Fast-forwards the branch to the specified commit, then creates the new commit on top", + Required: false, + Type: "bool", + } ) var allFlags = []Flag{ @@ -73,6 +86,8 @@ var allFlags = []Flag{ AllFlag, Untracked, DryRun, + BaseCommitFlag, + AllowFastForwardFlag, } type PrSettings struct { @@ -84,8 +99,10 @@ type PrSettings struct { } type CommitSettings struct { - CommitMessage string - CommitToBranch string + CommitMessage string + CommitToBranch string + BaseCommit string + AllowFastForward bool } type RepoSettings struct { @@ -181,6 +198,8 @@ func ValidateAndConfigureRun(args []string, cmd *cobra.Command, rs *RepoSettings usePr, _ := cmd.Flags().GetBool(UsePrFlag.Long) branch, _ := cmd.Flags().GetString(BranchFlag.Long) commitMessage, _ := cmd.Flags().GetString(MessageFlag.Long) + baseCommit, _ := cmd.Flags().GetString(BaseCommitFlag.Long) + allowFastForward, _ := cmd.Flags().GetBool(AllowFastForwardFlag.Long) if usePr { headRef, _ := cmd.Flags().GetString(HeadRefFlag.Long) @@ -210,15 +229,19 @@ func ValidateAndConfigureRun(args []string, cmd *cobra.Command, rs *RepoSettings } commitSettings = &CommitSettings{ - CommitMessage: commitMessage, - CommitToBranch: headRef, + CommitMessage: commitMessage, + CommitToBranch: headRef, + BaseCommit: baseCommit, + AllowFastForward: allowFastForward, } } else { prSettings = nil commitSettings = &CommitSettings{ - CommitMessage: commitMessage, - CommitToBranch: branch, + CommitMessage: commitMessage, + CommitToBranch: branch, + BaseCommit: baseCommit, + AllowFastForward: allowFastForward, } } @@ -303,10 +326,16 @@ var rootCmd = &cobra.Command{ return fmt.Errorf("--message and --branch are both required flags") } + baseCommit, _ := cmd.Flags().GetString(BaseCommitFlag.Long) + allowFastForward, _ := cmd.Flags().GetBool(AllowFastForwardFlag.Long) + if allowFastForward && baseCommit == "" { + return fmt.Errorf("--%s is required if --%s is passed", BaseCommitFlag.Long, AllowFastForwardFlag.Long) + } + return nil }, RunE: func(cmd *cobra.Command, args []string) error { - + cmd.SilenceUsage = true path, err := ValidateLocalGit() if err != nil { return err diff --git a/cmd/execute.go b/cmd/execute.go index d2156dd..837e26f 100644 --- a/cmd/execute.go +++ b/cmd/execute.go @@ -79,38 +79,45 @@ func (rn *RunSettings) Commit() error { var err error var commitSha string - // Create branches so we don't have to worry about those errors later - if rn.PrSettings != nil { - commitSha, err = EnsureBranchesExist(rn.PrSettings.BaseRef, rn.PrSettings.HeadRef, rn.RepoSettings) + if rn.CommitSettings.BaseCommit == "" { + // Create branches so we don't have to worry about those errors later + if rn.PrSettings != nil { + commitSha, err = EnsureBranchesExist(rn.PrSettings.BaseRef, rn.PrSettings.HeadRef, rn.RepoSettings) + } else { + commitSha, err = EnsureBranchesExist(rn.CommitSettings.CommitToBranch, "", rn.RepoSettings) + } + if err != nil { + return fmt.Errorf("ensuring branch exists: %w", err) + } } else { - commitSha, err = EnsureBranchesExist(rn.CommitSettings.CommitToBranch, "", rn.RepoSettings) - } - if err != nil { - return err + commitSha = rn.CommitSettings.BaseCommit } // Commits reference trees. Trees have their own hashes. Get the hash // of the tip of the tree that we are pushing to currentTreeSha, err := GetTreeTip(commitSha) if err != nil { - return err + return fmt.Errorf("getting tree tip: %w", err) } blobs, err := CreateBlobs(rn.FileSelection) if err != nil { - return err + return fmt.Errorf("creating blobs: %w", err) } newTreeSha, err := CreateTree(currentTreeSha, blobs) - newCommit, err := CreateCommitFromTree(commitSha, newTreeSha, rn.CommitSettings.CommitMessage) + if err != nil { + return fmt.Errorf("creating tree: %w", err) + } + newCommit, err := CreateCommitFromTree(commitSha, newTreeSha, rn.CommitSettings.CommitMessage) if err != nil { - return err + return fmt.Errorf("creating commit from tree: %w", err) } - err = AssociateCommitWithBranch(rn.CommitSettings.CommitToBranch, newCommit) + err = AssociateCommitWithBranch(rn.CommitSettings.CommitToBranch, newCommit, rn.CommitSettings.AllowFastForward) if err != nil { - return err + return fmt.Errorf("associating commit with branch: %w", err) } if rn.PrSettings != nil { diff --git a/cmd/gh.go b/cmd/gh.go index d42baae..f45a4ef 100644 --- a/cmd/gh.go +++ b/cmd/gh.go @@ -221,10 +221,15 @@ func CreateCommitFromTree(latestCommit, treeSha, commitMessage string) (string, return newCommitResponse.Sha, nil } -func AssociateCommitWithBranch(branch string, commitSha string) error { +func AssociateCommitWithBranch(branch string, commitSha string, allowFastForward bool) error { body := map[string]interface{}{ "sha": commitSha, } + + if allowFastForward { + body["force"] = true + } + marshalled, _ := json.Marshal(body) err := client.Post(fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.Owner(), repo.Name(), branch), bytes.NewBuffer(marshalled), nil) if err != nil {