all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
blob 6c2b8f4d588e2adb82167787b65e83b02892de24 45127 bytes (raw)
name: test/lisp/erc/erc-d/erc-d.el 	 # note: path name is non-authoritative(*)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
 
;;; erc-d.el --- A dumb test server for ERC -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021 Free Software Foundation, Inc.
;;
;; Version: 1.1
;;
;; This file is part of GNU Emacs.
;;
;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see
;; <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This is a netcat style server for testing ERC.  The "d" in the name
;; stands for "daemon" as well as for "dialog" (as well as for "dumb"
;; because this server isn't very smart).  It either spits out a
;; canned reply when an incoming request matches the expected regexp
;; or signals an error and dies.  The entry point function is
;; `erc-d-run'.
;;
;; Canned scripts, or "dialogs," should be Lisp-Data files containing
;; one or more request/reply forms like this:
;;
;; |  ((mode-chan 1.5 "MODE #chan")          ; request: tag, expr, regex
;; |   (0.1 ":irc.org 324 bob #chan +Cint")  ; reply: delay, content
;; |   (0.0 ":irc.org 329 bob #chan 12345")) ; reply: ...
;;
;; These are referred to as "exchanges."  The first element is a list
;; whose CAR is a descriptive "tag" and whose CDR is an incoming
;; "spec" representing an inbound message from the client.  The rest
;; of the exchange is composed of outgoing specs representing
;; server-to-client messages.  A tag can be any symbol (ideally unique
;; in the dialog), but a leading tilde means the request should be
;; allowed to arrive out of order (within the allotted time).
;;
;; The first element in an incoming spec is a number indicating the
;; maximum number of seconds to wait for a match before raising an
;; error.  The CDR is interpreted as the arguments of an `rx' form to
;; be matched against the raw request (stripped of its CRLF line
;; ending).  A "string-start" backslash assertion, "\\`", is prepended
;; to all patterns.
;;
;; Similarly, the leading number in an *outgoing* spec indicates how
;; many seconds to wait before sending the line, which is rendered by
;; concatenating the other members after evaluating each in place.
;; CRLF line endings are appended on the way out and should be absent.
;;
;; Recall that IRC is "asynchronous," meaning some flow intervals
;; don't jibe with lockstep request-reply semantics.  However, for our
;; purposes, grouping things as [input, output1, ..., outputN] makes
;; sense, even though input and output may be completely unrelated.
;;
;; Template interpolation:
;;
;; A rudimentary templating facility is provided for additional
;; flexibility.  However, it's best to keep things simple (even if
;; overly verbose), so others can easily tell what's going on at a
;; glance.  If necessary, consult existing tests for examples (grep
;; for the variables `erc-d-spec-vars' and `erc-d-match-handlers').
;;
;; Subprocess or in-process?:
;;
;; Running in-process confers better visibility and easier setup at
;; the cost of additional cleanup and resource wrangling.  With a
;; subprocess, cleanup happens by pulling the plug, but configuration
;; means loading a separate file or passing -eval "(forms...)" during
;; invocation.  In some cases, a subprocess may be the only option,
;; like when trying to avoid `require'ing this file.
;;
;; Dialog objects:
;;
;; For a given exchange, the first argument passed to a request
;; handler is the `erc-d-dialog' object representing the overall
;; conversation with the connecting peer.  It can be used to pass
;; information between handlers during a session.  Some important
;; items are:
;;
;; * name (symbol); name of the current dialog
;;
;; * queue (ring); a backlog of unhandled raw requests, minus CRLF
;; endings.
;;
;; * timers (list of timers); when run, these send messages originally
;; deferred as per the most recently matched exchange's delay info.
;; Normally, all outgoing messages must be sent before another request
;; is considered.  (See `erc-d--send-outgoing' for an escape hatch.)
;;
;; * hunks (iterator of iterators); unconsumed exchanges as read from
;; a Lisp-Data dialog file.  The exchange iterators being dispensed
;; themselves yield portions of member forms as a 2- or 3-part
;; sequence: [tag] delay spec.  (BTW, here, "hunk" just means "list of
;; exchange elements" and has nothing to do with
;; https://maclisp.info/pitmanual/hunks.html.)
;;
;; * vars (alist of cons pairs); for sharing state among template
;; functions during the lifetime of an exchange.  Initially populated
;; by `erc-d-spec-vars', these KEY/VALUE pairs are made available in
;; the template environment as bound variables.  Updates can be made
;; by exchange handlers (see `erc-d-match-handlers').  When VALUE is a
;; function, occurrences of KEY in an outgoing spec are replaced with
;; the result of calling VALUE with match data set appropriately.  See
;; `erc-d--render-entries' for details.
;;
;; * exchanges (ring of erc-d-exchange objects); activated hunks
;; allowed to match out of order, plus the current active exchange
;; being yielded from, if any. See `erc-d-exchange'.
;;
;; TODO
;;
;; - Remove unused functionality and simplify API
;; - Maybe migrate d-u and d-i dependencies here
;; - Maybe remove generators to help with debugging/deobfuscation
;; - Maybe convert bindings to cons pairs passable directly to `eval'

;;; Code:
(eval-and-compile (let ((dir (getenv "EMACS_TEST_DIRECTORY")))
                    (when dir
                      (load (concat dir "/lisp/erc/erc-d/erc-d-i") nil t)
                      (load (concat dir "/lisp/erc/erc-d/erc-d-u") nil t))))
(require 'ring)
(require 'erc-d-i)
(require 'erc-d-u)

(defvar erc-d-server-name "erc-d-server"
  "Default name of a server process and basis for its buffer name.
Only relevant when starting a server with `erc-d-run'.")

(defvar erc-d-server-fqdn "irc.example.org"
  "Usually the same as the server's RPL_MYINFO \"announced name\".
Possibly used by overriding handlers, like the one for PING, and/or
dialog templates for the sender portion of a reply message.")

(defvar erc-d-linger-secs nil
  "Seconds to wait before quitting for all dialogs.
For more granular control, use the provided LINGER `rx' variable (alone)
as the incoming template spec of a dialog's last exchange.")

(defvar erc-d-spec-vars nil
  "An alist of template bindings available to client dialogs.
Populate it when calling `erc-d-run', and the contents will be made
available to all client dialogs through the `erc-d-dialog' \"vars\"
field and (therefore) to all templates as variables when rendering.  For
example, a key/value pair like (network . \"oftc\") will cause instances
of the (unquoted) symbol `network' to be replaced with \"oftc\" in the
rendered template string.

This list provides default template bindings common to all dialogs.
Each new client-connection process makes a shallow copy on init, but the
usual precautions apply when mutating member items.  Within the span of
a dialog, updates not applicable to all exchanges should die with their
exchange.  See `erc-d--render-entries' for details.  In the unlikely
event that an exchange-specific handler is needed, see
`erc-d-match-handlers'.")

(defvar erc-d-match-handlers nil
  "A plist of exchange-tag symbols mapped to request-handler functions.
This is meant to address edge cases for which `erc-d-spec-vars' comes up
short.  These may include (1) needing access to the client process
itself and/or (2) adding or altering outgoing response templates before
rendering.  Note that (2) requires using `erc-d-exchange-rebind' instead
of manipulating exchange bindings directly.

The hook-like function `erc-d-on-match' calls any handler whose key is
`eq' to the tag of the currently matched exchange (passing the client
`erc-d-dialog' as the first argument and the current `erc-d-exchange'
object as the second).  The handler runs just prior to sending the first
response.")

(defvar erc-d-auto-pong t
  "Handle PING requests automatically.")

(defvar erc-d--in-process t
  "Whether the server is running in the same Emacs as ERT.")

(defvar erc-d--slow-mo nil
  "Adjustment for all incoming timeouts.
This is to allow for human interaction or a slow Emacs or CI runner.
The value is the number of seconds to extend all incoming spec timeouts
by on init.  If the value is a negative number, it's negated and
interpreted as a lower bound to raise all incoming timeouts to.  If the
value is a function, it should take an existing timeout in seconds and
return a replacement.")

(defconst erc-d--eof-sentinel "__EOF__")
(defconst erc-d--linger-sentinel "__LINGER__")
(defconst erc-d--drop-sentinel "__DROP__")

(defvar erc-d--clients nil
  "List containing all clients for this server session.")

;; Some :type names may just be made up (not actual CL types)

(cl-defstruct (erc-d-exchange)
  "Object representing a request/response unit from a canned dialog."
  (dialog nil :type erc-d-dialog) ; owning dialog
  (tag nil :type symbol) ;  a.k.a. tag, the caar
  (pattern nil :type string) ; regexp to match requests against
  (specs nil :type function) ; iterator, see `erc-d--render-entries'
  (timeout nil :type number) ; time allotted for current request
  (timer nil :type timer) ; match timer fires when timeout expires
  (bindings nil :type list) ; let-style bindings (KEY VAL) ...
  (rx-bindings nil :type list) ; rx-let bindings
  (labels nil :type list) ; let-style bindings (KEY VAL) ...
  ;; Post-match
  (match-data nil :type match-data) ; from the latest matched request
  (request nil :type string)) ; the original request sans CRLF

(cl-defstruct (erc-d-dialog)
  "Session state for managing a client conversation."
  (process nil :type process) ; client-connection process
  (name nil :type symbol) ; likely the interned stem of the file
  (queue nil :type ring) ; backlog of incoming lines to process
  (hunks nil :type function) ; nil when done; yields raw exchange hunks
  (timers nil :type list) ; unsent replies
  (vars nil :type list) ; template bindings for rendering
  (exchanges nil :type ring) ; ring of erc-d-exchange objects
  (handler nil :type boolean) ; control agent for managing dialog
  (state nil :type symbol) ; handler's last recorded control state
  (message nil :type erc-d-i-message) ; `erc-d-i-message'
  (match-handlers nil :type list) ; copy of `erc-d-match-handlers'
  (server-fqdn nil :type string) ; copy of `erc-d-server-fqdn'
  (finalizer nil :type function) ; custom teardown, passed dialog and exchange
  ;; Post-match history is a plist whose keys are exchange tags
  ;; (symbols) and whose values are a cons of match-data and request
  ;; values from prior matches.
  (history nil :type list))

(defun erc-d--initialize-client (process)
  "Initialize state variables used by a client PROCESS."
  ;; Discard server-only/owned props
  (process-put process :dialog-dialogs nil)
  (let* ((server (process-get process :server))
         (ex-it (pop (process-get server :dialog-dialogs)))
         (name (pop ex-it))
         ;; Copy handlers so they can self-mutate per process
         (mat-h (copy-sequence (process-get process :dialog-match-handlers)))
         (fqdn (copy-sequence (process-get process :dialog-server-fqdn)))
         (vars (copy-sequence (process-get process :dialog-vars)))
         (dialog (make-erc-d-dialog :name name
                                    :process process
                                    :queue (make-ring 5)
                                    :exchanges (make-ring 10)
                                    :match-handlers mat-h
                                    :server-fqdn fqdn)))
    ;; Add items expected by convenience commands like `erc-d-exchange-reload'.
    (setf (alist-get 'EOF vars) `(: ,erc-d--eof-sentinel eot)
          (alist-get 'LINGER vars) `(: ,erc-d--linger-sentinel eot)
          (alist-get 'DROP vars) `(: ,erc-d--drop-sentinel eot)
          (erc-d-dialog-vars dialog) vars
          (erc-d-dialog-hunks dialog) (erc-d--advance-or-die dialog
                                                             (funcall ex-it)))
    (iter-next (setf (erc-d-dialog-handler dialog)
                     (erc-d--command-handle-all dialog)))
    ;; Add reverse link
    (process-put process :dialog dialog)
    ;; Register client
    (push process erc-d--clients)
    (erc-d--on-request process)))

(defun erc-d-load-replacement-dialog (dialog replacement &optional skip)
  "Find REPLACEMENT among backlog and swap out current DIALOG's iterator.
With int SKIP, advance past that many exchanges."
  (let* ((process (erc-d-dialog-process dialog))
         (server (process-get process :server))
         (ex-it (assoc-default replacement
                               (process-get server :dialog-dialogs)
                               #'eq)))
    (setq ex-it (funcall ex-it))
    (when skip (while (not (zerop skip))
                 (iter-next ex-it)
                 (cl-decf skip)))
    (dolist (timer (erc-d-dialog-timers dialog))
      (cancel-timer timer))
    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
      (cancel-timer (erc-d-exchange-timer exchange)))
    (setf (erc-d-dialog-hunks dialog) (erc-d--advance-or-die dialog ex-it))
    (iter-next (setf (erc-d-dialog-handler dialog)
                     (erc-d--command-handle-all dialog)))))

(defvar erc-d--m-debug (getenv "ERC_D_DEBUG"))

(defmacro erc-d--m (process format-string &rest args)
  "Output ARGS using FORMAT-STRING somewhere depending on context.
PROCESS should be a client connection or a server network process."
  `(let ((format-string (if erc-d--m-debug
                            (concat (format-time-string "%s.%N: ")
                                    ,format-string)
                          ,format-string))
         (want-insert (and ,process erc-d--in-process)))
     (when want-insert
       (with-current-buffer (process-buffer (process-get ,process :server))
         (goto-char (point-max))
         (insert (concat (format ,format-string ,@args) "\n"))))
     (when (or erc-d--m-debug (not want-insert))
       (message format-string ,@args))))

(defmacro erc-d--log (process string &optional outbound)
  "Log STRING sent to (OUTBOUND) or received from PROCESS peer."
  `(let ((id (or (process-get ,process :log-id)
                 (let ((port (erc-d-u--get-remote-port ,process)))
                   (process-put ,process :log-id port)
                   port)))
         (name (erc-d-dialog-name (process-get ,process :dialog))))
     (if ,outbound
         (erc-d--m process "-> %s:%s %s" name id ,string)
       (dolist (line (split-string ,string "\r\n"))
         (erc-d--m process "<- %s:%s %s" name id line)))))

(defun erc-d--send (process string)
  "Send STRING to PROCESS peer."
  (erc-d--log process string 'outbound)
  (process-send-string process (concat string "\r\n")))

(define-inline erc-d--fuzzy-p (exchange)
  (inline-letevals (exchange)
    (inline-quote
     (let ((tag (symbol-name (erc-d-exchange-tag ,exchange))))
       (eq ?~ (aref tag 0))))))

(define-error 'erc-d-timeout "Timed out awaiting expected request")

(defun erc-d--finalize-dialog (dialog)
  "Delete client-connection and finalize DIALOG.
Return associated server."
  (let ((process (erc-d-dialog-process dialog)))
    (setq erc-d--clients (delq process erc-d--clients))
    (dolist (timer (erc-d-dialog-timers dialog))
      (cancel-timer timer))
    (dolist (exchange (ring-elements (erc-d-dialog-exchanges dialog)))
      (cancel-timer (erc-d-exchange-timer exchange)))
    (prog1 (process-get process :server)
      (delete-process process))))

(defun erc-d--teardown (&optional sig &rest msg)
  "Clean up processes and maybe send signal SIG using MSG."
  (unless erc-d--in-process
    (when sig
      (erc-d--m nil "%s %s" sig (apply #'format-message msg)))
    (kill-emacs (if msg 1 0)))
  (let (process servers)
    (while (setq process (pop erc-d--clients))
      (push (erc-d--finalize-dialog (process-get process :dialog)) servers))
    (dolist (server servers)
      (delete-process server)))
  (dolist (timer timer-list)
    (when (memq (timer--function timer) '(erc-d--send erc-d--iter-next))
      (erc-d--m nil "Stray timer found: %S" (timer--function timer))
      (cancel-timer timer)))
  (when sig
    (dolist (buf erc-d-u--canned-buffers)
      (kill-buffer buf))
    (setq erc-d-u--canned-buffers nil)
    (signal sig (list (apply #'format-message msg)))))

(defun erc-d--teardown-this-dialog-at-least (dialog)
  "Run `erc-d--teardown' after destroying DIALOG if it's the last one."
  (let ((server (process-get (erc-d-dialog-process dialog) :server))
        (us (erc-d-dialog-process dialog)))
    (erc-d--finalize-dialog dialog)
    (cl-assert (not (memq us erc-d--clients)))
    (unless (or (process-get server :dialog-dialogs)
                (catch 'other
                  (dolist (process erc-d--clients)
                    (when (eq (process-get process :server) server)
                      (throw 'other process)))))
      (push us erc-d--clients)
      (erc-d--teardown))))

(defun erc-d--expire (dialog exchange)
  "Raise timeout error for EXCHANGE.
This will start the teardown for DIALOG."
  (setf (erc-d-exchange-specs exchange) nil)
  (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
      (funcall finalizer dialog exchange)
    (erc-d--teardown 'erc-d-timeout "Timed out awaiting request: %s"
                     (list :name (erc-d-exchange-tag exchange)
                           :pattern (erc-d-exchange-pattern exchange)
                           :timeout (erc-d-exchange-timeout exchange)
                           :dialog (erc-d-dialog-name dialog)))))

;; Using `run-at-time' here allows test cases to examine replies as
;; they arrive instead of forcing tests to wait until an exchange
;; completes.  The `run-at-time' in `erc-d--command-meter-replies'
;; does the same.  When running as a subprocess, a normal while loop
;; with a `sleep-for' works fine (including with multiple dialogs).
;; FYI, this issue was still present in older versions that called
;; this directly from `erc-d--filter'.

(defun erc-d--on-request (process)
  "Handle one request for client-connection PROCESS."
  (when (process-live-p process)
    (let* ((dialog (process-get process :dialog))
           (queue (erc-d-dialog-queue dialog)))
      (unless (ring-empty-p queue)
        (let* ((parsed (ring-remove queue))
               (cmd (intern (erc-d-i-message.command parsed))))
          (setf (erc-d-dialog-message dialog) parsed)
          (erc-d-command dialog cmd)))
      (run-at-time nil nil #'erc-d--on-request process))))

(defun erc-d--drop-p (exchange)
  (string-search erc-d--drop-sentinel (erc-d-exchange-pattern exchange)))

(defun erc-d--linger-p (exchange)
  (string-search erc-d--linger-sentinel (erc-d-exchange-pattern exchange)))

(defun erc-d--fake-eof (dialog)
  "Simulate receiving a fictitious \"EOF\" message from peer."
  (setf (erc-d-dialog-message dialog) ; use downcase for internal cmds
        (make-erc-d-i-message :command "eof" :unparsed erc-d--eof-sentinel))
  (run-at-time nil nil #'erc-d-command dialog 'eof))

(defun erc-d--process-sentinel (process event)
  "Set up or tear down client-connection PROCESS depending on EVENT."
  (erc-d--m process "Connection %s: %s" process (string-trim-right event))
  (if (eq 'open (process-status process))
      (erc-d--initialize-client process)
    (let* ((dialog (process-get process :dialog))
           (exes (and dialog (erc-d-dialog-exchanges dialog))))
      (if (and exes (not (ring-empty-p exes)))
          (cond ((string-prefix-p "connection broken" event)
                 (erc-d--fake-eof dialog))
                ;; Ignore disconnecting peer when pattern is DROP
                ((and (string-prefix-p "deleted" event)
                      (erc-d--drop-p (ring-ref exes -1))))
                (t (erc-d--teardown)))
        (erc-d--teardown)))))

(defun erc-d--filter (process string)
  "Handle input received from peer.
PROCESS represents a client peer connection and STRING is a raw request
including line delimiters."
  (let ((queue (erc-d-dialog-queue (process-get process :dialog))))
    (setq string (concat (process-get process :stashed-input) string))
    (while (and string (string-match "\\(?:\r\n\\)+" string))
      (let ((line (substring string 0 (match-beginning 0))))
        (setq string (unless (= (match-end 0) (length string))
                       (substring string (match-end 0))))
        (erc-d--log process line nil)
        (ring-insert queue (erc-d-i--parse-message line 'decode))))
    (when string
      (setf (process-get process :stashed-input) string))))

;; Misc process properties:
;;
;; The server property `:dialog-dialogs' is an alist of (symbol
;; . iterator) conses, each of which pairs a dialog name with a
;; dispenser of exchange hunks (described above in the Commentary).
;; This list is populated by `erc-d-run' at the start of each session.
;;
;; Client-connection processes keep a reference to their server via a
;; `:server' property, which can be used to share info with other
;; clients.  There is currently no built-in way to do the same with
;; clients of other servers.  Clients also keep references to their
;; dialogs and raw messages via `:dialog' and `:stashed-input'.
;;
;; The logger stores a unique, human-friendly process name in the
;; client-process property `:log-id'.

(defun erc-d--start (host service name &rest plist)
  "Serve canned replies on HOST at SERVICE.
Return the new server process immediately when `erc-d--in-process' is
non-nil.  Otherwise, serve forever.  PLIST becomes the plist of the
server process and is used to initialize the plists of connection
processes.  NAME is used for the process and the buffer."
  (let* ((buf (get-buffer-create (concat "*" name "*")))
         (proc (make-network-process :server t
                                     :buffer buf
                                     :noquery t
                                     :filter #'erc-d--filter
                                     :sentinel #'erc-d--process-sentinel
                                     :name name
                                     :family (if host 'ipv4 'local)
                                     :coding 'binary
                                     :service (or service t)
                                     :host host
                                     :plist plist)))
    (process-put proc :server proc)
    ;; We don't have a minor mode, so use an arbitrary variable to mark
    ;; buffers owned by us instead
    (with-current-buffer buf (setq erc-d-u--process-buffer t))
    (erc-d--m proc "Starting network process: %S %S"
              proc (erc-d-u--format-bind-address proc))
    (if erc-d--in-process
        proc
      (while (process-live-p proc)
        (accept-process-output nil 0.01)))))

(defun erc-d--wrap-func-val (dialog exchange key func)
  "Return a form invoking FUNC when evaluated.
Arrange for FUNC to be called with the args it expects based on
the description in `erc-d--render-entries'."
  (let (args)
    ;; Ignore &rest or &optional
    (pcase-let ((`(,n . ,_) (func-arity func)))
      (cl-case n
        (0)
        (1 (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
                 args))
        (2 (push exchange args)
           (push (apply-partially #'erc-d-exchange-multi dialog exchange key)
                 args))
        (t (error "Incompatible function: %s" func))))
    `(save-match-data (apply #',func ',args))))

(defun erc-d-exchange-reload (dialog exchange)
  "Rebuild all bindings for EXCHANGE from those in DIALOG."
  (cl-loop for (key . val) in (erc-d-dialog-vars dialog)
           unless (keywordp key) do
           (push (list key val) (erc-d-exchange-bindings exchange))
           ;; Massage list so it's suitable for an `rx-list' binding.
           ;; IOW, handle cases in which VAL is ([ARGLIST] RX-FORM)
           ;; rather than just RX-FORM.  KEY becomes the binding name.
           (push (if (and (listp val)
                          (cdr val)
                          (not (cddr val))
                          (consp (car val)))
                     (cons key val)
                   (list key val))
                 (erc-d-exchange-rx-bindings exchange))
           and when (functionp val) do
           (setq val
                 (erc-d--wrap-func-val dialog exchange key val))
           (push (list key val) (erc-d-exchange-labels exchange))))

(defun erc-d-exchange-rebind (dialog exchange key val &optional export)
  "Modify a binding between renders.

Bind symbol KEY to VAL, replacing whatever existed before, which may
have been a function.  A third, optional argument, if present and
non-nil, results in the DIALOG's bindings for all EXCHANGEs adopting
this binding.  VAL can either be a function of the type described in
`erc-d--render-entries' or any value acceptable as an argument to the
function `concat'.

DIALOG and EXCHANGE are the current `erc-d-dialog' and `erc-d-exchange'
objects for the request context."
  (when export
    (setf (alist-get key (erc-d-dialog-vars dialog)) val))
  (if (functionp val)
      (setf (alist-get key (erc-d-exchange-labels exchange))
            (list (erc-d--wrap-func-val dialog exchange key val)))
    (setf (alist-get key (erc-d-exchange-labels exchange) nil 'rm) nil
          (alist-get key (erc-d-exchange-rx-bindings exchange)) (list val)
          (alist-get key (erc-d-exchange-bindings exchange)) (list val)))
  val)

(defun erc-d-exchange-match (exchange match-number &optional tag)
  "Return match portion of current or previous request.
MATCH-NUMBER is the match group number.  TAG, if provided, means the
exchange tag (name) from some previously matched request."
  (if tag
      (pcase-let* ((dialog (erc-d-exchange-dialog exchange))
                   (`(,m-d . ,req) (plist-get (erc-d-dialog-history dialog)
                                              tag)))
        (set-match-data m-d)
        (match-string match-number req))
    (match-string match-number (erc-d-exchange-request exchange))))

(defun erc-d-exchange-multi (dialog exchange key cmd &rest args)
  "Call CMD with ARGS.
This is a utility helper passed as the first argument to all template
functions.  DIALOG and EXCHANGE are pre-applied.  A few pseudo commands,
like `:request', are provided for convenience so that the caller's
definition doesn't have to include this file.

Command :get-var KEY looks up an item in `erc-d-dialog-vars'.  Command
:get-binding KEY looks up an item in `erc-d-exchange-bindings'.  Command
:set sets the template item triggered to a new VAL, optionally EXPORTing
it to `erc-d-dialog-vars'."
  ;; Parameter-based dependency injection would be nicer perhaps, but
  ;; that would require actually learning how Emacs works
  (pcase cmd
    (:set (apply #'erc-d-exchange-rebind dialog exchange key args))
    (:reload (apply #'erc-d-exchange-reload dialog exchange args))
    (:rebind (apply #'erc-d-exchange-rebind dialog exchange args))
    (:match (apply #'erc-d-exchange-match exchange args))
    (:request (erc-d-exchange-request exchange))
    (:match-data (erc-d-exchange-match-data exchange))
    (:dialog-name (erc-d-dialog-name dialog))
    (:get-binding (cadr (assq (erc-d-exchange-bindings exchange) (car args))))
    (:get-var (alist-get (car args) (erc-d-dialog-vars dialog)))))

(defmacro erc-d--prep-outgoing-entry (exchange entry)
  "Rewrite ENTRY for `erc-d--render-entries' using EXCHANGE."
  ;; If we changed the dialog bindings from proper pairs to cons pair,
  ;; we could pass it to `eval' directly and drop this `macroexp-let'.
  `(macroexp-let* (cl-loop for (k v) in (erc-d-exchange-bindings ,exchange)
                           collect (list k (macroexp-quote v)))
                  `(cl-symbol-macrolet ,(erc-d-exchange-labels ,exchange)
                     (set-match-data ',(erc-d-exchange-match-data ,exchange))
                     ,(cons 'concat ,entry))))

(defun erc-d--render-entries (dialog exchange entries)
  "Return an iterator producing rendered strings from canned ENTRIES.
When an entry's CAR is an arbitrary symbol, yield that back first, and
consider the entry an \"incoming\" entry.  Then, regardless of the
entry's type (incoming or outgoing), yield back the next element, which
should be a number representing either a timeout (incoming) or a
delay (outgoing).  After that, yield a rendered template (outgoing) or a
regular expression (incoming).

When evaluating a template, bind the keys in the alist stored in
DIALOG's `vars' field to its values, but skip any self-quoters, like
:foo.  When an entry is incoming, replace occurrences of a key with its
value, which can be any valid `rx' form (see Info node `(elisp)
Extending Rx').  Do the same when an entry is outgoing, but expect a
value's form to be (anything that evaluates to) something acceptable by
`concat' or, alternatively, a function that returns the latter (meaning
a string or nil).

Repeat the last two steps for the remaining entries, all of which are
assumed to be outgoing.  That is, continue yielding a timeout/delay and
a rendered string for each entry, and signal `iter-end-of-sequence'.

Once again, for an incoming entry, the yielded string is a regexp to be
matched against the raw request.  For outgoing, it's the final response,
ready to be sent out (after adding the appropriate line ending).

To help with testing, bindings are not automatically created from
DIALOG's \"vars\" alist when this function is invoked.  But this can be
forced by sending a non-nil value into the generator on the second
`iter-next' call of a given iteration.  This clobbers any temporary
bindings that don't exist in the DIALOG's `vars' alist, such as those
added via `erc-d-exchange-rebind' (unless \"exported\").

As noted earlier, template symbols can be bound to functions.  When
called during rendering, the match data from the current (matched)
request is accessible by calling the function `match-data'.

A function may ask for up to two required args, which are provided as
needed.  When applicable, the first required arg is a `funcall'-able
helper that accepts various keyword-based commands, like :rebind, and a
variable number of args.  See `erc-d-exchange-multi' for details.  When
specified, the second required arg is the current `erc-d-exchange'
object, which has among its members its owning `erc-d-dialog' object.
This should suffice as a safety valve for any corner-case needs.
Non-required args are ignored."
  (let (head
        entry
        (state 0))
    ;; Fake generator
    (lambda (iter-cmd yield-result)
      (cl-assert (eq iter-cmd :next))
      (unless entry
        (setq entry (iter-next entries)))
      (catch 'yield
        (while entry
          (cl-case state
            (0 (setq state 1)
               (throw 'yield
                      (setq head (pop entry))))
            (1 (setq state 2)
               (when yield-result
                 (erc-d-exchange-reload dialog exchange))
               (if (numberp head)
                   (setq entry (erc-d--prep-outgoing-entry exchange entry))
                 (throw 'yield
                        (prog1 (pop entry)
                          (setq entry
                                `(rx-let ,(erc-d-exchange-rx-bindings exchange)
                                   (rx bos ,@entry)))))))
            (2 (setq state 0)
               (throw 'yield
                      (prog1 (funcall `(lambda () ,entry))
                        (setq entry nil))))))))))

(defun erc-d-on-match (dialog exchange)
  "Handle matched exchange request.
Allow the first handler in `erc-d-match-handlers' whose key matches TAG
to manipulate replies before they're sent to the DIALOG peer."
  (when-let* ((tag (erc-d-exchange-tag exchange))
              (handler (plist-get (erc-d-dialog-match-handlers dialog) tag)))
    (let ((md (erc-d-exchange-match-data exchange)))
      (set-match-data md)
      (funcall handler dialog exchange))))

(iter-defun erc-d--send-outgoing (dialog exchange)
  "Send outgoing lines for EXCHANGE to DIALOG peer.
Assume the next spec is outgoing.  If its delay value is zero, render
the template and send the resulting message straight away.  Do the same
when DELAY is negative, only arrange for its message to be sent (abs
DELAY) seconds later, and then keep on processing.  If DELAY is
positive, pause processing and yield DELAY."
  (erc-d-on-match dialog exchange)
  (let ((specs (erc-d-exchange-specs exchange))
        (process (erc-d-dialog-process dialog))
        delay)
    ;; Consume till next request encountered or exhausted
    (while (setq delay (iter-next specs))
      (cond ((zerop delay) (erc-d--send process (iter-next specs)))
            ((< delay 0) (push (run-at-time (- delay) nil #'erc-d--send process
                                            (iter-next specs))
                               (erc-d-dialog-timers dialog)))
            (t (iter-yield delay)
               (erc-d--send process (iter-next specs)))))))

(defun erc-d--add-dialog-linger (dialog exchange)
  "Add finalizer for EXCHANGE in DIALOG."
  (erc-d--m (erc-d-dialog-process dialog)
            "Lingering for %.2f seconds" (erc-d-exchange-timeout exchange))
  (let ((start (current-time)))
    (setf (erc-d-dialog-finalizer dialog)
          (lambda (&rest _)
            (erc-d--m (erc-d-dialog-process dialog)
                      "Lingered for %.2f seconds"
                      (float-time (time-subtract (current-time) start)))
            (erc-d--teardown-this-dialog-at-least dialog)))))

(defun erc-d--add-dialog-drop (dialog exchange)
  "Add finalizer for EXCHANGE in DIALOG."
  (erc-d--m (erc-d-dialog-process dialog)
            "Dropping in %.2f seconds" (erc-d-exchange-timeout exchange))
  (setf (erc-d-dialog-finalizer dialog)
        (lambda (&rest _)
          (erc-d--m (erc-d-dialog-process dialog)
                    "Dropping %S" (erc-d-dialog-name dialog))
          (erc-d--finalize-dialog dialog))))

(defun erc-d--create-exchange (dialog hunk)
  "Initialize next exchange HUNK for DIALOG."
  (let* ((exchange (make-erc-d-exchange :dialog dialog))
         (specs (erc-d--render-entries dialog exchange hunk)))
    (setf (erc-d-exchange-specs exchange) specs
          (erc-d-exchange-tag exchange) (iter-next specs)
          (erc-d-exchange-timeout exchange) (iter-next specs t)
          (erc-d-exchange-pattern exchange) (iter-next specs))
    (cond ((erc-d--linger-p exchange)
           (erc-d--add-dialog-linger dialog exchange))
          ((erc-d--drop-p exchange)
           (erc-d--add-dialog-drop dialog exchange)))
    (setf (erc-d-exchange-timer exchange)
          (run-at-time (erc-d-exchange-timeout exchange)
                       nil #'erc-d--expire dialog exchange))
    exchange))

(cl-defun erc-d--command-consider (dialog)
  "Maybe return next matched exchange for DIALOG.
Upon encountering a mismatch, schedule a teardown unless only fuzzies
remain in the exchange pool, in which case, return nil."
  (let* ((parsed (erc-d-dialog-message dialog))
         (line (erc-d-i-message.unparsed parsed))
         (exes (erc-d-dialog-exchanges dialog))
         ;;
         matched)
    (let ((elts (ring-elements exes)))
      (while (and (setq matched (pop elts))
                  (not (string-match (erc-d-exchange-pattern matched) line)))
        (if (and (not elts) (erc-d--fuzzy-p matched))
            ;; Nothing to do, so advance
            (cl-return-from erc-d--command-consider nil)
          (cl-assert (or (not elts) (erc-d--fuzzy-p matched))))))
    (unless matched
      (erc-d--teardown 'error "Match failed: %S %S" line
                       (list :exes (mapcar #'erc-d-exchange-pattern
                                           (ring-elements exes))
                             :dialog (erc-d-dialog-name dialog))))
    (setf (erc-d-exchange-request matched) line
          (erc-d-exchange-match-data matched) (match-data)
          ;; Also add current to match history, indexed by exchange tag
          (plist-get (erc-d-dialog-history dialog)
                     (erc-d-exchange-tag matched))
          (cons (match-data) line)) ; do we need to make a copy of this?
    (cancel-timer (erc-d-exchange-timer matched))
    (ring-remove exes (ring-member exes matched))))

(defun erc-d--active-ex-p (ring)
  "Return non-nil when RING has a non-fuzzy exchange.
That is, return nil when RING is empty or when it only has exchanges
with leading-tilde tags."
  (let ((i 0)
        (len (ring-length ring))
        ex found)
    (while (and (not found) (< i len))
      (unless (erc-d--fuzzy-p (setq ex (ring-ref ring i)))
        (setq found ex))
      (cl-incf i))
    found))

(iter-defun erc-d--advance-or-die (dialog hunks)
  "Govern the lifetime of DIALOG.
Convert raw HUNKS into exchanges and insert them into the pool of
expected matches, as produced.  Run teardown when exhausted."
  (let ((exes (erc-d-dialog-exchanges dialog)))
    (iter-do (hunk hunks)
      ;; Wait till actually needed
      (while (erc-d--active-ex-p exes)
        (iter-yield 'deferring))
      (let ((exchange (erc-d--create-exchange dialog hunk)))
        (if (erc-d--fuzzy-p exchange)
            (ring-insert exes exchange)
          (ring-insert-at-beginning exes exchange))
        ;; This seems a bit safer than yielding the exchange directly
        (iter-yield 'matching)))
    ;; Only die after all exchanges (including fuzzies) have matched
    (while (not (ring-empty-p exes))
      (iter-yield 'depleted))
    ;; Linger logic for individual dialogs is handled elsewhere
    (if-let ((finalizer (erc-d-dialog-finalizer dialog)))
        (funcall finalizer dialog)
      (let ((d (process-get (erc-d-dialog-process dialog)
                            :dialog-linger-secs)))
        (push (run-at-time d nil #'erc-d--teardown)
              (erc-d-dialog-timers dialog))))
    (iter-yield 'done)))

(defalias 'erc-d--iter-next #'iter-next) ; detect which timer funcs are ours

(iter-defun erc-d--command-meter-replies (dialog exchange)
  "Ignore requests until all replies have been sent.
Do this for some previously matched EXCHANGE in DIALOG."
  (iter-do (delay (erc-d--send-outgoing dialog exchange))
    (push (run-at-time delay nil #'erc-d--iter-next
                       (erc-d-dialog-handler dialog) 'resume)
          (erc-d-dialog-timers dialog))
    (unless (eq 'resume (iter-yield (erc-d-dialog-state dialog)))
      (while (not (eq 'resume (iter-yield (setf (erc-d-dialog-state dialog)
                                                'sending))))))))

(defun erc-d--die-unexpected (dialog)
  (erc-d--teardown 'error "Received unexpected input: %S"
                   (erc-d-i-message.unparsed (erc-d-dialog-message dialog))))

(iter-defun erc-d--command-handle-all (dialog)
  "Act as control agent and process next request for DIALOG.
Receive symbols representing IRC or control commands (lowercase).  And
yield symbols indicating the most recent control state.  Both are
only used for debugging."
  (let ((matched t))
    (iter-do (state (erc-d-dialog-hunks dialog))
      (if matched
          (cl-case (iter-yield (setf (erc-d-dialog-state dialog) state))
            ;; TODO remove these internal commands if unneeded
            ('eof (erc-d--m (erc-d-dialog-process dialog) "Received an EOF")))
        (cl-assert (memq state '(matching depleted)) t)
        (when (eq 'depleted state)
          (erc-d--die-unexpected dialog)))
      (when (ring-empty-p (erc-d-dialog-exchanges dialog))
        (erc-d--die-unexpected dialog))
      (when (setq matched (erc-d--command-consider dialog))
        (iter-yield-from (erc-d--command-meter-replies dialog matched))))))


;;;; Handlers for IRC commands

(cl-defgeneric erc-d-command ((dialog erc-d-dialog) cmd)
  "Handle new CMD from client for DIALOG.
By default, defer to this dialog's `erc-d--command-handle-all' instance,
which is stored in its `handler' field."
  (condition-case _err
      (when (eq 'sending (iter-next (erc-d-dialog-handler dialog) cmd))
        (ring-insert-at-beginning (erc-d-dialog-queue dialog)
                                  (erc-d-dialog-message dialog)))
    (iter-end-of-sequence (erc-d--teardown)))) ; likely never runs

;; A similar PONG handler would be useless because we know when to
;; expect them

(cl-defmethod erc-d-command ((dialog erc-d-dialog) (_cmd (eql PING))
                             &context (erc-d-auto-pong (eql t)))
  "Respond to PING request from DIALOG peer when ERC-D-AUTO-PONG is t."
  (let* ((parsed (erc-d-dialog-message dialog))
         (process (erc-d-dialog-process dialog))
         (nonce (car (erc-d-i-message.command-args parsed)))
         (fqdn (erc-d-dialog-server-fqdn dialog)))
    (erc-d--send process (format ":%s PONG %s :%s" fqdn fqdn nonce))))


;;;; Entry points

(defun erc-d-run (host service &optional server-name &rest dialogs)
  "Start serving DIALOGS on HOST at SERVICE.
Pass HOST and SERVICE directly to `make-network-process'.  When present,
use string SERVER-NAME for the server-process name as well as that of
its buffer (w. surrounding asterisks).  When absent, do the same with
`erc-d-server-name'.  When running \"in process,\" return the server
process, otherwise sleep for the duration of the server process.

A dialog must be a symbol matching the base name of a dialog file in
`erc-d-u-canned-dialog-dir'.

The variable `erc-d-spec-vars' determines the common members of the
`erc-d--render-entries' ENTRIES param.  Variables `erc-d-server-fqdn'
and `erc-d-linger-secs' determine the `erc-d-dialog' items
`:server-fqdn' and `:linger-secs' for all client processes.

The variable `erc-d-spec-vars' can be used to initialize the
process's `erc-d-dialog' vars item."
  (when erc-d--slow-mo
    (message "Slow mo is ON"))
  (when (and server-name (symbolp server-name))
    (push server-name dialogs)
    (setq server-name nil))
  (let (loaded)
    (dolist (dialog (nreverse dialogs))
      (let ((ex-it (erc-d-u--canned-load-dialog dialog)))
        (when erc-d--slow-mo
          (setq ex-it (erc-d-u--rewrite-for-slow-mo erc-d--slow-mo ex-it)))
        (push (cons (erc-d-u--normalize-canned-name dialog) ex-it) loaded)))
    (setq dialogs loaded))
  (erc-d--start host service (or server-name erc-d-server-name)
                :dialog-dialogs dialogs
                :dialog-vars erc-d-spec-vars
                :dialog-linger-secs erc-d-linger-secs
                :dialog-server-fqdn erc-d-server-fqdn
                :dialog-match-handlers (erc-d-u--unkeyword
                                        erc-d-match-handlers)))

(defun erc-d-serve ()
  "Start serving canned dialogs from the command line.
Although not autoloaded, this function is meant to be summoned via the
Emacs -f flag while starting a batch session.  It prints incoming and
outgoing messages to standard out.

The main options are --host HOST and --port PORT, which default to
localhost and auto, respectively.  The args are the dialogs to run.
Unlike with `erc-d-run', dialogs here *must* be file paths, meaning
Lisp-Data files adhering to the required format.  (These consist of
\"specs\" detailing timing and template info; see commentary for
specifics.)

An optional --add-time N option can also be passed to hike up timeouts
by some number of seconds N.  For example, you might run:

  $ emacs -Q -batch -L . \\
  >   -l erc-d.el \\
  >   -f erc-d-serve \\
  >   --host 192.168.124.1 \\
  >   --port 16667 \\
  >   --add-time 10 \\
  >   ./my-dialog.lispdata

from a Makefile or manually with \\<global-map>\\[compile]. And then in
another terminal, do:

  $ nc -C 192.168.124.1 16667 ; or telnet if your nc doesn't have -C
  > PASS changeme
  ...

Use `erc-d-run' instead to start the server from within Emacs."
  (unless noninteractive
    (error "Command-line func erc-d-serve not run in -batch session"))
  (setq erc-d--in-process nil)
  (let (port host dialogs erc-d--slow-mo)
    (while command-line-args-left
      (pcase (pop command-line-args-left)
        ("--add-time" (setq erc-d--slow-mo
                            (string-to-number (pop command-line-args-left))))
        ("--linger" (setq erc-d-linger-secs
                          (string-to-number (pop command-line-args-left))))
        ("--host" (setq host (pop command-line-args-left)))
        ("--port" (setq port (string-to-number (pop command-line-args-left))))
        (dialog (push dialog dialogs))))
    (setq dialogs (mapcar #'erc-d-u--massage-canned-name dialogs))
    (apply #'erc-d-run (or host "localhost") port nil (nreverse dialogs))))

(provide 'erc-d)

;;; erc-d.el ends here

debug log:

solving 6c2b8f4d58 ...
found 6c2b8f4d58 in https://yhetil.org/emacs/874k8i4tb6.fsf@neverwas.me/

applying [1/1] https://yhetil.org/emacs/874k8i4tb6.fsf@neverwas.me/
diff --git a/test/lisp/erc/erc-d/erc-d.el b/test/lisp/erc/erc-d/erc-d.el
new file mode 100644
index 0000000000..6c2b8f4d58

Checking patch test/lisp/erc/erc-d/erc-d.el...
Applied patch test/lisp/erc/erc-d/erc-d.el cleanly.

index at:
100644 6c2b8f4d588e2adb82167787b65e83b02892de24	test/lisp/erc/erc-d/erc-d.el

(*) Git path names are given by the tree(s) the blob belongs to.
    Blobs themselves have no identifier aside from the hash of its contents.^

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.