##// END OF EJS Templates
Merge with stable
Matt Mackall -
r10264:d6512b3e merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,52 +1,52 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # Copyright 2005-2007 by Intevation GmbH <intevation@intevation.de>
4 4 #
5 5 # Author(s):
6 6 # Thomas Arendsen Hein <thomas@intevation.de>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2, incorporated herein by reference.
9 # GNU General Public License version 2 or any later version.
10 10
11 11 """
12 12 hg-ssh - a wrapper for ssh access to a limited set of mercurial repos
13 13
14 14 To be used in ~/.ssh/authorized_keys with the "command" option, see sshd(8):
15 15 command="hg-ssh path/to/repo1 /path/to/repo2 ~/repo3 ~user/repo4" ssh-dss ...
16 16 (probably together with these other useful options:
17 17 no-port-forwarding,no-X11-forwarding,no-agent-forwarding)
18 18
19 19 This allows pull/push over ssh to to the repositories given as arguments.
20 20
21 21 If all your repositories are subdirectories of a common directory, you can
22 22 allow shorter paths with:
23 23 command="cd path/to/my/repositories && hg-ssh repo1 subdir/repo2"
24 24
25 25 You can use pattern matching of your normal shell, e.g.:
26 26 command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}"
27 27 """
28 28
29 29 # enable importing on demand to reduce startup time
30 30 from mercurial import demandimport; demandimport.enable()
31 31
32 32 from mercurial import dispatch
33 33
34 34 import sys, os
35 35
36 36 cwd = os.getcwd()
37 37 allowed_paths = [os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
38 38 for path in sys.argv[1:]]
39 39 orig_cmd = os.getenv('SSH_ORIGINAL_COMMAND', '?')
40 40
41 41 if orig_cmd.startswith('hg -R ') and orig_cmd.endswith(' serve --stdio'):
42 42 path = orig_cmd[6:-14]
43 43 repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
44 44 if repo in allowed_paths:
45 45 dispatch.dispatch(['-R', repo, 'serve', '--stdio'])
46 46 else:
47 47 sys.stderr.write("Illegal repository %r\n" % repo)
48 48 sys.exit(-1)
49 49 else:
50 50 sys.stderr.write("Illegal command %r\n" % orig_cmd)
51 51 sys.exit(-1)
52 52
@@ -1,36 +1,36 b''
1 1 # memory.py - track memory usage
2 2 #
3 3 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''helper extension to measure memory usage
9 9
10 10 Reads current and peak memory usage from ``/proc/self/status`` and
11 11 prints it to ``stderr`` on exit.
12 12 '''
13 13
14 14 import atexit
15 15
16 16 def memusage(ui):
17 17 """Report memory usage of the current process."""
18 18 status = None
19 19 result = {'peak': 0, 'rss': 0}
20 20 try:
21 21 # This will only work on systems with a /proc file system
22 22 # (like Linux).
23 23 status = open('/proc/self/status', 'r')
24 24 for line in status:
25 25 parts = line.split()
26 26 key = parts[0][2:-1].lower()
27 27 if key in result:
28 28 result[key] = int(parts[1])
29 29 finally:
30 30 if status is not None:
31 31 status.close()
32 32 ui.write_err(", ".join(["%s: %.1f MiB" % (key, value/1024.0)
33 33 for key, value in result.iteritems()]) + "\n")
34 34
35 35 def extsetup(ui):
36 36 atexit.register(memusage, ui)
@@ -1,1294 +1,1294 b''
1 1 ;;; mercurial.el --- Emacs support for the Mercurial distributed SCM
2 2
3 3 ;; Copyright (C) 2005, 2006 Bryan O'Sullivan
4 4
5 5 ;; Author: Bryan O'Sullivan <bos@serpentine.com>
6 6
7 7 ;; mercurial.el is free software; you can redistribute it and/or
8 ;; modify it under the terms of version 2 of the GNU General Public
9 ;; License as published by the Free Software Foundation.
8 ;; modify it under the terms of the GNU General Public License or any
9 ;; later version.
10 10
11 11 ;; mercurial.el is distributed in the hope that it will be useful, but
12 12 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 14 ;; General Public License for more details.
15 15
16 16 ;; You should have received a copy of the GNU General Public License
17 17 ;; along with mercurial.el, GNU Emacs, or XEmacs; see the file COPYING
18 18 ;; (`C-h C-l'). If not, write to the Free Software Foundation, Inc.,
19 19 ;; 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
20 20
21 21 ;;; Commentary:
22 22
23 23 ;; mercurial.el builds upon Emacs's VC mode to provide flexible
24 24 ;; integration with the Mercurial distributed SCM tool.
25 25
26 26 ;; To get going as quickly as possible, load mercurial.el into Emacs and
27 27 ;; type `C-c h h'; this runs hg-help-overview, which prints a helpful
28 28 ;; usage overview.
29 29
30 30 ;; Much of the inspiration for mercurial.el comes from Rajesh
31 31 ;; Vaidheeswarran's excellent p4.el, which does an admirably thorough
32 32 ;; job for the commercial Perforce SCM product. In fact, substantial
33 33 ;; chunks of code are adapted from p4.el.
34 34
35 35 ;; This code has been developed under XEmacs 21.5, and may not work as
36 36 ;; well under GNU Emacs (albeit tested under 21.4). Patches to
37 37 ;; enhance the portability of this code, fix bugs, and add features
38 38 ;; are most welcome.
39 39
40 40 ;; As of version 22.3, GNU Emacs's VC mode has direct support for
41 41 ;; Mercurial, so this package may not prove as useful there.
42 42
43 43 ;; Please send problem reports and suggestions to bos@serpentine.com.
44 44
45 45
46 46 ;;; Code:
47 47
48 48 (eval-when-compile (require 'cl))
49 49 (require 'diff-mode)
50 50 (require 'easymenu)
51 51 (require 'executable)
52 52 (require 'vc)
53 53
54 54 (defmacro hg-feature-cond (&rest clauses)
55 55 "Test CLAUSES for feature at compile time.
56 56 Each clause is (FEATURE BODY...)."
57 57 (dolist (x clauses)
58 58 (let ((feature (car x))
59 59 (body (cdr x)))
60 60 (when (or (eq feature t)
61 61 (featurep feature))
62 62 (return (cons 'progn body))))))
63 63
64 64
65 65 ;;; XEmacs has view-less, while GNU Emacs has view. Joy.
66 66
67 67 (hg-feature-cond
68 68 (xemacs (require 'view-less))
69 69 (t (require 'view)))
70 70
71 71
72 72 ;;; Variables accessible through the custom system.
73 73
74 74 (defgroup mercurial nil
75 75 "Mercurial distributed SCM."
76 76 :group 'tools)
77 77
78 78 (defcustom hg-binary
79 79 (or (executable-find "hg")
80 80 (dolist (path '("~/bin/hg" "/usr/bin/hg" "/usr/local/bin/hg"))
81 81 (when (file-executable-p path)
82 82 (return path))))
83 83 "The path to Mercurial's hg executable."
84 84 :type '(file :must-match t)
85 85 :group 'mercurial)
86 86
87 87 (defcustom hg-mode-hook nil
88 88 "Hook run when a buffer enters hg-mode."
89 89 :type 'sexp
90 90 :group 'mercurial)
91 91
92 92 (defcustom hg-commit-mode-hook nil
93 93 "Hook run when a buffer is created to prepare a commit."
94 94 :type 'sexp
95 95 :group 'mercurial)
96 96
97 97 (defcustom hg-pre-commit-hook nil
98 98 "Hook run before a commit is performed.
99 99 If you want to prevent the commit from proceeding, raise an error."
100 100 :type 'sexp
101 101 :group 'mercurial)
102 102
103 103 (defcustom hg-log-mode-hook nil
104 104 "Hook run after a buffer is filled with log information."
105 105 :type 'sexp
106 106 :group 'mercurial)
107 107
108 108 (defcustom hg-global-prefix "\C-ch"
109 109 "The global prefix for Mercurial keymap bindings."
110 110 :type 'sexp
111 111 :group 'mercurial)
112 112
113 113 (defcustom hg-commit-allow-empty-message nil
114 114 "Whether to allow changes to be committed with empty descriptions."
115 115 :type 'boolean
116 116 :group 'mercurial)
117 117
118 118 (defcustom hg-commit-allow-empty-file-list nil
119 119 "Whether to allow changes to be committed without any modified files."
120 120 :type 'boolean
121 121 :group 'mercurial)
122 122
123 123 (defcustom hg-rev-completion-limit 100
124 124 "The maximum number of revisions that hg-read-rev will offer to complete.
125 125 This affects memory usage and performance when prompting for revisions
126 126 in a repository with a lot of history."
127 127 :type 'integer
128 128 :group 'mercurial)
129 129
130 130 (defcustom hg-log-limit 50
131 131 "The maximum number of revisions that hg-log will display."
132 132 :type 'integer
133 133 :group 'mercurial)
134 134
135 135 (defcustom hg-update-modeline t
136 136 "Whether to update the modeline with the status of a file after every save.
137 137 Set this to nil on platforms with poor process management, such as Windows."
138 138 :type 'boolean
139 139 :group 'mercurial)
140 140
141 141 (defcustom hg-incoming-repository "default"
142 142 "The repository from which changes are pulled from by default.
143 143 This should be a symbolic repository name, since it is used for all
144 144 repository-related commands."
145 145 :type 'string
146 146 :group 'mercurial)
147 147
148 148 (defcustom hg-outgoing-repository "default-push"
149 149 "The repository to which changes are pushed to by default.
150 150 This should be a symbolic repository name, since it is used for all
151 151 repository-related commands."
152 152 :type 'string
153 153 :group 'mercurial)
154 154
155 155
156 156 ;;; Other variables.
157 157
158 158 (defvar hg-mode nil
159 159 "Is this file managed by Mercurial?")
160 160 (make-variable-buffer-local 'hg-mode)
161 161 (put 'hg-mode 'permanent-local t)
162 162
163 163 (defvar hg-status nil)
164 164 (make-variable-buffer-local 'hg-status)
165 165 (put 'hg-status 'permanent-local t)
166 166
167 167 (defvar hg-prev-buffer nil)
168 168 (make-variable-buffer-local 'hg-prev-buffer)
169 169 (put 'hg-prev-buffer 'permanent-local t)
170 170
171 171 (defvar hg-root nil)
172 172 (make-variable-buffer-local 'hg-root)
173 173 (put 'hg-root 'permanent-local t)
174 174
175 175 (defvar hg-view-mode nil)
176 176 (make-variable-buffer-local 'hg-view-mode)
177 177 (put 'hg-view-mode 'permanent-local t)
178 178
179 179 (defvar hg-view-file-name nil)
180 180 (make-variable-buffer-local 'hg-view-file-name)
181 181 (put 'hg-view-file-name 'permanent-local t)
182 182
183 183 (defvar hg-output-buffer-name "*Hg*"
184 184 "The name to use for Mercurial output buffers.")
185 185
186 186 (defvar hg-file-history nil)
187 187 (defvar hg-repo-history nil)
188 188 (defvar hg-rev-history nil)
189 189 (defvar hg-repo-completion-table nil) ; shut up warnings
190 190
191 191
192 192 ;;; Random constants.
193 193
194 194 (defconst hg-commit-message-start
195 195 "--- Enter your commit message. Type `C-c C-c' to commit. ---\n")
196 196
197 197 (defconst hg-commit-message-end
198 198 "--- Files in bold will be committed. Click to toggle selection. ---\n")
199 199
200 200 (defconst hg-state-alist
201 201 '((?M . modified)
202 202 (?A . added)
203 203 (?R . removed)
204 204 (?! . deleted)
205 205 (?C . normal)
206 206 (?I . ignored)
207 207 (?? . nil)))
208 208
209 209 ;;; hg-mode keymap.
210 210
211 211 (defvar hg-prefix-map
212 212 (let ((map (make-sparse-keymap)))
213 213 (hg-feature-cond (xemacs (set-keymap-name map 'hg-prefix-map))) ; XEmacs
214 214 (set-keymap-parent map vc-prefix-map)
215 215 (define-key map "=" 'hg-diff)
216 216 (define-key map "c" 'hg-undo)
217 217 (define-key map "g" 'hg-annotate)
218 218 (define-key map "i" 'hg-add)
219 219 (define-key map "l" 'hg-log)
220 220 (define-key map "n" 'hg-commit-start)
221 221 ;; (define-key map "r" 'hg-update)
222 222 (define-key map "u" 'hg-revert-buffer)
223 223 (define-key map "~" 'hg-version-other-window)
224 224 map)
225 225 "This keymap overrides some default vc-mode bindings.")
226 226
227 227 (defvar hg-mode-map
228 228 (let ((map (make-sparse-keymap)))
229 229 (define-key map "\C-xv" hg-prefix-map)
230 230 map))
231 231
232 232 (add-minor-mode 'hg-mode 'hg-mode hg-mode-map)
233 233
234 234
235 235 ;;; Global keymap.
236 236
237 237 (defvar hg-global-map
238 238 (let ((map (make-sparse-keymap)))
239 239 (define-key map "," 'hg-incoming)
240 240 (define-key map "." 'hg-outgoing)
241 241 (define-key map "<" 'hg-pull)
242 242 (define-key map "=" 'hg-diff-repo)
243 243 (define-key map ">" 'hg-push)
244 244 (define-key map "?" 'hg-help-overview)
245 245 (define-key map "A" 'hg-addremove)
246 246 (define-key map "U" 'hg-revert)
247 247 (define-key map "a" 'hg-add)
248 248 (define-key map "c" 'hg-commit-start)
249 249 (define-key map "f" 'hg-forget)
250 250 (define-key map "h" 'hg-help-overview)
251 251 (define-key map "i" 'hg-init)
252 252 (define-key map "l" 'hg-log-repo)
253 253 (define-key map "r" 'hg-root)
254 254 (define-key map "s" 'hg-status)
255 255 (define-key map "u" 'hg-update)
256 256 map))
257 257
258 258 (global-set-key hg-global-prefix hg-global-map)
259 259
260 260 ;;; View mode keymap.
261 261
262 262 (defvar hg-view-mode-map
263 263 (let ((map (make-sparse-keymap)))
264 264 (hg-feature-cond (xemacs (set-keymap-name map 'hg-view-mode-map))) ; XEmacs
265 265 (define-key map (hg-feature-cond (xemacs [button2])
266 266 (t [mouse-2]))
267 267 'hg-buffer-mouse-clicked)
268 268 map))
269 269
270 270 (add-minor-mode 'hg-view-mode "" hg-view-mode-map)
271 271
272 272
273 273 ;;; Commit mode keymaps.
274 274
275 275 (defvar hg-commit-mode-map
276 276 (let ((map (make-sparse-keymap)))
277 277 (define-key map "\C-c\C-c" 'hg-commit-finish)
278 278 (define-key map "\C-c\C-k" 'hg-commit-kill)
279 279 (define-key map "\C-xv=" 'hg-diff-repo)
280 280 map))
281 281
282 282 (defvar hg-commit-mode-file-map
283 283 (let ((map (make-sparse-keymap)))
284 284 (define-key map (hg-feature-cond (xemacs [button2])
285 285 (t [mouse-2]))
286 286 'hg-commit-mouse-clicked)
287 287 (define-key map " " 'hg-commit-toggle-file)
288 288 (define-key map "\r" 'hg-commit-toggle-file)
289 289 map))
290 290
291 291
292 292 ;;; Convenience functions.
293 293
294 294 (defsubst hg-binary ()
295 295 (if hg-binary
296 296 hg-binary
297 297 (error "No `hg' executable found!")))
298 298
299 299 (defsubst hg-replace-in-string (str regexp newtext &optional literal)
300 300 "Replace all matches in STR for REGEXP with NEWTEXT string.
301 301 Return the new string. Optional LITERAL non-nil means do a literal
302 302 replacement.
303 303
304 304 This function bridges yet another pointless impedance gap between
305 305 XEmacs and GNU Emacs."
306 306 (hg-feature-cond
307 307 (xemacs (replace-in-string str regexp newtext literal))
308 308 (t (replace-regexp-in-string regexp newtext str nil literal))))
309 309
310 310 (defsubst hg-strip (str)
311 311 "Strip leading and trailing blank lines from a string."
312 312 (hg-replace-in-string (hg-replace-in-string str "[\r\n][ \t\r\n]*\\'" "")
313 313 "\\`[ \t\r\n]*[\r\n]" ""))
314 314
315 315 (defsubst hg-chomp (str)
316 316 "Strip trailing newlines from a string."
317 317 (hg-replace-in-string str "[\r\n]+\\'" ""))
318 318
319 319 (defun hg-run-command (command &rest args)
320 320 "Run the shell command COMMAND, returning (EXIT-CODE . COMMAND-OUTPUT).
321 321 The list ARGS contains a list of arguments to pass to the command."
322 322 (let* (exit-code
323 323 (output
324 324 (with-output-to-string
325 325 (with-current-buffer
326 326 standard-output
327 327 (setq exit-code
328 328 (apply 'call-process command nil t nil args))))))
329 329 (cons exit-code output)))
330 330
331 331 (defun hg-run (command &rest args)
332 332 "Run the Mercurial command COMMAND, returning (EXIT-CODE . COMMAND-OUTPUT)."
333 333 (apply 'hg-run-command (hg-binary) command args))
334 334
335 335 (defun hg-run0 (command &rest args)
336 336 "Run the Mercurial command COMMAND, returning its output.
337 337 If the command does not exit with a zero status code, raise an error."
338 338 (let ((res (apply 'hg-run-command (hg-binary) command args)))
339 339 (if (not (eq (car res) 0))
340 340 (error "Mercurial command failed %s - exit code %s"
341 341 (cons command args)
342 342 (car res))
343 343 (cdr res))))
344 344
345 345 (defmacro hg-do-across-repo (path &rest body)
346 346 (let ((root-name (make-symbol "root-"))
347 347 (buf-name (make-symbol "buf-")))
348 348 `(let ((,root-name (hg-root ,path)))
349 349 (save-excursion
350 350 (dolist (,buf-name (buffer-list))
351 351 (set-buffer ,buf-name)
352 352 (when (and hg-status (equal (hg-root buffer-file-name) ,root-name))
353 353 ,@body))))))
354 354
355 355 (put 'hg-do-across-repo 'lisp-indent-function 1)
356 356
357 357 (defun hg-sync-buffers (path)
358 358 "Sync buffers visiting PATH with their on-disk copies.
359 359 If PATH is not being visited, but is under the repository root, sync
360 360 all buffers visiting files in the repository."
361 361 (let ((buf (find-buffer-visiting path)))
362 362 (if buf
363 363 (with-current-buffer buf
364 364 (vc-buffer-sync))
365 365 (hg-do-across-repo path
366 366 (vc-buffer-sync)))))
367 367
368 368 (defun hg-buffer-commands (pnt)
369 369 "Use the properties of a character to do something sensible."
370 370 (interactive "d")
371 371 (let ((rev (get-char-property pnt 'rev))
372 372 (file (get-char-property pnt 'file)))
373 373 (cond
374 374 (file
375 375 (find-file-other-window file))
376 376 (rev
377 377 (hg-diff hg-view-file-name rev rev))
378 378 ((message "I don't know how to do that yet")))))
379 379
380 380 (defsubst hg-event-point (event)
381 381 "Return the character position of the mouse event EVENT."
382 382 (hg-feature-cond (xemacs (event-point event))
383 383 (t (posn-point (event-start event)))))
384 384
385 385 (defsubst hg-event-window (event)
386 386 "Return the window over which mouse event EVENT occurred."
387 387 (hg-feature-cond (xemacs (event-window event))
388 388 (t (posn-window (event-start event)))))
389 389
390 390 (defun hg-buffer-mouse-clicked (event)
391 391 "Translate the mouse clicks in a HG log buffer to character events.
392 392 These are then handed off to `hg-buffer-commands'.
393 393
394 394 Handle frickin' frackin' gratuitous event-related incompatibilities."
395 395 (interactive "e")
396 396 (select-window (hg-event-window event))
397 397 (hg-buffer-commands (hg-event-point event)))
398 398
399 399 (defsubst hg-abbrev-file-name (file)
400 400 "Portable wrapper around abbreviate-file-name."
401 401 (hg-feature-cond (xemacs (abbreviate-file-name file t))
402 402 (t (abbreviate-file-name file))))
403 403
404 404 (defun hg-read-file-name (&optional prompt default)
405 405 "Read a file or directory name, or a pattern, to use with a command."
406 406 (save-excursion
407 407 (while hg-prev-buffer
408 408 (set-buffer hg-prev-buffer))
409 409 (let ((path (or default
410 410 (buffer-file-name)
411 411 (expand-file-name default-directory))))
412 412 (if (or (not path) current-prefix-arg)
413 413 (expand-file-name
414 414 (eval (list* 'read-file-name
415 415 (format "File, directory or pattern%s: "
416 416 (or prompt ""))
417 417 (and path (file-name-directory path))
418 418 nil nil
419 419 (and path (file-name-nondirectory path))
420 420 (hg-feature-cond
421 421 (xemacs (cons (quote 'hg-file-history) nil))
422 422 (t nil)))))
423 423 path))))
424 424
425 425 (defun hg-read-number (&optional prompt default)
426 426 "Read a integer value."
427 427 (save-excursion
428 428 (if (or (not default) current-prefix-arg)
429 429 (string-to-number
430 430 (eval (list* 'read-string
431 431 (or prompt "")
432 432 (if default (cons (format "%d" default) nil) nil))))
433 433 default)))
434 434
435 435 (defun hg-read-config ()
436 436 "Return an alist of (key . value) pairs of Mercurial config data.
437 437 Each key is of the form (section . name)."
438 438 (let (items)
439 439 (dolist (line (split-string (hg-chomp (hg-run0 "debugconfig")) "\n") items)
440 440 (string-match "^\\([^=]*\\)=\\(.*\\)" line)
441 441 (let* ((left (substring line (match-beginning 1) (match-end 1)))
442 442 (right (substring line (match-beginning 2) (match-end 2)))
443 443 (key (split-string left "\\."))
444 444 (value (hg-replace-in-string right "\\\\n" "\n" t)))
445 445 (setq items (cons (cons (cons (car key) (cadr key)) value) items))))))
446 446
447 447 (defun hg-config-section (section config)
448 448 "Return an alist of (name . value) pairs for SECTION of CONFIG."
449 449 (let (items)
450 450 (dolist (item config items)
451 451 (when (equal (caar item) section)
452 452 (setq items (cons (cons (cdar item) (cdr item)) items))))))
453 453
454 454 (defun hg-string-starts-with (sub str)
455 455 "Indicate whether string STR starts with the substring or character SUB."
456 456 (if (not (stringp sub))
457 457 (and (> (length str) 0) (equal (elt str 0) sub))
458 458 (let ((sub-len (length sub)))
459 459 (and (<= sub-len (length str))
460 460 (string= sub (substring str 0 sub-len))))))
461 461
462 462 (defun hg-complete-repo (string predicate all)
463 463 "Attempt to complete a repository name.
464 464 We complete on either symbolic names from Mercurial's config or real
465 465 directory names from the file system. We do not penalise URLs."
466 466 (or (if all
467 467 (all-completions string hg-repo-completion-table predicate)
468 468 (try-completion string hg-repo-completion-table predicate))
469 469 (let* ((str (expand-file-name string))
470 470 (dir (file-name-directory str))
471 471 (file (file-name-nondirectory str)))
472 472 (if all
473 473 (let (completions)
474 474 (dolist (name (delete "./" (file-name-all-completions file dir))
475 475 completions)
476 476 (let ((path (concat dir name)))
477 477 (when (file-directory-p path)
478 478 (setq completions (cons name completions))))))
479 479 (let ((comp (file-name-completion file dir)))
480 480 (if comp
481 481 (hg-abbrev-file-name (concat dir comp))))))))
482 482
483 483 (defun hg-read-repo-name (&optional prompt initial-contents default)
484 484 "Read the location of a repository."
485 485 (save-excursion
486 486 (while hg-prev-buffer
487 487 (set-buffer hg-prev-buffer))
488 488 (let (hg-repo-completion-table)
489 489 (if current-prefix-arg
490 490 (progn
491 491 (dolist (path (hg-config-section "paths" (hg-read-config)))
492 492 (setq hg-repo-completion-table
493 493 (cons (cons (car path) t) hg-repo-completion-table))
494 494 (unless (hg-string-starts-with (hg-feature-cond
495 495 (xemacs directory-sep-char)
496 496 (t ?/))
497 497 (cdr path))
498 498 (setq hg-repo-completion-table
499 499 (cons (cons (cdr path) t) hg-repo-completion-table))))
500 500 (completing-read (format "Repository%s: " (or prompt ""))
501 501 'hg-complete-repo
502 502 nil
503 503 nil
504 504 initial-contents
505 505 'hg-repo-history
506 506 default))
507 507 default))))
508 508
509 509 (defun hg-read-rev (&optional prompt default)
510 510 "Read a revision or tag, offering completions."
511 511 (save-excursion
512 512 (while hg-prev-buffer
513 513 (set-buffer hg-prev-buffer))
514 514 (let ((rev (or default "tip")))
515 515 (if current-prefix-arg
516 516 (let ((revs (split-string
517 517 (hg-chomp
518 518 (hg-run0 "-q" "log" "-l"
519 519 (format "%d" hg-rev-completion-limit)))
520 520 "[\n:]")))
521 521 (dolist (line (split-string (hg-chomp (hg-run0 "tags")) "\n"))
522 522 (setq revs (cons (car (split-string line "\\s-")) revs)))
523 523 (completing-read (format "Revision%s (%s): "
524 524 (or prompt "")
525 525 (or default "tip"))
526 526 (mapcar (lambda (x) (cons x x)) revs)
527 527 nil
528 528 nil
529 529 nil
530 530 'hg-rev-history
531 531 (or default "tip")))
532 532 rev))))
533 533
534 534 (defun hg-parents-for-mode-line (root)
535 535 "Format the parents of the working directory for the mode line."
536 536 (let ((parents (split-string (hg-chomp
537 537 (hg-run0 "--cwd" root "parents" "--template"
538 538 "{rev}\n")) "\n")))
539 539 (mapconcat 'identity parents "+")))
540 540
541 541 (defun hg-buffers-visiting-repo (&optional path)
542 542 "Return a list of buffers visiting the repository containing PATH."
543 543 (let ((root-name (hg-root (or path (buffer-file-name))))
544 544 bufs)
545 545 (save-excursion
546 546 (dolist (buf (buffer-list) bufs)
547 547 (set-buffer buf)
548 548 (let ((name (buffer-file-name)))
549 549 (when (and hg-status name (equal (hg-root name) root-name))
550 550 (setq bufs (cons buf bufs))))))))
551 551
552 552 (defun hg-update-mode-lines (path)
553 553 "Update the mode lines of all buffers visiting the same repository as PATH."
554 554 (let* ((root (hg-root path))
555 555 (parents (hg-parents-for-mode-line root)))
556 556 (save-excursion
557 557 (dolist (info (hg-path-status
558 558 root
559 559 (mapcar
560 560 (function
561 561 (lambda (buf)
562 562 (substring (buffer-file-name buf) (length root))))
563 563 (hg-buffers-visiting-repo root))))
564 564 (let* ((name (car info))
565 565 (status (cdr info))
566 566 (buf (find-buffer-visiting (concat root name))))
567 567 (when buf
568 568 (set-buffer buf)
569 569 (hg-mode-line-internal status parents)))))))
570 570
571 571
572 572 ;;; View mode bits.
573 573
574 574 (defun hg-exit-view-mode (buf)
575 575 "Exit from hg-view-mode.
576 576 We delete the current window if entering hg-view-mode split the
577 577 current frame."
578 578 (when (and (eq buf (current-buffer))
579 579 (> (length (window-list)) 1))
580 580 (delete-window))
581 581 (when (buffer-live-p buf)
582 582 (kill-buffer buf)))
583 583
584 584 (defun hg-view-mode (prev-buffer &optional file-name)
585 585 (goto-char (point-min))
586 586 (set-buffer-modified-p nil)
587 587 (toggle-read-only t)
588 588 (hg-feature-cond (xemacs (view-minor-mode prev-buffer 'hg-exit-view-mode))
589 589 (t (view-mode-enter nil 'hg-exit-view-mode)))
590 590 (setq hg-view-mode t)
591 591 (setq truncate-lines t)
592 592 (when file-name
593 593 (setq hg-view-file-name
594 594 (hg-abbrev-file-name file-name))))
595 595
596 596 (defun hg-file-status (file)
597 597 "Return status of FILE, or nil if FILE does not exist or is unmanaged."
598 598 (let* ((s (hg-run "status" file))
599 599 (exit (car s))
600 600 (output (cdr s)))
601 601 (if (= exit 0)
602 602 (let ((state (and (>= (length output) 2)
603 603 (= (aref output 1) ? )
604 604 (assq (aref output 0) hg-state-alist))))
605 605 (if state
606 606 (cdr state)
607 607 'normal)))))
608 608
609 609 (defun hg-path-status (root paths)
610 610 "Return status of PATHS in repo ROOT as an alist.
611 611 Each entry is a pair (FILE-NAME . STATUS)."
612 612 (let ((s (apply 'hg-run "--cwd" root "status" "-marduc" paths))
613 613 result)
614 614 (dolist (entry (split-string (hg-chomp (cdr s)) "\n") (nreverse result))
615 615 (let (state name)
616 616 (cond ((= (aref entry 1) ? )
617 617 (setq state (assq (aref entry 0) hg-state-alist)
618 618 name (substring entry 2)))
619 619 ((string-match "\\(.*\\): " entry)
620 620 (setq name (match-string 1 entry))))
621 621 (setq result (cons (cons name state) result))))))
622 622
623 623 (defmacro hg-view-output (args &rest body)
624 624 "Execute BODY in a clean buffer, then quickly display that buffer.
625 625 If the buffer contains one line, its contents are displayed in the
626 626 minibuffer. Otherwise, the buffer is displayed in view-mode.
627 627 ARGS is of the form (BUFFER-NAME &optional FILE), where BUFFER-NAME is
628 628 the name of the buffer to create, and FILE is the name of the file
629 629 being viewed."
630 630 (let ((prev-buf (make-symbol "prev-buf-"))
631 631 (v-b-name (car args))
632 632 (v-m-rest (cdr args)))
633 633 `(let ((view-buf-name ,v-b-name)
634 634 (,prev-buf (current-buffer)))
635 635 (get-buffer-create view-buf-name)
636 636 (kill-buffer view-buf-name)
637 637 (get-buffer-create view-buf-name)
638 638 (set-buffer view-buf-name)
639 639 (save-excursion
640 640 ,@body)
641 641 (case (count-lines (point-min) (point-max))
642 642 ((0)
643 643 (kill-buffer view-buf-name)
644 644 (message "(No output)"))
645 645 ((1)
646 646 (let ((msg (hg-chomp (buffer-substring (point-min) (point-max)))))
647 647 (kill-buffer view-buf-name)
648 648 (message "%s" msg)))
649 649 (t
650 650 (pop-to-buffer view-buf-name)
651 651 (setq hg-prev-buffer ,prev-buf)
652 652 (hg-view-mode ,prev-buf ,@v-m-rest))))))
653 653
654 654 (put 'hg-view-output 'lisp-indent-function 1)
655 655
656 656 ;;; Context save and restore across revert and other operations.
657 657
658 658 (defun hg-position-context (pos)
659 659 "Return information to help find the given position again."
660 660 (let* ((end (min (point-max) (+ pos 98))))
661 661 (list pos
662 662 (buffer-substring (max (point-min) (- pos 2)) end)
663 663 (- end pos))))
664 664
665 665 (defun hg-buffer-context ()
666 666 "Return information to help restore a user's editing context.
667 667 This is useful across reverts and merges, where a context is likely
668 668 to have moved a little, but not really changed."
669 669 (let ((point-context (hg-position-context (point)))
670 670 (mark-context (let ((mark (mark-marker)))
671 671 (and mark
672 672 ;; make sure active mark
673 673 (marker-buffer mark)
674 674 (marker-position mark)
675 675 (hg-position-context mark)))))
676 676 (list point-context mark-context)))
677 677
678 678 (defun hg-find-context (ctx)
679 679 "Attempt to find a context in the given buffer.
680 680 Always returns a valid, hopefully sane, position."
681 681 (let ((pos (nth 0 ctx))
682 682 (str (nth 1 ctx))
683 683 (fixup (nth 2 ctx)))
684 684 (save-excursion
685 685 (goto-char (max (point-min) (- pos 15000)))
686 686 (if (and (not (equal str ""))
687 687 (search-forward str nil t))
688 688 (- (point) fixup)
689 689 (max pos (point-min))))))
690 690
691 691 (defun hg-restore-context (ctx)
692 692 "Attempt to restore the user's editing context."
693 693 (let ((point-context (nth 0 ctx))
694 694 (mark-context (nth 1 ctx)))
695 695 (goto-char (hg-find-context point-context))
696 696 (when mark-context
697 697 (set-mark (hg-find-context mark-context)))))
698 698
699 699
700 700 ;;; Hooks.
701 701
702 702 (defun hg-mode-line-internal (status parents)
703 703 (setq hg-status status
704 704 hg-mode (and status (concat " Hg:"
705 705 parents
706 706 (cdr (assq status
707 707 '((normal . "")
708 708 (removed . "r")
709 709 (added . "a")
710 710 (deleted . "!")
711 711 (modified . "m"))))))))
712 712
713 713 (defun hg-mode-line (&optional force)
714 714 "Update the modeline with the current status of a file.
715 715 An update occurs if optional argument FORCE is non-nil,
716 716 hg-update-modeline is non-nil, or we have not yet checked the state of
717 717 the file."
718 718 (let ((root (hg-root)))
719 719 (when (and root (or force hg-update-modeline (not hg-mode)))
720 720 (let ((status (hg-file-status buffer-file-name))
721 721 (parents (hg-parents-for-mode-line root)))
722 722 (hg-mode-line-internal status parents)
723 723 status))))
724 724
725 725 (defun hg-mode (&optional toggle)
726 726 "Minor mode for Mercurial distributed SCM integration.
727 727
728 728 The Mercurial mode user interface is based on that of VC mode, so if
729 729 you're already familiar with VC, the same keybindings and functions
730 730 will generally work.
731 731
732 732 Below is a list of many common SCM tasks. In the list, `G/L\'
733 733 indicates whether a key binding is global (G) to a repository or
734 734 local (L) to a file. Many commands take a prefix argument.
735 735
736 736 SCM Task G/L Key Binding Command Name
737 737 -------- --- ----------- ------------
738 738 Help overview (what you are reading) G C-c h h hg-help-overview
739 739
740 740 Tell Mercurial to manage a file G C-c h a hg-add
741 741 Commit changes to current file only L C-x v n hg-commit-start
742 742 Undo changes to file since commit L C-x v u hg-revert-buffer
743 743
744 744 Diff file vs last checkin L C-x v = hg-diff
745 745
746 746 View file change history L C-x v l hg-log
747 747 View annotated file L C-x v a hg-annotate
748 748
749 749 Diff repo vs last checkin G C-c h = hg-diff-repo
750 750 View status of files in repo G C-c h s hg-status
751 751 Commit all changes G C-c h c hg-commit-start
752 752
753 753 Undo all changes since last commit G C-c h U hg-revert
754 754 View repo change history G C-c h l hg-log-repo
755 755
756 756 See changes that can be pulled G C-c h , hg-incoming
757 757 Pull changes G C-c h < hg-pull
758 758 Update working directory after pull G C-c h u hg-update
759 759 See changes that can be pushed G C-c h . hg-outgoing
760 760 Push changes G C-c h > hg-push"
761 761 (unless vc-make-backup-files
762 762 (set (make-local-variable 'backup-inhibited) t))
763 763 (run-hooks 'hg-mode-hook))
764 764
765 765 (defun hg-find-file-hook ()
766 766 (ignore-errors
767 767 (when (hg-mode-line)
768 768 (hg-mode))))
769 769
770 770 (add-hook 'find-file-hooks 'hg-find-file-hook)
771 771
772 772 (defun hg-after-save-hook ()
773 773 (ignore-errors
774 774 (let ((old-status hg-status))
775 775 (hg-mode-line)
776 776 (if (and (not old-status) hg-status)
777 777 (hg-mode)))))
778 778
779 779 (add-hook 'after-save-hook 'hg-after-save-hook)
780 780
781 781
782 782 ;;; User interface functions.
783 783
784 784 (defun hg-help-overview ()
785 785 "This is an overview of the Mercurial SCM mode for Emacs.
786 786
787 You can find the source code, license (GPL v2), and credits for this
787 You can find the source code, license (GPLv2+), and credits for this
788 788 code by typing `M-x find-library mercurial RET'."
789 789 (interactive)
790 790 (hg-view-output ("Mercurial Help Overview")
791 791 (insert (documentation 'hg-help-overview))
792 792 (let ((pos (point)))
793 793 (insert (documentation 'hg-mode))
794 794 (goto-char pos)
795 795 (end-of-line 1)
796 796 (delete-region pos (point)))
797 797 (let ((hg-root-dir (hg-root)))
798 798 (if (not hg-root-dir)
799 799 (error "error: %s: directory is not part of a Mercurial repository."
800 800 default-directory)
801 801 (cd hg-root-dir)))))
802 802
803 803 (defun hg-fix-paths ()
804 804 "Fix paths reported by some Mercurial commands."
805 805 (save-excursion
806 806 (goto-char (point-min))
807 807 (while (re-search-forward " \\.\\.." nil t)
808 808 (replace-match " " nil nil))))
809 809
810 810 (defun hg-add (path)
811 811 "Add PATH to the Mercurial repository on the next commit.
812 812 With a prefix argument, prompt for the path to add."
813 813 (interactive (list (hg-read-file-name " to add")))
814 814 (let ((buf (current-buffer))
815 815 (update (equal buffer-file-name path)))
816 816 (hg-view-output (hg-output-buffer-name)
817 817 (apply 'call-process (hg-binary) nil t nil (list "add" path))
818 818 (hg-fix-paths)
819 819 (goto-char (point-min))
820 820 (cd (hg-root path)))
821 821 (when update
822 822 (unless vc-make-backup-files
823 823 (set (make-local-variable 'backup-inhibited) t))
824 824 (with-current-buffer buf
825 825 (hg-mode-line)))))
826 826
827 827 (defun hg-addremove ()
828 828 (interactive)
829 829 (error "not implemented"))
830 830
831 831 (defun hg-annotate ()
832 832 (interactive)
833 833 (error "not implemented"))
834 834
835 835 (defun hg-commit-toggle-file (pos)
836 836 "Toggle whether or not the file at POS will be committed."
837 837 (interactive "d")
838 838 (save-excursion
839 839 (goto-char pos)
840 840 (let (face
841 841 (inhibit-read-only t)
842 842 bol)
843 843 (beginning-of-line)
844 844 (setq bol (+ (point) 4))
845 845 (setq face (get-text-property bol 'face))
846 846 (end-of-line)
847 847 (if (eq face 'bold)
848 848 (progn
849 849 (remove-text-properties bol (point) '(face nil))
850 850 (message "%s will not be committed"
851 851 (buffer-substring bol (point))))
852 852 (add-text-properties bol (point) '(face bold))
853 853 (message "%s will be committed"
854 854 (buffer-substring bol (point)))))))
855 855
856 856 (defun hg-commit-mouse-clicked (event)
857 857 "Toggle whether or not the file at POS will be committed."
858 858 (interactive "@e")
859 859 (hg-commit-toggle-file (hg-event-point event)))
860 860
861 861 (defun hg-commit-kill ()
862 862 "Kill the commit currently being prepared."
863 863 (interactive)
864 864 (when (or (not (buffer-modified-p)) (y-or-n-p "Really kill this commit? "))
865 865 (let ((buf hg-prev-buffer))
866 866 (kill-buffer nil)
867 867 (switch-to-buffer buf))))
868 868
869 869 (defun hg-commit-finish ()
870 870 "Finish preparing a commit, and perform the actual commit.
871 871 The hook hg-pre-commit-hook is run before anything else is done. If
872 872 the commit message is empty and hg-commit-allow-empty-message is nil,
873 873 an error is raised. If the list of files to commit is empty and
874 874 hg-commit-allow-empty-file-list is nil, an error is raised."
875 875 (interactive)
876 876 (let ((root hg-root))
877 877 (save-excursion
878 878 (run-hooks 'hg-pre-commit-hook)
879 879 (goto-char (point-min))
880 880 (search-forward hg-commit-message-start)
881 881 (let (message files)
882 882 (let ((start (point)))
883 883 (goto-char (point-max))
884 884 (search-backward hg-commit-message-end)
885 885 (setq message (hg-strip (buffer-substring start (point)))))
886 886 (when (and (= (length message) 0)
887 887 (not hg-commit-allow-empty-message))
888 888 (error "Cannot proceed - commit message is empty"))
889 889 (forward-line 1)
890 890 (beginning-of-line)
891 891 (while (< (point) (point-max))
892 892 (let ((pos (+ (point) 4)))
893 893 (end-of-line)
894 894 (when (eq (get-text-property pos 'face) 'bold)
895 895 (end-of-line)
896 896 (setq files (cons (buffer-substring pos (point)) files))))
897 897 (forward-line 1))
898 898 (when (and (= (length files) 0)
899 899 (not hg-commit-allow-empty-file-list))
900 900 (error "Cannot proceed - no files to commit"))
901 901 (setq message (concat message "\n"))
902 902 (apply 'hg-run0 "--cwd" hg-root "commit" "-m" message files))
903 903 (let ((buf hg-prev-buffer))
904 904 (kill-buffer nil)
905 905 (switch-to-buffer buf))
906 906 (hg-update-mode-lines root))))
907 907
908 908 (defun hg-commit-mode ()
909 909 "Mode for describing a commit of changes to a Mercurial repository.
910 910 This involves two actions: describing the changes with a commit
911 911 message, and choosing the files to commit.
912 912
913 913 To describe the commit, simply type some text in the designated area.
914 914
915 915 By default, all modified, added and removed files are selected for
916 916 committing. Files that will be committed are displayed in bold face\;
917 917 those that will not are displayed in normal face.
918 918
919 919 To toggle whether a file will be committed, move the cursor over a
920 920 particular file and hit space or return. Alternatively, middle click
921 921 on the file.
922 922
923 923 Key bindings
924 924 ------------
925 925 \\[hg-commit-finish] proceed with commit
926 926 \\[hg-commit-kill] kill commit
927 927
928 928 \\[hg-diff-repo] view diff of pending changes"
929 929 (interactive)
930 930 (use-local-map hg-commit-mode-map)
931 931 (set-syntax-table text-mode-syntax-table)
932 932 (setq local-abbrev-table text-mode-abbrev-table
933 933 major-mode 'hg-commit-mode
934 934 mode-name "Hg-Commit")
935 935 (set-buffer-modified-p nil)
936 936 (setq buffer-undo-list nil)
937 937 (run-hooks 'text-mode-hook 'hg-commit-mode-hook))
938 938
939 939 (defun hg-commit-start ()
940 940 "Prepare a commit of changes to the repository containing the current file."
941 941 (interactive)
942 942 (while hg-prev-buffer
943 943 (set-buffer hg-prev-buffer))
944 944 (let ((root (hg-root))
945 945 (prev-buffer (current-buffer))
946 946 modified-files)
947 947 (unless root
948 948 (error "Cannot commit outside a repository!"))
949 949 (hg-sync-buffers root)
950 950 (setq modified-files (hg-chomp (hg-run0 "--cwd" root "status" "-arm")))
951 951 (when (and (= (length modified-files) 0)
952 952 (not hg-commit-allow-empty-file-list))
953 953 (error "No pending changes to commit"))
954 954 (let* ((buf-name (format "*Mercurial: Commit %s*" root)))
955 955 (pop-to-buffer (get-buffer-create buf-name))
956 956 (when (= (point-min) (point-max))
957 957 (set (make-local-variable 'hg-root) root)
958 958 (setq hg-prev-buffer prev-buffer)
959 959 (insert "\n")
960 960 (let ((bol (point)))
961 961 (insert hg-commit-message-end)
962 962 (add-text-properties bol (point) '(face bold-italic)))
963 963 (let ((file-area (point)))
964 964 (insert modified-files)
965 965 (goto-char file-area)
966 966 (while (< (point) (point-max))
967 967 (let ((bol (point)))
968 968 (forward-char 1)
969 969 (insert " ")
970 970 (end-of-line)
971 971 (add-text-properties (+ bol 4) (point)
972 972 '(face bold mouse-face highlight)))
973 973 (forward-line 1))
974 974 (goto-char file-area)
975 975 (add-text-properties (point) (point-max)
976 976 `(keymap ,hg-commit-mode-file-map))
977 977 (goto-char (point-min))
978 978 (insert hg-commit-message-start)
979 979 (add-text-properties (point-min) (point) '(face bold-italic))
980 980 (insert "\n\n")
981 981 (forward-line -1)
982 982 (save-excursion
983 983 (goto-char (point-max))
984 984 (search-backward hg-commit-message-end)
985 985 (add-text-properties (match-beginning 0) (point-max)
986 986 '(read-only t))
987 987 (goto-char (point-min))
988 988 (search-forward hg-commit-message-start)
989 989 (add-text-properties (match-beginning 0) (match-end 0)
990 990 '(read-only t)))
991 991 (hg-commit-mode)
992 992 (cd root))))))
993 993
994 994 (defun hg-diff (path &optional rev1 rev2)
995 995 "Show the differences between REV1 and REV2 of PATH.
996 996 When called interactively, the default behaviour is to treat REV1 as
997 997 the \"parent\" revision, REV2 as the current edited version of the file, and
998 998 PATH as the file edited in the current buffer.
999 999 With a prefix argument, prompt for all of these."
1000 1000 (interactive (list (hg-read-file-name " to diff")
1001 1001 (let ((rev1 (hg-read-rev " to start with" 'parent)))
1002 1002 (and (not (eq rev1 'parent)) rev1))
1003 1003 (let ((rev2 (hg-read-rev " to end with" 'working-dir)))
1004 1004 (and (not (eq rev2 'working-dir)) rev2))))
1005 1005 (hg-sync-buffers path)
1006 1006 (let ((a-path (hg-abbrev-file-name path))
1007 1007 ;; none revision is specified explicitly
1008 1008 (none (and (not rev1) (not rev2)))
1009 1009 ;; only one revision is specified explicitly
1010 1010 (one (or (and (or (equal rev1 rev2) (not rev2)) rev1)
1011 1011 (and (not rev1) rev2)))
1012 1012 diff)
1013 1013 (hg-view-output ((cond
1014 1014 (none
1015 1015 (format "Mercurial: Diff against parent of %s" a-path))
1016 1016 (one
1017 1017 (format "Mercurial: Diff of rev %s of %s" one a-path))
1018 1018 (t
1019 1019 (format "Mercurial: Diff from rev %s to %s of %s"
1020 1020 rev1 rev2 a-path))))
1021 1021 (cond
1022 1022 (none
1023 1023 (call-process (hg-binary) nil t nil "diff" path))
1024 1024 (one
1025 1025 (call-process (hg-binary) nil t nil "diff" "-r" one path))
1026 1026 (t
1027 1027 (call-process (hg-binary) nil t nil "diff" "-r" rev1 "-r" rev2 path)))
1028 1028 (diff-mode)
1029 1029 (setq diff (not (= (point-min) (point-max))))
1030 1030 (font-lock-fontify-buffer)
1031 1031 (cd (hg-root path)))
1032 1032 diff))
1033 1033
1034 1034 (defun hg-diff-repo (path &optional rev1 rev2)
1035 1035 "Show the differences between REV1 and REV2 of repository containing PATH.
1036 1036 When called interactively, the default behaviour is to treat REV1 as
1037 1037 the \"parent\" revision, REV2 as the current edited version of the file, and
1038 1038 PATH as the `hg-root' of the current buffer.
1039 1039 With a prefix argument, prompt for all of these."
1040 1040 (interactive (list (hg-read-file-name " to diff")
1041 1041 (let ((rev1 (hg-read-rev " to start with" 'parent)))
1042 1042 (and (not (eq rev1 'parent)) rev1))
1043 1043 (let ((rev2 (hg-read-rev " to end with" 'working-dir)))
1044 1044 (and (not (eq rev2 'working-dir)) rev2))))
1045 1045 (hg-diff (hg-root path) rev1 rev2))
1046 1046
1047 1047 (defun hg-forget (path)
1048 1048 "Lose track of PATH, which has been added, but not yet committed.
1049 1049 This will prevent the file from being incorporated into the Mercurial
1050 1050 repository on the next commit.
1051 1051 With a prefix argument, prompt for the path to forget."
1052 1052 (interactive (list (hg-read-file-name " to forget")))
1053 1053 (let ((buf (current-buffer))
1054 1054 (update (equal buffer-file-name path)))
1055 1055 (hg-view-output (hg-output-buffer-name)
1056 1056 (apply 'call-process (hg-binary) nil t nil (list "forget" path))
1057 1057 ;; "hg forget" shows pathes relative NOT TO ROOT BUT TO REPOSITORY
1058 1058 (hg-fix-paths)
1059 1059 (goto-char (point-min))
1060 1060 (cd (hg-root path)))
1061 1061 (when update
1062 1062 (with-current-buffer buf
1063 1063 (when (local-variable-p 'backup-inhibited)
1064 1064 (kill-local-variable 'backup-inhibited))
1065 1065 (hg-mode-line)))))
1066 1066
1067 1067 (defun hg-incoming (&optional repo)
1068 1068 "Display changesets present in REPO that are not present locally."
1069 1069 (interactive (list (hg-read-repo-name " where changes would come from")))
1070 1070 (hg-view-output ((format "Mercurial: Incoming from %s to %s"
1071 1071 (hg-abbrev-file-name (hg-root))
1072 1072 (hg-abbrev-file-name
1073 1073 (or repo hg-incoming-repository))))
1074 1074 (call-process (hg-binary) nil t nil "incoming"
1075 1075 (or repo hg-incoming-repository))
1076 1076 (hg-log-mode)
1077 1077 (cd (hg-root))))
1078 1078
1079 1079 (defun hg-init ()
1080 1080 (interactive)
1081 1081 (error "not implemented"))
1082 1082
1083 1083 (defun hg-log-mode ()
1084 1084 "Mode for viewing a Mercurial change log."
1085 1085 (goto-char (point-min))
1086 1086 (when (looking-at "^searching for changes.*$")
1087 1087 (delete-region (match-beginning 0) (match-end 0)))
1088 1088 (run-hooks 'hg-log-mode-hook))
1089 1089
1090 1090 (defun hg-log (path &optional rev1 rev2 log-limit)
1091 1091 "Display the revision history of PATH.
1092 1092 History is displayed between REV1 and REV2.
1093 1093 Number of displayed changesets is limited to LOG-LIMIT.
1094 1094 REV1 defaults to the tip, while REV2 defaults to 0.
1095 1095 LOG-LIMIT defaults to `hg-log-limit'.
1096 1096 With a prefix argument, prompt for each parameter."
1097 1097 (interactive (list (hg-read-file-name " to log")
1098 1098 (hg-read-rev " to start with"
1099 1099 "tip")
1100 1100 (hg-read-rev " to end with"
1101 1101 "0")
1102 1102 (hg-read-number "Output limited to: "
1103 1103 hg-log-limit)))
1104 1104 (let ((a-path (hg-abbrev-file-name path))
1105 1105 (r1 (or rev1 "tip"))
1106 1106 (r2 (or rev2 "0"))
1107 1107 (limit (format "%d" (or log-limit hg-log-limit))))
1108 1108 (hg-view-output ((if (equal r1 r2)
1109 1109 (format "Mercurial: Log of rev %s of %s" rev1 a-path)
1110 1110 (format
1111 1111 "Mercurial: at most %s log(s) from rev %s to %s of %s"
1112 1112 limit r1 r2 a-path)))
1113 1113 (eval (list* 'call-process (hg-binary) nil t nil
1114 1114 "log"
1115 1115 "-r" (format "%s:%s" r1 r2)
1116 1116 "-l" limit
1117 1117 (if (> (length path) (length (hg-root path)))
1118 1118 (cons path nil)
1119 1119 nil)))
1120 1120 (hg-log-mode)
1121 1121 (cd (hg-root path)))))
1122 1122
1123 1123 (defun hg-log-repo (path &optional rev1 rev2 log-limit)
1124 1124 "Display the revision history of the repository containing PATH.
1125 1125 History is displayed between REV1 and REV2.
1126 1126 Number of displayed changesets is limited to LOG-LIMIT,
1127 1127 REV1 defaults to the tip, while REV2 defaults to 0.
1128 1128 LOG-LIMIT defaults to `hg-log-limit'.
1129 1129 With a prefix argument, prompt for each parameter."
1130 1130 (interactive (list (hg-read-file-name " to log")
1131 1131 (hg-read-rev " to start with"
1132 1132 "tip")
1133 1133 (hg-read-rev " to end with"
1134 1134 "0")
1135 1135 (hg-read-number "Output limited to: "
1136 1136 hg-log-limit)))
1137 1137 (hg-log (hg-root path) rev1 rev2 log-limit))
1138 1138
1139 1139 (defun hg-outgoing (&optional repo)
1140 1140 "Display changesets present locally that are not present in REPO."
1141 1141 (interactive (list (hg-read-repo-name " where changes would go to" nil
1142 1142 hg-outgoing-repository)))
1143 1143 (hg-view-output ((format "Mercurial: Outgoing from %s to %s"
1144 1144 (hg-abbrev-file-name (hg-root))
1145 1145 (hg-abbrev-file-name
1146 1146 (or repo hg-outgoing-repository))))
1147 1147 (call-process (hg-binary) nil t nil "outgoing"
1148 1148 (or repo hg-outgoing-repository))
1149 1149 (hg-log-mode)
1150 1150 (cd (hg-root))))
1151 1151
1152 1152 (defun hg-pull (&optional repo)
1153 1153 "Pull changes from repository REPO.
1154 1154 This does not update the working directory."
1155 1155 (interactive (list (hg-read-repo-name " to pull from")))
1156 1156 (hg-view-output ((format "Mercurial: Pull to %s from %s"
1157 1157 (hg-abbrev-file-name (hg-root))
1158 1158 (hg-abbrev-file-name
1159 1159 (or repo hg-incoming-repository))))
1160 1160 (call-process (hg-binary) nil t nil "pull"
1161 1161 (or repo hg-incoming-repository))
1162 1162 (cd (hg-root))))
1163 1163
1164 1164 (defun hg-push (&optional repo)
1165 1165 "Push changes to repository REPO."
1166 1166 (interactive (list (hg-read-repo-name " to push to")))
1167 1167 (hg-view-output ((format "Mercurial: Push from %s to %s"
1168 1168 (hg-abbrev-file-name (hg-root))
1169 1169 (hg-abbrev-file-name
1170 1170 (or repo hg-outgoing-repository))))
1171 1171 (call-process (hg-binary) nil t nil "push"
1172 1172 (or repo hg-outgoing-repository))
1173 1173 (cd (hg-root))))
1174 1174
1175 1175 (defun hg-revert-buffer-internal ()
1176 1176 (let ((ctx (hg-buffer-context)))
1177 1177 (message "Reverting %s..." buffer-file-name)
1178 1178 (hg-run0 "revert" buffer-file-name)
1179 1179 (revert-buffer t t t)
1180 1180 (hg-restore-context ctx)
1181 1181 (hg-mode-line)
1182 1182 (message "Reverting %s...done" buffer-file-name)))
1183 1183
1184 1184 (defun hg-revert-buffer ()
1185 1185 "Revert current buffer's file back to the latest committed version.
1186 1186 If the file has not changed, nothing happens. Otherwise, this
1187 1187 displays a diff and asks for confirmation before reverting."
1188 1188 (interactive)
1189 1189 (let ((vc-suppress-confirm nil)
1190 1190 (obuf (current-buffer))
1191 1191 diff)
1192 1192 (vc-buffer-sync)
1193 1193 (unwind-protect
1194 1194 (setq diff (hg-diff buffer-file-name))
1195 1195 (when diff
1196 1196 (unless (yes-or-no-p "Discard changes? ")
1197 1197 (error "Revert cancelled")))
1198 1198 (when diff
1199 1199 (let ((buf (current-buffer)))
1200 1200 (delete-window (selected-window))
1201 1201 (kill-buffer buf))))
1202 1202 (set-buffer obuf)
1203 1203 (when diff
1204 1204 (hg-revert-buffer-internal))))
1205 1205
1206 1206 (defun hg-root (&optional path)
1207 1207 "Return the root of the repository that contains the given path.
1208 1208 If the path is outside a repository, return nil.
1209 1209 When called interactively, the root is printed. A prefix argument
1210 1210 prompts for a path to check."
1211 1211 (interactive (list (hg-read-file-name)))
1212 1212 (if (or path (not hg-root))
1213 1213 (let ((root (do ((prev nil dir)
1214 1214 (dir (file-name-directory
1215 1215 (or
1216 1216 path
1217 1217 buffer-file-name
1218 1218 (expand-file-name default-directory)))
1219 1219 (file-name-directory (directory-file-name dir))))
1220 1220 ((equal prev dir))
1221 1221 (when (file-directory-p (concat dir ".hg"))
1222 1222 (return dir)))))
1223 1223 (when (interactive-p)
1224 1224 (if root
1225 1225 (message "The root of this repository is `%s'." root)
1226 1226 (message "The path `%s' is not in a Mercurial repository."
1227 1227 (hg-abbrev-file-name path))))
1228 1228 root)
1229 1229 hg-root))
1230 1230
1231 1231 (defun hg-cwd (&optional path)
1232 1232 "Return the current directory of PATH within the repository."
1233 1233 (do ((stack nil (cons (file-name-nondirectory
1234 1234 (directory-file-name dir))
1235 1235 stack))
1236 1236 (prev nil dir)
1237 1237 (dir (file-name-directory (or path buffer-file-name
1238 1238 (expand-file-name default-directory)))
1239 1239 (file-name-directory (directory-file-name dir))))
1240 1240 ((equal prev dir))
1241 1241 (when (file-directory-p (concat dir ".hg"))
1242 1242 (let ((cwd (mapconcat 'identity stack "/")))
1243 1243 (unless (equal cwd "")
1244 1244 (return (file-name-as-directory cwd)))))))
1245 1245
1246 1246 (defun hg-status (path)
1247 1247 "Print revision control status of a file or directory.
1248 1248 With prefix argument, prompt for the path to give status for.
1249 1249 Names are displayed relative to the repository root."
1250 1250 (interactive (list (hg-read-file-name " for status" (hg-root))))
1251 1251 (let ((root (hg-root)))
1252 1252 (hg-view-output ((format "Mercurial: Status of %s in %s"
1253 1253 (let ((name (substring (expand-file-name path)
1254 1254 (length root))))
1255 1255 (if (> (length name) 0)
1256 1256 name
1257 1257 "*"))
1258 1258 (hg-abbrev-file-name root)))
1259 1259 (apply 'call-process (hg-binary) nil t nil
1260 1260 (list "--cwd" root "status" path))
1261 1261 (cd (hg-root path)))))
1262 1262
1263 1263 (defun hg-undo ()
1264 1264 (interactive)
1265 1265 (error "not implemented"))
1266 1266
1267 1267 (defun hg-update ()
1268 1268 (interactive)
1269 1269 (error "not implemented"))
1270 1270
1271 1271 (defun hg-version-other-window (rev)
1272 1272 "Visit version REV of the current file in another window.
1273 1273 If the current file is named `F', the version is named `F.~REV~'.
1274 1274 If `F.~REV~' already exists, use it instead of checking it out again."
1275 1275 (interactive "sVersion to visit (default is workfile version): ")
1276 1276 (let* ((file buffer-file-name)
1277 1277 (version (if (string-equal rev "")
1278 1278 "tip"
1279 1279 rev))
1280 1280 (automatic-backup (vc-version-backup-file-name file version))
1281 1281 (manual-backup (vc-version-backup-file-name file version 'manual)))
1282 1282 (unless (file-exists-p manual-backup)
1283 1283 (if (file-exists-p automatic-backup)
1284 1284 (rename-file automatic-backup manual-backup nil)
1285 1285 (hg-run0 "-q" "cat" "-r" version "-o" manual-backup file)))
1286 1286 (find-file-other-window manual-backup)))
1287 1287
1288 1288
1289 1289 (provide 'mercurial)
1290 1290
1291 1291
1292 1292 ;;; Local Variables:
1293 1293 ;;; prompt-to-byte-compile: nil
1294 1294 ;;; end:
@@ -1,92 +1,92 b''
1 1 Summary: Mercurial -- a distributed SCM
2 2 Name: mercurial
3 3 Version: snapshot
4 4 Release: 0
5 License: GPLv2
5 License: GPLv2+
6 6 Group: Development/Tools
7 7 URL: http://mercurial.selenic.com/
8 8 Source0: http://mercurial.selenic.com/release/%{name}-%{version}.tar.gz
9 9 BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
10 10
11 11 # From the README:
12 12 #
13 13 # Note: some distributions fails to include bits of distutils by
14 14 # default, you'll need python-dev to install. You'll also need a C
15 15 # compiler and a 3-way merge tool like merge, tkdiff, or kdiff3.
16 16 #
17 17 # python-devel provides an adequate python-dev. The merge tool is a
18 18 # run-time dependency.
19 19 #
20 20 BuildRequires: python >= 2.4, python-devel, make, gcc, docutils >= 0.5
21 21 Provides: hg = %{version}-%{release}
22 22 Requires: python >= 2.4
23 23 # The hgk extension uses the wish tcl interpreter, but we don't enforce it
24 24 #Requires: tk
25 25
26 26 %define pythonver %(python -c 'import sys;print ".".join(map(str, sys.version_info[:2]))')
27 27 %define emacs_lispdir %{_datadir}/emacs/site-lisp
28 28
29 29 %description
30 30 Mercurial is a fast, lightweight source control management system designed
31 31 for efficient handling of very large distributed projects.
32 32
33 33 %prep
34 34 %setup -q
35 35
36 36 %build
37 37 make all
38 38
39 39 %install
40 40 rm -rf $RPM_BUILD_ROOT
41 41 python setup.py install --root $RPM_BUILD_ROOT --prefix %{_prefix}
42 42 make install-doc DESTDIR=$RPM_BUILD_ROOT MANDIR=%{_mandir}
43 43
44 44 install contrib/hgk $RPM_BUILD_ROOT%{_bindir}
45 45 install contrib/convert-repo $RPM_BUILD_ROOT%{_bindir}/mercurial-convert-repo
46 46 install contrib/hg-ssh $RPM_BUILD_ROOT%{_bindir}
47 47 install contrib/git-viz/hg-viz $RPM_BUILD_ROOT%{_bindir}
48 48 install contrib/git-viz/git-rev-tree $RPM_BUILD_ROOT%{_bindir}
49 49
50 50 bash_completion_dir=$RPM_BUILD_ROOT%{_sysconfdir}/bash_completion.d
51 51 mkdir -p $bash_completion_dir
52 52 install -m 644 contrib/bash_completion $bash_completion_dir/mercurial.sh
53 53
54 54 zsh_completion_dir=$RPM_BUILD_ROOT%{_datadir}/zsh/site-functions
55 55 mkdir -p $zsh_completion_dir
56 56 install -m 644 contrib/zsh_completion $zsh_completion_dir/_mercurial
57 57
58 58 mkdir -p $RPM_BUILD_ROOT%{emacs_lispdir}
59 59 install contrib/mercurial.el $RPM_BUILD_ROOT%{emacs_lispdir}
60 60 install contrib/mq.el $RPM_BUILD_ROOT%{emacs_lispdir}
61 61
62 62 mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/mercurial/hgrc.d
63 63 install contrib/mergetools.hgrc $RPM_BUILD_ROOT%{_sysconfdir}/mercurial/hgrc.d/mergetools.rc
64 64
65 65 %clean
66 66 rm -rf $RPM_BUILD_ROOT
67 67
68 68 %files
69 69 %defattr(-,root,root,-)
70 70 %doc CONTRIBUTORS COPYING doc/README doc/hg*.txt doc/hg*.html doc/ja *.cgi contrib/*.fcgi
71 71 %doc %attr(644,root,root) %{_mandir}/man?/hg*
72 72 %doc %attr(644,root,root) contrib/*.svg contrib/sample.hgrc
73 73 %{_sysconfdir}/bash_completion.d/mercurial.sh
74 74 %{_datadir}/zsh/site-functions/_mercurial
75 75 %{_datadir}/emacs/site-lisp/mercurial.el
76 76 %{_datadir}/emacs/site-lisp/mq.el
77 77 %{_bindir}/hg
78 78 %{_bindir}/hgk
79 79 %{_bindir}/hg-ssh
80 80 %{_bindir}/hg-viz
81 81 %{_bindir}/git-rev-tree
82 82 %{_bindir}/mercurial-convert-repo
83 83 %dir %{_sysconfdir}/bash_completion.d/
84 84 %dir %{_datadir}/zsh/site-functions/
85 85 %dir %{_sysconfdir}/mercurial
86 86 %dir %{_sysconfdir}/mercurial/hgrc.d
87 87 %config(noreplace) %{_sysconfdir}/mercurial/hgrc.d/mergetools.rc
88 88 %if "%{?pythonver}" != "2.4"
89 89 %{_libdir}/python%{pythonver}/site-packages/%{name}-*-py%{pythonver}.egg-info
90 90 %endif
91 91 %{_libdir}/python%{pythonver}/site-packages/%{name}
92 92 %{_libdir}/python%{pythonver}/site-packages/hgext
@@ -1,418 +1,418 b''
1 1 ;;; mq.el --- Emacs support for Mercurial Queues
2 2
3 3 ;; Copyright (C) 2006 Bryan O'Sullivan
4 4
5 5 ;; Author: Bryan O'Sullivan <bos@serpentine.com>
6 6
7 7 ;; mq.el is free software; you can redistribute it and/or modify it
8 ;; under the terms of version 2 of the GNU General Public License as
9 ;; published by the Free Software Foundation.
8 ;; under the terms of the GNU General Public License version 2 or any
9 ;; later version.
10 10
11 11 ;; mq.el is distributed in the hope that it will be useful, but
12 12 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 14 ;; General Public License for more details.
15 15
16 16 ;; You should have received a copy of the GNU General Public License
17 17 ;; along with mq.el, GNU Emacs, or XEmacs; see the file COPYING (`C-h
18 18 ;; C-l'). If not, write to the Free Software Foundation, Inc., 59
19 19 ;; Temple Place - Suite 330, Boston, MA 02111-1307, USA.
20 20
21 21 (eval-when-compile (require 'cl))
22 22 (require 'mercurial)
23 23
24 24
25 25 (defcustom mq-mode-hook nil
26 26 "Hook run when a buffer enters mq-mode."
27 27 :type 'sexp
28 28 :group 'mercurial)
29 29
30 30 (defcustom mq-global-prefix "\C-cq"
31 31 "The global prefix for Mercurial Queues keymap bindings."
32 32 :type 'sexp
33 33 :group 'mercurial)
34 34
35 35 (defcustom mq-edit-mode-hook nil
36 36 "Hook run after a buffer is populated to edit a patch description."
37 37 :type 'sexp
38 38 :group 'mercurial)
39 39
40 40 (defcustom mq-edit-finish-hook nil
41 41 "Hook run before a patch description is finished up with."
42 42 :type 'sexp
43 43 :group 'mercurial)
44 44
45 45 (defcustom mq-signoff-address nil
46 46 "Address with which to sign off on a patch."
47 47 :type 'string
48 48 :group 'mercurial)
49 49
50 50
51 51 ;;; Internal variables.
52 52
53 53 (defvar mq-mode nil
54 54 "Is this file managed by MQ?")
55 55 (make-variable-buffer-local 'mq-mode)
56 56 (put 'mq-mode 'permanent-local t)
57 57
58 58 (defvar mq-patch-history nil)
59 59
60 60 (defvar mq-top-patch '(nil))
61 61
62 62 (defvar mq-prev-buffer nil)
63 63 (make-variable-buffer-local 'mq-prev-buffer)
64 64 (put 'mq-prev-buffer 'permanent-local t)
65 65
66 66 (defvar mq-top nil)
67 67 (make-variable-buffer-local 'mq-top)
68 68 (put 'mq-top 'permanent-local t)
69 69
70 70 ;;; Global keymap.
71 71
72 72 (defvar mq-global-map
73 73 (let ((map (make-sparse-keymap)))
74 74 (define-key map "." 'mq-push)
75 75 (define-key map ">" 'mq-push-all)
76 76 (define-key map "," 'mq-pop)
77 77 (define-key map "<" 'mq-pop-all)
78 78 (define-key map "=" 'mq-diff)
79 79 (define-key map "r" 'mq-refresh)
80 80 (define-key map "e" 'mq-refresh-edit)
81 81 (define-key map "i" 'mq-new)
82 82 (define-key map "n" 'mq-next)
83 83 (define-key map "o" 'mq-signoff)
84 84 (define-key map "p" 'mq-previous)
85 85 (define-key map "s" 'mq-edit-series)
86 86 (define-key map "t" 'mq-top)
87 87 map))
88 88
89 89 (global-set-key mq-global-prefix mq-global-map)
90 90
91 91 (add-minor-mode 'mq-mode 'mq-mode)
92 92
93 93
94 94 ;;; Refresh edit mode keymap.
95 95
96 96 (defvar mq-edit-mode-map
97 97 (let ((map (make-sparse-keymap)))
98 98 (define-key map "\C-c\C-c" 'mq-edit-finish)
99 99 (define-key map "\C-c\C-k" 'mq-edit-kill)
100 100 (define-key map "\C-c\C-s" 'mq-signoff)
101 101 map))
102 102
103 103
104 104 ;;; Helper functions.
105 105
106 106 (defun mq-read-patch-name (&optional source prompt force)
107 107 "Read a patch name to use with a command.
108 108 May return nil, meaning \"use the default\"."
109 109 (let ((patches (split-string
110 110 (hg-chomp (hg-run0 (or source "qseries"))) "\n")))
111 111 (when force
112 112 (completing-read (format "Patch%s: " (or prompt ""))
113 113 (mapcar (lambda (x) (cons x x)) patches)
114 114 nil
115 115 nil
116 116 nil
117 117 'mq-patch-history))))
118 118
119 119 (defun mq-refresh-buffers (root)
120 120 (save-excursion
121 121 (dolist (buf (hg-buffers-visiting-repo root))
122 122 (when (not (verify-visited-file-modtime buf))
123 123 (set-buffer buf)
124 124 (let ((ctx (hg-buffer-context)))
125 125 (message "Refreshing %s..." (buffer-name))
126 126 (revert-buffer t t t)
127 127 (hg-restore-context ctx)
128 128 (message "Refreshing %s...done" (buffer-name))))))
129 129 (hg-update-mode-lines root)
130 130 (mq-update-mode-lines root))
131 131
132 132 (defun mq-last-line ()
133 133 (goto-char (point-max))
134 134 (beginning-of-line)
135 135 (when (looking-at "^$")
136 136 (forward-line -1))
137 137 (let ((bol (point)))
138 138 (end-of-line)
139 139 (let ((line (buffer-substring bol (point))))
140 140 (when (> (length line) 0)
141 141 line))))
142 142
143 143 (defun mq-push (&optional patch)
144 144 "Push patches until PATCH is reached.
145 145 If PATCH is nil, push at most one patch."
146 146 (interactive (list (mq-read-patch-name "qunapplied" " to push"
147 147 current-prefix-arg)))
148 148 (let ((root (hg-root))
149 149 (prev-buf (current-buffer))
150 150 last-line ok)
151 151 (unless root
152 152 (error "Cannot push outside a repository!"))
153 153 (hg-sync-buffers root)
154 154 (let ((buf-name (format "MQ: Push %s" (or patch "next patch"))))
155 155 (kill-buffer (get-buffer-create buf-name))
156 156 (split-window-vertically)
157 157 (other-window 1)
158 158 (switch-to-buffer (get-buffer-create buf-name))
159 159 (cd root)
160 160 (message "Pushing...")
161 161 (setq ok (= 0 (apply 'call-process (hg-binary) nil t t "qpush"
162 162 (if patch (list patch))))
163 163 last-line (mq-last-line))
164 164 (let ((lines (count-lines (point-min) (point-max))))
165 165 (if (or (<= lines 1)
166 166 (and (equal lines 2) (string-match "Now at:" last-line)))
167 167 (progn
168 168 (kill-buffer (current-buffer))
169 169 (delete-window))
170 170 (hg-view-mode prev-buf))))
171 171 (mq-refresh-buffers root)
172 172 (sit-for 0)
173 173 (when last-line
174 174 (if ok
175 175 (message "Pushing... %s" last-line)
176 176 (error "Pushing... %s" last-line)))))
177 177
178 178 (defun mq-push-all ()
179 179 "Push patches until all are applied."
180 180 (interactive)
181 181 (mq-push "-a"))
182 182
183 183 (defun mq-pop (&optional patch)
184 184 "Pop patches until PATCH is reached.
185 185 If PATCH is nil, pop at most one patch."
186 186 (interactive (list (mq-read-patch-name "qapplied" " to pop to"
187 187 current-prefix-arg)))
188 188 (let ((root (hg-root))
189 189 last-line ok)
190 190 (unless root
191 191 (error "Cannot pop outside a repository!"))
192 192 (hg-sync-buffers root)
193 193 (set-buffer (generate-new-buffer "qpop"))
194 194 (cd root)
195 195 (message "Popping...")
196 196 (setq ok (= 0 (apply 'call-process (hg-binary) nil t t "qpop"
197 197 (if patch (list patch))))
198 198 last-line (mq-last-line))
199 199 (kill-buffer (current-buffer))
200 200 (mq-refresh-buffers root)
201 201 (sit-for 0)
202 202 (when last-line
203 203 (if ok
204 204 (message "Popping... %s" last-line)
205 205 (error "Popping... %s" last-line)))))
206 206
207 207 (defun mq-pop-all ()
208 208 "Push patches until none are applied."
209 209 (interactive)
210 210 (mq-pop "-a"))
211 211
212 212 (defun mq-refresh-internal (root &rest args)
213 213 (hg-sync-buffers root)
214 214 (let ((patch (mq-patch-info "qtop")))
215 215 (message "Refreshing %s..." patch)
216 216 (let ((ret (apply 'hg-run "qrefresh" args)))
217 217 (if (equal (car ret) 0)
218 218 (message "Refreshing %s... done." patch)
219 219 (error "Refreshing %s... %s" patch (hg-chomp (cdr ret)))))))
220 220
221 221 (defun mq-refresh (&optional git)
222 222 "Refresh the topmost applied patch.
223 223 With a prefix argument, generate a git-compatible patch."
224 224 (interactive "P")
225 225 (let ((root (hg-root)))
226 226 (unless root
227 227 (error "Cannot refresh outside of a repository!"))
228 228 (apply 'mq-refresh-internal root (if git '("--git")))))
229 229
230 230 (defun mq-patch-info (cmd &optional msg)
231 231 (let* ((ret (hg-run cmd))
232 232 (info (hg-chomp (cdr ret))))
233 233 (if (equal (car ret) 0)
234 234 (if msg
235 235 (message "%s patch: %s" msg info)
236 236 info)
237 237 (error "%s" info))))
238 238
239 239 (defun mq-top ()
240 240 "Print the name of the topmost applied patch."
241 241 (interactive)
242 242 (mq-patch-info "qtop" "Top"))
243 243
244 244 (defun mq-next ()
245 245 "Print the name of the next patch to be pushed."
246 246 (interactive)
247 247 (mq-patch-info "qnext" "Next"))
248 248
249 249 (defun mq-previous ()
250 250 "Print the name of the first patch below the topmost applied patch.
251 251 This would become the active patch if popped to."
252 252 (interactive)
253 253 (mq-patch-info "qprev" "Previous"))
254 254
255 255 (defun mq-edit-finish ()
256 256 "Finish editing the description of this patch, and refresh the patch."
257 257 (interactive)
258 258 (unless (equal (mq-patch-info "qtop") mq-top)
259 259 (error "Topmost patch has changed!"))
260 260 (hg-sync-buffers hg-root)
261 261 (run-hooks 'mq-edit-finish-hook)
262 262 (mq-refresh-internal hg-root "-m" (buffer-substring (point-min) (point-max)))
263 263 (let ((buf mq-prev-buffer))
264 264 (kill-buffer nil)
265 265 (switch-to-buffer buf)))
266 266
267 267 (defun mq-edit-kill ()
268 268 "Kill the edit currently being prepared."
269 269 (interactive)
270 270 (when (or (not (buffer-modified-p)) (y-or-n-p "Really kill this edit? "))
271 271 (let ((buf mq-prev-buffer))
272 272 (kill-buffer nil)
273 273 (switch-to-buffer buf))))
274 274
275 275 (defun mq-get-top (root)
276 276 (let ((entry (assoc root mq-top-patch)))
277 277 (if entry
278 278 (cdr entry))))
279 279
280 280 (defun mq-set-top (root patch)
281 281 (let ((entry (assoc root mq-top-patch)))
282 282 (if entry
283 283 (if patch
284 284 (setcdr entry patch)
285 285 (setq mq-top-patch (delq entry mq-top-patch)))
286 286 (setq mq-top-patch (cons (cons root patch) mq-top-patch)))))
287 287
288 288 (defun mq-update-mode-lines (root)
289 289 (let ((cwd default-directory))
290 290 (cd root)
291 291 (condition-case nil
292 292 (mq-set-top root (mq-patch-info "qtop"))
293 293 (error (mq-set-top root nil)))
294 294 (cd cwd))
295 295 (let ((patch (mq-get-top root)))
296 296 (save-excursion
297 297 (dolist (buf (hg-buffers-visiting-repo root))
298 298 (set-buffer buf)
299 299 (if mq-mode
300 300 (setq mq-mode (or (and patch (concat " MQ:" patch)) " MQ")))))))
301 301
302 302 (defun mq-mode (&optional arg)
303 303 "Minor mode for Mercurial repositories with an MQ patch queue"
304 304 (interactive "i")
305 305 (cond ((hg-root)
306 306 (setq mq-mode (if (null arg) (not mq-mode)
307 307 arg))
308 308 (mq-update-mode-lines (hg-root))))
309 309 (run-hooks 'mq-mode-hook))
310 310
311 311 (defun mq-edit-mode ()
312 312 "Mode for editing the description of a patch.
313 313
314 314 Key bindings
315 315 ------------
316 316 \\[mq-edit-finish] use this description
317 317 \\[mq-edit-kill] abandon this description"
318 318 (interactive)
319 319 (use-local-map mq-edit-mode-map)
320 320 (set-syntax-table text-mode-syntax-table)
321 321 (setq local-abbrev-table text-mode-abbrev-table
322 322 major-mode 'mq-edit-mode
323 323 mode-name "MQ-Edit")
324 324 (set-buffer-modified-p nil)
325 325 (setq buffer-undo-list nil)
326 326 (run-hooks 'text-mode-hook 'mq-edit-mode-hook))
327 327
328 328 (defun mq-refresh-edit ()
329 329 "Refresh the topmost applied patch, editing the patch description."
330 330 (interactive)
331 331 (while mq-prev-buffer
332 332 (set-buffer mq-prev-buffer))
333 333 (let ((root (hg-root))
334 334 (prev-buffer (current-buffer))
335 335 (patch (mq-patch-info "qtop")))
336 336 (hg-sync-buffers root)
337 337 (let ((buf-name (format "*MQ: Edit description of %s*" patch)))
338 338 (switch-to-buffer (get-buffer-create buf-name))
339 339 (when (= (point-min) (point-max))
340 340 (set (make-local-variable 'hg-root) root)
341 341 (set (make-local-variable 'mq-top) patch)
342 342 (setq mq-prev-buffer prev-buffer)
343 343 (insert (hg-run0 "qheader"))
344 344 (goto-char (point-min)))
345 345 (mq-edit-mode)
346 346 (cd root)))
347 347 (message "Type `C-c C-c' to finish editing and refresh the patch."))
348 348
349 349 (defun mq-new (name)
350 350 "Create a new empty patch named NAME.
351 351 The patch is applied on top of the current topmost patch.
352 352 With a prefix argument, forcibly create the patch even if the working
353 353 directory is modified."
354 354 (interactive (list (mq-read-patch-name "qseries" " to create" t)))
355 355 (message "Creating patch...")
356 356 (let ((ret (if current-prefix-arg
357 357 (hg-run "qnew" "-f" name)
358 358 (hg-run "qnew" name))))
359 359 (if (equal (car ret) 0)
360 360 (progn
361 361 (hg-update-mode-lines (buffer-file-name))
362 362 (message "Creating patch... done."))
363 363 (error "Creating patch... %s" (hg-chomp (cdr ret))))))
364 364
365 365 (defun mq-edit-series ()
366 366 "Edit the MQ series file directly."
367 367 (interactive)
368 368 (let ((root (hg-root)))
369 369 (unless root
370 370 (error "Not in an MQ repository!"))
371 371 (find-file (concat root ".hg/patches/series"))))
372 372
373 373 (defun mq-diff (&optional git)
374 374 "Display a diff of the topmost applied patch.
375 375 With a prefix argument, display a git-compatible diff."
376 376 (interactive "P")
377 377 (hg-view-output ((format "MQ: Diff of %s" (mq-patch-info "qtop")))
378 378 (if git
379 379 (call-process (hg-binary) nil t nil "qdiff" "--git")
380 380 (call-process (hg-binary) nil t nil "qdiff"))
381 381 (diff-mode)
382 382 (font-lock-fontify-buffer)))
383 383
384 384 (defun mq-signoff ()
385 385 "Sign off on the current patch, in the style used by the Linux kernel.
386 386 If the variable mq-signoff-address is non-nil, it will be used, otherwise
387 387 the value of the ui.username item from your hgrc will be used."
388 388 (interactive)
389 389 (let ((was-editing (eq major-mode 'mq-edit-mode))
390 390 signed)
391 391 (unless was-editing
392 392 (mq-refresh-edit))
393 393 (save-excursion
394 394 (let* ((user (or mq-signoff-address
395 395 (hg-run0 "debugconfig" "ui.username")))
396 396 (signoff (concat "Signed-off-by: " user)))
397 397 (if (search-forward signoff nil t)
398 398 (message "You have already signed off on this patch.")
399 399 (goto-char (point-max))
400 400 (let ((case-fold-search t))
401 401 (if (re-search-backward "^Signed-off-by: " nil t)
402 402 (forward-line 1)
403 403 (insert "\n")))
404 404 (insert signoff)
405 405 (message "%s" signoff)
406 406 (setq signed t))))
407 407 (unless was-editing
408 408 (if signed
409 409 (mq-edit-finish)
410 410 (mq-edit-kill)))))
411 411
412 412
413 413 (provide 'mq)
414 414
415 415
416 416 ;;; Local Variables:
417 417 ;;; prompt-to-byte-compile: nil
418 418 ;;; end:
@@ -1,163 +1,162 b''
1 1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
2 2 <html>
3 3 <head>
4 4 <title>Mercurial for Windows</title>
5 5 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" >
6 6 <style type="text/css">
7 7 <!--
8 8 html {
9 9 font-family: sans-serif;
10 10 margin: 1em 2em;
11 11 }
12 12
13 13 p {
14 14 margin-top: 0.5em;
15 15 margin-bottom: 0.5em;
16 16 }
17 17
18 18 pre {
19 19 margin: 0.25em 0em;
20 20 padding: 0.5em;
21 21 background-color: #EEE;
22 22 border: thin solid #CCC;
23 23 }
24 24
25 25 .indented {
26 26 padding-left: 10pt;
27 27 }
28 28 -->
29 29 </style>
30 30 </head>
31 31
32 32 <body>
33 33 <h1>Mercurial for Windows</h1>
34 34
35 35 <p>Welcome to Mercurial for Windows!</p>
36 36
37 37 <p>
38 38 Mercurial is a command-line application. You must run it from
39 39 the Windows command prompt (or if you're hard core, a <a
40 40 href="http://www.mingw.org/">MinGW</a> shell).
41 41 </p>
42 42
43 43 <p class="indented">
44 44 <i>Note: the standard <a href="http://www.mingw.org/">MinGW</a>
45 45 msys startup script uses rxvt which has problems setting up
46 46 standard input and output. Running bash directly works
47 47 correctly.</i>
48 48 </p>
49 49
50 50 <p>
51 51 For documentation, please visit the <a
52 52 href="http://mercurial.selenic.com/">Mercurial web site</a>.
53 53 You can also download a free book, <a
54 54 href="http://hgbook.red-bean.com/">Mercurial: The Definitive
55 55 Guide</a>.
56 56 </p>
57 57
58 58 <p>
59 59 By default, Mercurial installs to <tt>C:\Program
60 60 Files\Mercurial</tt>. The Mercurial command is called
61 61 <tt>hg.exe</tt>.
62 62 </p>
63 63
64 64 <h1>Testing Mercurial after you've installed it</h1>
65 65
66 66 <p>
67 67 The easiest way to check that Mercurial is installed properly is
68 68 to just type the following at the command prompt:
69 69 </p>
70 70
71 71 <pre>
72 72 hg
73 73 </pre>
74 74
75 75 <p>
76 76 This command should print a useful help message. If it does,
77 77 other Mercurial commands should work fine for you.
78 78 </p>
79 79
80 80 <h1>Configuration notes</h1>
81 81 <h4>Default editor</h4>
82 82 <p>
83 83 The default editor for commit messages is 'notepad'. You can set
84 84 the <tt>EDITOR</tt> (or <tt>HGEDITOR</tt>) environment variable
85 85 to specify your preference or set it in <tt>mercurial.ini</tt>:
86 86 </p>
87 87 <pre>
88 88 [ui]
89 89 editor = whatever
90 90 </pre>
91 91
92 92 <h4>Configuring a Merge program</h4>
93 93 <p>
94 94 It should be emphasized that Mercurial by itself doesn't attempt
95 95 to do a Merge at the file level, neither does it make any
96 96 attempt to Resolve the conflicts.
97 97 </p>
98 98
99 99 <p>
100 100 By default, Mercurial will use the merge program defined by the
101 101 <tt>HGMERGE</tt> environment variable, or uses the one defined
102 102 in the <tt>mercurial.ini</tt> file. (see <a
103 103 href="http://mercurial.selenic.com/wiki/MergeProgram">MergeProgram</a>
104 104 on the Mercurial Wiki for more information)
105 105 </p>
106 106
107 107 <h1>Reporting problems</h1>
108 108
109 109 <p>
110 110 Before you report any problems, please consult the <a
111 111 href="http://mercurial.selenic.com/">Mercurial web site</a>
112 112 and see if your question is already in our list of <a
113 113 href="http://mercurial.selenic.com/wiki/FAQ">Frequently
114 114 Answered Questions</a> (the "FAQ").
115 115 </p>
116 116
117 117 <p>
118 118 If you cannot find an answer to your question, please feel free
119 119 to send mail to the Mercurial mailing list, at <a
120 120 href="mailto:mercurial@selenic.com">mercurial@selenic.com</a>.
121 121 <b>Remember</b>, the more useful information you include in your
122 122 report, the easier it will be for us to help you!
123 123 </p>
124 124
125 125 <p>
126 126 If you are IRC-savvy, that's usually the fastest way to get
127 127 help. Go to <tt>#mercurial</tt> on <tt>irc.freenode.net</tt>.
128 128 </p>
129 129
130 130 <h1>Author and copyright information</h1>
131 131
132 132 <p>
133 133 Mercurial was written by <a href="http://www.selenic.com">Matt
134 134 Mackall</a>, and is maintained by Matt and a team of volunteers.
135 135 </p>
136 136
137 137 <p>
138 138 The Windows installer was written by <a
139 139 href="http://www.serpentine.com/blog">Bryan O'Sullivan</a>.
140 140 </p>
141 141
142 142 <p>
143 143 Mercurial is Copyright 2005-2010 Matt Mackall and others. See
144 144 the <tt>Contributors.txt</tt> file for a list of contributors.
145 145 </p>
146 146
147 147 <p>
148 148 Mercurial is free software; you can redistribute it and/or
149 149 modify it under the terms of the <a
150 150 href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt">GNU
151 General Public License version 2</a> as published by the Free
152 Software Foundation.
151 General Public License version 2</a> or any later version.
153 152 </p>
154 153
155 154 <p>
156 155 Mercurial is distributed in the hope that it will be useful, but
157 156 <b>without any warranty</b>; without even the implied warranty
158 157 of <b>merchantability</b> or <b>fitness for a particular
159 158 purpose</b>. See the GNU General Public License for more
160 159 details.
161 160 </p>
162 161 </body>
163 162 </html>
@@ -1,100 +1,100 b''
1 1 ====
2 2 hg
3 3 ====
4 4
5 5 ---------------------------------------
6 6 Mercurial source code management system
7 7 ---------------------------------------
8 8
9 9 :Author: Matt Mackall <mpm@selenic.com>
10 10 :Organization: Mercurial
11 11 :Manual section: 1
12 12 :Manual group: Mercurial Manual
13 13
14 14 .. contents::
15 15 :backlinks: top
16 16 :class: htmlonly
17 17
18 18
19 19 Synopsis
20 20 --------
21 21 **hg** *command* [*option*]... [*argument*]...
22 22
23 23 Description
24 24 -----------
25 25 The **hg** command provides a command line interface to the Mercurial
26 26 system.
27 27
28 28 Command Elements
29 29 ----------------
30 30
31 31 files...
32 32 indicates one or more filename or relative path filenames; see
33 33 `File Name Patterns`_ for information on pattern matching
34 34
35 35 path
36 36 indicates a path on the local machine
37 37
38 38 revision
39 39 indicates a changeset which can be specified as a changeset
40 40 revision number, a tag, or a unique substring of the changeset
41 41 hash value
42 42
43 43 repository path
44 44 either the pathname of a local repository or the URI of a remote
45 45 repository.
46 46
47 47 .. include:: hg.1.gendoc.txt
48 48
49 49 Files
50 50 -----
51 51
52 52 ``.hgignore``
53 53 This file contains regular expressions (one per line) that
54 54 describe file names that should be ignored by **hg**. For details,
55 55 see |hgignore(5)|_.
56 56
57 57 ``.hgtags``
58 58 This file contains changeset hash values and text tag names (one
59 59 of each separated by spaces) that correspond to tagged versions of
60 60 the repository contents.
61 61
62 62 ``/etc/mercurial/hgrc``, ``$HOME/.hgrc``, ``.hg/hgrc``
63 63 This file contains defaults and configuration. Values in
64 64 ``.hg/hgrc`` override those in ``$HOME/.hgrc``, and these override
65 65 settings made in the global ``/etc/mercurial/hgrc`` configuration.
66 66 See |hgrc(5)|_ for details of the contents and format of these
67 67 files.
68 68
69 69 Some commands (e.g. revert) produce backup files ending in ``.orig``,
70 70 if the ``.orig`` file already exists and is not tracked by Mercurial,
71 71 it will be overwritten.
72 72
73 73 Bugs
74 74 ----
75 75 Probably lots, please post them to the mailing list (see Resources_
76 76 below) when you find them.
77 77
78 78 See Also
79 79 --------
80 80 |hgignore(5)|_, |hgrc(5)|_
81 81
82 82 Author
83 83 ------
84 84 Written by Matt Mackall <mpm@selenic.com>
85 85
86 86 Resources
87 87 ---------
88 88 Main Web Site: http://mercurial.selenic.com/
89 89
90 90 Source code repository: http://selenic.com/hg
91 91
92 92 Mailing list: http://selenic.com/mailman/listinfo/mercurial
93 93
94 94 Copying
95 95 -------
96 96 Copyright (C) 2005-2010 Matt Mackall.
97 97 Free use of this software is granted under the terms of the GNU General
98 Public License version 2.
98 Public License version 2 or any later version.
99 99
100 100 .. include:: common.txt
@@ -1,111 +1,111 b''
1 1 ==========
2 2 hgignore
3 3 ==========
4 4
5 5 ---------------------------------
6 6 syntax for Mercurial ignore files
7 7 ---------------------------------
8 8
9 9 :Author: Vadim Gelfer <vadim.gelfer@gmail.com>
10 10 :Organization: Mercurial
11 11 :Manual section: 5
12 12 :Manual group: Mercurial Manual
13 13
14 14 Synopsis
15 15 --------
16 16
17 17 The Mercurial system uses a file called ``.hgignore`` in the root
18 18 directory of a repository to control its behavior when it searches
19 19 for files that it is not currently tracking.
20 20
21 21 Description
22 22 -----------
23 23
24 24 The working directory of a Mercurial repository will often contain
25 25 files that should not be tracked by Mercurial. These include backup
26 26 files created by editors and build products created by compilers.
27 27 These files can be ignored by listing them in a ``.hgignore`` file in
28 28 the root of the working directory. The ``.hgignore`` file must be
29 29 created manually. It is typically put under version control, so that
30 30 the settings will propagate to other repositories with push and pull.
31 31
32 32 An untracked file is ignored if its path relative to the repository
33 33 root directory, or any prefix path of that path, is matched against
34 34 any pattern in ``.hgignore``.
35 35
36 36 For example, say we have an untracked file, ``file.c``, at
37 37 ``a/b/file.c`` inside our repository. Mercurial will ignore ``file.c``
38 38 if any pattern in ``.hgignore`` matches ``a/b/file.c``, ``a/b`` or ``a``.
39 39
40 40 In addition, a Mercurial configuration file can reference a set of
41 41 per-user or global ignore files. See the |hgrc(5)|_ man page for details
42 42 of how to configure these files. Look for the "ignore" entry in the
43 43 "ui" section.
44 44
45 45 To control Mercurial's handling of files that it manages, see the
46 46 |hg(1)|_ man page. Look for the ``-I`` and ``-X`` options.
47 47
48 48 Syntax
49 49 ------
50 50
51 51 An ignore file is a plain text file consisting of a list of patterns,
52 52 with one pattern per line. Empty lines are skipped. The ``#``
53 53 character is treated as a comment character, and the ``\`` character
54 54 is treated as an escape character.
55 55
56 56 Mercurial supports several pattern syntaxes. The default syntax used
57 57 is Python/Perl-style regular expressions.
58 58
59 59 To change the syntax used, use a line of the following form::
60 60
61 61 syntax: NAME
62 62
63 63 where ``NAME`` is one of the following:
64 64
65 65 ``regexp``
66 66 Regular expression, Python/Perl syntax.
67 67 ``glob``
68 68 Shell-style glob.
69 69
70 70 The chosen syntax stays in effect when parsing all patterns that
71 71 follow, until another syntax is selected.
72 72
73 73 Neither glob nor regexp patterns are rooted. A glob-syntax pattern of
74 74 the form ``*.c`` will match a file ending in ``.c`` in any directory,
75 75 and a regexp pattern of the form ``\.c$`` will do the same. To root a
76 76 regexp pattern, start it with ``^``.
77 77
78 78 Example
79 79 -------
80 80
81 81 Here is an example ignore file. ::
82 82
83 83 # use glob syntax.
84 84 syntax: glob
85 85
86 86 *.elc
87 87 *.pyc
88 88 *~
89 89
90 90 # switch to regexp syntax.
91 91 syntax: regexp
92 92 ^\.pc/
93 93
94 94 Author
95 95 ------
96 96 Vadim Gelfer <vadim.gelfer@gmail.com>
97 97
98 98 Mercurial was written by Matt Mackall <mpm@selenic.com>.
99 99
100 100 See Also
101 101 --------
102 102 |hg(1)|_, |hgrc(5)|_
103 103
104 104 Copying
105 105 -------
106 106 This manual page is copyright 2006 Vadim Gelfer.
107 107 Mercurial is copyright 2005-2010 Matt Mackall.
108 108 Free use of this software is granted under the terms of the GNU General
109 Public License version 2.
109 Public License version 2 or any later version.
110 110
111 111 .. include:: common.txt
@@ -1,960 +1,960 b''
1 1 ======
2 2 hgrc
3 3 ======
4 4
5 5 ---------------------------------
6 6 configuration files for Mercurial
7 7 ---------------------------------
8 8
9 9 :Author: Bryan O'Sullivan <bos@serpentine.com>
10 10 :Organization: Mercurial
11 11 :Manual section: 5
12 12 :Manual group: Mercurial Manual
13 13
14 14 .. contents::
15 15 :backlinks: top
16 16 :class: htmlonly
17 17
18 18
19 19 Synopsis
20 20 --------
21 21
22 22 The Mercurial system uses a set of configuration files to control
23 23 aspects of its behavior.
24 24
25 25 Files
26 26 -----
27 27
28 28 Mercurial reads configuration data from several files, if they exist.
29 29 The names of these files depend on the system on which Mercurial is
30 30 installed. ``*.rc`` files from a single directory are read in
31 31 alphabetical order, later ones overriding earlier ones. Where multiple
32 32 paths are given below, settings from earlier paths override later
33 33 ones.
34 34
35 35 | (Unix, Windows) ``<repo>/.hg/hgrc``
36 36
37 37 Per-repository configuration options that only apply in a
38 38 particular repository. This file is not version-controlled, and
39 39 will not get transferred during a "clone" operation. Options in
40 40 this file override options in all other configuration files. On
41 41 Unix, most of this file will be ignored if it doesn't belong to a
42 42 trusted user or to a trusted group. See the documentation for the
43 43 trusted_ section below for more details.
44 44
45 45 | (Unix) ``$HOME/.hgrc``
46 46 | (Windows) ``%USERPROFILE%\.hgrc``
47 47 | (Windows) ``%USERPROFILE%\Mercurial.ini``
48 48 | (Windows) ``%HOME%\.hgrc``
49 49 | (Windows) ``%HOME%\Mercurial.ini``
50 50
51 51 Per-user configuration file(s), for the user running Mercurial. On
52 52 Windows 9x, ``%HOME%`` is replaced by ``%APPDATA%``. Options in these
53 53 files apply to all Mercurial commands executed by this user in any
54 54 directory. Options in these files override per-system and per-installation
55 55 options.
56 56
57 57 | (Unix) ``/etc/mercurial/hgrc``
58 58 | (Unix) ``/etc/mercurial/hgrc.d/*.rc``
59 59
60 60 Per-system configuration files, for the system on which Mercurial
61 61 is running. Options in these files apply to all Mercurial commands
62 62 executed by any user in any directory. Options in these files
63 63 override per-installation options.
64 64
65 65 | (Unix) ``<install-root>/etc/mercurial/hgrc``
66 66 | (Unix) ``<install-root>/etc/mercurial/hgrc.d/*.rc``
67 67
68 68 Per-installation configuration files, searched for in the
69 69 directory where Mercurial is installed. ``<install-root>`` is the
70 70 parent directory of the **hg** executable (or symlink) being run. For
71 71 example, if installed in ``/shared/tools/bin/hg``, Mercurial will look
72 72 in ``/shared/tools/etc/mercurial/hgrc``. Options in these files apply
73 73 to all Mercurial commands executed by any user in any directory.
74 74
75 75 | (Windows) ``C:\Mercurial\Mercurial.ini``
76 76 | (Windows) ``HKEY_LOCAL_MACHINE\SOFTWARE\Mercurial``
77 77 | (Windows) ``<install-dir>\Mercurial.ini``
78 78
79 79 Per-installation/system configuration files, for the system on
80 80 which Mercurial is running. Options in these files apply to all
81 81 Mercurial commands executed by any user in any directory. Registry
82 82 keys contain PATH-like strings, every part of which must reference
83 83 a ``Mercurial.ini`` file or be a directory where ``*.rc`` files will
84 84 be read.
85 85
86 86 Syntax
87 87 ------
88 88
89 89 A configuration file consists of sections, led by a ``[section]`` header
90 90 and followed by ``name = value`` entries::
91 91
92 92 [spam]
93 93 eggs=ham
94 94 green=
95 95 eggs
96 96
97 97 Each line contains one entry. If the lines that follow are indented,
98 98 they are treated as continuations of that entry. Leading whitespace is
99 99 removed from values. Empty lines are skipped. Lines beginning with
100 100 ``#`` or ``;`` are ignored and may be used to provide comments.
101 101
102 102 A line of the form ``%include file`` will include ``file`` into the
103 103 current configuration file. The inclusion is recursive, which means
104 104 that included files can include other files. Filenames are relative to
105 105 the configuration file in which the ``%include`` directive is found.
106 106
107 107 A line with ``%unset name`` will remove ``name`` from the current
108 108 section, if it has been set previously.
109 109
110 110
111 111 Sections
112 112 --------
113 113
114 114 This section describes the different sections that may appear in a
115 115 Mercurial "hgrc" file, the purpose of each section, its possible keys,
116 116 and their possible values.
117 117
118 118 ``alias``
119 119 """""""""
120 120 Defines command aliases.
121 121 Aliases allow you to define your own commands in terms of other
122 122 commands (or aliases), optionally including arguments.
123 123
124 124 Alias definitions consist of lines of the form::
125 125
126 126 <alias> = <command> [<argument]...
127 127
128 128 For example, this definition::
129 129
130 130 latest = log --limit 5
131 131
132 132 creates a new command ``latest`` that shows only the five most recent
133 133 changesets. You can define subsequent aliases using earlier ones::
134 134
135 135 stable5 = latest -b stable
136 136
137 137 .. note:: It is possible to create aliases with the same names as
138 138 existing commands, which will then override the original
139 139 definitions. This is almost always a bad idea!
140 140
141 141
142 142 ``auth``
143 143 """"""""
144 144 Authentication credentials for HTTP authentication. Each line has
145 145 the following format::
146 146
147 147 <name>.<argument> = <value>
148 148
149 149 where ``<name>`` is used to group arguments into authentication
150 150 entries. Example::
151 151
152 152 foo.prefix = hg.intevation.org/mercurial
153 153 foo.username = foo
154 154 foo.password = bar
155 155 foo.schemes = http https
156 156
157 157 bar.prefix = secure.example.org
158 158 bar.key = path/to/file.key
159 159 bar.cert = path/to/file.cert
160 160 bar.schemes = https
161 161
162 162 Supported arguments:
163 163
164 164 ``prefix``
165 165 Either ``*`` or a URI prefix with or without the scheme part.
166 166 The authentication entry with the longest matching prefix is used
167 167 (where ``*`` matches everything and counts as a match of length
168 168 1). If the prefix doesn't include a scheme, the match is performed
169 169 against the URI with its scheme stripped as well, and the schemes
170 170 argument, q.v., is then subsequently consulted.
171 171 ``username``
172 172 Optional. Username to authenticate with. If not given, and the
173 173 remote site requires basic or digest authentication, the user
174 174 will be prompted for it.
175 175 ``password``
176 176 Optional. Password to authenticate with. If not given, and the
177 177 remote site requires basic or digest authentication, the user
178 178 will be prompted for it.
179 179 ``key``
180 180 Optional. PEM encoded client certificate key file.
181 181 ``cert``
182 182 Optional. PEM encoded client certificate chain file.
183 183 ``schemes``
184 184 Optional. Space separated list of URI schemes to use this
185 185 authentication entry with. Only used if the prefix doesn't include
186 186 a scheme. Supported schemes are http and https. They will match
187 187 static-http and static-https respectively, as well.
188 188 Default: https.
189 189
190 190 If no suitable authentication entry is found, the user is prompted
191 191 for credentials as usual if required by the remote.
192 192
193 193
194 194 ``decode/encode``
195 195 """""""""""""""""
196 196 Filters for transforming files on checkout/checkin. This would
197 197 typically be used for newline processing or other
198 198 localization/canonicalization of files.
199 199
200 200 Filters consist of a filter pattern followed by a filter command.
201 201 Filter patterns are globs by default, rooted at the repository root.
202 202 For example, to match any file ending in ``.txt`` in the root
203 203 directory only, use the pattern ``*.txt``. To match any file ending
204 204 in ``.c`` anywhere in the repository, use the pattern ``**.c``.
205 205 For each file only the first matching filter applies.
206 206
207 207 The filter command can start with a specifier, either ``pipe:`` or
208 208 ``tempfile:``. If no specifier is given, ``pipe:`` is used by default.
209 209
210 210 A ``pipe:`` command must accept data on stdin and return the transformed
211 211 data on stdout.
212 212
213 213 Pipe example::
214 214
215 215 [encode]
216 216 # uncompress gzip files on checkin to improve delta compression
217 217 # note: not necessarily a good idea, just an example
218 218 *.gz = pipe: gunzip
219 219
220 220 [decode]
221 221 # recompress gzip files when writing them to the working dir (we
222 222 # can safely omit "pipe:", because it's the default)
223 223 *.gz = gzip
224 224
225 225 A ``tempfile:`` command is a template. The string ``INFILE`` is replaced
226 226 with the name of a temporary file that contains the data to be
227 227 filtered by the command. The string ``OUTFILE`` is replaced with the name
228 228 of an empty temporary file, where the filtered data must be written by
229 229 the command.
230 230
231 231 .. note:: The tempfile mechanism is recommended for Windows systems,
232 232 where the standard shell I/O redirection operators often have
233 233 strange effects and may corrupt the contents of your files.
234 234
235 235 The most common usage is for LF <-> CRLF translation on Windows. For
236 236 this, use the "smart" converters which check for binary files::
237 237
238 238 [extensions]
239 239 hgext.win32text =
240 240 [encode]
241 241 ** = cleverencode:
242 242 [decode]
243 243 ** = cleverdecode:
244 244
245 245 or if you only want to translate certain files::
246 246
247 247 [extensions]
248 248 hgext.win32text =
249 249 [encode]
250 250 **.txt = dumbencode:
251 251 [decode]
252 252 **.txt = dumbdecode:
253 253
254 254
255 255 ``defaults``
256 256 """"""""""""
257 257
258 258 (defaults are deprecated. Don't use them. Use aliases instead)
259 259
260 260 Use the ``[defaults]`` section to define command defaults, i.e. the
261 261 default options/arguments to pass to the specified commands.
262 262
263 263 The following example makes ``hg log`` run in verbose mode, and ``hg
264 264 status`` show only the modified files, by default::
265 265
266 266 [defaults]
267 267 log = -v
268 268 status = -m
269 269
270 270 The actual commands, instead of their aliases, must be used when
271 271 defining command defaults. The command defaults will also be applied
272 272 to the aliases of the commands defined.
273 273
274 274
275 275 ``diff``
276 276 """"""""
277 277
278 278 Settings used when displaying diffs. They are all Boolean and
279 279 defaults to False.
280 280
281 281 ``git``
282 282 Use git extended diff format.
283 283 ``nodates``
284 284 Don't include dates in diff headers.
285 285 ``showfunc``
286 286 Show which function each change is in.
287 287 ``ignorews``
288 288 Ignore white space when comparing lines.
289 289 ``ignorewsamount``
290 290 Ignore changes in the amount of white space.
291 291 ``ignoreblanklines``
292 292 Ignore changes whose lines are all blank.
293 293
294 294 ``email``
295 295 """""""""
296 296 Settings for extensions that send email messages.
297 297
298 298 ``from``
299 299 Optional. Email address to use in "From" header and SMTP envelope
300 300 of outgoing messages.
301 301 ``to``
302 302 Optional. Comma-separated list of recipients' email addresses.
303 303 ``cc``
304 304 Optional. Comma-separated list of carbon copy recipients'
305 305 email addresses.
306 306 ``bcc``
307 307 Optional. Comma-separated list of blind carbon copy recipients'
308 308 email addresses. Cannot be set interactively.
309 309 ``method``
310 310 Optional. Method to use to send email messages. If value is ``smtp``
311 311 (default), use SMTP (see the SMTP_ section for configuration).
312 312 Otherwise, use as name of program to run that acts like sendmail
313 313 (takes ``-f`` option for sender, list of recipients on command line,
314 314 message on stdin). Normally, setting this to ``sendmail`` or
315 315 ``/usr/sbin/sendmail`` is enough to use sendmail to send messages.
316 316 ``charsets``
317 317 Optional. Comma-separated list of character sets considered
318 318 convenient for recipients. Addresses, headers, and parts not
319 319 containing patches of outgoing messages will be encoded in the
320 320 first character set to which conversion from local encoding
321 321 (``$HGENCODING``, ``ui.fallbackencoding``) succeeds. If correct
322 322 conversion fails, the text in question is sent as is. Defaults to
323 323 empty (explicit) list.
324 324
325 325 Order of outgoing email character sets:
326 326
327 327 1. ``us-ascii``: always first, regardless of settings
328 328 2. ``email.charsets``: in order given by user
329 329 3. ``ui.fallbackencoding``: if not in email.charsets
330 330 4. ``$HGENCODING``: if not in email.charsets
331 331 5. ``utf-8``: always last, regardless of settings
332 332
333 333 Email example::
334 334
335 335 [email]
336 336 from = Joseph User <joe.user@example.com>
337 337 method = /usr/sbin/sendmail
338 338 # charsets for western Europeans
339 339 # us-ascii, utf-8 omitted, as they are tried first and last
340 340 charsets = iso-8859-1, iso-8859-15, windows-1252
341 341
342 342
343 343 ``extensions``
344 344 """"""""""""""
345 345
346 346 Mercurial has an extension mechanism for adding new features. To
347 347 enable an extension, create an entry for it in this section.
348 348
349 349 If you know that the extension is already in Python's search path,
350 350 you can give the name of the module, followed by ``=``, with nothing
351 351 after the ``=``.
352 352
353 353 Otherwise, give a name that you choose, followed by ``=``, followed by
354 354 the path to the ``.py`` file (including the file name extension) that
355 355 defines the extension.
356 356
357 357 To explicitly disable an extension that is enabled in an hgrc of
358 358 broader scope, prepend its path with ``!``, as in
359 359 ``hgext.foo = !/ext/path`` or ``hgext.foo = !`` when path is not
360 360 supplied.
361 361
362 362 Example for ``~/.hgrc``::
363 363
364 364 [extensions]
365 365 # (the mq extension will get loaded from Mercurial's path)
366 366 hgext.mq =
367 367 # (this extension will get loaded from the file specified)
368 368 myfeature = ~/.hgext/myfeature.py
369 369
370 370
371 371 ``format``
372 372 """"""""""
373 373
374 374 ``usestore``
375 375 Enable or disable the "store" repository format which improves
376 376 compatibility with systems that fold case or otherwise mangle
377 377 filenames. Enabled by default. Disabling this option will allow
378 378 you to store longer filenames in some situations at the expense of
379 379 compatibility and ensures that the on-disk format of newly created
380 380 repositories will be compatible with Mercurial before version 0.9.4.
381 381
382 382 ``usefncache``
383 383 Enable or disable the "fncache" repository format which enhances
384 384 the "store" repository format (which has to be enabled to use
385 385 fncache) to allow longer filenames and avoids using Windows
386 386 reserved names, e.g. "nul". Enabled by default. Disabling this
387 387 option ensures that the on-disk format of newly created
388 388 repositories will be compatible with Mercurial before version 1.1.
389 389
390 390 ``merge-patterns``
391 391 """"""""""""""""""
392 392
393 393 This section specifies merge tools to associate with particular file
394 394 patterns. Tools matched here will take precedence over the default
395 395 merge tool. Patterns are globs by default, rooted at the repository
396 396 root.
397 397
398 398 Example::
399 399
400 400 [merge-patterns]
401 401 **.c = kdiff3
402 402 **.jpg = myimgmerge
403 403
404 404 ``merge-tools``
405 405 """""""""""""""
406 406
407 407 This section configures external merge tools to use for file-level
408 408 merges.
409 409
410 410 Example ``~/.hgrc``::
411 411
412 412 [merge-tools]
413 413 # Override stock tool location
414 414 kdiff3.executable = ~/bin/kdiff3
415 415 # Specify command line
416 416 kdiff3.args = $base $local $other -o $output
417 417 # Give higher priority
418 418 kdiff3.priority = 1
419 419
420 420 # Define new tool
421 421 myHtmlTool.args = -m $local $other $base $output
422 422 myHtmlTool.regkey = Software\FooSoftware\HtmlMerge
423 423 myHtmlTool.priority = 1
424 424
425 425 Supported arguments:
426 426
427 427 ``priority``
428 428 The priority in which to evaluate this tool.
429 429 Default: 0.
430 430 ``executable``
431 431 Either just the name of the executable or its pathname.
432 432 Default: the tool name.
433 433 ``args``
434 434 The arguments to pass to the tool executable. You can refer to the
435 435 files being merged as well as the output file through these
436 436 variables: ``$base``, ``$local``, ``$other``, ``$output``.
437 437 Default: ``$local $base $other``
438 438 ``premerge``
439 439 Attempt to run internal non-interactive 3-way merge tool before
440 440 launching external tool.
441 441 Default: True
442 442 ``binary``
443 443 This tool can merge binary files. Defaults to False, unless tool
444 444 was selected by file pattern match.
445 445 ``symlink``
446 446 This tool can merge symlinks. Defaults to False, even if tool was
447 447 selected by file pattern match.
448 448 ``checkconflicts``
449 449 Check whether there are conflicts even though the tool reported
450 450 success.
451 451 Default: False
452 452 ``checkchanged``
453 453 Check whether outputs were written even though the tool reported
454 454 success.
455 455 Default: False
456 456 ``fixeol``
457 457 Attempt to fix up EOL changes caused by the merge tool.
458 458 Default: False
459 459 ``gui``
460 460 This tool requires a graphical interface to run. Default: False
461 461 ``regkey``
462 462 Windows registry key which describes install location of this
463 463 tool. Mercurial will search for this key first under
464 464 ``HKEY_CURRENT_USER`` and then under ``HKEY_LOCAL_MACHINE``.
465 465 Default: None
466 466 ``regname``
467 467 Name of value to read from specified registry key. Defaults to the
468 468 unnamed (default) value.
469 469 ``regappend``
470 470 String to append to the value read from the registry, typically
471 471 the executable name of the tool.
472 472 Default: None
473 473
474 474
475 475 ``hooks``
476 476 """""""""
477 477 Commands or Python functions that get automatically executed by
478 478 various actions such as starting or finishing a commit. Multiple
479 479 hooks can be run for the same action by appending a suffix to the
480 480 action. Overriding a site-wide hook can be done by changing its
481 481 value or setting it to an empty string.
482 482
483 483 Example ``.hg/hgrc``::
484 484
485 485 [hooks]
486 486 # update working directory after adding changesets
487 487 changegroup.update = hg update
488 488 # do not use the site-wide hook
489 489 incoming =
490 490 incoming.email = /my/email/hook
491 491 incoming.autobuild = /my/build/hook
492 492
493 493 Most hooks are run with environment variables set that give useful
494 494 additional information. For each hook below, the environment
495 495 variables it is passed are listed with names of the form ``$HG_foo``.
496 496
497 497 ``changegroup``
498 498 Run after a changegroup has been added via push, pull or unbundle.
499 499 ID of the first new changeset is in ``$HG_NODE``. URL from which
500 500 changes came is in ``$HG_URL``.
501 501 ``commit``
502 502 Run after a changeset has been created in the local repository. ID
503 503 of the newly created changeset is in ``$HG_NODE``. Parent changeset
504 504 IDs are in ``$HG_PARENT1`` and ``$HG_PARENT2``.
505 505 ``incoming``
506 506 Run after a changeset has been pulled, pushed, or unbundled into
507 507 the local repository. The ID of the newly arrived changeset is in
508 508 ``$HG_NODE``. URL that was source of changes came is in ``$HG_URL``.
509 509 ``outgoing``
510 510 Run after sending changes from local repository to another. ID of
511 511 first changeset sent is in ``$HG_NODE``. Source of operation is in
512 512 ``$HG_SOURCE``; see "preoutgoing" hook for description.
513 513 ``post-<command>``
514 514 Run after successful invocations of the associated command. The
515 515 contents of the command line are passed as ``$HG_ARGS`` and the result
516 516 code in ``$HG_RESULT``. Hook failure is ignored.
517 517 ``pre-<command>``
518 518 Run before executing the associated command. The contents of the
519 519 command line are passed as ``$HG_ARGS``. If the hook returns failure,
520 520 the command doesn't execute and Mercurial returns the failure
521 521 code.
522 522 ``prechangegroup``
523 523 Run before a changegroup is added via push, pull or unbundle. Exit
524 524 status 0 allows the changegroup to proceed. Non-zero status will
525 525 cause the push, pull or unbundle to fail. URL from which changes
526 526 will come is in ``$HG_URL``.
527 527 ``precommit``
528 528 Run before starting a local commit. Exit status 0 allows the
529 529 commit to proceed. Non-zero status will cause the commit to fail.
530 530 Parent changeset IDs are in ``$HG_PARENT1`` and ``$HG_PARENT2``.
531 531 ``preoutgoing``
532 532 Run before collecting changes to send from the local repository to
533 533 another. Non-zero status will cause failure. This lets you prevent
534 534 pull over HTTP or SSH. Also prevents against local pull, push
535 535 (outbound) or bundle commands, but not effective, since you can
536 536 just copy files instead then. Source of operation is in
537 537 ``$HG_SOURCE``. If "serve", operation is happening on behalf of remote
538 538 SSH or HTTP repository. If "push", "pull" or "bundle", operation
539 539 is happening on behalf of repository on same system.
540 540 ``pretag``
541 541 Run before creating a tag. Exit status 0 allows the tag to be
542 542 created. Non-zero status will cause the tag to fail. ID of
543 543 changeset to tag is in ``$HG_NODE``. Name of tag is in ``$HG_TAG``. Tag is
544 544 local if ``$HG_LOCAL=1``, in repository if ``$HG_LOCAL=0``.
545 545 ``pretxnchangegroup``
546 546 Run after a changegroup has been added via push, pull or unbundle,
547 547 but before the transaction has been committed. Changegroup is
548 548 visible to hook program. This lets you validate incoming changes
549 549 before accepting them. Passed the ID of the first new changeset in
550 550 ``$HG_NODE``. Exit status 0 allows the transaction to commit. Non-zero
551 551 status will cause the transaction to be rolled back and the push,
552 552 pull or unbundle will fail. URL that was source of changes is in
553 553 ``$HG_URL``.
554 554 ``pretxncommit``
555 555 Run after a changeset has been created but the transaction not yet
556 556 committed. Changeset is visible to hook program. This lets you
557 557 validate commit message and changes. Exit status 0 allows the
558 558 commit to proceed. Non-zero status will cause the transaction to
559 559 be rolled back. ID of changeset is in ``$HG_NODE``. Parent changeset
560 560 IDs are in ``$HG_PARENT1`` and ``$HG_PARENT2``.
561 561 ``preupdate``
562 562 Run before updating the working directory. Exit status 0 allows
563 563 the update to proceed. Non-zero status will prevent the update.
564 564 Changeset ID of first new parent is in ``$HG_PARENT1``. If merge, ID
565 565 of second new parent is in ``$HG_PARENT2``.
566 566 ``tag``
567 567 Run after a tag is created. ID of tagged changeset is in ``$HG_NODE``.
568 568 Name of tag is in ``$HG_TAG``. Tag is local if ``$HG_LOCAL=1``, in
569 569 repository if ``$HG_LOCAL=0``.
570 570 ``update``
571 571 Run after updating the working directory. Changeset ID of first
572 572 new parent is in ``$HG_PARENT1``. If merge, ID of second new parent is
573 573 in ``$HG_PARENT2``. If the update succeeded, ``$HG_ERROR=0``. If the
574 574 update failed (e.g. because conflicts not resolved), ``$HG_ERROR=1``.
575 575
576 576 .. note:: It is generally better to use standard hooks rather than the
577 577 generic pre- and post- command hooks as they are guaranteed to be
578 578 called in the appropriate contexts for influencing transactions.
579 579 Also, hooks like "commit" will be called in all contexts that
580 580 generate a commit (e.g. tag) and not just the commit command.
581 581
582 582 .. note:: Environment variables with empty values may not be passed to
583 583 hooks on platforms such as Windows. As an example, ``$HG_PARENT2``
584 584 will have an empty value under Unix-like platforms for non-merge
585 585 changesets, while it will not be available at all under Windows.
586 586
587 587 The syntax for Python hooks is as follows::
588 588
589 589 hookname = python:modulename.submodule.callable
590 590 hookname = python:/path/to/python/module.py:callable
591 591
592 592 Python hooks are run within the Mercurial process. Each hook is
593 593 called with at least three keyword arguments: a ui object (keyword
594 594 ``ui``), a repository object (keyword ``repo``), and a ``hooktype``
595 595 keyword that tells what kind of hook is used. Arguments listed as
596 596 environment variables above are passed as keyword arguments, with no
597 597 ``HG_`` prefix, and names in lower case.
598 598
599 599 If a Python hook returns a "true" value or raises an exception, this
600 600 is treated as a failure.
601 601
602 602
603 603 ``http_proxy``
604 604 """"""""""""""
605 605 Used to access web-based Mercurial repositories through a HTTP
606 606 proxy.
607 607
608 608 ``host``
609 609 Host name and (optional) port of the proxy server, for example
610 610 "myproxy:8000".
611 611 ``no``
612 612 Optional. Comma-separated list of host names that should bypass
613 613 the proxy.
614 614 ``passwd``
615 615 Optional. Password to authenticate with at the proxy server.
616 616 ``user``
617 617 Optional. User name to authenticate with at the proxy server.
618 618
619 619 ``smtp``
620 620 """"""""
621 621 Configuration for extensions that need to send email messages.
622 622
623 623 ``host``
624 624 Host name of mail server, e.g. "mail.example.com".
625 625 ``port``
626 626 Optional. Port to connect to on mail server. Default: 25.
627 627 ``tls``
628 628 Optional. Whether to connect to mail server using TLS. True or
629 629 False. Default: False.
630 630 ``username``
631 631 Optional. User name to authenticate to SMTP server with. If
632 632 username is specified, password must also be specified.
633 633 Default: none.
634 634 ``password``
635 635 Optional. Password to authenticate to SMTP server with. If
636 636 username is specified, password must also be specified.
637 637 Default: none.
638 638 ``local_hostname``
639 639 Optional. It's the hostname that the sender can use to identify
640 640 itself to the MTA.
641 641
642 642
643 643 ``patch``
644 644 """""""""
645 645 Settings used when applying patches, for instance through the 'import'
646 646 command or with Mercurial Queues extension.
647 647
648 648 ``eol``
649 649 When set to 'strict' patch content and patched files end of lines
650 650 are preserved. When set to ``lf`` or ``crlf``, both files end of
651 651 lines are ignored when patching and the result line endings are
652 652 normalized to either LF (Unix) or CRLF (Windows). When set to
653 653 ``auto``, end of lines are again ignored while patching but line
654 654 endings in patched files are normalized to their original setting
655 655 on a per-file basis. If target file does not exist or has no end
656 656 of line, patch line endings are preserved.
657 657 Default: strict.
658 658
659 659
660 660 ``paths``
661 661 """""""""
662 662 Assigns symbolic names to repositories. The left side is the
663 663 symbolic name, and the right gives the directory or URL that is the
664 664 location of the repository. Default paths can be declared by setting
665 665 the following entries.
666 666
667 667 ``default``
668 668 Directory or URL to use when pulling if no source is specified.
669 669 Default is set to repository from which the current repository was
670 670 cloned.
671 671 ``default-push``
672 672 Optional. Directory or URL to use when pushing if no destination
673 673 is specified.
674 674
675 675
676 676 ``profiling``
677 677 """""""""""""
678 678 Specifies profiling format and file output. In this section
679 679 description, 'profiling data' stands for the raw data collected
680 680 during profiling, while 'profiling report' stands for a statistical
681 681 text report generated from the profiling data. The profiling is done
682 682 using lsprof.
683 683
684 684 ``format``
685 685 Profiling format.
686 686 Default: text.
687 687
688 688 ``text``
689 689 Generate a profiling report. When saving to a file, it should be
690 690 noted that only the report is saved, and the profiling data is
691 691 not kept.
692 692 ``kcachegrind``
693 693 Format profiling data for kcachegrind use: when saving to a
694 694 file, the generated file can directly be loaded into
695 695 kcachegrind.
696 696 ``output``
697 697 File path where profiling data or report should be saved. If the
698 698 file exists, it is replaced. Default: None, data is printed on
699 699 stderr
700 700
701 701 ``server``
702 702 """"""""""
703 703 Controls generic server settings.
704 704
705 705 ``uncompressed``
706 706 Whether to allow clients to clone a repository using the
707 707 uncompressed streaming protocol. This transfers about 40% more
708 708 data than a regular clone, but uses less memory and CPU on both
709 709 server and client. Over a LAN (100 Mbps or better) or a very fast
710 710 WAN, an uncompressed streaming clone is a lot faster (~10x) than a
711 711 regular clone. Over most WAN connections (anything slower than
712 712 about 6 Mbps), uncompressed streaming is slower, because of the
713 713 extra data transfer overhead. Default is False.
714 714
715 715
716 716 ``trusted``
717 717 """""""""""
718 718 For security reasons, Mercurial will not use the settings in the
719 719 ``.hg/hgrc`` file from a repository if it doesn't belong to a trusted
720 720 user or to a trusted group. The main exception is the web interface,
721 721 which automatically uses some safe settings, since it's common to
722 722 serve repositories from different users.
723 723
724 724 This section specifies what users and groups are trusted. The
725 725 current user is always trusted. To trust everybody, list a user or a
726 726 group with name ``*``.
727 727
728 728 ``users``
729 729 Comma-separated list of trusted users.
730 730 ``groups``
731 731 Comma-separated list of trusted groups.
732 732
733 733
734 734 ``ui``
735 735 """"""
736 736
737 737 User interface controls.
738 738
739 739 ``archivemeta``
740 740 Whether to include the .hg_archival.txt file containing meta data
741 741 (hashes for the repository base and for tip) in archives created
742 742 by the hg archive command or downloaded via hgweb.
743 743 Default is True.
744 744 ``askusername``
745 745 Whether to prompt for a username when committing. If True, and
746 746 neither ``$HGUSER`` nor ``$EMAIL`` has been specified, then the user will
747 747 be prompted to enter a username. If no username is entered, the
748 748 default ``USER@HOST`` is used instead.
749 749 Default is False.
750 750 ``debug``
751 751 Print debugging information. True or False. Default is False.
752 752 ``editor``
753 753 The editor to use during a commit. Default is ``$EDITOR`` or ``vi``.
754 754 ``fallbackencoding``
755 755 Encoding to try if it's not possible to decode the changelog using
756 756 UTF-8. Default is ISO-8859-1.
757 757 ``ignore``
758 758 A file to read per-user ignore patterns from. This file should be
759 759 in the same format as a repository-wide .hgignore file. This
760 760 option supports hook syntax, so if you want to specify multiple
761 761 ignore files, you can do so by setting something like
762 762 ``ignore.other = ~/.hgignore2``. For details of the ignore file
763 763 format, see the |hgignore(5)|_ man page.
764 764 ``interactive``
765 765 Allow to prompt the user. True or False. Default is True.
766 766 ``logtemplate``
767 767 Template string for commands that print changesets.
768 768 ``merge``
769 769 The conflict resolution program to use during a manual merge.
770 770 There are some internal tools available:
771 771
772 772 ``internal:local``
773 773 keep the local version
774 774 ``internal:other``
775 775 use the other version
776 776 ``internal:merge``
777 777 use the internal non-interactive merge tool
778 778 ``internal:fail``
779 779 fail to merge
780 780
781 781 For more information on configuring merge tools see the
782 782 merge-tools_ section.
783 783
784 784 ``patch``
785 785 command to use to apply patches. Look for ``gpatch`` or ``patch`` in
786 786 PATH if unset.
787 787 ``quiet``
788 788 Reduce the amount of output printed. True or False. Default is False.
789 789 ``remotecmd``
790 790 remote command to use for clone/push/pull operations. Default is ``hg``.
791 791 ``report_untrusted``
792 792 Warn if a ``.hg/hgrc`` file is ignored due to not being owned by a
793 793 trusted user or group. True or False. Default is True.
794 794 ``slash``
795 795 Display paths using a slash (``/``) as the path separator. This
796 796 only makes a difference on systems where the default path
797 797 separator is not the slash character (e.g. Windows uses the
798 798 backslash character (``\``)).
799 799 Default is False.
800 800 ``ssh``
801 801 command to use for SSH connections. Default is ``ssh``.
802 802 ``strict``
803 803 Require exact command names, instead of allowing unambiguous
804 804 abbreviations. True or False. Default is False.
805 805 ``style``
806 806 Name of style to use for command output.
807 807 ``timeout``
808 808 The timeout used when a lock is held (in seconds), a negative value
809 809 means no timeout. Default is 600.
810 810 ``traceback``
811 811 Mercurial always prints a traceback when an unknown exception
812 812 occurs. Setting this to True will make Mercurial print a traceback
813 813 on all exceptions, even those recognized by Mercurial (such as
814 814 IOError or MemoryError). Default is False.
815 815 ``username``
816 816 The committer of a changeset created when running "commit".
817 817 Typically a person's name and email address, e.g. ``Fred Widget
818 818 <fred@example.com>``. Default is ``$EMAIL`` or ``username@hostname``. If
819 819 the username in hgrc is empty, it has to be specified manually or
820 820 in a different hgrc file (e.g. ``$HOME/.hgrc``, if the admin set
821 821 ``username =`` in the system hgrc).
822 822 ``verbose``
823 823 Increase the amount of output printed. True or False. Default is False.
824 824
825 825
826 826 ``web``
827 827 """""""
828 828 Web interface configuration.
829 829
830 830 ``accesslog``
831 831 Where to output the access log. Default is stdout.
832 832 ``address``
833 833 Interface address to bind to. Default is all.
834 834 ``allow_archive``
835 835 List of archive format (bz2, gz, zip) allowed for downloading.
836 836 Default is empty.
837 837 ``allowbz2``
838 838 (DEPRECATED) Whether to allow .tar.bz2 downloading of repository
839 839 revisions.
840 840 Default is False.
841 841 ``allowgz``
842 842 (DEPRECATED) Whether to allow .tar.gz downloading of repository
843 843 revisions.
844 844 Default is False.
845 845 ``allowpull``
846 846 Whether to allow pulling from the repository. Default is True.
847 847 ``allow_push``
848 848 Whether to allow pushing to the repository. If empty or not set,
849 849 push is not allowed. If the special value ``*``, any remote user can
850 850 push, including unauthenticated users. Otherwise, the remote user
851 851 must have been authenticated, and the authenticated user name must
852 852 be present in this list (separated by whitespace or ``,``). The
853 853 contents of the allow_push list are examined after the deny_push
854 854 list.
855 855 ``allow_read``
856 856 If the user has not already been denied repository access due to
857 857 the contents of deny_read, this list determines whether to grant
858 858 repository access to the user. If this list is not empty, and the
859 859 user is unauthenticated or not present in the list (separated by
860 860 whitespace or ``,``), then access is denied for the user. If the
861 861 list is empty or not set, then access is permitted to all users by
862 862 default. Setting allow_read to the special value ``*`` is equivalent
863 863 to it not being set (i.e. access is permitted to all users). The
864 864 contents of the allow_read list are examined after the deny_read
865 865 list.
866 866 ``allowzip``
867 867 (DEPRECATED) Whether to allow .zip downloading of repository
868 868 revisions. Default is False. This feature creates temporary files.
869 869 ``baseurl``
870 870 Base URL to use when publishing URLs in other locations, so
871 871 third-party tools like email notification hooks can construct
872 872 URLs. Example: ``http://hgserver/repos/``.
873 873 ``contact``
874 874 Name or email address of the person in charge of the repository.
875 875 Defaults to ui.username or ``$EMAIL`` or "unknown" if unset or empty.
876 876 ``deny_push``
877 877 Whether to deny pushing to the repository. If empty or not set,
878 878 push is not denied. If the special value ``*``, all remote users are
879 879 denied push. Otherwise, unauthenticated users are all denied, and
880 880 any authenticated user name present in this list (separated by
881 881 whitespace or ``,``) is also denied. The contents of the deny_push
882 882 list are examined before the allow_push list.
883 883 ``deny_read``
884 884 Whether to deny reading/viewing of the repository. If this list is
885 885 not empty, unauthenticated users are all denied, and any
886 886 authenticated user name present in this list (separated by
887 887 whitespace or ``,``) is also denied access to the repository. If set
888 888 to the special value ``*``, all remote users are denied access
889 889 (rarely needed ;). If deny_read is empty or not set, the
890 890 determination of repository access depends on the presence and
891 891 content of the allow_read list (see description). If both
892 892 deny_read and allow_read are empty or not set, then access is
893 893 permitted to all users by default. If the repository is being
894 894 served via hgwebdir, denied users will not be able to see it in
895 895 the list of repositories. The contents of the deny_read list have
896 896 priority over (are examined before) the contents of the allow_read
897 897 list.
898 898 ``descend``
899 899 hgwebdir indexes will not descend into subdirectories. Only repositories
900 900 directly in the current path will be shown (other repositories are still
901 901 available from the index corresponding to their containing path).
902 902 ``description``
903 903 Textual description of the repository's purpose or contents.
904 904 Default is "unknown".
905 905 ``encoding``
906 906 Character encoding name.
907 907 Example: "UTF-8"
908 908 ``errorlog``
909 909 Where to output the error log. Default is stderr.
910 910 ``hidden``
911 911 Whether to hide the repository in the hgwebdir index.
912 912 Default is False.
913 913 ``ipv6``
914 914 Whether to use IPv6. Default is False.
915 915 ``name``
916 916 Repository name to use in the web interface. Default is current
917 917 working directory.
918 918 ``maxchanges``
919 919 Maximum number of changes to list on the changelog. Default is 10.
920 920 ``maxfiles``
921 921 Maximum number of files to list per changeset. Default is 10.
922 922 ``port``
923 923 Port to listen on. Default is 8000.
924 924 ``prefix``
925 925 Prefix path to serve from. Default is '' (server root).
926 926 ``push_ssl``
927 927 Whether to require that inbound pushes be transported over SSL to
928 928 prevent password sniffing. Default is True.
929 929 ``staticurl``
930 930 Base URL to use for static files. If unset, static files (e.g. the
931 931 hgicon.png favicon) will be served by the CGI script itself. Use
932 932 this setting to serve them directly with the HTTP server.
933 933 Example: ``http://hgserver/static/``.
934 934 ``stripes``
935 935 How many lines a "zebra stripe" should span in multiline output.
936 936 Default is 1; set to 0 to disable.
937 937 ``style``
938 938 Which template map style to use.
939 939 ``templates``
940 940 Where to find the HTML templates. Default is install path.
941 941
942 942
943 943 Author
944 944 ------
945 945 Bryan O'Sullivan <bos@serpentine.com>.
946 946
947 947 Mercurial was written by Matt Mackall <mpm@selenic.com>.
948 948
949 949 See Also
950 950 --------
951 951 |hg(1)|_, |hgignore(5)|_
952 952
953 953 Copying
954 954 -------
955 955 This manual page is copyright 2005 Bryan O'Sullivan.
956 956 Mercurial is copyright 2005-2010 Matt Mackall.
957 957 Free use of this software is granted under the terms of the GNU General
958 Public License version 2.
958 Public License version 2 or any later version.
959 959
960 960 .. include:: common.txt
@@ -1,27 +1,27 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # mercurial - scalable distributed SCM
4 4 #
5 5 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2, incorporated herein by reference.
8 # GNU General Public License version 2 or any later version.
9 9
10 10 # enable importing on demand to reduce startup time
11 11 try:
12 12 from mercurial import demandimport; demandimport.enable()
13 13 except ImportError:
14 14 import sys
15 15 sys.stderr.write("abort: couldn't find mercurial libraries in [%s]\n" %
16 16 ' '.join(sys.path))
17 17 sys.stderr.write("(check your install and PYTHONPATH)\n")
18 18 sys.exit(-1)
19 19
20 20 import sys
21 21 import mercurial.util
22 22 import mercurial.dispatch
23 23
24 24 for fp in (sys.stdin, sys.stdout, sys.stderr):
25 25 mercurial.util.set_binary(fp)
26 26
27 27 mercurial.dispatch.run()
@@ -1,107 +1,106 b''
1 1 # acl.py - changeset access control for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
7 #
6 # GNU General Public License version 2 or any later version.
8 7
9 8 '''hooks for controlling repository access
10 9
11 10 This hook makes it possible to allow or deny write access to portions
12 11 of a repository when receiving incoming changesets.
13 12
14 13 The authorization is matched based on the local user name on the
15 14 system where the hook runs, and not the committer of the original
16 15 changeset (since the latter is merely informative).
17 16
18 17 The acl hook is best used along with a restricted shell like hgsh,
19 18 preventing authenticating users from doing anything other than
20 19 pushing or pulling. The hook is not safe to use if users have
21 20 interactive shell access, as they can then disable the hook.
22 21 Nor is it safe if remote users share an account, because then there
23 22 is no way to distinguish them.
24 23
25 24 To use this hook, configure the acl extension in your hgrc like this::
26 25
27 26 [extensions]
28 27 acl =
29 28
30 29 [hooks]
31 30 pretxnchangegroup.acl = python:hgext.acl.hook
32 31
33 32 [acl]
34 33 # Check whether the source of incoming changes is in this list
35 34 # ("serve" == ssh or http, "push", "pull", "bundle")
36 35 sources = serve
37 36
38 37 The allow and deny sections take a subtree pattern as key (with a glob
39 38 syntax by default), and a comma separated list of users as the
40 39 corresponding value. The deny list is checked before the allow list
41 40 is. ::
42 41
43 42 [acl.allow]
44 43 # If acl.allow is not present, all users are allowed by default.
45 44 # An empty acl.allow section means no users allowed.
46 45 docs/** = doc_writer
47 46 .hgtags = release_engineer
48 47
49 48 [acl.deny]
50 49 # If acl.deny is not present, no users are refused by default.
51 50 # An empty acl.deny section means all users allowed.
52 51 glob pattern = user4, user5
53 52 ** = user6
54 53 '''
55 54
56 55 from mercurial.i18n import _
57 56 from mercurial import util, match
58 57 import getpass, urllib
59 58
60 59 def buildmatch(ui, repo, user, key):
61 60 '''return tuple of (match function, list enabled).'''
62 61 if not ui.has_section(key):
63 62 ui.debug('acl: %s not enabled\n' % key)
64 63 return None
65 64
66 65 pats = [pat for pat, users in ui.configitems(key)
67 66 if user in users.replace(',', ' ').split()]
68 67 ui.debug('acl: %s enabled, %d entries for user %s\n' %
69 68 (key, len(pats), user))
70 69 if pats:
71 70 return match.match(repo.root, '', pats)
72 71 return match.exact(repo.root, '', [])
73 72
74 73
75 74 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
76 75 if hooktype != 'pretxnchangegroup':
77 76 raise util.Abort(_('config error - hook type "%s" cannot stop '
78 77 'incoming changesets') % hooktype)
79 78 if source not in ui.config('acl', 'sources', 'serve').split():
80 79 ui.debug('acl: changes have source "%s" - skipping\n' % source)
81 80 return
82 81
83 82 user = None
84 83 if source == 'serve' and 'url' in kwargs:
85 84 url = kwargs['url'].split(':')
86 85 if url[0] == 'remote' and url[1].startswith('http'):
87 86 user = urllib.unquote(url[3])
88 87
89 88 if user is None:
90 89 user = getpass.getuser()
91 90
92 91 cfg = ui.config('acl', 'config')
93 92 if cfg:
94 93 ui.readconfig(cfg, sections = ['acl.allow', 'acl.deny'])
95 94 allow = buildmatch(ui, repo, user, 'acl.allow')
96 95 deny = buildmatch(ui, repo, user, 'acl.deny')
97 96
98 97 for rev in xrange(repo[node], len(repo)):
99 98 ctx = repo[rev]
100 99 for f in ctx.files():
101 100 if deny and deny(f):
102 101 ui.debug('acl: user %s denied on %s\n' % (user, f))
103 102 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
104 103 if allow and not allow(f):
105 104 ui.debug('acl: user %s not allowed on %s\n' % (user, f))
106 105 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
107 106 ui.debug('acl: allowing changeset %s\n' % ctx)
@@ -1,327 +1,327 b''
1 1 # Mercurial extension to provide the 'hg bookmark' command
2 2 #
3 3 # Copyright 2008 David Soria Parra <dsp@php.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''track a line of development with movable markers
9 9
10 10 Bookmarks are local movable markers to changesets. Every bookmark
11 11 points to a changeset identified by its hash. If you commit a
12 12 changeset that is based on a changeset that has a bookmark on it, the
13 13 bookmark shifts to the new changeset.
14 14
15 15 It is possible to use bookmark names in every revision lookup (e.g. hg
16 16 merge, hg update).
17 17
18 18 By default, when several bookmarks point to the same changeset, they
19 19 will all move forward together. It is possible to obtain a more
20 20 git-like experience by adding the following configuration option to
21 21 your .hgrc::
22 22
23 23 [bookmarks]
24 24 track.current = True
25 25
26 26 This will cause Mercurial to track the bookmark that you are currently
27 27 using, and only update it. This is similar to git's approach to
28 28 branching.
29 29 '''
30 30
31 31 from mercurial.i18n import _
32 32 from mercurial.node import nullid, nullrev, hex, short
33 33 from mercurial import util, commands, localrepo, repair, extensions
34 34 import os
35 35
36 36 def write(repo):
37 37 '''Write bookmarks
38 38
39 39 Write the given bookmark => hash dictionary to the .hg/bookmarks file
40 40 in a format equal to those of localtags.
41 41
42 42 We also store a backup of the previous state in undo.bookmarks that
43 43 can be copied back on rollback.
44 44 '''
45 45 refs = repo._bookmarks
46 46 if os.path.exists(repo.join('bookmarks')):
47 47 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
48 48 if repo._bookmarkcurrent not in refs:
49 49 setcurrent(repo, None)
50 50 wlock = repo.wlock()
51 51 try:
52 52 file = repo.opener('bookmarks', 'w', atomictemp=True)
53 53 for refspec, node in refs.iteritems():
54 54 file.write("%s %s\n" % (hex(node), refspec))
55 55 file.rename()
56 56 finally:
57 57 wlock.release()
58 58
59 59 def setcurrent(repo, mark):
60 60 '''Set the name of the bookmark that we are currently on
61 61
62 62 Set the name of the bookmark that we are on (hg update <bookmark>).
63 63 The name is recorded in .hg/bookmarks.current
64 64 '''
65 65 current = repo._bookmarkcurrent
66 66 if current == mark:
67 67 return
68 68
69 69 refs = repo._bookmarks
70 70
71 71 # do not update if we do update to a rev equal to the current bookmark
72 72 if (mark and mark not in refs and
73 73 current and refs[current] == repo.changectx('.').node()):
74 74 return
75 75 if mark not in refs:
76 76 mark = ''
77 77 wlock = repo.wlock()
78 78 try:
79 79 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
80 80 file.write(mark)
81 81 file.rename()
82 82 finally:
83 83 wlock.release()
84 84 repo._bookmarkcurrent = mark
85 85
86 86 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
87 87 '''track a line of development with movable markers
88 88
89 89 Bookmarks are pointers to certain commits that move when
90 90 committing. Bookmarks are local. They can be renamed, copied and
91 91 deleted. It is possible to use bookmark names in 'hg merge' and
92 92 'hg update' to merge and update respectively to a given bookmark.
93 93
94 94 You can use 'hg bookmark NAME' to set a bookmark on the working
95 95 directory's parent revision with the given name. If you specify
96 96 a revision using -r REV (where REV may be an existing bookmark),
97 97 the bookmark is assigned to that revision.
98 98 '''
99 99 hexfn = ui.debugflag and hex or short
100 100 marks = repo._bookmarks
101 101 cur = repo.changectx('.').node()
102 102
103 103 if rename:
104 104 if rename not in marks:
105 105 raise util.Abort(_("a bookmark of this name does not exist"))
106 106 if mark in marks and not force:
107 107 raise util.Abort(_("a bookmark of the same name already exists"))
108 108 if mark is None:
109 109 raise util.Abort(_("new bookmark name required"))
110 110 marks[mark] = marks[rename]
111 111 del marks[rename]
112 112 if repo._bookmarkcurrent == rename:
113 113 setcurrent(repo, mark)
114 114 write(repo)
115 115 return
116 116
117 117 if delete:
118 118 if mark is None:
119 119 raise util.Abort(_("bookmark name required"))
120 120 if mark not in marks:
121 121 raise util.Abort(_("a bookmark of this name does not exist"))
122 122 if mark == repo._bookmarkcurrent:
123 123 setcurrent(repo, None)
124 124 del marks[mark]
125 125 write(repo)
126 126 return
127 127
128 128 if mark != None:
129 129 if "\n" in mark:
130 130 raise util.Abort(_("bookmark name cannot contain newlines"))
131 131 mark = mark.strip()
132 132 if mark in marks and not force:
133 133 raise util.Abort(_("a bookmark of the same name already exists"))
134 134 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
135 135 and not force):
136 136 raise util.Abort(
137 137 _("a bookmark cannot have the name of an existing branch"))
138 138 if rev:
139 139 marks[mark] = repo.lookup(rev)
140 140 else:
141 141 marks[mark] = repo.changectx('.').node()
142 142 setcurrent(repo, mark)
143 143 write(repo)
144 144 return
145 145
146 146 if mark is None:
147 147 if rev:
148 148 raise util.Abort(_("bookmark name required"))
149 149 if len(marks) == 0:
150 150 ui.status("no bookmarks set\n")
151 151 else:
152 152 for bmark, n in marks.iteritems():
153 153 if ui.configbool('bookmarks', 'track.current'):
154 154 current = repo._bookmarkcurrent
155 155 prefix = (bmark == current and n == cur) and '*' or ' '
156 156 else:
157 157 prefix = (n == cur) and '*' or ' '
158 158
159 159 if ui.quiet:
160 160 ui.write("%s\n" % bmark)
161 161 else:
162 162 ui.write(" %s %-25s %d:%s\n" % (
163 163 prefix, bmark, repo.changelog.rev(n), hexfn(n)))
164 164 return
165 165
166 166 def _revstostrip(changelog, node):
167 167 srev = changelog.rev(node)
168 168 tostrip = [srev]
169 169 saveheads = []
170 170 for r in xrange(srev, len(changelog)):
171 171 parents = changelog.parentrevs(r)
172 172 if parents[0] in tostrip or parents[1] in tostrip:
173 173 tostrip.append(r)
174 174 if parents[1] != nullrev:
175 175 for p in parents:
176 176 if p not in tostrip and p > srev:
177 177 saveheads.append(p)
178 178 return [r for r in tostrip if r not in saveheads]
179 179
180 180 def strip(oldstrip, ui, repo, node, backup="all"):
181 181 """Strip bookmarks if revisions are stripped using
182 182 the mercurial.strip method. This usually happens during
183 183 qpush and qpop"""
184 184 revisions = _revstostrip(repo.changelog, node)
185 185 marks = repo._bookmarks
186 186 update = []
187 187 for mark, n in marks.iteritems():
188 188 if repo.changelog.rev(n) in revisions:
189 189 update.append(mark)
190 190 oldstrip(ui, repo, node, backup)
191 191 if len(update) > 0:
192 192 for m in update:
193 193 marks[m] = repo.changectx('.').node()
194 194 write(repo)
195 195
196 196 def reposetup(ui, repo):
197 197 if not repo.local():
198 198 return
199 199
200 200 class bookmark_repo(repo.__class__):
201 201
202 202 @util.propertycache
203 203 def _bookmarks(self):
204 204 '''Parse .hg/bookmarks file and return a dictionary
205 205
206 206 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
207 207 in the .hg/bookmarks file. They are read returned as a dictionary
208 208 with name => hash values.
209 209 '''
210 210 try:
211 211 bookmarks = {}
212 212 for line in self.opener('bookmarks'):
213 213 sha, refspec = line.strip().split(' ', 1)
214 214 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
215 215 except:
216 216 pass
217 217 return bookmarks
218 218
219 219 @util.propertycache
220 220 def _bookmarkcurrent(self):
221 221 '''Get the current bookmark
222 222
223 223 If we use gittishsh branches we have a current bookmark that
224 224 we are on. This function returns the name of the bookmark. It
225 225 is stored in .hg/bookmarks.current
226 226 '''
227 227 mark = None
228 228 if os.path.exists(self.join('bookmarks.current')):
229 229 file = self.opener('bookmarks.current')
230 230 # No readline() in posixfile_nt, reading everything is cheap
231 231 mark = (file.readlines() or [''])[0]
232 232 if mark == '':
233 233 mark = None
234 234 file.close()
235 235 return mark
236 236
237 237 def rollback(self):
238 238 if os.path.exists(self.join('undo.bookmarks')):
239 239 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
240 240 return super(bookmark_repo, self).rollback()
241 241
242 242 def lookup(self, key):
243 243 if key in self._bookmarks:
244 244 key = self._bookmarks[key]
245 245 return super(bookmark_repo, self).lookup(key)
246 246
247 247 def _bookmarksupdate(self, parents, node):
248 248 marks = self._bookmarks
249 249 update = False
250 250 if ui.configbool('bookmarks', 'track.current'):
251 251 mark = self._bookmarkcurrent
252 252 if mark and marks[mark] in parents:
253 253 marks[mark] = node
254 254 update = True
255 255 else:
256 256 for mark, n in marks.items():
257 257 if n in parents:
258 258 marks[mark] = node
259 259 update = True
260 260 if update:
261 261 write(self)
262 262
263 263 def commitctx(self, ctx, error=False):
264 264 """Add a revision to the repository and
265 265 move the bookmark"""
266 266 wlock = self.wlock() # do both commit and bookmark with lock held
267 267 try:
268 268 node = super(bookmark_repo, self).commitctx(ctx, error)
269 269 if node is None:
270 270 return None
271 271 parents = self.changelog.parents(node)
272 272 if parents[1] == nullid:
273 273 parents = (parents[0],)
274 274
275 275 self._bookmarksupdate(parents, node)
276 276 return node
277 277 finally:
278 278 wlock.release()
279 279
280 280 def addchangegroup(self, source, srctype, url, emptyok=False):
281 281 parents = self.dirstate.parents()
282 282
283 283 result = super(bookmark_repo, self).addchangegroup(
284 284 source, srctype, url, emptyok)
285 285 if result > 1:
286 286 # We have more heads than before
287 287 return result
288 288 node = self.changelog.tip()
289 289
290 290 self._bookmarksupdate(parents, node)
291 291 return result
292 292
293 293 def _findtags(self):
294 294 """Merge bookmarks with normal tags"""
295 295 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
296 296 tags.update(self._bookmarks)
297 297 return (tags, tagtypes)
298 298
299 299 repo.__class__ = bookmark_repo
300 300
301 301 def uisetup(ui):
302 302 extensions.wrapfunction(repair, "strip", strip)
303 303 if ui.configbool('bookmarks', 'track.current'):
304 304 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
305 305
306 306 def updatecurbookmark(orig, ui, repo, *args, **opts):
307 307 '''Set the current bookmark
308 308
309 309 If the user updates to a bookmark we update the .hg/bookmarks.current
310 310 file.
311 311 '''
312 312 res = orig(ui, repo, *args, **opts)
313 313 rev = opts['rev']
314 314 if not rev and len(args) > 0:
315 315 rev = args[0]
316 316 setcurrent(repo, rev)
317 317 return res
318 318
319 319 cmdtable = {
320 320 "bookmarks":
321 321 (bookmark,
322 322 [('f', 'force', False, _('force')),
323 323 ('r', 'rev', '', _('revision')),
324 324 ('d', 'delete', False, _('delete a given bookmark')),
325 325 ('m', 'rename', '', _('rename a given bookmark'))],
326 326 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
327 327 }
@@ -1,439 +1,439 b''
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''hooks for integrating with the Bugzilla bug tracker
9 9
10 10 This hook extension adds comments on bugs in Bugzilla when changesets
11 11 that refer to bugs by Bugzilla ID are seen. The hook does not change
12 12 bug status.
13 13
14 14 The hook updates the Bugzilla database directly. Only Bugzilla
15 15 installations using MySQL are supported.
16 16
17 17 The hook relies on a Bugzilla script to send bug change notification
18 18 emails. That script changes between Bugzilla versions; the
19 19 'processmail' script used prior to 2.18 is replaced in 2.18 and
20 20 subsequent versions by 'config/sendbugmail.pl'. Note that these will
21 21 be run by Mercurial as the user pushing the change; you will need to
22 22 ensure the Bugzilla install file permissions are set appropriately.
23 23
24 24 The extension is configured through three different configuration
25 25 sections. These keys are recognized in the [bugzilla] section:
26 26
27 27 host
28 28 Hostname of the MySQL server holding the Bugzilla database.
29 29
30 30 db
31 31 Name of the Bugzilla database in MySQL. Default 'bugs'.
32 32
33 33 user
34 34 Username to use to access MySQL server. Default 'bugs'.
35 35
36 36 password
37 37 Password to use to access MySQL server.
38 38
39 39 timeout
40 40 Database connection timeout (seconds). Default 5.
41 41
42 42 version
43 43 Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later,
44 44 '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior
45 45 to 2.18.
46 46
47 47 bzuser
48 48 Fallback Bugzilla user name to record comments with, if changeset
49 49 committer cannot be found as a Bugzilla user.
50 50
51 51 bzdir
52 52 Bugzilla install directory. Used by default notify. Default
53 53 '/var/www/html/bugzilla'.
54 54
55 55 notify
56 56 The command to run to get Bugzilla to send bug change notification
57 57 emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id)
58 58 and 'user' (committer bugzilla email). Default depends on version;
59 59 from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
60 60 %(id)s %(user)s".
61 61
62 62 regexp
63 63 Regular expression to match bug IDs in changeset commit message.
64 64 Must contain one "()" group. The default expression matches 'Bug
65 65 1234', 'Bug no. 1234', 'Bug number 1234', 'Bugs 1234,5678', 'Bug
66 66 1234 and 5678' and variations thereof. Matching is case insensitive.
67 67
68 68 style
69 69 The style file to use when formatting comments.
70 70
71 71 template
72 72 Template to use when formatting comments. Overrides style if
73 73 specified. In addition to the usual Mercurial keywords, the
74 74 extension specifies::
75 75
76 76 {bug} The Bugzilla bug ID.
77 77 {root} The full pathname of the Mercurial repository.
78 78 {webroot} Stripped pathname of the Mercurial repository.
79 79 {hgweb} Base URL for browsing Mercurial repositories.
80 80
81 81 Default 'changeset {node|short} in repo {root} refers '
82 82 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
83 83
84 84 strip
85 85 The number of slashes to strip from the front of {root} to produce
86 86 {webroot}. Default 0.
87 87
88 88 usermap
89 89 Path of file containing Mercurial committer ID to Bugzilla user ID
90 90 mappings. If specified, the file should contain one mapping per
91 91 line, "committer"="Bugzilla user". See also the [usermap] section.
92 92
93 93 The [usermap] section is used to specify mappings of Mercurial
94 94 committer ID to Bugzilla user ID. See also [bugzilla].usermap.
95 95 "committer"="Bugzilla user"
96 96
97 97 Finally, the [web] section supports one entry:
98 98
99 99 baseurl
100 100 Base URL for browsing Mercurial repositories. Reference from
101 101 templates as {hgweb}.
102 102
103 103 Activating the extension::
104 104
105 105 [extensions]
106 106 bugzilla =
107 107
108 108 [hooks]
109 109 # run bugzilla hook on every change pulled or pushed in here
110 110 incoming.bugzilla = python:hgext.bugzilla.hook
111 111
112 112 Example configuration:
113 113
114 114 This example configuration is for a collection of Mercurial
115 115 repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
116 116 installation in /opt/bugzilla-3.2. ::
117 117
118 118 [bugzilla]
119 119 host=localhost
120 120 password=XYZZY
121 121 version=3.0
122 122 bzuser=unknown@domain.com
123 123 bzdir=/opt/bugzilla-3.2
124 124 template=Changeset {node|short} in {root|basename}.
125 125 {hgweb}/{webroot}/rev/{node|short}\\n
126 126 {desc}\\n
127 127 strip=5
128 128
129 129 [web]
130 130 baseurl=http://dev.domain.com/hg
131 131
132 132 [usermap]
133 133 user@emaildomain.com=user.name@bugzilladomain.com
134 134
135 135 Commits add a comment to the Bugzilla bug record of the form::
136 136
137 137 Changeset 3b16791d6642 in repository-name.
138 138 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
139 139
140 140 Changeset commit comment. Bug 1234.
141 141 '''
142 142
143 143 from mercurial.i18n import _
144 144 from mercurial.node import short
145 145 from mercurial import cmdutil, templater, util
146 146 import re, time
147 147
148 148 MySQLdb = None
149 149
150 150 def buglist(ids):
151 151 return '(' + ','.join(map(str, ids)) + ')'
152 152
153 153 class bugzilla_2_16(object):
154 154 '''support for bugzilla version 2.16.'''
155 155
156 156 def __init__(self, ui):
157 157 self.ui = ui
158 158 host = self.ui.config('bugzilla', 'host', 'localhost')
159 159 user = self.ui.config('bugzilla', 'user', 'bugs')
160 160 passwd = self.ui.config('bugzilla', 'password')
161 161 db = self.ui.config('bugzilla', 'db', 'bugs')
162 162 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
163 163 usermap = self.ui.config('bugzilla', 'usermap')
164 164 if usermap:
165 165 self.ui.readconfig(usermap, sections=['usermap'])
166 166 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
167 167 (host, db, user, '*' * len(passwd)))
168 168 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
169 169 db=db, connect_timeout=timeout)
170 170 self.cursor = self.conn.cursor()
171 171 self.longdesc_id = self.get_longdesc_id()
172 172 self.user_ids = {}
173 173 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
174 174
175 175 def run(self, *args, **kwargs):
176 176 '''run a query.'''
177 177 self.ui.note(_('query: %s %s\n') % (args, kwargs))
178 178 try:
179 179 self.cursor.execute(*args, **kwargs)
180 180 except MySQLdb.MySQLError:
181 181 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
182 182 raise
183 183
184 184 def get_longdesc_id(self):
185 185 '''get identity of longdesc field'''
186 186 self.run('select fieldid from fielddefs where name = "longdesc"')
187 187 ids = self.cursor.fetchall()
188 188 if len(ids) != 1:
189 189 raise util.Abort(_('unknown database schema'))
190 190 return ids[0][0]
191 191
192 192 def filter_real_bug_ids(self, ids):
193 193 '''filter not-existing bug ids from list.'''
194 194 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
195 195 return sorted([c[0] for c in self.cursor.fetchall()])
196 196
197 197 def filter_unknown_bug_ids(self, node, ids):
198 198 '''filter bug ids from list that already refer to this changeset.'''
199 199
200 200 self.run('''select bug_id from longdescs where
201 201 bug_id in %s and thetext like "%%%s%%"''' %
202 202 (buglist(ids), short(node)))
203 203 unknown = set(ids)
204 204 for (id,) in self.cursor.fetchall():
205 205 self.ui.status(_('bug %d already knows about changeset %s\n') %
206 206 (id, short(node)))
207 207 unknown.discard(id)
208 208 return sorted(unknown)
209 209
210 210 def notify(self, ids, committer):
211 211 '''tell bugzilla to send mail.'''
212 212
213 213 self.ui.status(_('telling bugzilla to send mail:\n'))
214 214 (user, userid) = self.get_bugzilla_user(committer)
215 215 for id in ids:
216 216 self.ui.status(_(' bug %s\n') % id)
217 217 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
218 218 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
219 219 try:
220 220 # Backwards-compatible with old notify string, which
221 221 # took one string. This will throw with a new format
222 222 # string.
223 223 cmd = cmdfmt % id
224 224 except TypeError:
225 225 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
226 226 self.ui.note(_('running notify command %s\n') % cmd)
227 227 fp = util.popen('(%s) 2>&1' % cmd)
228 228 out = fp.read()
229 229 ret = fp.close()
230 230 if ret:
231 231 self.ui.warn(out)
232 232 raise util.Abort(_('bugzilla notify command %s') %
233 233 util.explain_exit(ret)[0])
234 234 self.ui.status(_('done\n'))
235 235
236 236 def get_user_id(self, user):
237 237 '''look up numeric bugzilla user id.'''
238 238 try:
239 239 return self.user_ids[user]
240 240 except KeyError:
241 241 try:
242 242 userid = int(user)
243 243 except ValueError:
244 244 self.ui.note(_('looking up user %s\n') % user)
245 245 self.run('''select userid from profiles
246 246 where login_name like %s''', user)
247 247 all = self.cursor.fetchall()
248 248 if len(all) != 1:
249 249 raise KeyError(user)
250 250 userid = int(all[0][0])
251 251 self.user_ids[user] = userid
252 252 return userid
253 253
254 254 def map_committer(self, user):
255 255 '''map name of committer to bugzilla user name.'''
256 256 for committer, bzuser in self.ui.configitems('usermap'):
257 257 if committer.lower() == user.lower():
258 258 return bzuser
259 259 return user
260 260
261 261 def get_bugzilla_user(self, committer):
262 262 '''see if committer is a registered bugzilla user. Return
263 263 bugzilla username and userid if so. If not, return default
264 264 bugzilla username and userid.'''
265 265 user = self.map_committer(committer)
266 266 try:
267 267 userid = self.get_user_id(user)
268 268 except KeyError:
269 269 try:
270 270 defaultuser = self.ui.config('bugzilla', 'bzuser')
271 271 if not defaultuser:
272 272 raise util.Abort(_('cannot find bugzilla user id for %s') %
273 273 user)
274 274 userid = self.get_user_id(defaultuser)
275 275 user = defaultuser
276 276 except KeyError:
277 277 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
278 278 (user, defaultuser))
279 279 return (user, userid)
280 280
281 281 def add_comment(self, bugid, text, committer):
282 282 '''add comment to bug. try adding comment as committer of
283 283 changeset, otherwise as default bugzilla user.'''
284 284 (user, userid) = self.get_bugzilla_user(committer)
285 285 now = time.strftime('%Y-%m-%d %H:%M:%S')
286 286 self.run('''insert into longdescs
287 287 (bug_id, who, bug_when, thetext)
288 288 values (%s, %s, %s, %s)''',
289 289 (bugid, userid, now, text))
290 290 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
291 291 values (%s, %s, %s, %s)''',
292 292 (bugid, userid, now, self.longdesc_id))
293 293 self.conn.commit()
294 294
295 295 class bugzilla_2_18(bugzilla_2_16):
296 296 '''support for bugzilla 2.18 series.'''
297 297
298 298 def __init__(self, ui):
299 299 bugzilla_2_16.__init__(self, ui)
300 300 self.default_notify = "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
301 301
302 302 class bugzilla_3_0(bugzilla_2_18):
303 303 '''support for bugzilla 3.0 series.'''
304 304
305 305 def __init__(self, ui):
306 306 bugzilla_2_18.__init__(self, ui)
307 307
308 308 def get_longdesc_id(self):
309 309 '''get identity of longdesc field'''
310 310 self.run('select id from fielddefs where name = "longdesc"')
311 311 ids = self.cursor.fetchall()
312 312 if len(ids) != 1:
313 313 raise util.Abort(_('unknown database schema'))
314 314 return ids[0][0]
315 315
316 316 class bugzilla(object):
317 317 # supported versions of bugzilla. different versions have
318 318 # different schemas.
319 319 _versions = {
320 320 '2.16': bugzilla_2_16,
321 321 '2.18': bugzilla_2_18,
322 322 '3.0': bugzilla_3_0
323 323 }
324 324
325 325 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
326 326 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
327 327
328 328 _bz = None
329 329
330 330 def __init__(self, ui, repo):
331 331 self.ui = ui
332 332 self.repo = repo
333 333
334 334 def bz(self):
335 335 '''return object that knows how to talk to bugzilla version in
336 336 use.'''
337 337
338 338 if bugzilla._bz is None:
339 339 bzversion = self.ui.config('bugzilla', 'version')
340 340 try:
341 341 bzclass = bugzilla._versions[bzversion]
342 342 except KeyError:
343 343 raise util.Abort(_('bugzilla version %s not supported') %
344 344 bzversion)
345 345 bugzilla._bz = bzclass(self.ui)
346 346 return bugzilla._bz
347 347
348 348 def __getattr__(self, key):
349 349 return getattr(self.bz(), key)
350 350
351 351 _bug_re = None
352 352 _split_re = None
353 353
354 354 def find_bug_ids(self, ctx):
355 355 '''find valid bug ids that are referred to in changeset
356 356 comments and that do not already have references to this
357 357 changeset.'''
358 358
359 359 if bugzilla._bug_re is None:
360 360 bugzilla._bug_re = re.compile(
361 361 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
362 362 re.IGNORECASE)
363 363 bugzilla._split_re = re.compile(r'\D+')
364 364 start = 0
365 365 ids = set()
366 366 while True:
367 367 m = bugzilla._bug_re.search(ctx.description(), start)
368 368 if not m:
369 369 break
370 370 start = m.end()
371 371 for id in bugzilla._split_re.split(m.group(1)):
372 372 if not id: continue
373 373 ids.add(int(id))
374 374 if ids:
375 375 ids = self.filter_real_bug_ids(ids)
376 376 if ids:
377 377 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
378 378 return ids
379 379
380 380 def update(self, bugid, ctx):
381 381 '''update bugzilla bug with reference to changeset.'''
382 382
383 383 def webroot(root):
384 384 '''strip leading prefix of repo root and turn into
385 385 url-safe path.'''
386 386 count = int(self.ui.config('bugzilla', 'strip', 0))
387 387 root = util.pconvert(root)
388 388 while count > 0:
389 389 c = root.find('/')
390 390 if c == -1:
391 391 break
392 392 root = root[c+1:]
393 393 count -= 1
394 394 return root
395 395
396 396 mapfile = self.ui.config('bugzilla', 'style')
397 397 tmpl = self.ui.config('bugzilla', 'template')
398 398 t = cmdutil.changeset_templater(self.ui, self.repo,
399 399 False, None, mapfile, False)
400 400 if not mapfile and not tmpl:
401 401 tmpl = _('changeset {node|short} in repo {root} refers '
402 402 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
403 403 if tmpl:
404 404 tmpl = templater.parsestring(tmpl, quoted=False)
405 405 t.use_template(tmpl)
406 406 self.ui.pushbuffer()
407 407 t.show(ctx, changes=ctx.changeset(),
408 408 bug=str(bugid),
409 409 hgweb=self.ui.config('web', 'baseurl'),
410 410 root=self.repo.root,
411 411 webroot=webroot(self.repo.root))
412 412 data = self.ui.popbuffer()
413 413 self.add_comment(bugid, data, util.email(ctx.user()))
414 414
415 415 def hook(ui, repo, hooktype, node=None, **kwargs):
416 416 '''add comment to bugzilla for each changeset that refers to a
417 417 bugzilla bug id. only add a comment once per bug, so same change
418 418 seen multiple times does not fill bug with duplicate data.'''
419 419 try:
420 420 import MySQLdb as mysql
421 421 global MySQLdb
422 422 MySQLdb = mysql
423 423 except ImportError, err:
424 424 raise util.Abort(_('python mysql support not available: %s') % err)
425 425
426 426 if node is None:
427 427 raise util.Abort(_('hook type %s does not pass a changeset id') %
428 428 hooktype)
429 429 try:
430 430 bz = bugzilla(ui, repo)
431 431 ctx = repo[node]
432 432 ids = bz.find_bug_ids(ctx)
433 433 if ids:
434 434 for id in ids:
435 435 bz.update(id, ctx)
436 436 bz.notify(ids, util.email(ctx.user()))
437 437 except MySQLdb.MySQLError, err:
438 438 raise util.Abort(_('database error: %s') % err[1])
439 439
@@ -1,44 +1,44 b''
1 1 # Mercurial extension to provide the 'hg children' command
2 2 #
3 3 # Copyright 2007 by Intevation GmbH <intevation@intevation.de>
4 4 #
5 5 # Author(s):
6 6 # Thomas Arendsen Hein <thomas@intevation.de>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2, incorporated herein by reference.
9 # GNU General Public License version 2 or any later version.
10 10
11 11 '''command to display child changesets'''
12 12
13 13 from mercurial import cmdutil
14 14 from mercurial.commands import templateopts
15 15 from mercurial.i18n import _
16 16
17 17
18 18 def children(ui, repo, file_=None, **opts):
19 19 """show the children of the given or working directory revision
20 20
21 21 Print the children of the working directory's revisions. If a
22 22 revision is given via -r/--rev, the children of that revision will
23 23 be printed. If a file argument is given, revision in which the
24 24 file was last changed (after the working directory revision or the
25 25 argument to --rev if given) is printed.
26 26 """
27 27 rev = opts.get('rev')
28 28 if file_:
29 29 ctx = repo.filectx(file_, changeid=rev)
30 30 else:
31 31 ctx = repo[rev]
32 32
33 33 displayer = cmdutil.show_changeset(ui, repo, opts)
34 34 for cctx in ctx.children():
35 35 displayer.show(cctx)
36 36 displayer.close()
37 37
38 38 cmdtable = {
39 39 "children":
40 40 (children,
41 41 [('r', 'rev', '', _('show children of the specified revision')),
42 42 ] + templateopts,
43 43 _('hg children [-r REV] [FILE]')),
44 44 }
@@ -1,192 +1,192 b''
1 1 # churn.py - create a graph of revisions count grouped by template
2 2 #
3 3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''command to display statistics about repository history'''
10 10
11 11 from mercurial.i18n import _
12 12 from mercurial import patch, cmdutil, util, templater
13 13 import sys, os
14 14 import time, datetime
15 15
16 16 def maketemplater(ui, repo, tmpl):
17 17 tmpl = templater.parsestring(tmpl, quoted=False)
18 18 try:
19 19 t = cmdutil.changeset_templater(ui, repo, False, None, None, False)
20 20 except SyntaxError, inst:
21 21 raise util.Abort(inst.args[0])
22 22 t.use_template(tmpl)
23 23 return t
24 24
25 25 def changedlines(ui, repo, ctx1, ctx2, fns):
26 26 added, removed = 0, 0
27 27 fmatch = cmdutil.matchfiles(repo, fns)
28 28 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
29 29 for l in diff.split('\n'):
30 30 if l.startswith("+") and not l.startswith("+++ "):
31 31 added += 1
32 32 elif l.startswith("-") and not l.startswith("--- "):
33 33 removed += 1
34 34 return (added, removed)
35 35
36 36 def countrate(ui, repo, amap, *pats, **opts):
37 37 """Calculate stats"""
38 38 if opts.get('dateformat'):
39 39 def getkey(ctx):
40 40 t, tz = ctx.date()
41 41 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
42 42 return date.strftime(opts['dateformat'])
43 43 else:
44 44 tmpl = opts.get('template', '{author|email}')
45 45 tmpl = maketemplater(ui, repo, tmpl)
46 46 def getkey(ctx):
47 47 ui.pushbuffer()
48 48 tmpl.show(ctx)
49 49 return ui.popbuffer()
50 50
51 51 state = {'count': 0, 'pct': 0}
52 52 rate = {}
53 53 df = False
54 54 if opts.get('date'):
55 55 df = util.matchdate(opts['date'])
56 56
57 57 m = cmdutil.match(repo, pats, opts)
58 58 def prep(ctx, fns):
59 59 rev = ctx.rev()
60 60 if df and not df(ctx.date()[0]): # doesn't match date format
61 61 return
62 62
63 63 key = getkey(ctx)
64 64 key = amap.get(key, key) # alias remap
65 65 if opts.get('changesets'):
66 66 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
67 67 else:
68 68 parents = ctx.parents()
69 69 if len(parents) > 1:
70 70 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
71 71 return
72 72
73 73 ctx1 = parents[0]
74 74 lines = changedlines(ui, repo, ctx1, ctx, fns)
75 75 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
76 76
77 77 if opts.get('progress'):
78 78 state['count'] += 1
79 79 newpct = int(100.0 * state['count'] / max(len(repo), 1))
80 80 if state['pct'] < newpct:
81 81 state['pct'] = newpct
82 82 ui.write("\r" + _("generating stats: %d%%") % state['pct'])
83 83 sys.stdout.flush()
84 84
85 85 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
86 86 continue
87 87
88 88 if opts.get('progress'):
89 89 ui.write("\r")
90 90 sys.stdout.flush()
91 91
92 92 return rate
93 93
94 94
95 95 def churn(ui, repo, *pats, **opts):
96 96 '''histogram of changes to the repository
97 97
98 98 This command will display a histogram representing the number
99 99 of changed lines or revisions, grouped according to the given
100 100 template. The default template will group changes by author.
101 101 The --dateformat option may be used to group the results by
102 102 date instead.
103 103
104 104 Statistics are based on the number of changed lines, or
105 105 alternatively the number of matching revisions if the
106 106 --changesets option is specified.
107 107
108 108 Examples::
109 109
110 110 # display count of changed lines for every committer
111 111 hg churn -t '{author|email}'
112 112
113 113 # display daily activity graph
114 114 hg churn -f '%H' -s -c
115 115
116 116 # display activity of developers by month
117 117 hg churn -f '%Y-%m' -s -c
118 118
119 119 # display count of lines changed in every year
120 120 hg churn -f '%Y' -s
121 121
122 122 It is possible to map alternate email addresses to a main address
123 123 by providing a file using the following format::
124 124
125 125 <alias email> <actual email>
126 126
127 127 Such a file may be specified with the --aliases option, otherwise
128 128 a .hgchurn file will be looked for in the working directory root.
129 129 '''
130 130 def pad(s, l):
131 131 return (s + " " * l)[:l]
132 132
133 133 amap = {}
134 134 aliases = opts.get('aliases')
135 135 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
136 136 aliases = repo.wjoin('.hgchurn')
137 137 if aliases:
138 138 for l in open(aliases, "r"):
139 139 l = l.strip()
140 140 alias, actual = l.split()
141 141 amap[alias] = actual
142 142
143 143 rate = countrate(ui, repo, amap, *pats, **opts).items()
144 144 if not rate:
145 145 return
146 146
147 147 sortkey = ((not opts.get('sort')) and (lambda x: -sum(x[1])) or None)
148 148 rate.sort(key=sortkey)
149 149
150 150 # Be careful not to have a zero maxcount (issue833)
151 151 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
152 152 maxname = max(len(k) for k, v in rate)
153 153
154 154 ttywidth = util.termwidth()
155 155 ui.debug("assuming %i character terminal\n" % ttywidth)
156 156 width = ttywidth - maxname - 2 - 2 - 2
157 157
158 158 if opts.get('diffstat'):
159 159 width -= 15
160 160 def format(name, (added, removed)):
161 161 return "%s %15s %s%s\n" % (pad(name, maxname),
162 162 '+%d/-%d' % (added, removed),
163 163 '+' * charnum(added),
164 164 '-' * charnum(removed))
165 165 else:
166 166 width -= 6
167 167 def format(name, count):
168 168 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
169 169 '*' * charnum(sum(count)))
170 170
171 171 def charnum(count):
172 172 return int(round(count*width/maxcount))
173 173
174 174 for name, count in rate:
175 175 ui.write(format(name, count))
176 176
177 177
178 178 cmdtable = {
179 179 "churn":
180 180 (churn,
181 181 [('r', 'rev', [], _('count rate for the specified revision or range')),
182 182 ('d', 'date', '', _('count rate for revisions matching date spec')),
183 183 ('t', 'template', '{author|email}', _('template to group changesets')),
184 184 ('f', 'dateformat', '',
185 185 _('strftime-compatible format for grouping by date')),
186 186 ('c', 'changesets', False, _('count rate by number of changesets')),
187 187 ('s', 'sort', False, _('sort by key (default: sort by count)')),
188 188 ('', 'diffstat', False, _('display added/removed lines separately')),
189 189 ('', 'aliases', '', _('file with email aliases')),
190 190 ('', 'progress', None, _('show progress'))],
191 191 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [--progress] [FILE]")),
192 192 }
@@ -1,294 +1,294 b''
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''import revisions from foreign VCS repositories into Mercurial'''
9 9
10 10 import convcmd
11 11 import cvsps
12 12 import subversion
13 13 from mercurial import commands
14 14 from mercurial.i18n import _
15 15
16 16 # Commands definition was moved elsewhere to ease demandload job.
17 17
18 18 def convert(ui, src, dest=None, revmapfile=None, **opts):
19 19 """convert a foreign SCM repository to a Mercurial one.
20 20
21 21 Accepted source formats [identifiers]:
22 22
23 23 - Mercurial [hg]
24 24 - CVS [cvs]
25 25 - Darcs [darcs]
26 26 - git [git]
27 27 - Subversion [svn]
28 28 - Monotone [mtn]
29 29 - GNU Arch [gnuarch]
30 30 - Bazaar [bzr]
31 31 - Perforce [p4]
32 32
33 33 Accepted destination formats [identifiers]:
34 34
35 35 - Mercurial [hg]
36 36 - Subversion [svn] (history on branches is not preserved)
37 37
38 38 If no revision is given, all revisions will be converted.
39 39 Otherwise, convert will only import up to the named revision
40 40 (given in a format understood by the source).
41 41
42 42 If no destination directory name is specified, it defaults to the
43 43 basename of the source with '-hg' appended. If the destination
44 44 repository doesn't exist, it will be created.
45 45
46 46 By default, all sources except Mercurial will use --branchsort.
47 47 Mercurial uses --sourcesort to preserve original revision numbers
48 48 order. Sort modes have the following effects:
49 49
50 50 --branchsort convert from parent to child revision when possible,
51 51 which means branches are usually converted one after
52 52 the other. It generates more compact repositories.
53 53
54 54 --datesort sort revisions by date. Converted repositories have
55 55 good-looking changelogs but are often an order of
56 56 magnitude larger than the same ones generated by
57 57 --branchsort.
58 58
59 59 --sourcesort try to preserve source revisions order, only
60 60 supported by Mercurial sources.
61 61
62 62 If <REVMAP> isn't given, it will be put in a default location
63 63 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text file
64 64 that maps each source commit ID to the destination ID for that
65 65 revision, like so::
66 66
67 67 <source ID> <destination ID>
68 68
69 69 If the file doesn't exist, it's automatically created. It's
70 70 updated on each commit copied, so convert-repo can be interrupted
71 71 and can be run repeatedly to copy new commits.
72 72
73 73 The [username mapping] file is a simple text file that maps each
74 74 source commit author to a destination commit author. It is handy
75 75 for source SCMs that use unix logins to identify authors (eg:
76 76 CVS). One line per author mapping and the line format is:
77 77 srcauthor=whatever string you want
78 78
79 79 The filemap is a file that allows filtering and remapping of files
80 80 and directories. Comment lines start with '#'. Each line can
81 81 contain one of the following directives::
82 82
83 83 include path/to/file
84 84
85 85 exclude path/to/file
86 86
87 87 rename from/file to/file
88 88
89 89 The 'include' directive causes a file, or all files under a
90 90 directory, to be included in the destination repository, and the
91 91 exclusion of all other files and directories not explicitly
92 92 included. The 'exclude' directive causes files or directories to
93 93 be omitted. The 'rename' directive renames a file or directory. To
94 94 rename from a subdirectory into the root of the repository, use
95 95 '.' as the path to rename to.
96 96
97 97 The splicemap is a file that allows insertion of synthetic
98 98 history, letting you specify the parents of a revision. This is
99 99 useful if you want to e.g. give a Subversion merge two parents, or
100 100 graft two disconnected series of history together. Each entry
101 101 contains a key, followed by a space, followed by one or two
102 102 comma-separated values. The key is the revision ID in the source
103 103 revision control system whose parents should be modified (same
104 104 format as a key in .hg/shamap). The values are the revision IDs
105 105 (in either the source or destination revision control system) that
106 106 should be used as the new parents for that node. For example, if
107 107 you have merged "release-1.0" into "trunk", then you should
108 108 specify the revision on "trunk" as the first parent and the one on
109 109 the "release-1.0" branch as the second.
110 110
111 111 The branchmap is a file that allows you to rename a branch when it is
112 112 being brought in from whatever external repository. When used in
113 113 conjunction with a splicemap, it allows for a powerful combination
114 114 to help fix even the most badly mismanaged repositories and turn them
115 115 into nicely structured Mercurial repositories. The branchmap contains
116 116 lines of the form "original_branch_name new_branch_name".
117 117 "original_branch_name" is the name of the branch in the source
118 118 repository, and "new_branch_name" is the name of the branch is the
119 119 destination repository. This can be used to (for instance) move code
120 120 in one repository from "default" to a named branch.
121 121
122 122 Mercurial Source
123 123 ----------------
124 124
125 125 --config convert.hg.ignoreerrors=False (boolean)
126 126 ignore integrity errors when reading. Use it to fix Mercurial
127 127 repositories with missing revlogs, by converting from and to
128 128 Mercurial.
129 129 --config convert.hg.saverev=False (boolean)
130 130 store original revision ID in changeset (forces target IDs to
131 131 change)
132 132 --config convert.hg.startrev=0 (hg revision identifier)
133 133 convert start revision and its descendants
134 134
135 135 CVS Source
136 136 ----------
137 137
138 138 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
139 139 to indicate the starting point of what will be converted. Direct
140 140 access to the repository files is not needed, unless of course the
141 141 repository is :local:. The conversion uses the top level directory
142 142 in the sandbox to find the CVS repository, and then uses CVS rlog
143 143 commands to find files to convert. This means that unless a
144 144 filemap is given, all files under the starting directory will be
145 145 converted, and that any directory reorganization in the CVS
146 146 sandbox is ignored.
147 147
148 148 The options shown are the defaults.
149 149
150 150 --config convert.cvsps.cache=True (boolean)
151 151 Set to False to disable remote log caching, for testing and
152 152 debugging purposes.
153 153 --config convert.cvsps.fuzz=60 (integer)
154 154 Specify the maximum time (in seconds) that is allowed between
155 155 commits with identical user and log message in a single
156 156 changeset. When very large files were checked in as part of a
157 157 changeset then the default may not be long enough.
158 158 --config convert.cvsps.mergeto='{{mergetobranch ([-\\w]+)}}'
159 159 Specify a regular expression to which commit log messages are
160 160 matched. If a match occurs, then the conversion process will
161 161 insert a dummy revision merging the branch on which this log
162 162 message occurs to the branch indicated in the regex.
163 163 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\\w]+)}}'
164 164 Specify a regular expression to which commit log messages are
165 165 matched. If a match occurs, then the conversion process will
166 166 add the most recent revision on the branch indicated in the
167 167 regex as the second parent of the changeset.
168 168 --config hook.cvslog
169 169 Specify a Python function to be called at the end of gathering
170 170 the CVS log. The function is passed a list with the log entries,
171 171 and can modify the entries in-place, or add or delete them.
172 172 --config hook.cvschangesets
173 173 Specify a Python function to be called after the changesets
174 174 are calculated from the the CVS log. The function is passed
175 175 a list with the changeset entries, and can modify the changesets
176 176 in-place, or add or delete them.
177 177
178 178 An additional "debugcvsps" Mercurial command allows the builtin
179 179 changeset merging code to be run without doing a conversion. Its
180 180 parameters and output are similar to that of cvsps 2.1. Please see
181 181 the command help for more details.
182 182
183 183 Subversion Source
184 184 -----------------
185 185
186 186 Subversion source detects classical trunk/branches/tags layouts.
187 187 By default, the supplied "svn://repo/path/" source URL is
188 188 converted as a single branch. If "svn://repo/path/trunk" exists it
189 189 replaces the default branch. If "svn://repo/path/branches" exists,
190 190 its subdirectories are listed as possible branches. If
191 191 "svn://repo/path/tags" exists, it is looked for tags referencing
192 192 converted branches. Default "trunk", "branches" and "tags" values
193 193 can be overridden with following options. Set them to paths
194 194 relative to the source URL, or leave them blank to disable auto
195 195 detection.
196 196
197 197 --config convert.svn.branches=branches (directory name)
198 198 specify the directory containing branches
199 199 --config convert.svn.tags=tags (directory name)
200 200 specify the directory containing tags
201 201 --config convert.svn.trunk=trunk (directory name)
202 202 specify the name of the trunk branch
203 203
204 204 Source history can be retrieved starting at a specific revision,
205 205 instead of being integrally converted. Only single branch
206 206 conversions are supported.
207 207
208 208 --config convert.svn.startrev=0 (svn revision number)
209 209 specify start Subversion revision.
210 210
211 211 Perforce Source
212 212 ---------------
213 213
214 214 The Perforce (P4) importer can be given a p4 depot path or a
215 215 client specification as source. It will convert all files in the
216 216 source to a flat Mercurial repository, ignoring labels, branches
217 217 and integrations. Note that when a depot path is given you then
218 218 usually should specify a target directory, because otherwise the
219 219 target may be named ...-hg.
220 220
221 221 It is possible to limit the amount of source history to be
222 222 converted by specifying an initial Perforce revision.
223 223
224 224 --config convert.p4.startrev=0 (perforce changelist number)
225 225 specify initial Perforce revision.
226 226
227 227 Mercurial Destination
228 228 ---------------------
229 229
230 230 --config convert.hg.clonebranches=False (boolean)
231 231 dispatch source branches in separate clones.
232 232 --config convert.hg.tagsbranch=default (branch name)
233 233 tag revisions branch name
234 234 --config convert.hg.usebranchnames=True (boolean)
235 235 preserve branch names
236 236
237 237 """
238 238 return convcmd.convert(ui, src, dest, revmapfile, **opts)
239 239
240 240 def debugsvnlog(ui, **opts):
241 241 return subversion.debugsvnlog(ui, **opts)
242 242
243 243 def debugcvsps(ui, *args, **opts):
244 244 '''create changeset information from CVS
245 245
246 246 This command is intended as a debugging tool for the CVS to
247 247 Mercurial converter, and can be used as a direct replacement for
248 248 cvsps.
249 249
250 250 Hg debugcvsps reads the CVS rlog for current directory (or any
251 251 named directory) in the CVS repository, and converts the log to a
252 252 series of changesets based on matching commit log entries and
253 253 dates.'''
254 254 return cvsps.debugcvsps(ui, *args, **opts)
255 255
256 256 commands.norepo += " convert debugsvnlog debugcvsps"
257 257
258 258 cmdtable = {
259 259 "convert":
260 260 (convert,
261 261 [('A', 'authors', '', _('username mapping filename')),
262 262 ('d', 'dest-type', '', _('destination repository type')),
263 263 ('', 'filemap', '', _('remap file names using contents of file')),
264 264 ('r', 'rev', '', _('import up to target revision REV')),
265 265 ('s', 'source-type', '', _('source repository type')),
266 266 ('', 'splicemap', '', _('splice synthesized history into place')),
267 267 ('', 'branchmap', '', _('change branch names while converting')),
268 268 ('', 'branchsort', None, _('try to sort changesets by branches')),
269 269 ('', 'datesort', None, _('try to sort changesets by date')),
270 270 ('', 'sourcesort', None, _('preserve source changesets order'))],
271 271 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]')),
272 272 "debugsvnlog":
273 273 (debugsvnlog,
274 274 [],
275 275 'hg debugsvnlog'),
276 276 "debugcvsps":
277 277 (debugcvsps,
278 278 [
279 279 # Main options shared with cvsps-2.1
280 280 ('b', 'branches', [], _('only return changes on specified branches')),
281 281 ('p', 'prefix', '', _('prefix to remove from file names')),
282 282 ('r', 'revisions', [], _('only return changes after or between specified tags')),
283 283 ('u', 'update-cache', None, _("update cvs log cache")),
284 284 ('x', 'new-cache', None, _("create new cvs log cache")),
285 285 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
286 286 ('', 'root', '', _('specify cvsroot')),
287 287 # Options specific to builtin cvsps
288 288 ('', 'parents', '', _('show parent changesets')),
289 289 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
290 290 # Options that are ignored for compatibility with cvsps-2.1
291 291 ('A', 'cvs-direct', None, _('ignored for compatibility')),
292 292 ],
293 293 _('hg debugcvsps [OPTION]... [PATH]...')),
294 294 }
@@ -1,259 +1,259 b''
1 1 # bzr.py - bzr support for the convert extension
2 2 #
3 3 # Copyright 2008, 2009 Marek Kubica <marek@xivilization.net> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 # This module is for handling 'bzr', that was formerly known as Bazaar-NG;
9 9 # it cannot access 'bar' repositories, but they were never used very much
10 10
11 11 import os
12 12 from mercurial import demandimport
13 13 # these do not work with demandimport, blacklist
14 14 demandimport.ignore.extend([
15 15 'bzrlib.transactions',
16 16 'bzrlib.urlutils',
17 17 ])
18 18
19 19 from mercurial.i18n import _
20 20 from mercurial import util
21 21 from common import NoRepo, commit, converter_source
22 22
23 23 try:
24 24 # bazaar imports
25 25 from bzrlib import branch, revision, errors
26 26 from bzrlib.revisionspec import RevisionSpec
27 27 except ImportError:
28 28 pass
29 29
30 30 supportedkinds = ('file', 'symlink')
31 31
32 32 class bzr_source(converter_source):
33 33 """Reads Bazaar repositories by using the Bazaar Python libraries"""
34 34
35 35 def __init__(self, ui, path, rev=None):
36 36 super(bzr_source, self).__init__(ui, path, rev=rev)
37 37
38 38 if not os.path.exists(os.path.join(path, '.bzr')):
39 39 raise NoRepo('%s does not look like a Bazaar repo' % path)
40 40
41 41 try:
42 42 # access bzrlib stuff
43 43 branch
44 44 except NameError:
45 45 raise NoRepo('Bazaar modules could not be loaded')
46 46
47 47 path = os.path.abspath(path)
48 48 self._checkrepotype(path)
49 49 self.branch = branch.Branch.open(path)
50 50 self.sourcerepo = self.branch.repository
51 51 self._parentids = {}
52 52
53 53 def _checkrepotype(self, path):
54 54 # Lightweight checkouts detection is informational but probably
55 55 # fragile at API level. It should not terminate the conversion.
56 56 try:
57 57 from bzrlib import bzrdir
58 58 dir = bzrdir.BzrDir.open_containing(path)[0]
59 59 try:
60 60 tree = dir.open_workingtree(recommend_upgrade=False)
61 61 branch = tree.branch
62 62 except (errors.NoWorkingTree, errors.NotLocalUrl), e:
63 63 tree = None
64 64 branch = dir.open_branch()
65 65 if (tree is not None and tree.bzrdir.root_transport.base !=
66 66 branch.bzrdir.root_transport.base):
67 67 self.ui.warn(_('warning: lightweight checkouts may cause '
68 68 'conversion failures, try with a regular '
69 69 'branch instead.\n'))
70 70 except:
71 71 self.ui.note(_('bzr source type could not be determined\n'))
72 72
73 73 def before(self):
74 74 """Before the conversion begins, acquire a read lock
75 75 for all the operations that might need it. Fortunately
76 76 read locks don't block other reads or writes to the
77 77 repository, so this shouldn't have any impact on the usage of
78 78 the source repository.
79 79
80 80 The alternative would be locking on every operation that
81 81 needs locks (there are currently two: getting the file and
82 82 getting the parent map) and releasing immediately after,
83 83 but this approach can take even 40% longer."""
84 84 self.sourcerepo.lock_read()
85 85
86 86 def after(self):
87 87 self.sourcerepo.unlock()
88 88
89 89 def getheads(self):
90 90 if not self.rev:
91 91 return [self.branch.last_revision()]
92 92 try:
93 93 r = RevisionSpec.from_string(self.rev)
94 94 info = r.in_history(self.branch)
95 95 except errors.BzrError:
96 96 raise util.Abort(_('%s is not a valid revision in current branch')
97 97 % self.rev)
98 98 return [info.rev_id]
99 99
100 100 def getfile(self, name, rev):
101 101 revtree = self.sourcerepo.revision_tree(rev)
102 102 fileid = revtree.path2id(name.decode(self.encoding or 'utf-8'))
103 103 kind = None
104 104 if fileid is not None:
105 105 kind = revtree.kind(fileid)
106 106 if kind not in supportedkinds:
107 107 # the file is not available anymore - was deleted
108 108 raise IOError(_('%s is not available in %s anymore') %
109 109 (name, rev))
110 110 if kind == 'symlink':
111 111 target = revtree.get_symlink_target(fileid)
112 112 if target is None:
113 113 raise util.Abort(_('%s.%s symlink has no target')
114 114 % (name, rev))
115 115 return target
116 116 else:
117 117 sio = revtree.get_file(fileid)
118 118 return sio.read()
119 119
120 120 def getmode(self, name, rev):
121 121 return self._modecache[(name, rev)]
122 122
123 123 def getchanges(self, version):
124 124 # set up caches: modecache and revtree
125 125 self._modecache = {}
126 126 self._revtree = self.sourcerepo.revision_tree(version)
127 127 # get the parentids from the cache
128 128 parentids = self._parentids.pop(version)
129 129 # only diff against first parent id
130 130 prevtree = self.sourcerepo.revision_tree(parentids[0])
131 131 return self._gettreechanges(self._revtree, prevtree)
132 132
133 133 def getcommit(self, version):
134 134 rev = self.sourcerepo.get_revision(version)
135 135 # populate parent id cache
136 136 if not rev.parent_ids:
137 137 parents = []
138 138 self._parentids[version] = (revision.NULL_REVISION,)
139 139 else:
140 140 parents = self._filterghosts(rev.parent_ids)
141 141 self._parentids[version] = parents
142 142
143 143 return commit(parents=parents,
144 144 date='%d %d' % (rev.timestamp, -rev.timezone),
145 145 author=self.recode(rev.committer),
146 146 # bzr returns bytestrings or unicode, depending on the content
147 147 desc=self.recode(rev.message),
148 148 rev=version)
149 149
150 150 def gettags(self):
151 151 if not self.branch.supports_tags():
152 152 return {}
153 153 tagdict = self.branch.tags.get_tag_dict()
154 154 bytetags = {}
155 155 for name, rev in tagdict.iteritems():
156 156 bytetags[self.recode(name)] = rev
157 157 return bytetags
158 158
159 159 def getchangedfiles(self, rev, i):
160 160 self._modecache = {}
161 161 curtree = self.sourcerepo.revision_tree(rev)
162 162 if i is not None:
163 163 parentid = self._parentids[rev][i]
164 164 else:
165 165 # no parent id, get the empty revision
166 166 parentid = revision.NULL_REVISION
167 167
168 168 prevtree = self.sourcerepo.revision_tree(parentid)
169 169 changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]]
170 170 return changes
171 171
172 172 def _gettreechanges(self, current, origin):
173 173 revid = current._revision_id;
174 174 changes = []
175 175 renames = {}
176 176 for (fileid, paths, changed_content, versioned, parent, name,
177 177 kind, executable) in current.iter_changes(origin):
178 178
179 179 if paths[0] == u'' or paths[1] == u'':
180 180 # ignore changes to tree root
181 181 continue
182 182
183 183 # bazaar tracks directories, mercurial does not, so
184 184 # we have to rename the directory contents
185 185 if kind[1] == 'directory':
186 186 if kind[0] not in (None, 'directory'):
187 187 # Replacing 'something' with a directory, record it
188 188 # so it can be removed.
189 189 changes.append((self.recode(paths[0]), revid))
190 190
191 191 if None not in paths and paths[0] != paths[1]:
192 192 # neither an add nor an delete - a move
193 193 # rename all directory contents manually
194 194 subdir = origin.inventory.path2id(paths[0])
195 195 # get all child-entries of the directory
196 196 for name, entry in origin.inventory.iter_entries(subdir):
197 197 # hg does not track directory renames
198 198 if entry.kind == 'directory':
199 199 continue
200 200 frompath = self.recode(paths[0] + '/' + name)
201 201 topath = self.recode(paths[1] + '/' + name)
202 202 # register the files as changed
203 203 changes.append((frompath, revid))
204 204 changes.append((topath, revid))
205 205 # add to mode cache
206 206 mode = ((entry.executable and 'x') or (entry.kind == 'symlink' and 's')
207 207 or '')
208 208 self._modecache[(topath, revid)] = mode
209 209 # register the change as move
210 210 renames[topath] = frompath
211 211
212 212 # no futher changes, go to the next change
213 213 continue
214 214
215 215 # we got unicode paths, need to convert them
216 216 path, topath = [self.recode(part) for part in paths]
217 217
218 218 if topath is None:
219 219 # file deleted
220 220 changes.append((path, revid))
221 221 continue
222 222
223 223 # renamed
224 224 if path and path != topath:
225 225 renames[topath] = path
226 226 changes.append((path, revid))
227 227
228 228 # populate the mode cache
229 229 kind, executable = [e[1] for e in (kind, executable)]
230 230 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
231 231 or '')
232 232 self._modecache[(topath, revid)] = mode
233 233 changes.append((topath, revid))
234 234
235 235 return changes, renames
236 236
237 237 def _filterghosts(self, ids):
238 238 """Filters out ghost revisions which hg does not support, see
239 239 <http://bazaar-vcs.org/GhostRevision>
240 240 """
241 241 parentmap = self.sourcerepo.get_parent_map(ids)
242 242 parents = tuple([parent for parent in ids if parent in parentmap])
243 243 return parents
244 244
245 245 def recode(self, s, encoding=None):
246 246 """This version of recode tries to encode unicode to bytecode,
247 247 and preferably using the UTF-8 codec.
248 248 Other types than Unicode are silently returned, this is by
249 249 intention, e.g. the None-type is not going to be encoded but instead
250 250 just passed through
251 251 """
252 252 if not encoding:
253 253 encoding = self.encoding or 'utf-8'
254 254
255 255 if isinstance(s, unicode):
256 256 return s.encode(encoding)
257 257 else:
258 258 # leave it alone
259 259 return s
@@ -1,391 +1,391 b''
1 1 # common.py - common code for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 import base64, errno
9 9 import os
10 10 import cPickle as pickle
11 11 from mercurial import util
12 12 from mercurial.i18n import _
13 13
14 14 def encodeargs(args):
15 15 def encodearg(s):
16 16 lines = base64.encodestring(s)
17 17 lines = [l.splitlines()[0] for l in lines]
18 18 return ''.join(lines)
19 19
20 20 s = pickle.dumps(args)
21 21 return encodearg(s)
22 22
23 23 def decodeargs(s):
24 24 s = base64.decodestring(s)
25 25 return pickle.loads(s)
26 26
27 27 class MissingTool(Exception): pass
28 28
29 29 def checktool(exe, name=None, abort=True):
30 30 name = name or exe
31 31 if not util.find_exe(exe):
32 32 exc = abort and util.Abort or MissingTool
33 33 raise exc(_('cannot find required "%s" tool') % name)
34 34
35 35 class NoRepo(Exception): pass
36 36
37 37 SKIPREV = 'SKIP'
38 38
39 39 class commit(object):
40 40 def __init__(self, author, date, desc, parents, branch=None, rev=None,
41 41 extra={}, sortkey=None):
42 42 self.author = author or 'unknown'
43 43 self.date = date or '0 0'
44 44 self.desc = desc
45 45 self.parents = parents
46 46 self.branch = branch
47 47 self.rev = rev
48 48 self.extra = extra
49 49 self.sortkey = sortkey
50 50
51 51 class converter_source(object):
52 52 """Conversion source interface"""
53 53
54 54 def __init__(self, ui, path=None, rev=None):
55 55 """Initialize conversion source (or raise NoRepo("message")
56 56 exception if path is not a valid repository)"""
57 57 self.ui = ui
58 58 self.path = path
59 59 self.rev = rev
60 60
61 61 self.encoding = 'utf-8'
62 62
63 63 def before(self):
64 64 pass
65 65
66 66 def after(self):
67 67 pass
68 68
69 69 def setrevmap(self, revmap):
70 70 """set the map of already-converted revisions"""
71 71 pass
72 72
73 73 def getheads(self):
74 74 """Return a list of this repository's heads"""
75 75 raise NotImplementedError()
76 76
77 77 def getfile(self, name, rev):
78 78 """Return file contents as a string. rev is the identifier returned
79 79 by a previous call to getchanges(). Raise IOError to indicate that
80 80 name was deleted in rev.
81 81 """
82 82 raise NotImplementedError()
83 83
84 84 def getmode(self, name, rev):
85 85 """Return file mode, eg. '', 'x', or 'l'. rev is the identifier
86 86 returned by a previous call to getchanges().
87 87 """
88 88 raise NotImplementedError()
89 89
90 90 def getchanges(self, version):
91 91 """Returns a tuple of (files, copies).
92 92
93 93 files is a sorted list of (filename, id) tuples for all files
94 94 changed between version and its first parent returned by
95 95 getcommit(). id is the source revision id of the file.
96 96
97 97 copies is a dictionary of dest: source
98 98 """
99 99 raise NotImplementedError()
100 100
101 101 def getcommit(self, version):
102 102 """Return the commit object for version"""
103 103 raise NotImplementedError()
104 104
105 105 def gettags(self):
106 106 """Return the tags as a dictionary of name: revision
107 107
108 108 Tag names must be UTF-8 strings.
109 109 """
110 110 raise NotImplementedError()
111 111
112 112 def recode(self, s, encoding=None):
113 113 if not encoding:
114 114 encoding = self.encoding or 'utf-8'
115 115
116 116 if isinstance(s, unicode):
117 117 return s.encode("utf-8")
118 118 try:
119 119 return s.decode(encoding).encode("utf-8")
120 120 except:
121 121 try:
122 122 return s.decode("latin-1").encode("utf-8")
123 123 except:
124 124 return s.decode(encoding, "replace").encode("utf-8")
125 125
126 126 def getchangedfiles(self, rev, i):
127 127 """Return the files changed by rev compared to parent[i].
128 128
129 129 i is an index selecting one of the parents of rev. The return
130 130 value should be the list of files that are different in rev and
131 131 this parent.
132 132
133 133 If rev has no parents, i is None.
134 134
135 135 This function is only needed to support --filemap
136 136 """
137 137 raise NotImplementedError()
138 138
139 139 def converted(self, rev, sinkrev):
140 140 '''Notify the source that a revision has been converted.'''
141 141 pass
142 142
143 143 def hasnativeorder(self):
144 144 """Return true if this source has a meaningful, native revision
145 145 order. For instance, Mercurial revisions are store sequentially
146 146 while there is no such global ordering with Darcs.
147 147 """
148 148 return False
149 149
150 150 def lookuprev(self, rev):
151 151 """If rev is a meaningful revision reference in source, return
152 152 the referenced identifier in the same format used by getcommit().
153 153 return None otherwise.
154 154 """
155 155 return None
156 156
157 157 class converter_sink(object):
158 158 """Conversion sink (target) interface"""
159 159
160 160 def __init__(self, ui, path):
161 161 """Initialize conversion sink (or raise NoRepo("message")
162 162 exception if path is not a valid repository)
163 163
164 164 created is a list of paths to remove if a fatal error occurs
165 165 later"""
166 166 self.ui = ui
167 167 self.path = path
168 168 self.created = []
169 169
170 170 def getheads(self):
171 171 """Return a list of this repository's heads"""
172 172 raise NotImplementedError()
173 173
174 174 def revmapfile(self):
175 175 """Path to a file that will contain lines
176 176 source_rev_id sink_rev_id
177 177 mapping equivalent revision identifiers for each system."""
178 178 raise NotImplementedError()
179 179
180 180 def authorfile(self):
181 181 """Path to a file that will contain lines
182 182 srcauthor=dstauthor
183 183 mapping equivalent authors identifiers for each system."""
184 184 return None
185 185
186 186 def putcommit(self, files, copies, parents, commit, source, revmap):
187 187 """Create a revision with all changed files listed in 'files'
188 188 and having listed parents. 'commit' is a commit object
189 189 containing at a minimum the author, date, and message for this
190 190 changeset. 'files' is a list of (path, version) tuples,
191 191 'copies' is a dictionary mapping destinations to sources,
192 192 'source' is the source repository, and 'revmap' is a mapfile
193 193 of source revisions to converted revisions. Only getfile(),
194 194 getmode(), and lookuprev() should be called on 'source'.
195 195
196 196 Note that the sink repository is not told to update itself to
197 197 a particular revision (or even what that revision would be)
198 198 before it receives the file data.
199 199 """
200 200 raise NotImplementedError()
201 201
202 202 def puttags(self, tags):
203 203 """Put tags into sink.
204 204
205 205 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
206 206 Return a pair (tag_revision, tag_parent_revision), or (None, None)
207 207 if nothing was changed.
208 208 """
209 209 raise NotImplementedError()
210 210
211 211 def setbranch(self, branch, pbranches):
212 212 """Set the current branch name. Called before the first putcommit
213 213 on the branch.
214 214 branch: branch name for subsequent commits
215 215 pbranches: (converted parent revision, parent branch) tuples"""
216 216 pass
217 217
218 218 def setfilemapmode(self, active):
219 219 """Tell the destination that we're using a filemap
220 220
221 221 Some converter_sources (svn in particular) can claim that a file
222 222 was changed in a revision, even if there was no change. This method
223 223 tells the destination that we're using a filemap and that it should
224 224 filter empty revisions.
225 225 """
226 226 pass
227 227
228 228 def before(self):
229 229 pass
230 230
231 231 def after(self):
232 232 pass
233 233
234 234
235 235 class commandline(object):
236 236 def __init__(self, ui, command):
237 237 self.ui = ui
238 238 self.command = command
239 239
240 240 def prerun(self):
241 241 pass
242 242
243 243 def postrun(self):
244 244 pass
245 245
246 246 def _cmdline(self, cmd, *args, **kwargs):
247 247 cmdline = [self.command, cmd] + list(args)
248 248 for k, v in kwargs.iteritems():
249 249 if len(k) == 1:
250 250 cmdline.append('-' + k)
251 251 else:
252 252 cmdline.append('--' + k.replace('_', '-'))
253 253 try:
254 254 if len(k) == 1:
255 255 cmdline.append('' + v)
256 256 else:
257 257 cmdline[-1] += '=' + v
258 258 except TypeError:
259 259 pass
260 260 cmdline = [util.shellquote(arg) for arg in cmdline]
261 261 if not self.ui.debugflag:
262 262 cmdline += ['2>', util.nulldev]
263 263 cmdline += ['<', util.nulldev]
264 264 cmdline = ' '.join(cmdline)
265 265 return cmdline
266 266
267 267 def _run(self, cmd, *args, **kwargs):
268 268 cmdline = self._cmdline(cmd, *args, **kwargs)
269 269 self.ui.debug('running: %s\n' % (cmdline,))
270 270 self.prerun()
271 271 try:
272 272 return util.popen(cmdline)
273 273 finally:
274 274 self.postrun()
275 275
276 276 def run(self, cmd, *args, **kwargs):
277 277 fp = self._run(cmd, *args, **kwargs)
278 278 output = fp.read()
279 279 self.ui.debug(output)
280 280 return output, fp.close()
281 281
282 282 def runlines(self, cmd, *args, **kwargs):
283 283 fp = self._run(cmd, *args, **kwargs)
284 284 output = fp.readlines()
285 285 self.ui.debug(''.join(output))
286 286 return output, fp.close()
287 287
288 288 def checkexit(self, status, output=''):
289 289 if status:
290 290 if output:
291 291 self.ui.warn(_('%s error:\n') % self.command)
292 292 self.ui.warn(output)
293 293 msg = util.explain_exit(status)[0]
294 294 raise util.Abort('%s %s' % (self.command, msg))
295 295
296 296 def run0(self, cmd, *args, **kwargs):
297 297 output, status = self.run(cmd, *args, **kwargs)
298 298 self.checkexit(status, output)
299 299 return output
300 300
301 301 def runlines0(self, cmd, *args, **kwargs):
302 302 output, status = self.runlines(cmd, *args, **kwargs)
303 303 self.checkexit(status, ''.join(output))
304 304 return output
305 305
306 306 def getargmax(self):
307 307 if '_argmax' in self.__dict__:
308 308 return self._argmax
309 309
310 310 # POSIX requires at least 4096 bytes for ARG_MAX
311 311 self._argmax = 4096
312 312 try:
313 313 self._argmax = os.sysconf("SC_ARG_MAX")
314 314 except:
315 315 pass
316 316
317 317 # Windows shells impose their own limits on command line length,
318 318 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
319 319 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
320 320 # details about cmd.exe limitations.
321 321
322 322 # Since ARG_MAX is for command line _and_ environment, lower our limit
323 323 # (and make happy Windows shells while doing this).
324 324
325 325 self._argmax = self._argmax/2 - 1
326 326 return self._argmax
327 327
328 328 def limit_arglist(self, arglist, cmd, *args, **kwargs):
329 329 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
330 330 bytes = 0
331 331 fl = []
332 332 for fn in arglist:
333 333 b = len(fn) + 3
334 334 if bytes + b < limit or len(fl) == 0:
335 335 fl.append(fn)
336 336 bytes += b
337 337 else:
338 338 yield fl
339 339 fl = [fn]
340 340 bytes = b
341 341 if fl:
342 342 yield fl
343 343
344 344 def xargs(self, arglist, cmd, *args, **kwargs):
345 345 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
346 346 self.run0(cmd, *(list(args) + l), **kwargs)
347 347
348 348 class mapfile(dict):
349 349 def __init__(self, ui, path):
350 350 super(mapfile, self).__init__()
351 351 self.ui = ui
352 352 self.path = path
353 353 self.fp = None
354 354 self.order = []
355 355 self._read()
356 356
357 357 def _read(self):
358 358 if not self.path:
359 359 return
360 360 try:
361 361 fp = open(self.path, 'r')
362 362 except IOError, err:
363 363 if err.errno != errno.ENOENT:
364 364 raise
365 365 return
366 366 for i, line in enumerate(fp):
367 367 try:
368 368 key, value = line.splitlines()[0].rsplit(' ', 1)
369 369 except ValueError:
370 370 raise util.Abort(_('syntax error in %s(%d): key/value pair expected')
371 371 % (self.path, i+1))
372 372 if key not in self:
373 373 self.order.append(key)
374 374 super(mapfile, self).__setitem__(key, value)
375 375 fp.close()
376 376
377 377 def __setitem__(self, key, value):
378 378 if self.fp is None:
379 379 try:
380 380 self.fp = open(self.path, 'a')
381 381 except IOError, err:
382 382 raise util.Abort(_('could not open map file %r: %s') %
383 383 (self.path, err.strerror))
384 384 self.fp.write('%s %s\n' % (key, value))
385 385 self.fp.flush()
386 386 super(mapfile, self).__setitem__(key, value)
387 387
388 388 def close(self):
389 389 if self.fp:
390 390 self.fp.close()
391 391 self.fp = None
@@ -1,403 +1,403 b''
1 1 # convcmd - convert extension commands definition
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 9 from cvs import convert_cvs
10 10 from darcs import darcs_source
11 11 from git import convert_git
12 12 from hg import mercurial_source, mercurial_sink
13 13 from subversion import svn_source, svn_sink
14 14 from monotone import monotone_source
15 15 from gnuarch import gnuarch_source
16 16 from bzr import bzr_source
17 17 from p4 import p4_source
18 18 import filemap
19 19
20 20 import os, shutil
21 21 from mercurial import hg, util, encoding
22 22 from mercurial.i18n import _
23 23
24 24 orig_encoding = 'ascii'
25 25
26 26 def recode(s):
27 27 if isinstance(s, unicode):
28 28 return s.encode(orig_encoding, 'replace')
29 29 else:
30 30 return s.decode('utf-8').encode(orig_encoding, 'replace')
31 31
32 32 source_converters = [
33 33 ('cvs', convert_cvs, 'branchsort'),
34 34 ('git', convert_git, 'branchsort'),
35 35 ('svn', svn_source, 'branchsort'),
36 36 ('hg', mercurial_source, 'sourcesort'),
37 37 ('darcs', darcs_source, 'branchsort'),
38 38 ('mtn', monotone_source, 'branchsort'),
39 39 ('gnuarch', gnuarch_source, 'branchsort'),
40 40 ('bzr', bzr_source, 'branchsort'),
41 41 ('p4', p4_source, 'branchsort'),
42 42 ]
43 43
44 44 sink_converters = [
45 45 ('hg', mercurial_sink),
46 46 ('svn', svn_sink),
47 47 ]
48 48
49 49 def convertsource(ui, path, type, rev):
50 50 exceptions = []
51 51 if type and type not in [s[0] for s in source_converters]:
52 52 raise util.Abort(_('%s: invalid source repository type') % type)
53 53 for name, source, sortmode in source_converters:
54 54 try:
55 55 if not type or name == type:
56 56 return source(ui, path, rev), sortmode
57 57 except (NoRepo, MissingTool), inst:
58 58 exceptions.append(inst)
59 59 if not ui.quiet:
60 60 for inst in exceptions:
61 61 ui.write("%s\n" % inst)
62 62 raise util.Abort(_('%s: missing or unsupported repository') % path)
63 63
64 64 def convertsink(ui, path, type):
65 65 if type and type not in [s[0] for s in sink_converters]:
66 66 raise util.Abort(_('%s: invalid destination repository type') % type)
67 67 for name, sink in sink_converters:
68 68 try:
69 69 if not type or name == type:
70 70 return sink(ui, path)
71 71 except NoRepo, inst:
72 72 ui.note(_("convert: %s\n") % inst)
73 73 raise util.Abort(_('%s: unknown repository type') % path)
74 74
75 75 class converter(object):
76 76 def __init__(self, ui, source, dest, revmapfile, opts):
77 77
78 78 self.source = source
79 79 self.dest = dest
80 80 self.ui = ui
81 81 self.opts = opts
82 82 self.commitcache = {}
83 83 self.authors = {}
84 84 self.authorfile = None
85 85
86 86 # Record converted revisions persistently: maps source revision
87 87 # ID to target revision ID (both strings). (This is how
88 88 # incremental conversions work.)
89 89 self.map = mapfile(ui, revmapfile)
90 90
91 91 # Read first the dst author map if any
92 92 authorfile = self.dest.authorfile()
93 93 if authorfile and os.path.exists(authorfile):
94 94 self.readauthormap(authorfile)
95 95 # Extend/Override with new author map if necessary
96 96 if opts.get('authors'):
97 97 self.readauthormap(opts.get('authors'))
98 98 self.authorfile = self.dest.authorfile()
99 99
100 100 self.splicemap = mapfile(ui, opts.get('splicemap'))
101 101 self.branchmap = mapfile(ui, opts.get('branchmap'))
102 102
103 103 def walktree(self, heads):
104 104 '''Return a mapping that identifies the uncommitted parents of every
105 105 uncommitted changeset.'''
106 106 visit = heads
107 107 known = set()
108 108 parents = {}
109 109 while visit:
110 110 n = visit.pop(0)
111 111 if n in known or n in self.map: continue
112 112 known.add(n)
113 113 commit = self.cachecommit(n)
114 114 parents[n] = []
115 115 for p in commit.parents:
116 116 parents[n].append(p)
117 117 visit.append(p)
118 118
119 119 return parents
120 120
121 121 def toposort(self, parents, sortmode):
122 122 '''Return an ordering such that every uncommitted changeset is
123 123 preceeded by all its uncommitted ancestors.'''
124 124
125 125 def mapchildren(parents):
126 126 """Return a (children, roots) tuple where 'children' maps parent
127 127 revision identifiers to children ones, and 'roots' is the list of
128 128 revisions without parents. 'parents' must be a mapping of revision
129 129 identifier to its parents ones.
130 130 """
131 131 visit = parents.keys()
132 132 seen = set()
133 133 children = {}
134 134 roots = []
135 135
136 136 while visit:
137 137 n = visit.pop(0)
138 138 if n in seen:
139 139 continue
140 140 seen.add(n)
141 141 # Ensure that nodes without parents are present in the
142 142 # 'children' mapping.
143 143 children.setdefault(n, [])
144 144 hasparent = False
145 145 for p in parents[n]:
146 146 if not p in self.map:
147 147 visit.append(p)
148 148 hasparent = True
149 149 children.setdefault(p, []).append(n)
150 150 if not hasparent:
151 151 roots.append(n)
152 152
153 153 return children, roots
154 154
155 155 # Sort functions are supposed to take a list of revisions which
156 156 # can be converted immediately and pick one
157 157
158 158 def makebranchsorter():
159 159 """If the previously converted revision has a child in the
160 160 eligible revisions list, pick it. Return the list head
161 161 otherwise. Branch sort attempts to minimize branch
162 162 switching, which is harmful for Mercurial backend
163 163 compression.
164 164 """
165 165 prev = [None]
166 166 def picknext(nodes):
167 167 next = nodes[0]
168 168 for n in nodes:
169 169 if prev[0] in parents[n]:
170 170 next = n
171 171 break
172 172 prev[0] = next
173 173 return next
174 174 return picknext
175 175
176 176 def makesourcesorter():
177 177 """Source specific sort."""
178 178 keyfn = lambda n: self.commitcache[n].sortkey
179 179 def picknext(nodes):
180 180 return sorted(nodes, key=keyfn)[0]
181 181 return picknext
182 182
183 183 def makedatesorter():
184 184 """Sort revisions by date."""
185 185 dates = {}
186 186 def getdate(n):
187 187 if n not in dates:
188 188 dates[n] = util.parsedate(self.commitcache[n].date)
189 189 return dates[n]
190 190
191 191 def picknext(nodes):
192 192 return min([(getdate(n), n) for n in nodes])[1]
193 193
194 194 return picknext
195 195
196 196 if sortmode == 'branchsort':
197 197 picknext = makebranchsorter()
198 198 elif sortmode == 'datesort':
199 199 picknext = makedatesorter()
200 200 elif sortmode == 'sourcesort':
201 201 picknext = makesourcesorter()
202 202 else:
203 203 raise util.Abort(_('unknown sort mode: %s') % sortmode)
204 204
205 205 children, actives = mapchildren(parents)
206 206
207 207 s = []
208 208 pendings = {}
209 209 while actives:
210 210 n = picknext(actives)
211 211 actives.remove(n)
212 212 s.append(n)
213 213
214 214 # Update dependents list
215 215 for c in children.get(n, []):
216 216 if c not in pendings:
217 217 pendings[c] = [p for p in parents[c] if p not in self.map]
218 218 try:
219 219 pendings[c].remove(n)
220 220 except ValueError:
221 221 raise util.Abort(_('cycle detected between %s and %s')
222 222 % (recode(c), recode(n)))
223 223 if not pendings[c]:
224 224 # Parents are converted, node is eligible
225 225 actives.insert(0, c)
226 226 pendings[c] = None
227 227
228 228 if len(s) != len(parents):
229 229 raise util.Abort(_("not all revisions were sorted"))
230 230
231 231 return s
232 232
233 233 def writeauthormap(self):
234 234 authorfile = self.authorfile
235 235 if authorfile:
236 236 self.ui.status(_('Writing author map file %s\n') % authorfile)
237 237 ofile = open(authorfile, 'w+')
238 238 for author in self.authors:
239 239 ofile.write("%s=%s\n" % (author, self.authors[author]))
240 240 ofile.close()
241 241
242 242 def readauthormap(self, authorfile):
243 243 afile = open(authorfile, 'r')
244 244 for line in afile:
245 245
246 246 line = line.strip()
247 247 if not line or line.startswith('#'):
248 248 continue
249 249
250 250 try:
251 251 srcauthor, dstauthor = line.split('=', 1)
252 252 except ValueError:
253 253 msg = _('Ignoring bad line in author map file %s: %s\n')
254 254 self.ui.warn(msg % (authorfile, line.rstrip()))
255 255 continue
256 256
257 257 srcauthor = srcauthor.strip()
258 258 dstauthor = dstauthor.strip()
259 259 if self.authors.get(srcauthor) in (None, dstauthor):
260 260 msg = _('mapping author %s to %s\n')
261 261 self.ui.debug(msg % (srcauthor, dstauthor))
262 262 self.authors[srcauthor] = dstauthor
263 263 continue
264 264
265 265 m = _('overriding mapping for author %s, was %s, will be %s\n')
266 266 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
267 267
268 268 afile.close()
269 269
270 270 def cachecommit(self, rev):
271 271 commit = self.source.getcommit(rev)
272 272 commit.author = self.authors.get(commit.author, commit.author)
273 273 commit.branch = self.branchmap.get(commit.branch, commit.branch)
274 274 self.commitcache[rev] = commit
275 275 return commit
276 276
277 277 def copy(self, rev):
278 278 commit = self.commitcache[rev]
279 279
280 280 changes = self.source.getchanges(rev)
281 281 if isinstance(changes, basestring):
282 282 if changes == SKIPREV:
283 283 dest = SKIPREV
284 284 else:
285 285 dest = self.map[changes]
286 286 self.map[rev] = dest
287 287 return
288 288 files, copies = changes
289 289 pbranches = []
290 290 if commit.parents:
291 291 for prev in commit.parents:
292 292 if prev not in self.commitcache:
293 293 self.cachecommit(prev)
294 294 pbranches.append((self.map[prev],
295 295 self.commitcache[prev].branch))
296 296 self.dest.setbranch(commit.branch, pbranches)
297 297 try:
298 298 parents = self.splicemap[rev].replace(',', ' ').split()
299 299 self.ui.status(_('spliced in %s as parents of %s\n') %
300 300 (parents, rev))
301 301 parents = [self.map.get(p, p) for p in parents]
302 302 except KeyError:
303 303 parents = [b[0] for b in pbranches]
304 304 newnode = self.dest.putcommit(files, copies, parents, commit,
305 305 self.source, self.map)
306 306 self.source.converted(rev, newnode)
307 307 self.map[rev] = newnode
308 308
309 309 def convert(self, sortmode):
310 310 try:
311 311 self.source.before()
312 312 self.dest.before()
313 313 self.source.setrevmap(self.map)
314 314 self.ui.status(_("scanning source...\n"))
315 315 heads = self.source.getheads()
316 316 parents = self.walktree(heads)
317 317 self.ui.status(_("sorting...\n"))
318 318 t = self.toposort(parents, sortmode)
319 319 num = len(t)
320 320 c = None
321 321
322 322 self.ui.status(_("converting...\n"))
323 323 for c in t:
324 324 num -= 1
325 325 desc = self.commitcache[c].desc
326 326 if "\n" in desc:
327 327 desc = desc.splitlines()[0]
328 328 # convert log message to local encoding without using
329 329 # tolocal() because encoding.encoding conver() use it as
330 330 # 'utf-8'
331 331 self.ui.status("%d %s\n" % (num, recode(desc)))
332 332 self.ui.note(_("source: %s\n") % recode(c))
333 333 self.copy(c)
334 334
335 335 tags = self.source.gettags()
336 336 ctags = {}
337 337 for k in tags:
338 338 v = tags[k]
339 339 if self.map.get(v, SKIPREV) != SKIPREV:
340 340 ctags[k] = self.map[v]
341 341
342 342 if c and ctags:
343 343 nrev, tagsparent = self.dest.puttags(ctags)
344 344 if nrev and tagsparent:
345 345 # write another hash correspondence to override the previous
346 346 # one so we don't end up with extra tag heads
347 347 tagsparents = [e for e in self.map.iteritems()
348 348 if e[1] == tagsparent]
349 349 if tagsparents:
350 350 self.map[tagsparents[0][0]] = nrev
351 351
352 352 self.writeauthormap()
353 353 finally:
354 354 self.cleanup()
355 355
356 356 def cleanup(self):
357 357 try:
358 358 self.dest.after()
359 359 finally:
360 360 self.source.after()
361 361 self.map.close()
362 362
363 363 def convert(ui, src, dest=None, revmapfile=None, **opts):
364 364 global orig_encoding
365 365 orig_encoding = encoding.encoding
366 366 encoding.encoding = 'UTF-8'
367 367
368 368 if not dest:
369 369 dest = hg.defaultdest(src) + "-hg"
370 370 ui.status(_("assuming destination %s\n") % dest)
371 371
372 372 destc = convertsink(ui, dest, opts.get('dest_type'))
373 373
374 374 try:
375 375 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
376 376 opts.get('rev'))
377 377 except Exception:
378 378 for path in destc.created:
379 379 shutil.rmtree(path, True)
380 380 raise
381 381
382 382 sortmodes = ('branchsort', 'datesort', 'sourcesort')
383 383 sortmode = [m for m in sortmodes if opts.get(m)]
384 384 if len(sortmode) > 1:
385 385 raise util.Abort(_('more than one sort mode specified'))
386 386 sortmode = sortmode and sortmode[0] or defaultsort
387 387 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
388 388 raise util.Abort(_('--sourcesort is not supported by this data source'))
389 389
390 390 fmap = opts.get('filemap')
391 391 if fmap:
392 392 srcc = filemap.filemap_source(ui, srcc, fmap)
393 393 destc.setfilemapmode(True)
394 394
395 395 if not revmapfile:
396 396 try:
397 397 revmapfile = destc.revmapfile()
398 398 except:
399 399 revmapfile = os.path.join(destc, "map")
400 400
401 401 c = converter(ui, srcc, destc, revmapfile, opts)
402 402 c.convert(sortmode)
403 403
@@ -1,276 +1,276 b''
1 1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 import os, locale, re, socket, errno
9 9 from cStringIO import StringIO
10 10 from mercurial import util
11 11 from mercurial.i18n import _
12 12
13 13 from common import NoRepo, commit, converter_source, checktool
14 14 import cvsps
15 15
16 16 class convert_cvs(converter_source):
17 17 def __init__(self, ui, path, rev=None):
18 18 super(convert_cvs, self).__init__(ui, path, rev=rev)
19 19
20 20 cvs = os.path.join(path, "CVS")
21 21 if not os.path.exists(cvs):
22 22 raise NoRepo("%s does not look like a CVS checkout" % path)
23 23
24 24 checktool('cvs')
25 25
26 26 self.changeset = None
27 27 self.files = {}
28 28 self.tags = {}
29 29 self.lastbranch = {}
30 30 self.socket = None
31 31 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
32 32 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
33 33 self.encoding = locale.getpreferredencoding()
34 34
35 35 self._connect()
36 36
37 37 def _parse(self):
38 38 if self.changeset is not None:
39 39 return
40 40 self.changeset = {}
41 41
42 42 maxrev = 0
43 43 if self.rev:
44 44 # TODO: handle tags
45 45 try:
46 46 # patchset number?
47 47 maxrev = int(self.rev)
48 48 except ValueError:
49 49 raise util.Abort(_('revision %s is not a patchset number') % self.rev)
50 50
51 51 d = os.getcwd()
52 52 try:
53 53 os.chdir(self.path)
54 54 id = None
55 55 state = 0
56 56 filerevids = {}
57 57
58 58 cache = 'update'
59 59 if not self.ui.configbool('convert', 'cvsps.cache', True):
60 60 cache = None
61 61 db = cvsps.createlog(self.ui, cache=cache)
62 62 db = cvsps.createchangeset(self.ui, db,
63 63 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
64 64 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
65 65 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
66 66
67 67 for cs in db:
68 68 if maxrev and cs.id>maxrev:
69 69 break
70 70 id = str(cs.id)
71 71 cs.author = self.recode(cs.author)
72 72 self.lastbranch[cs.branch] = id
73 73 cs.comment = self.recode(cs.comment)
74 74 date = util.datestr(cs.date)
75 75 self.tags.update(dict.fromkeys(cs.tags, id))
76 76
77 77 files = {}
78 78 for f in cs.entries:
79 79 files[f.file] = "%s%s" % ('.'.join([str(x) for x in f.revision]),
80 80 ['', '(DEAD)'][f.dead])
81 81
82 82 # add current commit to set
83 83 c = commit(author=cs.author, date=date,
84 84 parents=[str(p.id) for p in cs.parents],
85 85 desc=cs.comment, branch=cs.branch or '')
86 86 self.changeset[id] = c
87 87 self.files[id] = files
88 88
89 89 self.heads = self.lastbranch.values()
90 90 finally:
91 91 os.chdir(d)
92 92
93 93 def _connect(self):
94 94 root = self.cvsroot
95 95 conntype = None
96 96 user, host = None, None
97 97 cmd = ['cvs', 'server']
98 98
99 99 self.ui.status(_("connecting to %s\n") % root)
100 100
101 101 if root.startswith(":pserver:"):
102 102 root = root[9:]
103 103 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
104 104 root)
105 105 if m:
106 106 conntype = "pserver"
107 107 user, passw, serv, port, root = m.groups()
108 108 if not user:
109 109 user = "anonymous"
110 110 if not port:
111 111 port = 2401
112 112 else:
113 113 port = int(port)
114 114 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
115 115 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
116 116
117 117 if not passw:
118 118 passw = "A"
119 119 cvspass = os.path.expanduser("~/.cvspass")
120 120 try:
121 121 pf = open(cvspass)
122 122 for line in pf.read().splitlines():
123 123 part1, part2 = line.split(' ', 1)
124 124 if part1 == '/1':
125 125 # /1 :pserver:user@example.com:2401/cvsroot/foo Ah<Z
126 126 part1, part2 = part2.split(' ', 1)
127 127 format = format1
128 128 else:
129 129 # :pserver:user@example.com:/cvsroot/foo Ah<Z
130 130 format = format0
131 131 if part1 == format:
132 132 passw = part2
133 133 break
134 134 pf.close()
135 135 except IOError, inst:
136 136 if inst.errno != errno.ENOENT:
137 137 if not getattr(inst, 'filename', None):
138 138 inst.filename = cvspass
139 139 raise
140 140
141 141 sck = socket.socket()
142 142 sck.connect((serv, port))
143 143 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
144 144 "END AUTH REQUEST", ""]))
145 145 if sck.recv(128) != "I LOVE YOU\n":
146 146 raise util.Abort(_("CVS pserver authentication failed"))
147 147
148 148 self.writep = self.readp = sck.makefile('r+')
149 149
150 150 if not conntype and root.startswith(":local:"):
151 151 conntype = "local"
152 152 root = root[7:]
153 153
154 154 if not conntype:
155 155 # :ext:user@host/home/user/path/to/cvsroot
156 156 if root.startswith(":ext:"):
157 157 root = root[5:]
158 158 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
159 159 # Do not take Windows path "c:\foo\bar" for a connection strings
160 160 if os.path.isdir(root) or not m:
161 161 conntype = "local"
162 162 else:
163 163 conntype = "rsh"
164 164 user, host, root = m.group(1), m.group(2), m.group(3)
165 165
166 166 if conntype != "pserver":
167 167 if conntype == "rsh":
168 168 rsh = os.environ.get("CVS_RSH") or "ssh"
169 169 if user:
170 170 cmd = [rsh, '-l', user, host] + cmd
171 171 else:
172 172 cmd = [rsh, host] + cmd
173 173
174 174 # popen2 does not support argument lists under Windows
175 175 cmd = [util.shellquote(arg) for arg in cmd]
176 176 cmd = util.quotecommand(' '.join(cmd))
177 177 self.writep, self.readp = util.popen2(cmd)
178 178
179 179 self.realroot = root
180 180
181 181 self.writep.write("Root %s\n" % root)
182 182 self.writep.write("Valid-responses ok error Valid-requests Mode"
183 183 " M Mbinary E Checked-in Created Updated"
184 184 " Merged Removed\n")
185 185 self.writep.write("valid-requests\n")
186 186 self.writep.flush()
187 187 r = self.readp.readline()
188 188 if not r.startswith("Valid-requests"):
189 189 raise util.Abort(_("unexpected response from CVS server "
190 190 "(expected \"Valid-requests\", but got %r)")
191 191 % r)
192 192 if "UseUnchanged" in r:
193 193 self.writep.write("UseUnchanged\n")
194 194 self.writep.flush()
195 195 r = self.readp.readline()
196 196
197 197 def getheads(self):
198 198 self._parse()
199 199 return self.heads
200 200
201 201 def _getfile(self, name, rev):
202 202
203 203 def chunkedread(fp, count):
204 204 # file-objects returned by socked.makefile() do not handle
205 205 # large read() requests very well.
206 206 chunksize = 65536
207 207 output = StringIO()
208 208 while count > 0:
209 209 data = fp.read(min(count, chunksize))
210 210 if not data:
211 211 raise util.Abort(_("%d bytes missing from remote file") % count)
212 212 count -= len(data)
213 213 output.write(data)
214 214 return output.getvalue()
215 215
216 216 if rev.endswith("(DEAD)"):
217 217 raise IOError
218 218
219 219 args = ("-N -P -kk -r %s --" % rev).split()
220 220 args.append(self.cvsrepo + '/' + name)
221 221 for x in args:
222 222 self.writep.write("Argument %s\n" % x)
223 223 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
224 224 self.writep.flush()
225 225
226 226 data = ""
227 227 while 1:
228 228 line = self.readp.readline()
229 229 if line.startswith("Created ") or line.startswith("Updated "):
230 230 self.readp.readline() # path
231 231 self.readp.readline() # entries
232 232 mode = self.readp.readline()[:-1]
233 233 count = int(self.readp.readline()[:-1])
234 234 data = chunkedread(self.readp, count)
235 235 elif line.startswith(" "):
236 236 data += line[1:]
237 237 elif line.startswith("M "):
238 238 pass
239 239 elif line.startswith("Mbinary "):
240 240 count = int(self.readp.readline()[:-1])
241 241 data = chunkedread(self.readp, count)
242 242 else:
243 243 if line == "ok\n":
244 244 return (data, "x" in mode and "x" or "")
245 245 elif line.startswith("E "):
246 246 self.ui.warn(_("cvs server: %s\n") % line[2:])
247 247 elif line.startswith("Remove"):
248 248 self.readp.readline()
249 249 else:
250 250 raise util.Abort(_("unknown CVS response: %s") % line)
251 251
252 252 def getfile(self, file, rev):
253 253 self._parse()
254 254 data, mode = self._getfile(file, rev)
255 255 self.modecache[(file, rev)] = mode
256 256 return data
257 257
258 258 def getmode(self, file, rev):
259 259 return self.modecache[(file, rev)]
260 260
261 261 def getchanges(self, rev):
262 262 self._parse()
263 263 self.modecache = {}
264 264 return sorted(self.files[rev].iteritems()), {}
265 265
266 266 def getcommit(self, rev):
267 267 self._parse()
268 268 return self.changeset[rev]
269 269
270 270 def gettags(self):
271 271 self._parse()
272 272 return self.tags
273 273
274 274 def getchangedfiles(self, rev, i):
275 275 self._parse()
276 276 return sorted(self.files[rev])
@@ -1,836 +1,835 b''
1 #
2 1 # Mercurial built-in replacement for cvsps.
3 2 #
4 3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
5 4 #
6 5 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
8 7
9 8 import os
10 9 import re
11 10 import cPickle as pickle
12 11 from mercurial import util
13 12 from mercurial.i18n import _
14 13 from mercurial import hook
15 14
16 15 class logentry(object):
17 16 '''Class logentry has the following attributes:
18 17 .author - author name as CVS knows it
19 18 .branch - name of branch this revision is on
20 19 .branches - revision tuple of branches starting at this revision
21 20 .comment - commit message
22 21 .date - the commit date as a (time, tz) tuple
23 22 .dead - true if file revision is dead
24 23 .file - Name of file
25 24 .lines - a tuple (+lines, -lines) or None
26 25 .parent - Previous revision of this entry
27 26 .rcs - name of file as returned from CVS
28 27 .revision - revision number as tuple
29 28 .tags - list of tags on the file
30 29 .synthetic - is this a synthetic "file ... added on ..." revision?
31 30 .mergepoint- the branch that has been merged from
32 31 (if present in rlog output)
33 32 .branchpoints- the branches that start at the current entry
34 33 '''
35 34 def __init__(self, **entries):
36 35 self.__dict__.update(entries)
37 36
38 37 def __repr__(self):
39 38 return "<%s at 0x%x: %s %s>" % (self.__class__.__name__,
40 39 id(self),
41 40 self.file,
42 41 ".".join(map(str, self.revision)))
43 42
44 43 class logerror(Exception):
45 44 pass
46 45
47 46 def getrepopath(cvspath):
48 47 """Return the repository path from a CVS path.
49 48
50 49 >>> getrepopath('/foo/bar')
51 50 '/foo/bar'
52 51 >>> getrepopath('c:/foo/bar')
53 52 'c:/foo/bar'
54 53 >>> getrepopath(':pserver:10/foo/bar')
55 54 '/foo/bar'
56 55 >>> getrepopath(':pserver:10c:/foo/bar')
57 56 '/foo/bar'
58 57 >>> getrepopath(':pserver:/foo/bar')
59 58 '/foo/bar'
60 59 >>> getrepopath(':pserver:c:/foo/bar')
61 60 'c:/foo/bar'
62 61 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
63 62 '/foo/bar'
64 63 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
65 64 'c:/foo/bar'
66 65 """
67 66 # According to CVS manual, CVS paths are expressed like:
68 67 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
69 68 #
70 69 # Unfortunately, Windows absolute paths start with a drive letter
71 70 # like 'c:' making it harder to parse. Here we assume that drive
72 71 # letters are only one character long and any CVS component before
73 72 # the repository path is at least 2 characters long, and use this
74 73 # to disambiguate.
75 74 parts = cvspath.split(':')
76 75 if len(parts) == 1:
77 76 return parts[0]
78 77 # Here there is an ambiguous case if we have a port number
79 78 # immediately followed by a Windows driver letter. We assume this
80 79 # never happens and decide it must be CVS path component,
81 80 # therefore ignoring it.
82 81 if len(parts[-2]) > 1:
83 82 return parts[-1].lstrip('0123456789')
84 83 return parts[-2] + ':' + parts[-1]
85 84
86 85 def createlog(ui, directory=None, root="", rlog=True, cache=None):
87 86 '''Collect the CVS rlog'''
88 87
89 88 # Because we store many duplicate commit log messages, reusing strings
90 89 # saves a lot of memory and pickle storage space.
91 90 _scache = {}
92 91 def scache(s):
93 92 "return a shared version of a string"
94 93 return _scache.setdefault(s, s)
95 94
96 95 ui.status(_('collecting CVS rlog\n'))
97 96
98 97 log = [] # list of logentry objects containing the CVS state
99 98
100 99 # patterns to match in CVS (r)log output, by state of use
101 100 re_00 = re.compile('RCS file: (.+)$')
102 101 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
103 102 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
104 103 re_03 = re.compile("(Cannot access.+CVSROOT)|"
105 104 "(can't create temporary directory.+)$")
106 105 re_10 = re.compile('Working file: (.+)$')
107 106 re_20 = re.compile('symbolic names:')
108 107 re_30 = re.compile('\t(.+): ([\\d.]+)$')
109 108 re_31 = re.compile('----------------------------$')
110 109 re_32 = re.compile('======================================='
111 110 '======================================$')
112 111 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
113 112 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
114 113 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
115 114 r'(.*mergepoint:\s+([^;]+);)?')
116 115 re_70 = re.compile('branches: (.+);$')
117 116
118 117 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
119 118
120 119 prefix = '' # leading path to strip of what we get from CVS
121 120
122 121 if directory is None:
123 122 # Current working directory
124 123
125 124 # Get the real directory in the repository
126 125 try:
127 126 prefix = open(os.path.join('CVS','Repository')).read().strip()
128 127 if prefix == ".":
129 128 prefix = ""
130 129 directory = prefix
131 130 except IOError:
132 131 raise logerror('Not a CVS sandbox')
133 132
134 133 if prefix and not prefix.endswith(os.sep):
135 134 prefix += os.sep
136 135
137 136 # Use the Root file in the sandbox, if it exists
138 137 try:
139 138 root = open(os.path.join('CVS','Root')).read().strip()
140 139 except IOError:
141 140 pass
142 141
143 142 if not root:
144 143 root = os.environ.get('CVSROOT', '')
145 144
146 145 # read log cache if one exists
147 146 oldlog = []
148 147 date = None
149 148
150 149 if cache:
151 150 cachedir = os.path.expanduser('~/.hg.cvsps')
152 151 if not os.path.exists(cachedir):
153 152 os.mkdir(cachedir)
154 153
155 154 # The cvsps cache pickle needs a uniquified name, based on the
156 155 # repository location. The address may have all sort of nasties
157 156 # in it, slashes, colons and such. So here we take just the
158 157 # alphanumerics, concatenated in a way that does not mix up the
159 158 # various components, so that
160 159 # :pserver:user@server:/path
161 160 # and
162 161 # /pserver/user/server/path
163 162 # are mapped to different cache file names.
164 163 cachefile = root.split(":") + [directory, "cache"]
165 164 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
166 165 cachefile = os.path.join(cachedir,
167 166 '.'.join([s for s in cachefile if s]))
168 167
169 168 if cache == 'update':
170 169 try:
171 170 ui.note(_('reading cvs log cache %s\n') % cachefile)
172 171 oldlog = pickle.load(open(cachefile))
173 172 ui.note(_('cache has %d log entries\n') % len(oldlog))
174 173 except Exception, e:
175 174 ui.note(_('error reading cache: %r\n') % e)
176 175
177 176 if oldlog:
178 177 date = oldlog[-1].date # last commit date as a (time,tz) tuple
179 178 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
180 179
181 180 # build the CVS commandline
182 181 cmd = ['cvs', '-q']
183 182 if root:
184 183 cmd.append('-d%s' % root)
185 184 p = util.normpath(getrepopath(root))
186 185 if not p.endswith('/'):
187 186 p += '/'
188 187 prefix = p + util.normpath(prefix)
189 188 cmd.append(['log', 'rlog'][rlog])
190 189 if date:
191 190 # no space between option and date string
192 191 cmd.append('-d>%s' % date)
193 192 cmd.append(directory)
194 193
195 194 # state machine begins here
196 195 tags = {} # dictionary of revisions on current file with their tags
197 196 branchmap = {} # mapping between branch names and revision numbers
198 197 state = 0
199 198 store = False # set when a new record can be appended
200 199
201 200 cmd = [util.shellquote(arg) for arg in cmd]
202 201 ui.note(_("running %s\n") % (' '.join(cmd)))
203 202 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
204 203
205 204 pfp = util.popen(' '.join(cmd))
206 205 peek = pfp.readline()
207 206 while True:
208 207 line = peek
209 208 if line == '':
210 209 break
211 210 peek = pfp.readline()
212 211 if line.endswith('\n'):
213 212 line = line[:-1]
214 213 #ui.debug('state=%d line=%r\n' % (state, line))
215 214
216 215 if state == 0:
217 216 # initial state, consume input until we see 'RCS file'
218 217 match = re_00.match(line)
219 218 if match:
220 219 rcs = match.group(1)
221 220 tags = {}
222 221 if rlog:
223 222 filename = util.normpath(rcs[:-2])
224 223 if filename.startswith(prefix):
225 224 filename = filename[len(prefix):]
226 225 if filename.startswith('/'):
227 226 filename = filename[1:]
228 227 if filename.startswith('Attic/'):
229 228 filename = filename[6:]
230 229 else:
231 230 filename = filename.replace('/Attic/', '/')
232 231 state = 2
233 232 continue
234 233 state = 1
235 234 continue
236 235 match = re_01.match(line)
237 236 if match:
238 237 raise Exception(match.group(1))
239 238 match = re_02.match(line)
240 239 if match:
241 240 raise Exception(match.group(2))
242 241 if re_03.match(line):
243 242 raise Exception(line)
244 243
245 244 elif state == 1:
246 245 # expect 'Working file' (only when using log instead of rlog)
247 246 match = re_10.match(line)
248 247 assert match, _('RCS file must be followed by working file')
249 248 filename = util.normpath(match.group(1))
250 249 state = 2
251 250
252 251 elif state == 2:
253 252 # expect 'symbolic names'
254 253 if re_20.match(line):
255 254 branchmap = {}
256 255 state = 3
257 256
258 257 elif state == 3:
259 258 # read the symbolic names and store as tags
260 259 match = re_30.match(line)
261 260 if match:
262 261 rev = [int(x) for x in match.group(2).split('.')]
263 262
264 263 # Convert magic branch number to an odd-numbered one
265 264 revn = len(rev)
266 265 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
267 266 rev = rev[:-2] + rev[-1:]
268 267 rev = tuple(rev)
269 268
270 269 if rev not in tags:
271 270 tags[rev] = []
272 271 tags[rev].append(match.group(1))
273 272 branchmap[match.group(1)] = match.group(2)
274 273
275 274 elif re_31.match(line):
276 275 state = 5
277 276 elif re_32.match(line):
278 277 state = 0
279 278
280 279 elif state == 4:
281 280 # expecting '------' separator before first revision
282 281 if re_31.match(line):
283 282 state = 5
284 283 else:
285 284 assert not re_32.match(line), _('must have at least '
286 285 'some revisions')
287 286
288 287 elif state == 5:
289 288 # expecting revision number and possibly (ignored) lock indication
290 289 # we create the logentry here from values stored in states 0 to 4,
291 290 # as this state is re-entered for subsequent revisions of a file.
292 291 match = re_50.match(line)
293 292 assert match, _('expected revision number')
294 293 e = logentry(rcs=scache(rcs), file=scache(filename),
295 294 revision=tuple([int(x) for x in match.group(1).split('.')]),
296 295 branches=[], parent=None,
297 296 synthetic=False)
298 297 state = 6
299 298
300 299 elif state == 6:
301 300 # expecting date, author, state, lines changed
302 301 match = re_60.match(line)
303 302 assert match, _('revision must be followed by date line')
304 303 d = match.group(1)
305 304 if d[2] == '/':
306 305 # Y2K
307 306 d = '19' + d
308 307
309 308 if len(d.split()) != 3:
310 309 # cvs log dates always in GMT
311 310 d = d + ' UTC'
312 311 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
313 312 '%Y/%m/%d %H:%M:%S',
314 313 '%Y-%m-%d %H:%M:%S'])
315 314 e.author = scache(match.group(2))
316 315 e.dead = match.group(3).lower() == 'dead'
317 316
318 317 if match.group(5):
319 318 if match.group(6):
320 319 e.lines = (int(match.group(5)), int(match.group(6)))
321 320 else:
322 321 e.lines = (int(match.group(5)), 0)
323 322 elif match.group(6):
324 323 e.lines = (0, int(match.group(6)))
325 324 else:
326 325 e.lines = None
327 326
328 327 if match.group(7): # cvsnt mergepoint
329 328 myrev = match.group(8).split('.')
330 329 if len(myrev) == 2: # head
331 330 e.mergepoint = 'HEAD'
332 331 else:
333 332 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
334 333 branches = [b for b in branchmap if branchmap[b] == myrev]
335 334 assert len(branches) == 1, 'unknown branch: %s' % e.mergepoint
336 335 e.mergepoint = branches[0]
337 336 else:
338 337 e.mergepoint = None
339 338 e.comment = []
340 339 state = 7
341 340
342 341 elif state == 7:
343 342 # read the revision numbers of branches that start at this revision
344 343 # or store the commit log message otherwise
345 344 m = re_70.match(line)
346 345 if m:
347 346 e.branches = [tuple([int(y) for y in x.strip().split('.')])
348 347 for x in m.group(1).split(';')]
349 348 state = 8
350 349 elif re_31.match(line) and re_50.match(peek):
351 350 state = 5
352 351 store = True
353 352 elif re_32.match(line):
354 353 state = 0
355 354 store = True
356 355 else:
357 356 e.comment.append(line)
358 357
359 358 elif state == 8:
360 359 # store commit log message
361 360 if re_31.match(line):
362 361 state = 5
363 362 store = True
364 363 elif re_32.match(line):
365 364 state = 0
366 365 store = True
367 366 else:
368 367 e.comment.append(line)
369 368
370 369 # When a file is added on a branch B1, CVS creates a synthetic
371 370 # dead trunk revision 1.1 so that the branch has a root.
372 371 # Likewise, if you merge such a file to a later branch B2 (one
373 372 # that already existed when the file was added on B1), CVS
374 373 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
375 374 # these revisions now, but mark them synthetic so
376 375 # createchangeset() can take care of them.
377 376 if (store and
378 377 e.dead and
379 378 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
380 379 len(e.comment) == 1 and
381 380 file_added_re.match(e.comment[0])):
382 381 ui.debug('found synthetic revision in %s: %r\n'
383 382 % (e.rcs, e.comment[0]))
384 383 e.synthetic = True
385 384
386 385 if store:
387 386 # clean up the results and save in the log.
388 387 store = False
389 388 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
390 389 e.comment = scache('\n'.join(e.comment))
391 390
392 391 revn = len(e.revision)
393 392 if revn > 3 and (revn % 2) == 0:
394 393 e.branch = tags.get(e.revision[:-1], [None])[0]
395 394 else:
396 395 e.branch = None
397 396
398 397 # find the branches starting from this revision
399 398 branchpoints = set()
400 399 for branch, revision in branchmap.iteritems():
401 400 revparts = tuple([int(i) for i in revision.split('.')])
402 401 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
403 402 # normal branch
404 403 if revparts[:-2] == e.revision:
405 404 branchpoints.add(branch)
406 405 elif revparts == (1,1,1): # vendor branch
407 406 if revparts in e.branches:
408 407 branchpoints.add(branch)
409 408 e.branchpoints = branchpoints
410 409
411 410 log.append(e)
412 411
413 412 if len(log) % 100 == 0:
414 413 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
415 414
416 415 log.sort(key=lambda x: (x.rcs, x.revision))
417 416
418 417 # find parent revisions of individual files
419 418 versions = {}
420 419 for e in log:
421 420 branch = e.revision[:-1]
422 421 p = versions.get((e.rcs, branch), None)
423 422 if p is None:
424 423 p = e.revision[:-2]
425 424 e.parent = p
426 425 versions[(e.rcs, branch)] = e.revision
427 426
428 427 # update the log cache
429 428 if cache:
430 429 if log:
431 430 # join up the old and new logs
432 431 log.sort(key=lambda x: x.date)
433 432
434 433 if oldlog and oldlog[-1].date >= log[0].date:
435 434 raise logerror('Log cache overlaps with new log entries,'
436 435 ' re-run without cache.')
437 436
438 437 log = oldlog + log
439 438
440 439 # write the new cachefile
441 440 ui.note(_('writing cvs log cache %s\n') % cachefile)
442 441 pickle.dump(log, open(cachefile, 'w'))
443 442 else:
444 443 log = oldlog
445 444
446 445 ui.status(_('%d log entries\n') % len(log))
447 446
448 447 hook.hook(ui, None, "cvslog", True, log=log)
449 448
450 449 return log
451 450
452 451
453 452 class changeset(object):
454 453 '''Class changeset has the following attributes:
455 454 .id - integer identifying this changeset (list index)
456 455 .author - author name as CVS knows it
457 456 .branch - name of branch this changeset is on, or None
458 457 .comment - commit message
459 458 .date - the commit date as a (time,tz) tuple
460 459 .entries - list of logentry objects in this changeset
461 460 .parents - list of one or two parent changesets
462 461 .tags - list of tags on this changeset
463 462 .synthetic - from synthetic revision "file ... added on branch ..."
464 463 .mergepoint- the branch that has been merged from
465 464 (if present in rlog output)
466 465 .branchpoints- the branches that start at the current entry
467 466 '''
468 467 def __init__(self, **entries):
469 468 self.__dict__.update(entries)
470 469
471 470 def __repr__(self):
472 471 return "<%s at 0x%x: %s>" % (self.__class__.__name__,
473 472 id(self),
474 473 getattr(self, 'id', "(no id)"))
475 474
476 475 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
477 476 '''Convert log into changesets.'''
478 477
479 478 ui.status(_('creating changesets\n'))
480 479
481 480 # Merge changesets
482 481
483 482 log.sort(key=lambda x: (x.comment, x.author, x.branch, x.date))
484 483
485 484 changesets = []
486 485 files = set()
487 486 c = None
488 487 for i, e in enumerate(log):
489 488
490 489 # Check if log entry belongs to the current changeset or not.
491 490
492 491 # Since CVS is file centric, two different file revisions with
493 492 # different branchpoints should be treated as belonging to two
494 493 # different changesets (and the ordering is important and not
495 494 # honoured by cvsps at this point).
496 495 #
497 496 # Consider the following case:
498 497 # foo 1.1 branchpoints: [MYBRANCH]
499 498 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
500 499 #
501 500 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
502 501 # later version of foo may be in MYBRANCH2, so foo should be the
503 502 # first changeset and bar the next and MYBRANCH and MYBRANCH2
504 503 # should both start off of the bar changeset. No provisions are
505 504 # made to ensure that this is, in fact, what happens.
506 505 if not (c and
507 506 e.comment == c.comment and
508 507 e.author == c.author and
509 508 e.branch == c.branch and
510 509 (not hasattr(e, 'branchpoints') or
511 510 not hasattr (c, 'branchpoints') or
512 511 e.branchpoints == c.branchpoints) and
513 512 ((c.date[0] + c.date[1]) <=
514 513 (e.date[0] + e.date[1]) <=
515 514 (c.date[0] + c.date[1]) + fuzz) and
516 515 e.file not in files):
517 516 c = changeset(comment=e.comment, author=e.author,
518 517 branch=e.branch, date=e.date, entries=[],
519 518 mergepoint=getattr(e, 'mergepoint', None),
520 519 branchpoints=getattr(e, 'branchpoints', set()))
521 520 changesets.append(c)
522 521 files = set()
523 522 if len(changesets) % 100 == 0:
524 523 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
525 524 ui.status(util.ellipsis(t, 80) + '\n')
526 525
527 526 c.entries.append(e)
528 527 files.add(e.file)
529 528 c.date = e.date # changeset date is date of latest commit in it
530 529
531 530 # Mark synthetic changesets
532 531
533 532 for c in changesets:
534 533 # Synthetic revisions always get their own changeset, because
535 534 # the log message includes the filename. E.g. if you add file3
536 535 # and file4 on a branch, you get four log entries and three
537 536 # changesets:
538 537 # "File file3 was added on branch ..." (synthetic, 1 entry)
539 538 # "File file4 was added on branch ..." (synthetic, 1 entry)
540 539 # "Add file3 and file4 to fix ..." (real, 2 entries)
541 540 # Hence the check for 1 entry here.
542 541 synth = getattr(c.entries[0], 'synthetic', None)
543 542 c.synthetic = (len(c.entries) == 1 and synth)
544 543
545 544 # Sort files in each changeset
546 545
547 546 for c in changesets:
548 547 def pathcompare(l, r):
549 548 'Mimic cvsps sorting order'
550 549 l = l.split('/')
551 550 r = r.split('/')
552 551 nl = len(l)
553 552 nr = len(r)
554 553 n = min(nl, nr)
555 554 for i in range(n):
556 555 if i + 1 == nl and nl < nr:
557 556 return -1
558 557 elif i + 1 == nr and nl > nr:
559 558 return +1
560 559 elif l[i] < r[i]:
561 560 return -1
562 561 elif l[i] > r[i]:
563 562 return +1
564 563 return 0
565 564 def entitycompare(l, r):
566 565 return pathcompare(l.file, r.file)
567 566
568 567 c.entries.sort(entitycompare)
569 568
570 569 # Sort changesets by date
571 570
572 571 def cscmp(l, r):
573 572 d = sum(l.date) - sum(r.date)
574 573 if d:
575 574 return d
576 575
577 576 # detect vendor branches and initial commits on a branch
578 577 le = {}
579 578 for e in l.entries:
580 579 le[e.rcs] = e.revision
581 580 re = {}
582 581 for e in r.entries:
583 582 re[e.rcs] = e.revision
584 583
585 584 d = 0
586 585 for e in l.entries:
587 586 if re.get(e.rcs, None) == e.parent:
588 587 assert not d
589 588 d = 1
590 589 break
591 590
592 591 for e in r.entries:
593 592 if le.get(e.rcs, None) == e.parent:
594 593 assert not d
595 594 d = -1
596 595 break
597 596
598 597 return d
599 598
600 599 changesets.sort(cscmp)
601 600
602 601 # Collect tags
603 602
604 603 globaltags = {}
605 604 for c in changesets:
606 605 for e in c.entries:
607 606 for tag in e.tags:
608 607 # remember which is the latest changeset to have this tag
609 608 globaltags[tag] = c
610 609
611 610 for c in changesets:
612 611 tags = set()
613 612 for e in c.entries:
614 613 tags.update(e.tags)
615 614 # remember tags only if this is the latest changeset to have it
616 615 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
617 616
618 617 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
619 618 # by inserting dummy changesets with two parents, and handle
620 619 # {{mergefrombranch BRANCHNAME}} by setting two parents.
621 620
622 621 if mergeto is None:
623 622 mergeto = r'{{mergetobranch ([-\w]+)}}'
624 623 if mergeto:
625 624 mergeto = re.compile(mergeto)
626 625
627 626 if mergefrom is None:
628 627 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
629 628 if mergefrom:
630 629 mergefrom = re.compile(mergefrom)
631 630
632 631 versions = {} # changeset index where we saw any particular file version
633 632 branches = {} # changeset index where we saw a branch
634 633 n = len(changesets)
635 634 i = 0
636 635 while i<n:
637 636 c = changesets[i]
638 637
639 638 for f in c.entries:
640 639 versions[(f.rcs, f.revision)] = i
641 640
642 641 p = None
643 642 if c.branch in branches:
644 643 p = branches[c.branch]
645 644 else:
646 645 # first changeset on a new branch
647 646 # the parent is a changeset with the branch in its
648 647 # branchpoints such that it is the latest possible
649 648 # commit without any intervening, unrelated commits.
650 649
651 650 for candidate in xrange(i):
652 651 if c.branch not in changesets[candidate].branchpoints:
653 652 if p is not None:
654 653 break
655 654 continue
656 655 p = candidate
657 656
658 657 c.parents = []
659 658 if p is not None:
660 659 p = changesets[p]
661 660
662 661 # Ensure no changeset has a synthetic changeset as a parent.
663 662 while p.synthetic:
664 663 assert len(p.parents) <= 1, \
665 664 _('synthetic changeset cannot have multiple parents')
666 665 if p.parents:
667 666 p = p.parents[0]
668 667 else:
669 668 p = None
670 669 break
671 670
672 671 if p is not None:
673 672 c.parents.append(p)
674 673
675 674 if c.mergepoint:
676 675 if c.mergepoint == 'HEAD':
677 676 c.mergepoint = None
678 677 c.parents.append(changesets[branches[c.mergepoint]])
679 678
680 679 if mergefrom:
681 680 m = mergefrom.search(c.comment)
682 681 if m:
683 682 m = m.group(1)
684 683 if m == 'HEAD':
685 684 m = None
686 685 try:
687 686 candidate = changesets[branches[m]]
688 687 except KeyError:
689 688 ui.warn(_("warning: CVS commit message references "
690 689 "non-existent branch %r:\n%s\n")
691 690 % (m, c.comment))
692 691 if m in branches and c.branch != m and not candidate.synthetic:
693 692 c.parents.append(candidate)
694 693
695 694 if mergeto:
696 695 m = mergeto.search(c.comment)
697 696 if m:
698 697 try:
699 698 m = m.group(1)
700 699 if m == 'HEAD':
701 700 m = None
702 701 except:
703 702 m = None # if no group found then merge to HEAD
704 703 if m in branches and c.branch != m:
705 704 # insert empty changeset for merge
706 705 cc = changeset(author=c.author, branch=m, date=c.date,
707 706 comment='convert-repo: CVS merge from branch %s' % c.branch,
708 707 entries=[], tags=[], parents=[changesets[branches[m]], c])
709 708 changesets.insert(i + 1, cc)
710 709 branches[m] = i + 1
711 710
712 711 # adjust our loop counters now we have inserted a new entry
713 712 n += 1
714 713 i += 2
715 714 continue
716 715
717 716 branches[c.branch] = i
718 717 i += 1
719 718
720 719 # Drop synthetic changesets (safe now that we have ensured no other
721 720 # changesets can have them as parents).
722 721 i = 0
723 722 while i < len(changesets):
724 723 if changesets[i].synthetic:
725 724 del changesets[i]
726 725 else:
727 726 i += 1
728 727
729 728 # Number changesets
730 729
731 730 for i, c in enumerate(changesets):
732 731 c.id = i + 1
733 732
734 733 ui.status(_('%d changeset entries\n') % len(changesets))
735 734
736 735 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
737 736
738 737 return changesets
739 738
740 739
741 740 def debugcvsps(ui, *args, **opts):
742 741 '''Read CVS rlog for current directory or named path in
743 742 repository, and convert the log to changesets based on matching
744 743 commit log entries and dates.
745 744 '''
746 745 if opts["new_cache"]:
747 746 cache = "write"
748 747 elif opts["update_cache"]:
749 748 cache = "update"
750 749 else:
751 750 cache = None
752 751
753 752 revisions = opts["revisions"]
754 753
755 754 try:
756 755 if args:
757 756 log = []
758 757 for d in args:
759 758 log += createlog(ui, d, root=opts["root"], cache=cache)
760 759 else:
761 760 log = createlog(ui, root=opts["root"], cache=cache)
762 761 except logerror, e:
763 762 ui.write("%r\n"%e)
764 763 return
765 764
766 765 changesets = createchangeset(ui, log, opts["fuzz"])
767 766 del log
768 767
769 768 # Print changesets (optionally filtered)
770 769
771 770 off = len(revisions)
772 771 branches = {} # latest version number in each branch
773 772 ancestors = {} # parent branch
774 773 for cs in changesets:
775 774
776 775 if opts["ancestors"]:
777 776 if cs.branch not in branches and cs.parents and cs.parents[0].id:
778 777 ancestors[cs.branch] = (changesets[cs.parents[0].id-1].branch,
779 778 cs.parents[0].id)
780 779 branches[cs.branch] = cs.id
781 780
782 781 # limit by branches
783 782 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
784 783 continue
785 784
786 785 if not off:
787 786 # Note: trailing spaces on several lines here are needed to have
788 787 # bug-for-bug compatibility with cvsps.
789 788 ui.write('---------------------\n')
790 789 ui.write('PatchSet %d \n' % cs.id)
791 790 ui.write('Date: %s\n' % util.datestr(cs.date,
792 791 '%Y/%m/%d %H:%M:%S %1%2'))
793 792 ui.write('Author: %s\n' % cs.author)
794 793 ui.write('Branch: %s\n' % (cs.branch or 'HEAD'))
795 794 ui.write('Tag%s: %s \n' % (['', 's'][len(cs.tags)>1],
796 795 ','.join(cs.tags) or '(none)'))
797 796 branchpoints = getattr(cs, 'branchpoints', None)
798 797 if branchpoints:
799 798 ui.write('Branchpoints: %s \n' % ', '.join(branchpoints))
800 799 if opts["parents"] and cs.parents:
801 800 if len(cs.parents)>1:
802 801 ui.write('Parents: %s\n' % (','.join([str(p.id) for p in cs.parents])))
803 802 else:
804 803 ui.write('Parent: %d\n' % cs.parents[0].id)
805 804
806 805 if opts["ancestors"]:
807 806 b = cs.branch
808 807 r = []
809 808 while b:
810 809 b, c = ancestors[b]
811 810 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
812 811 if r:
813 812 ui.write('Ancestors: %s\n' % (','.join(r)))
814 813
815 814 ui.write('Log:\n')
816 815 ui.write('%s\n\n' % cs.comment)
817 816 ui.write('Members: \n')
818 817 for f in cs.entries:
819 818 fn = f.file
820 819 if fn.startswith(opts["prefix"]):
821 820 fn = fn[len(opts["prefix"]):]
822 821 ui.write('\t%s:%s->%s%s \n' % (fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
823 822 '.'.join([str(x) for x in f.revision]), ['', '(DEAD)'][f.dead]))
824 823 ui.write('\n')
825 824
826 825 # have we seen the start tag?
827 826 if revisions and off:
828 827 if revisions[0] == str(cs.id) or \
829 828 revisions[0] in cs.tags:
830 829 off = False
831 830
832 831 # see if we reached the end tag
833 832 if len(revisions)>1 and not off:
834 833 if revisions[1] == str(cs.id) or \
835 834 revisions[1] in cs.tags:
836 835 break
@@ -1,163 +1,163 b''
1 1 # darcs.py - darcs support for the convert extension
2 2 #
3 3 # Copyright 2007-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 from common import NoRepo, checktool, commandline, commit, converter_source
9 9 from mercurial.i18n import _
10 10 from mercurial import util
11 11 import os, shutil, tempfile
12 12
13 13 # The naming drift of ElementTree is fun!
14 14
15 15 try: from xml.etree.cElementTree import ElementTree
16 16 except ImportError:
17 17 try: from xml.etree.ElementTree import ElementTree
18 18 except ImportError:
19 19 try: from elementtree.cElementTree import ElementTree
20 20 except ImportError:
21 21 try: from elementtree.ElementTree import ElementTree
22 22 except ImportError: ElementTree = None
23 23
24 24
25 25 class darcs_source(converter_source, commandline):
26 26 def __init__(self, ui, path, rev=None):
27 27 converter_source.__init__(self, ui, path, rev=rev)
28 28 commandline.__init__(self, ui, 'darcs')
29 29
30 30 # check for _darcs, ElementTree, _darcs/inventory so that we can
31 31 # easily skip test-convert-darcs if ElementTree is not around
32 32 if not os.path.exists(os.path.join(path, '_darcs', 'inventories')):
33 33 raise NoRepo("%s does not look like a darcs repo" % path)
34 34
35 35 if not os.path.exists(os.path.join(path, '_darcs')):
36 36 raise NoRepo("%s does not look like a darcs repo" % path)
37 37
38 38 checktool('darcs')
39 39 version = self.run0('--version').splitlines()[0].strip()
40 40 if version < '2.1':
41 41 raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') %
42 42 version)
43 43
44 44 if ElementTree is None:
45 45 raise util.Abort(_("Python ElementTree module is not available"))
46 46
47 47 self.path = os.path.realpath(path)
48 48
49 49 self.lastrev = None
50 50 self.changes = {}
51 51 self.parents = {}
52 52 self.tags = {}
53 53
54 54 def before(self):
55 55 self.tmppath = tempfile.mkdtemp(
56 56 prefix='convert-' + os.path.basename(self.path) + '-')
57 57 output, status = self.run('init', repodir=self.tmppath)
58 58 self.checkexit(status)
59 59
60 60 tree = self.xml('changes', xml_output=True, summary=True,
61 61 repodir=self.path)
62 62 tagname = None
63 63 child = None
64 64 for elt in tree.findall('patch'):
65 65 node = elt.get('hash')
66 66 name = elt.findtext('name', '')
67 67 if name.startswith('TAG '):
68 68 tagname = name[4:].strip()
69 69 elif tagname is not None:
70 70 self.tags[tagname] = node
71 71 tagname = None
72 72 self.changes[node] = elt
73 73 self.parents[child] = [node]
74 74 child = node
75 75 self.parents[child] = []
76 76
77 77 def after(self):
78 78 self.ui.debug('cleaning up %s\n' % self.tmppath)
79 79 shutil.rmtree(self.tmppath, ignore_errors=True)
80 80
81 81 def xml(self, cmd, **kwargs):
82 82 etree = ElementTree()
83 83 fp = self._run(cmd, **kwargs)
84 84 etree.parse(fp)
85 85 self.checkexit(fp.close())
86 86 return etree.getroot()
87 87
88 88 def manifest(self):
89 89 man = []
90 90 output, status = self.run('show', 'files', no_directories=True,
91 91 repodir=self.tmppath)
92 92 self.checkexit(status)
93 93 for line in output.split('\n'):
94 94 path = line[2:]
95 95 if path:
96 96 man.append(path)
97 97 return man
98 98
99 99 def getheads(self):
100 100 return self.parents[None]
101 101
102 102 def getcommit(self, rev):
103 103 elt = self.changes[rev]
104 104 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
105 105 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
106 106 return commit(author=elt.get('author'), date=util.datestr(date),
107 107 desc=desc.strip(), parents=self.parents[rev])
108 108
109 109 def pull(self, rev):
110 110 output, status = self.run('pull', self.path, all=True,
111 111 match='hash %s' % rev,
112 112 no_test=True, no_posthook=True,
113 113 external_merge='/bin/false',
114 114 repodir=self.tmppath)
115 115 if status:
116 116 if output.find('We have conflicts in') == -1:
117 117 self.checkexit(status, output)
118 118 output, status = self.run('revert', all=True, repodir=self.tmppath)
119 119 self.checkexit(status, output)
120 120
121 121 def getchanges(self, rev):
122 122 copies = {}
123 123 changes = []
124 124 man = None
125 125 for elt in self.changes[rev].find('summary').getchildren():
126 126 if elt.tag in ('add_directory', 'remove_directory'):
127 127 continue
128 128 if elt.tag == 'move':
129 129 if man is None:
130 130 man = self.manifest()
131 131 source, dest = elt.get('from'), elt.get('to')
132 132 if source in man:
133 133 # File move
134 134 changes.append((source, rev))
135 135 changes.append((dest, rev))
136 136 copies[dest] = source
137 137 else:
138 138 # Directory move, deduce file moves from manifest
139 139 source = source + '/'
140 140 for f in man:
141 141 if not f.startswith(source):
142 142 continue
143 143 fdest = dest + '/' + f[len(source):]
144 144 changes.append((f, rev))
145 145 changes.append((fdest, rev))
146 146 copies[fdest] = f
147 147 else:
148 148 changes.append((elt.text.strip(), rev))
149 149 self.pull(rev)
150 150 self.lastrev = rev
151 151 return sorted(changes), copies
152 152
153 153 def getfile(self, name, rev):
154 154 if rev != self.lastrev:
155 155 raise util.Abort(_('internal calling inconsistency'))
156 156 return open(os.path.join(self.tmppath, name), 'rb').read()
157 157
158 158 def getmode(self, name, rev):
159 159 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
160 160 return (mode & 0111) and 'x' or ''
161 161
162 162 def gettags(self):
163 163 return self.tags
@@ -1,359 +1,359 b''
1 1 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
2 2 # Copyright 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2, incorporated herein by reference.
5 # GNU General Public License version 2 or any later version.
6 6
7 7 import shlex
8 8 from mercurial.i18n import _
9 9 from mercurial import util
10 10 from common import SKIPREV, converter_source
11 11
12 12 def rpairs(name):
13 13 e = len(name)
14 14 while e != -1:
15 15 yield name[:e], name[e+1:]
16 16 e = name.rfind('/', 0, e)
17 17 yield '.', name
18 18
19 19 class filemapper(object):
20 20 '''Map and filter filenames when importing.
21 21 A name can be mapped to itself, a new name, or None (omit from new
22 22 repository).'''
23 23
24 24 def __init__(self, ui, path=None):
25 25 self.ui = ui
26 26 self.include = {}
27 27 self.exclude = {}
28 28 self.rename = {}
29 29 if path:
30 30 if self.parse(path):
31 31 raise util.Abort(_('errors in filemap'))
32 32
33 33 def parse(self, path):
34 34 errs = 0
35 35 def check(name, mapping, listname):
36 36 if name in mapping:
37 37 self.ui.warn(_('%s:%d: %r already in %s list\n') %
38 38 (lex.infile, lex.lineno, name, listname))
39 39 return 1
40 40 return 0
41 41 lex = shlex.shlex(open(path), path, True)
42 42 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
43 43 cmd = lex.get_token()
44 44 while cmd:
45 45 if cmd == 'include':
46 46 name = lex.get_token()
47 47 errs += check(name, self.exclude, 'exclude')
48 48 self.include[name] = name
49 49 elif cmd == 'exclude':
50 50 name = lex.get_token()
51 51 errs += check(name, self.include, 'include')
52 52 errs += check(name, self.rename, 'rename')
53 53 self.exclude[name] = name
54 54 elif cmd == 'rename':
55 55 src = lex.get_token()
56 56 dest = lex.get_token()
57 57 errs += check(src, self.exclude, 'exclude')
58 58 self.rename[src] = dest
59 59 elif cmd == 'source':
60 60 errs += self.parse(lex.get_token())
61 61 else:
62 62 self.ui.warn(_('%s:%d: unknown directive %r\n') %
63 63 (lex.infile, lex.lineno, cmd))
64 64 errs += 1
65 65 cmd = lex.get_token()
66 66 return errs
67 67
68 68 def lookup(self, name, mapping):
69 69 for pre, suf in rpairs(name):
70 70 try:
71 71 return mapping[pre], pre, suf
72 72 except KeyError:
73 73 pass
74 74 return '', name, ''
75 75
76 76 def __call__(self, name):
77 77 if self.include:
78 78 inc = self.lookup(name, self.include)[0]
79 79 else:
80 80 inc = name
81 81 if self.exclude:
82 82 exc = self.lookup(name, self.exclude)[0]
83 83 else:
84 84 exc = ''
85 85 if (not self.include and exc) or (len(inc) <= len(exc)):
86 86 return None
87 87 newpre, pre, suf = self.lookup(name, self.rename)
88 88 if newpre:
89 89 if newpre == '.':
90 90 return suf
91 91 if suf:
92 92 return newpre + '/' + suf
93 93 return newpre
94 94 return name
95 95
96 96 def active(self):
97 97 return bool(self.include or self.exclude or self.rename)
98 98
99 99 # This class does two additional things compared to a regular source:
100 100 #
101 101 # - Filter and rename files. This is mostly wrapped by the filemapper
102 102 # class above. We hide the original filename in the revision that is
103 103 # returned by getchanges to be able to find things later in getfile
104 104 # and getmode.
105 105 #
106 106 # - Return only revisions that matter for the files we're interested in.
107 107 # This involves rewriting the parents of the original revision to
108 108 # create a graph that is restricted to those revisions.
109 109 #
110 110 # This set of revisions includes not only revisions that directly
111 111 # touch files we're interested in, but also merges that merge two
112 112 # or more interesting revisions.
113 113
114 114 class filemap_source(converter_source):
115 115 def __init__(self, ui, baseconverter, filemap):
116 116 super(filemap_source, self).__init__(ui)
117 117 self.base = baseconverter
118 118 self.filemapper = filemapper(ui, filemap)
119 119 self.commits = {}
120 120 # if a revision rev has parent p in the original revision graph, then
121 121 # rev will have parent self.parentmap[p] in the restricted graph.
122 122 self.parentmap = {}
123 123 # self.wantedancestors[rev] is the set of all ancestors of rev that
124 124 # are in the restricted graph.
125 125 self.wantedancestors = {}
126 126 self.convertedorder = None
127 127 self._rebuilt = False
128 128 self.origparents = {}
129 129 self.children = {}
130 130 self.seenchildren = {}
131 131
132 132 def before(self):
133 133 self.base.before()
134 134
135 135 def after(self):
136 136 self.base.after()
137 137
138 138 def setrevmap(self, revmap):
139 139 # rebuild our state to make things restartable
140 140 #
141 141 # To avoid calling getcommit for every revision that has already
142 142 # been converted, we rebuild only the parentmap, delaying the
143 143 # rebuild of wantedancestors until we need it (i.e. until a
144 144 # merge).
145 145 #
146 146 # We assume the order argument lists the revisions in
147 147 # topological order, so that we can infer which revisions were
148 148 # wanted by previous runs.
149 149 self._rebuilt = not revmap
150 150 seen = {SKIPREV: SKIPREV}
151 151 dummyset = set()
152 152 converted = []
153 153 for rev in revmap.order:
154 154 mapped = revmap[rev]
155 155 wanted = mapped not in seen
156 156 if wanted:
157 157 seen[mapped] = rev
158 158 self.parentmap[rev] = rev
159 159 else:
160 160 self.parentmap[rev] = seen[mapped]
161 161 self.wantedancestors[rev] = dummyset
162 162 arg = seen[mapped]
163 163 if arg == SKIPREV:
164 164 arg = None
165 165 converted.append((rev, wanted, arg))
166 166 self.convertedorder = converted
167 167 return self.base.setrevmap(revmap)
168 168
169 169 def rebuild(self):
170 170 if self._rebuilt:
171 171 return True
172 172 self._rebuilt = True
173 173 self.parentmap.clear()
174 174 self.wantedancestors.clear()
175 175 self.seenchildren.clear()
176 176 for rev, wanted, arg in self.convertedorder:
177 177 if rev not in self.origparents:
178 178 self.origparents[rev] = self.getcommit(rev).parents
179 179 if arg is not None:
180 180 self.children[arg] = self.children.get(arg, 0) + 1
181 181
182 182 for rev, wanted, arg in self.convertedorder:
183 183 parents = self.origparents[rev]
184 184 if wanted:
185 185 self.mark_wanted(rev, parents)
186 186 else:
187 187 self.mark_not_wanted(rev, arg)
188 188 self._discard(arg, *parents)
189 189
190 190 return True
191 191
192 192 def getheads(self):
193 193 return self.base.getheads()
194 194
195 195 def getcommit(self, rev):
196 196 # We want to save a reference to the commit objects to be able
197 197 # to rewrite their parents later on.
198 198 c = self.commits[rev] = self.base.getcommit(rev)
199 199 for p in c.parents:
200 200 self.children[p] = self.children.get(p, 0) + 1
201 201 return c
202 202
203 203 def _discard(self, *revs):
204 204 for r in revs:
205 205 if r is None:
206 206 continue
207 207 self.seenchildren[r] = self.seenchildren.get(r, 0) + 1
208 208 if self.seenchildren[r] == self.children[r]:
209 209 del self.wantedancestors[r]
210 210 del self.parentmap[r]
211 211 del self.seenchildren[r]
212 212 if self._rebuilt:
213 213 del self.children[r]
214 214
215 215 def wanted(self, rev, i):
216 216 # Return True if we're directly interested in rev.
217 217 #
218 218 # i is an index selecting one of the parents of rev (if rev
219 219 # has no parents, i is None). getchangedfiles will give us
220 220 # the list of files that are different in rev and in the parent
221 221 # indicated by i. If we're interested in any of these files,
222 222 # we're interested in rev.
223 223 try:
224 224 files = self.base.getchangedfiles(rev, i)
225 225 except NotImplementedError:
226 226 raise util.Abort(_("source repository doesn't support --filemap"))
227 227 for f in files:
228 228 if self.filemapper(f):
229 229 return True
230 230 return False
231 231
232 232 def mark_not_wanted(self, rev, p):
233 233 # Mark rev as not interesting and update data structures.
234 234
235 235 if p is None:
236 236 # A root revision. Use SKIPREV to indicate that it doesn't
237 237 # map to any revision in the restricted graph. Put SKIPREV
238 238 # in the set of wanted ancestors to simplify code elsewhere
239 239 self.parentmap[rev] = SKIPREV
240 240 self.wantedancestors[rev] = set((SKIPREV,))
241 241 return
242 242
243 243 # Reuse the data from our parent.
244 244 self.parentmap[rev] = self.parentmap[p]
245 245 self.wantedancestors[rev] = self.wantedancestors[p]
246 246
247 247 def mark_wanted(self, rev, parents):
248 248 # Mark rev ss wanted and update data structures.
249 249
250 250 # rev will be in the restricted graph, so children of rev in
251 251 # the original graph should still have rev as a parent in the
252 252 # restricted graph.
253 253 self.parentmap[rev] = rev
254 254
255 255 # The set of wanted ancestors of rev is the union of the sets
256 256 # of wanted ancestors of its parents. Plus rev itself.
257 257 wrev = set()
258 258 for p in parents:
259 259 wrev.update(self.wantedancestors[p])
260 260 wrev.add(rev)
261 261 self.wantedancestors[rev] = wrev
262 262
263 263 def getchanges(self, rev):
264 264 parents = self.commits[rev].parents
265 265 if len(parents) > 1:
266 266 self.rebuild()
267 267
268 268 # To decide whether we're interested in rev we:
269 269 #
270 270 # - calculate what parents rev will have if it turns out we're
271 271 # interested in it. If it's going to have more than 1 parent,
272 272 # we're interested in it.
273 273 #
274 274 # - otherwise, we'll compare it with the single parent we found.
275 275 # If any of the files we're interested in is different in the
276 276 # the two revisions, we're interested in rev.
277 277
278 278 # A parent p is interesting if its mapped version (self.parentmap[p]):
279 279 # - is not SKIPREV
280 280 # - is still not in the list of parents (we don't want duplicates)
281 281 # - is not an ancestor of the mapped versions of the other parents
282 282 mparents = []
283 283 wp = None
284 284 for i, p1 in enumerate(parents):
285 285 mp1 = self.parentmap[p1]
286 286 if mp1 == SKIPREV or mp1 in mparents:
287 287 continue
288 288 for p2 in parents:
289 289 if p1 == p2 or mp1 == self.parentmap[p2]:
290 290 continue
291 291 if mp1 in self.wantedancestors[p2]:
292 292 break
293 293 else:
294 294 mparents.append(mp1)
295 295 wp = i
296 296
297 297 if wp is None and parents:
298 298 wp = 0
299 299
300 300 self.origparents[rev] = parents
301 301
302 302 if len(mparents) < 2 and not self.wanted(rev, wp):
303 303 # We don't want this revision.
304 304 # Update our state and tell the convert process to map this
305 305 # revision to the same revision its parent as mapped to.
306 306 p = None
307 307 if parents:
308 308 p = parents[wp]
309 309 self.mark_not_wanted(rev, p)
310 310 self.convertedorder.append((rev, False, p))
311 311 self._discard(*parents)
312 312 return self.parentmap[rev]
313 313
314 314 # We want this revision.
315 315 # Rewrite the parents of the commit object
316 316 self.commits[rev].parents = mparents
317 317 self.mark_wanted(rev, parents)
318 318 self.convertedorder.append((rev, True, None))
319 319 self._discard(*parents)
320 320
321 321 # Get the real changes and do the filtering/mapping.
322 322 # To be able to get the files later on in getfile and getmode,
323 323 # we hide the original filename in the rev part of the return
324 324 # value.
325 325 changes, copies = self.base.getchanges(rev)
326 326 newnames = {}
327 327 files = []
328 328 for f, r in changes:
329 329 newf = self.filemapper(f)
330 330 if newf:
331 331 files.append((newf, (f, r)))
332 332 newnames[f] = newf
333 333
334 334 ncopies = {}
335 335 for c in copies:
336 336 newc = self.filemapper(c)
337 337 if newc:
338 338 newsource = self.filemapper(copies[c])
339 339 if newsource:
340 340 ncopies[newc] = newsource
341 341
342 342 return files, ncopies
343 343
344 344 def getfile(self, name, rev):
345 345 realname, realrev = rev
346 346 return self.base.getfile(realname, realrev)
347 347
348 348 def getmode(self, name, rev):
349 349 realname, realrev = rev
350 350 return self.base.getmode(realname, realrev)
351 351
352 352 def gettags(self):
353 353 return self.base.gettags()
354 354
355 355 def hasnativeorder(self):
356 356 return self.base.hasnativeorder()
357 357
358 358 def lookuprev(self, rev):
359 359 return self.base.lookuprev(rev)
@@ -1,152 +1,152 b''
1 1 # git.py - git support for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 import os
9 9 from mercurial import util
10 10
11 11 from common import NoRepo, commit, converter_source, checktool
12 12
13 13 class convert_git(converter_source):
14 14 # Windows does not support GIT_DIR= construct while other systems
15 15 # cannot remove environment variable. Just assume none have
16 16 # both issues.
17 17 if hasattr(os, 'unsetenv'):
18 18 def gitcmd(self, s):
19 19 prevgitdir = os.environ.get('GIT_DIR')
20 20 os.environ['GIT_DIR'] = self.path
21 21 try:
22 22 return util.popen(s, 'rb')
23 23 finally:
24 24 if prevgitdir is None:
25 25 del os.environ['GIT_DIR']
26 26 else:
27 27 os.environ['GIT_DIR'] = prevgitdir
28 28 else:
29 29 def gitcmd(self, s):
30 30 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
31 31
32 32 def __init__(self, ui, path, rev=None):
33 33 super(convert_git, self).__init__(ui, path, rev=rev)
34 34
35 35 if os.path.isdir(path + "/.git"):
36 36 path += "/.git"
37 37 if not os.path.exists(path + "/objects"):
38 38 raise NoRepo("%s does not look like a Git repo" % path)
39 39
40 40 checktool('git', 'git')
41 41
42 42 self.path = path
43 43
44 44 def getheads(self):
45 45 if not self.rev:
46 46 return self.gitcmd('git rev-parse --branches --remotes').read().splitlines()
47 47 else:
48 48 fh = self.gitcmd("git rev-parse --verify %s" % self.rev)
49 49 return [fh.read()[:-1]]
50 50
51 51 def catfile(self, rev, type):
52 52 if rev == "0" * 40: raise IOError()
53 53 fh = self.gitcmd("git cat-file %s %s" % (type, rev))
54 54 return fh.read()
55 55
56 56 def getfile(self, name, rev):
57 57 return self.catfile(rev, "blob")
58 58
59 59 def getmode(self, name, rev):
60 60 return self.modecache[(name, rev)]
61 61
62 62 def getchanges(self, version):
63 63 self.modecache = {}
64 64 fh = self.gitcmd("git diff-tree -z --root -m -r %s" % version)
65 65 changes = []
66 66 seen = set()
67 67 entry = None
68 68 for l in fh.read().split('\x00'):
69 69 if not entry:
70 70 if not l.startswith(':'):
71 71 continue
72 72 entry = l
73 73 continue
74 74 f = l
75 75 if f not in seen:
76 76 seen.add(f)
77 77 entry = entry.split()
78 78 h = entry[3]
79 79 p = (entry[1] == "100755")
80 80 s = (entry[1] == "120000")
81 81 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
82 82 changes.append((f, h))
83 83 entry = None
84 84 return (changes, {})
85 85
86 86 def getcommit(self, version):
87 87 c = self.catfile(version, "commit") # read the commit hash
88 88 end = c.find("\n\n")
89 89 message = c[end+2:]
90 90 message = self.recode(message)
91 91 l = c[:end].splitlines()
92 92 parents = []
93 93 author = committer = None
94 94 for e in l[1:]:
95 95 n, v = e.split(" ", 1)
96 96 if n == "author":
97 97 p = v.split()
98 98 tm, tz = p[-2:]
99 99 author = " ".join(p[:-2])
100 100 if author[0] == "<": author = author[1:-1]
101 101 author = self.recode(author)
102 102 if n == "committer":
103 103 p = v.split()
104 104 tm, tz = p[-2:]
105 105 committer = " ".join(p[:-2])
106 106 if committer[0] == "<": committer = committer[1:-1]
107 107 committer = self.recode(committer)
108 108 if n == "parent": parents.append(v)
109 109
110 110 if committer and committer != author:
111 111 message += "\ncommitter: %s\n" % committer
112 112 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
113 113 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
114 114 date = tm + " " + str(tz)
115 115
116 116 c = commit(parents=parents, date=date, author=author, desc=message,
117 117 rev=version)
118 118 return c
119 119
120 120 def gettags(self):
121 121 tags = {}
122 122 fh = self.gitcmd('git ls-remote --tags "%s"' % self.path)
123 123 prefix = 'refs/tags/'
124 124 for line in fh:
125 125 line = line.strip()
126 126 if not line.endswith("^{}"):
127 127 continue
128 128 node, tag = line.split(None, 1)
129 129 if not tag.startswith(prefix):
130 130 continue
131 131 tag = tag[len(prefix):-3]
132 132 tags[tag] = node
133 133
134 134 return tags
135 135
136 136 def getchangedfiles(self, version, i):
137 137 changes = []
138 138 if i is None:
139 139 fh = self.gitcmd("git diff-tree --root -m -r %s" % version)
140 140 for l in fh:
141 141 if "\t" not in l:
142 142 continue
143 143 m, f = l[:-1].split("\t")
144 144 changes.append(f)
145 145 fh.close()
146 146 else:
147 147 fh = self.gitcmd('git diff-tree --name-only --root -r %s "%s^%s" --'
148 148 % (version, version, i+1))
149 149 changes = [f.rstrip('\n') for f in fh]
150 150 fh.close()
151 151
152 152 return changes
@@ -1,342 +1,342 b''
1 1 # gnuarch.py - GNU Arch support for the convert extension
2 2 #
3 3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
4 4 # and others
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2 or any later version.
8 8
9 9 from common import NoRepo, commandline, commit, converter_source
10 10 from mercurial.i18n import _
11 11 from mercurial import util
12 12 import os, shutil, tempfile, stat, locale
13 13 from email.Parser import Parser
14 14
15 15 class gnuarch_source(converter_source, commandline):
16 16
17 17 class gnuarch_rev(object):
18 18 def __init__(self, rev):
19 19 self.rev = rev
20 20 self.summary = ''
21 21 self.date = None
22 22 self.author = ''
23 23 self.continuationof = None
24 24 self.add_files = []
25 25 self.mod_files = []
26 26 self.del_files = []
27 27 self.ren_files = {}
28 28 self.ren_dirs = {}
29 29
30 30 def __init__(self, ui, path, rev=None):
31 31 super(gnuarch_source, self).__init__(ui, path, rev=rev)
32 32
33 33 if not os.path.exists(os.path.join(path, '{arch}')):
34 34 raise NoRepo(_("%s does not look like a GNU Arch repo") % path)
35 35
36 36 # Could use checktool, but we want to check for baz or tla.
37 37 self.execmd = None
38 38 if util.find_exe('baz'):
39 39 self.execmd = 'baz'
40 40 else:
41 41 if util.find_exe('tla'):
42 42 self.execmd = 'tla'
43 43 else:
44 44 raise util.Abort(_('cannot find a GNU Arch tool'))
45 45
46 46 commandline.__init__(self, ui, self.execmd)
47 47
48 48 self.path = os.path.realpath(path)
49 49 self.tmppath = None
50 50
51 51 self.treeversion = None
52 52 self.lastrev = None
53 53 self.changes = {}
54 54 self.parents = {}
55 55 self.tags = {}
56 56 self.modecache = {}
57 57 self.catlogparser = Parser()
58 58 self.locale = locale.getpreferredencoding()
59 59 self.archives = []
60 60
61 61 def before(self):
62 62 # Get registered archives
63 63 self.archives = [i.rstrip('\n')
64 64 for i in self.runlines0('archives', '-n')]
65 65
66 66 if self.execmd == 'tla':
67 67 output = self.run0('tree-version', self.path)
68 68 else:
69 69 output = self.run0('tree-version', '-d', self.path)
70 70 self.treeversion = output.strip()
71 71
72 72 # Get name of temporary directory
73 73 version = self.treeversion.split('/')
74 74 self.tmppath = os.path.join(tempfile.gettempdir(),
75 75 'hg-%s' % version[1])
76 76
77 77 # Generate parents dictionary
78 78 self.parents[None] = []
79 79 treeversion = self.treeversion
80 80 child = None
81 81 while treeversion:
82 82 self.ui.status(_('analyzing tree version %s...\n') % treeversion)
83 83
84 84 archive = treeversion.split('/')[0]
85 85 if archive not in self.archives:
86 86 self.ui.status(_('tree analysis stopped because it points to '
87 87 'an unregistered archive %s...\n') % archive)
88 88 break
89 89
90 90 # Get the complete list of revisions for that tree version
91 91 output, status = self.runlines('revisions', '-r', '-f', treeversion)
92 92 self.checkexit(status, 'failed retrieveing revisions for %s' % treeversion)
93 93
94 94 # No new iteration unless a revision has a continuation-of header
95 95 treeversion = None
96 96
97 97 for l in output:
98 98 rev = l.strip()
99 99 self.changes[rev] = self.gnuarch_rev(rev)
100 100 self.parents[rev] = []
101 101
102 102 # Read author, date and summary
103 103 catlog, status = self.run('cat-log', '-d', self.path, rev)
104 104 if status:
105 105 catlog = self.run0('cat-archive-log', rev)
106 106 self._parsecatlog(catlog, rev)
107 107
108 108 # Populate the parents map
109 109 self.parents[child].append(rev)
110 110
111 111 # Keep track of the current revision as the child of the next
112 112 # revision scanned
113 113 child = rev
114 114
115 115 # Check if we have to follow the usual incremental history
116 116 # or if we have to 'jump' to a different treeversion given
117 117 # by the continuation-of header.
118 118 if self.changes[rev].continuationof:
119 119 treeversion = '--'.join(self.changes[rev].continuationof.split('--')[:-1])
120 120 break
121 121
122 122 # If we reached a base-0 revision w/o any continuation-of
123 123 # header, it means the tree history ends here.
124 124 if rev[-6:] == 'base-0':
125 125 break
126 126
127 127 def after(self):
128 128 self.ui.debug('cleaning up %s\n' % self.tmppath)
129 129 shutil.rmtree(self.tmppath, ignore_errors=True)
130 130
131 131 def getheads(self):
132 132 return self.parents[None]
133 133
134 134 def getfile(self, name, rev):
135 135 if rev != self.lastrev:
136 136 raise util.Abort(_('internal calling inconsistency'))
137 137
138 138 # Raise IOError if necessary (i.e. deleted files).
139 139 if not os.path.exists(os.path.join(self.tmppath, name)):
140 140 raise IOError
141 141
142 142 data, mode = self._getfile(name, rev)
143 143 self.modecache[(name, rev)] = mode
144 144
145 145 return data
146 146
147 147 def getmode(self, name, rev):
148 148 return self.modecache[(name, rev)]
149 149
150 150 def getchanges(self, rev):
151 151 self.modecache = {}
152 152 self._update(rev)
153 153 changes = []
154 154 copies = {}
155 155
156 156 for f in self.changes[rev].add_files:
157 157 changes.append((f, rev))
158 158
159 159 for f in self.changes[rev].mod_files:
160 160 changes.append((f, rev))
161 161
162 162 for f in self.changes[rev].del_files:
163 163 changes.append((f, rev))
164 164
165 165 for src in self.changes[rev].ren_files:
166 166 to = self.changes[rev].ren_files[src]
167 167 changes.append((src, rev))
168 168 changes.append((to, rev))
169 169 copies[to] = src
170 170
171 171 for src in self.changes[rev].ren_dirs:
172 172 to = self.changes[rev].ren_dirs[src]
173 173 chgs, cps = self._rendirchanges(src, to);
174 174 changes += [(f, rev) for f in chgs]
175 175 copies.update(cps)
176 176
177 177 self.lastrev = rev
178 178 return sorted(set(changes)), copies
179 179
180 180 def getcommit(self, rev):
181 181 changes = self.changes[rev]
182 182 return commit(author=changes.author, date=changes.date,
183 183 desc=changes.summary, parents=self.parents[rev], rev=rev)
184 184
185 185 def gettags(self):
186 186 return self.tags
187 187
188 188 def _execute(self, cmd, *args, **kwargs):
189 189 cmdline = [self.execmd, cmd]
190 190 cmdline += args
191 191 cmdline = [util.shellquote(arg) for arg in cmdline]
192 192 cmdline += ['>', util.nulldev, '2>', util.nulldev]
193 193 cmdline = util.quotecommand(' '.join(cmdline))
194 194 self.ui.debug(cmdline, '\n')
195 195 return os.system(cmdline)
196 196
197 197 def _update(self, rev):
198 198 self.ui.debug('applying revision %s...\n' % rev)
199 199 changeset, status = self.runlines('replay', '-d', self.tmppath,
200 200 rev)
201 201 if status:
202 202 # Something went wrong while merging (baz or tla
203 203 # issue?), get latest revision and try from there
204 204 shutil.rmtree(self.tmppath, ignore_errors=True)
205 205 self._obtainrevision(rev)
206 206 else:
207 207 old_rev = self.parents[rev][0]
208 208 self.ui.debug('computing changeset between %s and %s...\n'
209 209 % (old_rev, rev))
210 210 self._parsechangeset(changeset, rev)
211 211
212 212 def _getfile(self, name, rev):
213 213 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
214 214 if stat.S_ISLNK(mode):
215 215 data = os.readlink(os.path.join(self.tmppath, name))
216 216 mode = mode and 'l' or ''
217 217 else:
218 218 data = open(os.path.join(self.tmppath, name), 'rb').read()
219 219 mode = (mode & 0111) and 'x' or ''
220 220 return data, mode
221 221
222 222 def _exclude(self, name):
223 223 exclude = [ '{arch}', '.arch-ids', '.arch-inventory' ]
224 224 for exc in exclude:
225 225 if name.find(exc) != -1:
226 226 return True
227 227 return False
228 228
229 229 def _readcontents(self, path):
230 230 files = []
231 231 contents = os.listdir(path)
232 232 while len(contents) > 0:
233 233 c = contents.pop()
234 234 p = os.path.join(path, c)
235 235 # os.walk could be used, but here we avoid internal GNU
236 236 # Arch files and directories, thus saving a lot time.
237 237 if not self._exclude(p):
238 238 if os.path.isdir(p):
239 239 contents += [os.path.join(c, f) for f in os.listdir(p)]
240 240 else:
241 241 files.append(c)
242 242 return files
243 243
244 244 def _rendirchanges(self, src, dest):
245 245 changes = []
246 246 copies = {}
247 247 files = self._readcontents(os.path.join(self.tmppath, dest))
248 248 for f in files:
249 249 s = os.path.join(src, f)
250 250 d = os.path.join(dest, f)
251 251 changes.append(s)
252 252 changes.append(d)
253 253 copies[d] = s
254 254 return changes, copies
255 255
256 256 def _obtainrevision(self, rev):
257 257 self.ui.debug('obtaining revision %s...\n' % rev)
258 258 output = self._execute('get', rev, self.tmppath)
259 259 self.checkexit(output)
260 260 self.ui.debug('analyzing revision %s...\n' % rev)
261 261 files = self._readcontents(self.tmppath)
262 262 self.changes[rev].add_files += files
263 263
264 264 def _stripbasepath(self, path):
265 265 if path.startswith('./'):
266 266 return path[2:]
267 267 return path
268 268
269 269 def _parsecatlog(self, data, rev):
270 270 try:
271 271 catlog = self.catlogparser.parsestr(data)
272 272
273 273 # Commit date
274 274 self.changes[rev].date = util.datestr(
275 275 util.strdate(catlog['Standard-date'],
276 276 '%Y-%m-%d %H:%M:%S'))
277 277
278 278 # Commit author
279 279 self.changes[rev].author = self.recode(catlog['Creator'])
280 280
281 281 # Commit description
282 282 self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
283 283 catlog.get_payload()))
284 284 self.changes[rev].summary = self.recode(self.changes[rev].summary)
285 285
286 286 # Commit revision origin when dealing with a branch or tag
287 287 if 'Continuation-of' in catlog:
288 288 self.changes[rev].continuationof = self.recode(catlog['Continuation-of'])
289 289 except Exception:
290 290 raise util.Abort(_('could not parse cat-log of %s') % rev)
291 291
292 292 def _parsechangeset(self, data, rev):
293 293 for l in data:
294 294 l = l.strip()
295 295 # Added file (ignore added directory)
296 296 if l.startswith('A') and not l.startswith('A/'):
297 297 file = self._stripbasepath(l[1:].strip())
298 298 if not self._exclude(file):
299 299 self.changes[rev].add_files.append(file)
300 300 # Deleted file (ignore deleted directory)
301 301 elif l.startswith('D') and not l.startswith('D/'):
302 302 file = self._stripbasepath(l[1:].strip())
303 303 if not self._exclude(file):
304 304 self.changes[rev].del_files.append(file)
305 305 # Modified binary file
306 306 elif l.startswith('Mb'):
307 307 file = self._stripbasepath(l[2:].strip())
308 308 if not self._exclude(file):
309 309 self.changes[rev].mod_files.append(file)
310 310 # Modified link
311 311 elif l.startswith('M->'):
312 312 file = self._stripbasepath(l[3:].strip())
313 313 if not self._exclude(file):
314 314 self.changes[rev].mod_files.append(file)
315 315 # Modified file
316 316 elif l.startswith('M'):
317 317 file = self._stripbasepath(l[1:].strip())
318 318 if not self._exclude(file):
319 319 self.changes[rev].mod_files.append(file)
320 320 # Renamed file (or link)
321 321 elif l.startswith('=>'):
322 322 files = l[2:].strip().split(' ')
323 323 if len(files) == 1:
324 324 files = l[2:].strip().split('\t')
325 325 src = self._stripbasepath(files[0])
326 326 dst = self._stripbasepath(files[1])
327 327 if not self._exclude(src) and not self._exclude(dst):
328 328 self.changes[rev].ren_files[src] = dst
329 329 # Conversion from file to link or from link to file (modified)
330 330 elif l.startswith('ch'):
331 331 file = self._stripbasepath(l[2:].strip())
332 332 if not self._exclude(file):
333 333 self.changes[rev].mod_files.append(file)
334 334 # Renamed directory
335 335 elif l.startswith('/>'):
336 336 dirs = l[2:].strip().split(' ')
337 337 if len(dirs) == 1:
338 338 dirs = l[2:].strip().split('\t')
339 339 src = self._stripbasepath(dirs[0])
340 340 dst = self._stripbasepath(dirs[1])
341 341 if not self._exclude(src) and not self._exclude(dst):
342 342 self.changes[rev].ren_dirs[src] = dst
@@ -1,372 +1,372 b''
1 1 # hg.py - hg backend for convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 # Notes for hg->hg conversion:
9 9 #
10 10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 11 # of commit messages, but new versions do. Changesets created by
12 12 # those older versions, then converted, may thus have different
13 13 # hashes for changesets that are otherwise identical.
14 14 #
15 15 # * Using "--config convert.hg.saverev=true" will make the source
16 16 # identifier to be stored in the converted revision. This will cause
17 17 # the converted revision to have a different identity than the
18 18 # source.
19 19
20 20
21 21 import os, time, cStringIO
22 22 from mercurial.i18n import _
23 23 from mercurial.node import bin, hex, nullid
24 24 from mercurial import hg, util, context, error
25 25
26 26 from common import NoRepo, commit, converter_source, converter_sink
27 27
28 28 class mercurial_sink(converter_sink):
29 29 def __init__(self, ui, path):
30 30 converter_sink.__init__(self, ui, path)
31 31 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
32 32 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
33 33 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
34 34 self.lastbranch = None
35 35 if os.path.isdir(path) and len(os.listdir(path)) > 0:
36 36 try:
37 37 self.repo = hg.repository(self.ui, path)
38 38 if not self.repo.local():
39 39 raise NoRepo(_('%s is not a local Mercurial repo') % path)
40 40 except error.RepoError, err:
41 41 ui.traceback()
42 42 raise NoRepo(err.args[0])
43 43 else:
44 44 try:
45 45 ui.status(_('initializing destination %s repository\n') % path)
46 46 self.repo = hg.repository(self.ui, path, create=True)
47 47 if not self.repo.local():
48 48 raise NoRepo(_('%s is not a local Mercurial repo') % path)
49 49 self.created.append(path)
50 50 except error.RepoError:
51 51 ui.traceback()
52 52 raise NoRepo("could not create hg repo %s as sink" % path)
53 53 self.lock = None
54 54 self.wlock = None
55 55 self.filemapmode = False
56 56
57 57 def before(self):
58 58 self.ui.debug('run hg sink pre-conversion action\n')
59 59 self.wlock = self.repo.wlock()
60 60 self.lock = self.repo.lock()
61 61
62 62 def after(self):
63 63 self.ui.debug('run hg sink post-conversion action\n')
64 64 if self.lock:
65 65 self.lock.release()
66 66 if self.wlock:
67 67 self.wlock.release()
68 68
69 69 def revmapfile(self):
70 70 return os.path.join(self.path, ".hg", "shamap")
71 71
72 72 def authorfile(self):
73 73 return os.path.join(self.path, ".hg", "authormap")
74 74
75 75 def getheads(self):
76 76 h = self.repo.changelog.heads()
77 77 return [ hex(x) for x in h ]
78 78
79 79 def setbranch(self, branch, pbranches):
80 80 if not self.clonebranches:
81 81 return
82 82
83 83 setbranch = (branch != self.lastbranch)
84 84 self.lastbranch = branch
85 85 if not branch:
86 86 branch = 'default'
87 87 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
88 88 pbranch = pbranches and pbranches[0][1] or 'default'
89 89
90 90 branchpath = os.path.join(self.path, branch)
91 91 if setbranch:
92 92 self.after()
93 93 try:
94 94 self.repo = hg.repository(self.ui, branchpath)
95 95 except:
96 96 self.repo = hg.repository(self.ui, branchpath, create=True)
97 97 self.before()
98 98
99 99 # pbranches may bring revisions from other branches (merge parents)
100 100 # Make sure we have them, or pull them.
101 101 missings = {}
102 102 for b in pbranches:
103 103 try:
104 104 self.repo.lookup(b[0])
105 105 except:
106 106 missings.setdefault(b[1], []).append(b[0])
107 107
108 108 if missings:
109 109 self.after()
110 110 for pbranch, heads in missings.iteritems():
111 111 pbranchpath = os.path.join(self.path, pbranch)
112 112 prepo = hg.repository(self.ui, pbranchpath)
113 113 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
114 114 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
115 115 self.before()
116 116
117 117 def _rewritetags(self, source, revmap, data):
118 118 fp = cStringIO.StringIO()
119 119 for line in data.splitlines():
120 120 s = line.split(' ', 1)
121 121 if len(s) != 2:
122 122 continue
123 123 revid = revmap.get(source.lookuprev(s[0]))
124 124 if not revid:
125 125 continue
126 126 fp.write('%s %s\n' % (revid, s[1]))
127 127 return fp.getvalue()
128 128
129 129 def putcommit(self, files, copies, parents, commit, source, revmap):
130 130
131 131 files = dict(files)
132 132 def getfilectx(repo, memctx, f):
133 133 v = files[f]
134 134 data = source.getfile(f, v)
135 135 e = source.getmode(f, v)
136 136 if f == '.hgtags':
137 137 data = self._rewritetags(source, revmap, data)
138 138 return context.memfilectx(f, data, 'l' in e, 'x' in e, copies.get(f))
139 139
140 140 pl = []
141 141 for p in parents:
142 142 if p not in pl:
143 143 pl.append(p)
144 144 parents = pl
145 145 nparents = len(parents)
146 146 if self.filemapmode and nparents == 1:
147 147 m1node = self.repo.changelog.read(bin(parents[0]))[0]
148 148 parent = parents[0]
149 149
150 150 if len(parents) < 2: parents.append(nullid)
151 151 if len(parents) < 2: parents.append(nullid)
152 152 p2 = parents.pop(0)
153 153
154 154 text = commit.desc
155 155 extra = commit.extra.copy()
156 156 if self.branchnames and commit.branch:
157 157 extra['branch'] = commit.branch
158 158 if commit.rev:
159 159 extra['convert_revision'] = commit.rev
160 160
161 161 while parents:
162 162 p1 = p2
163 163 p2 = parents.pop(0)
164 164 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(), getfilectx,
165 165 commit.author, commit.date, extra)
166 166 self.repo.commitctx(ctx)
167 167 text = "(octopus merge fixup)\n"
168 168 p2 = hex(self.repo.changelog.tip())
169 169
170 170 if self.filemapmode and nparents == 1:
171 171 man = self.repo.manifest
172 172 mnode = self.repo.changelog.read(bin(p2))[0]
173 173 if not man.cmp(m1node, man.revision(mnode)):
174 174 self.ui.status(_("filtering out empty revision\n"))
175 175 self.repo.rollback()
176 176 return parent
177 177 return p2
178 178
179 179 def puttags(self, tags):
180 180 try:
181 181 parentctx = self.repo[self.tagsbranch]
182 182 tagparent = parentctx.node()
183 183 except error.RepoError:
184 184 parentctx = None
185 185 tagparent = nullid
186 186
187 187 try:
188 188 oldlines = sorted(parentctx['.hgtags'].data().splitlines(True))
189 189 except:
190 190 oldlines = []
191 191
192 192 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
193 193 if newlines == oldlines:
194 194 return None, None
195 195 data = "".join(newlines)
196 196 def getfilectx(repo, memctx, f):
197 197 return context.memfilectx(f, data, False, False, None)
198 198
199 199 self.ui.status(_("updating tags\n"))
200 200 date = "%s 0" % int(time.mktime(time.gmtime()))
201 201 extra = {'branch': self.tagsbranch}
202 202 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
203 203 [".hgtags"], getfilectx, "convert-repo", date,
204 204 extra)
205 205 self.repo.commitctx(ctx)
206 206 return hex(self.repo.changelog.tip()), hex(tagparent)
207 207
208 208 def setfilemapmode(self, active):
209 209 self.filemapmode = active
210 210
211 211 class mercurial_source(converter_source):
212 212 def __init__(self, ui, path, rev=None):
213 213 converter_source.__init__(self, ui, path, rev)
214 214 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
215 215 self.ignored = set()
216 216 self.saverev = ui.configbool('convert', 'hg.saverev', False)
217 217 try:
218 218 self.repo = hg.repository(self.ui, path)
219 219 # try to provoke an exception if this isn't really a hg
220 220 # repo, but some other bogus compatible-looking url
221 221 if not self.repo.local():
222 222 raise error.RepoError()
223 223 except error.RepoError:
224 224 ui.traceback()
225 225 raise NoRepo("%s is not a local Mercurial repo" % path)
226 226 self.lastrev = None
227 227 self.lastctx = None
228 228 self._changescache = None
229 229 self.convertfp = None
230 230 # Restrict converted revisions to startrev descendants
231 231 startnode = ui.config('convert', 'hg.startrev')
232 232 if startnode is not None:
233 233 try:
234 234 startnode = self.repo.lookup(startnode)
235 235 except error.RepoError:
236 236 raise util.Abort(_('%s is not a valid start revision')
237 237 % startnode)
238 238 startrev = self.repo.changelog.rev(startnode)
239 239 children = {startnode: 1}
240 240 for rev in self.repo.changelog.descendants(startrev):
241 241 children[self.repo.changelog.node(rev)] = 1
242 242 self.keep = children.__contains__
243 243 else:
244 244 self.keep = util.always
245 245
246 246 def changectx(self, rev):
247 247 if self.lastrev != rev:
248 248 self.lastctx = self.repo[rev]
249 249 self.lastrev = rev
250 250 return self.lastctx
251 251
252 252 def parents(self, ctx):
253 253 return [p for p in ctx.parents() if p and self.keep(p.node())]
254 254
255 255 def getheads(self):
256 256 if self.rev:
257 257 heads = [self.repo[self.rev].node()]
258 258 else:
259 259 heads = self.repo.heads()
260 260 return [hex(h) for h in heads if self.keep(h)]
261 261
262 262 def getfile(self, name, rev):
263 263 try:
264 264 return self.changectx(rev)[name].data()
265 265 except error.LookupError, err:
266 266 raise IOError(err)
267 267
268 268 def getmode(self, name, rev):
269 269 return self.changectx(rev).manifest().flags(name)
270 270
271 271 def getchanges(self, rev):
272 272 ctx = self.changectx(rev)
273 273 parents = self.parents(ctx)
274 274 if not parents:
275 275 files = sorted(ctx.manifest())
276 276 if self.ignoreerrors:
277 277 # calling getcopies() is a simple way to detect missing
278 278 # revlogs and populate self.ignored
279 279 self.getcopies(ctx, parents, files)
280 280 return [(f, rev) for f in files if f not in self.ignored], {}
281 281 if self._changescache and self._changescache[0] == rev:
282 282 m, a, r = self._changescache[1]
283 283 else:
284 284 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
285 285 # getcopies() detects missing revlogs early, run it before
286 286 # filtering the changes.
287 287 copies = self.getcopies(ctx, parents, m + a)
288 288 changes = [(name, rev) for name in m + a + r
289 289 if name not in self.ignored]
290 290 return sorted(changes), copies
291 291
292 292 def getcopies(self, ctx, parents, files):
293 293 copies = {}
294 294 for name in files:
295 295 if name in self.ignored:
296 296 continue
297 297 try:
298 298 copysource, copynode = ctx.filectx(name).renamed()
299 299 if copysource in self.ignored or not self.keep(copynode):
300 300 continue
301 301 # Ignore copy sources not in parent revisions
302 302 found = False
303 303 for p in parents:
304 304 if copysource in p:
305 305 found = True
306 306 break
307 307 if not found:
308 308 continue
309 309 copies[name] = copysource
310 310 except TypeError:
311 311 pass
312 312 except error.LookupError, e:
313 313 if not self.ignoreerrors:
314 314 raise
315 315 self.ignored.add(name)
316 316 self.ui.warn(_('ignoring: %s\n') % e)
317 317 return copies
318 318
319 319 def getcommit(self, rev):
320 320 ctx = self.changectx(rev)
321 321 parents = [p.hex() for p in self.parents(ctx)]
322 322 if self.saverev:
323 323 crev = rev
324 324 else:
325 325 crev = None
326 326 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
327 327 desc=ctx.description(), rev=crev, parents=parents,
328 328 branch=ctx.branch(), extra=ctx.extra(),
329 329 sortkey=ctx.rev())
330 330
331 331 def gettags(self):
332 332 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
333 333 return dict([(name, hex(node)) for name, node in tags
334 334 if self.keep(node)])
335 335
336 336 def getchangedfiles(self, rev, i):
337 337 ctx = self.changectx(rev)
338 338 parents = self.parents(ctx)
339 339 if not parents and i is None:
340 340 i = 0
341 341 changes = [], ctx.manifest().keys(), []
342 342 else:
343 343 i = i or 0
344 344 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
345 345 changes = [[f for f in l if f not in self.ignored] for l in changes]
346 346
347 347 if i == 0:
348 348 self._changescache = (rev, changes)
349 349
350 350 return changes[0] + changes[1] + changes[2]
351 351
352 352 def converted(self, rev, destrev):
353 353 if self.convertfp is None:
354 354 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
355 355 'a')
356 356 self.convertfp.write('%s %s\n' % (destrev, rev))
357 357 self.convertfp.flush()
358 358
359 359 def before(self):
360 360 self.ui.debug('run hg source pre-conversion action\n')
361 361
362 362 def after(self):
363 363 self.ui.debug('run hg source post-conversion action\n')
364 364
365 365 def hasnativeorder(self):
366 366 return True
367 367
368 368 def lookuprev(self, rev):
369 369 try:
370 370 return hex(self.repo.lookup(rev))
371 371 except error.RepoError:
372 372 return None
@@ -1,222 +1,222 b''
1 1 # monotone.py - monotone support for the convert extension
2 2 #
3 3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
4 4 # others
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2 or any later version.
8 8
9 9 import os, re
10 10 from mercurial import util
11 11 from common import NoRepo, commit, converter_source, checktool
12 12 from common import commandline
13 13 from mercurial.i18n import _
14 14
15 15 class monotone_source(converter_source, commandline):
16 16 def __init__(self, ui, path=None, rev=None):
17 17 converter_source.__init__(self, ui, path, rev)
18 18 commandline.__init__(self, ui, 'mtn')
19 19
20 20 self.ui = ui
21 21 self.path = path
22 22
23 23 norepo = NoRepo (_("%s does not look like a monotone repo") % path)
24 24 if not os.path.exists(os.path.join(path, '_MTN')):
25 25 # Could be a monotone repository (SQLite db file)
26 26 try:
27 27 header = file(path, 'rb').read(16)
28 28 except:
29 29 header = ''
30 30 if header != 'SQLite format 3\x00':
31 31 raise norepo
32 32
33 33 # regular expressions for parsing monotone output
34 34 space = r'\s*'
35 35 name = r'\s+"((?:\\"|[^"])*)"\s*'
36 36 value = name
37 37 revision = r'\s+\[(\w+)\]\s*'
38 38 lines = r'(?:.|\n)+'
39 39
40 40 self.dir_re = re.compile(space + "dir" + name)
41 41 self.file_re = re.compile(space + "file" + name + "content" + revision)
42 42 self.add_file_re = re.compile(space + "add_file" + name + "content" + revision)
43 43 self.patch_re = re.compile(space + "patch" + name + "from" + revision + "to" + revision)
44 44 self.rename_re = re.compile(space + "rename" + name + "to" + name)
45 45 self.delete_re = re.compile(space + "delete" + name)
46 46 self.tag_re = re.compile(space + "tag" + name + "revision" + revision)
47 47 self.cert_re = re.compile(lines + space + "name" + name + "value" + value)
48 48
49 49 attr = space + "file" + lines + space + "attr" + space
50 50 self.attr_execute_re = re.compile(attr + '"mtn:execute"' + space + '"true"')
51 51
52 52 # cached data
53 53 self.manifest_rev = None
54 54 self.manifest = None
55 55 self.files = None
56 56 self.dirs = None
57 57
58 58 checktool('mtn', abort=False)
59 59
60 60 # test if there are any revisions
61 61 self.rev = None
62 62 try:
63 63 self.getheads()
64 64 except:
65 65 raise norepo
66 66 self.rev = rev
67 67
68 68 def mtnrun(self, *args, **kwargs):
69 69 kwargs['d'] = self.path
70 70 return self.run0('automate', *args, **kwargs)
71 71
72 72 def mtnloadmanifest(self, rev):
73 73 if self.manifest_rev == rev:
74 74 return
75 75 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
76 76 self.manifest_rev = rev
77 77 self.files = {}
78 78 self.dirs = {}
79 79
80 80 for e in self.manifest:
81 81 m = self.file_re.match(e)
82 82 if m:
83 83 attr = ""
84 84 name = m.group(1)
85 85 node = m.group(2)
86 86 if self.attr_execute_re.match(e):
87 87 attr += "x"
88 88 self.files[name] = (node, attr)
89 89 m = self.dir_re.match(e)
90 90 if m:
91 91 self.dirs[m.group(1)] = True
92 92
93 93 def mtnisfile(self, name, rev):
94 94 # a non-file could be a directory or a deleted or renamed file
95 95 self.mtnloadmanifest(rev)
96 96 return name in self.files
97 97
98 98 def mtnisdir(self, name, rev):
99 99 self.mtnloadmanifest(rev)
100 100 return name in self.dirs
101 101
102 102 def mtngetcerts(self, rev):
103 103 certs = {"author":"<missing>", "date":"<missing>",
104 104 "changelog":"<missing>", "branch":"<missing>"}
105 105 certlist = self.mtnrun("certs", rev)
106 106 # mtn < 0.45:
107 107 # key "test@selenic.com"
108 108 # mtn >= 0.45:
109 109 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
110 110 certlist = re.split('\n\n key ["\[]', certlist)
111 111 for e in certlist:
112 112 m = self.cert_re.match(e)
113 113 if m:
114 114 name, value = m.groups()
115 115 value = value.replace(r'\"', '"')
116 116 value = value.replace(r'\\', '\\')
117 117 certs[name] = value
118 118 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
119 119 # and all times are stored in UTC
120 120 certs["date"] = certs["date"].split('.')[0] + " UTC"
121 121 return certs
122 122
123 123 # implement the converter_source interface:
124 124
125 125 def getheads(self):
126 126 if not self.rev:
127 127 return self.mtnrun("leaves").splitlines()
128 128 else:
129 129 return [self.rev]
130 130
131 131 def getchanges(self, rev):
132 132 #revision = self.mtncmd("get_revision %s" % rev).split("\n\n")
133 133 revision = self.mtnrun("get_revision", rev).split("\n\n")
134 134 files = {}
135 135 ignoremove = {}
136 136 renameddirs = []
137 137 copies = {}
138 138 for e in revision:
139 139 m = self.add_file_re.match(e)
140 140 if m:
141 141 files[m.group(1)] = rev
142 142 ignoremove[m.group(1)] = rev
143 143 m = self.patch_re.match(e)
144 144 if m:
145 145 files[m.group(1)] = rev
146 146 # Delete/rename is handled later when the convert engine
147 147 # discovers an IOError exception from getfile,
148 148 # but only if we add the "from" file to the list of changes.
149 149 m = self.delete_re.match(e)
150 150 if m:
151 151 files[m.group(1)] = rev
152 152 m = self.rename_re.match(e)
153 153 if m:
154 154 toname = m.group(2)
155 155 fromname = m.group(1)
156 156 if self.mtnisfile(toname, rev):
157 157 ignoremove[toname] = 1
158 158 copies[toname] = fromname
159 159 files[toname] = rev
160 160 files[fromname] = rev
161 161 elif self.mtnisdir(toname, rev):
162 162 renameddirs.append((fromname, toname))
163 163
164 164 # Directory renames can be handled only once we have recorded
165 165 # all new files
166 166 for fromdir, todir in renameddirs:
167 167 renamed = {}
168 168 for tofile in self.files:
169 169 if tofile in ignoremove:
170 170 continue
171 171 if tofile.startswith(todir + '/'):
172 172 renamed[tofile] = fromdir + tofile[len(todir):]
173 173 # Avoid chained moves like:
174 174 # d1(/a) => d3/d1(/a)
175 175 # d2 => d3
176 176 ignoremove[tofile] = 1
177 177 for tofile, fromfile in renamed.items():
178 178 self.ui.debug (_("copying file in renamed directory "
179 179 "from '%s' to '%s'")
180 180 % (fromfile, tofile), '\n')
181 181 files[tofile] = rev
182 182 copies[tofile] = fromfile
183 183 for fromfile in renamed.values():
184 184 files[fromfile] = rev
185 185
186 186 return (files.items(), copies)
187 187
188 188 def getmode(self, name, rev):
189 189 self.mtnloadmanifest(rev)
190 190 node, attr = self.files.get(name, (None, ""))
191 191 return attr
192 192
193 193 def getfile(self, name, rev):
194 194 if not self.mtnisfile(name, rev):
195 195 raise IOError() # file was deleted or renamed
196 196 try:
197 197 return self.mtnrun("get_file_of", name, r=rev)
198 198 except:
199 199 raise IOError() # file was deleted or renamed
200 200
201 201 def getcommit(self, rev):
202 202 certs = self.mtngetcerts(rev)
203 203 return commit(
204 204 author=certs["author"],
205 205 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
206 206 desc=certs["changelog"],
207 207 rev=rev,
208 208 parents=self.mtnrun("parents", rev).splitlines(),
209 209 branch=certs["branch"])
210 210
211 211 def gettags(self):
212 212 tags = {}
213 213 for e in self.mtnrun("tags").split("\n\n"):
214 214 m = self.tag_re.match(e)
215 215 if m:
216 216 tags[m.group(1)] = m.group(2)
217 217 return tags
218 218
219 219 def getchangedfiles(self, rev, i):
220 220 # This function is only needed to support --filemap
221 221 # ... and we don't support that
222 222 raise NotImplementedError()
@@ -1,205 +1,203 b''
1 #
2 1 # Perforce source for convert extension.
3 2 #
4 3 # Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk>
5 4 #
6 5 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
8 #
6 # GNU General Public License version 2 or any later version.
9 7
10 8 from mercurial import util
11 9 from mercurial.i18n import _
12 10
13 11 from common import commit, converter_source, checktool, NoRepo
14 12 import marshal
15 13 import re
16 14
17 15 def loaditer(f):
18 16 "Yield the dictionary objects generated by p4"
19 17 try:
20 18 while True:
21 19 d = marshal.load(f)
22 20 if not d:
23 21 break
24 22 yield d
25 23 except EOFError:
26 24 pass
27 25
28 26 class p4_source(converter_source):
29 27 def __init__(self, ui, path, rev=None):
30 28 super(p4_source, self).__init__(ui, path, rev=rev)
31 29
32 30 if "/" in path and not path.startswith('//'):
33 31 raise NoRepo('%s does not look like a P4 repo' % path)
34 32
35 33 checktool('p4', abort=False)
36 34
37 35 self.p4changes = {}
38 36 self.heads = {}
39 37 self.changeset = {}
40 38 self.files = {}
41 39 self.tags = {}
42 40 self.lastbranch = {}
43 41 self.parent = {}
44 42 self.encoding = "latin_1"
45 43 self.depotname = {} # mapping from local name to depot name
46 44 self.modecache = {}
47 45 self.re_type = re.compile("([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)(\+\w+)?$")
48 46 self.re_keywords = re.compile(r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author):[^$\n]*\$")
49 47 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
50 48
51 49 self._parse(ui, path)
52 50
53 51 def _parse_view(self, path):
54 52 "Read changes affecting the path"
55 53 cmd = 'p4 -G changes -s submitted "%s"' % path
56 54 stdout = util.popen(cmd, mode='rb')
57 55 for d in loaditer(stdout):
58 56 c = d.get("change", None)
59 57 if c:
60 58 self.p4changes[c] = True
61 59
62 60 def _parse(self, ui, path):
63 61 "Prepare list of P4 filenames and revisions to import"
64 62 ui.status(_('reading p4 views\n'))
65 63
66 64 # read client spec or view
67 65 if "/" in path:
68 66 self._parse_view(path)
69 67 if path.startswith("//") and path.endswith("/..."):
70 68 views = {path[:-3]:""}
71 69 else:
72 70 views = {"//": ""}
73 71 else:
74 72 cmd = 'p4 -G client -o "%s"' % path
75 73 clientspec = marshal.load(util.popen(cmd, mode='rb'))
76 74
77 75 views = {}
78 76 for client in clientspec:
79 77 if client.startswith("View"):
80 78 sview, cview = clientspec[client].split()
81 79 self._parse_view(sview)
82 80 if sview.endswith("...") and cview.endswith("..."):
83 81 sview = sview[:-3]
84 82 cview = cview[:-3]
85 83 cview = cview[2:]
86 84 cview = cview[cview.find("/") + 1:]
87 85 views[sview] = cview
88 86
89 87 # list of changes that affect our source files
90 88 self.p4changes = self.p4changes.keys()
91 89 self.p4changes.sort(key=int)
92 90
93 91 # list with depot pathnames, longest first
94 92 vieworder = views.keys()
95 93 vieworder.sort(key=len, reverse=True)
96 94
97 95 # handle revision limiting
98 96 startrev = self.ui.config('convert', 'p4.startrev', default=0)
99 97 self.p4changes = [x for x in self.p4changes
100 98 if ((not startrev or int(x) >= int(startrev)) and
101 99 (not self.rev or int(x) <= int(self.rev)))]
102 100
103 101 # now read the full changelists to get the list of file revisions
104 102 ui.status(_('collecting p4 changelists\n'))
105 103 lastid = None
106 104 for change in self.p4changes:
107 105 cmd = "p4 -G describe %s" % change
108 106 stdout = util.popen(cmd, mode='rb')
109 107 d = marshal.load(stdout)
110 108
111 109 desc = self.recode(d["desc"])
112 110 shortdesc = desc.split("\n", 1)[0]
113 111 t = '%s %s' % (d["change"], repr(shortdesc)[1:-1])
114 112 ui.status(util.ellipsis(t, 80) + '\n')
115 113
116 114 if lastid:
117 115 parents = [lastid]
118 116 else:
119 117 parents = []
120 118
121 119 date = (int(d["time"]), 0) # timezone not set
122 120 c = commit(author=self.recode(d["user"]), date=util.datestr(date),
123 121 parents=parents, desc=desc, branch='', extra={"p4": change})
124 122
125 123 files = []
126 124 i = 0
127 125 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
128 126 oldname = d["depotFile%d" % i]
129 127 filename = None
130 128 for v in vieworder:
131 129 if oldname.startswith(v):
132 130 filename = views[v] + oldname[len(v):]
133 131 break
134 132 if filename:
135 133 files.append((filename, d["rev%d" % i]))
136 134 self.depotname[filename] = oldname
137 135 i += 1
138 136 self.changeset[change] = c
139 137 self.files[change] = files
140 138 lastid = change
141 139
142 140 if lastid:
143 141 self.heads = [lastid]
144 142
145 143 def getheads(self):
146 144 return self.heads
147 145
148 146 def getfile(self, name, rev):
149 147 cmd = 'p4 -G print "%s#%s"' % (self.depotname[name], rev)
150 148 stdout = util.popen(cmd, mode='rb')
151 149
152 150 mode = None
153 151 contents = ""
154 152 keywords = None
155 153
156 154 for d in loaditer(stdout):
157 155 code = d["code"]
158 156 data = d.get("data")
159 157
160 158 if code == "error":
161 159 raise IOError(d["generic"], data)
162 160
163 161 elif code == "stat":
164 162 p4type = self.re_type.match(d["type"])
165 163 if p4type:
166 164 mode = ""
167 165 flags = (p4type.group(1) or "") + (p4type.group(3) or "")
168 166 if "x" in flags:
169 167 mode = "x"
170 168 if p4type.group(2) == "symlink":
171 169 mode = "l"
172 170 if "ko" in flags:
173 171 keywords = self.re_keywords_old
174 172 elif "k" in flags:
175 173 keywords = self.re_keywords
176 174
177 175 elif code == "text" or code == "binary":
178 176 contents += data
179 177
180 178 if mode is None:
181 179 raise IOError(0, "bad stat")
182 180
183 181 self.modecache[(name, rev)] = mode
184 182
185 183 if keywords:
186 184 contents = keywords.sub("$\\1$", contents)
187 185 if mode == "l" and contents.endswith("\n"):
188 186 contents = contents[:-1]
189 187
190 188 return contents
191 189
192 190 def getmode(self, name, rev):
193 191 return self.modecache[(name, rev)]
194 192
195 193 def getchanges(self, rev):
196 194 return self.files[rev], {}
197 195
198 196 def getcommit(self, rev):
199 197 return self.changeset[rev]
200 198
201 199 def gettags(self):
202 200 return self.tags
203 201
204 202 def getchangedfiles(self, rev, i):
205 203 return sorted([x[0] for x in self.files[rev]])
@@ -1,282 +1,282 b''
1 1 # extdiff.py - external diff program support for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to allow external programs to compare revisions
9 9
10 10 The extdiff Mercurial extension allows you to use external programs
11 11 to compare revisions, or revision with working directory. The external
12 12 diff programs are called with a configurable set of options and two
13 13 non-option arguments: paths to directories containing snapshots of
14 14 files to compare.
15 15
16 16 The extdiff extension also allows to configure new diff commands, so
17 17 you do not need to type "hg extdiff -p kdiff3" always. ::
18 18
19 19 [extdiff]
20 20 # add new command that runs GNU diff(1) in 'context diff' mode
21 21 cdiff = gdiff -Nprc5
22 22 ## or the old way:
23 23 #cmd.cdiff = gdiff
24 24 #opts.cdiff = -Nprc5
25 25
26 26 # add new command called vdiff, runs kdiff3
27 27 vdiff = kdiff3
28 28
29 29 # add new command called meld, runs meld (no need to name twice)
30 30 meld =
31 31
32 32 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
33 33 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
34 34 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
35 35 # your .vimrc
36 36 vimdiff = gvim -f '+next' '+execute "DirDiff" argv(0) argv(1)'
37 37
38 38 You can use -I/-X and list of file or directory names like normal "hg
39 39 diff" command. The extdiff extension makes snapshots of only needed
40 40 files, so running the external diff program will actually be pretty
41 41 fast (at least faster than having to compare the entire tree).
42 42 '''
43 43
44 44 from mercurial.i18n import _
45 45 from mercurial.node import short, nullid
46 46 from mercurial import cmdutil, util, commands, encoding
47 47 import os, shlex, shutil, tempfile, re
48 48
49 49 def snapshot(ui, repo, files, node, tmproot):
50 50 '''snapshot files as of some revision
51 51 if not using snapshot, -I/-X does not work and recursive diff
52 52 in tools like kdiff3 and meld displays too many files.'''
53 53 dirname = os.path.basename(repo.root)
54 54 if dirname == "":
55 55 dirname = "root"
56 56 if node is not None:
57 57 dirname = '%s.%s' % (dirname, short(node))
58 58 base = os.path.join(tmproot, dirname)
59 59 os.mkdir(base)
60 60 if node is not None:
61 61 ui.note(_('making snapshot of %d files from rev %s\n') %
62 62 (len(files), short(node)))
63 63 else:
64 64 ui.note(_('making snapshot of %d files from working directory\n') %
65 65 (len(files)))
66 66 wopener = util.opener(base)
67 67 fns_and_mtime = []
68 68 ctx = repo[node]
69 69 for fn in files:
70 70 wfn = util.pconvert(fn)
71 71 if not wfn in ctx:
72 72 # File doesn't exist; could be a bogus modify
73 73 continue
74 74 ui.note(' %s\n' % wfn)
75 75 dest = os.path.join(base, wfn)
76 76 fctx = ctx[wfn]
77 77 data = repo.wwritedata(wfn, fctx.data())
78 78 if 'l' in fctx.flags():
79 79 wopener.symlink(data, wfn)
80 80 else:
81 81 wopener(wfn, 'w').write(data)
82 82 if 'x' in fctx.flags():
83 83 util.set_flags(dest, False, True)
84 84 if node is None:
85 85 fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
86 86 return dirname, fns_and_mtime
87 87
88 88 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
89 89 '''Do the actuall diff:
90 90
91 91 - copy to a temp structure if diffing 2 internal revisions
92 92 - copy to a temp structure if diffing working revision with
93 93 another one and more than 1 file is changed
94 94 - just invoke the diff for a single file in the working dir
95 95 '''
96 96
97 97 revs = opts.get('rev')
98 98 change = opts.get('change')
99 99 args = ' '.join(diffopts)
100 100 do3way = '$parent2' in args
101 101
102 102 if revs and change:
103 103 msg = _('cannot specify --rev and --change at the same time')
104 104 raise util.Abort(msg)
105 105 elif change:
106 106 node2 = repo.lookup(change)
107 107 node1a, node1b = repo.changelog.parents(node2)
108 108 else:
109 109 node1a, node2 = cmdutil.revpair(repo, revs)
110 110 if not revs:
111 111 node1b = repo.dirstate.parents()[1]
112 112 else:
113 113 node1b = nullid
114 114
115 115 # Disable 3-way merge if there is only one parent
116 116 if do3way:
117 117 if node1b == nullid:
118 118 do3way = False
119 119
120 120 matcher = cmdutil.match(repo, pats, opts)
121 121 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher)[:3])
122 122 if do3way:
123 123 mod_b, add_b, rem_b = map(set, repo.status(node1b, node2, matcher)[:3])
124 124 else:
125 125 mod_b, add_b, rem_b = set(), set(), set()
126 126 modadd = mod_a | add_a | mod_b | add_b
127 127 common = modadd | rem_a | rem_b
128 128 if not common:
129 129 return 0
130 130
131 131 tmproot = tempfile.mkdtemp(prefix='extdiff.')
132 132 try:
133 133 # Always make a copy of node1a (and node1b, if applicable)
134 134 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
135 135 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot)[0]
136 136 if do3way:
137 137 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
138 138 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot)[0]
139 139 else:
140 140 dir1b = None
141 141
142 142 fns_and_mtime = []
143 143
144 144 # If node2 in not the wc or there is >1 change, copy it
145 145 dir2root = ''
146 146 if node2:
147 147 dir2 = snapshot(ui, repo, modadd, node2, tmproot)[0]
148 148 elif len(common) > 1:
149 149 #we only actually need to get the files to copy back to
150 150 #the working dir in this case (because the other cases
151 151 #are: diffing 2 revisions or single file -- in which case
152 152 #the file is already directly passed to the diff tool).
153 153 dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot)
154 154 else:
155 155 # This lets the diff tool open the changed file directly
156 156 dir2 = ''
157 157 dir2root = repo.root
158 158
159 159 # If only one change, diff the files instead of the directories
160 160 # Handle bogus modifies correctly by checking if the files exist
161 161 if len(common) == 1:
162 162 common_file = util.localpath(common.pop())
163 163 dir1a = os.path.join(dir1a, common_file)
164 164 if not os.path.isfile(os.path.join(tmproot, dir1a)):
165 165 dir1a = os.devnull
166 166 if do3way:
167 167 dir1b = os.path.join(dir1b, common_file)
168 168 if not os.path.isfile(os.path.join(tmproot, dir1b)):
169 169 dir1b = os.devnull
170 170 dir2 = os.path.join(dir2root, dir2, common_file)
171 171
172 172 # Function to quote file/dir names in the argument string.
173 173 # When not operating in 3-way mode, an empty string is
174 174 # returned for parent2
175 175 replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b, child=dir2)
176 176 def quote(match):
177 177 key = match.group()[1:]
178 178 if not do3way and key == 'parent2':
179 179 return ''
180 180 return util.shellquote(replace[key])
181 181
182 182 # Match parent2 first, so 'parent1?' will match both parent1 and parent
183 183 regex = '\$(parent2|parent1?|child)'
184 184 if not do3way and not re.search(regex, args):
185 185 args += ' $parent1 $child'
186 186 args = re.sub(regex, quote, args)
187 187 cmdline = util.shellquote(diffcmd) + ' ' + args
188 188
189 189 ui.debug('running %r in %s\n' % (cmdline, tmproot))
190 190 util.system(cmdline, cwd=tmproot)
191 191
192 192 for copy_fn, working_fn, mtime in fns_and_mtime:
193 193 if os.path.getmtime(copy_fn) != mtime:
194 194 ui.debug('file changed while diffing. '
195 195 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
196 196 util.copyfile(copy_fn, working_fn)
197 197
198 198 return 1
199 199 finally:
200 200 ui.note(_('cleaning up temp directory\n'))
201 201 shutil.rmtree(tmproot)
202 202
203 203 def extdiff(ui, repo, *pats, **opts):
204 204 '''use external program to diff repository (or selected files)
205 205
206 206 Show differences between revisions for the specified files, using
207 207 an external program. The default program used is diff, with
208 208 default options "-Npru".
209 209
210 210 To select a different program, use the -p/--program option. The
211 211 program will be passed the names of two directories to compare. To
212 212 pass additional options to the program, use -o/--option. These
213 213 will be passed before the names of the directories to compare.
214 214
215 215 When two revision arguments are given, then changes are shown
216 216 between those revisions. If only one revision is specified then
217 217 that revision is compared to the working directory, and, when no
218 218 revisions are specified, the working directory files are compared
219 219 to its parent.'''
220 220 program = opts.get('program')
221 221 option = opts.get('option')
222 222 if not program:
223 223 program = 'diff'
224 224 option = option or ['-Npru']
225 225 return dodiff(ui, repo, program, option, pats, opts)
226 226
227 227 cmdtable = {
228 228 "extdiff":
229 229 (extdiff,
230 230 [('p', 'program', '', _('comparison program to run')),
231 231 ('o', 'option', [], _('pass option to comparison program')),
232 232 ('r', 'rev', [], _('revision')),
233 233 ('c', 'change', '', _('change made by revision')),
234 234 ] + commands.walkopts,
235 235 _('hg extdiff [OPT]... [FILE]...')),
236 236 }
237 237
238 238 def uisetup(ui):
239 239 for cmd, path in ui.configitems('extdiff'):
240 240 if cmd.startswith('cmd.'):
241 241 cmd = cmd[4:]
242 242 if not path: path = cmd
243 243 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
244 244 diffopts = diffopts and [diffopts] or []
245 245 elif cmd.startswith('opts.'):
246 246 continue
247 247 else:
248 248 # command = path opts
249 249 if path:
250 250 diffopts = shlex.split(path)
251 251 path = diffopts.pop(0)
252 252 else:
253 253 path, diffopts = cmd, []
254 254 def save(cmd, path, diffopts):
255 255 '''use closure to save diff command to use'''
256 256 def mydiff(ui, repo, *pats, **opts):
257 257 return dodiff(ui, repo, path, diffopts + opts['option'],
258 258 pats, opts)
259 259 doc = _('''\
260 260 use %(path)s to diff repository (or selected files)
261 261
262 262 Show differences between revisions for the specified files, using
263 263 the %(path)s program.
264 264
265 265 When two revision arguments are given, then changes are shown
266 266 between those revisions. If only one revision is specified then
267 267 that revision is compared to the working directory, and, when no
268 268 revisions are specified, the working directory files are compared
269 269 to its parent.\
270 270 ''') % dict(path=util.uirepr(path))
271 271
272 272 # We must translate the docstring right away since it is
273 273 # used as a format string. The string will unfortunately
274 274 # be translated again in commands.helpcmd and this will
275 275 # fail when the docstring contains non-ASCII characters.
276 276 # Decoding the string to a Unicode string here (using the
277 277 # right encoding) prevents that.
278 278 mydiff.__doc__ = doc.decode(encoding.encoding)
279 279 return mydiff
280 280 cmdtable[cmd] = (save(cmd, path, diffopts),
281 281 cmdtable['extdiff'][1][1:],
282 282 _('hg %s [OPTION]... [FILE]...') % cmd)
@@ -1,148 +1,148 b''
1 1 # fetch.py - pull and merge remote changes
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''pull, update and merge in one command'''
9 9
10 10 from mercurial.i18n import _
11 11 from mercurial.node import nullid, short
12 12 from mercurial import commands, cmdutil, hg, util, url, error
13 13 from mercurial.lock import release
14 14
15 15 def fetch(ui, repo, source='default', **opts):
16 16 '''pull changes from a remote repository, merge new changes if needed.
17 17
18 18 This finds all changes from the repository at the specified path
19 19 or URL and adds them to the local repository.
20 20
21 21 If the pulled changes add a new branch head, the head is
22 22 automatically merged, and the result of the merge is committed.
23 23 Otherwise, the working directory is updated to include the new
24 24 changes.
25 25
26 26 When a merge occurs, the newly pulled changes are assumed to be
27 27 "authoritative". The head of the new changes is used as the first
28 28 parent, with local changes as the second. To switch the merge
29 29 order, use --switch-parent.
30 30
31 31 See 'hg help dates' for a list of formats valid for -d/--date.
32 32 '''
33 33
34 34 date = opts.get('date')
35 35 if date:
36 36 opts['date'] = util.parsedate(date)
37 37
38 38 parent, p2 = repo.dirstate.parents()
39 39 branch = repo.dirstate.branch()
40 40 branchnode = repo.branchtags().get(branch)
41 41 if parent != branchnode:
42 42 raise util.Abort(_('working dir not at branch tip '
43 43 '(use "hg update" to check out branch tip)'))
44 44
45 45 if p2 != nullid:
46 46 raise util.Abort(_('outstanding uncommitted merge'))
47 47
48 48 wlock = lock = None
49 49 try:
50 50 wlock = repo.wlock()
51 51 lock = repo.lock()
52 52 mod, add, rem, del_ = repo.status()[:4]
53 53
54 54 if mod or add or rem:
55 55 raise util.Abort(_('outstanding uncommitted changes'))
56 56 if del_:
57 57 raise util.Abort(_('working directory is missing some files'))
58 58 bheads = repo.branchheads(branch)
59 59 bheads = [head for head in bheads if len(repo[head].children()) == 0]
60 60 if len(bheads) > 1:
61 61 raise util.Abort(_('multiple heads in this branch '
62 62 '(use "hg heads ." and "hg merge" to merge)'))
63 63
64 64 other = hg.repository(cmdutil.remoteui(repo, opts),
65 65 ui.expandpath(source))
66 66 ui.status(_('pulling from %s\n') %
67 67 url.hidepassword(ui.expandpath(source)))
68 68 revs = None
69 69 if opts['rev']:
70 70 try:
71 71 revs = [other.lookup(rev) for rev in opts['rev']]
72 72 except error.CapabilityError:
73 73 err = _("Other repository doesn't support revision lookup, "
74 74 "so a rev cannot be specified.")
75 75 raise util.Abort(err)
76 76
77 77 # Are there any changes at all?
78 78 modheads = repo.pull(other, heads=revs)
79 79 if modheads == 0:
80 80 return 0
81 81
82 82 # Is this a simple fast-forward along the current branch?
83 83 newheads = repo.branchheads(branch)
84 84 newheads = [head for head in newheads if len(repo[head].children()) == 0]
85 85 newchildren = repo.changelog.nodesbetween([parent], newheads)[2]
86 86 if len(newheads) == 1:
87 87 if newchildren[0] != parent:
88 88 return hg.clean(repo, newchildren[0])
89 89 else:
90 90 return
91 91
92 92 # Are there more than one additional branch heads?
93 93 newchildren = [n for n in newchildren if n != parent]
94 94 newparent = parent
95 95 if newchildren:
96 96 newparent = newchildren[0]
97 97 hg.clean(repo, newparent)
98 98 newheads = [n for n in newheads if n != newparent]
99 99 if len(newheads) > 1:
100 100 ui.status(_('not merging with %d other new branch heads '
101 101 '(use "hg heads ." and "hg merge" to merge them)\n') %
102 102 (len(newheads) - 1))
103 103 return
104 104
105 105 # Otherwise, let's merge.
106 106 err = False
107 107 if newheads:
108 108 # By default, we consider the repository we're pulling
109 109 # *from* as authoritative, so we merge our changes into
110 110 # theirs.
111 111 if opts['switch_parent']:
112 112 firstparent, secondparent = newparent, newheads[0]
113 113 else:
114 114 firstparent, secondparent = newheads[0], newparent
115 115 ui.status(_('updating to %d:%s\n') %
116 116 (repo.changelog.rev(firstparent),
117 117 short(firstparent)))
118 118 hg.clean(repo, firstparent)
119 119 ui.status(_('merging with %d:%s\n') %
120 120 (repo.changelog.rev(secondparent), short(secondparent)))
121 121 err = hg.merge(repo, secondparent, remind=False)
122 122
123 123 if not err:
124 124 # we don't translate commit messages
125 125 message = (cmdutil.logmessage(opts) or
126 126 ('Automated merge with %s' %
127 127 url.removeauth(other.url())))
128 128 editor = cmdutil.commiteditor
129 129 if opts.get('force_editor') or opts.get('edit'):
130 130 editor = cmdutil.commitforceeditor
131 131 n = repo.commit(message, opts['user'], opts['date'], editor=editor)
132 132 ui.status(_('new changeset %d:%s merges remote changes '
133 133 'with local\n') % (repo.changelog.rev(n),
134 134 short(n)))
135 135
136 136 finally:
137 137 release(lock, wlock)
138 138
139 139 cmdtable = {
140 140 'fetch':
141 141 (fetch,
142 142 [('r', 'rev', [], _('a specific revision you would like to pull')),
143 143 ('e', 'edit', None, _('edit commit message')),
144 144 ('', 'force-editor', None, _('edit commit message (DEPRECATED)')),
145 145 ('', 'switch-parent', None, _('switch parents when merging')),
146 146 ] + commands.commitopts + commands.commitopts2 + commands.remoteopts,
147 147 _('hg fetch [SOURCE]')),
148 148 }
@@ -1,284 +1,284 b''
1 1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2, incorporated herein by reference.
4 # GNU General Public License version 2 or any later version.
5 5
6 6 '''commands to sign and verify changesets'''
7 7
8 8 import os, tempfile, binascii
9 9 from mercurial import util, commands, match
10 10 from mercurial import node as hgnode
11 11 from mercurial.i18n import _
12 12
13 13 class gpg(object):
14 14 def __init__(self, path, key=None):
15 15 self.path = path
16 16 self.key = (key and " --local-user \"%s\"" % key) or ""
17 17
18 18 def sign(self, data):
19 19 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
20 20 return util.filter(data, gpgcmd)
21 21
22 22 def verify(self, data, sig):
23 23 """ returns of the good and bad signatures"""
24 24 sigfile = datafile = None
25 25 try:
26 26 # create temporary files
27 27 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
28 28 fp = os.fdopen(fd, 'wb')
29 29 fp.write(sig)
30 30 fp.close()
31 31 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
32 32 fp = os.fdopen(fd, 'wb')
33 33 fp.write(data)
34 34 fp.close()
35 35 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
36 36 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
37 37 ret = util.filter("", gpgcmd)
38 38 finally:
39 39 for f in (sigfile, datafile):
40 40 try:
41 41 if f: os.unlink(f)
42 42 except: pass
43 43 keys = []
44 44 key, fingerprint = None, None
45 45 err = ""
46 46 for l in ret.splitlines():
47 47 # see DETAILS in the gnupg documentation
48 48 # filter the logger output
49 49 if not l.startswith("[GNUPG:]"):
50 50 continue
51 51 l = l[9:]
52 52 if l.startswith("ERRSIG"):
53 53 err = _("error while verifying signature")
54 54 break
55 55 elif l.startswith("VALIDSIG"):
56 56 # fingerprint of the primary key
57 57 fingerprint = l.split()[10]
58 58 elif (l.startswith("GOODSIG") or
59 59 l.startswith("EXPSIG") or
60 60 l.startswith("EXPKEYSIG") or
61 61 l.startswith("BADSIG")):
62 62 if key is not None:
63 63 keys.append(key + [fingerprint])
64 64 key = l.split(" ", 2)
65 65 fingerprint = None
66 66 if err:
67 67 return err, []
68 68 if key is not None:
69 69 keys.append(key + [fingerprint])
70 70 return err, keys
71 71
72 72 def newgpg(ui, **opts):
73 73 """create a new gpg instance"""
74 74 gpgpath = ui.config("gpg", "cmd", "gpg")
75 75 gpgkey = opts.get('key')
76 76 if not gpgkey:
77 77 gpgkey = ui.config("gpg", "key", None)
78 78 return gpg(gpgpath, gpgkey)
79 79
80 80 def sigwalk(repo):
81 81 """
82 82 walk over every sigs, yields a couple
83 83 ((node, version, sig), (filename, linenumber))
84 84 """
85 85 def parsefile(fileiter, context):
86 86 ln = 1
87 87 for l in fileiter:
88 88 if not l:
89 89 continue
90 90 yield (l.split(" ", 2), (context, ln))
91 91 ln +=1
92 92
93 93 # read the heads
94 94 fl = repo.file(".hgsigs")
95 95 for r in reversed(fl.heads()):
96 96 fn = ".hgsigs|%s" % hgnode.short(r)
97 97 for item in parsefile(fl.read(r).splitlines(), fn):
98 98 yield item
99 99 try:
100 100 # read local signatures
101 101 fn = "localsigs"
102 102 for item in parsefile(repo.opener(fn), fn):
103 103 yield item
104 104 except IOError:
105 105 pass
106 106
107 107 def getkeys(ui, repo, mygpg, sigdata, context):
108 108 """get the keys who signed a data"""
109 109 fn, ln = context
110 110 node, version, sig = sigdata
111 111 prefix = "%s:%d" % (fn, ln)
112 112 node = hgnode.bin(node)
113 113
114 114 data = node2txt(repo, node, version)
115 115 sig = binascii.a2b_base64(sig)
116 116 err, keys = mygpg.verify(data, sig)
117 117 if err:
118 118 ui.warn("%s:%d %s\n" % (fn, ln , err))
119 119 return None
120 120
121 121 validkeys = []
122 122 # warn for expired key and/or sigs
123 123 for key in keys:
124 124 if key[0] == "BADSIG":
125 125 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
126 126 continue
127 127 if key[0] == "EXPSIG":
128 128 ui.write(_("%s Note: Signature has expired"
129 129 " (signed by: \"%s\")\n") % (prefix, key[2]))
130 130 elif key[0] == "EXPKEYSIG":
131 131 ui.write(_("%s Note: This key has expired"
132 132 " (signed by: \"%s\")\n") % (prefix, key[2]))
133 133 validkeys.append((key[1], key[2], key[3]))
134 134 return validkeys
135 135
136 136 def sigs(ui, repo):
137 137 """list signed changesets"""
138 138 mygpg = newgpg(ui)
139 139 revs = {}
140 140
141 141 for data, context in sigwalk(repo):
142 142 node, version, sig = data
143 143 fn, ln = context
144 144 try:
145 145 n = repo.lookup(node)
146 146 except KeyError:
147 147 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
148 148 continue
149 149 r = repo.changelog.rev(n)
150 150 keys = getkeys(ui, repo, mygpg, data, context)
151 151 if not keys:
152 152 continue
153 153 revs.setdefault(r, [])
154 154 revs[r].extend(keys)
155 155 for rev in sorted(revs, reverse=True):
156 156 for k in revs[rev]:
157 157 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
158 158 ui.write("%-30s %s\n" % (keystr(ui, k), r))
159 159
160 160 def check(ui, repo, rev):
161 161 """verify all the signatures there may be for a particular revision"""
162 162 mygpg = newgpg(ui)
163 163 rev = repo.lookup(rev)
164 164 hexrev = hgnode.hex(rev)
165 165 keys = []
166 166
167 167 for data, context in sigwalk(repo):
168 168 node, version, sig = data
169 169 if node == hexrev:
170 170 k = getkeys(ui, repo, mygpg, data, context)
171 171 if k:
172 172 keys.extend(k)
173 173
174 174 if not keys:
175 175 ui.write(_("No valid signature for %s\n") % hgnode.short(rev))
176 176 return
177 177
178 178 # print summary
179 179 ui.write("%s is signed by:\n" % hgnode.short(rev))
180 180 for key in keys:
181 181 ui.write(" %s\n" % keystr(ui, key))
182 182
183 183 def keystr(ui, key):
184 184 """associate a string to a key (username, comment)"""
185 185 keyid, user, fingerprint = key
186 186 comment = ui.config("gpg", fingerprint, None)
187 187 if comment:
188 188 return "%s (%s)" % (user, comment)
189 189 else:
190 190 return user
191 191
192 192 def sign(ui, repo, *revs, **opts):
193 193 """add a signature for the current or given revision
194 194
195 195 If no revision is given, the parent of the working directory is used,
196 196 or tip if no revision is checked out.
197 197
198 198 See 'hg help dates' for a list of formats valid for -d/--date.
199 199 """
200 200
201 201 mygpg = newgpg(ui, **opts)
202 202 sigver = "0"
203 203 sigmessage = ""
204 204
205 205 date = opts.get('date')
206 206 if date:
207 207 opts['date'] = util.parsedate(date)
208 208
209 209 if revs:
210 210 nodes = [repo.lookup(n) for n in revs]
211 211 else:
212 212 nodes = [node for node in repo.dirstate.parents()
213 213 if node != hgnode.nullid]
214 214 if len(nodes) > 1:
215 215 raise util.Abort(_('uncommitted merge - please provide a '
216 216 'specific revision'))
217 217 if not nodes:
218 218 nodes = [repo.changelog.tip()]
219 219
220 220 for n in nodes:
221 221 hexnode = hgnode.hex(n)
222 222 ui.write("Signing %d:%s\n" % (repo.changelog.rev(n),
223 223 hgnode.short(n)))
224 224 # build data
225 225 data = node2txt(repo, n, sigver)
226 226 sig = mygpg.sign(data)
227 227 if not sig:
228 228 raise util.Abort(_("Error while signing"))
229 229 sig = binascii.b2a_base64(sig)
230 230 sig = sig.replace("\n", "")
231 231 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
232 232
233 233 # write it
234 234 if opts['local']:
235 235 repo.opener("localsigs", "ab").write(sigmessage)
236 236 return
237 237
238 238 for x in repo.status(unknown=True)[:5]:
239 239 if ".hgsigs" in x and not opts["force"]:
240 240 raise util.Abort(_("working copy of .hgsigs is changed "
241 241 "(please commit .hgsigs manually "
242 242 "or use --force)"))
243 243
244 244 repo.wfile(".hgsigs", "ab").write(sigmessage)
245 245
246 246 if '.hgsigs' not in repo.dirstate:
247 247 repo.add([".hgsigs"])
248 248
249 249 if opts["no_commit"]:
250 250 return
251 251
252 252 message = opts['message']
253 253 if not message:
254 254 # we don't translate commit messages
255 255 message = "\n".join(["Added signature for changeset %s"
256 256 % hgnode.short(n)
257 257 for n in nodes])
258 258 try:
259 259 m = match.exact(repo.root, '', ['.hgsigs'])
260 260 repo.commit(message, opts['user'], opts['date'], match=m)
261 261 except ValueError, inst:
262 262 raise util.Abort(str(inst))
263 263
264 264 def node2txt(repo, node, ver):
265 265 """map a manifest into some text"""
266 266 if ver == "0":
267 267 return "%s\n" % hgnode.hex(node)
268 268 else:
269 269 raise util.Abort(_("unknown signature version"))
270 270
271 271 cmdtable = {
272 272 "sign":
273 273 (sign,
274 274 [('l', 'local', None, _('make the signature local')),
275 275 ('f', 'force', None, _('sign even if the sigfile is modified')),
276 276 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
277 277 ('k', 'key', '', _('the key id to sign with')),
278 278 ('m', 'message', '', _('commit message')),
279 279 ] + commands.commitopts2,
280 280 _('hg sign [OPTION]... [REVISION]...')),
281 281 "sigcheck": (check, [], _('hg sigcheck REVISION')),
282 282 "sigs": (sigs, [], _('hg sigs')),
283 283 }
284 284
@@ -1,376 +1,376 b''
1 1 # ASCII graph log extension for Mercurial
2 2 #
3 3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to view revision graphs from a shell
9 9
10 10 This extension adds a --graph option to the incoming, outgoing and log
11 11 commands. When this options is given, an ASCII representation of the
12 12 revision graph is also shown.
13 13 '''
14 14
15 15 import os, sys
16 16 from mercurial.cmdutil import revrange, show_changeset
17 17 from mercurial.commands import templateopts
18 18 from mercurial.i18n import _
19 19 from mercurial.node import nullrev
20 20 from mercurial import bundlerepo, changegroup, cmdutil, commands, extensions
21 21 from mercurial import hg, url, util, graphmod
22 22
23 23 ASCIIDATA = 'ASC'
24 24
25 25 def asciiedges(seen, rev, parents):
26 26 """adds edge info to changelog DAG walk suitable for ascii()"""
27 27 if rev not in seen:
28 28 seen.append(rev)
29 29 nodeidx = seen.index(rev)
30 30
31 31 knownparents = []
32 32 newparents = []
33 33 for parent in parents:
34 34 if parent in seen:
35 35 knownparents.append(parent)
36 36 else:
37 37 newparents.append(parent)
38 38
39 39 ncols = len(seen)
40 40 seen[nodeidx:nodeidx + 1] = newparents
41 41 edges = [(nodeidx, seen.index(p)) for p in knownparents]
42 42
43 43 if len(newparents) > 0:
44 44 edges.append((nodeidx, nodeidx))
45 45 if len(newparents) > 1:
46 46 edges.append((nodeidx, nodeidx + 1))
47 47
48 48 nmorecols = len(seen) - ncols
49 49 return nodeidx, edges, ncols, nmorecols
50 50
51 51 def fix_long_right_edges(edges):
52 52 for (i, (start, end)) in enumerate(edges):
53 53 if end > start:
54 54 edges[i] = (start, end + 1)
55 55
56 56 def get_nodeline_edges_tail(
57 57 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
58 58 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
59 59 # Still going in the same non-vertical direction.
60 60 if n_columns_diff == -1:
61 61 start = max(node_index + 1, p_node_index)
62 62 tail = ["|", " "] * (start - node_index - 1)
63 63 tail.extend(["/", " "] * (n_columns - start))
64 64 return tail
65 65 else:
66 66 return ["\\", " "] * (n_columns - node_index - 1)
67 67 else:
68 68 return ["|", " "] * (n_columns - node_index - 1)
69 69
70 70 def draw_edges(edges, nodeline, interline):
71 71 for (start, end) in edges:
72 72 if start == end + 1:
73 73 interline[2 * end + 1] = "/"
74 74 elif start == end - 1:
75 75 interline[2 * start + 1] = "\\"
76 76 elif start == end:
77 77 interline[2 * start] = "|"
78 78 else:
79 79 nodeline[2 * end] = "+"
80 80 if start > end:
81 81 (start, end) = (end, start)
82 82 for i in range(2 * start + 1, 2 * end):
83 83 if nodeline[i] != "+":
84 84 nodeline[i] = "-"
85 85
86 86 def get_padding_line(ni, n_columns, edges):
87 87 line = []
88 88 line.extend(["|", " "] * ni)
89 89 if (ni, ni - 1) in edges or (ni, ni) in edges:
90 90 # (ni, ni - 1) (ni, ni)
91 91 # | | | | | | | |
92 92 # +---o | | o---+
93 93 # | | c | | c | |
94 94 # | |/ / | |/ /
95 95 # | | | | | |
96 96 c = "|"
97 97 else:
98 98 c = " "
99 99 line.extend([c, " "])
100 100 line.extend(["|", " "] * (n_columns - ni - 1))
101 101 return line
102 102
103 103 def asciistate():
104 104 """returns the initial value for the "state" argument to ascii()"""
105 105 return [0, 0]
106 106
107 107 def ascii(ui, state, type, char, text, coldata):
108 108 """prints an ASCII graph of the DAG
109 109
110 110 takes the following arguments (one call per node in the graph):
111 111
112 112 - ui to write to
113 113 - Somewhere to keep the needed state in (init to asciistate())
114 114 - Column of the current node in the set of ongoing edges.
115 115 - Type indicator of node data == ASCIIDATA.
116 116 - Payload: (char, lines):
117 117 - Character to use as node's symbol.
118 118 - List of lines to display as the node's text.
119 119 - Edges; a list of (col, next_col) indicating the edges between
120 120 the current node and its parents.
121 121 - Number of columns (ongoing edges) in the current revision.
122 122 - The difference between the number of columns (ongoing edges)
123 123 in the next revision and the number of columns (ongoing edges)
124 124 in the current revision. That is: -1 means one column removed;
125 125 0 means no columns added or removed; 1 means one column added.
126 126 """
127 127
128 128 idx, edges, ncols, coldiff = coldata
129 129 assert -2 < coldiff < 2
130 130 if coldiff == -1:
131 131 # Transform
132 132 #
133 133 # | | | | | |
134 134 # o | | into o---+
135 135 # |X / |/ /
136 136 # | | | |
137 137 fix_long_right_edges(edges)
138 138
139 139 # add_padding_line says whether to rewrite
140 140 #
141 141 # | | | | | | | |
142 142 # | o---+ into | o---+
143 143 # | / / | | | # <--- padding line
144 144 # o | | | / /
145 145 # o | |
146 146 add_padding_line = (len(text) > 2 and coldiff == -1 and
147 147 [x for (x, y) in edges if x + 1 < y])
148 148
149 149 # fix_nodeline_tail says whether to rewrite
150 150 #
151 151 # | | o | | | | o | |
152 152 # | | |/ / | | |/ /
153 153 # | o | | into | o / / # <--- fixed nodeline tail
154 154 # | |/ / | |/ /
155 155 # o | | o | |
156 156 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
157 157
158 158 # nodeline is the line containing the node character (typically o)
159 159 nodeline = ["|", " "] * idx
160 160 nodeline.extend([char, " "])
161 161
162 162 nodeline.extend(
163 163 get_nodeline_edges_tail(idx, state[1], ncols, coldiff,
164 164 state[0], fix_nodeline_tail))
165 165
166 166 # shift_interline is the line containing the non-vertical
167 167 # edges between this entry and the next
168 168 shift_interline = ["|", " "] * idx
169 169 if coldiff == -1:
170 170 n_spaces = 1
171 171 edge_ch = "/"
172 172 elif coldiff == 0:
173 173 n_spaces = 2
174 174 edge_ch = "|"
175 175 else:
176 176 n_spaces = 3
177 177 edge_ch = "\\"
178 178 shift_interline.extend(n_spaces * [" "])
179 179 shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
180 180
181 181 # draw edges from the current node to its parents
182 182 draw_edges(edges, nodeline, shift_interline)
183 183
184 184 # lines is the list of all graph lines to print
185 185 lines = [nodeline]
186 186 if add_padding_line:
187 187 lines.append(get_padding_line(idx, ncols, edges))
188 188 lines.append(shift_interline)
189 189
190 190 # make sure that there are as many graph lines as there are
191 191 # log strings
192 192 while len(text) < len(lines):
193 193 text.append("")
194 194 if len(lines) < len(text):
195 195 extra_interline = ["|", " "] * (ncols + coldiff)
196 196 while len(lines) < len(text):
197 197 lines.append(extra_interline)
198 198
199 199 # print lines
200 200 indentation_level = max(ncols, ncols + coldiff)
201 201 for (line, logstr) in zip(lines, text):
202 202 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
203 203 ui.write(ln.rstrip() + '\n')
204 204
205 205 # ... and start over
206 206 state[0] = coldiff
207 207 state[1] = idx
208 208
209 209 def get_revs(repo, rev_opt):
210 210 if rev_opt:
211 211 revs = revrange(repo, rev_opt)
212 212 return (max(revs), min(revs))
213 213 else:
214 214 return (len(repo) - 1, 0)
215 215
216 216 def check_unsupported_flags(opts):
217 217 for op in ["follow", "follow_first", "date", "copies", "keyword", "remove",
218 218 "only_merges", "user", "only_branch", "prune", "newest_first",
219 219 "no_merges", "include", "exclude"]:
220 220 if op in opts and opts[op]:
221 221 raise util.Abort(_("--graph option is incompatible with --%s")
222 222 % op.replace("_", "-"))
223 223
224 224 def generate(ui, dag, displayer, showparents, edgefn):
225 225 seen, state = [], asciistate()
226 226 for rev, type, ctx, parents in dag:
227 227 char = ctx.node() in showparents and '@' or 'o'
228 228 displayer.show(ctx)
229 229 lines = displayer.hunk.pop(rev).split('\n')[:-1]
230 230 ascii(ui, state, type, char, lines, edgefn(seen, rev, parents))
231 231
232 232 def graphlog(ui, repo, path=None, **opts):
233 233 """show revision history alongside an ASCII revision graph
234 234
235 235 Print a revision history alongside a revision graph drawn with
236 236 ASCII characters.
237 237
238 238 Nodes printed as an @ character are parents of the working
239 239 directory.
240 240 """
241 241
242 242 check_unsupported_flags(opts)
243 243 limit = cmdutil.loglimit(opts)
244 244 start, stop = get_revs(repo, opts["rev"])
245 245 if start == nullrev:
246 246 return
247 247
248 248 if path:
249 249 path = util.canonpath(repo.root, os.getcwd(), path)
250 250 if path: # could be reset in canonpath
251 251 revdag = graphmod.filerevs(repo, path, start, stop, limit)
252 252 else:
253 253 if limit is not None:
254 254 stop = max(stop, start - limit + 1)
255 255 revdag = graphmod.revisions(repo, start, stop)
256 256
257 257 displayer = show_changeset(ui, repo, opts, buffered=True)
258 258 showparents = [ctx.node() for ctx in repo[None].parents()]
259 259 generate(ui, revdag, displayer, showparents, asciiedges)
260 260
261 261 def graphrevs(repo, nodes, opts):
262 262 limit = cmdutil.loglimit(opts)
263 263 nodes.reverse()
264 264 if limit is not None:
265 265 nodes = nodes[:limit]
266 266 return graphmod.nodes(repo, nodes)
267 267
268 268 def goutgoing(ui, repo, dest=None, **opts):
269 269 """show the outgoing changesets alongside an ASCII revision graph
270 270
271 271 Print the outgoing changesets alongside a revision graph drawn with
272 272 ASCII characters.
273 273
274 274 Nodes printed as an @ character are parents of the working
275 275 directory.
276 276 """
277 277
278 278 check_unsupported_flags(opts)
279 279 dest, revs, checkout = hg.parseurl(
280 280 ui.expandpath(dest or 'default-push', dest or 'default'),
281 281 opts.get('rev'))
282 282 if revs:
283 283 revs = [repo.lookup(rev) for rev in revs]
284 284 other = hg.repository(cmdutil.remoteui(ui, opts), dest)
285 285 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
286 286 o = repo.findoutgoing(other, force=opts.get('force'))
287 287 if not o:
288 288 ui.status(_("no changes found\n"))
289 289 return
290 290
291 291 o = repo.changelog.nodesbetween(o, revs)[0]
292 292 revdag = graphrevs(repo, o, opts)
293 293 displayer = show_changeset(ui, repo, opts, buffered=True)
294 294 showparents = [ctx.node() for ctx in repo[None].parents()]
295 295 generate(ui, revdag, displayer, showparents, asciiedges)
296 296
297 297 def gincoming(ui, repo, source="default", **opts):
298 298 """show the incoming changesets alongside an ASCII revision graph
299 299
300 300 Print the incoming changesets alongside a revision graph drawn with
301 301 ASCII characters.
302 302
303 303 Nodes printed as an @ character are parents of the working
304 304 directory.
305 305 """
306 306
307 307 check_unsupported_flags(opts)
308 308 source, revs, checkout = hg.parseurl(ui.expandpath(source), opts.get('rev'))
309 309 other = hg.repository(cmdutil.remoteui(repo, opts), source)
310 310 ui.status(_('comparing with %s\n') % url.hidepassword(source))
311 311 if revs:
312 312 revs = [other.lookup(rev) for rev in revs]
313 313 incoming = repo.findincoming(other, heads=revs, force=opts["force"])
314 314 if not incoming:
315 315 try:
316 316 os.unlink(opts["bundle"])
317 317 except:
318 318 pass
319 319 ui.status(_("no changes found\n"))
320 320 return
321 321
322 322 cleanup = None
323 323 try:
324 324
325 325 fname = opts["bundle"]
326 326 if fname or not other.local():
327 327 # create a bundle (uncompressed if other repo is not local)
328 328 if revs is None:
329 329 cg = other.changegroup(incoming, "incoming")
330 330 else:
331 331 cg = other.changegroupsubset(incoming, revs, 'incoming')
332 332 bundletype = other.local() and "HG10BZ" or "HG10UN"
333 333 fname = cleanup = changegroup.writebundle(cg, fname, bundletype)
334 334 # keep written bundle?
335 335 if opts["bundle"]:
336 336 cleanup = None
337 337 if not other.local():
338 338 # use the created uncompressed bundlerepo
339 339 other = bundlerepo.bundlerepository(ui, repo.root, fname)
340 340
341 341 chlist = other.changelog.nodesbetween(incoming, revs)[0]
342 342 revdag = graphrevs(other, chlist, opts)
343 343 displayer = show_changeset(ui, other, opts, buffered=True)
344 344 showparents = [ctx.node() for ctx in repo[None].parents()]
345 345 generate(ui, revdag, displayer, showparents, asciiedges)
346 346
347 347 finally:
348 348 if hasattr(other, 'close'):
349 349 other.close()
350 350 if cleanup:
351 351 os.unlink(cleanup)
352 352
353 353 def uisetup(ui):
354 354 '''Initialize the extension.'''
355 355 _wrapcmd(ui, 'log', commands.table, graphlog)
356 356 _wrapcmd(ui, 'incoming', commands.table, gincoming)
357 357 _wrapcmd(ui, 'outgoing', commands.table, goutgoing)
358 358
359 359 def _wrapcmd(ui, cmd, table, wrapfn):
360 360 '''wrap the command'''
361 361 def graph(orig, *args, **kwargs):
362 362 if kwargs['graph']:
363 363 return wrapfn(*args, **kwargs)
364 364 return orig(*args, **kwargs)
365 365 entry = extensions.wrapcommand(table, cmd, graph)
366 366 entry[1].append(('G', 'graph', None, _("show the revision DAG")))
367 367
368 368 cmdtable = {
369 369 "glog":
370 370 (graphlog,
371 371 [('l', 'limit', '', _('limit number of changes displayed')),
372 372 ('p', 'patch', False, _('show patch')),
373 373 ('r', 'rev', [], _('show the specified revision or range')),
374 374 ] + templateopts,
375 375 _('hg glog [OPTION]... [FILE]')),
376 376 }
@@ -1,246 +1,248 b''
1 1 # Copyright (C) 2007-8 Brendan Cully <brendan@kublai.com>
2 # Published under the GNU GPL
2 #
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
3 5
4 6 """hooks for integrating with the CIA.vc notification service
5 7
6 8 This is meant to be run as a changegroup or incoming hook. To
7 9 configure it, set the following options in your hgrc::
8 10
9 11 [cia]
10 12 # your registered CIA user name
11 13 user = foo
12 14 # the name of the project in CIA
13 15 project = foo
14 16 # the module (subproject) (optional)
15 17 #module = foo
16 18 # Append a diffstat to the log message (optional)
17 19 #diffstat = False
18 20 # Template to use for log messages (optional)
19 21 #template = {desc}\\n{baseurl}/rev/{node}-- {diffstat}
20 22 # Style to use (optional)
21 23 #style = foo
22 24 # The URL of the CIA notification service (optional)
23 25 # You can use mailto: URLs to send by email, eg
24 26 # mailto:cia@cia.vc
25 27 # Make sure to set email.from if you do this.
26 28 #url = http://cia.vc/
27 29 # print message instead of sending it (optional)
28 30 #test = False
29 31
30 32 [hooks]
31 33 # one of these:
32 34 changegroup.cia = python:hgcia.hook
33 35 #incoming.cia = python:hgcia.hook
34 36
35 37 [web]
36 38 # If you want hyperlinks (optional)
37 39 baseurl = http://server/path/to/repo
38 40 """
39 41
40 42 from mercurial.i18n import _
41 43 from mercurial.node import *
42 44 from mercurial import cmdutil, patch, templater, util, mail
43 45 import email.Parser
44 46
45 47 import xmlrpclib
46 48 from xml.sax import saxutils
47 49
48 50 socket_timeout = 30 # seconds
49 51 try:
50 52 # set a timeout for the socket so you don't have to wait so looooong
51 53 # when cia.vc is having problems. requires python >= 2.3:
52 54 import socket
53 55 socket.setdefaulttimeout(socket_timeout)
54 56 except:
55 57 pass
56 58
57 59 HGCIA_VERSION = '0.1'
58 60 HGCIA_URL = 'http://hg.kublai.com/mercurial/hgcia'
59 61
60 62
61 63 class ciamsg(object):
62 64 """ A CIA message """
63 65 def __init__(self, cia, ctx):
64 66 self.cia = cia
65 67 self.ctx = ctx
66 68 self.url = self.cia.url
67 69
68 70 def fileelem(self, path, uri, action):
69 71 if uri:
70 72 uri = ' uri=%s' % saxutils.quoteattr(uri)
71 73 return '<file%s action=%s>%s</file>' % (
72 74 uri, saxutils.quoteattr(action), saxutils.escape(path))
73 75
74 76 def fileelems(self):
75 77 n = self.ctx.node()
76 78 f = self.cia.repo.status(self.ctx.parents()[0].node(), n)
77 79 url = self.url or ''
78 80 elems = []
79 81 for path in f[0]:
80 82 uri = '%s/diff/%s/%s' % (url, short(n), path)
81 83 elems.append(self.fileelem(path, url and uri, 'modify'))
82 84 for path in f[1]:
83 85 # TODO: copy/rename ?
84 86 uri = '%s/file/%s/%s' % (url, short(n), path)
85 87 elems.append(self.fileelem(path, url and uri, 'add'))
86 88 for path in f[2]:
87 89 elems.append(self.fileelem(path, '', 'remove'))
88 90
89 91 return '\n'.join(elems)
90 92
91 93 def sourceelem(self, project, module=None, branch=None):
92 94 msg = ['<source>', '<project>%s</project>' % saxutils.escape(project)]
93 95 if module:
94 96 msg.append('<module>%s</module>' % saxutils.escape(module))
95 97 if branch:
96 98 msg.append('<branch>%s</branch>' % saxutils.escape(branch))
97 99 msg.append('</source>')
98 100
99 101 return '\n'.join(msg)
100 102
101 103 def diffstat(self):
102 104 class patchbuf(object):
103 105 def __init__(self):
104 106 self.lines = []
105 107 # diffstat is stupid
106 108 self.name = 'cia'
107 109 def write(self, data):
108 110 self.lines.append(data)
109 111 def close(self):
110 112 pass
111 113
112 114 n = self.ctx.node()
113 115 pbuf = patchbuf()
114 116 patch.export(self.cia.repo, [n], fp=pbuf)
115 117 return patch.diffstat(pbuf.lines) or ''
116 118
117 119 def logmsg(self):
118 120 diffstat = self.cia.diffstat and self.diffstat() or ''
119 121 self.cia.ui.pushbuffer()
120 122 self.cia.templater.show(self.ctx, changes=self.ctx.changeset(),
121 123 url=self.cia.url, diffstat=diffstat)
122 124 return self.cia.ui.popbuffer()
123 125
124 126 def xml(self):
125 127 n = short(self.ctx.node())
126 128 src = self.sourceelem(self.cia.project, module=self.cia.module,
127 129 branch=self.ctx.branch())
128 130 # unix timestamp
129 131 dt = self.ctx.date()
130 132 timestamp = dt[0]
131 133
132 134 author = saxutils.escape(self.ctx.user())
133 135 rev = '%d:%s' % (self.ctx.rev(), n)
134 136 log = saxutils.escape(self.logmsg())
135 137
136 138 url = self.url and '<url>%s/rev/%s</url>' % (saxutils.escape(self.url),
137 139 n) or ''
138 140
139 141 msg = """
140 142 <message>
141 143 <generator>
142 144 <name>Mercurial (hgcia)</name>
143 145 <version>%s</version>
144 146 <url>%s</url>
145 147 <user>%s</user>
146 148 </generator>
147 149 %s
148 150 <body>
149 151 <commit>
150 152 <author>%s</author>
151 153 <version>%s</version>
152 154 <log>%s</log>
153 155 %s
154 156 <files>%s</files>
155 157 </commit>
156 158 </body>
157 159 <timestamp>%d</timestamp>
158 160 </message>
159 161 """ % \
160 162 (HGCIA_VERSION, saxutils.escape(HGCIA_URL),
161 163 saxutils.escape(self.cia.user), src, author, rev, log, url,
162 164 self.fileelems(), timestamp)
163 165
164 166 return msg
165 167
166 168
167 169 class hgcia(object):
168 170 """ CIA notification class """
169 171
170 172 deftemplate = '{desc}'
171 173 dstemplate = '{desc}\n-- \n{diffstat}'
172 174
173 175 def __init__(self, ui, repo):
174 176 self.ui = ui
175 177 self.repo = repo
176 178
177 179 self.ciaurl = self.ui.config('cia', 'url', 'http://cia.vc')
178 180 self.user = self.ui.config('cia', 'user')
179 181 self.project = self.ui.config('cia', 'project')
180 182 self.module = self.ui.config('cia', 'module')
181 183 self.diffstat = self.ui.configbool('cia', 'diffstat')
182 184 self.emailfrom = self.ui.config('email', 'from')
183 185 self.dryrun = self.ui.configbool('cia', 'test')
184 186 self.url = self.ui.config('web', 'baseurl')
185 187
186 188 style = self.ui.config('cia', 'style')
187 189 template = self.ui.config('cia', 'template')
188 190 if not template:
189 191 template = self.diffstat and self.dstemplate or self.deftemplate
190 192 template = templater.parsestring(template, quoted=False)
191 193 t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
192 194 style, False)
193 195 t.use_template(template)
194 196 self.templater = t
195 197
196 198 def sendrpc(self, msg):
197 199 srv = xmlrpclib.Server(self.ciaurl)
198 200 srv.hub.deliver(msg)
199 201
200 202 def sendemail(self, address, data):
201 203 p = email.Parser.Parser()
202 204 msg = p.parsestr(data)
203 205 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
204 206 msg['To'] = address
205 207 msg['From'] = self.emailfrom
206 208 msg['Subject'] = 'DeliverXML'
207 209 msg['Content-type'] = 'text/xml'
208 210 msgtext = msg.as_string()
209 211
210 212 self.ui.status(_('hgcia: sending update to %s\n') % address)
211 213 mail.sendmail(self.ui, util.email(self.emailfrom),
212 214 [address], msgtext)
213 215
214 216
215 217 def hook(ui, repo, hooktype, node=None, url=None, **kwargs):
216 218 """ send CIA notification """
217 219 def sendmsg(cia, ctx):
218 220 msg = ciamsg(cia, ctx).xml()
219 221 if cia.dryrun:
220 222 ui.write(msg)
221 223 elif cia.ciaurl.startswith('mailto:'):
222 224 if not cia.emailfrom:
223 225 raise util.Abort(_('email.from must be defined when '
224 226 'sending by email'))
225 227 cia.sendemail(cia.ciaurl[7:], msg)
226 228 else:
227 229 cia.sendrpc(msg)
228 230
229 231 n = bin(node)
230 232 cia = hgcia(ui, repo)
231 233 if not cia.user:
232 234 ui.debug('cia: no user specified')
233 235 return
234 236 if not cia.project:
235 237 ui.debug('cia: no project specified')
236 238 return
237 239 if hooktype == 'changegroup':
238 240 start = repo.changelog.rev(n)
239 241 end = len(repo.changelog)
240 242 for rev in xrange(start, end):
241 243 n = repo.changelog.node(rev)
242 244 ctx = repo.changectx(n)
243 245 sendmsg(cia, ctx)
244 246 else:
245 247 ctx = repo.changectx(n)
246 248 sendmsg(cia, ctx)
@@ -1,347 +1,347 b''
1 1 # Minimal support for git commands on an hg repository
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''browse the repository in a graphical way
9 9
10 10 The hgk extension allows browsing the history of a repository in a
11 11 graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is not
12 12 distributed with Mercurial.)
13 13
14 14 hgk consists of two parts: a Tcl script that does the displaying and
15 15 querying of information, and an extension to Mercurial named hgk.py,
16 16 which provides hooks for hgk to get information. hgk can be found in
17 17 the contrib directory, and the extension is shipped in the hgext
18 18 repository, and needs to be enabled.
19 19
20 20 The hg view command will launch the hgk Tcl script. For this command
21 21 to work, hgk must be in your search path. Alternately, you can specify
22 22 the path to hgk in your .hgrc file::
23 23
24 24 [hgk]
25 25 path=/location/of/hgk
26 26
27 27 hgk can make use of the extdiff extension to visualize revisions.
28 28 Assuming you had already configured extdiff vdiff command, just add::
29 29
30 30 [hgk]
31 31 vdiff=vdiff
32 32
33 33 Revisions context menu will now display additional entries to fire
34 34 vdiff on hovered and selected revisions.
35 35 '''
36 36
37 37 import os
38 38 from mercurial import commands, util, patch, revlog, cmdutil
39 39 from mercurial.node import nullid, nullrev, short
40 40 from mercurial.i18n import _
41 41
42 42 def difftree(ui, repo, node1=None, node2=None, *files, **opts):
43 43 """diff trees from two commits"""
44 44 def __difftree(repo, node1, node2, files=[]):
45 45 assert node2 is not None
46 46 mmap = repo[node1].manifest()
47 47 mmap2 = repo[node2].manifest()
48 48 m = cmdutil.match(repo, files)
49 49 modified, added, removed = repo.status(node1, node2, m)[:3]
50 50 empty = short(nullid)
51 51
52 52 for f in modified:
53 53 # TODO get file permissions
54 54 ui.write(":100664 100664 %s %s M\t%s\t%s\n" %
55 55 (short(mmap[f]), short(mmap2[f]), f, f))
56 56 for f in added:
57 57 ui.write(":000000 100664 %s %s N\t%s\t%s\n" %
58 58 (empty, short(mmap2[f]), f, f))
59 59 for f in removed:
60 60 ui.write(":100664 000000 %s %s D\t%s\t%s\n" %
61 61 (short(mmap[f]), empty, f, f))
62 62 ##
63 63
64 64 while True:
65 65 if opts['stdin']:
66 66 try:
67 67 line = raw_input().split(' ')
68 68 node1 = line[0]
69 69 if len(line) > 1:
70 70 node2 = line[1]
71 71 else:
72 72 node2 = None
73 73 except EOFError:
74 74 break
75 75 node1 = repo.lookup(node1)
76 76 if node2:
77 77 node2 = repo.lookup(node2)
78 78 else:
79 79 node2 = node1
80 80 node1 = repo.changelog.parents(node1)[0]
81 81 if opts['patch']:
82 82 if opts['pretty']:
83 83 catcommit(ui, repo, node2, "")
84 84 m = cmdutil.match(repo, files)
85 85 chunks = patch.diff(repo, node1, node2, match=m,
86 86 opts=patch.diffopts(ui, {'git': True}))
87 87 for chunk in chunks:
88 88 ui.write(chunk)
89 89 else:
90 90 __difftree(repo, node1, node2, files=files)
91 91 if not opts['stdin']:
92 92 break
93 93
94 94 def catcommit(ui, repo, n, prefix, ctx=None):
95 95 nlprefix = '\n' + prefix;
96 96 if ctx is None:
97 97 ctx = repo[n]
98 98 ui.write("tree %s\n" % short(ctx.changeset()[0])) # use ctx.node() instead ??
99 99 for p in ctx.parents():
100 100 ui.write("parent %s\n" % p)
101 101
102 102 date = ctx.date()
103 103 description = ctx.description().replace("\0", "")
104 104 lines = description.splitlines()
105 105 if lines and lines[-1].startswith('committer:'):
106 106 committer = lines[-1].split(': ')[1].rstrip()
107 107 else:
108 108 committer = ctx.user()
109 109
110 110 ui.write("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1]))
111 111 ui.write("committer %s %s %s\n" % (committer, int(date[0]), date[1]))
112 112 ui.write("revision %d\n" % ctx.rev())
113 113 ui.write("branch %s\n\n" % ctx.branch())
114 114
115 115 if prefix != "":
116 116 ui.write("%s%s\n" % (prefix, description.replace('\n', nlprefix).strip()))
117 117 else:
118 118 ui.write(description + "\n")
119 119 if prefix:
120 120 ui.write('\0')
121 121
122 122 def base(ui, repo, node1, node2):
123 123 """output common ancestor information"""
124 124 node1 = repo.lookup(node1)
125 125 node2 = repo.lookup(node2)
126 126 n = repo.changelog.ancestor(node1, node2)
127 127 ui.write(short(n) + "\n")
128 128
129 129 def catfile(ui, repo, type=None, r=None, **opts):
130 130 """cat a specific revision"""
131 131 # in stdin mode, every line except the commit is prefixed with two
132 132 # spaces. This way the our caller can find the commit without magic
133 133 # strings
134 134 #
135 135 prefix = ""
136 136 if opts['stdin']:
137 137 try:
138 138 (type, r) = raw_input().split(' ');
139 139 prefix = " "
140 140 except EOFError:
141 141 return
142 142
143 143 else:
144 144 if not type or not r:
145 145 ui.warn(_("cat-file: type or revision not supplied\n"))
146 146 commands.help_(ui, 'cat-file')
147 147
148 148 while r:
149 149 if type != "commit":
150 150 ui.warn(_("aborting hg cat-file only understands commits\n"))
151 151 return 1;
152 152 n = repo.lookup(r)
153 153 catcommit(ui, repo, n, prefix)
154 154 if opts['stdin']:
155 155 try:
156 156 (type, r) = raw_input().split(' ');
157 157 except EOFError:
158 158 break
159 159 else:
160 160 break
161 161
162 162 # git rev-tree is a confusing thing. You can supply a number of
163 163 # commit sha1s on the command line, and it walks the commit history
164 164 # telling you which commits are reachable from the supplied ones via
165 165 # a bitmask based on arg position.
166 166 # you can specify a commit to stop at by starting the sha1 with ^
167 167 def revtree(ui, args, repo, full="tree", maxnr=0, parents=False):
168 168 def chlogwalk():
169 169 count = len(repo)
170 170 i = count
171 171 l = [0] * 100
172 172 chunk = 100
173 173 while True:
174 174 if chunk > i:
175 175 chunk = i
176 176 i = 0
177 177 else:
178 178 i -= chunk
179 179
180 180 for x in xrange(chunk):
181 181 if i + x >= count:
182 182 l[chunk - x:] = [0] * (chunk - x)
183 183 break
184 184 if full != None:
185 185 l[x] = repo[i + x]
186 186 l[x].changeset() # force reading
187 187 else:
188 188 l[x] = 1
189 189 for x in xrange(chunk-1, -1, -1):
190 190 if l[x] != 0:
191 191 yield (i + x, full != None and l[x] or None)
192 192 if i == 0:
193 193 break
194 194
195 195 # calculate and return the reachability bitmask for sha
196 196 def is_reachable(ar, reachable, sha):
197 197 if len(ar) == 0:
198 198 return 1
199 199 mask = 0
200 200 for i in xrange(len(ar)):
201 201 if sha in reachable[i]:
202 202 mask |= 1 << i
203 203
204 204 return mask
205 205
206 206 reachable = []
207 207 stop_sha1 = []
208 208 want_sha1 = []
209 209 count = 0
210 210
211 211 # figure out which commits they are asking for and which ones they
212 212 # want us to stop on
213 213 for i, arg in enumerate(args):
214 214 if arg.startswith('^'):
215 215 s = repo.lookup(arg[1:])
216 216 stop_sha1.append(s)
217 217 want_sha1.append(s)
218 218 elif arg != 'HEAD':
219 219 want_sha1.append(repo.lookup(arg))
220 220
221 221 # calculate the graph for the supplied commits
222 222 for i, n in enumerate(want_sha1):
223 223 reachable.append(set());
224 224 visit = [n];
225 225 reachable[i].add(n)
226 226 while visit:
227 227 n = visit.pop(0)
228 228 if n in stop_sha1:
229 229 continue
230 230 for p in repo.changelog.parents(n):
231 231 if p not in reachable[i]:
232 232 reachable[i].add(p)
233 233 visit.append(p)
234 234 if p in stop_sha1:
235 235 continue
236 236
237 237 # walk the repository looking for commits that are in our
238 238 # reachability graph
239 239 for i, ctx in chlogwalk():
240 240 n = repo.changelog.node(i)
241 241 mask = is_reachable(want_sha1, reachable, n)
242 242 if mask:
243 243 parentstr = ""
244 244 if parents:
245 245 pp = repo.changelog.parents(n)
246 246 if pp[0] != nullid:
247 247 parentstr += " " + short(pp[0])
248 248 if pp[1] != nullid:
249 249 parentstr += " " + short(pp[1])
250 250 if not full:
251 251 ui.write("%s%s\n" % (short(n), parentstr))
252 252 elif full == "commit":
253 253 ui.write("%s%s\n" % (short(n), parentstr))
254 254 catcommit(ui, repo, n, ' ', ctx)
255 255 else:
256 256 (p1, p2) = repo.changelog.parents(n)
257 257 (h, h1, h2) = map(short, (n, p1, p2))
258 258 (i1, i2) = map(repo.changelog.rev, (p1, p2))
259 259
260 260 date = ctx.date()[0]
261 261 ui.write("%s %s:%s" % (date, h, mask))
262 262 mask = is_reachable(want_sha1, reachable, p1)
263 263 if i1 != nullrev and mask > 0:
264 264 ui.write("%s:%s " % (h1, mask)),
265 265 mask = is_reachable(want_sha1, reachable, p2)
266 266 if i2 != nullrev and mask > 0:
267 267 ui.write("%s:%s " % (h2, mask))
268 268 ui.write("\n")
269 269 if maxnr and count >= maxnr:
270 270 break
271 271 count += 1
272 272
273 273 def revparse(ui, repo, *revs, **opts):
274 274 """parse given revisions"""
275 275 def revstr(rev):
276 276 if rev == 'HEAD':
277 277 rev = 'tip'
278 278 return revlog.hex(repo.lookup(rev))
279 279
280 280 for r in revs:
281 281 revrange = r.split(':', 1)
282 282 ui.write('%s\n' % revstr(revrange[0]))
283 283 if len(revrange) == 2:
284 284 ui.write('^%s\n' % revstr(revrange[1]))
285 285
286 286 # git rev-list tries to order things by date, and has the ability to stop
287 287 # at a given commit without walking the whole repo. TODO add the stop
288 288 # parameter
289 289 def revlist(ui, repo, *revs, **opts):
290 290 """print revisions"""
291 291 if opts['header']:
292 292 full = "commit"
293 293 else:
294 294 full = None
295 295 copy = [x for x in revs]
296 296 revtree(ui, copy, repo, full, opts['max_count'], opts['parents'])
297 297
298 298 def config(ui, repo, **opts):
299 299 """print extension options"""
300 300 def writeopt(name, value):
301 301 ui.write('k=%s\nv=%s\n' % (name, value))
302 302
303 303 writeopt('vdiff', ui.config('hgk', 'vdiff', ''))
304 304
305 305
306 306 def view(ui, repo, *etc, **opts):
307 307 "start interactive history viewer"
308 308 os.chdir(repo.root)
309 309 optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v])
310 310 cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc))
311 311 ui.debug("running %s\n" % cmd)
312 312 util.system(cmd)
313 313
314 314 cmdtable = {
315 315 "^view":
316 316 (view,
317 317 [('l', 'limit', '', _('limit number of changes displayed'))],
318 318 _('hg view [-l LIMIT] [REVRANGE]')),
319 319 "debug-diff-tree":
320 320 (difftree,
321 321 [('p', 'patch', None, _('generate patch')),
322 322 ('r', 'recursive', None, _('recursive')),
323 323 ('P', 'pretty', None, _('pretty')),
324 324 ('s', 'stdin', None, _('stdin')),
325 325 ('C', 'copy', None, _('detect copies')),
326 326 ('S', 'search', "", _('search'))],
327 327 _('hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...')),
328 328 "debug-cat-file":
329 329 (catfile,
330 330 [('s', 'stdin', None, _('stdin'))],
331 331 _('hg debug-cat-file [OPTION]... TYPE FILE')),
332 332 "debug-config":
333 333 (config, [], _('hg debug-config')),
334 334 "debug-merge-base":
335 335 (base, [], _('hg debug-merge-base REV REV')),
336 336 "debug-rev-parse":
337 337 (revparse,
338 338 [('', 'default', '', _('ignored'))],
339 339 _('hg debug-rev-parse REV')),
340 340 "debug-rev-list":
341 341 (revlist,
342 342 [('H', 'header', None, _('header')),
343 343 ('t', 'topo-order', None, _('topo-order')),
344 344 ('p', 'parents', None, _('parents')),
345 345 ('n', 'max-count', 0, _('max-count'))],
346 346 _('hg debug-rev-list [OPTION]... REV...')),
347 347 }
@@ -1,61 +1,61 b''
1 1 # highlight - syntax highlighting in hgweb, based on Pygments
2 2 #
3 3 # Copyright 2008, 2009 Patrick Mezard <pmezard@gmail.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7 #
8 8 # The original module was split in an interface and an implementation
9 9 # file to defer pygments loading and speedup extension setup.
10 10
11 11 """syntax highlighting for hgweb (requires Pygments)
12 12
13 13 It depends on the Pygments syntax highlighting library:
14 14 http://pygments.org/
15 15
16 16 There is a single configuration option::
17 17
18 18 [web]
19 19 pygments_style = <style>
20 20
21 21 The default is 'colorful'.
22 22 """
23 23
24 24 import highlight
25 25 from mercurial.hgweb import webcommands, webutil, common
26 26 from mercurial import extensions, encoding
27 27
28 28 def filerevision_highlight(orig, web, tmpl, fctx):
29 29 mt = ''.join(tmpl('mimetype', encoding=encoding.encoding))
30 30 # only pygmentize for mimetype containing 'html' so we both match
31 31 # 'text/html' and possibly 'application/xhtml+xml' in the future
32 32 # so that we don't have to touch the extension when the mimetype
33 33 # for a template changes; also hgweb optimizes the case that a
34 34 # raw file is sent using rawfile() and doesn't call us, so we
35 35 # can't clash with the file's content-type here in case we
36 36 # pygmentize a html file
37 37 if 'html' in mt:
38 38 style = web.config('web', 'pygments_style', 'colorful')
39 39 highlight.pygmentize('fileline', fctx, style, tmpl)
40 40 return orig(web, tmpl, fctx)
41 41
42 42 def annotate_highlight(orig, web, req, tmpl):
43 43 mt = ''.join(tmpl('mimetype', encoding=encoding.encoding))
44 44 if 'html' in mt:
45 45 fctx = webutil.filectx(web.repo, req)
46 46 style = web.config('web', 'pygments_style', 'colorful')
47 47 highlight.pygmentize('annotateline', fctx, style, tmpl)
48 48 return orig(web, req, tmpl)
49 49
50 50 def generate_css(web, req, tmpl):
51 51 pg_style = web.config('web', 'pygments_style', 'colorful')
52 52 fmter = highlight.HtmlFormatter(style = pg_style)
53 53 req.respond(common.HTTP_OK, 'text/css')
54 54 return ['/* pygments_style = %s */\n\n' % pg_style, fmter.get_style_defs('')]
55 55
56 56 def extsetup():
57 57 # monkeypatch in the new version
58 58 extensions.wrapfunction(webcommands, '_filerevision', filerevision_highlight)
59 59 extensions.wrapfunction(webcommands, 'annotate', annotate_highlight)
60 60 webcommands.highlightcss = generate_css
61 61 webcommands.__all__.append('highlightcss')
@@ -1,61 +1,61 b''
1 1 # highlight.py - highlight extension implementation file
2 2 #
3 3 # Copyright 2007-2009 Adam Hupp <adam@hupp.org> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7 #
8 8 # The original module was split in an interface and an implementation
9 9 # file to defer pygments loading and speedup extension setup.
10 10
11 11 from mercurial import demandimport
12 12 demandimport.ignore.extend(['pkgutil', 'pkg_resources', '__main__',])
13 13 from mercurial import util, encoding
14 14
15 15 from pygments import highlight
16 16 from pygments.util import ClassNotFound
17 17 from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer
18 18 from pygments.formatters import HtmlFormatter
19 19
20 20 SYNTAX_CSS = ('\n<link rel="stylesheet" href="{url}highlightcss" '
21 21 'type="text/css" />')
22 22
23 23 def pygmentize(field, fctx, style, tmpl):
24 24
25 25 # append a <link ...> to the syntax highlighting css
26 26 old_header = ''.join(tmpl('header'))
27 27 if SYNTAX_CSS not in old_header:
28 28 new_header = old_header + SYNTAX_CSS
29 29 tmpl.cache['header'] = new_header
30 30
31 31 text = fctx.data()
32 32 if util.binary(text):
33 33 return
34 34
35 35 # Pygments is best used with Unicode strings:
36 36 # <http://pygments.org/docs/unicode/>
37 37 text = text.decode(encoding.encoding, 'replace')
38 38
39 39 # To get multi-line strings right, we can't format line-by-line
40 40 try:
41 41 lexer = guess_lexer_for_filename(fctx.path(), text[:1024])
42 42 except (ClassNotFound, ValueError):
43 43 try:
44 44 lexer = guess_lexer(text[:1024])
45 45 except (ClassNotFound, ValueError):
46 46 lexer = TextLexer()
47 47
48 48 formatter = HtmlFormatter(style=style)
49 49
50 50 colorized = highlight(text, lexer, formatter)
51 51 # strip wrapping div
52 52 colorized = colorized[:colorized.find('\n</pre>')]
53 53 colorized = colorized[colorized.find('<pre>')+5:]
54 54 coloriter = (s.encode(encoding.encoding, 'replace')
55 55 for s in colorized.splitlines())
56 56
57 57 tmpl.filters['colorize'] = lambda x: coloriter.next()
58 58
59 59 oldl = tmpl.cache[field]
60 60 newl = oldl.replace('line|escape', 'line|colorize')
61 61 tmpl.cache[field] = newl
@@ -1,87 +1,87 b''
1 1 # __init__.py - inotify-based status acceleration for Linux
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''accelerate status report using Linux's inotify service'''
10 10
11 11 # todo: socket permissions
12 12
13 13 from mercurial.i18n import _
14 14 from mercurial import cmdutil, util
15 15 import server
16 16 from client import client, QueryFailed
17 17
18 18 def serve(ui, repo, **opts):
19 19 '''start an inotify server for this repository'''
20 20 server.start(ui, repo.dirstate, repo.root, opts)
21 21
22 22 def debuginotify(ui, repo, **opts):
23 23 '''debugging information for inotify extension
24 24
25 25 Prints the list of directories being watched by the inotify server.
26 26 '''
27 27 cli = client(ui, repo)
28 28 response = cli.debugquery()
29 29
30 30 ui.write(_('directories being watched:\n'))
31 31 for path in response:
32 32 ui.write((' %s/\n') % path)
33 33
34 34 def reposetup(ui, repo):
35 35 if not hasattr(repo, 'dirstate'):
36 36 return
37 37
38 38 class inotifydirstate(repo.dirstate.__class__):
39 39
40 40 # We'll set this to false after an unsuccessful attempt so that
41 41 # next calls of status() within the same instance don't try again
42 42 # to start an inotify server if it won't start.
43 43 _inotifyon = True
44 44
45 45 def status(self, match, subrepos, ignored, clean, unknown=True):
46 46 files = match.files()
47 47 if '.' in files:
48 48 files = []
49 49 if self._inotifyon and not ignored and not subrepos and not self._dirty:
50 50 cli = client(ui, repo)
51 51 try:
52 52 result = cli.statusquery(files, match, False,
53 53 clean, unknown)
54 54 except QueryFailed, instr:
55 55 ui.debug(str(instr))
56 56 # don't retry within the same hg instance
57 57 inotifydirstate._inotifyon = False
58 58 pass
59 59 else:
60 60 if ui.config('inotify', 'debug'):
61 61 r2 = super(inotifydirstate, self).status(
62 62 match, False, clean, unknown)
63 63 for c,a,b in zip('LMARDUIC', result, r2):
64 64 for f in a:
65 65 if f not in b:
66 66 ui.warn('*** inotify: %s +%s\n' % (c, f))
67 67 for f in b:
68 68 if f not in a:
69 69 ui.warn('*** inotify: %s -%s\n' % (c, f))
70 70 result = r2
71 71 return result
72 72 return super(inotifydirstate, self).status(
73 73 match, subrepos, ignored, clean, unknown)
74 74
75 75 repo.dirstate.__class__ = inotifydirstate
76 76
77 77 cmdtable = {
78 78 'debuginotify':
79 79 (debuginotify, [], ('hg debuginotify')),
80 80 '^inserve':
81 81 (serve,
82 82 [('d', 'daemon', None, _('run server in background')),
83 83 ('', 'daemon-pipefds', '', _('used internally by daemon mode')),
84 84 ('t', 'idle-timeout', '', _('minutes to sit idle before exiting')),
85 85 ('', 'pid-file', '', _('name of file to write process ID to'))],
86 86 _('hg inserve [OPTION]...')),
87 87 }
@@ -1,171 +1,171 b''
1 1 # client.py - inotify status client
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 # Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2, incorporated herein by reference.
8 # GNU General Public License version 2 or any later version.
9 9
10 10 from mercurial.i18n import _
11 11 import common, server
12 12 import errno, os, socket, struct
13 13
14 14 class QueryFailed(Exception): pass
15 15
16 16 def start_server(function):
17 17 """
18 18 Decorator.
19 19 Tries to call function, if it fails, try to (re)start inotify server.
20 20 Raise QueryFailed if something went wrong
21 21 """
22 22 def decorated_function(self, *args):
23 23 result = None
24 24 try:
25 25 return function(self, *args)
26 26 except (OSError, socket.error), err:
27 27 autostart = self.ui.configbool('inotify', 'autostart', True)
28 28
29 29 if err[0] == errno.ECONNREFUSED:
30 30 self.ui.warn(_('inotify-client: found dead inotify server '
31 31 'socket; removing it\n'))
32 32 os.unlink(os.path.join(self.root, '.hg', 'inotify.sock'))
33 33 if err[0] in (errno.ECONNREFUSED, errno.ENOENT) and autostart:
34 34 self.ui.debug('(starting inotify server)\n')
35 35 try:
36 36 try:
37 37 server.start(self.ui, self.dirstate, self.root,
38 38 dict(daemon=True, daemon_pipefds=''))
39 39 except server.AlreadyStartedException, inst:
40 40 # another process may have started its own
41 41 # inotify server while this one was starting.
42 42 self.ui.debug(str(inst))
43 43 except Exception, inst:
44 44 self.ui.warn(_('inotify-client: could not start inotify '
45 45 'server: %s\n') % inst)
46 46 else:
47 47 try:
48 48 return function(self, *args)
49 49 except socket.error, err:
50 50 self.ui.warn(_('inotify-client: could not talk to new '
51 51 'inotify server: %s\n') % err[-1])
52 52 elif err[0] in (errno.ECONNREFUSED, errno.ENOENT):
53 53 # silently ignore normal errors if autostart is False
54 54 self.ui.debug('(inotify server not running)\n')
55 55 else:
56 56 self.ui.warn(_('inotify-client: failed to contact inotify '
57 57 'server: %s\n') % err[-1])
58 58
59 59 self.ui.traceback()
60 60 raise QueryFailed('inotify query failed')
61 61
62 62 return decorated_function
63 63
64 64
65 65 class client(object):
66 66 def __init__(self, ui, repo):
67 67 self.ui = ui
68 68 self.dirstate = repo.dirstate
69 69 self.root = repo.root
70 70 self.sock = socket.socket(socket.AF_UNIX)
71 71
72 72 def _connect(self):
73 73 sockpath = os.path.join(self.root, '.hg', 'inotify.sock')
74 74 try:
75 75 self.sock.connect(sockpath)
76 76 except socket.error, err:
77 77 if err[0] == "AF_UNIX path too long":
78 78 sockpath = os.readlink(sockpath)
79 79 self.sock.connect(sockpath)
80 80 else:
81 81 raise
82 82
83 83 def _send(self, type, data):
84 84 """Sends protocol version number, and the data"""
85 85 self.sock.sendall(chr(common.version) + type + data)
86 86
87 87 self.sock.shutdown(socket.SHUT_WR)
88 88
89 89 def _receive(self, type):
90 90 """
91 91 Read data, check version number, extract headers,
92 92 and returns a tuple (data descriptor, header)
93 93 Raises QueryFailed on error
94 94 """
95 95 cs = common.recvcs(self.sock)
96 96 try:
97 97 version = ord(cs.read(1))
98 98 except TypeError:
99 99 # empty answer, assume the server crashed
100 100 self.ui.warn(_('inotify-client: received empty answer from inotify '
101 101 'server'))
102 102 raise QueryFailed('server crashed')
103 103
104 104 if version != common.version:
105 105 self.ui.warn(_('(inotify: received response from incompatible '
106 106 'server version %d)\n') % version)
107 107 raise QueryFailed('incompatible server version')
108 108
109 109 readtype = cs.read(4)
110 110 if readtype != type:
111 111 self.ui.warn(_('(inotify: received \'%s\' response when expecting'
112 112 ' \'%s\')\n') % (readtype, type))
113 113 raise QueryFailed('wrong response type')
114 114
115 115 hdrfmt = common.resphdrfmts[type]
116 116 hdrsize = common.resphdrsizes[type]
117 117 try:
118 118 resphdr = struct.unpack(hdrfmt, cs.read(hdrsize))
119 119 except struct.error:
120 120 raise QueryFailed('unable to retrieve query response headers')
121 121
122 122 return cs, resphdr
123 123
124 124 def query(self, type, req):
125 125 self._connect()
126 126
127 127 self._send(type, req)
128 128
129 129 return self._receive(type)
130 130
131 131 @start_server
132 132 def statusquery(self, names, match, ignored, clean, unknown=True):
133 133
134 134 def genquery():
135 135 for n in names:
136 136 yield n
137 137 states = 'almrx!'
138 138 if ignored:
139 139 raise ValueError('this is insanity')
140 140 if clean: states += 'c'
141 141 if unknown: states += '?'
142 142 yield states
143 143
144 144 req = '\0'.join(genquery())
145 145
146 146 cs, resphdr = self.query('STAT', req)
147 147
148 148 def readnames(nbytes):
149 149 if nbytes:
150 150 names = cs.read(nbytes)
151 151 if names:
152 152 return filter(match, names.split('\0'))
153 153 return []
154 154 results = map(readnames, resphdr[:-1])
155 155
156 156 if names:
157 157 nbytes = resphdr[-1]
158 158 vdirs = cs.read(nbytes)
159 159 if vdirs:
160 160 for vdir in vdirs.split('\0'):
161 161 match.dir(vdir)
162 162
163 163 return results
164 164
165 165 @start_server
166 166 def debugquery(self):
167 167 cs, resphdr = self.query('DBUG', '')
168 168
169 169 nbytes = resphdr[0]
170 170 names = cs.read(nbytes)
171 171 return names.split('\0')
@@ -1,53 +1,53 b''
1 1 # server.py - inotify common protocol code
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2 or any later version.
8 8
9 9 import cStringIO, socket, struct
10 10
11 11 """
12 12 Protocol between inotify clients and server:
13 13
14 14 Client sending query:
15 15 1) send protocol version number
16 16 2) send query type (string, 4 letters long)
17 17 3) send query parameters:
18 18 - For STAT, N+1 \0-separated strings:
19 19 1) N different names that need checking
20 20 2) 1 string containing all the status types to match
21 21 - No parameter needed for DBUG
22 22
23 23 Server sending query answer:
24 24 1) send protocol version number
25 25 2) send query type
26 26 3) send struct.pack'ed headers describing the length of the content:
27 27 e.g. for STAT, receive 9 integers describing the length of the
28 28 9 \0-separated string lists to be read:
29 29 * one file list for each lmar!?ic status type
30 30 * one list containing the directories visited during lookup
31 31
32 32 """
33 33
34 34 version = 3
35 35
36 36 resphdrfmts = {
37 37 'STAT': '>lllllllll', # status requests
38 38 'DBUG': '>l' # debugging queries
39 39 }
40 40 resphdrsizes = dict((k, struct.calcsize(v))
41 41 for k, v in resphdrfmts.iteritems())
42 42
43 43 def recvcs(sock):
44 44 cs = cStringIO.StringIO()
45 45 s = True
46 46 try:
47 47 while s:
48 48 s = sock.recv(65536)
49 49 cs.write(s)
50 50 finally:
51 51 sock.shutdown(socket.SHUT_RD)
52 52 cs.seek(0)
53 53 return cs
@@ -1,41 +1,41 b''
1 1 # __init__.py - low-level interfaces to the Linux inotify subsystem
2 2
3 3 # Copyright 2006 Bryan O'Sullivan <bos@serpentine.com>
4 4
5 5 # This library is free software; you can redistribute it and/or modify
6 6 # it under the terms of version 2.1 of the GNU Lesser General Public
7 # License, incorporated herein by reference.
7 # License, or any later version.
8 8
9 9 '''Low-level interface to the Linux inotify subsystem.
10 10
11 11 The inotify subsystem provides an efficient mechanism for file status
12 12 monitoring and change notification.
13 13
14 14 This package provides the low-level inotify system call interface and
15 15 associated constants and helper functions.
16 16
17 17 For a higher-level interface that remains highly efficient, use the
18 18 inotify.watcher package.'''
19 19
20 20 __author__ = "Bryan O'Sullivan <bos@serpentine.com>"
21 21
22 22 from _inotify import *
23 23
24 24 procfs_path = '/proc/sys/fs/inotify'
25 25
26 26 def _read_procfs_value(name):
27 27 def read_value():
28 28 try:
29 29 return int(open(procfs_path + '/' + name).read())
30 30 except OSError:
31 31 return None
32 32
33 33 read_value.__doc__ = '''Return the value of the %s setting from /proc.
34 34
35 35 If inotify is not enabled on this system, return None.''' % name
36 36
37 37 return read_value
38 38
39 39 max_queued_events = _read_procfs_value('max_queued_events')
40 40 max_user_instances = _read_procfs_value('max_user_instances')
41 41 max_user_watches = _read_procfs_value('max_user_watches')
@@ -1,600 +1,600 b''
1 1 /*
2 2 * _inotify.c - Python extension interfacing to the Linux inotify subsystem
3 3 *
4 4 * Copyright 2006 Bryan O'Sullivan <bos@serpentine.com>
5 5 *
6 6 * This library is free software; you can redistribute it and/or
7 7 * modify it under the terms of version 2.1 of the GNU Lesser General
8 * Public License, incorporated herein by reference.
8 * Public License or any later version.
9 9 */
10 10
11 11 #include <Python.h>
12 12 #include <alloca.h>
13 13 #include <sys/inotify.h>
14 14 #include <stdint.h>
15 15 #include <sys/ioctl.h>
16 16 #include <unistd.h>
17 17
18 18 static PyObject *init(PyObject *self, PyObject *args)
19 19 {
20 20 PyObject *ret = NULL;
21 21 int fd = -1;
22 22
23 23 if (!PyArg_ParseTuple(args, ":init"))
24 24 goto bail;
25 25
26 26 Py_BEGIN_ALLOW_THREADS
27 27 fd = inotify_init();
28 28 Py_END_ALLOW_THREADS
29 29
30 30 if (fd == -1) {
31 31 PyErr_SetFromErrno(PyExc_OSError);
32 32 goto bail;
33 33 }
34 34
35 35 ret = PyInt_FromLong(fd);
36 36 if (ret == NULL)
37 37 goto bail;
38 38
39 39 goto done;
40 40
41 41 bail:
42 42 if (fd != -1)
43 43 close(fd);
44 44
45 45 Py_CLEAR(ret);
46 46
47 47 done:
48 48 return ret;
49 49 }
50 50
51 51 PyDoc_STRVAR(
52 52 init_doc,
53 53 "init() -> fd\n"
54 54 "\n"
55 55 "Initialise an inotify instance.\n"
56 56 "Return a file descriptor associated with a new inotify event queue.");
57 57
58 58 static PyObject *add_watch(PyObject *self, PyObject *args)
59 59 {
60 60 PyObject *ret = NULL;
61 61 uint32_t mask;
62 62 int wd = -1;
63 63 char *path;
64 64 int fd;
65 65
66 66 if (!PyArg_ParseTuple(args, "isI:add_watch", &fd, &path, &mask))
67 67 goto bail;
68 68
69 69 Py_BEGIN_ALLOW_THREADS
70 70 wd = inotify_add_watch(fd, path, mask);
71 71 Py_END_ALLOW_THREADS
72 72
73 73 if (wd == -1) {
74 74 PyErr_SetFromErrnoWithFilename(PyExc_OSError, path);
75 75 goto bail;
76 76 }
77 77
78 78 ret = PyInt_FromLong(wd);
79 79 if (ret == NULL)
80 80 goto bail;
81 81
82 82 goto done;
83 83
84 84 bail:
85 85 if (wd != -1)
86 86 inotify_rm_watch(fd, wd);
87 87
88 88 Py_CLEAR(ret);
89 89
90 90 done:
91 91 return ret;
92 92 }
93 93
94 94 PyDoc_STRVAR(
95 95 add_watch_doc,
96 96 "add_watch(fd, path, mask) -> wd\n"
97 97 "\n"
98 98 "Add a watch to an inotify instance, or modify an existing watch.\n"
99 99 "\n"
100 100 " fd: file descriptor returned by init()\n"
101 101 " path: path to watch\n"
102 102 " mask: mask of events to watch for\n"
103 103 "\n"
104 104 "Return a unique numeric watch descriptor for the inotify instance\n"
105 105 "mapped by the file descriptor.");
106 106
107 107 static PyObject *remove_watch(PyObject *self, PyObject *args)
108 108 {
109 109 uint32_t wd;
110 110 int fd;
111 111 int r;
112 112
113 113 if (!PyArg_ParseTuple(args, "iI:remove_watch", &fd, &wd))
114 114 return NULL;
115 115
116 116 Py_BEGIN_ALLOW_THREADS
117 117 r = inotify_rm_watch(fd, wd);
118 118 Py_END_ALLOW_THREADS
119 119
120 120 if (r == -1) {
121 121 PyErr_SetFromErrno(PyExc_OSError);
122 122 return NULL;
123 123 }
124 124
125 125 Py_INCREF(Py_None);
126 126 return Py_None;
127 127 }
128 128
129 129 PyDoc_STRVAR(
130 130 remove_watch_doc,
131 131 "remove_watch(fd, wd)\n"
132 132 "\n"
133 133 " fd: file descriptor returned by init()\n"
134 134 " wd: watch descriptor returned by add_watch()\n"
135 135 "\n"
136 136 "Remove a watch associated with the watch descriptor wd from the\n"
137 137 "inotify instance associated with the file descriptor fd.\n"
138 138 "\n"
139 139 "Removing a watch causes an IN_IGNORED event to be generated for this\n"
140 140 "watch descriptor.");
141 141
142 142 #define bit_name(x) {x, #x}
143 143
144 144 static struct {
145 145 int bit;
146 146 const char *name;
147 147 PyObject *pyname;
148 148 } bit_names[] = {
149 149 bit_name(IN_ACCESS),
150 150 bit_name(IN_MODIFY),
151 151 bit_name(IN_ATTRIB),
152 152 bit_name(IN_CLOSE_WRITE),
153 153 bit_name(IN_CLOSE_NOWRITE),
154 154 bit_name(IN_OPEN),
155 155 bit_name(IN_MOVED_FROM),
156 156 bit_name(IN_MOVED_TO),
157 157 bit_name(IN_CREATE),
158 158 bit_name(IN_DELETE),
159 159 bit_name(IN_DELETE_SELF),
160 160 bit_name(IN_MOVE_SELF),
161 161 bit_name(IN_UNMOUNT),
162 162 bit_name(IN_Q_OVERFLOW),
163 163 bit_name(IN_IGNORED),
164 164 bit_name(IN_ONLYDIR),
165 165 bit_name(IN_DONT_FOLLOW),
166 166 bit_name(IN_MASK_ADD),
167 167 bit_name(IN_ISDIR),
168 168 bit_name(IN_ONESHOT),
169 169 {0}
170 170 };
171 171
172 172 static PyObject *decode_mask(int mask)
173 173 {
174 174 PyObject *ret = PyList_New(0);
175 175 int i;
176 176
177 177 if (ret == NULL)
178 178 goto bail;
179 179
180 180 for (i = 0; bit_names[i].bit; i++) {
181 181 if (mask & bit_names[i].bit) {
182 182 if (bit_names[i].pyname == NULL) {
183 183 bit_names[i].pyname = PyString_FromString(bit_names[i].name);
184 184 if (bit_names[i].pyname == NULL)
185 185 goto bail;
186 186 }
187 187 Py_INCREF(bit_names[i].pyname);
188 188 if (PyList_Append(ret, bit_names[i].pyname) == -1)
189 189 goto bail;
190 190 }
191 191 }
192 192
193 193 goto done;
194 194
195 195 bail:
196 196 Py_CLEAR(ret);
197 197
198 198 done:
199 199 return ret;
200 200 }
201 201
202 202 static PyObject *pydecode_mask(PyObject *self, PyObject *args)
203 203 {
204 204 int mask;
205 205
206 206 if (!PyArg_ParseTuple(args, "i:decode_mask", &mask))
207 207 return NULL;
208 208
209 209 return decode_mask(mask);
210 210 }
211 211
212 212 PyDoc_STRVAR(
213 213 decode_mask_doc,
214 214 "decode_mask(mask) -> list_of_strings\n"
215 215 "\n"
216 216 "Decode an inotify mask value into a list of strings that give the\n"
217 217 "name of each bit set in the mask.");
218 218
219 219 static char doc[] = "Low-level inotify interface wrappers.";
220 220
221 221 static void define_const(PyObject *dict, const char *name, uint32_t val)
222 222 {
223 223 PyObject *pyval = PyInt_FromLong(val);
224 224 PyObject *pyname = PyString_FromString(name);
225 225
226 226 if (!pyname || !pyval)
227 227 goto bail;
228 228
229 229 PyDict_SetItem(dict, pyname, pyval);
230 230
231 231 bail:
232 232 Py_XDECREF(pyname);
233 233 Py_XDECREF(pyval);
234 234 }
235 235
236 236 static void define_consts(PyObject *dict)
237 237 {
238 238 define_const(dict, "IN_ACCESS", IN_ACCESS);
239 239 define_const(dict, "IN_MODIFY", IN_MODIFY);
240 240 define_const(dict, "IN_ATTRIB", IN_ATTRIB);
241 241 define_const(dict, "IN_CLOSE_WRITE", IN_CLOSE_WRITE);
242 242 define_const(dict, "IN_CLOSE_NOWRITE", IN_CLOSE_NOWRITE);
243 243 define_const(dict, "IN_OPEN", IN_OPEN);
244 244 define_const(dict, "IN_MOVED_FROM", IN_MOVED_FROM);
245 245 define_const(dict, "IN_MOVED_TO", IN_MOVED_TO);
246 246
247 247 define_const(dict, "IN_CLOSE", IN_CLOSE);
248 248 define_const(dict, "IN_MOVE", IN_MOVE);
249 249
250 250 define_const(dict, "IN_CREATE", IN_CREATE);
251 251 define_const(dict, "IN_DELETE", IN_DELETE);
252 252 define_const(dict, "IN_DELETE_SELF", IN_DELETE_SELF);
253 253 define_const(dict, "IN_MOVE_SELF", IN_MOVE_SELF);
254 254 define_const(dict, "IN_UNMOUNT", IN_UNMOUNT);
255 255 define_const(dict, "IN_Q_OVERFLOW", IN_Q_OVERFLOW);
256 256 define_const(dict, "IN_IGNORED", IN_IGNORED);
257 257
258 258 define_const(dict, "IN_ONLYDIR", IN_ONLYDIR);
259 259 define_const(dict, "IN_DONT_FOLLOW", IN_DONT_FOLLOW);
260 260 define_const(dict, "IN_MASK_ADD", IN_MASK_ADD);
261 261 define_const(dict, "IN_ISDIR", IN_ISDIR);
262 262 define_const(dict, "IN_ONESHOT", IN_ONESHOT);
263 263 define_const(dict, "IN_ALL_EVENTS", IN_ALL_EVENTS);
264 264 }
265 265
266 266 struct event {
267 267 PyObject_HEAD
268 268 PyObject *wd;
269 269 PyObject *mask;
270 270 PyObject *cookie;
271 271 PyObject *name;
272 272 };
273 273
274 274 static PyObject *event_wd(PyObject *self, void *x)
275 275 {
276 276 struct event *evt = (struct event *) self;
277 277 Py_INCREF(evt->wd);
278 278 return evt->wd;
279 279 }
280 280
281 281 static PyObject *event_mask(PyObject *self, void *x)
282 282 {
283 283 struct event *evt = (struct event *) self;
284 284 Py_INCREF(evt->mask);
285 285 return evt->mask;
286 286 }
287 287
288 288 static PyObject *event_cookie(PyObject *self, void *x)
289 289 {
290 290 struct event *evt = (struct event *) self;
291 291 Py_INCREF(evt->cookie);
292 292 return evt->cookie;
293 293 }
294 294
295 295 static PyObject *event_name(PyObject *self, void *x)
296 296 {
297 297 struct event *evt = (struct event *) self;
298 298 Py_INCREF(evt->name);
299 299 return evt->name;
300 300 }
301 301
302 302 static struct PyGetSetDef event_getsets[] = {
303 303 {"wd", event_wd, NULL,
304 304 "watch descriptor"},
305 305 {"mask", event_mask, NULL,
306 306 "event mask"},
307 307 {"cookie", event_cookie, NULL,
308 308 "rename cookie, if rename-related event"},
309 309 {"name", event_name, NULL,
310 310 "file name"},
311 311 {NULL}
312 312 };
313 313
314 314 PyDoc_STRVAR(
315 315 event_doc,
316 316 "event: Structure describing an inotify event.");
317 317
318 318 static PyObject *event_new(PyTypeObject *t, PyObject *a, PyObject *k)
319 319 {
320 320 return (*t->tp_alloc)(t, 0);
321 321 }
322 322
323 323 static void event_dealloc(struct event *evt)
324 324 {
325 325 Py_XDECREF(evt->wd);
326 326 Py_XDECREF(evt->mask);
327 327 Py_XDECREF(evt->cookie);
328 328 Py_XDECREF(evt->name);
329 329
330 330 (*evt->ob_type->tp_free)(evt);
331 331 }
332 332
333 333 static PyObject *event_repr(struct event *evt)
334 334 {
335 335 int wd = PyInt_AsLong(evt->wd);
336 336 int cookie = evt->cookie == Py_None ? -1 : PyInt_AsLong(evt->cookie);
337 337 PyObject *ret = NULL, *pymasks = NULL, *pymask = NULL;
338 338 PyObject *join = NULL;
339 339 char *maskstr;
340 340
341 341 join = PyString_FromString("|");
342 342 if (join == NULL)
343 343 goto bail;
344 344
345 345 pymasks = decode_mask(PyInt_AsLong(evt->mask));
346 346 if (pymasks == NULL)
347 347 goto bail;
348 348
349 349 pymask = _PyString_Join(join, pymasks);
350 350 if (pymask == NULL)
351 351 goto bail;
352 352
353 353 maskstr = PyString_AsString(pymask);
354 354
355 355 if (evt->name != Py_None) {
356 356 PyObject *pyname = PyString_Repr(evt->name, 1);
357 357 char *name = pyname ? PyString_AsString(pyname) : "???";
358 358
359 359 if (cookie == -1)
360 360 ret = PyString_FromFormat("event(wd=%d, mask=%s, name=%s)",
361 361 wd, maskstr, name);
362 362 else
363 363 ret = PyString_FromFormat("event(wd=%d, mask=%s, "
364 364 "cookie=0x%x, name=%s)",
365 365 wd, maskstr, cookie, name);
366 366
367 367 Py_XDECREF(pyname);
368 368 } else {
369 369 if (cookie == -1)
370 370 ret = PyString_FromFormat("event(wd=%d, mask=%s)",
371 371 wd, maskstr);
372 372 else {
373 373 ret = PyString_FromFormat("event(wd=%d, mask=%s, cookie=0x%x)",
374 374 wd, maskstr, cookie);
375 375 }
376 376 }
377 377
378 378 goto done;
379 379 bail:
380 380 Py_CLEAR(ret);
381 381
382 382 done:
383 383 Py_XDECREF(pymask);
384 384 Py_XDECREF(pymasks);
385 385 Py_XDECREF(join);
386 386
387 387 return ret;
388 388 }
389 389
390 390 static PyTypeObject event_type = {
391 391 PyObject_HEAD_INIT(NULL)
392 392 0, /*ob_size*/
393 393 "_inotify.event", /*tp_name*/
394 394 sizeof(struct event), /*tp_basicsize*/
395 395 0, /*tp_itemsize*/
396 396 (destructor)event_dealloc, /*tp_dealloc*/
397 397 0, /*tp_print*/
398 398 0, /*tp_getattr*/
399 399 0, /*tp_setattr*/
400 400 0, /*tp_compare*/
401 401 (reprfunc)event_repr, /*tp_repr*/
402 402 0, /*tp_as_number*/
403 403 0, /*tp_as_sequence*/
404 404 0, /*tp_as_mapping*/
405 405 0, /*tp_hash */
406 406 0, /*tp_call*/
407 407 0, /*tp_str*/
408 408 0, /*tp_getattro*/
409 409 0, /*tp_setattro*/
410 410 0, /*tp_as_buffer*/
411 411 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
412 412 event_doc, /* tp_doc */
413 413 0, /* tp_traverse */
414 414 0, /* tp_clear */
415 415 0, /* tp_richcompare */
416 416 0, /* tp_weaklistoffset */
417 417 0, /* tp_iter */
418 418 0, /* tp_iternext */
419 419 0, /* tp_methods */
420 420 0, /* tp_members */
421 421 event_getsets, /* tp_getset */
422 422 0, /* tp_base */
423 423 0, /* tp_dict */
424 424 0, /* tp_descr_get */
425 425 0, /* tp_descr_set */
426 426 0, /* tp_dictoffset */
427 427 0, /* tp_init */
428 428 0, /* tp_alloc */
429 429 event_new, /* tp_new */
430 430 };
431 431
432 432 PyObject *read_events(PyObject *self, PyObject *args)
433 433 {
434 434 PyObject *ctor_args = NULL;
435 435 PyObject *pybufsize = NULL;
436 436 PyObject *ret = NULL;
437 437 int bufsize = 65536;
438 438 char *buf = NULL;
439 439 int nread, pos;
440 440 int fd;
441 441
442 442 if (!PyArg_ParseTuple(args, "i|O:read", &fd, &pybufsize))
443 443 goto bail;
444 444
445 445 if (pybufsize && pybufsize != Py_None)
446 446 bufsize = PyInt_AsLong(pybufsize);
447 447
448 448 ret = PyList_New(0);
449 449 if (ret == NULL)
450 450 goto bail;
451 451
452 452 if (bufsize <= 0) {
453 453 int r;
454 454
455 455 Py_BEGIN_ALLOW_THREADS
456 456 r = ioctl(fd, FIONREAD, &bufsize);
457 457 Py_END_ALLOW_THREADS
458 458
459 459 if (r == -1) {
460 460 PyErr_SetFromErrno(PyExc_OSError);
461 461 goto bail;
462 462 }
463 463 if (bufsize == 0)
464 464 goto done;
465 465 }
466 466 else {
467 467 static long name_max;
468 468 static long name_fd = -1;
469 469 long min;
470 470
471 471 if (name_fd != fd) {
472 472 name_fd = fd;
473 473 Py_BEGIN_ALLOW_THREADS
474 474 name_max = fpathconf(fd, _PC_NAME_MAX);
475 475 Py_END_ALLOW_THREADS
476 476 }
477 477
478 478 min = sizeof(struct inotify_event) + name_max + 1;
479 479
480 480 if (bufsize < min) {
481 481 PyErr_Format(PyExc_ValueError, "bufsize must be at least %d",
482 482 (int) min);
483 483 goto bail;
484 484 }
485 485 }
486 486
487 487 buf = alloca(bufsize);
488 488
489 489 Py_BEGIN_ALLOW_THREADS
490 490 nread = read(fd, buf, bufsize);
491 491 Py_END_ALLOW_THREADS
492 492
493 493 if (nread == -1) {
494 494 PyErr_SetFromErrno(PyExc_OSError);
495 495 goto bail;
496 496 }
497 497
498 498 ctor_args = PyTuple_New(0);
499 499
500 500 if (ctor_args == NULL)
501 501 goto bail;
502 502
503 503 pos = 0;
504 504
505 505 while (pos < nread) {
506 506 struct inotify_event *in = (struct inotify_event *) (buf + pos);
507 507 struct event *evt;
508 508 PyObject *obj;
509 509
510 510 obj = PyObject_CallObject((PyObject *) &event_type, ctor_args);
511 511
512 512 if (obj == NULL)
513 513 goto bail;
514 514
515 515 evt = (struct event *) obj;
516 516
517 517 evt->wd = PyInt_FromLong(in->wd);
518 518 evt->mask = PyInt_FromLong(in->mask);
519 519 if (in->mask & IN_MOVE)
520 520 evt->cookie = PyInt_FromLong(in->cookie);
521 521 else {
522 522 Py_INCREF(Py_None);
523 523 evt->cookie = Py_None;
524 524 }
525 525 if (in->len)
526 526 evt->name = PyString_FromString(in->name);
527 527 else {
528 528 Py_INCREF(Py_None);
529 529 evt->name = Py_None;
530 530 }
531 531
532 532 if (!evt->wd || !evt->mask || !evt->cookie || !evt->name)
533 533 goto mybail;
534 534
535 535 if (PyList_Append(ret, obj) == -1)
536 536 goto mybail;
537 537
538 538 pos += sizeof(struct inotify_event) + in->len;
539 539 continue;
540 540
541 541 mybail:
542 542 Py_CLEAR(evt->wd);
543 543 Py_CLEAR(evt->mask);
544 544 Py_CLEAR(evt->cookie);
545 545 Py_CLEAR(evt->name);
546 546 Py_DECREF(obj);
547 547
548 548 goto bail;
549 549 }
550 550
551 551 goto done;
552 552
553 553 bail:
554 554 Py_CLEAR(ret);
555 555
556 556 done:
557 557 Py_XDECREF(ctor_args);
558 558
559 559 return ret;
560 560 }
561 561
562 562 PyDoc_STRVAR(
563 563 read_doc,
564 564 "read(fd, bufsize[=65536]) -> list_of_events\n"
565 565 "\n"
566 566 "\nRead inotify events from a file descriptor.\n"
567 567 "\n"
568 568 " fd: file descriptor returned by init()\n"
569 569 " bufsize: size of buffer to read into, in bytes\n"
570 570 "\n"
571 571 "Return a list of event objects.\n"
572 572 "\n"
573 573 "If bufsize is > 0, block until events are available to be read.\n"
574 574 "Otherwise, immediately return all events that can be read without\n"
575 575 "blocking.");
576 576
577 577
578 578 static PyMethodDef methods[] = {
579 579 {"init", init, METH_VARARGS, init_doc},
580 580 {"add_watch", add_watch, METH_VARARGS, add_watch_doc},
581 581 {"remove_watch", remove_watch, METH_VARARGS, remove_watch_doc},
582 582 {"read", read_events, METH_VARARGS, read_doc},
583 583 {"decode_mask", pydecode_mask, METH_VARARGS, decode_mask_doc},
584 584 {NULL},
585 585 };
586 586
587 587 void init_inotify(void)
588 588 {
589 589 PyObject *mod, *dict;
590 590
591 591 if (PyType_Ready(&event_type) == -1)
592 592 return;
593 593
594 594 mod = Py_InitModule3("_inotify", methods, doc);
595 595
596 596 dict = PyModule_GetDict(mod);
597 597
598 598 if (dict)
599 599 define_consts(dict);
600 600 }
@@ -1,335 +1,335 b''
1 1 # watcher.py - high-level interfaces to the Linux inotify subsystem
2 2
3 3 # Copyright 2006 Bryan O'Sullivan <bos@serpentine.com>
4 4
5 5 # This library is free software; you can redistribute it and/or modify
6 6 # it under the terms of version 2.1 of the GNU Lesser General Public
7 # License, incorporated herein by reference.
7 # License, or any later version.
8 8
9 9 '''High-level interfaces to the Linux inotify subsystem.
10 10
11 11 The inotify subsystem provides an efficient mechanism for file status
12 12 monitoring and change notification.
13 13
14 14 The watcher class hides the low-level details of the inotify
15 15 interface, and provides a Pythonic wrapper around it. It generates
16 16 events that provide somewhat more information than raw inotify makes
17 17 available.
18 18
19 19 The autowatcher class is more useful, as it automatically watches
20 20 newly-created directories on your behalf.'''
21 21
22 22 __author__ = "Bryan O'Sullivan <bos@serpentine.com>"
23 23
24 24 import _inotify as inotify
25 25 import array
26 26 import errno
27 27 import fcntl
28 28 import os
29 29 import termios
30 30
31 31
32 32 class event(object):
33 33 '''Derived inotify event class.
34 34
35 35 The following fields are available:
36 36
37 37 mask: event mask, indicating what kind of event this is
38 38
39 39 cookie: rename cookie, if a rename-related event
40 40
41 41 path: path of the directory in which the event occurred
42 42
43 43 name: name of the directory entry to which the event occurred
44 44 (may be None if the event happened to a watched directory)
45 45
46 46 fullpath: complete path at which the event occurred
47 47
48 48 wd: watch descriptor that triggered this event'''
49 49
50 50 __slots__ = (
51 51 'cookie',
52 52 'fullpath',
53 53 'mask',
54 54 'name',
55 55 'path',
56 56 'raw',
57 57 'wd',
58 58 )
59 59
60 60 def __init__(self, raw, path):
61 61 self.path = path
62 62 self.raw = raw
63 63 if raw.name:
64 64 self.fullpath = path + '/' + raw.name
65 65 else:
66 66 self.fullpath = path
67 67
68 68 self.wd = raw.wd
69 69 self.mask = raw.mask
70 70 self.cookie = raw.cookie
71 71 self.name = raw.name
72 72
73 73 def __repr__(self):
74 74 r = repr(self.raw)
75 75 return 'event(path=' + repr(self.path) + ', ' + r[r.find('(')+1:]
76 76
77 77
78 78 _event_props = {
79 79 'access': 'File was accessed',
80 80 'modify': 'File was modified',
81 81 'attrib': 'Attribute of a directory entry was changed',
82 82 'close_write': 'File was closed after being written to',
83 83 'close_nowrite': 'File was closed without being written to',
84 84 'open': 'File was opened',
85 85 'moved_from': 'Directory entry was renamed from this name',
86 86 'moved_to': 'Directory entry was renamed to this name',
87 87 'create': 'Directory entry was created',
88 88 'delete': 'Directory entry was deleted',
89 89 'delete_self': 'The watched directory entry was deleted',
90 90 'move_self': 'The watched directory entry was renamed',
91 91 'unmount': 'Directory was unmounted, and can no longer be watched',
92 92 'q_overflow': 'Kernel dropped events due to queue overflow',
93 93 'ignored': 'Directory entry is no longer being watched',
94 94 'isdir': 'Event occurred on a directory',
95 95 }
96 96
97 97 for k, v in _event_props.iteritems():
98 98 mask = getattr(inotify, 'IN_' + k.upper())
99 99 def getter(self):
100 100 return self.mask & mask
101 101 getter.__name__ = k
102 102 getter.__doc__ = v
103 103 setattr(event, k, property(getter, doc=v))
104 104
105 105 del _event_props
106 106
107 107
108 108 class watcher(object):
109 109 '''Provide a Pythonic interface to the low-level inotify API.
110 110
111 111 Also adds derived information to each event that is not available
112 112 through the normal inotify API, such as directory name.'''
113 113
114 114 __slots__ = (
115 115 'fd',
116 116 '_paths',
117 117 '_wds',
118 118 )
119 119
120 120 def __init__(self):
121 121 '''Create a new inotify instance.'''
122 122
123 123 self.fd = inotify.init()
124 124 self._paths = {}
125 125 self._wds = {}
126 126
127 127 def fileno(self):
128 128 '''Return the file descriptor this watcher uses.
129 129
130 130 Useful for passing to select and poll.'''
131 131
132 132 return self.fd
133 133
134 134 def add(self, path, mask):
135 135 '''Add or modify a watch.
136 136
137 137 Return the watch descriptor added or modified.'''
138 138
139 139 path = os.path.normpath(path)
140 140 wd = inotify.add_watch(self.fd, path, mask)
141 141 self._paths[path] = wd, mask
142 142 self._wds[wd] = path, mask
143 143 return wd
144 144
145 145 def remove(self, wd):
146 146 '''Remove the given watch.'''
147 147
148 148 inotify.remove_watch(self.fd, wd)
149 149 self._remove(wd)
150 150
151 151 def _remove(self, wd):
152 152 path_mask = self._wds.pop(wd, None)
153 153 if path_mask is not None:
154 154 self._paths.pop(path_mask[0])
155 155
156 156 def path(self, path):
157 157 '''Return a (watch descriptor, event mask) pair for the given path.
158 158
159 159 If the path is not being watched, return None.'''
160 160
161 161 return self._paths.get(path)
162 162
163 163 def wd(self, wd):
164 164 '''Return a (path, event mask) pair for the given watch descriptor.
165 165
166 166 If the watch descriptor is not valid or not associated with
167 167 this watcher, return None.'''
168 168
169 169 return self._wds.get(wd)
170 170
171 171 def read(self, bufsize=None):
172 172 '''Read a list of queued inotify events.
173 173
174 174 If bufsize is zero, only return those events that can be read
175 175 immediately without blocking. Otherwise, block until events are
176 176 available.'''
177 177
178 178 events = []
179 179 for evt in inotify.read(self.fd, bufsize):
180 180 events.append(event(evt, self._wds[evt.wd][0]))
181 181 if evt.mask & inotify.IN_IGNORED:
182 182 self._remove(evt.wd)
183 183 elif evt.mask & inotify.IN_UNMOUNT:
184 184 self.close()
185 185 return events
186 186
187 187 def close(self):
188 188 '''Shut down this watcher.
189 189
190 190 All subsequent method calls are likely to raise exceptions.'''
191 191
192 192 os.close(self.fd)
193 193 self.fd = None
194 194 self._paths = None
195 195 self._wds = None
196 196
197 197 def __len__(self):
198 198 '''Return the number of active watches.'''
199 199
200 200 return len(self._paths)
201 201
202 202 def __iter__(self):
203 203 '''Yield a (path, watch descriptor, event mask) tuple for each
204 204 entry being watched.'''
205 205
206 206 for path, (wd, mask) in self._paths.iteritems():
207 207 yield path, wd, mask
208 208
209 209 def __del__(self):
210 210 if self.fd is not None:
211 211 os.close(self.fd)
212 212
213 213 ignored_errors = [errno.ENOENT, errno.EPERM, errno.ENOTDIR]
214 214
215 215 def add_iter(self, path, mask, onerror=None):
216 216 '''Add or modify watches over path and its subdirectories.
217 217
218 218 Yield each added or modified watch descriptor.
219 219
220 220 To ensure that this method runs to completion, you must
221 221 iterate over all of its results, even if you do not care what
222 222 they are. For example:
223 223
224 224 for wd in w.add_iter(path, mask):
225 225 pass
226 226
227 227 By default, errors are ignored. If optional arg "onerror" is
228 228 specified, it should be a function; it will be called with one
229 229 argument, an OSError instance. It can report the error to
230 230 continue with the walk, or raise the exception to abort the
231 231 walk.'''
232 232
233 233 # Add the IN_ONLYDIR flag to the event mask, to avoid a possible
234 234 # race when adding a subdirectory. In the time between the
235 235 # event being queued by the kernel and us processing it, the
236 236 # directory may have been deleted, or replaced with a different
237 237 # kind of entry with the same name.
238 238
239 239 submask = mask | inotify.IN_ONLYDIR
240 240
241 241 try:
242 242 yield self.add(path, mask)
243 243 except OSError, err:
244 244 if onerror and err.errno not in self.ignored_errors:
245 245 onerror(err)
246 246 for root, dirs, names in os.walk(path, topdown=False, onerror=onerror):
247 247 for d in dirs:
248 248 try:
249 249 yield self.add(root + '/' + d, submask)
250 250 except OSError, err:
251 251 if onerror and err.errno not in self.ignored_errors:
252 252 onerror(err)
253 253
254 254 def add_all(self, path, mask, onerror=None):
255 255 '''Add or modify watches over path and its subdirectories.
256 256
257 257 Return a list of added or modified watch descriptors.
258 258
259 259 By default, errors are ignored. If optional arg "onerror" is
260 260 specified, it should be a function; it will be called with one
261 261 argument, an OSError instance. It can report the error to
262 262 continue with the walk, or raise the exception to abort the
263 263 walk.'''
264 264
265 265 return [w for w in self.add_iter(path, mask, onerror)]
266 266
267 267
268 268 class autowatcher(watcher):
269 269 '''watcher class that automatically watches newly created directories.'''
270 270
271 271 __slots__ = (
272 272 'addfilter',
273 273 )
274 274
275 275 def __init__(self, addfilter=None):
276 276 '''Create a new inotify instance.
277 277
278 278 This instance will automatically watch newly created
279 279 directories.
280 280
281 281 If the optional addfilter parameter is not None, it must be a
282 282 callable that takes one parameter. It will be called each time
283 283 a directory is about to be automatically watched. If it returns
284 284 True, the directory will be watched if it still exists,
285 285 otherwise, it will beb skipped.'''
286 286
287 287 super(autowatcher, self).__init__()
288 288 self.addfilter = addfilter
289 289
290 290 _dir_create_mask = inotify.IN_ISDIR | inotify.IN_CREATE
291 291
292 292 def read(self, bufsize=None):
293 293 events = super(autowatcher, self).read(bufsize)
294 294 for evt in events:
295 295 if evt.mask & self._dir_create_mask == self._dir_create_mask:
296 296 if self.addfilter is None or self.addfilter(evt):
297 297 parentmask = self._wds[evt.wd][1]
298 298 # See note about race avoidance via IN_ONLYDIR above.
299 299 mask = parentmask | inotify.IN_ONLYDIR
300 300 try:
301 301 self.add_all(evt.fullpath, mask)
302 302 except OSError, err:
303 303 if err.errno not in self.ignored_errors:
304 304 raise
305 305 return events
306 306
307 307
308 308 class threshold(object):
309 309 '''Class that indicates whether a file descriptor has reached a
310 310 threshold of readable bytes available.
311 311
312 312 This class is not thread-safe.'''
313 313
314 314 __slots__ = (
315 315 'fd',
316 316 'threshold',
317 317 '_iocbuf',
318 318 )
319 319
320 320 def __init__(self, fd, threshold=1024):
321 321 self.fd = fd
322 322 self.threshold = threshold
323 323 self._iocbuf = array.array('i', [0])
324 324
325 325 def readable(self):
326 326 '''Return the number of bytes readable on this file descriptor.'''
327 327
328 328 fcntl.ioctl(self.fd, termios.FIONREAD, self._iocbuf, True)
329 329 return self._iocbuf[0]
330 330
331 331 def __call__(self):
332 332 '''Indicate whether the number of readable bytes has met or
333 333 exceeded the threshold.'''
334 334
335 335 return self.readable() >= self.threshold
@@ -1,441 +1,441 b''
1 1 # linuxserver.py - inotify status server for linux
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2 or any later version.
8 8
9 9 from mercurial.i18n import _
10 10 from mercurial import osutil, util
11 11 import common
12 12 import server
13 13 import errno, os, select, stat, sys, time
14 14
15 15 try:
16 16 import linux as inotify
17 17 from linux import watcher
18 18 except ImportError:
19 19 raise
20 20
21 21 def walkrepodirs(dirstate, absroot):
22 22 '''Iterate over all subdirectories of this repo.
23 23 Exclude the .hg directory, any nested repos, and ignored dirs.'''
24 24 def walkit(dirname, top):
25 25 fullpath = server.join(absroot, dirname)
26 26 try:
27 27 for name, kind in osutil.listdir(fullpath):
28 28 if kind == stat.S_IFDIR:
29 29 if name == '.hg':
30 30 if not top:
31 31 return
32 32 else:
33 33 d = server.join(dirname, name)
34 34 if dirstate._ignore(d):
35 35 continue
36 36 for subdir in walkit(d, False):
37 37 yield subdir
38 38 except OSError, err:
39 39 if err.errno not in server.walk_ignored_errors:
40 40 raise
41 41 yield fullpath
42 42
43 43 return walkit('', True)
44 44
45 45 def _explain_watch_limit(ui, dirstate, rootabs):
46 46 path = '/proc/sys/fs/inotify/max_user_watches'
47 47 try:
48 48 limit = int(file(path).read())
49 49 except IOError, err:
50 50 if err.errno != errno.ENOENT:
51 51 raise
52 52 raise util.Abort(_('this system does not seem to '
53 53 'support inotify'))
54 54 ui.warn(_('*** the current per-user limit on the number '
55 55 'of inotify watches is %s\n') % limit)
56 56 ui.warn(_('*** this limit is too low to watch every '
57 57 'directory in this repository\n'))
58 58 ui.warn(_('*** counting directories: '))
59 59 ndirs = len(list(walkrepodirs(dirstate, rootabs)))
60 60 ui.warn(_('found %d\n') % ndirs)
61 61 newlimit = min(limit, 1024)
62 62 while newlimit < ((limit + ndirs) * 1.1):
63 63 newlimit *= 2
64 64 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
65 65 (limit, newlimit))
66 66 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
67 67 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
68 68 % rootabs)
69 69
70 70 class pollable(object):
71 71 """
72 72 Interface to support polling.
73 73 The file descriptor returned by fileno() is registered to a polling
74 74 object.
75 75 Usage:
76 76 Every tick, check if an event has happened since the last tick:
77 77 * If yes, call handle_events
78 78 * If no, call handle_timeout
79 79 """
80 80 poll_events = select.POLLIN
81 81 instances = {}
82 82 poll = select.poll()
83 83
84 84 def fileno(self):
85 85 raise NotImplementedError
86 86
87 87 def handle_events(self, events):
88 88 raise NotImplementedError
89 89
90 90 def handle_timeout(self):
91 91 raise NotImplementedError
92 92
93 93 def shutdown(self):
94 94 raise NotImplementedError
95 95
96 96 def register(self, timeout):
97 97 fd = self.fileno()
98 98
99 99 pollable.poll.register(fd, pollable.poll_events)
100 100 pollable.instances[fd] = self
101 101
102 102 self.registered = True
103 103 self.timeout = timeout
104 104
105 105 def unregister(self):
106 106 pollable.poll.unregister(self)
107 107 self.registered = False
108 108
109 109 @classmethod
110 110 def run(cls):
111 111 while True:
112 112 timeout = None
113 113 timeobj = None
114 114 for obj in cls.instances.itervalues():
115 115 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
116 116 timeout, timeobj = obj.timeout, obj
117 117 try:
118 118 events = cls.poll.poll(timeout)
119 119 except select.error, err:
120 120 if err[0] == errno.EINTR:
121 121 continue
122 122 raise
123 123 if events:
124 124 by_fd = {}
125 125 for fd, event in events:
126 126 by_fd.setdefault(fd, []).append(event)
127 127
128 128 for fd, events in by_fd.iteritems():
129 129 cls.instances[fd].handle_pollevents(events)
130 130
131 131 elif timeobj:
132 132 timeobj.handle_timeout()
133 133
134 134 def eventaction(code):
135 135 """
136 136 Decorator to help handle events in repowatcher
137 137 """
138 138 def decorator(f):
139 139 def wrapper(self, wpath):
140 140 if code == 'm' and wpath in self.lastevent and \
141 141 self.lastevent[wpath] in 'cm':
142 142 return
143 143 self.lastevent[wpath] = code
144 144 self.timeout = 250
145 145
146 146 f(self, wpath)
147 147
148 148 wrapper.func_name = f.func_name
149 149 return wrapper
150 150 return decorator
151 151
152 152 class repowatcher(server.repowatcher, pollable):
153 153 """
154 154 Watches inotify events
155 155 """
156 156 mask = (
157 157 inotify.IN_ATTRIB |
158 158 inotify.IN_CREATE |
159 159 inotify.IN_DELETE |
160 160 inotify.IN_DELETE_SELF |
161 161 inotify.IN_MODIFY |
162 162 inotify.IN_MOVED_FROM |
163 163 inotify.IN_MOVED_TO |
164 164 inotify.IN_MOVE_SELF |
165 165 inotify.IN_ONLYDIR |
166 166 inotify.IN_UNMOUNT |
167 167 0)
168 168
169 169 def __init__(self, ui, dirstate, root):
170 170 server.repowatcher.__init__(self, ui, dirstate, root)
171 171
172 172 self.lastevent = {}
173 173 self.dirty = False
174 174 try:
175 175 self.watcher = watcher.watcher()
176 176 except OSError, err:
177 177 raise util.Abort(_('inotify service not available: %s') %
178 178 err.strerror)
179 179 self.threshold = watcher.threshold(self.watcher)
180 180 self.fileno = self.watcher.fileno
181 181 self.register(timeout=None)
182 182
183 183 self.handle_timeout()
184 184 self.scan()
185 185
186 186 def event_time(self):
187 187 last = self.last_event
188 188 now = time.time()
189 189 self.last_event = now
190 190
191 191 if last is None:
192 192 return 'start'
193 193 delta = now - last
194 194 if delta < 5:
195 195 return '+%.3f' % delta
196 196 if delta < 50:
197 197 return '+%.2f' % delta
198 198 return '+%.1f' % delta
199 199
200 200 def add_watch(self, path, mask):
201 201 if not path:
202 202 return
203 203 if self.watcher.path(path) is None:
204 204 if self.ui.debugflag:
205 205 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
206 206 try:
207 207 self.watcher.add(path, mask)
208 208 except OSError, err:
209 209 if err.errno in (errno.ENOENT, errno.ENOTDIR):
210 210 return
211 211 if err.errno != errno.ENOSPC:
212 212 raise
213 213 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
214 214
215 215 def setup(self):
216 216 self.ui.note(_('watching directories under %r\n') % self.wprefix)
217 217 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
218 218
219 219 def scan(self, topdir=''):
220 220 ds = self.dirstate._map.copy()
221 221 self.add_watch(server.join(self.wprefix, topdir), self.mask)
222 222 for root, dirs, files in server.walk(self.dirstate, self.wprefix,
223 223 topdir):
224 224 for d in dirs:
225 225 self.add_watch(server.join(root, d), self.mask)
226 226 wroot = root[self.prefixlen:]
227 227 for fn in files:
228 228 wfn = server.join(wroot, fn)
229 229 self.updatefile(wfn, self.getstat(wfn))
230 230 ds.pop(wfn, None)
231 231 wtopdir = topdir
232 232 if wtopdir and wtopdir[-1] != '/':
233 233 wtopdir += '/'
234 234 for wfn, state in ds.iteritems():
235 235 if not wfn.startswith(wtopdir):
236 236 continue
237 237 try:
238 238 st = self.stat(wfn)
239 239 except OSError:
240 240 status = state[0]
241 241 self.deletefile(wfn, status)
242 242 else:
243 243 self.updatefile(wfn, st)
244 244 self.check_deleted('!')
245 245 self.check_deleted('r')
246 246
247 247 @eventaction('c')
248 248 def created(self, wpath):
249 249 if wpath == '.hgignore':
250 250 self.update_hgignore()
251 251 try:
252 252 st = self.stat(wpath)
253 253 if stat.S_ISREG(st[0]) or stat.S_ISLNK(st[0]):
254 254 self.updatefile(wpath, st)
255 255 except OSError:
256 256 pass
257 257
258 258 @eventaction('m')
259 259 def modified(self, wpath):
260 260 if wpath == '.hgignore':
261 261 self.update_hgignore()
262 262 try:
263 263 st = self.stat(wpath)
264 264 if stat.S_ISREG(st[0]):
265 265 if self.dirstate[wpath] in 'lmn':
266 266 self.updatefile(wpath, st)
267 267 except OSError:
268 268 pass
269 269
270 270 @eventaction('d')
271 271 def deleted(self, wpath):
272 272 if wpath == '.hgignore':
273 273 self.update_hgignore()
274 274 elif wpath.startswith('.hg/'):
275 275 return
276 276
277 277 self.deletefile(wpath, self.dirstate[wpath])
278 278
279 279 def process_create(self, wpath, evt):
280 280 if self.ui.debugflag:
281 281 self.ui.note(_('%s event: created %s\n') %
282 282 (self.event_time(), wpath))
283 283
284 284 if evt.mask & inotify.IN_ISDIR:
285 285 self.scan(wpath)
286 286 else:
287 287 self.created(wpath)
288 288
289 289 def process_delete(self, wpath, evt):
290 290 if self.ui.debugflag:
291 291 self.ui.note(_('%s event: deleted %s\n') %
292 292 (self.event_time(), wpath))
293 293
294 294 if evt.mask & inotify.IN_ISDIR:
295 295 tree = self.tree.dir(wpath)
296 296 todelete = [wfn for wfn, ignore in tree.walk('?')]
297 297 for fn in todelete:
298 298 self.deletefile(fn, '?')
299 299 self.scan(wpath)
300 300 else:
301 301 self.deleted(wpath)
302 302
303 303 def process_modify(self, wpath, evt):
304 304 if self.ui.debugflag:
305 305 self.ui.note(_('%s event: modified %s\n') %
306 306 (self.event_time(), wpath))
307 307
308 308 if not (evt.mask & inotify.IN_ISDIR):
309 309 self.modified(wpath)
310 310
311 311 def process_unmount(self, evt):
312 312 self.ui.warn(_('filesystem containing %s was unmounted\n') %
313 313 evt.fullpath)
314 314 sys.exit(0)
315 315
316 316 def handle_pollevents(self, events):
317 317 if self.ui.debugflag:
318 318 self.ui.note(_('%s readable: %d bytes\n') %
319 319 (self.event_time(), self.threshold.readable()))
320 320 if not self.threshold():
321 321 if self.registered:
322 322 if self.ui.debugflag:
323 323 self.ui.note(_('%s below threshold - unhooking\n') %
324 324 (self.event_time()))
325 325 self.unregister()
326 326 self.timeout = 250
327 327 else:
328 328 self.read_events()
329 329
330 330 def read_events(self, bufsize=None):
331 331 events = self.watcher.read(bufsize)
332 332 if self.ui.debugflag:
333 333 self.ui.note(_('%s reading %d events\n') %
334 334 (self.event_time(), len(events)))
335 335 for evt in events:
336 336 if evt.fullpath == self.wprefix[:-1]:
337 337 # events on the root of the repository
338 338 # itself, e.g. permission changes or repository move
339 339 continue
340 340 assert evt.fullpath.startswith(self.wprefix)
341 341 wpath = evt.fullpath[self.prefixlen:]
342 342
343 343 # paths have been normalized, wpath never ends with a '/'
344 344
345 345 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
346 346 # ignore subdirectories of .hg/ (merge, patches...)
347 347 continue
348 348 if wpath == ".hg/wlock":
349 349 if evt.mask & inotify.IN_DELETE:
350 350 self.dirstate.invalidate()
351 351 self.dirty = False
352 352 self.scan()
353 353 elif evt.mask & inotify.IN_CREATE:
354 354 self.dirty = True
355 355 else:
356 356 if self.dirty:
357 357 continue
358 358
359 359 if evt.mask & inotify.IN_UNMOUNT:
360 360 self.process_unmount(wpath, evt)
361 361 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
362 362 self.process_modify(wpath, evt)
363 363 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
364 364 inotify.IN_MOVED_FROM):
365 365 self.process_delete(wpath, evt)
366 366 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
367 367 self.process_create(wpath, evt)
368 368
369 369 self.lastevent.clear()
370 370
371 371 def handle_timeout(self):
372 372 if not self.registered:
373 373 if self.ui.debugflag:
374 374 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
375 375 (self.event_time(), self.threshold.readable()))
376 376 self.read_events(0)
377 377 self.register(timeout=None)
378 378
379 379 self.timeout = None
380 380
381 381 def shutdown(self):
382 382 self.watcher.close()
383 383
384 384 def debug(self):
385 385 """
386 386 Returns a sorted list of relatives paths currently watched,
387 387 for debugging purposes.
388 388 """
389 389 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
390 390
391 391 class socketlistener(server.socketlistener, pollable):
392 392 """
393 393 Listens for client queries on unix socket inotify.sock
394 394 """
395 395 def __init__(self, ui, root, repowatcher, timeout):
396 396 server.socketlistener.__init__(self, ui, root, repowatcher, timeout)
397 397 self.register(timeout=timeout)
398 398
399 399 def handle_timeout(self):
400 400 pass
401 401
402 402 def handle_pollevents(self, events):
403 403 for e in events:
404 404 self.accept_connection()
405 405
406 406 def shutdown(self):
407 407 self.sock.close()
408 408 try:
409 409 os.unlink(self.sockpath)
410 410 if self.realsockpath:
411 411 os.unlink(self.realsockpath)
412 412 os.rmdir(os.path.dirname(self.realsockpath))
413 413 except OSError, err:
414 414 if err.errno != errno.ENOENT:
415 415 raise
416 416
417 417 def answer_stat_query(self, cs):
418 418 if self.repowatcher.timeout:
419 419 # We got a query while a rescan is pending. Make sure we
420 420 # rescan before responding, or we could give back a wrong
421 421 # answer.
422 422 self.repowatcher.handle_timeout()
423 423 return server.socketlistener.answer_stat_query(self, cs)
424 424
425 425 class master(object):
426 426 def __init__(self, ui, dirstate, root, timeout=None):
427 427 self.ui = ui
428 428 self.repowatcher = repowatcher(ui, dirstate, root)
429 429 self.socketlistener = socketlistener(ui, root, self.repowatcher,
430 430 timeout)
431 431
432 432 def shutdown(self):
433 433 for obj in pollable.instances.itervalues():
434 434 obj.shutdown()
435 435
436 436 def run(self):
437 437 self.repowatcher.setup()
438 438 self.ui.note(_('finished setup\n'))
439 439 if os.getenv('TIME_STARTUP'):
440 440 sys.exit(0)
441 441 pollable.run()
@@ -1,478 +1,478 b''
1 1 # server.py - common entry point for inotify status server
2 2 #
3 3 # Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial import cmdutil, osutil, util
10 10 import common
11 11
12 12 import errno
13 13 import os
14 14 import socket
15 15 import stat
16 16 import struct
17 17 import sys
18 18 import tempfile
19 19
20 20 class AlreadyStartedException(Exception): pass
21 21
22 22 def join(a, b):
23 23 if a:
24 24 if a[-1] == '/':
25 25 return a + b
26 26 return a + '/' + b
27 27 return b
28 28
29 29 def split(path):
30 30 c = path.rfind('/')
31 31 if c == -1:
32 32 return '', path
33 33 return path[:c], path[c+1:]
34 34
35 35 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
36 36
37 37 def walk(dirstate, absroot, root):
38 38 '''Like os.walk, but only yields regular files.'''
39 39
40 40 # This function is critical to performance during startup.
41 41
42 42 def walkit(root, reporoot):
43 43 files, dirs = [], []
44 44
45 45 try:
46 46 fullpath = join(absroot, root)
47 47 for name, kind in osutil.listdir(fullpath):
48 48 if kind == stat.S_IFDIR:
49 49 if name == '.hg':
50 50 if not reporoot:
51 51 return
52 52 else:
53 53 dirs.append(name)
54 54 path = join(root, name)
55 55 if dirstate._ignore(path):
56 56 continue
57 57 for result in walkit(path, False):
58 58 yield result
59 59 elif kind in (stat.S_IFREG, stat.S_IFLNK):
60 60 files.append(name)
61 61 yield fullpath, dirs, files
62 62
63 63 except OSError, err:
64 64 if err.errno == errno.ENOTDIR:
65 65 # fullpath was a directory, but has since been replaced
66 66 # by a file.
67 67 yield fullpath, dirs, files
68 68 elif err.errno not in walk_ignored_errors:
69 69 raise
70 70
71 71 return walkit(root, root == '')
72 72
73 73 class directory(object):
74 74 """
75 75 Representing a directory
76 76
77 77 * path is the relative path from repo root to this directory
78 78 * files is a dict listing the files in this directory
79 79 - keys are file names
80 80 - values are file status
81 81 * dirs is a dict listing the subdirectories
82 82 - key are subdirectories names
83 83 - values are directory objects
84 84 """
85 85 def __init__(self, relpath=''):
86 86 self.path = relpath
87 87 self.files = {}
88 88 self.dirs = {}
89 89
90 90 def dir(self, relpath):
91 91 """
92 92 Returns the directory contained at the relative path relpath.
93 93 Creates the intermediate directories if necessary.
94 94 """
95 95 if not relpath:
96 96 return self
97 97 l = relpath.split('/')
98 98 ret = self
99 99 while l:
100 100 next = l.pop(0)
101 101 try:
102 102 ret = ret.dirs[next]
103 103 except KeyError:
104 104 d = directory(join(ret.path, next))
105 105 ret.dirs[next] = d
106 106 ret = d
107 107 return ret
108 108
109 109 def walk(self, states, visited=None):
110 110 """
111 111 yield (filename, status) pairs for items in the trees
112 112 that have status in states.
113 113 filenames are relative to the repo root
114 114 """
115 115 for file, st in self.files.iteritems():
116 116 if st in states:
117 117 yield join(self.path, file), st
118 118 for dir in self.dirs.itervalues():
119 119 if visited is not None:
120 120 visited.add(dir.path)
121 121 for e in dir.walk(states):
122 122 yield e
123 123
124 124 def lookup(self, states, path, visited):
125 125 """
126 126 yield root-relative filenames that match path, and whose
127 127 status are in states:
128 128 * if path is a file, yield path
129 129 * if path is a directory, yield directory files
130 130 * if path is not tracked, yield nothing
131 131 """
132 132 if path[-1] == '/':
133 133 path = path[:-1]
134 134
135 135 paths = path.split('/')
136 136
137 137 # we need to check separately for last node
138 138 last = paths.pop()
139 139
140 140 tree = self
141 141 try:
142 142 for dir in paths:
143 143 tree = tree.dirs[dir]
144 144 except KeyError:
145 145 # path is not tracked
146 146 visited.add(tree.path)
147 147 return
148 148
149 149 try:
150 150 # if path is a directory, walk it
151 151 target = tree.dirs[last]
152 152 visited.add(target.path)
153 153 for file, st in target.walk(states, visited):
154 154 yield file
155 155 except KeyError:
156 156 try:
157 157 if tree.files[last] in states:
158 158 # path is a file
159 159 visited.add(tree.path)
160 160 yield path
161 161 except KeyError:
162 162 # path is not tracked
163 163 pass
164 164
165 165 class repowatcher(object):
166 166 """
167 167 Watches inotify events
168 168 """
169 169 statuskeys = 'almr!?'
170 170
171 171 def __init__(self, ui, dirstate, root):
172 172 self.ui = ui
173 173 self.dirstate = dirstate
174 174
175 175 self.wprefix = join(root, '')
176 176 self.prefixlen = len(self.wprefix)
177 177
178 178 self.tree = directory()
179 179 self.statcache = {}
180 180 self.statustrees = dict([(s, directory()) for s in self.statuskeys])
181 181
182 182 self.ds_info = self.dirstate_info()
183 183
184 184 self.last_event = None
185 185
186 186
187 187 def handle_timeout(self):
188 188 pass
189 189
190 190 def dirstate_info(self):
191 191 try:
192 192 st = os.lstat(self.wprefix + '.hg/dirstate')
193 193 return st.st_mtime, st.st_ino
194 194 except OSError, err:
195 195 if err.errno != errno.ENOENT:
196 196 raise
197 197 return 0, 0
198 198
199 199 def filestatus(self, fn, st):
200 200 try:
201 201 type_, mode, size, time = self.dirstate._map[fn][:4]
202 202 except KeyError:
203 203 type_ = '?'
204 204 if type_ == 'n':
205 205 st_mode, st_size, st_mtime = st
206 206 if size == -1:
207 207 return 'l'
208 208 if size and (size != st_size or (mode ^ st_mode) & 0100):
209 209 return 'm'
210 210 if time != int(st_mtime):
211 211 return 'l'
212 212 return 'n'
213 213 if type_ == '?' and self.dirstate._ignore(fn):
214 214 return 'i'
215 215 return type_
216 216
217 217 def updatefile(self, wfn, osstat):
218 218 '''
219 219 update the file entry of an existing file.
220 220
221 221 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
222 222 '''
223 223
224 224 self._updatestatus(wfn, self.filestatus(wfn, osstat))
225 225
226 226 def deletefile(self, wfn, oldstatus):
227 227 '''
228 228 update the entry of a file which has been deleted.
229 229
230 230 oldstatus: char in statuskeys, status of the file before deletion
231 231 '''
232 232 if oldstatus == 'r':
233 233 newstatus = 'r'
234 234 elif oldstatus in 'almn':
235 235 newstatus = '!'
236 236 else:
237 237 newstatus = None
238 238
239 239 self.statcache.pop(wfn, None)
240 240 self._updatestatus(wfn, newstatus)
241 241
242 242 def _updatestatus(self, wfn, newstatus):
243 243 '''
244 244 Update the stored status of a file.
245 245
246 246 newstatus: - char in (statuskeys + 'ni'), new status to apply.
247 247 - or None, to stop tracking wfn
248 248 '''
249 249 root, fn = split(wfn)
250 250 d = self.tree.dir(root)
251 251
252 252 oldstatus = d.files.get(fn)
253 253 # oldstatus can be either:
254 254 # - None : fn is new
255 255 # - a char in statuskeys: fn is a (tracked) file
256 256
257 257 if self.ui.debugflag and oldstatus != newstatus:
258 258 self.ui.note(_('status: %r %s -> %s\n') %
259 259 (wfn, oldstatus, newstatus))
260 260
261 261 if oldstatus and oldstatus in self.statuskeys \
262 262 and oldstatus != newstatus:
263 263 del self.statustrees[oldstatus].dir(root).files[fn]
264 264
265 265 if newstatus in (None, 'i'):
266 266 d.files.pop(fn, None)
267 267 elif oldstatus != newstatus:
268 268 d.files[fn] = newstatus
269 269 if newstatus != 'n':
270 270 self.statustrees[newstatus].dir(root).files[fn] = newstatus
271 271
272 272 def check_deleted(self, key):
273 273 # Files that had been deleted but were present in the dirstate
274 274 # may have vanished from the dirstate; we must clean them up.
275 275 nuke = []
276 276 for wfn, ignore in self.statustrees[key].walk(key):
277 277 if wfn not in self.dirstate:
278 278 nuke.append(wfn)
279 279 for wfn in nuke:
280 280 root, fn = split(wfn)
281 281 del self.statustrees[key].dir(root).files[fn]
282 282 del self.tree.dir(root).files[fn]
283 283
284 284 def update_hgignore(self):
285 285 # An update of the ignore file can potentially change the
286 286 # states of all unknown and ignored files.
287 287
288 288 # XXX If the user has other ignore files outside the repo, or
289 289 # changes their list of ignore files at run time, we'll
290 290 # potentially never see changes to them. We could get the
291 291 # client to report to us what ignore data they're using.
292 292 # But it's easier to do nothing than to open that can of
293 293 # worms.
294 294
295 295 if '_ignore' in self.dirstate.__dict__:
296 296 delattr(self.dirstate, '_ignore')
297 297 self.ui.note(_('rescanning due to .hgignore change\n'))
298 298 self.handle_timeout()
299 299 self.scan()
300 300
301 301 def getstat(self, wpath):
302 302 try:
303 303 return self.statcache[wpath]
304 304 except KeyError:
305 305 try:
306 306 return self.stat(wpath)
307 307 except OSError, err:
308 308 if err.errno != errno.ENOENT:
309 309 raise
310 310
311 311 def stat(self, wpath):
312 312 try:
313 313 st = os.lstat(join(self.wprefix, wpath))
314 314 ret = st.st_mode, st.st_size, st.st_mtime
315 315 self.statcache[wpath] = ret
316 316 return ret
317 317 except OSError:
318 318 self.statcache.pop(wpath, None)
319 319 raise
320 320
321 321 class socketlistener(object):
322 322 """
323 323 Listens for client queries on unix socket inotify.sock
324 324 """
325 325 def __init__(self, ui, root, repowatcher, timeout):
326 326 self.ui = ui
327 327 self.repowatcher = repowatcher
328 328 self.sock = socket.socket(socket.AF_UNIX)
329 329 self.sockpath = join(root, '.hg/inotify.sock')
330 330 self.realsockpath = None
331 331 try:
332 332 self.sock.bind(self.sockpath)
333 333 except socket.error, err:
334 334 if err[0] == errno.EADDRINUSE:
335 335 raise AlreadyStartedException( _('cannot start: socket is '
336 336 'already bound'))
337 337 if err[0] == "AF_UNIX path too long":
338 338 if os.path.islink(self.sockpath) and \
339 339 not os.path.exists(self.sockpath):
340 340 raise util.Abort('inotify-server: cannot start: '
341 341 '.hg/inotify.sock is a broken symlink')
342 342 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
343 343 self.realsockpath = os.path.join(tempdir, "inotify.sock")
344 344 try:
345 345 self.sock.bind(self.realsockpath)
346 346 os.symlink(self.realsockpath, self.sockpath)
347 347 except (OSError, socket.error), inst:
348 348 try:
349 349 os.unlink(self.realsockpath)
350 350 except:
351 351 pass
352 352 os.rmdir(tempdir)
353 353 if inst.errno == errno.EEXIST:
354 354 raise AlreadyStartedException(_('cannot start: tried '
355 355 'linking .hg/inotify.sock to a temporary socket but'
356 356 ' .hg/inotify.sock already exists'))
357 357 raise
358 358 else:
359 359 raise
360 360 self.sock.listen(5)
361 361 self.fileno = self.sock.fileno
362 362
363 363 def answer_stat_query(self, cs):
364 364 names = cs.read().split('\0')
365 365
366 366 states = names.pop()
367 367
368 368 self.ui.note(_('answering query for %r\n') % states)
369 369
370 370 visited = set()
371 371 if not names:
372 372 def genresult(states, tree):
373 373 for fn, state in tree.walk(states):
374 374 yield fn
375 375 else:
376 376 def genresult(states, tree):
377 377 for fn in names:
378 378 for f in tree.lookup(states, fn, visited):
379 379 yield f
380 380
381 381 return ['\0'.join(r) for r in [
382 382 genresult('l', self.repowatcher.statustrees['l']),
383 383 genresult('m', self.repowatcher.statustrees['m']),
384 384 genresult('a', self.repowatcher.statustrees['a']),
385 385 genresult('r', self.repowatcher.statustrees['r']),
386 386 genresult('!', self.repowatcher.statustrees['!']),
387 387 '?' in states
388 388 and genresult('?', self.repowatcher.statustrees['?'])
389 389 or [],
390 390 [],
391 391 'c' in states and genresult('n', self.repowatcher.tree) or [],
392 392 visited
393 393 ]]
394 394
395 395 def answer_dbug_query(self):
396 396 return ['\0'.join(self.repowatcher.debug())]
397 397
398 398 def accept_connection(self):
399 399 sock, addr = self.sock.accept()
400 400
401 401 cs = common.recvcs(sock)
402 402 version = ord(cs.read(1))
403 403
404 404 if version != common.version:
405 405 self.ui.warn(_('received query from incompatible client '
406 406 'version %d\n') % version)
407 407 try:
408 408 # try to send back our version to the client
409 409 # this way, the client too is informed of the mismatch
410 410 sock.sendall(chr(common.version))
411 411 except:
412 412 pass
413 413 return
414 414
415 415 type = cs.read(4)
416 416
417 417 if type == 'STAT':
418 418 results = self.answer_stat_query(cs)
419 419 elif type == 'DBUG':
420 420 results = self.answer_dbug_query()
421 421 else:
422 422 self.ui.warn(_('unrecognized query type: %s\n') % type)
423 423 return
424 424
425 425 try:
426 426 try:
427 427 v = chr(common.version)
428 428
429 429 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
430 430 *map(len, results)))
431 431 sock.sendall(''.join(results))
432 432 finally:
433 433 sock.shutdown(socket.SHUT_WR)
434 434 except socket.error, err:
435 435 if err[0] != errno.EPIPE:
436 436 raise
437 437
438 438 if sys.platform == 'linux2':
439 439 import linuxserver as _server
440 440 else:
441 441 raise ImportError
442 442
443 443 master = _server.master
444 444
445 445 def start(ui, dirstate, root, opts):
446 446 timeout = opts.get('timeout')
447 447 if timeout:
448 448 timeout = float(timeout) * 1e3
449 449
450 450 class service(object):
451 451 def init(self):
452 452 try:
453 453 self.master = master(ui, dirstate, root, timeout)
454 454 except AlreadyStartedException, inst:
455 455 raise util.Abort("inotify-server: %s" % inst)
456 456
457 457 def run(self):
458 458 try:
459 459 self.master.run()
460 460 finally:
461 461 self.master.shutdown()
462 462
463 463 if 'inserve' not in sys.argv:
464 464 runargs = util.hgcmd() + ['inserve', '-R', root]
465 465 else:
466 466 runargs = util.hgcmd() + sys.argv[1:]
467 467
468 468 pidfile = ui.config('inotify', 'pidfile')
469 469 if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs:
470 470 runargs.append("--pid-file=%s" % pidfile)
471 471
472 472 service = service()
473 473 logfile = ui.config('inotify', 'log')
474 474
475 475 appendpid = ui.configbool('inotify', 'appendpid', False)
476 476
477 477 cmdutil.service(opts, initfn=service.init, runfn=service.run,
478 478 logfile=logfile, runargs=runargs, appendpid=appendpid)
@@ -1,80 +1,80 b''
1 1 # interhg.py - interhg
2 2 #
3 3 # Copyright 2007 OHASHI Hideya <ohachige@gmail.com>
4 4 #
5 5 # Contributor(s):
6 6 # Edward Lee <edward.lee@engineering.uiuc.edu>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2, incorporated herein by reference.
9 # GNU General Public License version 2 or any later version.
10 10
11 11 '''expand expressions into changelog and summaries
12 12
13 13 This extension allows the use of a special syntax in summaries, which
14 14 will be automatically expanded into links or any other arbitrary
15 15 expression, much like InterWiki does.
16 16
17 17 A few example patterns (link to bug tracking, etc.) that may be used
18 18 in your hgrc::
19 19
20 20 [interhg]
21 21 issues = s!issue(\\d+)!<a href="http://bts/issue\\1">issue\\1</a>!
22 22 bugzilla = s!((?:bug|b=|(?=#?\\d{4,}))(?:\\s*#?)(\\d+))!<a..=\\2">\\1</a>!i
23 23 boldify = s!(^|\\s)#(\\d+)\\b! <b>#\\2</b>!
24 24 '''
25 25
26 26 import re
27 27 from mercurial.hgweb import hgweb_mod
28 28 from mercurial import templatefilters, extensions
29 29 from mercurial.i18n import _
30 30
31 31 orig_escape = templatefilters.filters["escape"]
32 32
33 33 interhg_table = []
34 34
35 35 def interhg_escape(x):
36 36 escstr = orig_escape(x)
37 37 for regexp, format in interhg_table:
38 38 escstr = regexp.sub(format, escstr)
39 39 return escstr
40 40
41 41 templatefilters.filters["escape"] = interhg_escape
42 42
43 43 def interhg_refresh(orig, self):
44 44 interhg_table[:] = []
45 45 for key, pattern in self.repo.ui.configitems('interhg'):
46 46 # grab the delimiter from the character after the "s"
47 47 unesc = pattern[1]
48 48 delim = re.escape(unesc)
49 49
50 50 # identify portions of the pattern, taking care to avoid escaped
51 51 # delimiters. the replace format and flags are optional, but delimiters
52 52 # are required.
53 53 match = re.match(r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
54 54 % (delim, delim, delim), pattern)
55 55 if not match:
56 56 self.repo.ui.warn(_("interhg: invalid pattern for %s: %s\n")
57 57 % (key, pattern))
58 58 continue
59 59
60 60 # we need to unescape the delimiter for regexp and format
61 61 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
62 62 regexp = delim_re.sub(unesc, match.group(1))
63 63 format = delim_re.sub(unesc, match.group(2))
64 64
65 65 # the pattern allows for 6 regexp flags, so set them if necessary
66 66 flagin = match.group(3)
67 67 flags = 0
68 68 if flagin:
69 69 for flag in flagin.upper():
70 70 flags |= re.__dict__[flag]
71 71
72 72 try:
73 73 regexp = re.compile(regexp, flags)
74 74 interhg_table.append((regexp, format))
75 75 except re.error:
76 76 self.repo.ui.warn(_("interhg: invalid regexp for %s: %s\n")
77 77 % (key, regexp))
78 78 return orig(self)
79 79
80 80 extensions.wrapfunction(hgweb_mod.hgweb, 'refresh', interhg_refresh)
@@ -1,566 +1,566 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2009 Christian Ebert <blacktrash@gmx.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2 or any later version.
7 7 #
8 8 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a DSCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an
15 15 # audience not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Files to act upon/ignore are specified in the [keyword] section.
25 25 # Customized keyword template mappings in the [keywordmaps] section.
26 26 #
27 27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
28 28
29 29 '''expand keywords in tracked files
30 30
31 31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 32 tracked text files selected by your configuration.
33 33
34 34 Keywords are only expanded in local repositories and not stored in the
35 35 change history. The mechanism can be regarded as a convenience for the
36 36 current user or for archive distribution.
37 37
38 38 Configuration is done in the [keyword] and [keywordmaps] sections of
39 39 hgrc files.
40 40
41 41 Example::
42 42
43 43 [keyword]
44 44 # expand keywords in every python file except those matching "x*"
45 45 **.py =
46 46 x* = ignore
47 47
48 48 NOTE: the more specific you are in your filename patterns the less you
49 49 lose speed in huge repositories.
50 50
51 51 For [keywordmaps] template mapping and expansion demonstration and
52 52 control run "hg kwdemo". See "hg help templates" for a list of
53 53 available templates and filters.
54 54
55 55 An additional date template filter {date|utcdate} is provided. It
56 56 returns a date like "2006/09/18 15:13:13".
57 57
58 58 The default template mappings (view with "hg kwdemo -d") can be
59 59 replaced with customized keywords and templates. Again, run "hg
60 60 kwdemo" to control the results of your config changes.
61 61
62 62 Before changing/disabling active keywords, run "hg kwshrink" to avoid
63 63 the risk of inadvertently storing expanded keywords in the change
64 64 history.
65 65
66 66 To force expansion after enabling it, or a configuration change, run
67 67 "hg kwexpand".
68 68
69 69 Also, when committing with the record extension or using mq's qrecord,
70 70 be aware that keywords cannot be updated. Again, run "hg kwexpand" on
71 71 the files in question to update keyword expansions after all changes
72 72 have been checked in.
73 73
74 74 Expansions spanning more than one line and incremental expansions,
75 75 like CVS' $Log$, are not supported. A keyword template map "Log =
76 76 {desc}" expands to the first line of the changeset description.
77 77 '''
78 78
79 79 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
80 80 from mercurial import patch, localrepo, templater, templatefilters, util, match
81 81 from mercurial.hgweb import webcommands
82 82 from mercurial.lock import release
83 83 from mercurial.node import nullid
84 84 from mercurial.i18n import _
85 85 import re, shutil, tempfile
86 86
87 87 commands.optionalrepo += ' kwdemo'
88 88
89 89 # hg commands that do not act on keywords
90 90 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
91 91 ' log outgoing push rename rollback tip verify'
92 92 ' convert email glog')
93 93
94 94 # hg commands that trigger expansion only when writing to working dir,
95 95 # not when reading filelog, and unexpand when reading from working dir
96 96 restricted = ('merge record resolve qfold qimport qnew qpush qrefresh qrecord'
97 97 ' transplant')
98 98
99 99 # provide cvs-like UTC date filter
100 100 utcdate = lambda x: util.datestr(x, '%Y/%m/%d %H:%M:%S')
101 101
102 102 # make keyword tools accessible
103 103 kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
104 104
105 105
106 106 class kwtemplater(object):
107 107 '''
108 108 Sets up keyword templates, corresponding keyword regex, and
109 109 provides keyword substitution functions.
110 110 '''
111 111 templates = {
112 112 'Revision': '{node|short}',
113 113 'Author': '{author|user}',
114 114 'Date': '{date|utcdate}',
115 115 'RCSfile': '{file|basename},v',
116 116 'RCSFile': '{file|basename},v', # kept for backwards compatibility
117 117 # with hg-keyword
118 118 'Source': '{root}/{file},v',
119 119 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
120 120 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
121 121 }
122 122
123 123 def __init__(self, ui, repo):
124 124 self.ui = ui
125 125 self.repo = repo
126 126 self.match = match.match(repo.root, '', [],
127 127 kwtools['inc'], kwtools['exc'])
128 128 self.restrict = kwtools['hgcmd'] in restricted.split()
129 129
130 130 kwmaps = self.ui.configitems('keywordmaps')
131 131 if kwmaps: # override default templates
132 132 self.templates = dict((k, templater.parsestring(v, False))
133 133 for k, v in kwmaps)
134 134 escaped = map(re.escape, self.templates.keys())
135 135 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
136 136 self.re_kw = re.compile(kwpat)
137 137
138 138 templatefilters.filters['utcdate'] = utcdate
139 139 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
140 140 False, None, '', False)
141 141
142 142 def substitute(self, data, path, ctx, subfunc):
143 143 '''Replaces keywords in data with expanded template.'''
144 144 def kwsub(mobj):
145 145 kw = mobj.group(1)
146 146 self.ct.use_template(self.templates[kw])
147 147 self.ui.pushbuffer()
148 148 self.ct.show(ctx, root=self.repo.root, file=path)
149 149 ekw = templatefilters.firstline(self.ui.popbuffer())
150 150 return '$%s: %s $' % (kw, ekw)
151 151 return subfunc(kwsub, data)
152 152
153 153 def expand(self, path, node, data):
154 154 '''Returns data with keywords expanded.'''
155 155 if not self.restrict and self.match(path) and not util.binary(data):
156 156 ctx = self.repo.filectx(path, fileid=node).changectx()
157 157 return self.substitute(data, path, ctx, self.re_kw.sub)
158 158 return data
159 159
160 160 def iskwfile(self, path, flagfunc):
161 161 '''Returns true if path matches [keyword] pattern
162 162 and is not a symbolic link.
163 163 Caveat: localrepository._link fails on Windows.'''
164 164 return self.match(path) and not 'l' in flagfunc(path)
165 165
166 166 def overwrite(self, node, expand, files):
167 167 '''Overwrites selected files expanding/shrinking keywords.'''
168 168 ctx = self.repo[node]
169 169 mf = ctx.manifest()
170 170 if node is not None: # commit
171 171 files = [f for f in ctx.files() if f in mf]
172 172 notify = self.ui.debug
173 173 else: # kwexpand/kwshrink
174 174 notify = self.ui.note
175 175 candidates = [f for f in files if self.iskwfile(f, ctx.flags)]
176 176 if candidates:
177 177 self.restrict = True # do not expand when reading
178 178 msg = (expand and _('overwriting %s expanding keywords\n')
179 179 or _('overwriting %s shrinking keywords\n'))
180 180 for f in candidates:
181 181 fp = self.repo.file(f)
182 182 data = fp.read(mf[f])
183 183 if util.binary(data):
184 184 continue
185 185 if expand:
186 186 if node is None:
187 187 ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
188 188 data, found = self.substitute(data, f, ctx,
189 189 self.re_kw.subn)
190 190 else:
191 191 found = self.re_kw.search(data)
192 192 if found:
193 193 notify(msg % f)
194 194 self.repo.wwrite(f, data, mf.flags(f))
195 195 if node is None:
196 196 self.repo.dirstate.normal(f)
197 197 self.restrict = False
198 198
199 199 def shrinktext(self, text):
200 200 '''Unconditionally removes all keyword substitutions from text.'''
201 201 return self.re_kw.sub(r'$\1$', text)
202 202
203 203 def shrink(self, fname, text):
204 204 '''Returns text with all keyword substitutions removed.'''
205 205 if self.match(fname) and not util.binary(text):
206 206 return self.shrinktext(text)
207 207 return text
208 208
209 209 def shrinklines(self, fname, lines):
210 210 '''Returns lines with keyword substitutions removed.'''
211 211 if self.match(fname):
212 212 text = ''.join(lines)
213 213 if not util.binary(text):
214 214 return self.shrinktext(text).splitlines(True)
215 215 return lines
216 216
217 217 def wread(self, fname, data):
218 218 '''If in restricted mode returns data read from wdir with
219 219 keyword substitutions removed.'''
220 220 return self.restrict and self.shrink(fname, data) or data
221 221
222 222 class kwfilelog(filelog.filelog):
223 223 '''
224 224 Subclass of filelog to hook into its read, add, cmp methods.
225 225 Keywords are "stored" unexpanded, and processed on reading.
226 226 '''
227 227 def __init__(self, opener, kwt, path):
228 228 super(kwfilelog, self).__init__(opener, path)
229 229 self.kwt = kwt
230 230 self.path = path
231 231
232 232 def read(self, node):
233 233 '''Expands keywords when reading filelog.'''
234 234 data = super(kwfilelog, self).read(node)
235 235 return self.kwt.expand(self.path, node, data)
236 236
237 237 def add(self, text, meta, tr, link, p1=None, p2=None):
238 238 '''Removes keyword substitutions when adding to filelog.'''
239 239 text = self.kwt.shrink(self.path, text)
240 240 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
241 241
242 242 def cmp(self, node, text):
243 243 '''Removes keyword substitutions for comparison.'''
244 244 text = self.kwt.shrink(self.path, text)
245 245 if self.renamed(node):
246 246 t2 = super(kwfilelog, self).read(node)
247 247 return t2 != text
248 248 return revlog.revlog.cmp(self, node, text)
249 249
250 250 def _status(ui, repo, kwt, *pats, **opts):
251 251 '''Bails out if [keyword] configuration is not active.
252 252 Returns status of working directory.'''
253 253 if kwt:
254 254 unknown = (opts.get('unknown') or opts.get('all')
255 255 or opts.get('untracked'))
256 256 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
257 257 unknown=unknown)
258 258 if ui.configitems('keyword'):
259 259 raise util.Abort(_('[keyword] patterns cannot match'))
260 260 raise util.Abort(_('no [keyword] patterns configured'))
261 261
262 262 def _kwfwrite(ui, repo, expand, *pats, **opts):
263 263 '''Selects files and passes them to kwtemplater.overwrite.'''
264 264 if repo.dirstate.parents()[1] != nullid:
265 265 raise util.Abort(_('outstanding uncommitted merge'))
266 266 kwt = kwtools['templater']
267 267 status = _status(ui, repo, kwt, *pats, **opts)
268 268 modified, added, removed, deleted = status[:4]
269 269 if modified or added or removed or deleted:
270 270 raise util.Abort(_('outstanding uncommitted changes'))
271 271 wlock = lock = None
272 272 try:
273 273 wlock = repo.wlock()
274 274 lock = repo.lock()
275 275 kwt.overwrite(None, expand, status[6])
276 276 finally:
277 277 release(lock, wlock)
278 278
279 279 def demo(ui, repo, *args, **opts):
280 280 '''print [keywordmaps] configuration and an expansion example
281 281
282 282 Show current, custom, or default keyword template maps and their
283 283 expansions.
284 284
285 285 Extend the current configuration by specifying maps as arguments
286 286 and using -f/--rcfile to source an external hgrc file.
287 287
288 288 Use -d/--default to disable current configuration.
289 289
290 290 See "hg help templates" for information on templates and filters.
291 291 '''
292 292 def demoitems(section, items):
293 293 ui.write('[%s]\n' % section)
294 294 for k, v in sorted(items):
295 295 ui.write('%s = %s\n' % (k, v))
296 296
297 297 msg = 'hg keyword config and expansion example'
298 298 fn = 'demo.txt'
299 299 branchname = 'demobranch'
300 300 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
301 301 ui.note(_('creating temporary repository at %s\n') % tmpdir)
302 302 repo = localrepo.localrepository(ui, tmpdir, True)
303 303 ui.setconfig('keyword', fn, '')
304 304
305 305 uikwmaps = ui.configitems('keywordmaps')
306 306 if args or opts.get('rcfile'):
307 307 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
308 308 if uikwmaps:
309 309 ui.status(_('\textending current template maps\n'))
310 310 if opts.get('default') or not uikwmaps:
311 311 ui.status(_('\toverriding default template maps\n'))
312 312 if opts.get('rcfile'):
313 313 ui.readconfig(opts.get('rcfile'))
314 314 if args:
315 315 # simulate hgrc parsing
316 316 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
317 317 fp = repo.opener('hgrc', 'w')
318 318 fp.writelines(rcmaps)
319 319 fp.close()
320 320 ui.readconfig(repo.join('hgrc'))
321 321 kwmaps = dict(ui.configitems('keywordmaps'))
322 322 elif opts.get('default'):
323 323 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
324 324 kwmaps = kwtemplater.templates
325 325 if uikwmaps:
326 326 ui.status(_('\tdisabling current template maps\n'))
327 327 for k, v in kwmaps.iteritems():
328 328 ui.setconfig('keywordmaps', k, v)
329 329 else:
330 330 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
331 331 kwmaps = dict(uikwmaps) or kwtemplater.templates
332 332
333 333 uisetup(ui)
334 334 reposetup(ui, repo)
335 335 for k, v in ui.configitems('extensions'):
336 336 if k.endswith('keyword'):
337 337 extension = '%s = %s' % (k, v)
338 338 break
339 339 ui.write('[extensions]\n%s\n' % extension)
340 340 demoitems('keyword', ui.configitems('keyword'))
341 341 demoitems('keywordmaps', kwmaps.iteritems())
342 342 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
343 343 repo.wopener(fn, 'w').write(keywords)
344 344 repo.add([fn])
345 345 path = repo.wjoin(fn)
346 346 ui.note(_('\nkeywords written to %s:\n') % path)
347 347 ui.note(keywords)
348 348 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
349 349 # silence branch command if not verbose
350 350 quiet = ui.quiet
351 351 ui.quiet = not ui.verbose
352 352 commands.branch(ui, repo, branchname)
353 353 ui.quiet = quiet
354 354 for name, cmd in ui.configitems('hooks'):
355 355 if name.split('.', 1)[0].find('commit') > -1:
356 356 repo.ui.setconfig('hooks', name, '')
357 357 ui.note(_('unhooked all commit hooks\n'))
358 358 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
359 359 repo.commit(text=msg)
360 360 ui.status(_('\n\tkeywords expanded\n'))
361 361 ui.write(repo.wread(fn))
362 362 ui.debug('\nremoving temporary repository %s\n' % tmpdir)
363 363 shutil.rmtree(tmpdir, ignore_errors=True)
364 364
365 365 def expand(ui, repo, *pats, **opts):
366 366 '''expand keywords in the working directory
367 367
368 368 Run after (re)enabling keyword expansion.
369 369
370 370 kwexpand refuses to run if given files contain local changes.
371 371 '''
372 372 # 3rd argument sets expansion to True
373 373 _kwfwrite(ui, repo, True, *pats, **opts)
374 374
375 375 def files(ui, repo, *pats, **opts):
376 376 '''show files configured for keyword expansion
377 377
378 378 List which files in the working directory are matched by the
379 379 [keyword] configuration patterns.
380 380
381 381 Useful to prevent inadvertent keyword expansion and to speed up
382 382 execution by including only files that are actual candidates for
383 383 expansion.
384 384
385 385 See "hg help keyword" on how to construct patterns both for
386 386 inclusion and exclusion of files.
387 387
388 388 With -A/--all and -v/--verbose the codes used to show the status
389 389 of files are::
390 390
391 391 K = keyword expansion candidate
392 392 k = keyword expansion candidate (not tracked)
393 393 I = ignored
394 394 i = ignored (not tracked)
395 395 '''
396 396 kwt = kwtools['templater']
397 397 status = _status(ui, repo, kwt, *pats, **opts)
398 398 cwd = pats and repo.getcwd() or ''
399 399 modified, added, removed, deleted, unknown, ignored, clean = status
400 400 files = []
401 401 if not (opts.get('unknown') or opts.get('untracked')) or opts.get('all'):
402 402 files = sorted(modified + added + clean)
403 403 wctx = repo[None]
404 404 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
405 405 kwunknown = [f for f in unknown if kwt.iskwfile(f, wctx.flags)]
406 406 if not opts.get('ignore') or opts.get('all'):
407 407 showfiles = kwfiles, kwunknown
408 408 else:
409 409 showfiles = [], []
410 410 if opts.get('all') or opts.get('ignore'):
411 411 showfiles += ([f for f in files if f not in kwfiles],
412 412 [f for f in unknown if f not in kwunknown])
413 413 for char, filenames in zip('KkIi', showfiles):
414 414 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
415 415 for f in filenames:
416 416 ui.write(fmt % repo.pathto(f, cwd))
417 417
418 418 def shrink(ui, repo, *pats, **opts):
419 419 '''revert expanded keywords in the working directory
420 420
421 421 Run before changing/disabling active keywords or if you experience
422 422 problems with "hg import" or "hg merge".
423 423
424 424 kwshrink refuses to run if given files contain local changes.
425 425 '''
426 426 # 3rd argument sets expansion to False
427 427 _kwfwrite(ui, repo, False, *pats, **opts)
428 428
429 429
430 430 def uisetup(ui):
431 431 '''Collects [keyword] config in kwtools.
432 432 Monkeypatches dispatch._parse if needed.'''
433 433
434 434 for pat, opt in ui.configitems('keyword'):
435 435 if opt != 'ignore':
436 436 kwtools['inc'].append(pat)
437 437 else:
438 438 kwtools['exc'].append(pat)
439 439
440 440 if kwtools['inc']:
441 441 def kwdispatch_parse(orig, ui, args):
442 442 '''Monkeypatch dispatch._parse to obtain running hg command.'''
443 443 cmd, func, args, options, cmdoptions = orig(ui, args)
444 444 kwtools['hgcmd'] = cmd
445 445 return cmd, func, args, options, cmdoptions
446 446
447 447 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
448 448
449 449 def reposetup(ui, repo):
450 450 '''Sets up repo as kwrepo for keyword substitution.
451 451 Overrides file method to return kwfilelog instead of filelog
452 452 if file matches user configuration.
453 453 Wraps commit to overwrite configured files with updated
454 454 keyword substitutions.
455 455 Monkeypatches patch and webcommands.'''
456 456
457 457 try:
458 458 if (not repo.local() or not kwtools['inc']
459 459 or kwtools['hgcmd'] in nokwcommands.split()
460 460 or '.hg' in util.splitpath(repo.root)
461 461 or repo._url.startswith('bundle:')):
462 462 return
463 463 except AttributeError:
464 464 pass
465 465
466 466 kwtools['templater'] = kwt = kwtemplater(ui, repo)
467 467
468 468 class kwrepo(repo.__class__):
469 469 def file(self, f):
470 470 if f[0] == '/':
471 471 f = f[1:]
472 472 return kwfilelog(self.sopener, kwt, f)
473 473
474 474 def wread(self, filename):
475 475 data = super(kwrepo, self).wread(filename)
476 476 return kwt.wread(filename, data)
477 477
478 478 def commit(self, *args, **opts):
479 479 # use custom commitctx for user commands
480 480 # other extensions can still wrap repo.commitctx directly
481 481 self.commitctx = self.kwcommitctx
482 482 try:
483 483 return super(kwrepo, self).commit(*args, **opts)
484 484 finally:
485 485 del self.commitctx
486 486
487 487 def kwcommitctx(self, ctx, error=False):
488 488 wlock = lock = None
489 489 try:
490 490 wlock = self.wlock()
491 491 lock = self.lock()
492 492 # store and postpone commit hooks
493 493 commithooks = {}
494 494 for name, cmd in ui.configitems('hooks'):
495 495 if name.split('.', 1)[0] == 'commit':
496 496 commithooks[name] = cmd
497 497 ui.setconfig('hooks', name, None)
498 498 if commithooks:
499 499 # store parents for commit hooks
500 500 p1, p2 = ctx.p1(), ctx.p2()
501 501 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
502 502
503 503 n = super(kwrepo, self).commitctx(ctx, error)
504 504
505 505 kwt.overwrite(n, True, None)
506 506 if commithooks:
507 507 for name, cmd in commithooks.iteritems():
508 508 ui.setconfig('hooks', name, cmd)
509 509 self.hook('commit', node=n, parent1=xp1, parent2=xp2)
510 510 return n
511 511 finally:
512 512 release(lock, wlock)
513 513
514 514 # monkeypatches
515 515 def kwpatchfile_init(orig, self, ui, fname, opener,
516 516 missing=False, eol=None):
517 517 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
518 518 rejects or conflicts due to expanded keywords in working dir.'''
519 519 orig(self, ui, fname, opener, missing, eol)
520 520 # shrink keywords read from working dir
521 521 self.lines = kwt.shrinklines(self.fname, self.lines)
522 522
523 523 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
524 524 opts=None):
525 525 '''Monkeypatch patch.diff to avoid expansion except when
526 526 comparing against working dir.'''
527 527 if node2 is not None:
528 528 kwt.match = util.never
529 529 elif node1 is not None and node1 != repo['.'].node():
530 530 kwt.restrict = True
531 531 return orig(repo, node1, node2, match, changes, opts)
532 532
533 533 def kwweb_skip(orig, web, req, tmpl):
534 534 '''Wraps webcommands.x turning off keyword expansion.'''
535 535 kwt.match = util.never
536 536 return orig(web, req, tmpl)
537 537
538 538 repo.__class__ = kwrepo
539 539
540 540 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
541 541 if not kwt.restrict:
542 542 extensions.wrapfunction(patch, 'diff', kw_diff)
543 543 for c in 'annotate changeset rev filediff diff'.split():
544 544 extensions.wrapfunction(webcommands, c, kwweb_skip)
545 545
546 546 cmdtable = {
547 547 'kwdemo':
548 548 (demo,
549 549 [('d', 'default', None, _('show default keyword template maps')),
550 550 ('f', 'rcfile', '', _('read maps from rcfile'))],
551 551 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
552 552 'kwexpand': (expand, commands.walkopts,
553 553 _('hg kwexpand [OPTION]... [FILE]...')),
554 554 'kwfiles':
555 555 (files,
556 556 [('A', 'all', None, _('show keyword status flags of all files')),
557 557 ('i', 'ignore', None, _('show files excluded from expansion')),
558 558 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
559 559 ('a', 'all', None,
560 560 _('show keyword status flags of all files (DEPRECATED)')),
561 561 ('u', 'untracked', None, _('only show untracked files (DEPRECATED)')),
562 562 ] + commands.walkopts,
563 563 _('hg kwfiles [OPTION]... [FILE]...')),
564 564 'kwshrink': (shrink, commands.walkopts,
565 565 _('hg kwshrink [OPTION]... [FILE]...')),
566 566 }
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now