Skip to content

Commit 8a7def7

Browse files
committed
WIP: reimplement patch, git-apply and jj-split for Emacs
1 parent acbbe21 commit 8a7def7

File tree

2 files changed

+239
-3
lines changed

2 files changed

+239
-3
lines changed

jj.el

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
;; Example mappings:
2+
3+
(defun my/example-bindings ()
4+
(progn
5+
6+
(global-set-key (kbd "C-c g g") '(lambda () (interactive) (shell-command "jj show --git")))
7+
8+
;; Split selected portions of the
9+
;; Use this inside "jj show --git" output because it needs a unified
10+
;; diff and a commit-ID or preferrably change-ID).
11+
(global-set-key (kbd "C-c g i") 'my/jj-split)
12+
13+
;; Apply (selected portions of the diff)
14+
(global-set-key (kbd "C-c g a") '(lambda () (interactive) (my/git-apply)))
15+
;; Apply, tolerating context mismatch.
16+
(global-set-key (kbd "C-c g 3") '(lambda () (interactive) (my/git-apply "--3way")))
17+
;; Revert
18+
(global-set-key (kbd "C-c g x") '(lambda () (interactive) (my/git-apply "--reverse")))
19+
20+
;; The following are useful when working without jj
21+
;; Stage
22+
(global-set-key (kbd "C-c g s") '(lambda () (interactive) (my/git-apply "--cached")))
23+
;; Unstage
24+
(global-set-key (kbd "C-c g u") '(lambda () (interactive) (my/git-apply "--reverse" "--cached")))
25+
;; Revert and unstage
26+
(global-set-key (kbd "C-c g X") '(lambda () (interactive) (my/git-apply "--reverse" "--index")))
27+
))
28+
29+
;; TODO
30+
;; - documentation
31+
;; - move to an actual plugin
32+
;; - make it more idiomatic (I haven't used Emacs in 6 years)
33+
34+
(defvar my/--parent-directory
35+
(file-name-directory (or load-file-name (buffer-file-name))))
36+
37+
(defun my/patch (&rest patch-cmd-argv)
38+
"
39+
TODO:
40+
- support multiple selections
41+
"
42+
(interactive)
43+
(progn
44+
(let*
45+
;; from Claude:
46+
;; Prompts: consider jj.el. Change my/patch such that if the region
47+
;; contains no newline, select the entire hunk (starting at
48+
;; the line starting with @@) before doing anything else.
49+
;;
50+
;; 1. it should also work if there is no region
51+
;; 2. make sure it works if the current hunk is the last hunk in the file
52+
((region-contains-newline
53+
(and (region-active-p)
54+
(string-match-p "\n" (buffer-substring (region-beginning) (region-end)))))
55+
(min-line
56+
(line-number-at-pos
57+
(if
58+
(region-active-p)
59+
(region-beginning)
60+
(point))))
61+
(max-line
62+
(line-number-at-pos
63+
(if
64+
(region-active-p)
65+
(region-end)
66+
(point)))))
67+
;; adapted from Claude:
68+
;; Prompt (on top of the above one;; ): also, if no region is
69+
;; active, ;; and the cursor is on a diff header line (starting
70+
;; with ;; "diff"), then select the entire file-diff, i.e. from
71+
;; the ;; "diff" line to the next one or the end of file
72+
(when (not (region-active-p))
73+
(save-excursion
74+
;; Check if cursor is on a diff header line
75+
(beginning-of-line)
76+
(if (looking-at "^diff ")
77+
;; Select entire file-diff from current line to next diff or end of buffer
78+
(progn
79+
(setq min-line (line-number-at-pos))
80+
(forward-line 1)
81+
(if (re-search-forward "^diff " nil t)
82+
;; Found next diff, go to line before it
83+
(progn
84+
(forward-line -1)
85+
(end-of-line)
86+
(setq max-line (line-number-at-pos)))
87+
;; No next diff found, go to end of buffer
88+
(goto-char (point-max))
89+
(setq max-line (line-number-at-pos))))
90+
;; Not on diff header, find current hunk (line starting with @@)
91+
(when (re-search-backward "^@@" nil t)
92+
(setq min-line (line-number-at-pos))
93+
;; Find end of current hunk (next @@ line or end of buffer)
94+
(forward-line 1)
95+
(if (re-search-forward "^@@" nil t)
96+
;; Found next hunk, go to line before it
97+
(progn
98+
(forward-line -1)
99+
(end-of-line)
100+
(setq max-line (line-number-at-pos)))
101+
;; No next hunk found, go to end of buffer
102+
(goto-char (point-max))
103+
(setq max-line (line-number-at-pos)))))))
104+
(let*
105+
((start-pos
106+
(save-excursion
107+
(goto-char (if (region-active-p) (region-beginning) (point)))
108+
(if (re-search-backward "^diff " nil t)
109+
(point)
110+
1)))
111+
(line-offset (- (line-number-at-pos start-pos) 1))
112+
(cmd
113+
(format
114+
;; NOTE: we redirect normal output to stderr because
115+
;; stdout is used to pipe back the remaining diff.
116+
"%s/rc/tools/patch-range.pl -print-remaining-diff %d %d %s '>&2'"
117+
my/--parent-directory
118+
(- min-line line-offset)
119+
(- max-line line-offset)
120+
(string-join
121+
(mapcar
122+
(lambda (arg)
123+
(shell-quote-argument arg 'POSIX))
124+
patch-cmd-argv)
125+
" ")
126+
))
127+
(lines-before (count-lines (point-min) (point-max)))
128+
)
129+
(if
130+
(not
131+
(eq
132+
0
133+
(shell-command-on-region
134+
start-pos
135+
(point-max)
136+
cmd
137+
nil
138+
'REPLACE
139+
"*patch-stderr*")))
140+
nil
141+
(progn
142+
;; from Claude:
143+
;; Prompt: [...] actually keep the goto-line but subtract
144+
;; from the target line number the net number of removed
145+
;; lines. you can assume that it's non-negative
146+
(let* ((lines-after (count-lines (point-min) (point-max)))
147+
(net-removed-lines (- lines-before lines-after))
148+
(adjusted-max-line (- max-line net-removed-lines)))
149+
(goto-line adjusted-max-line))
150+
t
151+
))))))
152+
153+
(defun my/git-apply (&rest args)
154+
(interactive)
155+
(apply
156+
'my/patch
157+
(append (list "git" "apply")
158+
args)))
159+
160+
(defun my/jj-split ()
161+
"
162+
TODO:
163+
- forward arguments (but ignore selections when passed a fileset)
164+
"
165+
(interactive)
166+
(let*
167+
((revision
168+
(save-mark-and-excursion
169+
(if
170+
(search-backward-regexp
171+
"^\\(?:commit\\|Change ID:\\) \\(\\w+\\)$"
172+
nil
173+
'NOERROR)
174+
(match-string 1)
175+
"@" ;; assume we're splitting the working copy commit
176+
)))
177+
(state-file
178+
(car
179+
(process-lines
180+
"mktemp"
181+
(format
182+
"%s/jj.el-split.XXXXXXXX"
183+
(or
184+
(getenv "TMPDIR")
185+
"/tmp")))))
186+
(is-description-empty
187+
(if
188+
(process-lines
189+
"jj" "log" "--no-graph" "--ignore-working-copy"
190+
"-r" revision "-T" "description")
191+
"false"
192+
"true"))
193+
;; Don't prompt for descriptions; instead clear the description of the first split.
194+
(description-editor
195+
(format
196+
"sh %s/rc/tools/jj-split-editor %s %s"
197+
my/--parent-directory
198+
is-description-empty
199+
state-file))
200+
)
201+
(and
202+
(my/patch
203+
(format
204+
"JJ_EDITOR=%s"
205+
(shell-quote-argument description-editor 'POSIX))
206+
"jj"
207+
"split"
208+
"-r"
209+
revision
210+
(format
211+
"--tool=%s/rc/tools/jj-split-tool"
212+
my/--parent-directory))
213+
;; The first split will inherit the change ID from this diff, if
214+
;; any. But typically -- when the diff is from "jj show --git" --
215+
;; the remaining diff corresponds to the second split. Update the
216+
;; change ID accordingly. Among other things, this means that multiple
217+
;; successive splits will create a simple, linear history.
218+
(when
219+
(and nil
220+
(not (string-equal revision "@")))
221+
(save-mark-and-excursion
222+
(progn
223+
(search-backward-regexp
224+
"^\\(?:commit\\|Change ID:\\) \\(\\w+\\)$")
225+
(end-of-line)
226+
(backward-word)
227+
(shell-command-on-region
228+
(point)
229+
(line-end-position)
230+
(format
231+
"jj log --no-graph --ignore-working-copy -r %s+ -T change_id"
232+
revision)
233+
nil
234+
'REPLACE)
235+
)
236+
)
237+
))))
238+

rc/tools/jj-split-editor

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ desc_file=$3
66
# If the original description was empty, we'll be called only once.
77
if ! $original_description_was_empty && [ -z "$(cat "$state_file")" ]; then
88
echo second >"$state_file"
9-
cmd=d
9+
:> "$desc_file"
1010
else
1111
rm "$state_file"
12-
cmd=
1312
fi
14-
kak -f "$cmd" "$desc_file"

0 commit comments

Comments
 (0)