├── README.md └── magit-svn.el /README.md: -------------------------------------------------------------------------------- 1 | # Unmaintained 2 | 3 | I never used this package myself and no longer want to maintain it. 4 | I might still fix bugs but all feature requests will be closed. 5 | Contact me if you are interested in becoming the new maintainer. 6 | 7 | # `git svn` extension for Magit 8 | 9 | This package provides basic support for `git svn`, a Git command 10 | that aims to provide bidirectional operation between a Subversion 11 | repository and Git. See its manual page `git-svn(1)` for more 12 | information. 13 | 14 | When `magit-svn-mode` is turned on then the unpushed and unpulled 15 | commit relative to the Subversion repository are displayed in the 16 | status buffer, and `N` is bound to a popup with commands that wrap 17 | the `git svn` subcommands `fetch`, `rebase`, `dcommit`, `branch` 18 | and `tag`, as well as a few extras. 19 | 20 | To enable the mode in a particular repository use: 21 | 22 | cd /path/to/repository 23 | git config --add magit.extension svn 24 | 25 | To enable the mode for all repositories use: 26 | 27 | git config --global --add magit.extension svn 28 | 29 | To enable the mode globally without dropping to a shell: 30 | 31 | (add-hook 'magit-mode-hook 'magit-svn-mode) 32 | 33 | If you are looking for native SVN support in Emacs, then have a 34 | look at `psvn.el` and info node `(emacs)Version Control`. 35 | -------------------------------------------------------------------------------- /magit-svn.el: -------------------------------------------------------------------------------- 1 | ;;; magit-svn.el --- Git-Svn extension for Magit -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2010-2025 The Magit Project Contributors 4 | 5 | ;; Author: Phil Jackson 6 | ;; Keywords: vc tools 7 | ;; Package: magit-svn 8 | ;; Package-Requires: ((emacs "27.1") (dash "2.19.1") (magit "4.3.0") (transient "0.8.4")) 9 | ;; SPDX-License-Identifier: GPL-3.0-or-later 10 | 11 | ;; Magit is free software; you can redistribute it and/or modify it 12 | ;; under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation; either version 3, or (at your option) 14 | ;; any later version. 15 | ;; 16 | ;; Magit is distributed in the hope that it will be useful, but WITHOUT 17 | ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 18 | ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 19 | ;; License for more details. 20 | ;; 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with Magit. If not, see http://www.gnu.org/licenses. 23 | 24 | ;;; Commentary: 25 | 26 | ;; This package provides basic support for Git-Svn. 27 | ;; 28 | ;; Git-Svn is a Git command that aims to provide bidirectional 29 | ;; operation between a Subversion repository and Git. 30 | ;; 31 | ;; For information about Git-Svn see its manual page `git-svn(1)'. 32 | ;; 33 | ;; If you are looking for native SVN support in Emacs, then have a 34 | ;; look at `psvn.el' and info node `(emacs)Version Control'. 35 | 36 | ;; When `magit-svn-mode' is turned on then the unpushed and unpulled 37 | ;; commit relative to the Subversion repository are displayed in the 38 | ;; status buffer, and "N" is bound to a transient command with 39 | ;; suffixes that wrap the `git-svn' subcommands fetch, rebase, 40 | ;; dcommit, branch and tag, as well as a few extras. 41 | 42 | ;; To enable the mode in a particular repository use: 43 | ;; 44 | ;; cd /path/to/repository 45 | ;; git config --add magit.extension svn 46 | ;; 47 | ;; To enable the mode for all repositories use: 48 | ;; 49 | ;; git config --global --add magit.extension svn 50 | ;; 51 | ;; To enable the mode globally without dropping to a shell: 52 | ;; 53 | ;; (add-hook 'magit-mode-hook 'magit-svn-mode) 54 | 55 | ;; This package is unmaintained see the README for more information. 56 | 57 | ;;; Code: 58 | 59 | (require 'cl-lib) 60 | (require 'dash) 61 | 62 | (eval-when-compile 63 | (require 'subr-x)) 64 | 65 | (require 'transient) 66 | 67 | (require 'magit) 68 | 69 | (declare-function find-lisp-find-files-internal 'find-lisp) 70 | 71 | ;;; Options 72 | 73 | (defgroup magit-svn nil 74 | "Git-Svn support for Magit." 75 | :group 'magit-extensions) 76 | 77 | (defcustom magit-svn-externals-dir ".git_externals" 78 | "Directory from repository root that stores cloned SVN externals." 79 | :group 'magit-svn 80 | :type 'string) 81 | 82 | (defcustom magit-svn-mode-lighter " Msvn" 83 | "Mode-line lighter for Magit-Svn mode." 84 | :group 'magit-svn 85 | :type 'string) 86 | 87 | ;;; Utilities 88 | 89 | (defun magit-svn-get-url () 90 | (magit-git-string "svn" "info" "--url")) 91 | 92 | (defun magit-svn-get-rev () 93 | (--when-let (--first (string-match "^Revision: \\(.+\\)" it) 94 | (magit-git-lines "svn" "info")) 95 | (match-string 1 it))) 96 | 97 | ;;;; magit-svn-get-ref 98 | 99 | (defun magit-svn-expand-braces-in-branches (branch) 100 | (if (not (string-match "\\(.+\\){\\(.+,.+\\)}\\(.*\\):\\(.*\\)\\\*" branch)) 101 | (list branch) 102 | (let ((prefix (match-string 1 branch)) 103 | (suffix (match-string 3 branch)) 104 | (rhs (match-string 4 branch)) 105 | (pieces (split-string (match-string 2 branch) ","))) 106 | (mapcar (lambda (p) (concat prefix p suffix ":" rhs p)) pieces)))) 107 | 108 | (defun magit-svn-get-local-ref (url) 109 | (let* ((branches (cons (magit-get "svn-remote" "svn" "fetch") 110 | (magit-get-all "svn-remote" "svn" "branches"))) 111 | (branches (apply 'nconc 112 | (mapcar 'magit-svn-expand-braces-in-branches 113 | branches))) 114 | (base-url (magit-get "svn-remote" "svn" "url")) 115 | (result nil)) 116 | (while branches 117 | (let* ((pats (split-string (pop branches) ":")) 118 | (src (replace-regexp-in-string "\\*" "\\\\(.*\\\\)" (car pats))) 119 | (dst (replace-regexp-in-string "\\*" "\\\\1" (cadr pats))) 120 | (base-url (replace-regexp-in-string "\\+" "\\\\+" base-url)) 121 | (base-url (replace-regexp-in-string "//.+@" "//" base-url)) 122 | (pat1 (concat "^" src "$")) 123 | (pat2 (cond ((equal src "") (concat "^" base-url "$")) 124 | (t (concat "^" base-url "/" src "$"))))) 125 | (cond ((string-match pat1 url) 126 | (setq result (replace-match dst nil nil url)) 127 | (setq branches nil)) 128 | ((string-match pat2 url) 129 | (setq result (replace-match dst nil nil url)) 130 | (setq branches nil))))) 131 | result)) 132 | 133 | (defvar magit-svn-get-ref-info-cache nil 134 | "A cache for svn-ref-info. 135 | As `magit-get-svn-ref-info' might be considered a quite 136 | expensive operation a cache is taken so that `magit-status' 137 | doesn't repeatedly call it.") 138 | 139 | (defun magit-svn-get-ref-info (&optional use-cache) 140 | "Gather details about the current git-svn repository. 141 | Return nil if there isn't one. Keys of the alist are ref-path, 142 | trunk-ref-name and local-ref-name. 143 | If USE-CACHE is non-nil then return the value of 144 | `magit-get-svn-ref-info-cache'." 145 | (if (and use-cache magit-svn-get-ref-info-cache) 146 | magit-svn-get-ref-info-cache 147 | (let* ((fetch (magit-get "svn-remote" "svn" "fetch")) 148 | (url) 149 | (revision)) 150 | (when fetch 151 | (let* ((ref (cadr (split-string fetch ":"))) 152 | (ref-path (file-name-directory ref)) 153 | (trunk-ref-name (file-name-nondirectory ref))) 154 | (setq-local magit-svn-get-ref-info-cache 155 | (list 156 | (cons 'ref-path ref-path) 157 | (cons 'trunk-ref-name trunk-ref-name) 158 | ;; get the local ref from the log. This is actually 159 | ;; the way that git-svn does it. 160 | (cons 'local-ref 161 | (with-temp-buffer 162 | (magit-git-insert "log" "-1" "--first-parent" 163 | "--grep" "git-svn") 164 | (goto-char (point-min)) 165 | (cond ((re-search-forward 166 | "git-svn-id: \\(.+/.+?\\)@\\([0-9]+\\)" nil t) 167 | (setq url (match-string 1) 168 | revision (match-string 2)) 169 | (magit-svn-get-local-ref url)) 170 | (t 171 | (setq url (magit-get "svn-remote" "svn" "url")) 172 | nil)))) 173 | (cons 'revision revision) 174 | (cons 'url url)))))))) 175 | 176 | (defun magit-svn-get-ref (&optional use-cache) 177 | "Get the best guess remote ref for the current git-svn based branch. 178 | If USE-CACHE is non-nil, use the cached information." 179 | (cdr (assoc 'local-ref (magit-svn-get-ref-info use-cache)))) 180 | 181 | ;;; Commands 182 | 183 | (transient-define-prefix magit-svn () 184 | "Invoke `git-svn' commands." 185 | :man-page "git-svn" 186 | ["Arguments" 187 | ("-n" "Dry run" "--dry-run")] 188 | ["Actions" 189 | ("c" "DCommit" magit-svn-dcommit) 190 | ("r" "Rebase" magit-svn-rebase) 191 | ("f" "Fetch" magit-svn-fetch) 192 | ("x" "Fetch Externals" magit-svn-fetch-externals) 193 | ("s" "Show commit" magit-svn-show-commit) 194 | ("b" "Create branch" magit-svn-create-branch) 195 | ("t" "Create tag" magit-svn-create-tag)]) 196 | 197 | ;;;###autoload 198 | (defun magit-svn-show-commit (rev &optional branch) 199 | "Show the Git commit for a Svn revision read from the user. 200 | With a prefix argument also read a branch to search in." 201 | (interactive (list (read-number "SVN revision: ") 202 | (and current-prefix-arg 203 | (magit-read-local-branch "In branch")))) 204 | (--if-let (magit-git-string "svn" "find-rev" (format "r%i" rev) branch) 205 | (magit-show-commit it) 206 | (user-error "Revision r%s could not be mapped to a commit" rev))) 207 | 208 | ;;;###autoload 209 | (defun magit-svn-create-branch (name &optional args) 210 | "Create svn branch NAME. 211 | \n(git svn branch [--dry-run] NAME)" 212 | (interactive (list (read-string "Branch name: ") 213 | (transient-args 'magit-svn))) 214 | (magit-run-git "svn" "branch" args name)) 215 | 216 | ;;;###autoload 217 | (defun magit-svn-create-tag (name &optional args) 218 | "Create svn tag NAME. 219 | \n(git svn tag [--dry-run] NAME)" 220 | (interactive (list (read-string "Tag name: ") 221 | (transient-args 'magit-svn))) 222 | (magit-run-git "svn" "tag" args name)) 223 | 224 | ;;;###autoload 225 | (defun magit-svn-rebase (&optional args) 226 | "Fetch revisions from Svn and rebase the current Git commits. 227 | \n(git svn rebase [--dry-run])" 228 | (interactive (list (transient-args 'magit-svn))) 229 | (magit-run-git-async "svn" "rebase" args)) 230 | 231 | ;;;###autoload 232 | (defun magit-svn-dcommit (&optional args) 233 | "Run git-svn dcommit. 234 | \n(git svn dcommit [--dry-run])" 235 | (interactive (list (transient-args 'magit-svn))) 236 | (magit-run-git-async "svn" "dcommit" args)) 237 | 238 | ;;;###autoload 239 | (defun magit-svn-fetch () 240 | "Fetch revisions from Svn updating the tracking branches. 241 | \n(git svn fetch)" 242 | (interactive) 243 | (magit-run-git-async "svn" "fetch")) 244 | 245 | ;;;###autoload 246 | (defun magit-svn-fetch-externals() 247 | "Fetch and rebase all external repositories. 248 | Loops through all external repositories found 249 | in `magit-svn-external-directories' and runs 250 | `git svn rebase' on each of them." 251 | (interactive) 252 | (require 'find-lisp) 253 | (--if-let (find-lisp-find-files-internal 254 | (expand-file-name magit-svn-externals-dir) 255 | (lambda (file _dir) 256 | (string-equal file ".git")) 257 | 'find-lisp-default-directory-predicate) 258 | (dolist (external it) 259 | (let ((default-directory (file-name-directory external))) 260 | (magit-run-git "svn" "rebase"))) 261 | (user-error "No SVN Externals found. Check magit-svn-externals-dir")) 262 | (magit-refresh)) 263 | 264 | ;;; Mode 265 | 266 | (defvar magit-svn-mode-map 267 | (let ((map (make-sparse-keymap))) 268 | (define-key map (kbd "N") 'magit-svn) 269 | map)) 270 | 271 | ;;;###autoload 272 | (define-minor-mode magit-svn-mode 273 | "Git-Svn support for Magit." 274 | :lighter magit-svn-mode-lighter 275 | :keymap magit-svn-mode-map 276 | (unless (derived-mode-p 'magit-mode) 277 | (user-error "This mode only makes sense with Magit")) 278 | (cond 279 | (magit-svn-mode 280 | (magit-add-section-hook 'magit-status-sections-hook 281 | 'magit-insert-svn-unpulled 282 | 'magit-insert-unpulled-commits t t) 283 | (magit-add-section-hook 'magit-status-sections-hook 284 | 'magit-insert-svn-unpushed 285 | 'magit-insert-unpushed-commits t t) 286 | (magit-add-section-hook 'magit-status-headers-hook 287 | 'magit-insert-svn-remote nil t t)) 288 | (t 289 | (remove-hook 'magit-status-sections-hook 'magit-insert-svn-unpulled t) 290 | (remove-hook 'magit-status-sections-hook 'magit-insert-svn-unpushed t) 291 | (remove-hook 'magit-status-headers-hook 'magit-insert-svn-remote t))) 292 | (when (called-interactively-p 'any) 293 | (magit-refresh))) 294 | 295 | ;;;###autoload 296 | (custom-add-option 'magit-mode-hook #'magit-svn-mode) 297 | 298 | (easy-menu-define magit-svn-mode-menu nil "Magit-Svn mode menu" 299 | '("Git-Svn" 300 | :visible magit-svn-mode 301 | :active (lambda () (magit-get "svn-remote" "svn" "fetch")) 302 | ["Dcommit" magit-svn-dcommit] 303 | ["Rebase" magit-svn-rebase] 304 | ["Fetch" magit-svn-fetch] 305 | ["Fetch Externals" magit-svn-fetch-externals] 306 | ["Show commit" magit-svn-show-commit] 307 | ["Create branch" magit-svn-create-branch] 308 | ["Create tag" magit-svn-create-tag])) 309 | 310 | (easy-menu-add-item 'magit-mode-menu '("Extensions") magit-svn-mode-menu) 311 | 312 | ;;; Sections 313 | 314 | (defun magit-insert-svn-unpulled () 315 | (--when-let (magit-svn-get-ref) 316 | (magit-insert-section (svn-unpulled) 317 | (magit-insert-heading "Unpulled svn commits:") 318 | (magit--insert-log nil (format "HEAD..%s" it))))) 319 | 320 | (defun magit-insert-svn-unpushed () 321 | (--when-let (magit-svn-get-ref) 322 | (magit-insert-section (svn-unpushed) 323 | (magit-insert-heading "Unpushed git commits:") 324 | (magit--insert-log nil (format "%s..HEAD" it))))) 325 | 326 | (magit-define-section-jumper magit-jump-to-svn-unpulled 327 | "Unpulled svn commits" svn-unpulled) 328 | 329 | (magit-define-section-jumper magit-jump-to-svn-unpushed 330 | "Unpushed git commits" svn-unpushed) 331 | 332 | (defun magit-insert-svn-remote () 333 | (--when-let (magit-svn-get-rev) 334 | (magit-insert-section (line) 335 | (insert (format "%-10s%s from %s\n" "Remote:" 336 | (propertize (concat "r" it) 'face 'magit-hash) 337 | (magit-svn-get-url)))))) 338 | 339 | ;;; _ 340 | (provide 'magit-svn) 341 | ;; Local Variables: 342 | ;; indent-tabs-mode: nil 343 | ;; End: 344 | ;;; magit-svn.el ends here 345 | --------------------------------------------------------------------------------