1989 lines
84 KiB
EmacsLisp
1989 lines
84 KiB
EmacsLisp
;;; helm-ls-git.el --- list git files. -*- lexical-binding: t -*-
|
||
|
||
;; Copyright (C) 2012 ~ 2022 Thierry Volpiatto
|
||
|
||
;; Package-Requires: ((helm "1.7.8"))
|
||
;; Package-Version: 20220914.954
|
||
;; Package-Commit: a0dbaa0df28942a70f580dcbd73febe29b6c2167
|
||
|
||
;; 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/>.
|
||
|
||
;;; 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).
|
||
NOTE: This reflects the number of candidates fetched and stored, not
|
||
the number of candidates displayed which is relative to
|
||
`helm-candidate-number-limit'. IOW if `helm-candidate-number-limit'
|
||
== 500 and `helm-ls-git-log-max-commits' == 600, only 500 candidates
|
||
will be displayed but if you search for a candidate which is in the
|
||
range 500/600 you will find it."
|
||
: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)
|
||
|
||
|
||
(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)
|
||
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)
|
||
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)
|
||
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).
|
||
|
||
*** 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 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.
|
||
|
||
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
|
||
\\<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.")
|
||
|
||
|
||
|
||
;; 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)))
|
||
"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" 'helm-find-many-files
|
||
(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)
|
||
"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)
|
||
(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 (branch)
|
||
(let ((name (replace-regexp-in-string "[ *]" "" 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 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))))
|
||
(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))
|
||
: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-show-commit-1 (candidate)
|
||
(let ((sha (car (split-string 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 (car (split-string 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 (car (split-string 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 (car (split-string 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 (car (split-string (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))
|
||
|
||
(defun helm-ls-git-log-soft-reset (_candidate)
|
||
(helm-ls-git-log-reset-1 'soft))
|
||
|
||
(defun helm-ls-git-log-revert (_candidate)
|
||
(let ((rev (car (split-string (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-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 (car (split-string 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
|
||
(with-current-buffer (setq buf (find-file-noselect path))
|
||
(insert str)
|
||
;; Prevent kill-buffer asking after ediff ends.
|
||
(set-buffer-modified-p (not buffer-only))
|
||
(goto-char (point-min))
|
||
(unless buffer-only
|
||
(save-buffer)))
|
||
(if buffer-only buf (find-file path)))
|
||
(error "No such file %s at %s" file rev)))))
|
||
|
||
(defun helm-ls-git-log-find-file (candidate)
|
||
(helm-ls-git-log-find-file-1 candidate))
|
||
|
||
(defun helm-ls-git-ediff-file-at-revs (_candidate)
|
||
(let* ((marked (helm-marked-candidates))
|
||
(tip (unless (cdr marked)
|
||
(helm-ls-git-oneline-log (helm-ls-git--branch))))
|
||
buf1 buf2 file)
|
||
(when tip
|
||
(cl-assert (not (string= (car (split-string tip))
|
||
(car (split-string (car marked)))))
|
||
nil "Can't ediff a file at same revision"))
|
||
(setq file (helm :sources (helm-build-in-buffer-source "Git cat-file"
|
||
:data (helm-ls-git-list-files))
|
||
:buffer "*helm-ls-git cat-file*"))
|
||
(setq buf1 (helm-ls-git-log-find-file-1 (or tip (car marked)) file :buffer-only)
|
||
buf2 (helm-ls-git-log-find-file-1 (if tip (car marked) (cadr marked))
|
||
file :buffer-only))
|
||
(ediff-buffers buf1 buf2)))
|
||
|
||
(defun helm-ls-git-log-cherry-pick (_candidate)
|
||
(let* ((commits (cl-loop for c in (helm-marked-candidates)
|
||
collect (car (split-string c)) into revs
|
||
finally return (sort revs #'string-greaterp))))
|
||
(with-helm-default-directory (helm-ls-git-root-dir
|
||
(helm-default-directory))
|
||
(with-current-buffer-window "*git cherry-pick*" '(display-buffer-below-selected
|
||
(window-height . fit-window-to-buffer)
|
||
(preserve-size . (nil . t)))
|
||
nil
|
||
(apply #'process-file "git" nil "*git cherry-pick*" nil "cherry-pick" commits)))))
|
||
|
||
(defun helm-ls-git-cherry-pick-abort (_candidate)
|
||
(with-helm-default-directory (helm-default-directory)
|
||
(process-file "git" nil nil nil "cherry-pick" "--abort")))
|
||
|
||
(defun helm-ls-git-rebase-abort (_candidate)
|
||
(with-helm-default-directory (helm-default-directory)
|
||
(process-file "git" nil nil nil "rebase" "--abort")))
|
||
|
||
(defun helm-ls-git-merge-abort (_candidate)
|
||
(with-helm-default-directory (helm-default-directory)
|
||
(process-file "git" nil nil nil "merge" "--abort")))
|
||
|
||
(defun helm-ls-git-rebase-continue (_candidate)
|
||
(helm-ls-git-with-editor "rebase" "--continue"))
|
||
|
||
(defun helm-ls-git-cherry-pick-continue (_candidate)
|
||
(helm-ls-git-with-editor "cherry-pick" "--continue"))
|
||
|
||
(defun helm-ls-git-am-continue (_candidate)
|
||
(helm-ls-git-with-editor "am" "--continue"))
|
||
|
||
(defun helm-ls-git-merge-continue (_candidate)
|
||
(helm-ls-git-with-editor "merge" "--continue"))
|
||
|
||
(defun helm-ls-git-rebase-running-p ()
|
||
(with-helm-buffer
|
||
(with-helm-default-directory (helm-ls-git-root-dir)
|
||
(let ((git-dir (expand-file-name ".git" default-directory)))
|
||
(or (file-exists-p (expand-file-name "rebase-merge" git-dir))
|
||
(file-exists-p (expand-file-name "rebase-apply/onto" git-dir)))))))
|
||
|
||
(defun helm-ls-git-log-interactive-rebase (_candidate)
|
||
"Rebase interactively current branch from CANDIDATE.
|
||
Where CANDIDATE is a candidate from git log source and its commit
|
||
object will be passed git rebase i.e. git rebase -i <hash>."
|
||
(if (helm-ls-git-rebase-running-p)
|
||
(if (y-or-n-p "A rebase is already running, continue ?")
|
||
(helm-ls-git-rebase-continue nil)
|
||
(helm-ls-git-rebase-abort nil))
|
||
(let ((hash (helm-ls-git-log-get-long-hash)))
|
||
(helm-ls-git-with-editor "rebase" "-i" hash))))
|
||
|
||
(defun helm-ls-git-run-show-log ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-exit-and-execute-action #'helm-ls-git-show-log)))
|
||
(put 'helm-ls-git-run-show-log 'no-helm-mx t)
|
||
|
||
|
||
;;; Git branch basic management
|
||
;;
|
||
(defvar helm-ls-git-branches-show-all nil)
|
||
|
||
(defun helm-ls-git-collect-branches (&optional arg)
|
||
(helm-aif (helm-ls-git-root-dir)
|
||
(with-helm-default-directory it
|
||
(with-output-to-string
|
||
(with-current-buffer standard-output
|
||
(cond ((null arg)
|
||
;; Only local branches.
|
||
(apply #'process-file "git" nil t nil '("branch")))
|
||
(t
|
||
(apply #'process-file "git" nil t nil '("branch" "-a")))))))
|
||
""))
|
||
|
||
(defun helm-ls-git-branches-toggle-show-all ()
|
||
(interactive)
|
||
(setq helm-ls-git-branches-show-all (not helm-ls-git-branches-show-all))
|
||
(helm-force-update))
|
||
(put 'helm-ls-git-branches-toggle-show-all 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-checkout (candidate)
|
||
(let ((default-directory (helm-default-directory)))
|
||
(if (and helm-ls-git-auto-checkout
|
||
(helm-ls-git-modified-p t))
|
||
(helm-ls-git-stash-1 "")
|
||
(cl-assert (not (helm-ls-git-modified-p t))
|
||
nil "Please commit or stash your changes before proceeding"))
|
||
(with-helm-default-directory (helm-ls-git-root-dir)
|
||
(let* ((branch (replace-regexp-in-string "[ ]" "" candidate))
|
||
(real (replace-regexp-in-string "\\`\\*" "" branch)))
|
||
(if (string-match "\\`[*]" candidate)
|
||
(message "Already on %s branch" real)
|
||
(let* ((switches (if (string-match "\\`[Rr]emotes" real)
|
||
`("checkout" "-b"
|
||
,(car (last (split-string real "/" t)))
|
||
"-t" ,real)
|
||
`("checkout" ,real)))
|
||
(status (apply #'process-file "git"
|
||
nil nil nil
|
||
switches)))
|
||
(if (= status 0)
|
||
(progn (message "Switched to %s branch" real)
|
||
(helm-ls-git-revert-buffers-in-project))
|
||
(error "Process exit with non zero status"))))))))
|
||
|
||
(defun helm-ls-git-branches-create (candidate)
|
||
(with-helm-default-directory (helm-ls-git-root-dir)
|
||
(process-file "git" nil nil nil
|
||
"checkout" "-B" candidate "-t" (helm-ls-git--branch))))
|
||
|
||
(defun helm-ls-git-branches-delete (candidate)
|
||
(with-helm-default-directory (helm-ls-git-root-dir)
|
||
(let* ((branch (helm-ls-git-normalize-branch-name candidate))
|
||
(remote (string-match "remotes/" candidate))
|
||
(switches (if remote
|
||
`("-D" "-r" ,branch)
|
||
`("-D" ,branch))))
|
||
(cl-assert (not (string-match "\\`[*]" candidate))
|
||
nil "Can't delete current branch")
|
||
(if (= (apply #'process-file "git" nil nil nil "branch" switches) 0)
|
||
(progn
|
||
(when (and remote
|
||
(or helm-ls-git-delete-branch-on-remote
|
||
(y-or-n-p (format "Delete `%s' branch on remote as well ?" branch))))
|
||
(let ((proc (start-file-process
|
||
"git" "*helm-ls-git branch delete*"
|
||
"git" "push" "origin" "--delete"
|
||
(car (last (split-string branch "/" t))))))
|
||
(set-process-sentinel
|
||
proc
|
||
(lambda (_process event)
|
||
(if (string= event "finished\n")
|
||
(message "Remote branch %s deleted successfully" branch)
|
||
(message "Failed to delete remote branch %s" branch))))))
|
||
(message "Local branch %s deleted successfully" branch))
|
||
(message "failed to delete branch %s" branch)))))
|
||
|
||
(defun helm-ls-git-normalize-branch-names (names)
|
||
(cl-loop for name in names collect
|
||
(helm-ls-git-normalize-branch-name name)))
|
||
|
||
(defun helm-ls-git-normalize-branch-name (name)
|
||
(helm-aand name
|
||
(replace-regexp-in-string " " "" it)
|
||
(replace-regexp-in-string "[*]" "" it)
|
||
(replace-regexp-in-string "remotes/" "" it)))
|
||
|
||
(defun helm-ls-git-delete-marked-branches (_candidate)
|
||
(let* ((branches (helm-marked-candidates))
|
||
(bnames (helm-ls-git-normalize-branch-names branches))
|
||
(display-buf "*helm-ls-git deleted branches*"))
|
||
(with-helm-display-marked-candidates
|
||
display-buf bnames
|
||
(when (y-or-n-p "Really delete branche(s) ?")
|
||
(cl-loop for b in branches
|
||
do (helm-ls-git-branches-delete b))))))
|
||
|
||
(defun helm-ls-git-modified-p (&optional ignore-untracked)
|
||
(with-helm-default-directory (helm-ls-git-root-dir)
|
||
(not (string= (helm-ls-git-status ignore-untracked) ""))))
|
||
|
||
(defun helm-ls-git-branches-merge (candidate)
|
||
(with-helm-default-directory (helm-ls-git-root-dir)
|
||
(let ((branch (replace-regexp-in-string "[ ]" "" candidate))
|
||
(current (helm-ls-git--branch)))
|
||
(when (y-or-n-p (format "Merge branch %s into %s?" branch current))
|
||
(if (= (process-file "git" nil nil nil "merge" branch) 0)
|
||
(progn (message "Branch %s merged successfully into %s" branch current)
|
||
(helm-ls-git-revert-buffers-in-project))
|
||
(message "failed to merge branch %s" branch))))))
|
||
|
||
(defvar helm-ls-git-create-branch-source
|
||
(helm-build-dummy-source "Create branch"
|
||
:filtered-candidate-transformer
|
||
(lambda (_candidates _source)
|
||
(list (or (and (not (string= helm-pattern ""))
|
||
helm-pattern)
|
||
"Enter new branch name")))
|
||
:action 'helm-ls-git-branches-create))
|
||
|
||
(defun helm-ls-git-oneline-log (branch)
|
||
(let ((output (with-output-to-string
|
||
(with-current-buffer standard-output
|
||
(process-file
|
||
"git" nil t nil
|
||
"log" (car (split-string branch "->"))
|
||
"-n" "1" "--oneline")))))
|
||
(replace-regexp-in-string "\n" "" output)))
|
||
|
||
(defun helm-ls-git-branches-transformer (candidates)
|
||
(cl-loop for c in candidates
|
||
for maxlen = (cl-loop for i in candidates maximize (length i))
|
||
for name = (replace-regexp-in-string "[ *]" "" c)
|
||
for log = (helm-ls-git-oneline-log name)
|
||
for disp = (if (string-match "\\`\\([*]\\)\\(.*\\)" c)
|
||
(format "%s%s: %s%s"
|
||
(propertize (match-string 1 c)
|
||
'face 'helm-ls-git-branches-current)
|
||
(propertize (match-string 2 c)
|
||
'face 'helm-ls-git-branches-name-current)
|
||
(make-string (- maxlen (length c)) ? )
|
||
log)
|
||
(format "%s: %s%s"
|
||
(propertize c 'face 'helm-ls-git-branches-name)
|
||
(make-string (- maxlen (length c)) ? )
|
||
log))
|
||
collect (cons disp c)))
|
||
|
||
(defun helm-ls-git-push (_candidate)
|
||
(with-helm-default-directory (helm-default-directory)
|
||
(let ((branch (helm-ls-git--branch))
|
||
pr tm)
|
||
(when (y-or-n-p (format "Really push branch `%s' on remote ?" branch))
|
||
(setq pr (make-progress-reporter
|
||
(format "Pushing branch `%s' on remote..." branch))
|
||
tm (run-at-time 1 0.1 #'progress-reporter-update pr))
|
||
(let ((proc (start-file-process
|
||
"git" "*helm-ls-git push*" "git" "push" "origin" "HEAD")))
|
||
(set-process-sentinel
|
||
proc (lambda (_process event)
|
||
(if (string= event "finished\n")
|
||
(progress-reporter-done pr)
|
||
(message "Failed to push branch `%s' on remote" branch))
|
||
(cancel-timer tm))))))))
|
||
|
||
(defun helm-ls-git-run-push ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-exit-and-execute-action #'helm-ls-git-push)))
|
||
(put 'helm-ls-git-run-push 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-remotes ()
|
||
(with-helm-default-directory (helm-default-directory)
|
||
(with-output-to-string
|
||
(with-current-buffer standard-output
|
||
(process-file "git" nil t nil "remote")))))
|
||
|
||
(defun helm-ls-git--pull-or-fetch (command &rest args)
|
||
(with-helm-default-directory (helm-default-directory)
|
||
(let* ((remote "origin")
|
||
(pcommand (capitalize command))
|
||
(branch (helm-ls-git--branch))
|
||
;; A `C-g' in helm-comp-read will quit function as well.
|
||
(switches (if current-prefix-arg
|
||
(append (list command)
|
||
args
|
||
(list (setq remote
|
||
(helm-comp-read
|
||
(format "%s from: " pcommand)
|
||
(split-string
|
||
(helm-ls-git-remotes)
|
||
"\n")
|
||
:allow-nest t)))
|
||
(list branch))
|
||
(append (list command) args)))
|
||
(pr (make-progress-reporter
|
||
(format "%sing from `%s/%s'..." pcommand remote branch)))
|
||
(tm (run-at-time 1 0.1 (lambda () (progress-reporter-update pr))))
|
||
process-connection-type
|
||
proc)
|
||
(setq proc (apply #'helm-ls-git-with-editor switches))
|
||
(with-current-buffer (process-buffer proc) (erase-buffer))
|
||
(set-process-filter proc 'helm-ls-git--filter-process)
|
||
(save-selected-window
|
||
(display-buffer (process-buffer proc)))
|
||
(set-process-sentinel
|
||
proc (lambda (process event)
|
||
(let ((status (process-exit-status process)))
|
||
(if (string= event "finished\n")
|
||
(progn (progress-reporter-done pr)
|
||
(when helm-alive-p
|
||
(with-helm-window (helm-force-update "^\\*"))))
|
||
(message "Failed %sing from %s: Process exited with code %s"
|
||
command remote status))
|
||
(and tm (cancel-timer tm))))))))
|
||
|
||
(defun helm-ls-git--filter-process (proc string)
|
||
(when (buffer-live-p (process-buffer proc))
|
||
(with-current-buffer (process-buffer proc)
|
||
(let ((moving (= (point) (process-mark proc))))
|
||
(save-excursion
|
||
;; Insert the text, advancing the process marker.
|
||
(goto-char (process-mark proc))
|
||
;; Ignore git progress reporter lines.
|
||
(unless (string-match-p "
|
||
\\'" string)
|
||
(insert string))
|
||
(set-marker (process-mark proc) (point)))
|
||
(when moving
|
||
(goto-char (process-mark proc)))))))
|
||
|
||
(defun helm-ls-git-pull (_candidate)
|
||
(helm-ls-git--pull-or-fetch "pull" "--stat"))
|
||
|
||
(defun helm-ls-git-fetch (_candidate)
|
||
(helm-ls-git--pull-or-fetch "fetch"))
|
||
|
||
(defun helm-ls-git-run-pull ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-set-attr 'pull 'helm-ls-git-pull)
|
||
(helm-execute-persistent-action 'pull)))
|
||
(put 'helm-ls-git-run-pull 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-run-fetch ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-set-attr 'fetch '(helm-ls-git-fetch . never-split))
|
||
(helm-execute-persistent-action 'fetch)))
|
||
(put 'helm-ls-git-run-fetch 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-branch-rebase (candidate)
|
||
"Rebase CANDIDATE branch on current branch."
|
||
(if (helm-ls-git-rebase-running-p)
|
||
(if (y-or-n-p "A rebase is already running, continue ?")
|
||
(helm-ls-git-rebase-continue nil)
|
||
(helm-ls-git-rebase-abort nil))
|
||
(let ((branch (helm-ls-git-normalize-branch-name candidate))
|
||
(current (helm-ls-git--branch)))
|
||
(when (y-or-n-p (format "Rebase branch %s from %s?" current branch))
|
||
(if (= (process-file "git" nil nil nil "rebase" branch) 0)
|
||
(progn (message "Branch %s rebased successfully from %s" current branch)
|
||
(helm-ls-git-revert-buffers-in-project))
|
||
(message "failed to rebase from branch %s, try to abort rebasing or resolve conflicts" branch))))))
|
||
|
||
(defvar helm-ls-git-branches-source
|
||
(helm-build-in-buffer-source "Git branches"
|
||
:init (lambda ()
|
||
(let ((data (helm-ls-git-collect-branches
|
||
helm-ls-git-branches-show-all)))
|
||
(helm-init-candidates-in-buffer 'global data)))
|
||
:candidate-transformer 'helm-ls-git-branches-transformer
|
||
:action-transformer (lambda (actions candidate)
|
||
(cond ((string-match "\\`[*] detached" candidate)
|
||
(append
|
||
actions
|
||
'(("Git rebase continue" .
|
||
helm-ls-git-rebase-continue)
|
||
("Git cherry-pick continue" .
|
||
helm-ls-git-cherry-pick-continue)
|
||
("Git AM continue" .
|
||
helm-ls-git-am-continue)
|
||
("Git merge continue" .
|
||
helm-ls-git-merge-continue)
|
||
("Git revert continue" .
|
||
helm-ls-git-log-revert-continue)
|
||
("Git cherry-pick abort" .
|
||
helm-ls-git-cherry-pick-abort)
|
||
("Git rebase abort" .
|
||
helm-ls-git-rebase-abort)
|
||
("Git AM abort" .
|
||
helm-ls-git-am-abort)
|
||
("Git merge abort" .
|
||
helm-ls-git-merge-abort)
|
||
("Git revert abort" .
|
||
helm-ls-git-log-revert-abort))))
|
||
((not (string-match "\\`[*]" candidate))
|
||
(append
|
||
'(("Checkout" . helm-ls-git-checkout)
|
||
("Delete branche(s)" . helm-ls-git-delete-marked-branches)
|
||
("Merge in current" .
|
||
helm-ls-git-branches-merge)
|
||
("Rebase in current" .
|
||
helm-ls-git-branch-rebase))
|
||
actions))
|
||
(t (helm-append-at-nth
|
||
actions
|
||
'(("Git amend" . helm-ls-git-amend-commit)
|
||
("Git push (C-c P)" . helm-ls-git-push))
|
||
2))))
|
||
:help-message 'helm-ls-git-help-message
|
||
:cleanup (lambda () (setq helm-ls-git-branches-show-all nil))
|
||
:persistent-help "Checkout"
|
||
:persistent-action (lambda (candidate)
|
||
(helm-ls-git-checkout candidate)
|
||
(helm-force-update "^\\*"))
|
||
:action (helm-make-actions
|
||
(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
|
||
"Git log (M-L)" #'helm-ls-git-show-log)
|
||
:keymap 'helm-ls-git-branches-map
|
||
:group 'helm-ls-git))
|
||
|
||
|
||
;;; Stashing
|
||
;;
|
||
(defun helm-ls-git-list-stashes ()
|
||
(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
|
||
(list "stash" "list")))))))
|
||
|
||
(defun helm-ls-git-get-stash-name (candidate)
|
||
(when (string-match "stash@[{][0-9]+[}]" candidate)
|
||
(match-string 0 candidate)))
|
||
|
||
(defun helm-ls-git-stash-show (candidate)
|
||
(if (and (eq last-command 'helm-execute-persistent-action)
|
||
(get-buffer-window "*stash diff*" 'visible))
|
||
(kill-buffer "*stash diff*")
|
||
(let ((stash (helm-ls-git-get-stash-name candidate)))
|
||
(with-current-buffer (get-buffer-create "*stash diff*")
|
||
(let ((inhibit-read-only t))
|
||
(erase-buffer)
|
||
(insert (with-helm-default-directory (helm-ls-git-root-dir)
|
||
(with-output-to-string
|
||
(with-current-buffer standard-output
|
||
(process-file
|
||
"git" nil (list t helm-ls-git-log-file) nil
|
||
"stash" "show" "-p" stash)))))
|
||
(diff-mode))
|
||
(display-buffer (current-buffer))))))
|
||
|
||
(defun helm-ls-git-stash-apply (candidate)
|
||
(let ((num (helm-ls-git-get-stash-name candidate)))
|
||
(if (eq (process-file "git" nil nil nil "stash" "apply" num) 0)
|
||
(progn
|
||
(helm-ls-git-revert-buffers-in-project)
|
||
(message "Stash <%s> applied" candidate))
|
||
(error "Couldn't apply stash <%s>" candidate))))
|
||
|
||
(defun helm-ls-git-stash-pop (candidate)
|
||
(let ((num (helm-ls-git-get-stash-name candidate)))
|
||
(if (eq (process-file "git" nil nil nil "stash" "pop" num) 0)
|
||
(progn
|
||
(helm-ls-git-revert-buffers-in-project)
|
||
(message "Stashed pop <%s>" candidate))
|
||
(error "Couldn't stash pop <%s>" candidate))))
|
||
|
||
(defun helm-ls-git-stash-1 (name)
|
||
(with-helm-default-directory (helm-ls-git-root-dir)
|
||
(apply #'process-file "git" nil nil nil `("stash" "push" "-m" ,name))
|
||
(helm-ls-git-revert-buffers-in-project)))
|
||
|
||
(defun helm-ls-git-stash (_candidate)
|
||
(let ((name (read-string "Stash name: ")))
|
||
(helm-ls-git-stash-1 name)))
|
||
|
||
(defun helm-ls-git-run-stash ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-exit-and-execute-action 'helm-ls-git-stash)))
|
||
(put 'helm-ls-git-run-stash 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-stash-snapshot (_candidate)
|
||
(vc-git-stash-snapshot))
|
||
|
||
(defun helm-ls-git-run-stash-snapshot ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-exit-and-execute-action 'helm-ls-git-stash-snapshot)))
|
||
(put 'helm-ls-git-run-stash-snapshot 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-stash-drop (candidate)
|
||
(let ((num (helm-ls-git-get-stash-name candidate)))
|
||
(if (eq (process-file "git" nil nil nil "stash" "drop" num) 0)
|
||
(message "Stash <%s> deleted" candidate)
|
||
(error "Couldn't delete <%s>" candidate))))
|
||
|
||
(defun helm-ls-git-stash-drop-marked (_candidate)
|
||
(let ((mkd (helm-marked-candidates)))
|
||
(cl-loop with sorted =
|
||
(sort mkd (lambda (s1 s2)
|
||
(let ((n1 (and (string-match
|
||
"^stash@[{]\\([0-9]+\\)[}]" s1)
|
||
(match-string 1 s1)))
|
||
(n2 (and (string-match
|
||
"^stash@[{]\\([0-9]+\\)[}]" s2)
|
||
(match-string 1 s2))))
|
||
(string-greaterp n1 n2))))
|
||
for c in sorted do (helm-ls-git-stash-drop c))))
|
||
|
||
(defun helm-ls-git-apply-patch (_candidate)
|
||
(with-helm-default-directory (helm-default-directory)
|
||
(let ((patchs (helm-marked-candidates)))
|
||
(with-current-buffer-window "*git apply*" '(display-buffer-below-selected
|
||
(window-height . fit-window-to-buffer)
|
||
(preserve-size . (nil . t)))
|
||
nil
|
||
(apply #'process-file "git" nil t t "apply" patchs)
|
||
(helm-ls-git-revert-buffers-in-project)))))
|
||
|
||
(defvar helm-ls-git-stashes-source
|
||
(helm-build-in-buffer-source "Stashes"
|
||
:data 'helm-ls-git-list-stashes
|
||
:persistent-action 'helm-ls-git-stash-show
|
||
:action '(("Apply stash" . helm-ls-git-stash-apply)
|
||
("Pop stash" . helm-ls-git-stash-pop)
|
||
("Drop stashe(s)" . helm-ls-git-stash-drop-marked))
|
||
:group 'helm-ls-git))
|
||
|
||
;;; Git status
|
||
(defun helm-ls-git-status (&optional ignore-untracked)
|
||
(when (and helm-ls-git-log-file
|
||
(file-exists-p helm-ls-git-log-file))
|
||
(delete-file helm-ls-git-log-file))
|
||
(let ((process-environment
|
||
(cons "GIT_OPTIONAL_LOCKS=0" process-environment)))
|
||
(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
|
||
(if ignore-untracked
|
||
(list "status" "-uno" "--porcelain")
|
||
(list "status" "--porcelain")))))))))
|
||
|
||
(defun helm-ls-git-status-transformer (candidates _source)
|
||
(cl-loop with root = (helm-ls-git-root-dir)
|
||
for i in candidates
|
||
collect
|
||
(cond ((string-match "^\\( M \\)\\(.*\\)" i) ; modified.
|
||
(cons (propertize i 'face 'helm-ls-git-modified-not-staged-face)
|
||
(expand-file-name (match-string 2 i) root)))
|
||
((string-match "^\\(M+ *\\)\\(.*\\)" i) ; modified and staged.
|
||
(cons (propertize i 'face 'helm-ls-git-modified-and-staged-face)
|
||
(expand-file-name (match-string 2 i) root)))
|
||
((string-match "^\\([?]\\{2\\} \\)\\(.*\\)" i)
|
||
(cons (propertize i 'face 'helm-ls-git-untracked-face)
|
||
(expand-file-name (match-string 2 i) root)))
|
||
((string-match "^\\(AC? +\\)\\(.*\\)" i)
|
||
(cons (propertize i 'face 'helm-ls-git-added-copied-face)
|
||
(expand-file-name (match-string 2 i) root)))
|
||
((string-match "^\\( [D] \\)\\(.*\\)" i)
|
||
(cons (propertize i 'face 'helm-ls-git-deleted-not-staged-face)
|
||
(expand-file-name (match-string 2 i) root)))
|
||
((string-match "^\\(RM?\\).* -> \\(.*\\)" i)
|
||
(cons (propertize i 'face 'helm-ls-git-renamed-modified-face)
|
||
(expand-file-name (match-string 2 i) root)))
|
||
((string-match "^\\(A?D +\\)\\(.*\\)" i)
|
||
(cons (propertize i 'face 'helm-ls-git-deleted-and-staged-face)
|
||
(expand-file-name (match-string 2 i) root)))
|
||
((string-match "^\\(UU \\)\\(.*\\)" i)
|
||
(cons (propertize i 'face 'helm-ls-git-conflict-face)
|
||
(expand-file-name (match-string 2 i) root)))
|
||
((string-match "^\\(AM \\)\\(.*\\)" i)
|
||
(cons (propertize i 'face 'helm-ls-git-added-modified-face)
|
||
(expand-file-name (match-string 2 i) root)))
|
||
(t i))))
|
||
|
||
(defun helm-ls-git-status-action-transformer (actions _candidate)
|
||
(let ((disp (helm-get-selection nil t))
|
||
(mofified-actions
|
||
(helm-make-actions "Diff file" 'helm-ls-git-diff
|
||
"Revert file(s)" 'helm-ls-git-status-revert-files
|
||
"Copy file(s) `C-u to follow'" 'helm-find-files-copy
|
||
"Rename file(s) `C-u to follow'" 'helm-find-files-rename)))
|
||
;; Unregistered files
|
||
(cond ((string-match "^[?]\\{2\\}" disp)
|
||
(append actions
|
||
(helm-make-actions "Add file(s)"
|
||
(lambda (candidate)
|
||
(let ((default-directory
|
||
(file-name-directory candidate))
|
||
(marked (helm-marked-candidates)))
|
||
(vc-call-backend 'Git 'register marked)))
|
||
"Delete file(s)"
|
||
'helm-ff-delete-files
|
||
(lambda ()
|
||
(and (string-match "\\`[?]\\{2\\}.*\\.patch\\|diff" disp)
|
||
"Apply patch"))
|
||
'helm-ls-git-apply-patch
|
||
(lambda ()
|
||
(and (string-match "\\`[?]\\{2\\}.*\\.patch" disp)
|
||
"Git AM patches"))
|
||
'helm-ls-git-am-files
|
||
"Copy bnames to .gitignore"
|
||
(lambda (candidate)
|
||
(let ((default-directory
|
||
(file-name-directory candidate))
|
||
(marked (helm-marked-candidates)))
|
||
(with-current-buffer (find-file-noselect
|
||
(expand-file-name
|
||
".gitignore"
|
||
(helm-ls-git-root-dir)))
|
||
(goto-char (point-max))
|
||
(cl-loop with last-bname
|
||
for f in marked
|
||
for bname = (helm-basename f)
|
||
unless (string= bname last-bname)
|
||
do (insert (concat bname "\n"))
|
||
do (setq last-bname bname))
|
||
(save-buffer)))))))
|
||
((string-match "^AM? " disp)
|
||
(append actions '(("Commit staged file(s)"
|
||
. helm-ls-git-commit)
|
||
("Extend commit"
|
||
. helm-ls-git-extend-commit)
|
||
("Amend commit"
|
||
. helm-ls-git-amend-commit)
|
||
("Unstage file(s)"
|
||
. helm-ls-git-unstage-files))))
|
||
;; Modified but not staged
|
||
((string-match "^ M+ *" disp)
|
||
(append actions (helm-append-at-nth
|
||
mofified-actions
|
||
'(("Stage file(s) (C-c s)"
|
||
. helm-ls-git-stage-files)
|
||
("Stage marked file(s) and commit (C-c c)"
|
||
. helm-ls-git-stage-marked-and-commit)
|
||
("Stage marked file(s) and extend commit (C-c e)"
|
||
. helm-ls-git-stage-marked-and-extend-commit)
|
||
("Stage marked file(s) and amend commit (C-c a)"
|
||
. helm-ls-git-stage-marked-and-amend-commit)
|
||
("Stash (C-c z)" . helm-ls-git-stash)
|
||
("Stash snapshot (C-c Z)" . helm-ls-git-stash-snapshot))
|
||
1)))
|
||
;; Modified and staged
|
||
((string-match "^M+ +" disp)
|
||
(append actions (helm-append-at-nth
|
||
mofified-actions
|
||
'(("Commit staged file(s)"
|
||
. helm-ls-git-commit)
|
||
("Extend commit"
|
||
. helm-ls-git-extend-commit)
|
||
("Amend commit"
|
||
. helm-ls-git-amend-commit)
|
||
("Unstage file(s)"
|
||
. helm-ls-git-unstage-files)
|
||
("Git rebase continue" .
|
||
helm-ls-git-rebase-continue)
|
||
("Git cherry-pick continue" .
|
||
helm-ls-git-cherry-pick-continue)
|
||
("Git AM continue" .
|
||
helm-ls-git-am-continue)
|
||
("Git merge continue" .
|
||
helm-ls-git-merge-continue)
|
||
("Git revert continue" .
|
||
helm-ls-git-log-revert-continue))
|
||
1)))
|
||
;; Deleted
|
||
((string-match "^ D " disp)
|
||
(append actions (list '("Git delete" . (lambda (_candidate)
|
||
(let ((mkd (helm-marked-candidates)))
|
||
(cl-loop for c in mkd
|
||
do (helm-ls-git-rm c)))))
|
||
'("Stage file(s)"
|
||
. helm-ls-git-stage-files))))
|
||
;; Deleted and staged
|
||
((string-match "^A?D +" disp)
|
||
(append actions (list '("Commit staged file(s)"
|
||
. helm-ls-git-commit)
|
||
'("Unstage file(s)"
|
||
. helm-ls-git-unstage-files)
|
||
'("Update index"
|
||
. (lambda (_candidate)
|
||
(let ((default-directory (helm-default-directory)))
|
||
(process-file "git" nil nil nil
|
||
"add" "-u"))))
|
||
'("Stage marked file(s) and commit"
|
||
. helm-ls-git-stage-marked-and-commit))))
|
||
;; Conflict
|
||
((string-match "^U+ +" disp)
|
||
(append actions (list '("Git cherry-pick abort" . helm-ls-git-cherry-pick-abort)
|
||
'("Git rebase abort" . helm-ls-git-rebase-abort)
|
||
'("Git AM abort" . helm-ls-git-am-abort)
|
||
'("Git merge abort" . helm-ls-git-merge-abort)
|
||
'("Git revert abort" . helm-ls-git-log-revert-abort))))
|
||
(t actions))))
|
||
|
||
(defun helm-ls-git-am-files (_candidate)
|
||
(let ((files (helm-marked-candidates)))
|
||
(cl-assert (cl-loop for f in files
|
||
for ext = (file-name-extension f)
|
||
always (and ext (string= ext "patch"))))
|
||
(with-current-buffer-window "*git am*" '(display-buffer-below-selected
|
||
(window-height . fit-window-to-buffer)
|
||
(preserve-size . (nil . t)))
|
||
nil
|
||
(apply #'process-file "git" nil t nil "am" files))))
|
||
|
||
(defun helm-ls-git-am-abort (_candidate)
|
||
(with-helm-default-directory (helm-default-directory)
|
||
(process-file "git" nil nil nil "am" "--abort")))
|
||
|
||
(defun helm-ls-git-rm (_candidate)
|
||
(with-helm-default-directory (helm-default-directory)
|
||
(let ((files (helm-marked-candidates)))
|
||
(apply #'process-file "git" nil nil nil "rm" files))))
|
||
|
||
(defun helm-ls-git-switch-to-shell (_candidate)
|
||
(let ((helm-ff-default-directory
|
||
(helm-ls-git-root-dir)))
|
||
(helm-ff-switch-to-shell nil)))
|
||
|
||
(defun helm-ls-git-run-switch-to-shell ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-exit-and-execute-action 'helm-ls-git-switch-to-shell)))
|
||
(put 'helm-ls-git-run-switch-to-shell 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-status-revert-files (_candidate)
|
||
(let ((marked (helm-marked-candidates)))
|
||
(cl-loop for f in marked do
|
||
(progn
|
||
(vc-git-revert f)
|
||
(helm-aif (get-file-buffer f)
|
||
(with-current-buffer it
|
||
(revert-buffer t t)))))
|
||
(when helm-in-persistent-action (helm-force-update))))
|
||
|
||
(defun helm-ls-git-run-status-revert-files ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-set-attr 'revert 'helm-ls-git-status-revert-files)
|
||
(helm-execute-persistent-action 'revert)))
|
||
(put 'helm-ls-git-run-status-revert-files 'no-helm-mx t)
|
||
|
||
|
||
;;; Stage and commit
|
||
;;
|
||
(defun helm-ls-git-stage-files (_candidate)
|
||
"Stage marked files."
|
||
(let* ((files (helm-marked-candidates))
|
||
(default-directory (helm-default-directory)))
|
||
(apply #'process-file "git" nil nil nil "stage" files)))
|
||
|
||
(defun helm-ls-git-run-stage-files (arg)
|
||
(interactive "P")
|
||
(with-helm-alive-p
|
||
(helm-exit-and-execute-action (if arg
|
||
'helm-ls-git-unstage-files
|
||
'helm-ls-git-stage-files))))
|
||
(put 'helm-ls-git-run-stage-files 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-unstage-files (_candidate)
|
||
"Unstage marked files."
|
||
(let* ((files (helm-marked-candidates))
|
||
(default-directory (file-name-directory (car files))))
|
||
(apply #'process-file "git" nil nil nil "reset" "HEAD" "--" files)))
|
||
|
||
(defun helm-ls-git-stage-marked-and-commit (_candidate)
|
||
"Stage marked files and commit."
|
||
(helm-ls-git-stage-files nil)
|
||
(let ((proc (helm-ls-git-with-editor "commit" "-v")))
|
||
(set-process-sentinel proc 'helm-ls-git-commit-sentinel)))
|
||
|
||
(defun helm-ls-git-commit-sentinel (process event)
|
||
(let ((default-directory (with-current-buffer (process-buffer process)
|
||
default-directory)))
|
||
(when (string= event "finished\n")
|
||
(let ((commit (helm-ls-git-oneline-log (helm-ls-git--branch))))
|
||
(when (string-match "\\`\\([^ ]+\\)+ +\\(.*\\)" commit)
|
||
(add-face-text-property 0 (match-end 1)
|
||
'font-lock-type-face nil commit)
|
||
(add-face-text-property (1+ (match-end 1))
|
||
(match-end 2)
|
||
'font-lock-function-name-face nil commit))
|
||
(message "Commit done, now at `%s'" commit)))))
|
||
|
||
(defun helm-ls-git-run-stage-marked-and-commit ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(condition-case _err
|
||
;; Fail when helm-ls-git-stage-marked-and-commit is not in
|
||
;; action list because file is already staged.
|
||
(helm-exit-and-execute-action 'helm-ls-git-stage-marked-and-commit)
|
||
(error (helm-exit-and-execute-action 'helm-ls-git-commit) nil))))
|
||
(put 'helm-ls-git-run-stage-marked-and-commit 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-stage-marked-and-extend-commit (candidate)
|
||
"Stage marked files and extend these changes to last commit"
|
||
(helm-ls-git-stage-files nil)
|
||
(helm-ls-git-extend-commit candidate))
|
||
|
||
(defun helm-ls-git-run-stage-marked-and-extend-commit ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-exit-and-execute-action 'helm-ls-git-stage-marked-and-extend-commit)))
|
||
(put 'helm-ls-git-run-stage-marked-and-extend-commit 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-stage-marked-and-amend-commit (candidate)
|
||
"Stage marked files and amend last commit."
|
||
(helm-ls-git-stage-files nil)
|
||
(helm-ls-git-amend-commit candidate))
|
||
|
||
(defun helm-ls-git-run-stage-marked-and-amend-commit ()
|
||
(interactive)
|
||
(with-helm-alive-p
|
||
(helm-exit-and-execute-action 'helm-ls-git-stage-marked-and-amend-commit)))
|
||
(put 'helm-ls-git-run-stage-marked-and-amend-commit 'no-helm-mx t)
|
||
|
||
(defun helm-ls-git-extend-commit (candidate)
|
||
(let ((default-directory (file-name-directory candidate)))
|
||
(process-file "git" nil nil nil "commit" "--amend" "--no-edit")))
|
||
|
||
(defun helm-ls-git-amend-commit (_candidate)
|
||
"Amend last commit."
|
||
(let ((proc (helm-ls-git-with-editor "commit" "-v" "--amend")))
|
||
(set-process-sentinel proc 'helm-ls-git-commit-sentinel)))
|
||
|
||
(defun helm-ls-git-commit (_candidate)
|
||
"Commit already staged files."
|
||
(let ((proc (helm-ls-git-with-editor "commit" "-v")))
|
||
(set-process-sentinel proc 'helm-ls-git-commit-sentinel)))
|
||
|
||
|
||
;;; Emacsclient as git editor
|
||
;;
|
||
;;;###autoload
|
||
(add-to-list 'auto-mode-alist '("/COMMIT_EDITMSG$" . helm-ls-git-commit-mode))
|
||
|
||
(defvar helm-ls-git-author-name-history nil)
|
||
(defvar helm-ls-git-author-email-history nil)
|
||
|
||
(defun helm-ls-git-with-editor (&rest args)
|
||
"Binds GIT_EDITOR env var to emacsclient and run git with ARGS.
|
||
Bound `default-directory' to the root dir of project determining value
|
||
from the helm-buffer, so don't use this function outside of helm
|
||
context i.e. use it in helm actions."
|
||
(require 'server)
|
||
(let ((default-directory (expand-file-name
|
||
(helm-ls-git-root-dir
|
||
(helm-default-directory))))
|
||
(process-environment process-environment)
|
||
(bname (format "*helm-ls-git %s*" (car args)))
|
||
(alt-auth (and helm-current-prefix-arg
|
||
(string= (car args) "commit")
|
||
(list (read-string "Author name: "
|
||
nil 'helm-ls-git-author-name-history)
|
||
(read-string "Author email: "
|
||
nil 'helm-ls-git-author-email-history)))))
|
||
;; It seems git once it knows GIT_EDITOR reuse the same value
|
||
;; along its whole process e.g. when squashing in a rebase
|
||
;; process, so even if the env setting goes away after initial
|
||
;; process, git should reuse same GIT_EDITOR in subsequent
|
||
;; commits.
|
||
(when (get-buffer bname) (kill-buffer bname))
|
||
(push "GIT_EDITOR=emacsclient $@" process-environment)
|
||
(when (and alt-auth (not (member "" alt-auth)))
|
||
(push (format "GIT_AUTHOR_NAME=%s" (car alt-auth)) process-environment)
|
||
(push (format "GIT_AUTHOR_EMAIL=%s" (cadr alt-auth)) process-environment))
|
||
(unless (server-running-p)
|
||
(server-start))
|
||
(apply #'start-file-process "git" bname "git" args)))
|
||
|
||
(defun helm-ls-git-server-edit ()
|
||
(interactive)
|
||
(cl-assert server-clients nil "No server editing buffers exist")
|
||
;; Prevent server asking to save file when done.
|
||
(helm-aif buffer-file-name
|
||
(save-buffer it))
|
||
(server-edit))
|
||
|
||
;; Same as `server-edit-abort' from emacs-28 but kill edit buffer as well.
|
||
(defun helm-ls-git-server-edit-abort ()
|
||
"Abort editing the current client buffer."
|
||
(interactive)
|
||
(if server-clients
|
||
(progn
|
||
(mapc (lambda (proc)
|
||
(server-send-string
|
||
proc (concat "-error "
|
||
(server-quote-arg "Aborted by the user"))))
|
||
server-clients)
|
||
(kill-buffer))
|
||
(message "This buffer has no clients")))
|
||
|
||
(defvar helm-ls-git-commit-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map (kbd "C-c C-c") 'helm-ls-git-server-edit)
|
||
(define-key map (kbd "C-c C-k") 'helm-ls-git-server-edit-abort)
|
||
map))
|
||
|
||
;;;###autoload
|
||
(define-derived-mode helm-ls-git-commit-mode diff-mode "helm-ls-git-commit"
|
||
"Mode to edit COMMIT_EDITMSG files.
|
||
|
||
Commands:
|
||
\\{helm-ls-git-commit-mode-map}
|
||
"
|
||
(helm-ls-git-with-editor-setup))
|
||
|
||
(defun helm-ls-git-with-editor-setup ()
|
||
(setq fill-column 70)
|
||
;; For some reasons, using (setq buffer-read-only nil) in emacs-29
|
||
;; doesn't work anymore.
|
||
(read-only-mode -1)
|
||
(set (make-local-variable 'comment-start) "#")
|
||
(set (make-local-variable 'comment-end) "")
|
||
(auto-fill-mode 1)
|
||
(run-at-time
|
||
0.1 nil
|
||
(lambda ()
|
||
(message
|
||
"When done with a buffer, type `C-c C-c', to abort type `C-c C-k'"))))
|
||
|
||
;;; Git rebase
|
||
;;
|
||
;;;###autoload
|
||
(add-to-list 'auto-mode-alist '("/git-rebase-todo$" . helm-ls-git-rebase-todo-mode))
|
||
|
||
(defconst helm-ls-git-rebase-actions
|
||
'(("p" . "pick")
|
||
("r" . "reword")
|
||
("e" . "edit")
|
||
("s" . "squash")
|
||
("f" . "fixup")
|
||
("x" . "exec")
|
||
("d" . "drop")))
|
||
|
||
(defvar helm-ls-git-rebase-todo-font-lock-keywords
|
||
'(("^\\([a-z]+\\) \\([0-9a-f]+\\) \\(.*\\)$"
|
||
(1 'font-lock-keyword-face)
|
||
(2 'font-lock-function-name-face))
|
||
("^#.*$" . 'font-lock-comment-face))
|
||
"Keywords in `helm-ls-git-rebase-todo' mode.")
|
||
|
||
(defvar helm-ls-git-rebase-todo-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map (kbd "M-n") 'helm-ls-git-rebase-todo-move-down)
|
||
(define-key map (kbd "M-p") 'helm-ls-git-rebase-todo-move-up)
|
||
(define-key map (kbd "C-c C-c") 'helm-ls-git-server-edit)
|
||
(define-key map (kbd "C-c C-k") 'helm-ls-git-server-edit-abort)
|
||
map)
|
||
"Keymap used in `helm-ls-git-rebase-todo-mode' buffers.")
|
||
|
||
(defun helm-ls-git-rebase-todo-move-down ()
|
||
"Move commit line one line down."
|
||
(interactive)
|
||
(beginning-of-line)
|
||
(let* ((next (+ 1 (line-end-position)))
|
||
(line (buffer-substring (point) next)))
|
||
(delete-region (point) next)
|
||
(forward-line 1)
|
||
(insert line)
|
||
(forward-line -1)))
|
||
|
||
(defun helm-ls-git-rebase-todo-move-up ()
|
||
"Move commit line on line up."
|
||
(interactive)
|
||
(beginning-of-line)
|
||
(let* ((next (+ 1 (line-end-position)))
|
||
(line (buffer-substring (point) next)))
|
||
(delete-region (point) next)
|
||
(forward-line -1)
|
||
(insert line)
|
||
(forward-line -1)))
|
||
|
||
(defun helm-ls-git-rebase-action (action)
|
||
"Replace the current rebase command at bol by ACTION.
|
||
ACTION is the cdr entry of one of `helm-ls-git-rebase-actions'."
|
||
(let* ((assocs helm-ls-git-rebase-actions)
|
||
(regexp (cl-loop with len = (length assocs)
|
||
for (_k . v) in assocs
|
||
for count from 1 to len
|
||
concat (concat v (if (= count len) "" "\\|")) into str
|
||
finally return (concat "^\\(" str "\\) +")))
|
||
(inhibit-read-only t))
|
||
(goto-char (point-at-bol))
|
||
(save-excursion
|
||
(when (re-search-forward regexp (point-at-eol) t)
|
||
(delete-region (point-at-bol) (match-end 1))))
|
||
(insert (cdr (rassoc action assocs)))
|
||
(forward-line 1)))
|
||
|
||
(cl-defun helm-ls-git-rebase-build-commands ()
|
||
"build a function for each `helm-ls-git-rebase-actions' entry.
|
||
Bind it to the car of each entry of `helm-ls-git-rebase-actions'."
|
||
(cl-loop for (k . v) in helm-ls-git-rebase-actions
|
||
for sym = (intern (concat "helm-ls-git-rebase-" v))
|
||
for doc = (format "Replace current rebase command at bol by `%s'." v)
|
||
do (progn
|
||
(defalias sym `(lambda () (interactive)
|
||
(helm-ls-git-rebase-action ,v))
|
||
doc)
|
||
(define-key helm-ls-git-rebase-todo-mode-map (kbd k) sym))))
|
||
|
||
;;;###autoload
|
||
(define-derived-mode helm-ls-git-rebase-todo-mode fundamental-mode "helm-ls-git-rebase-todo"
|
||
"Major Mode to edit git-rebase-todo files when using git rebase -i.
|
||
|
||
Commands:
|
||
\\{helm-ls-git-rebase-todo-mode-map}
|
||
"
|
||
(set (make-local-variable 'font-lock-defaults)
|
||
'(helm-ls-git-rebase-todo-font-lock-keywords t))
|
||
(helm-ls-git-rebase-build-commands)
|
||
(set (make-local-variable 'comment-start) "#")
|
||
(set (make-local-variable 'comment-end) "")
|
||
(run-at-time
|
||
0.1 nil
|
||
(lambda ()
|
||
(message
|
||
"When done with a buffer, type `C-c C-c', to abort type `C-c C-k'"))))
|
||
|
||
|
||
;;; Build sources
|
||
;;
|
||
;; Overhide the actions of helm-type-buffer.
|
||
(cl-defmethod helm--setup-source :after ((source helm-source-buffers))
|
||
(let ((name (slot-value source 'name)))
|
||
(when (string= name "Buffers in git project")
|
||
(setf (slot-value source 'action)
|
||
(helm-append-at-nth
|
||
helm-type-buffer-actions
|
||
(helm-make-actions (lambda ()
|
||
(and helm-ls-git-status-command "Git status"))
|
||
(lambda (_candidate)
|
||
(funcall helm-ls-git-status-command
|
||
(helm-default-directory))))
|
||
1)))))
|
||
|
||
(defun helm-ls-git-build-git-status-source ()
|
||
"Build `helm-source-ls-git-status'.
|
||
|
||
Do nothing when `helm-source-ls-git-status' is not member of
|
||
`helm-ls-git-default-sources'."
|
||
(and (memq 'helm-source-ls-git-status helm-ls-git-default-sources)
|
||
(helm-make-source "Git status" 'helm-ls-git-status-source
|
||
:fuzzy-match helm-ls-git-fuzzy-match
|
||
:group 'helm-ls-git)))
|
||
|
||
(defun helm-ls-git-build-ls-git-source ()
|
||
"Build `helm-source-ls-git'.
|
||
|
||
Do nothing when `helm-source-ls-git' is not member of
|
||
`helm-ls-git-default-sources'."
|
||
(and (memq 'helm-source-ls-git helm-ls-git-default-sources)
|
||
(helm-make-source "Git files" 'helm-ls-git-source
|
||
:fuzzy-match helm-ls-git-fuzzy-match
|
||
:action (helm-ls-git-actions-list helm-type-file-actions)
|
||
:group 'helm-ls-git)))
|
||
|
||
(defun helm-ls-git-build-buffers-source ()
|
||
"Build `helm-source-ls-git-buffers'.
|
||
|
||
Do nothing when `helm-source-ls-git-buffers' is not member of
|
||
`helm-ls-git-default-sources'."
|
||
(and (memq 'helm-source-ls-git-buffers helm-ls-git-default-sources)
|
||
(helm-make-source "Buffers in git project" 'helm-source-buffers
|
||
:header-name #'helm-ls-git-header-name
|
||
:buffer-list (lambda () (helm-browse-project-get-buffers
|
||
(helm-ls-git-root-dir)))
|
||
:keymap 'helm-ls-git-buffer-map)))
|
||
|
||
|
||
;;;###autoload
|
||
(defun helm-ls-git (&optional arg)
|
||
(interactive "p")
|
||
(let ((helm-ff-default-directory
|
||
(or helm-ff-default-directory
|
||
default-directory)))
|
||
(when (and arg (helm-ls-git-not-inside-git-repo))
|
||
(error "Not inside a Git repository"))
|
||
(unless (cl-loop for s in helm-ls-git-default-sources
|
||
always (symbol-value s))
|
||
(setq helm-source-ls-git-status
|
||
(helm-ls-git-build-git-status-source)
|
||
helm-source-ls-git
|
||
(helm-ls-git-build-ls-git-source)
|
||
helm-source-ls-git-buffers
|
||
(helm-ls-git-build-buffers-source)))
|
||
(helm-set-local-variable 'helm-ls-git--current-branch (helm-ls-git--branch))
|
||
(helm :sources helm-ls-git-default-sources
|
||
:ff-transformer-show-only-basename nil
|
||
:truncate-lines helm-buffers-truncate-lines
|
||
:buffer "*helm lsgit*")))
|
||
|
||
(defalias 'helm-ls-git-ls 'helm-ls-git)
|
||
(make-obsolete 'helm-ls-git-ls 'helm-ls-git "1.9.2")
|
||
(put 'helm-ls-git-ls 'no-helm-mx t)
|
||
|
||
|
||
(provide 'helm-ls-git)
|
||
|
||
;;; helm-ls-git.el ends here
|