branch: externals/vc-hgcmd
commit 90d6c16e963e3179076471782da79f9118b97fcf
Author: muffinmad <andreyk....@gmail.com>
Commit: muffinmad <andreyk....@gmail.com>

    vc-hgcmd
---
 README.md   |  64 ++++-
 vc-hgcmd.el | 820 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 883 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index d381447..97b161d 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,64 @@
+[![License GPL 
3](https://img.shields.io/badge/license-GPL_3-green.svg)](http://www.gnu.org/copyleft/gpl.html)
+
 # emacs-vc-hgcmd
-Emacs VC backend to work with hg repositories through hg command server
+
+Emacs VC backend to work with mercurial repositories through [hg command 
server](https://www.mercurial-scm.org/wiki/CommandServer)
+
+## Advantage over `vc-hg`
+
+The main advantage compared to `vc-hg` is speed.
+
+Because communicating with single `hg` process over pipe is much faster than 
starting separate `hg` process for each command.
+
+### Other improvements and differences
+
+#### File renames and short log
+
+`vc-hgcmd` can't show file renames in `vc-dir` and doesn't have short log 
version yet
+
+#### Unresolved conflict status for a file
+
+Files with unresolved merge conflicts have appropriate status in `vc-dir`.
+Also you can use `vc-find-conflicted-file` to find next file with unresolved 
merge conflict.
+
+#### hg summary as `vc-dir` extra headers
+
+`hg cummary` command gives useful information about commit, update and phase 
states.
+
+#### Current branch is displayed on mode line.
+
+It's not customizable yet.
+
+#### Amend and close branch commits
+
+While editing commit message you can togle `--amend` and `--close-branch` 
flags.
+
+#### Merge branch
+
+`vc-hgcmd` will ask for branch name to merge.
+
+#### Default pull argements
+
+You can customize default `hg pulll` command arguments.
+By default it's `--update`. You can change it for particular pull by invoking 
`vc-pull` with prefix argument.
+
+#### Branches and tags as revision completion table
+
+Instead of list of all revisions of file `vc-hgcmd` provides list of named 
branches and tags.
+It's very useful on `vc-retrieve-tag`.
+You can specify `-C` to run `hg update` with `-C` flag and discard all 
uncommited changes.
+
+#### Filenames in `vc-annotate` buffer are hidden
+
+They are needed to annotate changes across renames but mostly useless in 
annotate buffer.
+`vc-hgcmd` removes it from annotate buffer but keep it in text properties.
+
+#### Create tag
+
+`vc-hgcmd` creates tag on `vc-create-tag`.
+If `vc-create-tag` is invoked with prefix argument then named branch will be 
created.
+
+#### Predefined commit message
+
+While commiting merge changes commit message will be set to `merged <branch>` 
if
+different branch was merged or to `merged <node>`
diff --git a/vc-hgcmd.el b/vc-hgcmd.el
new file mode 100644
index 0000000..d40e4ae
--- /dev/null
+++ b/vc-hgcmd.el
@@ -0,0 +1,820 @@
+;;; vc-hgcmd.el --- VC backend to work with hg repositories through hg command 
server -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2018 Andrii Kolomoiets
+
+;; Author: Andrii Kolomoiets <andreyk....@gmail.com>
+;; Keywords: vc
+;; URL: https://github.com/muffinmad/emacs-vc-hgcmd
+;; Package-Version: 1.0
+;; Package-Requires: ((emacs "25.1"))
+
+;; This file is NOT part of GNU Emacs.
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; VC backend to work with hg repositories through hg command server.
+;; https://www.mercurial-scm.org/wiki/CommandServer
+;;
+;; The main advantage compared to vc-hg is speed.
+;; Because communicating with hg over pipe is much faster than starting hg for 
each command.
+;;
+;; Also there are some other improvements and differences:
+;;
+;; - vc-hgcmd can't show file renames in `vc-dir' and doesn't have short log 
version yet
+;;
+;; - Unresolved conflict status for a file
+;; Files with unresolved merge conflicts have appropriate status in `vc-dir'.
+;; Also you can use `vc-find-conflicted-file' to find next file with 
unresolved merge conflict.
+;;
+;; - hg summary as `vc-dir' extra headers
+;; hg cummary command gives useful information about commit, update and phase 
states.
+;;
+;; - Current branch is displayed on mode line.
+;; It's not customizable yet.
+;;
+;; - Amend and close branch commits
+;; While editing commit message you can togle --amend and --close-branch flags.
+;;
+;; - Merge branch
+;; vc-hgcmd will ask for branch name to merge.
+;;
+;; - Default pull argements
+;; You can customize default hg pulll command arguments.
+;; By default it's --update. You can change it for particular pull by invoking 
`vc-pull' with prefix argument.
+;;
+;; - Branches and tags as revision completion table
+;; Instead of list of all revisions of file vc-hgcmd provides list of named 
branches and tags.
+;; It's very useful on `vc-retrieve-tag'.
+;; You can specify -C to run hg update with -C flag and discard all uncommited 
changes.
+;;
+;; - Filenames in vc-annotate buffer are hidden
+;; They are needed to annotate changes across renames but mostly useless in 
annotate buffer.
+;; vc-hgcmd removes it from annotate buffer but keep it in text properties.
+;;
+;; - Create tag
+;; vc-hgcmd creates tag on `vc-create-tag'
+;; If `vc-create-tag' is invoked with prefix argument then named branch will 
be created.
+;;
+;; - Predefined commit message
+;; While commiting merge changes commit message will be set to 'merged 
<branch>' if
+;; different branch was merged or to 'merged <node>'
+
+;;; Code:
+
+(require 'bindat)
+(require 'cl-lib)
+(require 'seq)
+(require 'subr-x)
+(require 'vc)
+(require 'vc-dir)
+
+
+;;;; Customization
+
+
+(defgroup vc-hgcmd nil
+  "Settings for VC mercurial commandserver backend."
+  :group 'vc
+  :prefix "vc-hgcmd-")
+
+(defcustom vc-hgcmd-hg-executable "hg"
+  "Hg executable."
+  :type '(string))
+
+(defcustom vc-hgcmd-pull-args "--update"
+  "Arguments for pull command.
+This arguments will be used for each pull command.
+You can edit this arguments for specific pull command by invoke `vc-pull' with 
prefix argument."
+  :type '(string))
+
+(defcustom vc-hgcmd-push-alternate-args "--new-branch"
+  "Initial value for hg push arguments when asked."
+  :type '(string))
+
+
+;;;; Consts. Customizing this can lead to unexpected behaviour
+
+
+(defconst vc-hgcmd-cmdserver-args
+  '(
+    ;; TODO: cmdserver clients must handle I and L channels
+    ;; "--config" "ui.interactive=True"
+    "--config" "ui.editor=emacsclient"
+    "--config" "pager.pager="
+    "serve"
+    "--cmdserver" "pipe")
+  "Args to start hg command server.")
+
+(defconst vc-hgcmd-cmdserver-process-environment
+  '("TERM=dumb"
+    "HGPLAIN="
+    "LANGUAGE=C"
+    "HGENCODING=UTF-8"
+    "ALTERNATE_EDITOR=emacs")
+  "Environment variables for hg command server process.")
+
+
+;;;; Modes
+
+
+(define-derived-mode vc-hgcmd-process-mode fundamental-mode "Hgcmd process"
+  "Major mode for hg cmdserver process"
+  (hack-dir-local-variables-non-file-buffer)
+  (set-buffer-multibyte nil)
+  (setq
+   buffer-undo-list t
+   list-buffers-directory (abbreviate-file-name default-directory)
+   buffer-read-only t))
+
+(define-derived-mode vc-hgcmd-output-mode compilation-mode "Hgcmd output"
+  "Major mode for hg output"
+  (hack-dir-local-variables-non-file-buffer)
+  (setq
+   buffer-undo-list t
+   list-buffers-directory (abbreviate-file-name default-directory)
+   buffer-read-only t))
+
+
+;;;; cmdserver communication
+
+
+(defvar vc-hgcmd--process-buffers-by-dir (make-hash-table :test #'equal))
+
+(cl-defstruct (vc-hgcmd--command (:copier nil)) command output-buffer 
result-code wait callback callback-args)
+
+(defvar-local vc-hgcmd--current-command nil
+  "Current running hgcmd command. Future commands will wait until the current 
command will finish.")
+(put 'vc-hgcmd--current-command 'permanent-local t)
+
+(defun vc-hgcmd--project-name (dir)
+  "Get project name based on DIR."
+  (file-name-nondirectory (directory-file-name dir)))
+
+(defun vc-hgcmd--read-output ()
+  "Parse process output in current buffer."
+  (when (> (point-max) 5)
+    (let* ((data (bindat-unpack '((c byte) (d u32)) (vconcat 
(buffer-substring-no-properties 1 6))))
+           (channel (bindat-get-field data 'c))
+           (size (bindat-get-field data 'd)))
+      (when (> (point-max) (+ 5 size))
+        (let ((data (vconcat (buffer-substring-no-properties 6 (+ 6 size))))
+              (inhibit-read-only t))
+          (delete-region 1 (+ 6 size))
+          (cons channel data))))))
+
+(defun vc-hgcmd--cmdserver-process-filter (process output)
+  "Filter OUTPUT for hg cmdserver PROCESS.
+Insert output to process buffer and check if amount of data is enought to 
parse it to output buffer."
+  (let ((buffer (process-buffer process)))
+    (when (buffer-live-p buffer)
+      (let* ((current-command (with-current-buffer buffer 
vc-hgcmd--current-command))
+             (output-buffer (vc-hgcmd--command-output-buffer current-command)))
+        (when output-buffer
+          (with-current-buffer buffer
+            (goto-char (point-max))
+            (let ((inhibit-read-only t)) (insert output))
+            (while
+                (let ((data (vc-hgcmd--read-output)))
+                  (when data
+                    (let ((channel (car data))
+                          (data (cdr data)))
+                      (with-current-buffer output-buffer
+                        (let ((inhibit-read-only t))
+                          (goto-char (point-max))
+                          (cond ((or (eq channel ?e) (eq channel ?o))
+                                 (insert (decode-coding-string 
(bindat-get-field (bindat-unpack `((f str ,(length data))) data) 'f) 'utf-8)))
+                                ((eq channel ?r)
+                                 (setf (vc-hgcmd--command-result-code 
current-command) (bindat-get-field (bindat-unpack `((f u32)) data) 'f))
+                                 (with-current-buffer buffer (setq 
vc-hgcmd--current-command nil))
+                                 (let ((callback (vc-hgcmd--command-callback 
current-command))
+                                       (args (vc-hgcmd--command-callback-args 
current-command)))
+                                   (when callback
+                                     (if args (funcall callback args) (funcall 
callback)))))
+                                ;; TODO: cmdserver clients must handle I and L 
channels
+                                (t (error (format "unknown channel %c\n" 
channel)))))))
+                    t)))))))))
+
+(defun vc-hgcmd--check-buffer-process (buffer)
+  "Create hg cmdserver process in BUFFER if needed."
+  (unless (get-buffer-process buffer)
+    (let ((process-environment (append vc-hgcmd-cmdserver-process-environment 
process-environment))
+          (process-connection-type nil))
+      (with-current-buffer buffer
+        (let ((process (apply
+                        #'start-file-process
+                        (concat "vc-hgcmd process: " (vc-hgcmd--project-name 
default-directory))
+                        buffer
+                        vc-hgcmd-hg-executable
+                        vc-hgcmd-cmdserver-args)))
+          (set-process-sentinel process #'ignore)
+          (set-process-query-on-exit-flag process nil)
+          (set-process-coding-system process 'no-conversion 'no-conversion)
+          ;; read hello message
+          ;; TODO parse encoding
+          (while (not (vc-hgcmd--read-output))
+            (accept-process-output process 0.1 nil t))
+          (set-process-filter process #'vc-hgcmd--cmdserver-process-filter)
+          process)))))
+
+(defun vc-hgcmd--repo-dir ()
+  "Get repo dir."
+  (abbreviate-file-name (or (vc-hgcmd-root default-directory) 
default-directory)))
+
+(defun vc-hgcmd--create-process-buffer (dir)
+  "Create hg cmdserver process buffer for repo in DIR."
+  (let ((buffer (generate-new-buffer (concat "*hgcmd process: " 
(vc-hgcmd--project-name dir) "*"))))
+    (with-current-buffer buffer
+      (setq default-directory dir)
+      (vc-hgcmd-process-mode))
+    buffer))
+
+(defun vc-hgcmd--get-process-buffer (dir)
+  "Get hg cmdserver process buffer for repo in DIR."
+  (let ((buffer (gethash dir vc-hgcmd--process-buffers-by-dir)))
+    (when (buffer-live-p buffer) buffer)))
+
+(defun vc-hgcmd--process-buffer ()
+  "Get hg cmdserver process buffer for repo in `default-directory'."
+  (let* ((dir (vc-hgcmd--repo-dir))
+         (buffer (or (vc-hgcmd--get-process-buffer dir)
+                     (puthash dir (vc-hgcmd--create-process-buffer dir) 
vc-hgcmd--process-buffers-by-dir))))
+    (when buffer (vc-hgcmd--check-buffer-process buffer))
+    buffer))
+
+(defun vc-hgcmd--create-output-buffer (dir)
+  "Create hg output buffer for repo in DIR."
+  (let ((buffer (generate-new-buffer (concat "*hgcmd output: " 
(vc-hgcmd--project-name dir) "*"))))
+    (with-current-buffer buffer
+      (setq default-directory dir)
+      (vc-hgcmd-output-mode))
+    buffer))
+
+(defun vc-hgcmd--get-output-buffer (&optional command)
+  "Get hg output buffer for repo in `default-directory'.
+Insert 'Running command' and display buffer text if COMMAND"
+  (let* ((dir (vc-hgcmd--repo-dir))
+         (buffer (or (seq-find (lambda (buffer)
+                                 (with-current-buffer buffer
+                                   (and (eq major-mode 'vc-hgcmd-output-mode)
+                                        (equal (abbreviate-file-name 
default-directory) dir))))
+                               (buffer-list))
+                     (vc-hgcmd--create-output-buffer dir)))
+         window-start)
+    (when command
+      (with-current-buffer buffer
+        (let ((inhibit-read-only t))
+          (goto-char (point-max))
+          (unless (eq (point) (point-min)) (insert "\n"))
+          (setq window-start (point))
+          (insert (concat "Running \"" (mapconcat #'identity command " ") 
"%s\"...\n"))))
+      (let ((window (display-buffer buffer)))
+        (when window (set-window-start window window-start))))
+    buffer))
+
+(defun vc-hgcmd--prepare-command-to-send (command)
+  "Prepare COMMAND to send to hg process."
+  (let ((args (mapconcat #'identity command "\0")))
+    (concat (bindat-pack '((l u32)) `((l . ,(length args)))) args)))
+
+(defun vc-hgcmd--run-command (cmd)
+  "Run hg CMD."
+  (let* ((buffer (vc-hgcmd--process-buffer))
+         (process (get-buffer-process buffer)))
+    (with-current-buffer buffer
+      (while vc-hgcmd--current-command
+        (accept-process-output process 0.1 nil t))
+      (setq vc-hgcmd--current-command cmd)
+      (process-send-string process (concat "runcommand\n" 
(vc-hgcmd--prepare-command-to-send (vc-hgcmd--command-command cmd))))
+      (when (vc-hgcmd--command-wait cmd)
+        (while vc-hgcmd--current-command
+          (accept-process-output process 0.1 nil t))))))
+
+(defun vc-hgcmd-command (&rest command)
+  "Run hg COMMAND and return it's output."
+  (with-temp-buffer
+    (let ((cmd (make-vc-hgcmd--command :command command :output-buffer 
(current-buffer) :wait t)))
+      (vc-hgcmd--run-command cmd)
+      ;; TODO handle result codes
+      (let ((result (string-trim-right (buffer-string))))
+        (when (> (length result) 0)
+          result)))))
+
+(defun vc-hgcmd-command-output-buffer (buffer &rest command)
+  "Send output of COMMAND to BUFFER and wait COMMAND to finish."
+  (vc-setup-buffer buffer)
+  (vc-hgcmd--run-command (make-vc-hgcmd--command :command command 
:output-buffer buffer :wait t)))
+
+(defun vc-hgcmd--update-callback (buffer)
+  "Update BUFFER where was command called from after command finished."
+  (when (buffer-live-p buffer)
+    (with-current-buffer buffer
+      (cond
+       ((derived-mode-p 'vc-dir-mode)
+        (vc-dir-refresh))
+       ((derived-mode-p 'dired-mode)
+        (revert-buffer))))))
+
+(defun vc-hgcmd-command-update-callback (command)
+  "Run COMMAND and update current buffer afret command finished."
+  (vc-hgcmd--run-command
+   (make-vc-hgcmd--command
+    :command command
+    :output-buffer (vc-hgcmd--get-output-buffer command)
+    :callback #'vc-hgcmd--update-callback
+    :callback-args (current-buffer))))
+
+(defconst vc-hgcmd--translation-status '((?C . up-to-date)
+                                         (?= . up-to-date)
+                                         (?A . added)
+                                         (?I . ignored)
+                                         (?R . removed)
+                                         (?! . missing)
+                                         (?? . unregistered)
+                                         (?M . edited)
+                                         (?  . origin))
+  "Translation for status command output.")
+
+(defconst vc-hgcmd--translation-resolve '((?U . conflict)
+                                          (?R . edited))
+  "Translation for resolve command output.")
+
+(defun vc-hgcmd--branches ()
+  "Return branches list."
+  (split-string (vc-hgcmd-command "branches" "-T" "{branch}\n") "\n"))
+
+(defun vc-hgcmd--tags ()
+  "Return tags list."
+  (split-string (vc-hgcmd-command "tags" "-q") "\n"))
+
+(defconst vc-hgcmd--no-file-re
+  ".+: No such file or directory$")
+
+;;;; VC backend
+
+(defun vc-hgcmd-revision-granularity ()
+  "Per-repository revision number."
+  'repository)
+
+;;;###autoload (defun vc-hgcmd-registered (file)
+;;;###autoload   (when (vc-find-root file ".hg")
+;;;###autoload     (load "vc-hgcmd" nil t)
+;;;###autoload     (vc-hgcmd-registered file)))
+
+(defun vc-hgcmd-registered (file)
+  "Is file FILE is registered."
+  (when (vc-hgcmd-root file)
+    (let ((state (vc-hgcmd-state file)))
+      (and state (not (memq state '(ignored unregistered)))))))
+
+(defun vc-hgcmd-state (file)
+  "State for FILE."
+  (let ((out (vc-hgcmd-command "status" "-A" file)))
+    (when (and out (null (string-match-p vc-hgcmd--no-file-re out)))
+      (let ((state (cdr (assoc (aref out 0) vc-hgcmd--translation-status))))
+        (if (and (eq state 'edited) (vc-hgcmd--file-unresolved-p file))
+            'conflict
+          state)))))
+
+(defun vc-hgcmd--dir-status-callback (update-function)
+  "Call UPDATE-FUNCTION with result of status command."
+  (let ((result nil)
+        (conflicted (vc-hgcmd-conflicted-files)))
+    (goto-char (point-min))
+    (while (not (eobp))
+      (unless (looking-at vc-hgcmd--no-file-re)
+        (let* ((file (buffer-substring-no-properties (+ (point) 2) 
(line-end-position)))
+               (state (if (member file conflicted)
+                          'conflict
+                        (cdr (assoc (char-after) 
vc-hgcmd--translation-status)))))
+          (push (list file state nil) result)))
+      (forward-line))
+    (funcall update-function result)))
+
+(defun vc-hgcmd-dir-status-files (dir files update-function)
+  "Call UPDATE-FUNCTION with status for files in DIR or FILES."
+    ;; TODO track file renames with -C option
+    (let ((command (if files
+                       (nconc (list "status" "-A") files)
+                     (list "status" dir))))
+    (vc-hgcmd--run-command
+     (make-vc-hgcmd--command
+      :command command
+      :output-buffer (current-buffer)
+      :callback #'vc-hgcmd--dir-status-callback
+      :callback-args update-function))))
+
+(defun vc-hgcmd--extra-header (name value)
+  "Format NAME and VALUE as dir extra header."
+  (concat (propertize name 'face 'font-lock-type-face)
+          (propertize value 'face 'font-lock-variable-name-face)
+          "\n"))
+
+(defun vc-hgcmd--parent-info (data)
+  "Parse and propertize parent log info from DATA."
+  (when data
+    (cl-multiple-value-bind (rev branch tags desc) (split-string data "\0")
+      (apply #'concat
+             (list
+              (vc-hgcmd--extra-header "Parent     : " (concat rev " " branch " 
" tags))
+              (vc-hgcmd--extra-header "           : " desc))))))
+
+(defun vc-hgcmd--summary-info (search name)
+  "Search for summary info prefixed by SEARCH and propertize with NAME."
+  (goto-char (point-min))
+  (when (search-forward-regexp (format "^%s: \\(.*\\)" search) nil t)
+    (vc-hgcmd--extra-header name (match-string-no-properties 1))))
+
+(defun vc-hgcmd-dir-extra-headers (_dir)
+  "Return summary command for DIR output as dir extra headers."
+  (let* ((parents (split-string (vc-hgcmd-command "log" "-r" "p1()+p2()" 
"--template" "{rev}:{node|short}\\0{branch}\\0{tags}\\0{desc|firstline}\n") 
"\n"))
+         (result (apply #'concat (mapcar #'vc-hgcmd--parent-info parents))))
+    (with-temp-buffer
+      (vc-hgcmd-command-output-buffer (current-buffer) "summary")
+      (concat result
+              (vc-hgcmd--summary-info "branch" "Branch     : ")
+              (vc-hgcmd--summary-info "commit" "Commit     : ")
+              (vc-hgcmd--summary-info "update" "Update     : ")
+              (vc-hgcmd--summary-info "phases" "Phases     : ")))))
+
+;; TODO dir-printer
+;; TODO status-fileinfo-extra
+
+(defun vc-hgcmd-working-revision (file)
+  "Working revision. Return repository working revision if FILE is commited."
+  (if (and file (eq 'added (vc-state file)))
+      "0"
+    (or (vc-hgcmd-command "log" "-l" "1" "-f" "-T" "{rev}") "0")))
+
+(defun vc-hgcmd-checkout-model (_files)
+  "Files are always writable."
+  'implicit)
+
+(defun vc-hgcmd-mode-line-string (file)
+  "Return a string for `vc-mode-line' to put in the mode line for FILE."
+  (let* ((state (vc-state file))
+            (state-echo nil)
+            (face nil)
+         ;; TODO allow to customize it.
+            (branch (vc-hgcmd-command "branch")))
+    (propertize
+     (concat
+      "Hgcmd"
+      (cond
+       ((eq state 'up-to-date)
+        (setq state-echo "Up to date file")
+           (setq face 'vc-up-to-date-state)
+           "-")
+       ((eq state 'added)
+        (setq state-echo "Locally added file")
+           (setq face 'vc-locally-added-state)
+        "@")
+       ((eq state 'conflict)
+        (setq state-echo "File contains conflicts after the last merge")
+           (setq face 'vc-conflict-state)
+        "!")
+       ((eq state 'removed)
+        (setq state-echo "File removed from the VC system")
+           (setq face 'vc-removed-state)
+        "!")
+       ((eq state 'missing)
+        (setq state-echo "File tracked by the VC system, but missing from the 
file system")
+           (setq face 'vc-missing-state)
+        "?")
+          (t
+           (setq state-echo "Locally modified file")
+           (setq face 'vc-edited-state)
+           ":"))
+      branch)
+     'face face
+     'help-echo (concat state-echo " under the Hg version control system"))))
+
+(defun vc-hgcmd-create-repo ()
+  "Init Hg repository."
+  (vc-hgcmd-command "init"))
+
+;; TODO vc switches
+
+(defun vc-hgcmd-register (files &optional _comment)
+  "Register FILES."
+  (apply #'vc-hgcmd-command (nconc (list "add") files)))
+
+(defalias 'vc-hgcmd-responsible-p 'vc-hgcmd-root)
+
+;; TODO receive-file
+
+(defun vc-hgcmd-unregister (file)
+  "Forget FILE."
+  (vc-hgcmd-command "forget" file))
+
+(declare-function log-edit-extract-headers "log-edit" (headers string))
+(declare-function log-edit-toggle-header "log-edit" (header value))
+(declare-function log-edit-set-header "log-edit" (header value &optional 
toggle))
+
+(defun vc-hgcmd--arg-close-branch (value)
+  "If VALUE is yes then --close-branch."
+  (when (equal "yes" value) (list "--close-branch")))
+
+(defun vc-hgcmd--arg-amend (value)
+  "If VALUE is yes then --close-branch."
+  (when (equal "yes" value) (list "--amend")))
+
+(defun vc-hgcmd-checkin (files comment &optional _rev)
+  "Commit FILES with COMMENT."
+  (apply #'vc-hgcmd-command
+         (nconc
+          (list "commit" "-m")
+          (log-edit-extract-headers `(("Author" . "--user")
+                                      ("Date" . "--date")
+                                      ("Amend" . vc-hgcmd--arg-amend)
+                                      ("Close-branch" . 
vc-hgcmd--arg-close-branch))
+                                    (encode-coding-string comment 'utf-8))
+          files)))
+
+(defun vc-hgcmd-find-revision (file rev buffer)
+  "Put REV of FILE to BUFFER."
+  (apply #'vc-hgcmd-command-output-buffer buffer (if rev (list "cat" "-r" rev 
file) (list "cat" file))))
+
+(defun vc-hgcmd-checkout (file &optional rev)
+  "Retrieve revision REV of FILE."
+  (vc-hgcmd-find-revision file rev (or (get-file-buffer file) 
(current-buffer))))
+
+(defun vc-hg-revert (file &optional contents-done)
+  "Refert FILE if not CONTENTS-DONE."
+  (unless contents-done
+    (vc-hgcmd-command "revert" file)))
+
+(defun vc-hgcmd-merge-branch ()
+  "Merge."
+  (let* ((completion-fun (if (and (boundp 'ido-mode) ido-mode) 
#'ido-completing-read #'completing-read))
+         (branch (funcall completion-fun "Merge from branch: " (nconc (list 
"") (vc-hgcmd--branches) (vc-hgcmd--tags)))))
+    (vc-hgcmd-command-update-callback
+     (if (> (length branch) 0)
+         (list "merge" branch)
+       (list "merge")))))
+
+(defun vc-hgcmd-pull (prompt)
+  "Pull. Prompt for args if PROMPT."
+  (vc-hgcmd-command-update-callback
+   (nconc
+    (list "pull")
+    (split-string-and-unquote (if prompt (read-from-minibuffer "Hg pull: " 
vc-hgcmd-pull-args) vc-hgcmd-pull-args)))))
+
+(defun vc-hgcmd-push (prompt)
+  "Pull. Prompt for args if PROMPT."
+  (vc-hgcmd-command-update-callback
+   (nconc
+    (list "push")
+    (when prompt (split-string-and-unquote (read-from-minibuffer "Hg push: " 
vc-hgcmd-push-alternate-args))))))
+
+(defun vc-hgcmd-mark-resolved (files)
+  "Mark FILES resolved."
+  (apply #'vc-hgcmd-command (nconc (list "resolve" "-m") files)))
+
+(defun vc-hgcmd-print-log (files buffer &optional shortlog start-revision 
limit)
+  "Put maybe SHORTLOG log of FILES to BUFFER starting with START-REVISION 
limited by LIMIT."
+  ;; TODO short log
+  (let ((command
+         (nconc
+          (list "log")
+          (when start-revision
+            ;; start revision is used for branch log or specific revision log 
when limit is 1
+            (list (if (eq limit 1) "-r" "-b") start-revision))
+          (when limit (list "-l" (number-to-string limit)))
+          (unless (or shortlog (eq limit 1)) (list "-f")) ; follow file renames
+          files)))
+    ;; If limit is 1 or vc-log-show-limit then it is initial diff and better 
move to working revision
+    ;; otherwise remember point position and restore it later
+    (let ((p (with-current-buffer buffer (unless (or (member limit (list 1 
vc-log-show-limit))) (point)))))
+      (apply #'vc-hgcmd-command-output-buffer buffer command)
+      (with-current-buffer buffer
+        (if p
+            (goto-char p)
+          (unless start-revision (vc-hgcmd-show-log-entry nil)))))))
+
+(defun vc-hgcmd--log-in-or-out (type buffer remote-location)
+  "Log TYPE changesets for REMOTE-LOCATION to BUFFER."
+  (apply #'vc-hgcmd-command-output-buffer buffer type (unless (string= "" 
remote-location) remote-location)))
+
+
+(defun vc-hgcmd-log-outgoing (buffer remote-location)
+  "Log outgoing for REMOTE-LOCATION to BUFFER."
+  (vc-hgcmd--log-in-or-out "outgoing" buffer remote-location))
+
+(defun vc-hgcmd-log-incoming (buffer remote-location)
+  "Log incoming from REMOTE-LOCATION to BUFFER."
+  (vc-hgcmd--log-in-or-out "incoming" buffer remote-location))
+
+(defconst vc-hgcmd--message-re "^changeset:\\s-*\\(%s\\):\\([[:xdigit:]]+\\)")
+(defconst vc-hgcmd--log-view-message-re (format vc-hgcmd--message-re 
"[[:digit:]]+"))
+(defvar log-view-per-file-logs)
+(defvar log-view-message-re)
+(defvar log-view-font-lock-keywords)
+
+(define-derived-mode vc-hgcmd-log-view-mode log-view-mode "Log-View/Hgcmd"
+  (require 'add-log)
+  (set (make-local-variable 'log-view-per-file-logs) nil)
+  (set (make-local-variable 'log-view-message-re) 
vc-hgcmd--log-view-message-re)
+  (set (make-local-variable 'log-view-font-lock-keywords)
+  (append
+   log-view-font-lock-keywords
+   '(
+     ("^user:[ \t]+\\([^<(]+?\\)[ 
\t]*[(<]\\([A-Za-z0-9_.+-]+@[A-Za-z0-9_.-]+\\)[>)]"
+         (1 'change-log-name)
+         (2 'change-log-email))
+        ("^user:[ \t]+\\([A-Za-z0-9_.+-]+\\(?:@[A-Za-z0-9_.-]+\\)?\\)"
+         (1 'change-log-email))
+        ("^date: \\(.+\\)" (1 'change-log-date))
+     ("^parent:[ \t]+\\([[:digit:]]+:[[:xdigit:]]+\\)" (1 
'change-log-acknowledgment))
+        ("^tag: +\\([^ ]+\\)$" (1 'highlight))
+        ("^summary:[ \t]+\\(.+\\)" (1 'log-view-message))))))
+
+(defun vc-hgcmd-show-log-entry (revision)
+  "Show log entry positioning on REVISION."
+  ;; REVISION might be branch name while print-branch-log
+  ;; if 'changeset: revision' not found try move to working rev
+  (goto-char (point-min))
+  (if (search-forward-regexp (format vc-hgcmd--message-re revision) nil t)
+      (goto-char (match-beginning 0))
+    (when (search-forward-regexp (format vc-hgcmd--message-re 
(vc-hgcmd-working-revision nil)) nil t)
+      (goto-char (match-beginning 0)))))
+
+(defun vc-hgcmd-diff (files &optional rev1 rev2 buffer _async)
+  "Place diff of FILES between REV1 and REV2 into BUFFER."
+  (let ((command (nconc (list "diff") (when rev1 (list "-r" rev1)) (when rev2 
(list "-r" rev2)) files)))
+    (apply #'vc-hgcmd-command-output-buffer buffer command)))
+
+(defun vc-hgcmd-revision-completion-table (_files)
+  "Return branches and tags as they are more usefull than file revisions."
+  (letrec ((table (lazy-completion-table table (lambda () (nconc 
(vc-hgcmd--branches) (vc-hgcmd--tags))))))))
+
+(defconst vc-hgcmd-annotate-re
+  (concat
+   "^\\(?: *[^ ]+ +\\)?\\([0-9]+\\) "
+   "\\([0-9]\\{4\\}-[0-1][0-9]-[0-3][0-9]\\): "
+   ))
+
+(defconst vc-hgcmd-annotate-filename-re
+  "^\\(?: *[^ ]+ +\\)?\\([0-9]+\\) [0-9]\\{4\\}-[0-1][0-9]-[0-3][0-9]\\( 
+\\([^:]+\\)\\):"
+  )
+
+(defun vc-hgcmd-annotate-command (file buffer &optional revision)
+  "Annotate REVISION of FILE to BUFFER."
+  (apply #'vc-hgcmd-command-output-buffer buffer
+         (nconc
+          (list "annotate" "-qdnuf")
+          (when revision (list "-r" revision))
+          (list file)))
+  ;; hide filenames but keep it in properties
+  (with-current-buffer buffer
+    (let ((inhibit-read-only t))
+      (goto-char (point-min))
+      (while (not (eobp))
+        (when (looking-at vc-hgcmd-annotate-filename-re)
+          (add-text-properties (line-beginning-position) (line-end-position)
+                               (list 'vc-hgcmd-annotate-filename 
(match-string-no-properties 3)
+                                     'vc-hgcmd-annotate-revision 
(match-string-no-properties 1)))
+          (delete-region (match-beginning 2) (match-end 2)))
+        (forward-line)))))
+
+(declare-function vc-annotate-convert-time "vc-annotate" (&optional time))
+
+(defun vc-hgcmd-annotate-time ()
+  "Return the time of the next line of annotation at or after point, as a 
floating point fractional number of days."
+  (when (looking-at vc-hgcmd-annotate-re)
+    (goto-char (match-end 0))
+    (vc-annotate-convert-time
+     (let ((str (match-string-no-properties 2)))
+       (encode-time 0 0 0
+                    (string-to-number (substring str 6 8))
+                    (string-to-number (substring str 4 6))
+                    (string-to-number (substring str 0 4)))))))
+
+(defun vc-hgcmd-annotate-extract-revision-at-line ()
+  "Return revision at line."
+  (cons (get-text-property (point) 'vc-hgcmd-annotate-revision)
+        (expand-file-name (get-text-property (point) 
'vc-hgcmd-annotate-filename) (vc-hgcmd-root default-directory))))
+
+(defun vc-hgcmd-create-tag (_dir name branchp)
+  "Create tag NAME. If BRANCHP create named branch."
+  (vc-hgcmd-command (if branchp "branch" "tag") name))
+
+(defun vc-hgcmd-retrieve-tag (_dir name _update)
+  "Update to branch NAME."
+  (if (string-empty-p name)
+      (vc-hgcmd-command "update")
+    (vc-hgcmd-command "update" name)))
+
+(defun vc-hgcmd-root (file)
+  "Return root folder of repository for FILE."
+  (vc-find-root file ".hg"))
+
+(defun vc-hgcmd-previous-revision (_file rev)
+  "Revison prior to REV."
+  (unless (string= rev "0")
+    (vc-hgcmd-command "id" "-n" "-r" (concat rev "^"))))
+
+(defun vc-hgcmd-next-revision (_file rev)
+  "Revision after REV."
+  (let ((newrev (1+ (string-to-number rev))))
+    (when (<= newrev (string-to-number (vc-hgcmd-command "tip" "-T" "{rev}")))
+      (number-to-string newrev))))
+
+(declare-function log-edit-mode "log-edit" ())
+
+(defun vc-hgcmd--set-log-edit-summary ()
+  "Set summary of commit message to 'merged ...' if commiting after merge."
+  (let* ((parents (split-string (vc-hgcmd-command "log" "-r" "p1()+p2()" 
"--template" "{node}\\0{branch}\n") "\n"))
+         (p1 (car parents))
+         (p2 (cadr parents)))
+    (when p2
+      (let ((p1 (split-string p1 "\0"))
+            (p2 (split-string p2 "\0")))
+        (save-excursion
+          (insert (concat "merged " (if (string= (cadr p1) (cadr p2)) (car p2) 
(cadr p2)))))))))
+
+(defun vc-hgcmd-log-edit-toggle-close-branch ()
+  "Toggle --close-branch commit option."
+  (interactive)
+  (log-edit-toggle-header "Close-branch" "yes"))
+
+(defun vc-hgcmd-log-edit-toggle-amend ()
+  "Toggle --amend commit option. If on, insert commit message of the previous 
commit."
+  (interactive)
+  (when (log-edit-toggle-header "Amend" "yes")
+    (log-edit-set-header "Summary" (vc-hgcmd-command "log" "-l" "1" 
"--template" "{desc}"))))
+
+(defvar vc-hgcmd-log-edit-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd "C-c C-e") 'vc-hgcmd-log-edit-toggle-amend)
+    (define-key map (kbd "C-c C-l") 'vc-hgcmd-log-edit-toggle-close-branch)
+    map)
+  "Keymap for log edit mode.")
+
+(define-derived-mode vc-hgcmd-log-edit-mode log-edit-mode "Log-Edit/Hgcmd"
+  "Major mode for editing Hgcmd log messages.
+
+\\{vc-hgcmd-log-edit-mode-map}"
+  ;; if there are two parents create maybe helpful commit message
+  ;; it must be done in log-edit-hook
+  (add-hook 'log-edit-hook #'vc-hgcmd--set-log-edit-summary t t))
+
+(defun vc-hgcmd-delete-file (file)
+  "Delete FILE."
+  (vc-hgcmd-command "remove" "--force" file))
+
+(defun vc-hgcmd-rename-file (old new)
+  "Rename file from OLD to NEW using `hg mv'."
+  (vc-hgcmd-command "move" old new))
+
+(defun vc-hgcmd--file-unresolved-p (file)
+  "Return t if FILE is in conflict state."
+  (let ((out (vc-hgcmd-command "resolve" "-l" file)))
+    (and out (eq (aref out 0) ?U))))
+
+(defun vc-hgcmd--after-save-hook ()
+  "After save hook. Mark file as resolved if vc state eq conflict and no 
smerge mode."
+  (when (and buffer-file-name
+             (not (save-excursion
+                    (goto-char (point-min))
+                    (re-search-forward "^<<<<<<< " nil t)))
+             (yes-or-no-p (format "Hgcmd: Mark %s as resolved? " 
buffer-file-name)))
+    (vc-hgcmd-mark-resolved (list buffer-file-name))
+    (remove-hook 'after-save-hook #'vc-hgcmd--after-save-hook t)))
+
+;; TODO It's really handy to autostart smerge but additional hg command will 
be called on every find-file
+(defun vc-hgcmd-find-file-hook ()
+  "Find file hook. Start smerge session if vc state eq conflict."
+  (when (vc-hgcmd--file-unresolved-p buffer-file-name)
+    (smerge-start-session)
+    (add-hook 'after-save-hook #'vc-hgcmd--after-save-hook nil t)
+    (vc-message-unresolved-conflicts buffer-file-name)))
+
+;; TODO extra menu
+
+;; TODO extra-dir-menu. update -C for example or commit --close-branch or 
--amend without changes
+
+(defun vc-hgcmd-conflicted-files (&optional _dir)
+  "List of files where conflict resolution is needed."
+  (let ((out (vc-hgcmd-command "files" "set:unresolved()")))
+    (and out (split-string out "\n"))))
+
+(defun vc-hgcmd-find-ignore-file (file)
+  "Return the ignore file of the repository of FILE."
+  (expand-file-name ".hgignore" (vc-hgcmd-root file)))
+
+(provide 'vc-hgcmd)
+
+;;; vc-hgcmd.el ends here

Reply via email to