Use Perforce inside Emacs
CREATED:
UPDATED:
Perforce is a proprietary VCS. It's less powerful than Git so need extra effort to be use with Emacs.
For example, git log -p file-path
displays the detailed history of a single file. There is no way you can do this in perforce. Even ClI like p4 changes file-path | awk '{print $2}' | xargs -i p4 describe -du {}
can't do it. I have to use Emacs Lisp to clean the output of p4
.
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 checkout files - 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. Thenp4 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
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 assigns unique URL for each physical file. If I only feed p4
that URL, the operation is always successful.
Emacs
I developed vc-msg which shows "commit message of current line in Emacs". It supports Perforce out of box (you still need p4 login
at first.)
My patched emacs-git-gutter show Perforce gutters. You only need setup git-gutter:exp-to-create-diff
. Here is a sample,
(setq-local git-gutter:exp-to-create-diff
(shell-command-to-string (format "p4 diff -du -db %s"
(file-relative-name buffer-file-name))))
I also provided commands like 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)
buffer-file-name))
(defun p4-generate-cmd (opts)
(format "p4 %s %s" opts (p4-current-file-url)))
(defun p4edit ()
"p4 edit current file."
(interactive)
(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."
(interactive)
(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 "^==== //.*====$")
sep
seps
(start 0)
(original (if chg (shell-command-to-string (format "p4 describe -du %s" chg)) ""))
rlt)
(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]+\\)+"
""
rlt))
(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))
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))
(save-current-buffer
(switch-to-buffer-other-window rlt-buf)
(set-buffer rlt-buf)
(erase-buffer)
(insert content)
(diff-mode)
(goto-char (point-min))
;; nice imenu output
(if enable-imenu
(setq imenu-create-index-function
(lambda ()
(save-excursion
(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'."
(interactive)
(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'."
(interactive)
(let* ((changes (split-string (shell-command-to-string (p4-generate-cmd "changes")) "\n"))
(content (mapconcat 'p4-show-changelist-patch
changes
"\n\n")))
(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/...")
"\n")
: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/..."))
You can also check my emacs.d to get latest code.
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 $@
}
fi
# 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"
else
p4 diff2 -dub -q -u ...@$1 ...@$2
fi
}
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/percol.py | awk '{print $2}' | xargs -i p4 describe -du {} | vim -c "set syntax=diff" -R -
else
p4 describe -du -db $@ | vim -c "set syntax=diff" -R -
fi
}
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"
else
find $1 -type f -print | p4 -x - add
fi
}
# p4's suggestion,http://kb.perforce.com/article/27/creating-release-notes
# @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
else
# p4log since-changelist-number
p4 changes -l ...@$1,#head|perl -pe "if(\$_=~/$1/){ last;};"
fi
}
function p4ml(){
# my p4 log
if [ -z "$1" ]; then
# show the full log
p4 changes -l -u $P4USERNAME ... | less
else
# p4log since-changelist-number
p4 changes -l -u $P4USERNAME ...@$1,#head|perl -pe "if(\$_=~/$1/){ last;};"
fi
}
# }}