Skip to content

Commit da21415

Browse files
committed
chore: implement runRevert in readme
1 parent 61a64e8 commit da21415

3 files changed

Lines changed: 271 additions & 10 deletions

File tree

README.md

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,19 @@ commitdog reads what you actually changed and writes the message for you. You pi
3030

3131
## install
3232

33+
**Linux and macOS**
34+
3335
```sh
34-
curl -fsSL https://get.commitdog.dev | sh
36+
curl -fsSL https://aysdog.pages.dev/install-commitdog.sh | sh
3537
```
3638

37-
that's it. one command. works on Linux and macOS.
38-
39-
<details>
40-
<summary>Windows</summary>
39+
**Windows** — open PowerShell as Administrator and run:
4140

42-
Download `commitdog-windows-amd64.exe` from the [releases page](https://github.com/aysdog/commitdog/releases), rename it to `commitdog.exe` and add it to your PATH.
41+
```powershell
42+
irm https://aysdog.pages.dev/install-commitdog.ps1 | iex
43+
```
4344

44-
</details>
45+
downloads the binary, adds it to PATH automatically. restart your terminal and `commitdog` just works.
4546

4647
<details>
4748
<summary>build from source (needs Go 1.21+)</summary>
@@ -101,6 +102,36 @@ pick a number. press enter to push. that's the whole thing.
101102

102103
---
103104

105+
## made a mistake? revert it
106+
107+
```sh
108+
commitdog revert
109+
```
110+
111+
```
112+
recent commits:
113+
114+
1 63baabe docs(dummy): update dummy (2 minutes ago)
115+
2 3b7486d feat(auth): add refreshToken (1 hour ago)
116+
3 c90ace2 refactor: update 10 files (6 hours ago)
117+
4 57f7669 refactor(docs): update docs (6 hours ago)
118+
5 4403b0f refactor: update 13 files (6 hours ago)
119+
120+
[1-5] pick, [e] enter hash, [q] quit › 1
121+
122+
reverting 63baabe — docs(dummy): update dummy
123+
⚠ this creates a new revert commit. continue? [Y/n] ›
124+
125+
✓ reverted 63baabe
126+
127+
push to origin/main? [Y/n] ›
128+
✓ pushed to origin/main
129+
```
130+
131+
pick the bad commit. confirm. done. no git syntax needed.
132+
133+
---
134+
104135
## starting a brand new project
105136

106137
no more going to GitHub, creating a repo, copying the URL, setting the remote. commitdog does all of it:
@@ -143,6 +174,7 @@ commitdog init
143174
| command | what it does |
144175
|---------|-------------|
145176
| `commitdog` | suggest commit message for staged changes |
177+
| `commitdog revert` | pick from last 5 commits and revert |
146178
| `commitdog init` | create a GitHub repo and do the first push |
147179
| `commitdog setup` | configure email and GitHub token (do once) |
148180
| `commitdog --version` | print version |

main.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import (
55
"os"
66
)
77

8-
const version = "0.1.1"
8+
const version = "0.1.2"
99

1010
func main() {
1111
if len(os.Args) > 1 {
1212
switch os.Args[1] {
1313
case "--version", "-v":
14-
fmt.Printf("commitdog v%s\n", version)
15-
fmt.Println("zero-bs commits · no AI · no bs")
14+
fmt.Println("commitdog v" + version)
15+
fmt.Println("zero-bs commits · no AI · no telemetry")
1616
fmt.Println("aysdog.pages.dev")
1717
os.Exit(0)
1818
case "--help", "-h":
@@ -24,6 +24,9 @@ func main() {
2424
case "init":
2525
runInit()
2626
os.Exit(0)
27+
case "revert":
28+
runRevert()
29+
os.Exit(0)
2730
default:
2831
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
2932
fmt.Fprintf(os.Stderr, "run 'commitdog --help' for usage.\n")
@@ -76,6 +79,7 @@ usage:
7679
commitdog generate commit message from staged diff
7780
commitdog init create a new GitHub repo and do the first push
7881
commitdog setup configure email and GitHub token
82+
commitdog revert pick from last 5 commits and revert
7983
commitdog -v show version
8084
commitdog -h show this help
8185
@@ -89,6 +93,9 @@ workflow:
8993
git add .
9094
commitdog ← suggests message, commits, asks to push
9195
96+
oops:
97+
commitdog revert ← pick a commit to revert, push
98+
9299
no ai. no network (except init). no telemetry. just works.`)
93100
}
94101

revert.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
)
10+
11+
type commitEntry struct {
12+
hash string
13+
subject string
14+
date string
15+
}
16+
17+
func runRevert() {
18+
if err := verifyGitRepo(); err != nil {
19+
fatal("not a git repository.")
20+
}
21+
22+
commits, err := getRecentCommits(5)
23+
if err != nil {
24+
fatal("could not read git log: %v", err)
25+
}
26+
if len(commits) == 0 {
27+
fatal("no commits found in this repository.")
28+
}
29+
30+
fmt.Println()
31+
fmt.Println(" recent commits:")
32+
fmt.Println()
33+
34+
for i, c := range commits {
35+
fmt.Printf(" %d %s %s %s\n",
36+
i+1,
37+
colorDim(c.hash),
38+
c.subject,
39+
colorMuted("("+c.date+")"),
40+
)
41+
}
42+
43+
fmt.Println()
44+
fmt.Printf(" [1-%d] pick, [e] enter hash, [q] quit › ", len(commits))
45+
46+
var chosen string
47+
48+
for {
49+
input := readLine()
50+
51+
switch input {
52+
case "q", "quit", "exit":
53+
fmt.Println(" aborted.")
54+
return
55+
case "e", "edit":
56+
chosen = askForHash()
57+
if chosen == "" {
58+
return
59+
}
60+
goto revert
61+
}
62+
63+
for i, c := range commits {
64+
if input == fmt.Sprintf("%d", i+1) {
65+
chosen = c.hash
66+
goto revert
67+
}
68+
}
69+
70+
fmt.Printf(" enter 1-%d, e, or q › ", len(commits))
71+
}
72+
73+
revert:
74+
chosen = strings.TrimSpace(chosen)
75+
chosen = strings.ToLower(chosen)
76+
if !isSafeHash(chosen) {
77+
fatal("invalid commit hash: %q", chosen)
78+
}
79+
80+
subject := subjectForHash(chosen, commits)
81+
fmt.Println()
82+
if subject != "" {
83+
fmt.Printf(" reverting %s — %s\n", chosen[:7], subject)
84+
} else {
85+
fmt.Printf(" reverting %s\n", chosen[:7])
86+
}
87+
88+
fmt.Printf(" ⚠ this creates a new revert commit. continue? [Y/n] › ")
89+
confirm := readLine()
90+
if confirm == "n" || confirm == "no" {
91+
fmt.Println(" aborted.")
92+
return
93+
}
94+
95+
if err := gitRevert(chosen); err != nil {
96+
fatal("revert failed: %v\n\n tip: if there are conflicts, resolve them manually and run 'git revert --continue'", err)
97+
}
98+
99+
fmt.Printf("\n ✓ reverted %s\n", chosen[:7])
100+
101+
askPush()
102+
}
103+
104+
func getRecentCommits(n int) ([]commitEntry, error) {
105+
// use a unique separator that won't appear in commit messages
106+
sep := "|||"
107+
format := "%h" + sep + "%s" + sep + "%cr"
108+
109+
cmd := exec.Command(
110+
"git", "log",
111+
fmt.Sprintf("-%d", n),
112+
"--no-color",
113+
"--pretty=format:"+format,
114+
)
115+
cmd.Env = append(os.Environ(), "GIT_PAGER=cat")
116+
117+
var stdout, stderr bytes.Buffer
118+
cmd.Stdout = &stdout
119+
cmd.Stderr = &stderr
120+
121+
if err := cmd.Run(); err != nil {
122+
return nil, fmt.Errorf("%s", strings.TrimSpace(stderr.String()))
123+
}
124+
125+
lines := strings.Split(strings.TrimSpace(stdout.String()), "\n")
126+
var commits []commitEntry
127+
128+
for _, line := range lines {
129+
line = strings.TrimSpace(line)
130+
if line == "" {
131+
continue
132+
}
133+
parts := strings.SplitN(line, sep, 3)
134+
if len(parts) != 3 {
135+
continue
136+
}
137+
138+
hash := strings.TrimSpace(parts[0])
139+
subject := strings.TrimSpace(parts[1])
140+
date := strings.TrimSpace(parts[2])
141+
142+
if len(subject) > 60 {
143+
subject = subject[:57] + "..."
144+
}
145+
146+
if !isSafeHash(hash) {
147+
continue
148+
}
149+
150+
commits = append(commits, commitEntry{
151+
hash: hash,
152+
subject: subject,
153+
date: date,
154+
})
155+
}
156+
157+
return commits, nil
158+
}
159+
160+
func askForHash() string {
161+
fmt.Println()
162+
fmt.Printf(" enter commit hash (7-40 hex chars) › ")
163+
164+
for {
165+
input := strings.TrimSpace(readLine())
166+
input = strings.ToLower(input)
167+
168+
if input == "" || input == "q" {
169+
fmt.Println(" aborted.")
170+
return ""
171+
}
172+
if !isSafeHash(input) {
173+
fmt.Printf(" invalid hash — only hex characters (0-9, a-f), 7-40 chars › ")
174+
continue
175+
}
176+
return input
177+
}
178+
}
179+
180+
func gitRevert(hash string) error {
181+
cmd := exec.Command("git", "revert", hash, "--no-edit")
182+
cmd.Env = append(os.Environ(), "GIT_PAGER=cat")
183+
var stderr bytes.Buffer
184+
cmd.Stderr = &stderr
185+
if err := cmd.Run(); err != nil {
186+
msg := strings.TrimSpace(stderr.String())
187+
if msg == "" {
188+
return fmt.Errorf("git revert failed")
189+
}
190+
return fmt.Errorf("%s", msg)
191+
}
192+
return nil
193+
}
194+
195+
func isSafeHash(s string) bool {
196+
if len(s) < 7 || len(s) > 40 {
197+
return false
198+
}
199+
for _, c := range s {
200+
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
201+
return false
202+
}
203+
}
204+
return true
205+
}
206+
207+
func subjectForHash(hash string, commits []commitEntry) string {
208+
for _, c := range commits {
209+
if strings.HasPrefix(c.hash, hash) || strings.HasPrefix(hash, c.hash) {
210+
return c.subject
211+
}
212+
}
213+
return ""
214+
}
215+
216+
func colorDim(s string) string {
217+
return "\033[2m" + s + "\033[0m"
218+
}
219+
220+
func colorMuted(s string) string {
221+
return "\033[90m" + s + "\033[0m"
222+
}

0 commit comments

Comments
 (0)