diff options
| author | David Engster | 2014-11-27 18:22:00 +0100 |
|---|---|---|
| committer | David Engster | 2014-11-27 18:22:00 +0100 |
| commit | f42adad94bd8cf4f7a86bdced796bb88ec7e5bb2 (patch) | |
| tree | c7ac01d576897b14c47565e1eb4f7c5359e9cc0f /admin/gitmerge.el | |
| parent | 9ec7bd97faa0085694377426ca4cca8593fa3606 (diff) | |
| download | emacs-f42adad94bd8cf4f7a86bdced796bb88ec7e5bb2.tar.gz emacs-f42adad94bd8cf4f7a86bdced796bb88ec7e5bb2.zip | |
admin: Add gitmerge.el
* gitmerge.el: New file.
Diffstat (limited to 'admin/gitmerge.el')
| -rw-r--r-- | admin/gitmerge.el | 528 |
1 files changed, 528 insertions, 0 deletions
diff --git a/admin/gitmerge.el b/admin/gitmerge.el new file mode 100644 index 00000000000..88c633393a6 --- /dev/null +++ b/admin/gitmerge.el | |||
| @@ -0,0 +1,528 @@ | |||
| 1 | ;;; gitmerge.el --- help merge one Emacs branch into another | ||
| 2 | |||
| 3 | ;; Copyright (C) 2010-2014 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Authors: David Engster <deng@randomsample.de> | ||
| 6 | ;; Stefan Monnier <monnier@iro.umontreal.ca> | ||
| 7 | |||
| 8 | ;; Keywords: maint | ||
| 9 | |||
| 10 | ;; GNU Emacs is free software: you can redistribute it and/or modify | ||
| 11 | ;; it under the terms of the GNU General Public License as published by | ||
| 12 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 13 | ;; (at your option) any later version. | ||
| 14 | |||
| 15 | ;; GNU Emacs is distributed in the hope that it will be useful, | ||
| 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 18 | ;; GNU General Public License for more details. | ||
| 19 | |||
| 20 | ;; You should have received a copy of the GNU General Public License | ||
| 21 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. | ||
| 22 | |||
| 23 | ;;; Commentary: | ||
| 24 | |||
| 25 | ;; Rewrite of bzrmerge.el, but using git. | ||
| 26 | ;; | ||
| 27 | ;; In a nutshell: For merging foo into master, do | ||
| 28 | ;; | ||
| 29 | ;; - 'git checkout master' in Emacs repository | ||
| 30 | ;; - Start Emacs, cd to Emacs repository | ||
| 31 | ;; - M-x gitmerge | ||
| 32 | ;; - Choose branch 'foo' or 'origin/foo', depending on whether you | ||
| 33 | ;; like to merge from a local tracking branch or from the remote | ||
| 34 | ;; (does not make a difference if the local tracking branch is | ||
| 35 | ;; up-to-date). | ||
| 36 | ;; - Mark commits you'd like to skip, meaning to only merge their | ||
| 37 | ;; metadata (merge strategy 'ours'). | ||
| 38 | ;; - Hit 'm' to start merging. Skipped commits will be merged separately. | ||
| 39 | ;; - If conflicts cannot be resolved automatically, you'll have to do | ||
| 40 | ;; it manually. In that case, resolve the conflicts and restart | ||
| 41 | ;; gitmerge, which will automatically resume. It will add resolved | ||
| 42 | ;; files, commit the pending merge and continue merging the rest. | ||
| 43 | ;; - Inspect master branch, and if everything looks OK, push. | ||
| 44 | |||
| 45 | ;;; Code: | ||
| 46 | |||
| 47 | (require 'vc-git) | ||
| 48 | (require 'smerge-mode) | ||
| 49 | |||
| 50 | (defvar gitmerge-skip-regexp | ||
| 51 | "back[- ]?port\\|merge\\|sync\\|re-?generate\\|bump version\\|from trunk\\|\ | ||
| 52 | Auto-commit" | ||
| 53 | "Regexp matching logs of revisions that might be skipped. | ||
| 54 | `gitmerge-missing' will ask you if it should skip any matches.") | ||
| 55 | |||
| 56 | (defvar gitmerge-status-file (expand-file-name "gitmerge-status" | ||
| 57 | user-emacs-directory) | ||
| 58 | "File where missing commits will be saved between sessions.") | ||
| 59 | |||
| 60 | (defvar gitmerge-ignore-branches-regexp | ||
| 61 | "origin/\\(\\(HEAD\\|master\\)$\\|\\(old-branches\\|other-branches\\)/\\)" | ||
| 62 | "Regexp matching branches we want to ignore.") | ||
| 63 | |||
| 64 | (defface gitmerge-skip-face | ||
| 65 | '((t (:strike-through t))) | ||
| 66 | "Face for skipped commits.") | ||
| 67 | |||
| 68 | (defconst gitmerge-default-branch "origin/emacs-24" | ||
| 69 | "Default for branch that should be merged.") | ||
| 70 | |||
| 71 | (defconst gitmerge-buffer "*gitmerge*" | ||
| 72 | "Working buffer for gitmerge.") | ||
| 73 | |||
| 74 | (defconst gitmerge-output-buffer "*gitmerge output*" | ||
| 75 | "Buffer for displaying git output.") | ||
| 76 | |||
| 77 | (defconst gitmerge-warning-buffer "*gitmerge warnings*" | ||
| 78 | "Buffer where gitmerge will display any warnings.") | ||
| 79 | |||
| 80 | (defvar gitmerge-log-regexp | ||
| 81 | "^\\([A-Z ]\\)\\s-*\\([0-9a-f]+\\) \\(.+?\\): \\(.*\\)$") | ||
| 82 | |||
| 83 | (defvar gitmerge-mode-map | ||
| 84 | (let ((map (make-keymap))) | ||
| 85 | (define-key map [(l)] 'gitmerge-show-log) | ||
| 86 | (define-key map [(d)] 'gitmerge-show-diff) | ||
| 87 | (define-key map [(f)] 'gitmerge-show-files) | ||
| 88 | (define-key map [(s)] 'gitmerge-toggle-skip) | ||
| 89 | (define-key map [(m)] 'gitmerge-start-merge) | ||
| 90 | map) | ||
| 91 | "Keymap for gitmerge major mode.") | ||
| 92 | |||
| 93 | (defvar gitmerge--commits nil) | ||
| 94 | (defvar gitmerge--from nil) | ||
| 95 | |||
| 96 | (defun gitmerge-get-sha1 () | ||
| 97 | "Get SHA1 from commit at point." | ||
| 98 | (save-excursion | ||
| 99 | (goto-char (point-at-bol)) | ||
| 100 | (when (looking-at "^[A-Z ]\\s-*\\([a-f0-9]+\\)") | ||
| 101 | (match-string 1)))) | ||
| 102 | |||
| 103 | (defun gitmerge-show-log () | ||
| 104 | "Show log of commit at point." | ||
| 105 | (interactive) | ||
| 106 | (save-selected-window | ||
| 107 | (let ((commit (gitmerge-get-sha1))) | ||
| 108 | (when commit | ||
| 109 | (pop-to-buffer (get-buffer-create gitmerge-output-buffer)) | ||
| 110 | (fundamental-mode) | ||
| 111 | (erase-buffer) | ||
| 112 | (call-process "git" nil t nil "log" "-1" commit) | ||
| 113 | (goto-char (point-min)) | ||
| 114 | (gitmerge-highlight-skip-regexp))))) | ||
| 115 | |||
| 116 | (defun gitmerge-show-diff () | ||
| 117 | "Show diff of commit at point." | ||
| 118 | (interactive) | ||
| 119 | (save-selected-window | ||
| 120 | (let ((commit (gitmerge-get-sha1))) | ||
| 121 | (when commit | ||
| 122 | (pop-to-buffer (get-buffer-create gitmerge-output-buffer)) | ||
| 123 | (erase-buffer) | ||
| 124 | (call-process "git" nil t nil "diff-tree" "-p" commit) | ||
| 125 | (goto-char (point-min)) | ||
| 126 | (diff-mode))))) | ||
| 127 | |||
| 128 | (defun gitmerge-show-files () | ||
| 129 | "Show changed files of commit at point." | ||
| 130 | (interactive) | ||
| 131 | (save-selected-window | ||
| 132 | (let ((commit (gitmerge-get-sha1))) | ||
| 133 | (when commit | ||
| 134 | (pop-to-buffer (get-buffer-create gitmerge-output-buffer)) | ||
| 135 | (erase-buffer) | ||
| 136 | (fundamental-mode) | ||
| 137 | (call-process "git" nil t nil "diff" "--name-only" (concat commit "^!")) | ||
| 138 | (goto-char (point-min)))))) | ||
| 139 | |||
| 140 | (defun gitmerge-toggle-skip () | ||
| 141 | "Toggle skipping of commit at point." | ||
| 142 | (interactive) | ||
| 143 | (let ((commit (gitmerge-get-sha1)) | ||
| 144 | skip) | ||
| 145 | (when commit | ||
| 146 | (save-excursion | ||
| 147 | (goto-char (point-at-bol)) | ||
| 148 | (when (looking-at "^\\([A-Z ]\\)\\s-*\\([a-f0-9]+\\)") | ||
| 149 | (setq skip (string= (match-string 1) " ")) | ||
| 150 | (goto-char (match-beginning 2)) | ||
| 151 | (gitmerge-handle-skip-overlay skip) | ||
| 152 | (dolist (ct gitmerge--commits) | ||
| 153 | (when (string-match commit (car ct)) | ||
| 154 | (setcdr ct (when skip "M")))) | ||
| 155 | (goto-char (point-at-bol)) | ||
| 156 | (setq buffer-read-only nil) | ||
| 157 | (delete-char 1) | ||
| 158 | (insert (if skip "M" " ")) | ||
| 159 | (setq buffer-read-only t)))))) | ||
| 160 | |||
| 161 | (defun gitmerge-highlight-skip-regexp () | ||
| 162 | "Highlight strings that match `gitmerge-skip-regexp'." | ||
| 163 | (save-excursion | ||
| 164 | (while (re-search-forward gitmerge-skip-regexp nil t) | ||
| 165 | (put-text-property (match-beginning 0) (match-end 0) | ||
| 166 | 'face 'font-lock-warning-face)))) | ||
| 167 | |||
| 168 | (defun gitmerge-missing (from) | ||
| 169 | "Return the list of revisions that need to be merged from FROM. | ||
| 170 | Will detect a default set of skipped revision by looking at | ||
| 171 | cherry mark and search for `gitmerge-skip-regexp'. The result is | ||
| 172 | a list with entries of the form (SHA1 . SKIP), where SKIP denotes | ||
| 173 | if and why this commit should be skipped." | ||
| 174 | (let (commits) | ||
| 175 | ;; Go through the log and remember all commits that match | ||
| 176 | ;; `gitmerge-skip-regexp' or are marked by --cherry-mark. | ||
| 177 | (with-temp-buffer | ||
| 178 | (call-process "git" nil t nil "log" "--cherry-mark" from | ||
| 179 | (concat "^" (car (vc-git-branches)))) | ||
| 180 | (goto-char (point-max)) | ||
| 181 | (while (re-search-backward "^commit \\(.+\\) \\([0-9a-f]+\\).*" nil t) | ||
| 182 | (let ((cherrymark (match-string 1)) | ||
| 183 | (commit (match-string 2))) | ||
| 184 | (push (list commit) commits) | ||
| 185 | (if (string= cherrymark "=") | ||
| 186 | ;; Commit was recognized as backported by cherry-mark. | ||
| 187 | (setcdr (car commits) "C") | ||
| 188 | (save-excursion | ||
| 189 | (let ((case-fold-search t)) | ||
| 190 | (while (not (looking-at "^\\s-+[^ ]+")) | ||
| 191 | (forward-line)) | ||
| 192 | (when (re-search-forward gitmerge-skip-regexp nil t) | ||
| 193 | (setcdr (car commits) "R")))))) | ||
| 194 | (delete-region (point) (point-max)))) | ||
| 195 | (nreverse commits))) | ||
| 196 | |||
| 197 | (defun gitmerge-setup-log-buffer (commits from) | ||
| 198 | "Create the buffer for choosing commits." | ||
| 199 | (with-current-buffer (get-buffer-create gitmerge-buffer) | ||
| 200 | (erase-buffer) | ||
| 201 | (call-process "git" nil t nil "log" | ||
| 202 | "--pretty=format:%h %<(20,trunc) %an: %<(100,trunc) %s" | ||
| 203 | from (concat "^" (car (vc-git-branches)))) | ||
| 204 | (goto-char (point-min)) | ||
| 205 | (while (looking-at "^\\([a-f0-9]+\\)") | ||
| 206 | (let ((skipreason (gitmerge-skip-commit-p (match-string 1) commits))) | ||
| 207 | (if (null skipreason) | ||
| 208 | (insert " ") | ||
| 209 | (insert skipreason " ") | ||
| 210 | (gitmerge-handle-skip-overlay t))) | ||
| 211 | (forward-line)) | ||
| 212 | (current-buffer))) | ||
| 213 | |||
| 214 | (defun gitmerge-handle-skip-overlay (skip) | ||
| 215 | "Create or delete overlay on SHA1, depending on SKIP." | ||
| 216 | (when (looking-at "[0-9a-f]+") | ||
| 217 | (if skip | ||
| 218 | (let ((ov (make-overlay (point) | ||
| 219 | (match-end 0)))) | ||
| 220 | (overlay-put ov 'face 'gitmerge-skip-face)) | ||
| 221 | (remove-overlays (point) (match-end 0) | ||
| 222 | 'face 'gitmerge-skip-face)))) | ||
| 223 | |||
| 224 | (defun gitmerge-skip-commit-p (commit skips) | ||
| 225 | "Tell whether COMMIT should be skipped. | ||
| 226 | COMMIT is an (possibly abbreviated) SHA1. SKIPS is list of | ||
| 227 | cons'es with commits that should be skipped and the reason. | ||
| 228 | Return value is string which denotes reason, or nil if commit | ||
| 229 | should not be skipped." | ||
| 230 | (let (found skip) | ||
| 231 | (while (and (setq skip (pop skips)) | ||
| 232 | (not found)) | ||
| 233 | (when (string-match commit (car skip)) | ||
| 234 | (setq found (cdr skip)))) | ||
| 235 | found)) | ||
| 236 | |||
| 237 | (defun gitmerge-resolve (file) | ||
| 238 | "Try to resolve conflicts in FILE with smerge. | ||
| 239 | Returns non-nil if conflicts remain." | ||
| 240 | (unless (file-exists-p file) (error "Gitmerge-resolve: Can't find %s" file)) | ||
| 241 | (with-demoted-errors | ||
| 242 | (let ((exists (find-buffer-visiting file))) | ||
| 243 | (with-current-buffer (let ((enable-local-variables :safe) | ||
| 244 | (enable-local-eval nil)) | ||
| 245 | (find-file-noselect file)) | ||
| 246 | (if (buffer-modified-p) | ||
| 247 | (user-error "Unsaved changes in %s" (current-buffer))) | ||
| 248 | (save-excursion | ||
| 249 | (cond | ||
| 250 | ((derived-mode-p 'change-log-mode) | ||
| 251 | ;; Fix up dates before resolving the conflicts. | ||
| 252 | (goto-char (point-min)) | ||
| 253 | (let ((diff-auto-refine-mode nil)) | ||
| 254 | (while (re-search-forward smerge-begin-re nil t) | ||
| 255 | (smerge-match-conflict) | ||
| 256 | (smerge-ensure-match 3) | ||
| 257 | (let ((start1 (match-beginning 1)) | ||
| 258 | (end1 (match-end 1)) | ||
| 259 | (start3 (match-beginning 3)) | ||
| 260 | (end3 (copy-marker (match-end 3) t))) | ||
| 261 | (goto-char start3) | ||
| 262 | (while (re-search-forward change-log-start-entry-re end3 t) | ||
| 263 | (let* ((str (match-string 0)) | ||
| 264 | (newstr (save-match-data | ||
| 265 | (concat (add-log-iso8601-time-string) | ||
| 266 | (when (string-match " *\\'" str) | ||
| 267 | (match-string 0 str)))))) | ||
| 268 | (replace-match newstr t t))) | ||
| 269 | ;; change-log-resolve-conflict prefers to put match-1's | ||
| 270 | ;; elements first (for equal dates), whereas we want to put | ||
| 271 | ;; match-3's first. | ||
| 272 | (let ((match3 (buffer-substring start3 end3)) | ||
| 273 | (match1 (buffer-substring start1 end1))) | ||
| 274 | (delete-region start3 end3) | ||
| 275 | (goto-char start3) | ||
| 276 | (insert match1) | ||
| 277 | (delete-region start1 end1) | ||
| 278 | (goto-char start1) | ||
| 279 | (insert match3))))) | ||
| 280 | ;; (pop-to-buffer (current-buffer)) (debug 'before-resolve) | ||
| 281 | )) | ||
| 282 | ;; Try to resolve the conflicts. | ||
| 283 | (cond | ||
| 284 | ((member file '("configure" "lisp/ldefs-boot.el" | ||
| 285 | "lisp/emacs-lisp/cl-loaddefs.el")) | ||
| 286 | ;; We are in the file's buffer, so names are relative. | ||
| 287 | (call-process "git" nil t nil "checkout" "--" | ||
| 288 | (file-name-nondirectory file)) | ||
| 289 | (revert-buffer nil 'noconfirm)) | ||
| 290 | (t | ||
| 291 | (goto-char (point-max)) | ||
| 292 | (while (re-search-backward smerge-begin-re nil t) | ||
| 293 | (save-excursion | ||
| 294 | (ignore-errors | ||
| 295 | (smerge-match-conflict) | ||
| 296 | (smerge-resolve)))) | ||
| 297 | ;; (when (derived-mode-p 'change-log-mode) | ||
| 298 | ;; (pop-to-buffer (current-buffer)) (debug 'after-resolve)) | ||
| 299 | (save-buffer))) | ||
| 300 | (goto-char (point-min)) | ||
| 301 | (prog1 (re-search-forward smerge-begin-re nil t) | ||
| 302 | (unless exists (kill-buffer)))))))) | ||
| 303 | |||
| 304 | (defun gitmerge-commit-message (beg end skip branch) | ||
| 305 | "Create commit message for merging BEG to END from BRANCH. | ||
| 306 | SKIP denotes whether those commits are actually skipped. If END | ||
| 307 | is nil, only the single commit BEG is merged." | ||
| 308 | (with-temp-buffer | ||
| 309 | (insert "Merge from " branch "\n\n" | ||
| 310 | (if skip | ||
| 311 | (concat "The following commit" | ||
| 312 | (if end "s were " " was ") | ||
| 313 | "skipped:\n\n") | ||
| 314 | "")) | ||
| 315 | (apply 'call-process "git" nil t nil "log" "--oneline" | ||
| 316 | (if end (list (concat beg "~.." end)) | ||
| 317 | `("-1" ,beg))) | ||
| 318 | (insert "\n") | ||
| 319 | (buffer-string))) | ||
| 320 | |||
| 321 | (defun gitmerge-apply (missing from) | ||
| 322 | "Merge commits in MISSING from branch FROM. | ||
| 323 | MISSING must be a list of SHA1 strings." | ||
| 324 | (with-current-buffer (get-buffer-create gitmerge-output-buffer) | ||
| 325 | (erase-buffer) | ||
| 326 | (let* ((skip (cdar missing)) | ||
| 327 | (beg (car (pop missing))) | ||
| 328 | end commitmessage) | ||
| 329 | ;; Determine last revision with same boolean skip status. | ||
| 330 | (while (and missing | ||
| 331 | (eq (null (cdar missing)) | ||
| 332 | (null skip))) | ||
| 333 | (setq end (car (pop missing)))) | ||
| 334 | (setq commitmessage | ||
| 335 | (gitmerge-commit-message beg end skip from)) | ||
| 336 | (message "%s %s%s" | ||
| 337 | (if skip "Skipping" "Merging") | ||
| 338 | (substring beg 0 6) | ||
| 339 | (if end (concat ".." (substring end 0 6)) "")) | ||
| 340 | (unless end | ||
| 341 | (setq end beg)) | ||
| 342 | (unless (zerop | ||
| 343 | (apply 'call-process "git" nil t nil "merge" "--no-ff" | ||
| 344 | (append (when skip '("-s" "ours")) | ||
| 345 | `("-m" ,commitmessage ,end)))) | ||
| 346 | (gitmerge-write-missing missing from) | ||
| 347 | (gitmerge-resolve-unmerged))) | ||
| 348 | missing)) | ||
| 349 | |||
| 350 | (defun gitmerge-resolve-unmerged () | ||
| 351 | "Resolve all files that are unmerged. | ||
| 352 | Throw an user-error if we cannot resolve automatically." | ||
| 353 | (with-current-buffer (get-buffer-create gitmerge-output-buffer) | ||
| 354 | (erase-buffer) | ||
| 355 | (let (files conflicted) | ||
| 356 | ;; List unmerged files | ||
| 357 | (if (not (zerop | ||
| 358 | (call-process "git" nil t nil | ||
| 359 | "diff" "--name-only" "--diff-filter=U"))) | ||
| 360 | (error "Error listing unmerged files. Resolve manually.") | ||
| 361 | (goto-char (point-min)) | ||
| 362 | (while (not (eobp)) | ||
| 363 | (push (buffer-substring (point) (line-end-position)) files) | ||
| 364 | (forward-line)) | ||
| 365 | (dolist (file files) | ||
| 366 | (if (gitmerge-resolve file) | ||
| 367 | ;; File still has conflicts | ||
| 368 | (setq conflicted t) | ||
| 369 | ;; Mark as resolved | ||
| 370 | (call-process "git" nil t nil "add" file))) | ||
| 371 | (when conflicted | ||
| 372 | (with-current-buffer (get-buffer-create gitmerge-warning-buffer) | ||
| 373 | (erase-buffer) | ||
| 374 | (insert "For the following files, conflicts could\n" | ||
| 375 | "not be resolved automatically:\n\n") | ||
| 376 | (call-process "git" nil t nil | ||
| 377 | "diff" "--name-only" "--diff-filter=U") | ||
| 378 | (insert "\nResolve the conflicts manually, then run gitmerge again." | ||
| 379 | "\nNote:\n - You don't have to add resolved files or " | ||
| 380 | "commit the merge yourself (but you can)." | ||
| 381 | "\n - You can safely close this Emacs session and do this " | ||
| 382 | "in a new one." | ||
| 383 | "\n - When running gitmerge again, remember that you must " | ||
| 384 | "that from within the Emacs repo.\n") | ||
| 385 | (pop-to-buffer (current-buffer))) | ||
| 386 | (user-error "Resolve the conflicts manually")))))) | ||
| 387 | |||
| 388 | (defun gitmerge-repo-clean () | ||
| 389 | "Return non-nil if repository is clean." | ||
| 390 | (with-temp-buffer | ||
| 391 | (call-process "git" nil t nil | ||
| 392 | "diff" "--staged" "--name-only") | ||
| 393 | (call-process "git" nil t nil | ||
| 394 | "diff" "--name-only") | ||
| 395 | (zerop (buffer-size)))) | ||
| 396 | |||
| 397 | (defun gitmerge-maybe-resume () | ||
| 398 | "Check if we have to resume a merge. | ||
| 399 | If so, add no longer conflicted files and commit." | ||
| 400 | (let ((mergehead (file-exists-p | ||
| 401 | (expand-file-name ".git/MERGE_HEAD" default-directory))) | ||
| 402 | (statusexist (file-exists-p gitmerge-status-file))) | ||
| 403 | (when (and mergehead (not statusexist)) | ||
| 404 | (user-error "Unfinished merge, but no record of a previous gitmerge run")) | ||
| 405 | (when (and (not mergehead) | ||
| 406 | (not (gitmerge-repo-clean))) | ||
| 407 | (user-error "Repository is not clean")) | ||
| 408 | (when statusexist | ||
| 409 | (if (not (y-or-n-p "Resume merge? ")) | ||
| 410 | (progn | ||
| 411 | (delete-file gitmerge-status-file) | ||
| 412 | ;; No resume. | ||
| 413 | nil) | ||
| 414 | (message "OK, resuming...") | ||
| 415 | (gitmerge-resolve-unmerged) | ||
| 416 | ;; Commit the merge. | ||
| 417 | (when mergehead | ||
| 418 | (with-current-buffer (get-buffer-create gitmerge-output-buffer) | ||
| 419 | (erase-buffer) | ||
| 420 | (unless (zerop (call-process "git" nil t nil | ||
| 421 | "commit" "--no-edit")) | ||
| 422 | (error "Git error during merge - fix it manually")))) | ||
| 423 | ;; Sucesfully resumed. | ||
| 424 | t)))) | ||
| 425 | |||
| 426 | (defun gitmerge-get-all-branches () | ||
| 427 | "Return list of all branches, including remotes." | ||
| 428 | (with-temp-buffer | ||
| 429 | (unless (zerop (call-process "git" nil t nil | ||
| 430 | "branch" "-a")) | ||
| 431 | (error "Git error listing remote branches")) | ||
| 432 | (goto-char (point-min)) | ||
| 433 | (let (branches branch) | ||
| 434 | (while (not (eobp)) | ||
| 435 | (when (looking-at "^[^\\*]\\s-*\\(?:remotes/\\)?\\(.+\\)$") | ||
| 436 | (setq branch (match-string 1)) | ||
| 437 | (unless (string-match gitmerge-ignore-branches-regexp branch) | ||
| 438 | (push branch branches))) | ||
| 439 | (forward-line)) | ||
| 440 | (nreverse branches)))) | ||
| 441 | |||
| 442 | (defun gitmerge-write-missing (missing from) | ||
| 443 | "Write list of commits MISSING into `gitmerge-status-file'. | ||
| 444 | Branch FROM will be prepended to the list." | ||
| 445 | (with-current-buffer | ||
| 446 | (find-file-noselect gitmerge-status-file) | ||
| 447 | (erase-buffer) | ||
| 448 | (insert | ||
| 449 | (prin1-to-string (append (list from) missing)) | ||
| 450 | "\n") | ||
| 451 | (save-buffer) | ||
| 452 | (kill-buffer))) | ||
| 453 | |||
| 454 | (defun gitmerge-read-missing () | ||
| 455 | "Read list of missing commits from `gitmerge-status-file'." | ||
| 456 | (with-current-buffer | ||
| 457 | (find-file-noselect gitmerge-status-file) | ||
| 458 | (unless (zerop (buffer-size)) | ||
| 459 | (prog1 (read (buffer-string)) | ||
| 460 | (kill-buffer))))) | ||
| 461 | |||
| 462 | (defun gitmerge-mode () | ||
| 463 | "Major mode for Emacs branch merging." | ||
| 464 | (interactive) | ||
| 465 | (kill-all-local-variables) | ||
| 466 | (setq major-mode 'gitmerge-mode) | ||
| 467 | (setq mode-name "gitmerge") | ||
| 468 | (set-syntax-table text-mode-syntax-table) | ||
| 469 | (use-local-map gitmerge-mode-map) | ||
| 470 | (make-local-variable 'font-lock-defaults) | ||
| 471 | (setq gitmerge-mode-font-lock-keywords | ||
| 472 | (list (list gitmerge-log-regexp | ||
| 473 | '(1 font-lock-warning-face) | ||
| 474 | '(2 font-lock-constant-face) | ||
| 475 | '(3 font-lock-builtin-face) | ||
| 476 | '(4 font-lock-comment-face)))) | ||
| 477 | (setq buffer-read-only t) | ||
| 478 | (setq font-lock-defaults '(gitmerge-mode-font-lock-keywords))) | ||
| 479 | |||
| 480 | (defun gitmerge (from) | ||
| 481 | "Merge from branch FROM into `default-directory'." | ||
| 482 | (interactive | ||
| 483 | (if (not (vc-git-root default-directory)) | ||
| 484 | (user-error "Not in a git tree") | ||
| 485 | (let ((default-directory (vc-git-root default-directory))) | ||
| 486 | (list | ||
| 487 | (if (gitmerge-maybe-resume) | ||
| 488 | 'resume | ||
| 489 | (completing-read "Merge branch: " (gitmerge-get-all-branches) | ||
| 490 | nil t gitmerge-default-branch)))))) | ||
| 491 | (let ((default-directory (vc-git-root default-directory))) | ||
| 492 | (if (eq from 'resume) | ||
| 493 | (progn | ||
| 494 | (setq gitmerge--commits (gitmerge-read-missing)) | ||
| 495 | (setq gitmerge--from (pop gitmerge--commits)) | ||
| 496 | ;; Directly continue with the merge. | ||
| 497 | (gitmerge-start-merge)) | ||
| 498 | (setq gitmerge--commits (gitmerge-missing from)) | ||
| 499 | (setq gitmerge--from from) | ||
| 500 | (when (null gitmerge--commits) | ||
| 501 | (user-error "Nothing to merge")) | ||
| 502 | (with-current-buffer | ||
| 503 | (gitmerge-setup-log-buffer gitmerge--commits gitmerge--from) | ||
| 504 | (goto-char (point-min)) | ||
| 505 | (insert (propertize "Commands: " 'font-lock-face 'bold) | ||
| 506 | "(s) Toggle skip, (l) Show log, (d) Show diff, " | ||
| 507 | "(f) Show files, (m) Start merge\n" | ||
| 508 | (propertize "Flags: " 'font-lock-face 'bold) | ||
| 509 | "(C) Detected backport (cherry-mark), (R) Log matches " | ||
| 510 | "regexp, (M) Manually picked\n\n") | ||
| 511 | (gitmerge-mode) | ||
| 512 | (pop-to-buffer (current-buffer)))))) | ||
| 513 | |||
| 514 | (defun gitmerge-start-merge () | ||
| 515 | (interactive) | ||
| 516 | (when (not (vc-git-root default-directory)) | ||
| 517 | (user-error "Not in a git tree")) | ||
| 518 | (let ((default-directory (vc-git-root default-directory))) | ||
| 519 | (while gitmerge--commits | ||
| 520 | (setq gitmerge--commits | ||
| 521 | (gitmerge-apply gitmerge--commits gitmerge--from))) | ||
| 522 | (when (file-exists-p gitmerge-status-file) | ||
| 523 | (delete-file gitmerge-status-file)) | ||
| 524 | (message "Merging from %s...done" gitmerge--from))) | ||
| 525 | |||
| 526 | (provide 'gitmerge) | ||
| 527 | |||
| 528 | ;;; gitmerge.el ends here | ||