Use Perforce inside Emacs

  |   Source

CREATED: <2016-12-05>

UPDATED: <2017-02-08 Wed>

Perforce is a proprietary VCS which is not as powerful as Git so it takes extra effort to use it with Emacs.

For example, git can display the detailed history of a single file using git log -p file-path. There is no way you can do this in perforce. Combination of perforce cli like p4 changes file-path | awk '{print $2}' | xargs -i p4 describe -du {} displays all the files of related changes. You have to turn to Emacs Lisp to clean the output of p4.

1 Perforce workflow

  • p4 set to set up
  • p4 login to login
  • p4 client creates a new work space to tell server the directories/files to check out or to ignore
  • p4 sync //depot/url/... to actually checkout
  • Files are read-only by default. You need p4 edit file to make files writable before editing
  • p4 add files to add new files. p4 revert to revert edited file to it original status and lose the local changes
  • p4 change to create a pending change. Then p4 submit -c #changelist to actually submit code to main branch. Pending change gives you a chance to tweak the change before submit
  • Or p4 submit -d"description" file to submit a single file directly

2 My solution

Perforce cygwin portable is not recommended.

I suggest using executable from Perforce Windows native version which works on both Cygwin and Windows.

Perforce server has one unique URL for every physical file on the disk. If I only feed p4 that URL, the operation is always successful.

2.1 Emacs

I provide p4edit, p4revert, p4submit, p4diff, and p4history,

