-
-
Notifications
You must be signed in to change notification settings - Fork 19
/
helm-ls-git.el
2166 lines (1903 loc) · 90.8 KB
/
helm-ls-git.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
;;; helm-ls-git.el --- The git project manager for helm. -*- lexical-binding: t -*-
;; Copyright (C) 2012 ~ 2023 Thierry Volpiatto
;; Package-Requires: ((helm "3.9.5") (emacs "25.3"))
;; URL: https://github.com/emacs-helm/helm-ls-git
;; Version: 1.9.4
;; Keywords: helm, convenience, vc, files, buffers, completion, diff, log, git
;; 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 <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Features:
;;
;; Display list of branches in project and provide related actions.
;;
;; Provide git log view from branches source with related actions (diff, patches, reset, find file etc...)
;;
;; Display the open buffers in project.
;;
;; Display a status source showing state of project (modified files etc...).
;;
;; Provide its own commit facilities with a mode to edit commit (commit, amend etc...)
;;
;; Allow rebasing and provide a mode to edit rebase-todo files
;;
;; Display stashes list and provide related actions.
;;
;; Display a list of all files in project under git control.
;;
;; Allow looking quickly at diff on modified files.
;;
;; Allow switching to git status with your preferred frontend (vc-dir, magit,etc...)
;;
;; Full integration of git-grep, allow also usage of helm-grep (you can use ack-grep instead of grep).
;;
;; Integrate usage of gid from id-utils.
;;
;; Full integration with helm-find-files, allow you to browse project unrelated to current-buffer.
;;
;; In addition, all actions of type files and buffers are provided.
;;; Code
(require 'cl-lib)
(require 'vc)
(require 'vc-git)
(require 'helm-files) ; helm-grep is required in helm-files.
(require 'helm-types)
(defvaralias 'helm-c-source-ls-git 'helm-source-ls-git)
(make-obsolete-variable 'helm-c-source-ls-git 'helm-source-ls-git "1.5.1")
(defvaralias 'helm-c-source-ls-git-status 'helm-source-ls-git-status)
(make-obsolete-variable 'helm-c-source-ls-git-status 'helm-source-ls-git-status "1.5.1")
;; Now the git-grep command is defined in helm-grep.el,
;; alias it for backward compatibility.
(defvar helm-ls-git-grep-command)
(defvaralias 'helm-ls-git-grep-command 'helm-grep-git-grep-command)
(make-obsolete-variable 'helm-ls-git-grep-command 'helm-grep-git-grep-command "1.8.0")
(defvar server-clients)
(declare-function helm-comp-read "ext:helm-mode.el")
(declare-function server-running-p "server.el")
(declare-function server-edit "server.el")
(declare-function server-send-string "server.el")
(declare-function server-quote-arg "server.el")
;; Define the sources.
(defvar helm-source-ls-git-status nil
"This source will built at runtime.
It can be build explicitly with function
`helm-ls-git-build-git-status-source'.")
(defvar helm-source-ls-git nil
"This source will built at runtime.
It can be build explicitly with function
`helm-ls-git-build-ls-git-source'.")
(defvar helm-source-ls-git-buffers nil
"This source will built at runtime.
It can be build explicitly with function
`helm-ls-git-build-buffers-source'.")
(defgroup helm-ls-git nil
"Helm completion for git repos."
:group 'helm)
(defcustom helm-ls-git-show-abs-or-relative 'relative
"Show full path or relative path to repo when using `helm-ff-toggle-basename'.
Valid values are symbol \\='absolute' or \\='relative' (default)."
:type '(radio :tag "Show full path or relative path to Git repo when toggling"
(const :tag "Show full path" absolute)
(const :tag "Show relative path" relative)))
(defcustom helm-ls-git-status-command nil
"Favorite git-status command for emacs.
When set, you will have an additional action allowing to
switch to a git status buffer e.g. `vc-dir' or `magit-status'.
If you want to use magit use `magit-status-setup-buffer' and not
`magit-status' which is working only interactively."
:type 'symbol)
(defcustom helm-ls-git-fuzzy-match nil
"Enable fuzzy matching in `helm-source-ls-git-status' and `helm-source-ls-git'."
:set (lambda (var val)
(set var val)
(setq helm-source-ls-git nil
helm-source-ls-git-status nil
helm-source-ls-git-buffers nil))
:type 'boolean)
(defcustom helm-ls-git-default-sources '(helm-source-ls-git-status
helm-ls-git-branches-source
helm-source-ls-git-buffers
helm-source-ls-git
helm-ls-git-stashes-source
helm-ls-git-create-branch-source)
"Default sources for `helm-ls-git-ls'."
:type '(repeat symbol))
(defcustom helm-ls-git-format-glob-string "'%s'"
"String to format globs in `helm-grep-get-file-extensions'.
Glob are enclosed in single quotes by default."
:type 'string)
(defcustom helm-ls-git-ls-switches '("ls-files" "--full-name" "--")
"A list of arguments to pass to `git-ls-files'.
To see files in submodules add the option \"--recurse-submodules\".
If you have problems displaying unicode filenames use
\\='(\"-c\" \"core.quotePath=false\" \"ls-files\" \"--full-name\" \"--\").
See Issue #52."
:type '(repeat string))
(defcustom helm-ls-git-auto-checkout nil
"Stash automatically uncommited changes before checking out a branch."
:type 'boolean)
(defcustom helm-ls-git-log-max-commits "100"
"Max number of commits to show in git log (git log -n option).
This can be increased later with C-u <n> C-c C-u."
:type 'string)
(defcustom helm-ls-git-delete-branch-on-remote nil
"Delete remote branch without asking when non nil.
This happen only when deleting a remote branch e.g. remotes/origin/foo."
:type 'boolean)
(defcustom helm-ls-git-auto-refresh-at-eob t
"Increase git log by `window-height' lines when non nil.
When non nil this disable `helm-move-to-line-cycle-in-source'."
:type 'boolean)
(defcustom helm-ls-git-with-editor-fill-column 70
"The `fill-column' value used in commits."
:type 'integer)
(defgroup helm-ls-git-faces nil
"Customize the appearance of helm-files."
:prefix "helm-ls-git-"
:group 'helm-ls-git
:group 'helm-faces)
(defface helm-ls-git-modified-not-staged-face
'((t :foreground "yellow"))
"Files which are modified but not yet staged.")
(defface helm-ls-git-modified-and-staged-face
'((t :foreground "goldenrod"))
"Files which are modified and already staged.")
(defface helm-ls-git-renamed-modified-face
'((t :foreground "goldenrod"))
"Files which are renamed or renamed and modified.")
(defface helm-ls-git-untracked-face
'((t :foreground "red"))
"Files which are not yet tracked by git.")
(defface helm-ls-git-added-copied-face
'((t :foreground "green"))
"Files which are newly added or copied.")
(defface helm-ls-git-added-modified-face
'((t :foreground "LightSkyBlue"))
"Files which are newly added and have unstaged modifications.")
(defface helm-ls-git-deleted-not-staged-face
'((t :foreground "DarkGoldenrod3"))
"Files which are deleted but not staged.")
(defface helm-ls-git-deleted-and-staged-face
'((t :foreground "DimGray"))
"Files which are deleted and staged.")
(defface helm-ls-git-conflict-face
'((t :foreground "MediumVioletRed"))
"Files which contain rebase/merge conflicts.")
(defface helm-ls-git-branches-current
'((t :foreground "gold"))
"Color of the star prefixing current branch.")
(defface helm-ls-git-branches-name
'((t :foreground "red"))
"Color of branches names.")
(defface helm-ls-git-branches-name-current
'((t :foreground "green"))
"Color of current branch name.")
(defvar helm-ls-git-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map helm-generic-files-map)
(define-key map (kbd "C-s") 'helm-ff-run-grep)
(define-key map (kbd "M-g g") 'helm-ls-git-run-grep)
(define-key map (kbd "C-c g") 'helm-ff-run-gid)
(define-key map (kbd "C-c i") 'helm-ls-git-ls-files-show-others)
(define-key map (kbd "M-e") 'helm-ls-git-run-switch-to-shell)
(define-key map (kbd "M-L") 'undefined)
(define-key map (kbd "M-L") 'helm-ls-git-run-file-log)
map))
(defvar helm-ls-git-buffer-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map helm-buffer-map)
(define-key map (kbd "C-c i") 'helm-ls-git-ls-files-show-others)
map))
(defvar helm-ls-git-branches-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map helm-map)
(define-key map (kbd "C-c b") 'helm-ls-git-branches-toggle-show-all)
(define-key map (kbd "M-L") 'helm-ls-git-run-show-log)
(define-key map (kbd "C-c P") 'helm-ls-git-run-push)
(define-key map (kbd "C-c F") 'helm-ls-git-run-pull)
(define-key map (kbd "C-c f") 'helm-ls-git-run-fetch)
(define-key map (kbd "M-e") 'helm-ls-git-run-switch-to-shell)
(define-key map (kbd "C-c i") 'helm-ls-git-status-toggle-ignored)
map))
(defvar helm-ls-git-status-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map helm-ls-git-map)
(define-key map (kbd "C-c c") 'helm-ls-git-run-stage-marked-and-commit)
(define-key map (kbd "C-c a") 'helm-ls-git-run-stage-marked-and-amend-commit)
(define-key map (kbd "C-c s") 'helm-ls-git-run-stage-files)
(define-key map (kbd "C-c e") 'helm-ls-git-run-stage-marked-and-extend-commit)
(define-key map (kbd "C-c z") 'helm-ls-git-run-stash)
(define-key map (kbd "C-c Z") 'helm-ls-git-run-stash-snapshot)
(define-key map (kbd "C-c R") 'helm-ls-git-run-status-revert-files)
(define-key map (kbd "M-e") 'helm-ls-git-run-switch-to-shell)
(define-key map (kbd "C-c i") 'helm-ls-git-status-toggle-ignored)
map))
(defvar helm-ls-git-help-message
"* Helm ls git
** Tips
*** Start helm-ls-git
You can start with `helm-ls-git' but you can also use the generic
`helm-browse-project' which will use `helm-ls-git' if you are in
a git project (actually supported backends are git and hg though
helm-ls-hg is no more maintained).
Using the browse project action from `helm-find-files' allow
you to switch to another project easily (\\<helm-find-files-map>\\[helm-ff-run-browse-project]) .
See also the command `helm-projects-history' bound to
\\[helm-projects-history] to switch to one project to the other.
Also using bookmarks to switch projects is a good alternative.
*** Git status command
By default `helm-ls-git-status-command' is nil,
but you can set it if needed to `magit-status', `vc-dir' or whatever.
However helm-ls-git provides most of what you need to basically
manage your git repo so you may not need to use another tool,
here an overview of its features:
- Status of modified files
- List project branches
- List project files
- List project buffers
- List of Stashs
- Git log for each branches
- Create a new branch from current one
- Commit your changes from status source
- Rebase interactively
- Amend
- Diffs
- Pull and fetch
- Push
- Stash
- Revert
- Reset
- Format patches
- Git AM
- Cherry pick
etc...
Of course all these features are not enhanced as what you could
find in a Git specialized tool like Magit but it may fit most of
your needs.
*** Git branches
From this source you can see all locals branches and switch to them as needed.
The current branch is prefixed with a star.
You can toggle branches view (locales and remotes) with \\<helm-ls-git-branches-map>\\[helm-ls-git-branches-toggle-show-all].
See the action menu to see other available actions.
*** Git log
From branches source, you can launch git log. With a numeric
prefix arg specify the number of commits to show, once you are in
git log and you want more commits, use a numeric prefix arg with
\\<helm-map>\\[helm-refresh] to specify the number of commits to show.
When scrolling down with an empty pattern, helm can increase
automatically the number of candidates displayed when you reach
end of buffer if `helm-ls-git-auto-refresh-at-eob' is non nil.
If you search a specific commit by narrowing candidates list as usual
but want to show all candidates around the selection you are interested
in, you will have a problem because if you delete minibuffer input to
show whole list you will loose your position, to solve this problem you
have two solutions:
- With cursor at end of minibuffer use C-u C-k.
- Mark the selection and then delete minibuffer contents and find back the
selection by hitting M-).
NOTE: When searching in git log, Helm search in the candidates
computed initially, this mean that when you have 100 candidates
displayed (see `helm-ls-git-log-max-commits') and you search for
a commit containing \"foo\", this commit will not be found if it
is located in the 101 commit which is not displayed. So if you
don't find something you are looking for, increase the number of
commits with \\<global-map>\\[universal-argument] <n> \\<helm-map>\\[helm-refresh].
**** Specify a range of commits
Once you are in Git log you can specify with 2 marked
candidates a range of commits, specifying more than two marked
candidate for actions accepting only ranges will fail. When
specifying a range of commits, the top commit will be included in
range whereas the bottom commit will not be included, e.g. if you
mark commit-2 and commit-5, and use the format-patch action, git
will make 01-commit-4.patch, 02-commit-3.patch, and
03-commit-2.patch files taking care of naming files in the
reverse order for applying patches later, commit-5 beeing
excluded.
NOTE: For commodity, commits are specified as short hash for all actions, witch
may clash if more than one commit have the same short ID (rare
but may happen), you should have an error in such case.
**** Apply patches from one branch to current
You can apply patches from one branch to current
branch using git AM action.
Patches are specified as a range of commits, see [[Specify a range of commits][Specify a range of commits]].
**** Persistent action in git log
Persistent action in git log shows diff of selected commit, if you
want to always show diff while moving from one commit to the
other use follow-mode (C-c C-f).
*** Git commit
Commits will be done using emacsclient as GIT_EDITOR, with
major-mode `helm-ls-git-commmit-mode' which provide following commands:
\\<helm-ls-git-commit-mode-map>
|Keys|Description
|-------------+--------------|
|\\[helm-ls-git-server-edit]|Exit when done
|\\[helm-ls-git-server-edit-abort]|Abort
If you want to specify another author, use a prefix arg when
calling commit action, you will be prompted for author name and
email.
NOTE: This mode is based on diff-mode, this to show a colorized
diff of your commit, you can use any regular emacs editing
commands from there.
*** Git rebase
helm-ls-git provide two rebase actions, one that run
interactively from git log source and one that work
non-interactively from branches source. With the former you can
rebase interactively from a given commit you selected from git log
and this ONLY for current branch, once done you can rebase one
branch into the other from branches source. This is one workflow
that helm-ls-git provide, other workflows may not work, so for
more complex usage switch to command line or a more enhaced tool
like Magit. For editing the first file git rebase use for
rebasing (\"git-rebase-todo\") helm-ls-git use a major-mode
called `helm-ls-git-rebase-todo-mode' which provide several commands:
\\<helm-ls-git-rebase-todo-mode-map>
|Keys|Description
|-------------+--------------|
|p|pick
|r|reword
|e|edit
|s|squash
|f|fixup
|x|exec
|d|drop
|\\[helm-ls-git-rebase-todo-move-down]|Move line down
|\\[helm-ls-git-rebase-todo-move-up]|Move line up
|\\[helm-ls-git-server-edit]|Exit when done
|\\[helm-ls-git-server-edit-abort]|Abort
*** Git grep usage
The behavior is not exactly the same as what you have when you
launch git-grep from `helm-find-files', here in what it differ:
1) The prefix arg allow to grep only the `default-directory' whereas
with `helm-find-files' the prefix arg allow browsing the whole repo.
So with `helm-ls-git' the default is to grep the whole repo.
2) With `helm-ls-git', because you have the whole list of files of the repo
you can mark some of the files to grep only those, if no files are marked grep
the whole repo or the files under current directory depending of prefix arg.
NOTE: The previous behavior was prompting user for the file
extensions to grep, this is non sense because we have here the
whole list of files (recursive) of current repo and not only the
file under current directory, so we have better time
selectionning the files we want to grep.
**** Grep a subdirectory of current repository.
Switch to `helm-find-files' with `C-x C-f', navigate to your directory
and launch git-grep from there.
*** Problem with unicode filenames (chinese etc...)
See docstring of `helm-ls-git-ls-switches'.
** Commands
*** List files source
\\<helm-ls-git-map>
|Keys|Description
|-----------+----------|
|\\[helm-ls-git-run-grep]|Run git-grep.
|\\[helm-ff-run-gid]|Run Gid.
|\\[helm-ls-git-ls-files-show-others]|Toggle tracked/non tracked files view.
|\\[helm-ls-git-run-switch-to-shell]|Switch to shell
|\\<helm-generic-files-map>
|\\[helm-ff-run-toggle-basename]|Toggle basename.
|\\[helm-ff-run-zgrep]|Run zgrep.
|\\[helm-ff-run-pdfgrep]|Run Pdfgrep on marked files.
|\\[helm-ff-run-copy-file]|Copy file(s)
|\\[helm-ff-run-rename-file]|Rename file(s).
|\\[helm-ff-run-symlink-file]|Symlink file(s).
|\\[helm-ff-run-hardlink-file]|Hardlink file(s).
|\\[helm-ff-run-delete-file]|Delete file(s).
|\\[helm-ff-run-byte-compile-file]|Byte compile file(s) (C-u load) (elisp).
|\\[helm-ff-run-load-file]|Load file(s) (elisp).
|\\[helm-ff-run-ediff-file]|Ediff file.
|\\[helm-ff-run-ediff-merge-file]|Ediff merge file.
|\\[helm-ff-run-switch-other-window]|Switch other window.
|\\[helm-ff-properties-persistent]|Show file properties.
|\\[helm-ff-run-etags]|Run etags (C-u use tap, C-u C-u reload DB).
|\\[helm-yank-text-at-point]|Yank text at point.
|\\[helm-ff-run-open-file-externally]|Open file with external program (C-u to choose).
|\\[helm-ff-run-open-file-with-default-tool]|Open file externally with default tool.
|\\[helm-ff-run-insert-org-link]|Insert org link.
*** Buffers source
\\<helm-ls-git-buffer-map>
|Keys|Description
|-----------+----------|
|\\[helm-ls-git-ls-files-show-others]|Toggle view of tracked/not tracked files.
*** Status source
\\<helm-ls-git-status-map>
|Keys|Description
|-----------+----------|
|\\[helm-ls-git-run-stage-marked-and-commit]|Commit marked files.
|\\[helm-ls-git-run-stage-marked-and-amend-commit]|Stage marked files and amend.
|\\[helm-ls-git-run-stage-files]|Stage files.
|\\[helm-ls-git-run-stage-marked-and-extend-commit]|Stage marked files and extend commit.
|\\[helm-ls-git-run-stash]|Stash.
|\\[helm-ls-git-run-stash-snapshot]|Stash snapshot (no revert).
|\\[helm-ls-git-run-status-revert-files]|Revert marked files.
|\\[helm-ls-git-run-switch-to-shell]|Switch to shell.
*** Branches source
\\<helm-ls-git-branches-map>
|Keys|Description
|-----------+----------|
|\\[helm-ls-git-branches-toggle-show-all]|Show all branches locales and remotes.
|\\[helm-ls-git-run-show-log]|Show log.
|\\[helm-ls-git-run-push]|Push.
|\\[helm-ls-git-run-pull]|Pull.
|\\[helm-ls-git-run-fetch]|Fetch.
|\\[helm-ls-git-run-switch-to-shell]|Switch to shell.
")
;; Append visited files from `helm-source-ls-git' to `file-name-history'.
(add-to-list 'helm-files-save-history-extra-sources "Git files")
(defvar helm-ls-git-log-file nil) ; Set it for debugging.
(defun helm-ls-git-list-files ()
(when (and helm-ls-git-log-file
(file-exists-p helm-ls-git-log-file))
(delete-file helm-ls-git-log-file))
;; `helm-resume' will use the local value of `default-directory'
;; in `helm-buffer' as value for `default-directory'.
(helm-aif (helm-ls-git-root-dir)
(with-helm-default-directory it
(with-output-to-string
(with-current-buffer standard-output
(apply #'process-file
"git"
nil (list t helm-ls-git-log-file) nil
helm-ls-git-ls-switches))))
;; Return empty string to give to `split-string'
;; in `helm-ls-git-init'.
""))
(defun helm-ls-git-ls-files-show-others ()
"Toggle view of tracked/non tracked files."
(interactive)
(with-helm-alive-p
(setq helm-ls-git-ls-switches
(if (member "-o" helm-ls-git-ls-switches)
(remove "-o" helm-ls-git-ls-switches)
(helm-append-at-nth helm-ls-git-ls-switches "-o" 1)))
(helm-force-update)))
(put 'helm-ls-git-ls-files-show-others 'no-helm-mx t)
(cl-defun helm-ls-git-root-dir (&optional (directory default-directory))
(locate-dominating-file directory ".git"))
(defun helm-ls-git-not-inside-git-repo ()
(not (helm-ls-git-root-dir)))
(defun helm-ls-git-transformer (candidates _source)
(cl-loop with root = (helm-ls-git-root-dir)
with untracking = (member "-o" helm-ls-git-ls-switches)
for file in candidates
for abs = (expand-file-name file root)
for disp = (if (and helm-ff-transformer-show-only-basename
(not (string-match "[.]\\{1,2\\}\\'" file)))
(helm-basename file) file)
collect
(cons (propertize (if untracking (concat "? " disp) disp)
'face (if untracking
'helm-ls-git-untracked-face
'helm-ff-file))
abs)))
(defun helm-ls-git-sort-fn (candidates _source)
"Transformer for sorting candidates."
(helm-ff-sort-candidates candidates nil))
(defun helm-ls-git-init ()
(let ((data (cl-loop with root = (helm-ls-git-root-dir)
for c in (split-string (helm-ls-git-list-files) "\n" t)
collect (if (eq helm-ls-git-show-abs-or-relative 'relative)
c (expand-file-name c root)))))
(when (null data)
(setq data
(if helm-ls-git-log-file
(with-current-buffer
(find-file-noselect helm-ls-git-log-file)
(prog1
(buffer-substring-no-properties
(point-min) (point-max))
(kill-buffer)))
data)))
(helm-init-candidates-in-buffer 'global data)))
(defvar helm-ls-git--current-branch nil)
(defun helm-ls-git--branch ()
(or helm-ls-git--current-branch
(with-temp-buffer
(let ((ret (process-file "git" nil t nil "symbolic-ref" "--short" "HEAD")))
;; Use sha of HEAD when branch name is missing.
(unless (zerop ret)
(erase-buffer)
(process-file "git" nil t nil "rev-parse" "--short" "HEAD")))
;; We use here (goto-char (point-min)) instead of (point-min)
;; to not endup with a ^J control char at end of branch name.
(buffer-substring-no-properties (goto-char (point-min))
(line-end-position)))))
(defun helm-ls-git-header-name (name)
(format "%s (%s)" name (helm-ls-git--branch)))
(defun helm-ls-git-actions-list (&optional actions)
(helm-append-at-nth
actions
(helm-make-actions (lambda ()
(and helm-ls-git-status-command "Git status"))
(lambda (_candidate)
(funcall helm-ls-git-status-command
(helm-default-directory)))
"Git Log for file" 'helm-ls-git-show-log-for-file
"Switch to shell" 'helm-ls-git-switch-to-shell
"Git grep files (`C-u' only current directory)"
'helm-ls-git-grep
"Gid" 'helm-ff-gid)
1))
(defun helm-ls-git-match-part (candidate)
(if (with-helm-buffer helm-ff-transformer-show-only-basename)
(helm-basename candidate)
candidate))
(defclass helm-ls-git-source (helm-source-in-buffer)
((header-name :initform 'helm-ls-git-header-name)
(init :initform 'helm-ls-git-init)
(cleanup :initform (lambda ()
(setq helm-ls-git-ls-switches (remove "-o" helm-ls-git-ls-switches))))
(update :initform (lambda ()
(helm-set-local-variable
'helm-ls-git--current-branch nil)))
(keymap :initform 'helm-ls-git-map)
(help-message :initform 'helm-ls-git-help-message)
(match-part :initform 'helm-ls-git-match-part)
(filtered-candidate-transformer
:initform '(helm-ls-git-transformer
helm-ls-git-sort-fn))
(action-transformer :initform 'helm-transform-file-load-el)
(group :initform 'helm-ls-git)))
(defclass helm-ls-git-status-source (helm-source-in-buffer)
((header-name :initform 'helm-ls-git-header-name)
(init :initform
(lambda ()
(helm-init-candidates-in-buffer 'global
(helm-ls-git-status))))
(keymap :initform 'helm-ls-git-status-map)
(filtered-candidate-transformer :initform 'helm-ls-git-status-transformer)
(persistent-action :initform 'helm-ls-git-diff)
(persistent-help :initform "Diff")
(help-message :initform 'helm-ls-git-help-message)
(action-transformer :initform 'helm-ls-git-status-action-transformer)
(action :initform
(helm-make-actions
"Find file" (lambda (_candidate)
(let ((helm--reading-passwd-or-string t))
(mapc 'find-file (helm-marked-candidates))))
(lambda ()
(and helm-ls-git-status-command "Git status"))
(lambda (_candidate)
(funcall helm-ls-git-status-command
(helm-default-directory)))
"Switch to shell" #'helm-ls-git-switch-to-shell))
(group :initform 'helm-ls-git)))
(defun helm-ls-git-revert-buffers-in-project ()
(cl-loop for buf in (helm-browse-project-get-buffers (helm-ls-git-root-dir))
for fname = (buffer-file-name (get-buffer buf))
when (and fname (file-exists-p fname))
do (with-current-buffer buf (revert-buffer nil t))))
(defun helm-ls-git-diff (candidate)
(let ((default-directory
(expand-file-name (file-name-directory candidate)))
(win (get-buffer-window "*vc-diff*" 'visible)))
(if (and win
(eq last-command 'helm-execute-persistent-action))
(with-helm-window
(kill-buffer "*vc-diff*")
(if (and helm-persistent-action-display-window
(window-dedicated-p (next-window win 1)))
(delete-window helm-persistent-action-display-window)
(set-window-buffer win helm-current-buffer)))
(when (buffer-live-p (get-buffer "*vc-diff*"))
(kill-buffer "*vc-diff*"))
(vc-git-diff (helm-marked-candidates))
(pop-to-buffer "*vc-diff*")
(diff-mode))))
;;; Git grep
;;
(defun helm-ls-git-grep (_candidate)
(let* ((helm-grep-default-command helm-ls-git-grep-command)
helm-grep-default-recurse-command
(mkd (helm-marked-candidates))
(files (if (cdr mkd) mkd '("")))
;; Expand filename of each candidate with the git root dir.
;; The filename will be in the help-echo prop.
(helm-grep-default-directory-fn 'helm-ls-git-root-dir)
;; set `helm-ff-default-directory' to the root of project.
(helm-ff-default-directory (if helm-current-prefix-arg
default-directory
(helm-ls-git-root-dir))))
(helm-do-grep-1 files)))
(defun helm-ls-git-run-grep ()
"Run Git Grep action from helm-ls-git."
(interactive)
(with-helm-alive-p
(helm-exit-and-execute-action 'helm-ls-git-grep)))
(put 'helm-ls-git-run-grep 'no-helm-mx t)
;;; Git log
;;
(defvar helm-ls-git-log--last-log ""
"Cache for git log during the helm-ls-git-log session.")
(defvar helm-ls-git-log--last-number-commits "0"
"The number of commits actually displayed in this session.")
(defvar helm-ls-git-log--is-full nil)
(defun helm-ls-git-auto-refresh-and-scroll ()
"Increase git log by `window-height' lines."
(with-helm-window
(let ((wlines (window-height)))
(when (and (helm-end-of-source-p)
(string= helm-pattern "")
(eq this-command 'helm-next-line))
(let ((current-prefix-arg wlines))
(with-helm-after-update-hook
(setq unread-command-events nil))
(helm-force-update))))))
(defun helm-ls-git-log (&optional branch num file)
"Run git log branch -n num and return the resulting string."
(when (and branch (string-match "->" branch))
(setq branch (car (last (split-string branch "->")))))
(let* ((last-number-commits (string-to-number
helm-ls-git-log--last-number-commits))
(commits-number (if num
(number-to-string
(if (> num last-number-commits)
(- num last-number-commits)
num))
helm-ls-git-log-max-commits))
(switches `("log" "--color"
"--date=local"
"--pretty=format:%C(yellow)%h%Creset \
%C(green)%ad%Creset %<(60,trunc)%s %Cred%an%Creset %C(auto)%d%Creset"
"-n" ,commits-number
"--skip" ,(helm-stringify
helm-ls-git-log--last-number-commits)
,(or branch "") "--"))
output)
(when file (setq switches (append switches `("--follow" ,file))))
(unless helm-ls-git-log--is-full
(setq helm-ls-git-log--last-number-commits
(number-to-string
(+ last-number-commits
(string-to-number commits-number))))
(helm-set-attr 'candidate-number-limit
(string-to-number helm-ls-git-log--last-number-commits))
(message "Git log on `%s' updating to `%s' commits..."
branch helm-ls-git-log--last-number-commits)
(with-helm-default-directory (helm-ls-git-root-dir)
(setq helm-ls-git-log--last-log
(concat helm-ls-git-log--last-log
;; Avoid adding a newline at first run.
(unless (zerop last-number-commits) "\n")
(setq output
(with-output-to-string
(with-current-buffer standard-output
(apply #'process-file "git" nil t nil switches)))))))
(when (and (stringp output) (string= output ""))
(setq helm-ls-git-log--is-full t)))
(if helm-ls-git-log--is-full
(message "No more commits on `%s' branch" branch)
(message "Git log on `%s' updating to `%s' commits done"
branch helm-ls-git-log--last-number-commits))
helm-ls-git-log--last-log))
(defun helm-ls-git-show-log-for-file (file)
(helm-ls-git-show-log (helm-ls-git--branch) file))
(helm-make-command-from-action helm-ls-git-run-file-log
"Git log for candidate FILE."
'helm-ls-git-show-log-for-file)
(defun helm-ls-git-show-log (branch &optional file)
(let ((name (if (helm-ls-git-detached-state-p)
(helm-ls-git--branch)
(helm-ls-git-normalize-branch-name branch)))
;; Use helm-current-prefix-arg only on first call
;; of init function.
(prefarg helm-current-prefix-arg))
(when (buffer-live-p "*git log diff*")
(kill-buffer "*git log diff*"))
(helm :sources (helm-build-in-buffer-source "Git log"
:header-name (lambda (sname)
(format "%s (%s)"
sname (substring-no-properties name)))
:init (lambda ()
(helm-init-candidates-in-buffer 'global
(helm-ls-git-log
name (helm-aif (or prefarg
;; for force-update.
current-prefix-arg)
(prefix-numeric-value it))
file))
(setq prefarg nil))
:get-line 'buffer-substring
:marked-with-props 'withprop
:cleanup (lambda ()
(setq helm-ls-git-log--last-log ""
helm-ls-git-log--last-number-commits "0"
helm-ls-git-log--is-full nil))
:help-message 'helm-ls-git-help-message
:action '(("Show commit" . helm-ls-git-log-show-commit)
("Find file at rev" . helm-ls-git-log-find-file)
("Ediff file at revs" . helm-ls-git-ediff-file-at-revs)
("Kill rev as short hash" .
helm-ls-git-log-kill-short-hash)
("Kill rev as long hash" .
helm-ls-git-log-kill-long-hash)
("Cherry-pick" . helm-ls-git-log-cherry-pick)
("Format patches (range between 2 marked)" . helm-ls-git-log-format-patch)
("Git am (range between 2 marked)" . helm-ls-git-log-am)
("Git interactive rebase" . helm-ls-git-log-interactive-rebase)
("Hard reset" . helm-ls-git-log-hard-reset)
("Soft reset" . helm-ls-git-log-soft-reset)
("Git revert" . helm-ls-git-log-revert)
("Checkout" . helm-ls-git-log-checkout))
:candidate-transformer
(lambda (candidates)
(cl-loop for c in candidates
collect (ansi-color-apply c)))
:group 'helm-ls-git)
:move-selection-before-hook (and helm-ls-git-auto-refresh-at-eob
'helm-ls-git-auto-refresh-and-scroll)
:move-to-line-cycle-in-source (unless helm-ls-git-auto-refresh-at-eob
(default-value 'helm-move-to-line-cycle-in-source))
:buffer "*helm-ls-git log*")))
(defun helm-ls-git-log-get-rev (candidate)
(car (split-string candidate)))
(defun helm-ls-git-log-show-commit-1 (candidate)
(let ((sha (helm-ls-git-log-get-rev candidate)))
(with-current-buffer (get-buffer-create "*git log diff*")
(let ((inhibit-read-only t))
(erase-buffer)
(insert (with-helm-default-directory (helm-ls-git-root-dir
(helm-default-directory))
(with-output-to-string
(with-current-buffer standard-output
(process-file
"git" nil (list t helm-ls-git-log-file) nil
"show" "-p" sha)))))
(goto-char (point-min))
(diff-mode))
(display-buffer (current-buffer)))))
(defun helm-ls-git-log-kill-short-hash (candidate)
(kill-new (helm-ls-git-log-get-rev candidate)))
(defun helm-ls-git-log-kill-long-hash (_candidate)
(helm-ls-git-log-get-long-hash 'kill))
(defun helm-ls-git-log-get-long-hash (&optional kill)
(with-helm-buffer
(let* ((cand (helm-get-selection nil 'withprop))
(short-hash (helm-ls-git-log-get-rev cand))
str)
(setq str
(replace-regexp-in-string
"\n" ""
(shell-command-to-string
(format "git rev-parse --default %s" short-hash))))
(if kill (kill-new str) str))))
(defun helm-ls-git-log-format-patch (_candidate)
(helm-ls-git-log-format-patch-1))
(defun helm-ls-git-log-am (_candidate)
(helm-ls-git-log-format-patch-1 'am))
(defun helm-ls-git-log-format-patch-1 (&optional am)
(let ((commits (cl-loop for c in (helm-marked-candidates)
collect (helm-ls-git-log-get-rev c)))
range switches)
(cond ((= 2 (length commits))
;; Using "..." makes a range from top marked (included) to
;; bottom marked (not included) e.g. when we have commit-2
;; marked and commit-5 marked the serie of patches will be
;; 01-commit-4.patch, 02-commit-3.patch, 03-commit-2.patch,
;; git taking care of numering the patch in reversed order
;; for further apply.
(setq range (mapconcat 'identity (sort commits #'string-lessp) "...")
switches `("format-patch" ,range)))
((not (cdr commits))
(setq range (car commits)
switches `("format-patch" "-1" ,range)))
((> (length commits) 2)
(error "Specify either a single commit or a range with only two marked commits")))
(with-helm-default-directory (helm-ls-git-root-dir
(helm-default-directory))
(if am
(with-current-buffer-window "*git am*" '(display-buffer-below-selected
(window-height . fit-window-to-buffer)
(preserve-size . (nil . t)))
nil
(process-file-shell-command
(format "git %s | git am -3 -k"
(mapconcat 'identity (helm-append-at-nth switches '("-k --stdout") 1) " "))
nil t t))
(apply #'process-file "git" nil "*git format-patch*" nil switches)))))
(defun helm-ls-git-log-reset-1 (hard-or-soft)
(let ((rev (helm-ls-git-log-get-rev (helm-get-selection nil 'withprop)))
(arg (cl-case hard-or-soft
(hard "--hard")
(soft "--soft"))))
(with-helm-default-directory (helm-ls-git-root-dir
(helm-default-directory))
(when (and (y-or-n-p (format "%s reset to <%s>?"
(capitalize (symbol-name hard-or-soft)) rev))
(= (process-file "git" nil nil nil "reset" arg rev) 0))
(message "Now at `%s'" (helm-ls-git-oneline-log
(helm-ls-git--branch)))))))
(defun helm-ls-git-log-hard-reset (_candidate)
(helm-ls-git-log-reset-1 'hard)
(helm-ls-git-revert-buffers-in-project))
(defun helm-ls-git-log-soft-reset (_candidate)
(helm-ls-git-log-reset-1 'soft))
(defun helm-ls-git-log-revert (_candidate)
(let ((rev (helm-ls-git-log-get-rev (helm-get-selection nil 'withprop))))
(helm-ls-git-with-editor "revert" rev)))
(defun helm-ls-git-log-revert-continue (_candidate)
(helm-ls-git-with-editor "revert" "--continue"))
(defun helm-ls-git-log-revert-abort (_candidate)
(with-helm-default-directory (helm-default-directory)
(process-file "git" nil nil nil "revert" "--abort")))
(defun helm-ls-git-log-checkout (_candidate)
(let ((rev (helm-ls-git-log-get-rev (helm-get-selection nil 'withprop))))
(helm-ls-git-checkout rev)))
(defun helm-ls-git-log-show-commit (candidate)
(if (and (eq last-command 'helm-execute-persistent-action)
(get-buffer-window "*git log diff*" 'visible))
(kill-buffer "*git log diff*")
(helm-ls-git-log-show-commit-1 candidate)))
(defun helm-ls-git-log-find-file-1 (candidate &optional file buffer-only)
(with-helm-default-directory (helm-default-directory)
(let* ((rev (substring-no-properties (helm-ls-git-log-get-rev candidate)))
(file (or file
(helm :sources (helm-build-in-buffer-source "Git cat-file"
:data (helm-ls-git-list-files))
:buffer "*helm-ls-git cat-file*")))
;; Git command line needs 1234:lisp/foo.
(fname (concat rev ":" file))
;; Whereas the file created will be lisp/1234:foo.
(path (expand-file-name
(concat (helm-basedir file) rev ":" (helm-basename file))
(helm-ls-git-root-dir)))
str status buf)
(setq str (with-output-to-string
(with-current-buffer standard-output
(setq status (process-file
"git" nil t nil "cat-file" "-p" fname)))))
(if (zerop status)
(progn