forked from filsinger/p4.el
-
Notifications
You must be signed in to change notification settings - Fork 29
/
p4.el
3735 lines (3298 loc) · 147 KB
/
p4.el
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
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; p4.el --- Simple Perforce-Emacs Integration
;; Copyright (c) 1996-1997 Eric Promislow
;; Copyright (c) 1997-2004 Rajesh Vaidheeswarran
;; Copyright (c) 2005 Peter Osterlund
;; Copyright (c) 2009 Fujii Hironori
;; Copyright (c) 2012 Jason Filsinger
;; Copyright (c) 2013-2015 Gareth Rees <gdr@garethrees.org>
;; Author: Gareth Rees <gdr@garethrees.org>
;; URL: https://github.com/gareth-rees/p4.el
;; Version: 12.0
;;; Commentary:
;; p4.el integrates the Perforce software version management system
;; into Emacs. It is designed for users who are familiar with Perforce
;; and want to access it from Emacs: it provides Emacs interfaces that
;; map directly to Perforce commands.
;;; License:
;; 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 2 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, write to the Free Software
;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
;;; Installation:
;; In your .emacs, ensure the path to the directory containing p4.el
;; is in the `load-path' variable:
;;
;; (add-to-list 'load-path "/full/path/to/dir/containing/p4.el/")
;;
;; Then load the library:
;;
;; (require 'p4)
;;
;; By default, the P4 global key bindings start with C-x p. If you
;; prefer a different key prefix, then you should customize the
;; setting `p4-global-key-prefix'.
;;
;; To compile the Perforce help text into the Emacs documentation
;; strings for each command, you must byte-compile this file:
;;
;; $ emacs -Q -batch -f batch-byte-compile /full/path/to/file/p4.el
;;; Code:
(require 'compile) ; compilation-error-regexp-alist
(require 'comint) ; comint-check-proc
(require 'dired) ; dired-get-filename
(require 'diff-mode) ; diff-font-lock-defaults, ...
(require 'ps-print) ; ps-print-ensure-fontified
(eval-when-compile (require 'cl)) ; defstruct, loop, dolist, lexical-let, ...
(defvar p4-version "12.0" "Perforce-Emacs Integration version.")
;; Forward declarations to avoid byte-compile warning "reference to
;; free variable"
(defvar p4-global-key-prefix)
(defvar p4-basic-mode-map)
(defvar p4-annotate-mode-map)
;;; User options:
(defgroup p4 nil "Perforce VC System." :group 'tools)
(eval-and-compile
;; This is needed at compile time by p4-help-text.
(defcustom p4-executable
(locate-file "p4" (append exec-path '("/usr/local/bin" "~/bin" ""))
(if (memq system-type '(ms-dos windows-nt)) '(".exe")))
"The p4 executable."
:type 'string
:group 'p4))
(defcustom p4-cygpath-exec "cygpath"
"Path to cygpath binary on cygwin systems."
:type 'string
:group 'p4)
(defcustom p4-default-diff-options "-du"
"Options to pass to diff, diff2, describe, and resolve.
Set to:
-dn (RCS)
-dc[n] (context; optional argument specifies number of context lines)
-ds (summary)
-du[n] (unified; optional argument specifies number of context lines)
-db (ignore whitespace changes)
-dw (ignore whitespace)
-dl (ignore line endings)"
:type 'string
:group 'p4)
(defcustom p4-auto-refresh t
"If non-NIL, automatically refresh files under Perforce control
when they change on disk."
:type 'boolean
:group 'p4)
(defcustom p4-check-empty-diffs nil
"If non-NIL, check for files with empty diffs before submitting."
:type 'boolean
:group 'p4)
(defcustom p4-follow-symlinks nil
"If non-NIL, call `file-truename' on all opened files."
:type 'boolean
:group 'p4)
(defcustom p4-synchronous-commands '(add delete edit lock logout reopen revert
unlock)
"List of Perforce commands that are run synchronously."
:type (let ((cmds '(add branch branches change changes client clients delete
describe diff diff2 edit filelog files fix fixes flush
fstat group groups have info integ job jobs jobspec label
labels labelsync lock logout move opened passwd print
reconcile reopen revert set shelve status submit sync
tickets unlock unshelve update user users where)))
(cons 'set (loop for cmd in cmds collect (list 'const cmd))))
:group 'p4)
(defcustom p4-password-source nil
"Action to take when Perforce needs a password.
If NIL, prompt the user to enter password.
Otherwise, this is a string containing a shell command that
prints the password. This command is run in an environment where
P4PORT and P4USER and set from the current Perforce settings."
:type '(radio (const :tag "Prompt user to enter password." nil)
(const :tag "Fetch password from OS X Keychain.\n\n\tFor each Perforce account, use Keychain Access to create an\n\tapplication password with \"Account\" the Perforce user name\n\t(P4USER) and \"Where\" the Perforce server setting (P4PORT).\n"
"security find-generic-password -s $P4PORT -a $P4USER -w")
(const :tag "Fetch password from Python keyring.\n\n\tFor each Perforce account, run:\n\t python -c \"import keyring,sys;keyring.set_password(*sys.argv[1:])\" \\\n\t P4PORT P4USER PASSWORD\n\treplacing P4PORT with the Perforce server setting, P4PORT with the\n\tPerforce user name, and PASSWORD with the password.\n"
"python -c \"import keyring, sys; print(keyring.get_password(*sys.argv[1:3]))\" \"$P4PORT\" \"$P4USER\"")
(string :tag "Run custom command"))
:group 'p4)
(defcustom p4-mode-hook nil
"Hook run by `p4-mode'."
:type 'hook
:group 'p4)
(defcustom p4-form-mode-hook nil
"Hook run by `p4-form-mode'."
:type 'hook
:group 'p4)
(defcustom p4-edit-hook nil
"Hook run after opening a file for edit."
:type 'hook
:group 'p4)
(defcustom p4-set-client-hooks nil
"Hook run after client is changed."
:type 'hook
:group 'p4)
(defcustom p4-strict-complete t
"If non-NIL, `p4-set-my-client' requires an exact match."
:type 'boolean
:group 'p4)
;; This is also set by the command `p4-toggle-vc-mode'.
(defcustom p4-do-find-file t
"If non-NIL, display Perforce revision and opened status in the
mode line."
:type 'boolean
:group 'p4)
(defcustom p4-cleanup-time 600
"Time in seconds after which a cache of information from the
Perforce server becomes stale."
:type 'integer
:group 'p4)
(defcustom p4-my-clients nil
"The list of Perforce clients that the function
`p4-set-client-name' will complete on, or NIL if it should
complete on all clients."
:type '(repeat (string))
:group 'p4)
(eval-and-compile
;; This is needed at compile time by p4-help-text.
(defcustom p4-modify-args-function #'identity
"Function that modifies a Perforce command line argument list.
All calls to the Perforce executable are routed through this
function to enable global modifications of argument vectors. The
function will be called with one argument, the list of command
line arguments for Perforce (excluding the program name). It
should return a possibly modified command line argument list.
This can be used to e.g. support wrapper scripts taking custom
flags."
:type 'function
:group 'p4))
(defgroup p4-faces nil "Perforce VC System Faces." :group 'p4)
(defface p4-description-face '((t))
"Face used for change descriptions."
:group 'p4-faces)
(defface p4-heading-face '((t))
"Face used for section heading."
:group 'p4-faces)
(defface p4-link-face '((t (:weight bold)))
"Face used to highlight clickable links."
:group 'p4-faces)
(defface p4-action-face '((t (:inherit p4-link-face)))
"Face used to highlight Perforce actions (add/edit/integrate/delete)."
:group 'p4-faces)
(defface p4-branch-face '((t (:inherit p4-link-face)))
"Face used to highlight Perforce branches."
:group 'p4-faces)
(defface p4-change-face '((t (:inherit p4-link-face)))
"Face used to highlight Perforce change numbers."
:group 'p4-faces)
(defface p4-client-face '((t (:inherit p4-link-face)))
"Face used to highlight Perforce users."
:group 'p4-faces)
(defface p4-filespec-face '((t (:inherit p4-link-face)))
"Face used to highlight Perforce filespec."
:group 'p4-faces)
(defface p4-job-face '((t (:inherit p4-link-face)))
"Face used to highlight Perforce job names."
:group 'p4-faces)
(defface p4-label-face '((t (:inherit p4-link-face)))
"Face used to highlight Perforce labels."
:group 'p4-faces)
(defface p4-revision-face '((t (:inherit p4-link-face)))
"Face used to highlight Perforce revision numbers."
:group 'p4-faces)
(defface p4-user-face '((t (:inherit p4-link-face)))
"Face used to highlight Perforce users."
:group 'p4-faces)
(defface p4-depot-add-face
'((((class color) (background light)) (:foreground "blue"))
(((class color) (background dark)) (:foreground "cyan")))
"Face used for files open for add."
:group 'p4-faces)
(defface p4-depot-branch-face
'((((class color) (background light)) (:foreground "blue4"))
(((class color) (background dark)) (:foreground "sky blue")))
"Face used for files open for integrate."
:group 'p4-faces)
(defface p4-depot-delete-face
'((((class color) (background light)) (:foreground "red"))
(((class color) (background dark)) (:foreground "pink")))
"Face used for files open for delete."
:group 'p4-faces)
(defface p4-depot-edit-face
'((((class color) (background light)) (:foreground "dark green"))
(((class color) (background dark)) (:foreground "light green")))
"Face used for files open for edit."
:group 'p4-faces)
(defface p4-form-comment-face '((t (:inherit font-lock-comment-face)))
"Face for comment in P4 Form mode."
:group 'p4-faces)
(defface p4-form-keyword-face '((t (:inherit font-lock-keyword-face)))
"Face for keyword in P4 Form mode."
:group 'p4-faces)
;; Local variables in all buffers.
(defvar p4-mode nil "P4 minor mode.")
(defvar p4-vc-revision nil
"Perforce revision to which this buffer's file is synced.")
(defvar p4-vc-status nil
"Perforce status for this buffer. A symbol:
NIL if file is not known to be under control of Perforce.
`add' if file is opened for add.
`branch' if file opened for branch.
`delete' if file is opened for delete.
`edit' if file is opened for edit.
`integrate' if file is opened for integrate.
`sync' if file is synced but not opened.
`depot' if the file is from the depot.")
;; Local variables in P4 process buffers.
(defvar p4-process-args nil "List of p4 command and arguments.")
(defvar p4-process-callback nil
"Function run when p4 command completes successfully.")
(defvar p4-process-after-show nil
"Function run after showing output of successful p4 command.")
(defvar p4-process-auto-login nil
"If non-NIL, automatically prompt user to log in.")
(defvar p4-process-buffers nil
"List of buffers whose status is being updated here.")
(defvar p4-process-pending nil
"Pending status update structure being updated here.")
(defvar p4-process-pop-up-output nil
"Function that returns non-NIL to display output in a pop-up
window, or NIL to display it in the echo area.")
(defvar p4-process-synchronous nil
"If non-NIL, run p4 command synchronously.")
;; Local variables in P4 Form buffers.
(defvar p4-form-commit-command nil
"p4 command to run when committing this form.")
(defvar p4-form-commit-success-callback nil
"Function run if commit succeeds. It receives two arguments:
the commit command and the buffer containing the output from the
commit command.")
(defvar p4-form-commit-failure-callback nil
"Function run if commit fails. It receives two arguments:
the commit command and the buffer containing the output from the
commit command.")
(defvar p4-form-head-text
(format "# Created using Perforce-Emacs Integration version %s.
# Type C-c C-c to send the form to the server.
# Type C-x k to cancel the operation.
#\n" p4-version)
"Text added to top of generic form.")
;; Local variables in P4 depot buffers.
(defvar p4-default-directory nil "Original value of default-directory.")
(dolist (var '(p4-mode p4-vc-revision p4-vc-status
p4-process-args p4-process-callback
p4-process-buffers p4-process-pending
p4-process-after-show p4-process-auto-login
p4-process-pop-up-output p4-process-synchronous
p4-form-commit-command
p4-form-commit-success-callback
p4-form-commit-failure-callback p4-default-directory))
(make-variable-buffer-local var)
(put var 'permanent-local t))
;;; P4 minor mode:
(add-to-list 'minor-mode-alist '(p4-mode p4-mode))
;;; Keymap:
(defvar p4-prefix-map
(let ((map (make-sparse-keymap)))
(define-key map "a" 'p4-add)
(define-key map "b" 'p4-branch)
(define-key map "B" 'p4-branches)
(define-key map "c" 'p4-client)
(define-key map "C" 'p4-changes)
(define-key map "d" 'p4-diff2)
(define-key map "D" 'p4-describe)
(define-key map "e" 'p4-edit)
(define-key map "E" 'p4-reopen)
(define-key map "\C-f" 'p4-depot-find-file)
(define-key map "f" 'p4-filelog)
(define-key map "F" 'p4-files)
(define-key map "G" 'p4-get-client-name)
(define-key map "g" 'p4-update)
(define-key map "h" 'p4-help)
(define-key map "H" 'p4-have)
(define-key map "i" 'p4-info)
(define-key map "I" 'p4-integ)
(define-key map "j" 'p4-job)
(define-key map "J" 'p4-jobs)
(define-key map "l" 'p4-label)
(define-key map "L" 'p4-labels)
(define-key map "\C-l" 'p4-labelsync)
(define-key map "m" 'p4-move)
(define-key map "o" 'p4-opened)
(define-key map "p" 'p4-print)
(define-key map "P" 'p4-set-p4-port)
(define-key map "q" 'quit-window)
(define-key map "r" 'p4-revert)
(define-key map "R" 'p4-refresh)
(define-key map "\C-r" 'p4-resolve)
(define-key map "s" 'p4-status)
(define-key map "S" 'p4-submit)
(define-key map "t" 'p4-toggle-vc-mode)
(define-key map "u" 'p4-user)
(define-key map "U" 'p4-users)
(define-key map "v" 'p4-version)
(define-key map "V" 'p4-annotate)
(define-key map "w" 'p4-where)
(define-key map "x" 'p4-delete)
(define-key map "X" 'p4-fix)
(define-key map "z" 'p4-reconcile)
(define-key map "=" 'p4-diff)
(define-key map (kbd "C-=") 'p4-diff-all-opened)
(define-key map "-" 'p4-ediff)
map)
"The prefix map for global p4.el commands.")
(fset 'p4-prefix-map p4-prefix-map)
(defun p4-update-global-key-prefix (symbol value)
"Update the P4 global key prefix based on the
`p4-global-key-prefix' user setting."
(set symbol value)
(let ((map (current-global-map)))
;; Remove old binding(s).
(dolist (key (where-is-internal p4-prefix-map map))
(define-key map key nil))
;; Add new binding.
(when p4-global-key-prefix
(define-key map p4-global-key-prefix p4-prefix-map))))
(defcustom p4-global-key-prefix (kbd "C-x p")
"The global key prefix for P4 commands."
:type '(radio (const :tag "No global key prefix" nil) (key-sequence))
:set 'p4-update-global-key-prefix
:group 'p4)
;;; Menu:
;; The menu definition is in the XEmacs format. Emacs parses and converts
;; this definition to its own menu creation commands.
(defvar p4-menu-spec
'(["Specify Arguments..." universal-argument t]
["--" nil nil]
["Open for Add" p4-add
(and buffer-file-name (or (not p4-do-find-file) (not p4-vc-status)))]
["Open for Edit" p4-edit
(and buffer-file-name (or (not p4-do-find-file) (eq p4-vc-status 'sync)))]
["Reopen" p4-reopen
(and buffer-file-name (or (not p4-do-find-file) (eq p4-vc-status 'edit)))]
["Revert" p4-revert
(and buffer-file-name (or (not p4-do-find-file) (memq p4-vc-status '(add branch edit delete integrate))))]
["Open for Delete" p4-delete
(and buffer-file-name (or (not p4-do-find-file) (eq p4-vc-status 'sync)))]
["Move Open File" p4-move
(and buffer-file-name (or (not p4-do-find-file) (eq p4-vc-status 'edit)))]
["Submit Changes" p4-submit t]
["--" nil nil]
["Update Files from Depot" p4-update t]
["Status of Files on Client" p4-status t]
["Reconcile Files with Depot" p4-reconcile t]
["--" nil nil]
["Show Opened Files" p4-opened t]
["File info" p4-fstat
(and buffer-file-name (or (not p4-do-find-file) p4-vc-status))]
["Filelog" p4-filelog
(and buffer-file-name (or (not p4-do-find-file) p4-vc-status))]
["Changes" p4-changes t]
["Describe Change" p4-describe t]
["--" nil nil]
["Diff 2 Versions" p4-diff2
(and buffer-file-name (or (not p4-do-find-file) p4-vc-status))]
["Diff Current" p4-diff
(and buffer-file-name (or (not p4-do-find-file) (eq p4-vc-status 'edit)))]
["Diff All Opened Files" p4-diff-all-opened t]
["Diff Current with Ediff" p4-ediff
(and buffer-file-name (or (not p4-do-find-file) (eq p4-vc-status 'edit)))]
["Diff 2 Versions with Ediff" p4-ediff2
(and buffer-file-name (or (not p4-do-find-file) p4-vc-status))]
["--" nil nil]
["Open for Integrate" p4-integ t]
["Resolve Conflicts" p4-resolve t]
["--" nil nil]
["Print" p4-print
(and buffer-file-name (or (not p4-do-find-file) p4-vc-status))]
["Print with Revision History" p4-annotate
(and buffer-file-name (or (not p4-do-find-file) p4-vc-status))]
["Find File using Depot Spec" p4-depot-find-file t]
["--" nil nil]
["Edit a Branch Specification" p4-branch t]
["Edit a Label Specification" p4-label t]
["Edit a Client Specification" p4-client t]
["Edit a User Specification" p4-user t]
["--" nil nil]
["Disable Status Check" p4-toggle-vc-mode-off p4-do-find-file]
["Enable Status Check" p4-toggle-vc-mode-on (not p4-do-find-file)]
["--" nil nil]
["Set P4CONFIG" p4-set-p4-config t]
["Set P4CLIENT" p4-set-client-name t]
["Set P4PORT" p4-set-p4-port t]
["Show client info" p4-set t]
["Show server info" p4-info t]
["--" nil nil]
["About P4" p4-version t]
)
"The P4 menu definition")
(easy-menu-change '("tools") "P4" p4-menu-spec "Version Control")
;;; Macros (must be defined before use if compilation is to work)
(defmacro p4-with-temp-buffer (args &rest body)
"Run p4 ARGS in a temporary buffer, place point at the start of
the output, and evaluate BODY if the command completed successfully."
`(let ((dir (or p4-default-directory default-directory)))
(with-temp-buffer
(cd dir)
(when (zerop (p4-run ,args)) ,@body))))
(put 'p4-with-temp-buffer 'lisp-indent-function 1)
(defmacro p4-with-set-output (&rest body)
"Run p4 set in a temporary buffer, place point at the start of
the output, and evaluate BODY if the command completed successfully."
;; Can't use `p4-with-temp-buffer' for this, because that would lead
;; to infinite recursion via `p4-coding-system'.
`(let ((dir (or p4-default-directory default-directory)))
(with-temp-buffer
(cd dir)
(when (zerop (save-excursion
(p4-call-process nil t nil "set")))
,@body))))
(put 'p4-with-set-output 'lisp-indent-function 0)
(defmacro p4-with-coding-system (&rest body)
"Evaluate BODY with coding-system-for-read and -write set to
the result of `p4-coding-system'."
`(let* ((coding (p4-coding-system))
(coding-system-for-read coding)
(coding-system-for-write coding))
,@body))
(put 'p4-with-coding-system 'lisp-indent-function 0)
;;; Environment:
(defun p4-version ()
"Describe the Emacs-Perforce Integration version."
(interactive)
(message "Emacs-P4 Integration version %s" p4-version))
(defun p4-current-setting (var &optional default)
"Return the current Perforce client setting for VAR, or DEFAULT
if there is no setting."
(or (p4-with-set-output
(let ((re (format "^%s=\\(\\S-+\\)" (regexp-quote var))))
(when (re-search-forward re nil t)
(match-string 1))))
default))
(defun p4-current-environment ()
"Return `process-environment' updated with the current Perforce
client settings."
(append
(p4-with-set-output
(loop while (re-search-forward "^P4[A-Z]+=\\S-+" nil t)
collect (match-string 0)))
;; Default values for P4PORT and P4USER may be needed by
;; p4-password-source even if not supplied by "p4 set". See:
;; http://www.perforce.com/perforce/doc.current/manuals/cmdref/P4PORT.html
;; http://www.perforce.com/perforce/doc.current/manuals/cmdref/P4USER.html
(list
"P4PORT=perforce:1666"
(concat "P4USER="
(or (getenv "USER") (getenv "USERNAME") (user-login-name))))
process-environment))
(defvar p4-coding-system-alist
;; I've preferred the IANA name, where possible. See
;; <http://www.iana.org/assignments/character-sets/character-sets.xhtml>
;; Note that Emacs (as of 24.3) does not support utf-32 and its
;; variants; these will lead to an error in `p4-coding-system'.
'(("cp1251" . windows-1251)
("cp936" . windows-936)
("cp949" . euc-kr)
("cp950" . big5)
("eucjp" . euc-jp)
("iso8859-1" . iso-8859-1)
("iso8859-15" . iso-8859-15)
("iso8859-5" . iso-8859-5)
("koi8-r" . koi8-r)
("macosroman" . macintosh)
("shiftjis" . shift_jis)
("utf16" . utf-16-with-signature)
("utf16-nobom" . utf-16)
("utf16be" . utf-16be)
("utf16be-bom" . utf-16be-with-signature)
("utf16le" . utf-16le)
("utf16le-bom" . utf-16le-with-signature)
("utf8" . utf-8)
("utf8-bom" . utf-8-with-signature)
("winansi" . windows-1252)
("none" . utf-8)
(nil . utf-8))
"Association list mapping P4CHARSET to Emacs coding system.")
(defun p4-coding-system ()
"Return an Emacs coding system equivalent to P4CHARSET."
(let* ((charset (p4-current-setting "P4CHARSET"))
(c (assoc charset p4-coding-system-alist)))
(if c (cdr c)
(error "Coding system %s not available in Emacs" charset))))
(defun p4-set-process-coding-system (process)
"Set coding systems of PROCESS appropriately."
(let ((coding (p4-coding-system)))
(set-process-coding-system process coding coding)))
(defun p4-current-client ()
"Return the current Perforce client."
(p4-current-setting "P4CLIENT"))
(defun p4-get-client-name ()
"Display the name of the current Perforce client."
(interactive)
(message "P4CLIENT=%s" (p4-current-client)))
(defun p4-current-server-port ()
"Return the current Perforce port."
;; http://www.perforce.com/perforce/doc.current/manuals/cmdref/P4PORT.html
(or (p4-current-setting "P4PORT") "perforce:1666"))
(defvar p4-server-version-cache nil
"Association list mapping P4PORT to Perforce server version on that port.")
(defun p4-server-version ()
"Return the version number of the Perforce server, or NIL if unknown."
(let ((p4-port (p4-current-server-port)))
(or (cdr (assoc p4-port p4-server-version-cache))
(p4-with-temp-buffer '("info")
(when (re-search-forward "^Server version: .*/\\([1-9][0-9]\\{3\\}\\)\\.[0-9]+/" nil t)
(let ((version (string-to-number (match-string 1))))
(push (cons p4-port version) p4-server-version-cache)
version))))))
(defun p4-set-client-name (value)
"Set the P4CLIENT environment variable to VALUE.
If the setting `p4-set-my-clients' is non-NIL, complete on those
clients only. If `p4-strict-complete' is non-NIL, require an
exact match."
(interactive
(list
(completing-read
"P4CLIENT="
(or p4-my-clients
(p4-completion-arg-completion-fn (p4-get-completion 'client)))
nil p4-strict-complete (p4-current-client) 'p4-client-history)))
(setenv "P4CLIENT" (unless (string-equal value "") value))
(run-hooks 'p4-set-client-hooks))
(defun p4-set-p4-config (value)
"Set the P4CONFIG environment variable to VALUE."
(interactive (list (read-string "P4CONFIG=" (p4-current-setting "P4CONFIG"))))
(setenv "P4CONFIG" (unless (string-equal value "") value)))
(defun p4-set-p4-port (value)
"Set the P4PORT environment variable to VALUE."
(interactive (list (read-string "P4PORT=" (p4-current-setting "P4PORT"))))
(setenv "P4PORT" (unless (string-equal value "") value)))
;;; File handler:
(defun p4-dirs-and-attributes (dir)
(let ((now (current-time)))
(loop for f in (p4-output-matches (list "dirs" (concat dir "*"))
"^//[^ \n]+$")
collect (list f t 0 0 0 now now now 0 "dr--r--r--" nil 0 0))))
(defun p4-files-and-attributes (dir)
(let ((now (current-time)))
(loop for f in (p4-output-matches (list "files" (concat dir "*"))
"^\\(//[^#\n]+#[1-9][0-9]*\\) - " 1)
collect (list f nil 0 0 0 now now now 0 "-r--r--r--" nil 0 0))))
(defun p4-directory-files-and-attributes (dir &optional full match nosort id-format)
(let* ((from (length dir))
(files (loop for f in (append (p4-dirs-and-attributes dir)
(p4-files-and-attributes dir))
unless (and match (not (string-match match (first f))))
collect (if full f
(cons (substring (first f) from) (cdr f))))))
(if nosort files
(sort files 'file-attributes-lessp))))
(defun p4-file-exists-p (filename)
(or (p4-file-directory-p filename)
(p4-with-temp-buffer (list "-s" "files" filename) (looking-at "info:"))))
(defun p4-file-directory-p (filename)
(p4-with-temp-buffer (list "-s" "dirs" filename) (looking-at "info:")))
(defun p4-file-name-sans-versions (filename &optional keep-backup-version)
(string-match "\\(.*?\\)\\(?:#[1-9][0-9]*\\|@[^#@ \t\n]+\\)?$" filename)
(match-string 1 filename))
(defun p4-insert-directory (file switches &optional wildcard full-directory-p)
(message "%s" (list file switches wildcard full-directory-p))
(loop for f in (p4-directory-files-and-attributes file)
do (insert (format " %s - - - %d %s %s\n" (nth 9 f)
(nth 8 f) (format-time-string "%b %e %Y" (nth 6 f))
(nth 0 f)))))
(defun p4-insert-file-contents (filename &optional visit beg end replace)
(unless (zerop (p4-run (list "print" "-q" filename)))
(signal 'file-error (buffer-substring (point-min) (point-max))))
(when visit
(p4-update-mode (current-buffer) 'depot nil)
(setq p4-default-directory (or p4-default-directory default-directory))
(setq buffer-file-name filename)
(set-buffer-modified-p nil))
(setq buffer-read-only t))
(defun p4-file-name-handler (operation &rest args)
(case operation
((expand-file-name file-truename substitute-in-file-name)
(car args))
(directory-files (apply 'p4-directory-files args))
(file-directory-p (apply 'p4-file-directory-p args))
(file-exists-p (apply 'p4-file-exists-p args))
(file-name-sans-versions (apply 'p4-file-name-sans-versions args))
(file-remote-p t)
(file-writable-p nil)
(insert-directory (apply 'p4-insert-directory args))
(insert-file-contents (apply 'p4-insert-file-contents args))
(vc-registered nil)
((add-name-to-file delete-directory delete-file dired-compress-file
make-directory make-directory-internal make-symbolic-link rename-file
set-file-modes set-file-times shell-command write-region)
(error "%s not supported for Perforce depot files." operation))
(t
(message "(p4-file-name-handler %s %s)" operation args)
(let ((inhibit-file-name-handlers
(cons 'p4-file-name-handler
(and (eq inhibit-file-name-operation operation)
inhibit-file-name-handlers)))
(inhibit-file-name-operation operation))
(apply operation args)))))
;;; Utilities:
(defun p4-find-file-or-print-other-window (client-name depot-name)
(if client-name
(find-file-other-window client-name)
(p4-depot-find-file depot-name)))
(defvar p4-filespec-buffer-cache nil
"Association list mapping filespec to buffer visiting that filespec.")
(defun p4-purge-filespec-buffer-cache ()
"Remove stale entries from `p4-filespec-buffer-cache'."
(let ((stale (time-subtract (current-time)
(seconds-to-time p4-cleanup-time))))
(setf p4-filespec-buffer-cache
(loop for c in p4-filespec-buffer-cache
when (and (time-less-p stale (second c))
(buffer-live-p (third c)))
collect c))))
(defun p4-visit-filespec (filespec)
"Visit FILESPEC in some buffer and return the buffer."
(p4-purge-filespec-buffer-cache)
(let ((cached (assoc filespec p4-filespec-buffer-cache)))
(if cached (third cached)
(let ((args (list "print" filespec)))
(set-buffer (p4-make-output-buffer (p4-process-buffer-name args)))
(if (zerop (p4-run args))
(progn
(p4-activate-print-buffer t)
(push (list filespec (current-time) (current-buffer))
p4-filespec-buffer-cache)
(current-buffer))
(p4-process-show-error))))))
(defun p4-depot-find-file-noselect (filespec)
"Read depot FILESPEC in to a buffer and return the buffer.
If a buffer exists visiting FILESPEC, return that one."
(string-match "\\(.*?\\)\\(#[1-9][0-9]*\\|\\(@\\S-+\\)\\)?$" filespec)
(let* ((file (match-string 1 filespec))
(spec (match-string 2 filespec))
(change (match-string 3 filespec)))
(if change
;; TODO: work out if we have the file synced at this
;; changelevel, perhaps by running sync -n and seeing if it
;; prints "files(s) up to date"?
(p4-visit-filespec filespec)
(with-temp-buffer
(if (and (zerop (p4-run (list "have" file)))
(not (looking-at "//[^ \n]+ - file(s) not on client"))
(looking-at "//.*?\\(#[1-9][0-9]*\\) - \\(.*\\)$")
(or (not spec) (string-equal spec (match-string 1))))
(find-file-noselect (match-string 2))
(p4-visit-filespec filespec))))))
(defun p4-depot-find-file (filespec &optional line offset)
"Visit the client file corresponding to depot FILESPEC,
if the file is mapped (and synced to the right revision if
necessary), otherwise print FILESPEC to a new buffer
synchronously and pop to it. With optional arguments LINE and
OFFSET, go to line number LINE and move forward by OFFSET
characters."
(interactive (list (p4-read-arg-string "Enter filespec: " "//" 'filespec)))
(let ((buffer (p4-depot-find-file-noselect filespec)))
(when buffer
(pop-to-buffer buffer)
(when line (p4-goto-line line)
(when offset (forward-char offset))))))
(defun p4-make-derived-map (base-map)
(let ((map (make-sparse-keymap)))
(set-keymap-parent map base-map)
map))
(defun p4-goto-line (line)
(goto-char (point-min))
(forward-line (1- line)))
(defun p4-join-list (list) (mapconcat 'identity list " "))
;; Break up a string into a list of words
;; (p4-make-list-from-string "ab 'c de' \"'f'\"") -> ("ab" "c de" "'f'")
(defun p4-make-list-from-string (str)
(let (lst)
(while (or (string-match "^ *\"\\([^\"]*\\)\"" str)
(string-match "^ *\'\\([^\']*\\)\'" str)
(string-match "^ *\\([^ ]+\\)" str))
(setq lst (append lst (list (match-string 1 str))))
(setq str (substring str (match-end 0))))
lst))
(defun p4-force-mode-line-update ()
"Force the mode line update."
(if (featurep 'xemacs)
(redraw-modeline)
(force-mode-line-update)))
(defun p4-dired-get-marked-files ()
;; Wrapper for `dired-get-marked-files'. In Emacs 24.2 (and earlier)
;; this raises an error if there are no marked files and no file on
;; the current line, so we suppress the error here.
;;
;; The (delq nil ...) works around a bug in Dired+. See issue #172
;; <https://github.com/gareth-rees/p4.el/issues/172>
(ignore-errors (delq nil (dired-get-marked-files nil))))
(defun p4-follow-link-name (name)
(p4-cygpath
(if p4-follow-symlinks
(file-truename name)
name)))
(defun p4-buffer-file-name (&optional buffer)
"Return name of file BUFFER is visiting, or NIL if none,
respecting the `p4-follow-symlinks' setting."
(let ((f (buffer-file-name buffer)))
(when f (p4-follow-link-name f))))
(defun p4-process-output (cmd &rest args)
"Run CMD (with the given ARGS) and return the output as a string,
except for the final newlines."
(with-temp-buffer
(apply 'call-process cmd nil t nil args)
(skip-chars-backward "\n")
(buffer-substring (point-min) (point))))
(defun p4-cygpath (name)
(if (and (memq system-type '(cygwin32 cygwin))
(not (p4-with-temp-buffer '("-V") (search-forward "CYGWIN" nil t))))
(p4-process-output p4-cygpath-exec "-w" name)
name))
(defun p4-startswith (string prefix)
"Return non-NIL if STRING starts with PREFIX."
(let ((l (length prefix)))
(and (>= (length string) l) (string-equal (substring string 0 l) prefix))))
;;; Running Perforce:
(eval-and-compile
;; This is needed at compile time by p4-help-text.
(defun p4-executable ()
"Check if `p4-executable' is NIL, and if so, prompt the user
for a valid `p4-executable'."
(interactive)
(or p4-executable (call-interactively 'p4-set-p4-executable))))
(defun p4-set-p4-executable (filename)
"Set `p4-executable' to the argument FILENAME.
To set the executable for future sessions, customize
`p4-executable' instead."
(interactive "fFull path to your p4 executable: ")
(if (and (file-executable-p filename) (not (file-directory-p filename)))
(setq p4-executable filename)
(error "%s is not an executable file." filename)))
(eval-and-compile
;; This is needed at compile time by p4-help-text.
(defun p4-call-process (&optional infile destination display &rest args)
"Call Perforce synchronously in separate process.
The program to be executed is taken from `p4-executable'; INFILE,
DESTINATION, and DISPLAY are to be interpreted as for
`call-process'. The argument list ARGS is modified using
`p4-modify-args-function'."
(apply #'call-process (p4-executable) infile destination display
(funcall p4-modify-args-function args))))
(defun p4-call-process-region (start end &optional delete buffer display &rest args)
"Send text from START to END to a synchronous Perforce process.
The program to be executed is taken from `p4-executable'; START,
END, DELETE, BUFFER, and DISPLAY are to be interpreted as for
`call-process-region'. The argument list ARGS is modified using
`p4-modify-args-function'."
(apply #'call-process-region start end (p4-executable) delete buffer display
(funcall p4-modify-args-function args)))
(defun p4-start-process (name buffer &rest program-args)
"Start Perforce in a subprocess. Return the process object for it.
The program to be executed is taken from `p4-executable'; NAME
and BUFFER are to be interpreted as for `start-process'. The
argument list PROGRAM-ARGS is modified using
`p4-modify-args-function'."
(apply #'start-process name buffer (p4-executable)
(funcall p4-modify-args-function program-args)))
(defun p4-compilation-start (args &optional mode name-function highlight-regexp)
"Run Perforce with arguments ARGS in a compilation buffer.
The program to be executed is taken from `p4-executable'; MODE,
NAME-FUNCTION, and HIGHLIGHT-REGEXP are to be interpreted as for
`compilation-start'. ARGS, however, is an argument vector, not a
shell command. It will be modified using
`p4-modify-args-function'."
(apply #'compilation-start
(mapconcat #'shell-quote-argument
(cons (p4-executable)
(funcall p4-modify-args-function args))
" ")
mode name-function highlight-regexp))
(defun p4-make-comint (name &optional startfile &rest switches)
"Make a Comint process NAME in a buffer, running Perforce.
The program to be executed is taken from `p4-executable';
STARTFILE is to be interpreted as for `p4-make-comint'. SWITCHES
is modified using `p4-modify-args'."
(apply #'make-comint name (p4-executable) startfile
(funcall p4-modify-args-function switches)))
(defun p4-make-output-buffer (buffer-name &optional mode)
"Make a read-only buffer named BUFFER-NAME and return it.
Run the function MODE if non-NIL, otherwise `p4-basic-mode'."
(let ((dir (or p4-default-directory default-directory))
(inhibit-read-only t))
(with-current-buffer (get-buffer-create buffer-name)
(erase-buffer)
(funcall (or mode 'p4-basic-mode))
(setq buffer-read-only t)
(setq buffer-undo-list t)
(cd dir)
(current-buffer))))
(defvar p4-no-session-regexp
(concat "\\(?:error: \\)?"
"\\(?:Perforce password (P4PASSWD) invalid or unset\\|"
"Your session has expired, please login again\\)")
"Regular expression matching output from Perforce when you are logged out.")
(defvar p4-untrusted-regexp
(concat "\\(?:error: \\)?"
"\\(?:The authenticity of '.*' can't be established"
"\\|\\** WARNING P4PORT IDENTIFICATION HAS CHANGED! \\**\\)")
"Regular expression matching output from an untrusted Perforce server.")
(defvar p4-connect-failed-regexp
(concat "\\(?:error: \\)?"
"Perforce client error:\n"
"\tConnect to server failed")
"Regular expression matching output from Perforce when it can't
connect to the server.")
(defun p4-request-trust ()
"Ask the user for permission to trust the Perforce server."
(with-selected-window (display-buffer (current-buffer))
(goto-char (point-min)))
(unless (yes-or-no-p "Trust server? ")
(error "Server not trusted."))
(with-temp-buffer
(insert "yes\n")
(p4-with-coding-system
(p4-call-process-region (point-min) (point-max)