Skip to content

Commit

Permalink
✨ Add hook support
Browse files Browse the repository at this point in the history
Git supports hooks that can run before the editor is displayed. A
prepare commit message hook was added to apply a commit using the
default Git commit command.

An install and uninstall flag was added and checks for a signature. This
signature prevents an unmanaged hook from being updated or deleted.

By default Git adds comment blocks to the message file. These comments
were displayed when applying a commit. A change has been made to the
output to remove these comments to reflect the actual commit message.

The hook will not be used for commits that are classified as merge,
squash or template.

There are a number of limitations in the existing implementation:

- Changing the author is not supported. The hook only allows updating
  of the message and no other fields.

- The default editor will be displayed after the commit is accepted.
  This is how prepare commit message hook works as it has been designed
  to work before the editor is started to modify the default message.
  • Loading branch information
mikelorant committed Feb 18, 2023
1 parent cbc8836 commit d3604de
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 14 deletions.
16 changes: 16 additions & 0 deletions cmd/hook.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package cmd

import (
"fmt"

"github.com/mikelorant/committed/internal/hook"
"github.com/spf13/cobra"
)

const (
hookInstallSuccess = "✅ Hook installed."
hookUninstallSuccess = "❎ Hook uninstalled."
)

func NewHookCmd(a App) *cobra.Command {
var hookOptions hook.Options

Expand All @@ -19,6 +26,15 @@ func NewHookCmd(a App) *cobra.Command {

if err := a.Hooker.Do(hookOptions); err != nil {
a.Logger.Fatalf("Unable to install or uninstall hook.")

return
}

switch {
case hookOptions.Install:
fmt.Fprintln(a.Writer, hookInstallSuccess)
case hookOptions.Uninstall:
fmt.Fprintln(a.Writer, hookUninstallSuccess)
}
},
}
Expand Down
10 changes: 10 additions & 0 deletions cmd/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,17 @@ func TestHookCmd(t *testing.T) {
args args
want want
}{
{
name: "empty",
args: args{
args: []string{"--"},
},
},
{
name: "help",
args: args{
args: []string{"--help"},
},
},
{
name: "install",
Expand Down Expand Up @@ -100,6 +109,7 @@ func TestHookCmd(t *testing.T) {
a := cmd.App{
Hooker: &h,
Logger: mlog,
Writer: &buf,
}

hook := cmd.NewHookCmd(a)
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions cmd/testdata/install.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
✅ Hook installed.
1 change: 1 addition & 0 deletions cmd/testdata/uninstall.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
❎ Hook uninstalled.
13 changes: 13 additions & 0 deletions internal/ui/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,16 @@ func defaultAmendSave(st *commit.State) savedState {

return s
}

func defaultHookSave(st *commit.State) savedState {
s := savedState{
summary: commit.MessageToSummary(st.Hook.Message),
body: commit.MessageToBody(st.Hook.Message),
}

if e := commit.MessageToEmoji(st.Emojis, st.Hook.Message); e.Valid {
s.emoji = e.Emoji
}

return s
}
27 changes: 25 additions & 2 deletions internal/ui/message/message.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package message

import (
"bufio"
"fmt"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
Expand Down Expand Up @@ -50,8 +52,9 @@ func (m Model) View() string {
message = m.styles.summary.Render(s)
}

if m.body != "" {
b := m.styles.body.Render(m.body)
body := removeComments(m.body)
if body != "" {
b := m.styles.body.Render(body)
message = lipgloss.JoinVertical(lipgloss.Top, message, b)
}

Expand All @@ -62,3 +65,23 @@ func (m Model) View() string {

return m.styles.message.Render(message)
}

func removeComments(str string) string {
var sb strings.Builder

r := strings.NewReader(str)

scanner := bufio.NewScanner(r)

for scanner.Scan() {
txt := scanner.Text()

if strings.HasPrefix(txt, "#") {
continue
}

fmt.Fprintln(&sb, txt)
}

return strings.TrimSpace(sb.String())
}
25 changes: 25 additions & 0 deletions internal/ui/message/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,31 @@ func TestModel(t *testing.T) {
footer: "footer",
},
},
{
name: "comments_body",
args: args{
emoji: ":art:",
summary: "summary",
body: "# body",
},
},
{
name: "comments_body_mixed",
args: args{
emoji: ":art:",
summary: "summary",
body: "line 1\n# line 2\nline 3\n",
},
},
{
name: "comments_body_footer",
args: args{
emoji: ":art:",
summary: "summary",
body: "# body",
footer: "footer",
},
},
}

for _, tt := range tests {
Expand Down
2 changes: 2 additions & 0 deletions internal/ui/message/testdata/comments_body.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:art: summary

4 changes: 4 additions & 0 deletions internal/ui/message/testdata/comments_body_footer.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:art: summary

footer

5 changes: 5 additions & 0 deletions internal/ui/message/testdata/comments_body_mixed.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:art: summary

line 1
line 3

22 changes: 19 additions & 3 deletions internal/ui/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,29 @@ func (m *Model) backupModel() savedState {
}

func (m *Model) setSaves() {
m.amend = m.state.Options.Amend
m.hook = m.state.Options.Hook.Enable
m.amend = m.state.Options.Amend || m.state.Hook.Amend

switch m.amend {
case true:
m.currentSave = defaultAmendSave(m.state)
switch m.state.Hook.Amend {
case true:
m.currentSave = defaultHookSave(m.state)
default:
m.currentSave = defaultAmendSave(m.state)
}

case false:
m.previousSave = defaultAmendSave(m.state)
if m.hook {
m.currentSave = defaultHookSave(m.state)
}

switch m.state.Hook.Amend {
case true:
m.previousSave = defaultHookSave(m.state)
default:
m.previousSave = defaultAmendSave(m.state)
}
}
}

Expand Down
21 changes: 12 additions & 9 deletions internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Model struct {
models Models
quit quit
amend bool
hook bool
signoff bool
err error
ready bool
Expand Down Expand Up @@ -135,7 +136,7 @@ func (m *Model) Configure(state *commit.State) {
m.restoreModel(m.currentSave)
m.setCompatibility()

if m.state.Snapshot.Restore && m.setSave() {
if (m.state.Snapshot.Restore && m.setSave()) || m.hook {
m.resetCursor()
}
}
Expand Down Expand Up @@ -421,13 +422,15 @@ func (m Model) commit(q quit) Model {
}

m.Request = &commit.Request{
Author: m.models.info.Author,
Emoji: emoji,
Summary: m.models.header.Summary(),
Body: m.models.body.Value(),
RawBody: m.models.body.RawValue(),
Footer: m.models.footer.Value(),
Amend: m.amend,
Author: m.models.info.Author,
Emoji: emoji,
Summary: m.models.header.Summary(),
Body: m.models.body.Value(),
RawBody: m.models.body.RawValue(),
Footer: m.models.footer.Value(),
Amend: m.amend,
Hook: m.hook,
MessageFile: m.state.Options.Hook.MessageFile,
}

if m.quit == applyQuit {
Expand All @@ -443,7 +446,7 @@ func (m Model) validate() bool {
staged := m.state.Repository.Worktree.IsStaged()
summary := m.models.header.Summary()

return (staged || m.amend) && summary != ""
return (staged || m.amend) && (summary != "" || m.hook)
}

func (m *Model) resetCursor() {
Expand Down

0 comments on commit d3604de

Please sign in to comment.