Eshell completion for git, bzr, and hg

Posted: May 31, 2013 in Emacs, Emacs Lisp
Tags: ,

After reading this post on the MasteringEmacs blog I gave eshell a try and I like it. On this other post he shows how to implement completion with pcomplete that is automatically used by eshell.

Without further ado, here are completions for git, bzr, and hg. The git completion is basically his completion with some improvements. For example, it completes all git commands by parsing the output of git help --all.

;;**** Git Completion

(defun pcmpl-git-commands ()
  "Return the most common git commands by parsing the git output."
  (with-temp-buffer
    (call-process-shell-command "git" nil (current-buffer) nil "help" "--all")
    (goto-char 0)
    (search-forward "available git commands in")
    (let (commands)
      (while (re-search-forward
	      "^[[:blank:]]+\\([[:word:]-.]+\\)[[:blank:]]*\\([[:word:]-.]+\\)?"
	      nil t)
	(push (match-string 1) commands)
	(when (match-string 2)
	  (push (match-string 2) commands)))
      (sort commands #'string<))))

(defconst pcmpl-git-commands (pcmpl-git-commands)
  "List of `git' commands.")

(defvar pcmpl-git-ref-list-cmd "git for-each-ref refs/ --format='%(refname)'"
  "The `git' command to run to get a list of refs.")

(defun pcmpl-git-get-refs (type)
  "Return a list of `git' refs filtered by TYPE."
  (with-temp-buffer
    (insert (shell-command-to-string pcmpl-git-ref-list-cmd))
    (goto-char (point-min))
    (let (refs)
      (while (re-search-forward (concat "^refs/" type "/\\(.+\\)$") nil t)
	(push (match-string 1) refs))
      (nreverse refs))))

(defun pcmpl-git-remotes ()
  "Return a list of remote repositories."
  (split-string (shell-command-to-string "git remote")))

(defun pcomplete/git ()
  "Completion for `git'."
  ;; Completion for the command argument.
  (pcomplete-here* pcmpl-git-commands)
  (cond
   ((pcomplete-match "help" 1)
    (pcomplete-here* pcmpl-git-commands))
   ((pcomplete-match (regexp-opt '("pull" "push")) 1)
    (pcomplete-here (pcmpl-git-remotes)))
   ;; provide branch completion for the command `checkout'.
   ((pcomplete-match "checkout" 1)
    (pcomplete-here* (append (pcmpl-git-get-refs "heads")
			     (pcmpl-git-get-refs "tags"))))
   (t
    (while (pcomplete-here (pcomplete-entries))))))

;;**** Bzr Completion

(defun pcmpl-bzr-commands ()
  "Return the most common bzr commands by parsing the bzr output."
  (with-temp-buffer
    (call-process-shell-command "bzr" nil (current-buffer) nil "help" "commands")
    (goto-char 0)
    (let (commands)
      (while (re-search-forward "^\\([[:word:]-]+\\)[[:blank:]]+" nil t)
	(push (match-string 1) commands))
      (sort commands #'string<))))

(defconst pcmpl-bzr-commands (pcmpl-bzr-commands)
  "List of `bzr' commands.")

(defun pcomplete/bzr ()
  "Completion for `bzr'."
  ;; Completion for the command argument.
  (pcomplete-here* pcmpl-bzr-commands)
  (cond
   ((pcomplete-match "help" 1)
    (pcomplete-here* pcmpl-bzr-commands))
   (t
    (while (pcomplete-here (pcomplete-entries))))))

;;**** Mercurial (hg) Completion

(defun pcmpl-hg-commands ()
  "Return the most common hg commands by parsing the hg output."
  (with-temp-buffer
    (call-process-shell-command "hg" nil (current-buffer) nil "-v" "help")
    (goto-char 0)
    (search-forward "list of commands:")
    (let (commands
	  (bound (save-excursion
		   (re-search-forward "^[[:alpha:]]")
		   (forward-line 0)
		   (point))))
      (while (re-search-forward
	      "^[[:blank:]]\\([[:word:]]+\\(?:, [[:word:]]+\\)*\\)" bound t)
	(let ((match (match-string 1)))
	  (if (not (string-match "," match))
	      (push (match-string 1) commands)
	    (dolist (c (split-string match ", ?"))
	      (push c commands)))))
      (sort commands #'string<))))

(defconst pcmpl-hg-commands (pcmpl-hg-commands)
  "List of `hg' commands.")

(defun pcomplete/hg ()
  "Completion for `hg'."
  ;; Completion for the command argument.
  (pcomplete-here* pcmpl-hg-commands)
  (cond
   ((pcomplete-match "help" 1)
    (pcomplete-here* pcmpl-hg-commands))
   (t
    (while (pcomplete-here (pcomplete-entries))))))
About these ads
Comments
  1. Damon Haley says:

    Tassilo, this post was extremely helpful.

    Because of your examples, I was able to add pcomplete functionality to drupal-mode at:

    https://github.com/arnested/drupal-mode/commit/ae4dd40f05514b1a8a4206339704db69fd6097f2

    However, commands that start with @, won’t expand as drush aliases but instead as tramp locations. But it works fine in shell (shell-mode). Would you know how to override this (@ symbol expansion) behavior in eshell?

    • Tassilo Horn says:

      Hi Damon. I’m using eshell only for four days, so I’m not too experienced with it’s code. But a quick look showed that the @-hostname completion is done because eshell-complete-host-reference is in the buffer-local value of pcomplete-try-first-hook. So you could probably remove it from there temporarily when completing your drush aliases. Something like this might do the trick:

      (let ((pcomplete-try-first-hook (remove 'eshell-complete-host-reference
      					pcomplete-try-first-hook)))
        ;; your completion here
        )
      • Hi Tassillo, your suggestion worked great for completing aliases beginning with ‘@’ in eshell.

        I had another issue I wanted to run by you.

        For my pcomplete drush function, I need to set the default-directory to a specific directory to get the desired output from my call-process function.

        Do you know of a way to do something like this (attempted code below):

        https://gist.github.com/dhaley/5860753

        Any pointers would be appreciated.

        • Tassilo Horn says:

          Hi Damon,

          doesn’t it work exactly the way you’ve done it in the gist, that is, call call-process when default-directory is let-bound to what you need? At least some experimentation with

          (let ((default-directory "~/Repos/"))
            (call-process "pwd" nil "*pwd*"))

          suggests that it does the right thing.

  2. Dmitry Gutov says:

    Any reason not to contribute this to Emacs, or at least release as a package?

    • tsdh80 says:

      No, no particular reason other than it’s incomplete and not important enough for me to maintain. And also I’m not sure if calling programs and parsing their output is the right way to go. At least it’s very tied to the output of the programs in their current version (or the versions I have installed) and is likely to break over time.

  3. This was extremely useful to me, especially because I like to keep many branches around in my repos. Thank you!

    The only thing I miss is for it to pick up on the git alias list from ~/.gitconfig, roughly matching:

    git config –get-regexp alias | grep -o “\.[a-z]*\s” | grep -o “[a-z]*”

    That can probably be simplified.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s