;; {{ perforce utilities
(defvar p4-file-to-url '("" "")
  "(car p4-file-to-url) is the original file prefix
(cadr p4-file-to-url) is the url prefix")

(defun p4-current-file-url ()
  (replace-regexp-in-string (car p4-file-to-url)
                            (cadr p4-file-to-url)

(defun p4-generate-cmd (opts)
  (format "p4 %s %s" opts (p4-current-file-url)))

(defun p4edit ()
  "p4 edit current file."
  (shell-command (p4-generate-cmd "edit"))
  (read-only-mode -1))

(defun p4submit (&optional file-opened)
  "p4 submit current file.
If FILE-OPENED, current file is still opened."
  (interactive "P")
  (let* ((msg (read-string "Say (ENTER to abort):"))
         (open-opts (if file-opened "-f leaveunchanged+reopen -r" ""))
         (full-opts (format "submit -d '%s' %s" msg open-opts)))
    ;; (message "(p4-generate-cmd full-opts)=%s" (p4-generate-cmd full-opts))
    (if (string= "" msg)
        (message "Abort submit.")
      (shell-command (p4-generate-cmd full-opts))
      (unless file-opened (read-only-mode 1))
      (message (format "%s submitted."
                       (file-name-nondirectory buffer-file-name))))))

(defun p4revert ()
  "p4 revert current file."
  (shell-command (p4-generate-cmd "revert"))
  (read-only-mode 1))

(defun p4-show-changelist-patch (line)
  (let* ((chg (nth 1 (split-string line "[\t ]+")))
         (url (p4-current-file-url))
         (pattern "^==== //.*====$")
         (start 0)
         (original (if chg (shell-command-to-string (format "p4 describe -du %s" chg)) ""))

    (while (setq sep (string-match pattern original start))
      (let* ((str (match-string 0 original)))
        (setq start (+ sep (length str)))
        (add-to-list 'seps (list sep str) t)))
    (setq rlt (substring original 0 (car (nth 0 seps))))
    (let* ((i 0) found)
      (while (and (not found)
                  (< i (length seps)))
        (when (string-match url (cadr (nth i seps)))
          (setq rlt (concat rlt (substring original
                                           (car (nth i seps))
                                           (if (= i (- (length seps) 1))
                                               (length original)
                                             (car (nth (+ 1 i) seps))))))
          ;; out of loop now since current file patch found
          (setq found t))
        (setq i (+ 1 i))))

    ;; remove p4 verbose bullshit
    (setq rlt (replace-regexp-in-string "^\\(Affected\\|Moved\\) files \.\.\.[\r\n]+\\(\.\.\. .*[\r\n]+\\)+"
    (setq rlt (replace-regexp-in-string "Differences \.\.\.[\r\n]+" "" rlt))
    ;; one line short description of change list
    (setq rlt (replace-regexp-in-string "Change \\([0-9]+\\) by \\([^ @]+\\)@[^ @]+ on \\([^ \r\n]*\\).*[\r\n \t]+\\([^ \t].*\\)" "\\1 by \\2@\\3 \\4" rlt))

(defun p4--create-buffer (buf-name content &optional enable-imenu)
  (let* (rlt-buf)
    (if (get-buffer buf-name)
        (kill-buffer buf-name))
    (setq rlt-buf (get-buffer-create buf-name))
      (switch-to-buffer-other-window rlt-buf)
      (set-buffer rlt-buf)
      (insert content)
      (goto-char (point-min))
      ;; nice imenu output
      (if enable-imenu
          (setq imenu-create-index-function
                (lambda ()
                    (imenu--generic-function '((nil "^[0-9]+ by .*" 0)))))))
      ;; quit easily in evil-mode
      (evil-local-set-key 'normal "q" (lambda () (interactive) (quit-window t))))))

(defun p4diff ()
  "Show diff of current file like `git diff'."
  (let* ((content (shell-command-to-string (p4-generate-cmd "diff -du -db"))))
    (p4--create-buffer "*p4diff*" content)))

(defun p4history ()
  "Show history of current file like `git log -p'."
  (let* ((changes (split-string (shell-command-to-string (p4-generate-cmd "changes")) "\n"))
         (content (mapconcat 'p4-show-changelist-patch
    (p4--create-buffer "*p4log*" content t)))
;; }}

As a bonus tip, if you use find-file-in-project, insert below code into prog-mode-hook to view any perforce change inside Emacs,

(setq-local ffip-diff-backends
            '((ivy-read "p4 change to show:"
                        (split-string (shell-command-to-string "p4 changes //depot/development/DIR/PROJ1/...")
                        :action (lambda (i)
                                  (if (string-match "^ Change \\([0-9]*\\)" i)
                                      (shell-command-to-string (format "p4 describe -du -db %s"
                                                                       (match-string 1 i))))))
              "p4 diff -du -db //depot/development/DIR/PROJ1/..."))

2.2 Bash Shell

Other operations are finished in Bash Shell,

# {{ Perforce, I hope I will never use it
if [ "$OS_NAME" = "CYGWIN" ]; then
    function p4() {
        export PWD=`cygpath -wa .`
        /cygdrive/c/Program\ Files/Perforce/p4.exe $@

# p4 workflow:
#   # basic setup
#   p4 set P4CLIENT=clientname  # set your default client
#   p4 set P4PORT=SERVER:1666
#   p4 set P4USER=username
#   p4 client # create/edit client, client views selected files
#   # checkout code
#   p4 sync [-f] //depot/project-name/path/...
#   p4 edit file[s]
#   ... do some editing ...
#   # submit code
#   either `p4 submit -d"say hi" file` or `p4 change`
#   I recommend `p4 change` because you can edit files list before submit happens.
#   After `p4 change`,  `p4 submit -c changelist#` to actually submit change.
alias p4clr='p4 diff -sr | p4 -x - revert' # like `git reset HEAD`
alias p4blame='p4 annotate -c -db ' # could add -a see deleted lines
alias p4cr='p4 submit -f leaveunchanged+reopen -r'
alias reviewcl='ccollab addchangelist new'
alias p4pending='p4 changes -s pending' # add ... for current directory
alias p4untrack='find . -type f| p4 -x - fstat >/dev/null'
alias p4v='p4 resolve' # after `p4 sync ...`, maybe resolve
alias p4r='p4 revert' # discard changes
alias p4e='p4 edit'
alias p4s='p4 submit'
alias p4sr='p4 submit -f submitunchanged+reopen' #submit&reopen
alias p4up='p4 sync ...' # synchronize from current directory
alias p4o='p4 opened' # list opened files
alias p4c='p4 changes' # create a new pending change
alias p4chg='p4 change' # create a pending change
alias p4d='p4 diff -du -db'
alias p4ds='p4 diff -du -db | lsdiff' # diff summary, patchutils required
alias p4i='p4 integrate'
alias p4unsh='p4 unshelve -s' # Usage: p4unsh changelist#, like `git stash apply`
alias p4h='p4 changes -m 1 ...' # show the head change

function p4mypending {
    local P4USERNAME="`p4 user -o | grep '^User:\s' | sed 's/User:\s\([a-bA-B0-9]*\)/\1/g'`"
    p4 changes -s pending -u $P4USERNAME

function p4shelved {
    local P4USERNAME="`p4 user -o | grep '^User:\s' | sed 's/User:\s\([a-bA-B0-9]*\)/\1/g'`"
    p4 changes -s shelved -u $P4USERNAME # add ... for current directory

function p4cmp {
    if [ -z "$1" ]; then
        echo "Usage: p4cmp changelist-number changelist-number"
        p4 diff2 -dub -q -u ...@$1 ...@$2

function p4dl {
    # git diff
    p4 diff -du -db $@ | vim -c "set syntax=diff" -R -
function p4sh(){
    # show specific change or the latest change
    if [ -z "$1" ]; then
        p4 changes | python ~/bin/ | awk '{print $2}' | xargs -i p4 describe -du {} | vim -c "set syntax=diff" -R -
        p4 describe -du -db $@ | vim -c "set syntax=diff" -R -

function p4lp {
    #like `git log -p`
    p4 changes $@ | awk '{print $2}' | xargs -i p4 describe -du {} | less -F

function p4mlp {
    #like `git log -p`
    p4 changes -u $P4USERNAME $@ | awk '{print $2}' | xargs -i p4 describe -du {} | less -F

function p4adddir(){
    if [ -z "$1" ]; then
        echo "Usage: p4adddir directory"
        find $1 -type f -print | p4 -x - add

# p4's suggestion,
# @google "assing variable from bash to perl in a bash script"
function p4l(){
    # p4 log
    if [ -z "$1" ]; then
        # show the full log
        p4 changes -l ... | less
        # p4log since-changelist-number
        p4 changes -l ...@$1,#head|perl -pe "if(\$_=~/$1/){ last;};"

function p4ml(){
    # my p4 log
    if [ -z "$1" ]; then
        # show the full log
        p4 changes -l -u $P4USERNAME ... | less
        # p4log since-changelist-number
        p4 changes -l -u $P4USERNAME ...@$1,#head|perl -pe "if(\$_=~/$1/){ last;};"
# }}

Comments powered by Disqus