Skip to content

Commit

Permalink
✨ Add Git editor support
Browse files Browse the repository at this point in the history
Add functionality to act as a Git editor. This is similar to hooks
however this resolves the issue of duplicate message editors.

By taking over the editing capabilities, there are some complexities
with merge and rebase where Git commands can be provided. Comment blocks
also needed to be trimmed to prevent reflow converting the new lines
into commands (which were not understood and failed).

There are likely a number of edge cases not handled (or considered) and
this feature should be used with caution and is experimental.

Implementation allowed for merging the hooks and editor functionality
into reading a file as they were very similar.
  • Loading branch information
mikelorant committed Feb 19, 2023
1 parent fe73906 commit fd493a9
Show file tree
Hide file tree
Showing 28 changed files with 324 additions and 108 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,10 +370,56 @@ You can then commit changes with:
git co
```

### Prepare Message Hook

Committed can be installed as a Git prepare message hook. Be aware that any
existing `prepare-commit-msg` hook will not be replaced and it is necessary to
remove this hook before installing.

Installation:

```shell
committed hook --install
```

Removal:

```shell
committed hook --uninstall
```

### Editor

Committed can replace the default Git editor which allows commits to be applied
using `git commit`. Most of the standard Git command arguments can be used.

```shell
git config --global core.editor "committed --editor"
```

This can be removed with:

```shell
git config --global --unset-all core.editor
```

There are some limitations related to acting as an editor.

- Comment lines will be truncated to the width of the editor.
- Interactive rebasing and other operations which require edit commands may have
visual issues. The first command may be part of the summary.
- Author cannot be set. The configured Git author will be used and will be
selected using the default Git method (repository followed by global).
- When amending, subject line may be part of the body.
- When amending, emoji character or shortcode must be in the existing data set.
- When amending, summary will be truncated if more than 72 characters.
- When amending, trailers will be imported into the body.

### Amend

There are certain limitations when amending commits and it is recommended only
for use with commits created with Committed. The limitations are:
for use with commits created with Committed. The limitations share similarities
with using Git as an editor.

- Emoji character or shortcode must be in the existing data set.
- Trailers will be imported into the body.
Expand Down
28 changes: 24 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ type App struct {

req *commit.Request
opts commit.Options
hook bool
}

type Options struct {
Hook bool
}

func NewRootCmd(a App) *cobra.Command {
Expand Down Expand Up @@ -76,10 +81,12 @@ func NewRootCmd(a App) *cobra.Command {
cmd.Flags().StringVarP(&a.opts.SnapshotFile, "snapshot", "", defaultSnapshotFile, "Snapshot file location")
cmd.Flags().BoolVarP(&a.opts.DryRun, "dry-run", "", defaultDryRun, "Simulate applying a commit")
cmd.Flags().BoolVarP(&a.opts.Amend, "amend", "a", false, "Replace the tip of the current branch by creating a new commit")
cmd.Flags().BoolVarP(&a.opts.Hook.Enable, "hook", "", false, "Install and uninstall Git hook")
cmd.Flags().StringVarP(&a.opts.Hook.MessageFile, "message-file", "", "", "")
cmd.Flags().StringVarP(&a.opts.Hook.Source, "source", "", "", "")
cmd.Flags().StringVarP(&a.opts.Hook.SHA, "sha", "", "", "")
cmd.Flags().StringVarP(&a.opts.File.MessageFile, "editor", "", "", "")
cmd.Flags().BoolVarP(&a.hook, "hook", "", false, "")
cmd.Flags().StringVarP(&a.opts.File.MessageFile, "message-file", "", "", "")
cmd.Flags().StringVarP(&a.opts.File.Source, "source", "", "", "")
cmd.Flags().StringVarP(&a.opts.File.SHA, "sha", "", "", "")
cmd.Flags().MarkHidden("editor")
cmd.Flags().MarkHidden("hook")
cmd.Flags().MarkHidden("message-file")
cmd.Flags().MarkHidden("source")
Expand Down Expand Up @@ -123,6 +130,8 @@ func NewApp() App {
}

func (a *App) configure(opts commit.Options) error {
a.mode()

state, err := a.Commiter.Configure(opts)
switch {
case err == nil:
Expand Down Expand Up @@ -158,3 +167,14 @@ func (a *App) apply() error {

return nil
}

func (a *App) mode() {
switch {
case !a.hook && a.opts.File.MessageFile != "":
a.opts.Mode = commit.ModeEditor
case a.hook:
a.opts.Mode = commit.ModeHook
default:
a.opts.Mode = commit.ModeCommit
}
}
70 changes: 50 additions & 20 deletions internal/commit/commit.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package commit

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"

"github.com/mikelorant/committed/internal/config"
"github.com/mikelorant/committed/internal/emoji"
Expand Down Expand Up @@ -56,11 +58,11 @@ type Options struct {
SnapshotFile string
DryRun bool
Amend bool
Hook HookOptions
Mode Mode
File FileOptions
}

type HookOptions struct {
Enable bool
type FileOptions struct {
MessageFile string
Source string
SHA string
Expand All @@ -76,10 +78,19 @@ type Request struct {
Author repository.User
Amend bool
DryRun bool
Hook bool
File bool
MessageFile string
}

type Mode int

const (
ModeUnset Mode = iota
ModeCommit
ModeEditor
ModeHook
)

func New() Commit {
return Commit{
Emojier: emoji.New,
Expand Down Expand Up @@ -118,16 +129,15 @@ func (c *Commit) Configure(opts Options) (*State, error) {
return nil, fmt.Errorf("unable to get snapshot: %w", err)
}

var hook Hook

if opts.Hook.Enable {
hook, err = getHook(c.ReadFiler, opts.Hook)
var file File
if opts.Mode > ModeCommit {
file, err = readFile(c.ReadFiler, opts)
if err != nil {
return nil, fmt.Errorf("unable to get hook mesage: %w", err)
return nil, fmt.Errorf("unable to read message file: %w", err)
}
}

if opts.Hook.Enable && opts.Hook.SHA == "HEAD" {
if file.Amend {
opts.Amend = true
}

Expand All @@ -140,7 +150,7 @@ func (c *Commit) Configure(opts Options) (*State, error) {
Config: cfg,
Snapshot: snap,
Options: opts,
Hook: hook,
File: file,
}, nil
}

Expand All @@ -156,7 +166,7 @@ func (c *Commit) Apply(req *Request) error {
Footer: req.Footer,
Amend: req.Amend,
DryRun: req.DryRun,
Hook: req.Hook,
File: req.File,
MessageFile: req.MessageFile,
}

Expand Down Expand Up @@ -268,19 +278,39 @@ func getEmojis(emojier Emojier, cfg config.Config) *emoji.Set {
return emojier(fn)
}

func getHook(readFile ReadFiler, ho HookOptions) (Hook, error) {
msg, err := readFile(ho.MessageFile)
func readFile(readFile ReadFiler, opts Options) (File, error) {
data, err := readFile(opts.File.MessageFile)
if err != nil {
return Hook{}, fmt.Errorf("unable to read file: %w", err)
return File{}, fmt.Errorf("unable to read file: %w", err)
}

msg := string(data)

f := File{
Message: msg,
}

h := Hook{
Message: string(msg),
if isAmend(msg, opts) {
f.Amend = true
}

if ho.SHA == "HEAD" {
h.Amend = true
return f, nil
}

func isAmend(msg string, opts Options) bool {
if opts.Mode == ModeHook && opts.File.SHA == "HEAD" {
return true
}

r := strings.NewReader(msg)

scanner := bufio.NewScanner(r)

for scanner.Scan() {
if !strings.HasPrefix(scanner.Text(), "#") && strings.TrimSpace(scanner.Text()) != "" {
return true
}
}

return h, nil
return false
}
50 changes: 26 additions & 24 deletions internal/commit/commit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,12 @@ func TestConfigure(t *testing.T) {
},
},
{
name: "hook",
name: "file_hook",
args: args{
opts: commit.Options{
Hook: commit.HookOptions{
Enable: true,
Mode: commit.ModeHook,
File: commit.FileOptions{
MessageFile: "test",
},
},
},
Expand All @@ -306,20 +307,22 @@ func TestConfigure(t *testing.T) {
Config: config.Config{},
Emojis: &emoji.Set{},
Options: commit.Options{
Hook: commit.HookOptions{
Enable: true,
Mode: commit.ModeHook,
File: commit.FileOptions{
MessageFile: "test",
},
},
},
},
},
{
name: "hook_sha",
name: "file_hook_sha",
args: args{
opts: commit.Options{
Hook: commit.HookOptions{
Enable: true,
SHA: "HEAD",
Mode: commit.ModeHook,
File: commit.FileOptions{
MessageFile: "test",
SHA: "HEAD",
},
},
},
Expand All @@ -329,13 +332,14 @@ func TestConfigure(t *testing.T) {
Config: config.Config{},
Emojis: &emoji.Set{},
Options: commit.Options{
Hook: commit.HookOptions{
Enable: true,
SHA: "HEAD",
Mode: commit.ModeHook,
File: commit.FileOptions{
MessageFile: "test",
SHA: "HEAD",
},
Amend: true,
},
Hook: commit.Hook{
File: commit.File{
Amend: true,
},
},
Expand Down Expand Up @@ -434,17 +438,15 @@ func TestConfigure(t *testing.T) {
},
},
{
name: "hook_error",
name: "file_hook_error",
args: args{
opts: commit.Options{
Hook: commit.HookOptions{
Enable: true,
},
Mode: commit.ModeHook,
},
readFileErr: errMock,
},
want: want{
err: "unable to get hook mesage: unable to read file: error",
err: "unable to read message file: unable to read file: error",
},
},
}
Expand Down Expand Up @@ -586,31 +588,31 @@ func TestApply(t *testing.T) {
},
},
{
name: "hook",
name: "file",
args: args{
req: &commit.Request{
Apply: true,
Hook: true,
File: true,
},
},
want: want{
cfg: repository.Commit{
Hook: true,
File: true,
},
},
},
{
name: "hook_message",
name: "file_message",
args: args{
req: &commit.Request{
Apply: true,
Hook: true,
File: true,
MessageFile: "test",
},
},
want: want{
cfg: repository.Commit{
Hook: true,
File: true,
MessageFile: "test",
},
},
Expand Down
4 changes: 2 additions & 2 deletions internal/commit/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type State struct {
Config config.Config
Snapshot snapshot.Snapshot
Options Options
Hook Hook
File File
}

type Placeholders struct {
Expand All @@ -34,7 +34,7 @@ type Config struct {
Authors []repository.User
}

type Hook struct {
type File struct {
Amend bool
Message string
}
Expand Down
1 change: 1 addition & 0 deletions internal/commit/testdata/comment.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# 1234567890123456789012345678901234567890
1 change: 1 addition & 0 deletions internal/commit/testdata/comment.input
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# 1234567890123456789012345678901234567890
Loading

0 comments on commit fd493a9

Please sign in to comment.