「EmacsでChangeSetベースのVCSと仲良くする」(菅原泰樹)
「Emacsのトラノマキ」連載第八回「EmacsでChangeSetベースのVCSと仲良くする」
EmacsでChangeSetベースのVCSと仲良くする
今回は前回の予告通りChangeSetベースのVCSを扱うためのあれこれを書いていきます.GitやMercurialなどの分散型VCSだけではなく,現場で使うことの多いSubversionの話もあるのでご安心を.
今回の記事にあるelispのカスタイマイズ例は筆者の会社のブログにも載せるつもりです.書き写すのが面倒な人はそちらを見るようにしてください.
VCで仲良くする
Emacs23からは標準で付いているVCにもいくつかChangeSet向けの機能が追加されました.diffのChangeSet対応とvc-dirです.VCを使うメリットは各種のVCSで全て同じ操作ができることと,標準で使えることです.ファイル単位での操作については前回の記事で書いたのでそちらを参照してください.
diffのChangeSet対応を試すにはC-x v l (vc-print-log)
として,ログを表示してから対象のリビジョンの上でD (log-view-diff-changeset)
として下さい.ログを表示したファイルの差分だけではなく,そのリビジョンで変更された全てのファイルの差分が表示されます.残念ながらChangeSetの詳細を表示する機能や,ChangeSetでの変更をediffで表示する機能はまだ実装されていません.
vc-dirは手元やリポジトリで変更があったファイルを一覧してくれるコマンドで,Emacs22までにあったvc-directoryより断然使いやすくなっています.非同期で状態を表示してくれるのが素晴らしいです.感覚としてはpcl-cvsのcvs-statusを各VCSで使えるようにした感じでしょうか.
vc-dirを使うにはC-x v d (vc-dir)
を入力します.するとVC status for directory:
と対象のディレクトリを聞いてくるので作業コピーのルートのディレクトリ(または確認したいディレクトリ)を入力します.入力すると以下のように作業コピーの状態が表示されます.
ここでg (revert-buffer)
を入力すると状態の更新,+ (vc-update)
を入力するとファイルの更新が行なえます.複数のファイルをまとめて操作するにはm (vc-dir-mark)
でファイルをマークしておきます.そのあとv (vc-next-action)
でマークしたファイルをまとめてコミットしたり,= (vc-diff)
でまとめて差分を見たりすることができます.
便利なvc-dirですが,悲しいお知らせが一つあります.vc-updateが非同期処理ではなく同期処理なのです.巨大なソースツリーを相手にしているときにこれは致命的です.とはいえ,状態の更新の方は非同期ですし,色々なVCSに対してこういった機能が標準で同じ操作で使えるというのはやはり魅力的です.今はまだ微妙な箇所もありますが,Emacs22までとくらべれば大分進化していますし,まだまだ良くなって行くでしょう.これからに期待です.
コラム?: revert-buffer と revert-buffer-function
vc-dirでのg
がrevert-bufferとなっている事を不思議に思った読者もいるのではないでしょうか?普通はrevert-bufferはそのバッファの内容を実際のファイルの内容で読み直すコマンドとして使われますよね.実はrevert-bufferはファイルと紐付いていない場合はrevert-buffer-function関数で挙動を制御できるんです.vc-dirの場合は,vc-dir-revert-buffer-functionが指定されていて,この関数がvc-dir-refreshを呼ぶことで非同期にバッファの内容を最新にするようになっています.Emacsって奥が深いですね.
コラム?: vc-dirでunregisteredを消す
vc-dirで一覧を表示したときに,動的に生成されたファイルがunregisteredという状態で大量に出てきて辟易した読者もいるかと思います.本来ならば .gitignore, .hgignore, svn:ignore などを使って無視するファイルを定義するべきなんですが,そんな事を言ってられない場合があるのも事実です.実はvc-dirにはup-to-dateという状態のファイルを表示しないためのvc-dir-hide-up-to-dateというコマンドが準備されています.ところが,vc-dir-hide-unregisteredが準備されていません.実はちょこっと変えるだけでunregisteredに対応させることができるので,興味のある方はハックしてみてください.本来ならvc-dir-hide-stateがあるべきなんですけどね.
DVCで仲良くする
DVCとは「Distributed Version Control for Emacs」の略で分散型VCSをVCのように共通の操作で扱えるようにしようというパッケージです.分散型VCSのためのパッケージなのでSubversionには対応していません.どうもDVCはWebを見てもあまり情報がないように思えます.ここでは,そんなDVCを紹介していこうかと思います.
DVCに関する情報は以下を見るとよいです.
- http://www.emacswiki.org/emacs/DistributedVersionControl
- http://download.gna.org/dvc/
- http://download.gna.org/dvc/docs/dvc-snapshot.html
DVCのインストール
DVCは大きなパッケージなのでinstall-elispだけでのインストールができません.まずはwget等でダウンロードします.
$ wget http://download.gna.org/dvc/download/dvc-snapshot.tar.gz
あとは普通にconfigure, makeです.
$ tar zxf dvc-snapshot.tar.gz
$ cd dvc-snapshot
$ autoconf
$ configure
$ make
$ sudo make install
.emacs.elに以下を追記しておきます.
(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/dvc")
(require 'dvc-autoloads)
(setq dvc-tips-enabled nil)
DVCを使う
まずはC-x V s (dvc-status)
を入力してください.もし今いる場所がVCS配下の場所なら,そのままプロジェクトのルートの状態が表示され,そうでない場合は,プロジェクトの場所を聞いてきます.dvc-statusを実行するとvc-dirのように作業コピーの状態が表示されます.
ここでは,commit, pull, update, ログの表示,差分の表示などが行なえます.dvc-statusモードでの主な操作を以下にまとめておきます.他にもいろいろあるのでメニューから機能を辿ってみることをお勧めします.
| キーバインド | コマンド | 説明 |
| g | dvc-generic-refresh | 状態の更新 |
| RET | dvc-diff-jump-to-change | ファイルを表示 |
| m | dvc-diff-mark-file | ファイルをマーク |
| u | dvc-diff-unmark-file | ファイルをアンマーク |
| c | dvc-log-edit | マークしたファイルをコミット |
| L | dvc-diff-log-tree | プロジェクト全体の履歴を見る |
| l | dvc-diff-log-single | 履歴を見る |
| = | dvc-diff-diff | 差分を表示 |
| e | dvc-diff-ediff | 差分をediffで表示 |
| a | dvc-fileinfo-add-files | マークしたファイルを追加 |
| U | dvc-fileinfo-revert-files | マークしたファイルを取消 |
| i | dvc-fileinfo-ignore-files | マークしたファイルを無視リストに追加 |
| M f | dvc-pull | リモートからpullする |
| | dvc-push | リモートにpushする |
| M u | dvc-update | ローカルのHEADの状態に更新する |
| G p | xgit-status-add-patch | (Git)hunkをstageに追加 |
| G r | xgit-status-reset-mixed | (Git)stagedなファイルをunstagedにする |
| G s | xgit-diff-cached | (Git)stagedなファイルの差分を表示 |
| G u | xgit-diff-index | (Git)unstagedなファイルの差分を表示 |
Gitの場合は,ステージを扱うための機能がいくつか追加されています.たとえばすでに管理済みのファイルをaddするとstagedなファイルとして扱われます.また,G
ではじまるキーでステージを扱えるようになっています.コミットするときも「Use git index (y/n/a/e/c/?)? 」と聞いてくるので,ここでy
と答えればstagedなファイルだけコミットされます.
DVCにはファイルを編集しているときにVCSを扱うコマンドも当然準備されています.以下に主な操作をまとめておきます.これらの操作をしたあとはdvc-statusに戻って,変更を確認してからコミットすればよいです.
| キーバインド | コマンド | 説明 |
| C-x V L | dvc-log | 現在のファイルの履歴を表示 |
| C-x V l | dvc-changelog | 全ての履歴を表示 |
| C-x V = | dvc-diff | 全ての差分を表示 |
| C-x V d | dvc-file-diff | 現在のファイルの差分を表示 |
| C-x V e | dvc-file-ediff | 現在のファイルの差分をediffで表示 |
| C-x V f D | dvc-remove-files | ファイルをVCSから削除 |
| C-x V f M | dvc-rename | ファイルをVCS上でリネーム |
| C-x V f R | dvc-revert-files | ファイルの変更を取消 |
| C-x V f a | dvc-add-files | ファイルを追加 |
xhgの履歴表示を便利にする
DVCのMercurialフロントエンドのxhg-dvcの履歴表示は何故か差分の表示ができなかったり表示が遅かったりと散々な出来になっています.しかし,xhgそのものが持っている履歴表示機能はさくっと表示できますしインラインで差分を表示したりと便利な機能を持っています.そこで,xhg-dvcの履歴機能をxhgの履歴表示機能に置き換えてしまいます(半分以上xhg-logからのコピーです).以下のようにxhg-dvcの関数定義を置き換えてしまえばMercurialでの履歴表示が使いものになると思います.
(defun xhg-dvc-log (path last-n)
"Show a dvc formatted log for xhg."
(interactive (list default-directory nil))
(let ((buffer (dvc-get-buffer-create 'xhg 'log))
(cur-dir default-directory)
(args (append
'("log")
(and last-n (list "--limit" last-n))
(and path (list path)))))
(dvc-switch-to-buffer-maybe buffer)
(let ((inhibit-read-only t)
(default-directory cur-dir))
(erase-buffer)
(xhg-log-mode)
(dvc-run-dvc-async
'xhg args
:finished
(dvc-capturing-lambda (output error status arguments)
(progn
(with-current-buffer (capture buffer)
(let ((inhibit-read-only t))
(erase-buffer)
(insert-buffer-substring output)
(goto-char (point-min))
(insert (format "hg log for %s\n\n" (or (capture path) cur-dir)))
(toggle-read-only 1)))))))))
(defun xhg-dvc-changelog ()
"Show a dvc formatted changelog for xhg."
(call-interactively 'xhg-dvc-log))
専用のパッケージを使う
Emacsには各種のVCS向けのパッケージがいくつも作成されています.ここでは共通の操作を目指してものではなく,VCS専用の機能を提供するパッケージを紹介していきます.
Subversion
Subversion用のパッケージには主に以下のものがあります.
- psvn: http://svn.collab.net/repos/svn/trunk/contrib/client-side/emacs/psvn.el
- dsvn: http://svn.collab.net/repos/svn/trunk/contrib/client-side/emacs/dsvn.el
ここではpsvnを紹介します.dsvnの方が速いという話もあるのですが,正しく設定すれば最近のpsvnなら十分速いですし,なんといってもpsvnの方が見ためが楽しいので.
さて,まずはpsvnをインストールします.以下のようにinstall-elispでさっくり入れてしまいましょう.
M-x install-elisp
Install Emacs Lisp from URL: http://svn.collab.net/repos/svn/trunk/contrib/client-side/emacs/psvn.el
.emacs.elには以下のように書いておきます.
(require 'psvn)
(setq svn-status-verbose nil)
(setq svn-status-hide-unmodified t)
;; ログにファイル名を出さない
;; (setq svn-status-default-log-arguments nil)
;; プレフィクスをC-x sにする
;; (global-set-key (kbd "C-x s") svn-global-keymap)
svn-status-verboseは絶対にnilにしておいてください.そうしないと遅すぎて使いものになりません.
psvn を使うにはM-x svn-status
とします.すると以下のように作業コピーの状態が表示されます.
ここでは状態の更新やファイルの更新,ファイルに対しての各種操作が行なえます.svn-statusモードでの主な操作を以下にまとめておきます.他にも色々あるのでメニューを見て覚えてください.
| キーバインド | コマンド | 説明 |
| g | svn-status-update | 状態を更新 |
| U | svn-status-update-cmd | ファイルを更新 |
| m | svn-status-set-user-mark | ファイルをマーク |
| u | svn-status-unset-user-mark | ファイルをアンマーク |
| c | svn-status-commit | マークしたファイルをコミット |
| r | svn-status-revert | マークしたファイルを取消 |
| = | svn-status-show-svn-diff | 差分を見る |
| E | svn-status-ediff-with-revision | ediffで差分を見る |
| l | svn-status-show-svn-log | ログを見る |
| ? | svn-status-toggle-hide-unknown | 管理外のファイル表示をトグル |
vc-dirと違ってファイルの更新が非同期なのが嬉しいです.使い方はvc-dirはdvc-statusとあまり変わりはありません.
psvnも当然ファイルに対して操作する機能を持っているんですが,デフォルトのキーバインドがあまりにも変態すぎます.なんとプレフィクスがC-x M-s
です.使いづらすぎます.対処方はふたつあります.ひとつめはsvn-global-keymapを適当なキーに設定する方法です..emacs.elの設定の「プレフィクスをC-x sにする」の箇所のコメントをはずせば,プレフィクスがC-x s
になります.もうひとつは,Subversionで管理しているファイルのときだけC-x v
をのっとってしまう方法です.以下を.emacs.elに追加して下さい.モードラインに「PSVN」と書かれているバッファではC-x v
でpsvnの機能が使えるようになります.こうするとC-x v s
でsvn-statusが実行できたりといろいろ便利になります.
(define-minor-mode svn-minor-mode
"Subversion Minor Mode."
:global nil :lighter " PSVN"
:keymap (list (cons (kbd "C-x v") svn-global-keymap)))
(defun svn-find-file-hook ()
(when (eq (vc-backend default-directory) 'SVN)
(svn-minor-mode 1)))
(add-hook 'find-file-hook 'svn-find-file-hook)
(define-key svn-global-keymap "l" 'svn-status-show-svn-log)
(define-key svn-global-keymap "g" 'svn-status-blame)
(define-key svn-global-keymap "e" 'svn-file-show-svn-ediff)
Mercurial
Mercurial用のパッケージには主に以下のものがあります.
- mercurial.el: http://hg.intevation.org/mercurial/file/tip/contrib/mercurial.el
- ahg: http://disi.unitn.it/~griggio/ahg.html
ahgの方が機能も豊富なのに,あまりWebに情報がないのでここではahgの紹介をします.
インストールはinstall-elispで簡単に行なえます.hg clone してload-pathに通してもOKです.
M-x install-elisp
Install Emacs Lisp from URL: http://bitbucket.org/agriggio/ahg/raw/tip/ahg.el
.emacs.elには以下を書いておきます.
(require 'ahg)
;; プレフィクスをC-c hにする
;; (global-set-key (kbd "C-c h") ahg-global-map)
ahgもpsvnと同じようにahg-statusで状態を表示する画面が表示されます.
使い方もほとんど一緒です.主な操作を以下にまとめておきます.やはり他の操作はメニューを見て操作を覚えてください.MQのためのコマンドなんかもあります.そのかわりpush, pullのためのコマンドはありません.このへんは端末から行なってください.
| キーバインド | コマンド | 説明 |
| g | ahg-status-refresh | 状態を更新 |
| m | ahg-status-mark | ファイルをマーク |
| u | ahg-status-unmark | ファイルをアンマーク |
| c | ahg-status-commit | マークしたファイルをコミット |
| U | ahg-status-undo | マークしたファイルを取消 |
| = | ahg-status-diff | 差分を見る |
| D | ahg-status-diff-all | マークしたファイルの差分を見る |
| l | ahg-status-short-log | シンプルな履歴を表示 |
| L | ahg-status-log | 履歴を表示 |
| G | ahg-status-glog | グラフログを表示 |
| a | ahg-status-add | マークしたファイルを追加 |
| r | ahg-status-remove | マークしたファイルを削除 |
ahgは履歴の表示にちょっと癖があります.ahg-statusでl
を入力すると「hg log (on selected files), R1:」と質問されます.ここはデフォルトのまま「tip」と入力します.次に「hg log (on selected files), R2:」と質問されます.ここで「0」と答えると全ての履歴が表示されます.適当な番号を入れればそのリビジョンまでです.履歴を表示すると以下のような画面が表示されます.
こんな感じでなかなか分かりやすいログが表示されます.ここでSPC
を押すとそのリビジョンのログの詳細が,=
で差分が表示されます.リビジョン間の差分がちょっと面倒で,D
を入力してから比較対象のリビジョンを入力する必要があります.
さて,ahgもpsvnと同じように変態的なプレフィクスをもっています.今度はC-c h g
です.3ストロークです.そしてやはり対処方はふたつあります.ひとつめはahg-global-mapを適当なキーに設定する方法です..emacs.elの設定の「プレフィクスをC-c sにする」の箇所のコメントをはずせば,プレフィクスがC-c h
になります.C-x v
をのっとっるには以下を.emacs.elに追加して下さい.モードラインに「AHG」と書かれているバッファではC-x v
でahgの機能が使えるようになります.
(define-minor-mode ahg-minor-mode
"AHG Minor Mode."
:global nil :lighter " AHG"
:keymap (list (cons (kbd "C-x v") ahg-global-map)))
(defun ahg-find-file-hook ()
(when (eq (vc-backend default-directory) 'Hg)
(ahg-minor-mode 1)))
(add-hook 'find-file-hook 'ahg-find-file-hook)
;; C-x v l で今のファイルの履歴を表示するには以下を設定する
(define-key ahg-global-map "l" 'ahg-log-cur-file)
(define-key ahg-global-map (kbd "C-l") 'ahg-short-log)
コラム?: MercurialのマージプログラムをEmacsにする
Mercurialを使っているちょこちょこ衝突がおきます.そして普通はそれを解決するために.hgrcのmerge-toolsセクションにマージプログラムを登録しているかと思います.Emacsを使っている人ならやっぱりemergeを使って解決したいですよね?MercurialWikiの以下のページにEmacsを使ってマージをする方法がのっているので参考にしてみて下さい.マージの為にEmacsを起動するとても富豪的なやりかたです.
- http://mercurial.selenic.com/wiki/MergingWithEmacs
Git
流行っているだけあってGit用のパッケージはいろいろあります.主に以下が有名所でしょうか.
- git.el: http://www.kernel.org/git/?p=git/git.git;a=tree;hb=HEAD;f=contrib/emacs
- egg.el: http://github.com/bogolisk/egg/tree/master
以下にいろいろなGit用のパッケージがのっているので参考にしてみて下さい. - http://www.emacswiki.org/emacs/Git
ここでは高機能と評判のegg.elを紹介します.
egg.elのインストールはgit cloneしてからload-pathを通すといった手順になります.まずはegg.elを取ってきます.
$ git clone git://github.com/bogolisk/egg.git ~/.emacs.d/site-lisp/egg
次に.emacs.elに以下を書いておいてください. (add-to-list 'load-path "~/.emacs.d/site-lisp/egg") (require 'egg)
eggの場合も主に使うのはegg-statusです.おなじみですね.こんな画面が表示されます.とっても派手です.
使い方は他とちょっと違います.n
, p
で変更点に移動して,s (egg-diff-section-cmd-stage)
でその変更をステージに移動します.Gitらしく,変更の一部だけステージに移動する事も可能です.ステージが納得のゆく変更になったらc (egg-commit-log-edit)
でステージにある変更をコミットします.主な操作を以下にまとめておきます.
共通のコマンド
| c | egg-commit-log-edit | ステージにある変更をコミット |
| l | egg-log | 履歴を表示 |
| = | | 差分をediffで表示 |
Unstagedで使うコマンド
| キーバインド | コマンド | 説明 |
| s | egg-diff-section-cmd-stage | 変更をステージへ移動 |
| u | egg-diff-section-cmd-undo | 変更を取消 |
Stagedで使うコマンド
| キーバインド | コマンド | 説明 |
| s | egg-diff-section-cmd-unstage | 変更をステージから除く |
eggは履歴の表示も特徴的です.egg-logで履歴を表示するとまずは概要のログが表示されます.そこでSPC
を入力するとインラインで詳細と差分が表示されます.閉じるにはh
です.もう一度開くのにはh
です.これがちょっと違和感感じますね.履歴表示は以下のような画面が表示されます.
egg.elは他のモードと違って標準でC-x v
で操作できるようになっています.素晴しいです.C-x v v
でそれっぽく次にやるべきアクションを教えてくれるのもよいです.完成度の高いegg.elですがちょっとだけ不満もあります.C-x v l
が全体のログを見る設定になっていることと,egg-logでSPC
だけでトグルできないことです.この二つを以下のようにして解消します.
(defun egg-log-buffer-hide-show-dwim ()
(interactive)
(let* ((pos (point))
(next (next-single-property-change pos :diff))
(sha1 (and next (get-text-property next :commit)))
(nav (get-text-property pos :navigation)))
(if (equal (get-text-property pos :commit) sha1)
(egg-section-cmd-toggle-hide-show nav)
(egg-log-buffer-do-insert-commit pos))))
(define-key egg-file-cmd-map "l" 'egg-file-log)
(define-key egg-file-cmd-map (kbd "C-l") 'egg-log)
(define-key egg-hide-show-map (kbd "SPC") 'egg-section-cmd-toggle-hide-show)
(define-key egg-log-commit-base-map (kbd "SPC") 'egg-log-buffer-hide-show-dwim)
おわりに
今回はChangeSetベースのVCSの扱い方を紹介しました.なんだかんだでこの手のものを扱うには専用のパッケージを使うのが一番便利なのが現状なようです.個人的にはVCかDVCがもっと機能が充実してくれれば嬉しいのですが,そうなるにはまだまだ時間がかかりそうです.