diff --git a/.hgsigs b/.hgsigs --- a/.hgsigs +++ b/.hgsigs @@ -1,1 +1,2 @@ 35fb62a3a673d5322f6274a44ba6456e5e4b3b37 0 iD8DBQBEYmO2ywK+sNU5EO8RAnaYAKCO7x15xUn5mnhqWNXqk/ehlhRt2QCfRDfY0LrUq2q4oK/KypuJYPHgq1A= +2be3001847cb18a23c403439d9e7d0ace30804e9 0 iD8DBQBExUbjywK+sNU5EO8RAhzxAKCtyHAQUzcTSZTqlfJ0by6vhREwWQCghaQFHfkfN0l9/40EowNhuMOKnJk= diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -11,3 +11,4 @@ 979c049974485125e1f9357f6bbe9c1b548a64c3 3a56574f329a368d645853e0f9e09472aee62349 0.8 6a03cff2b0f5d30281e6addefe96b993582f2eac 0.8.1 35fb62a3a673d5322f6274a44ba6456e5e4b3b37 0.9 +2be3001847cb18a23c403439d9e7d0ace30804e9 0.9.1 diff --git a/CONTRIBUTORS b/CONTRIBUTORS --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -4,6 +4,7 @@ Goffredo Baroncelli Mikael Berthe Benoit Boissinot +Brendan Cully Vincent Danjean Jake Edge Michael Fetterman diff --git a/MANIFEST.in b/MANIFEST.in --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,7 @@ include hg recursive-include mercurial *.py include hgweb.cgi hgwebdir.cgi include hgeditor rewrite-log -include tests/README tests/coverage.py tests/run-tests.py tests/md5sum.py tests/test-*[a-z0-9] tests/*.out +include tests/README tests/*.py tests/test-*[a-z0-9] tests/*.out prune tests/*.err include *.txt include templates/map templates/map-*[a-z0-9] @@ -10,8 +10,10 @@ include templates/*.tmpl include templates/static/* include doc/README doc/Makefile doc/gendoc.py doc/*.txt doc/*.html doc/*.[0-9] recursive-include contrib * +recursive-include hgext * include README include CONTRIBUTORS include COPYING include Makefile include MANIFEST.in +prune *.elc *.orig *.rej *~ *.o *.so *.pyc *.swp *.prof diff --git a/contrib/bash_completion b/contrib/bash_completion --- a/contrib/bash_completion +++ b/contrib/bash_completion @@ -288,7 +288,7 @@ complete -o bashdefault -o default -F _h _hg_cmd_qdelete() { - _hg_ext_mq_patchlist qseries + _hg_ext_mq_patchlist qunapplied } _hg_cmd_qsave() @@ -313,6 +313,11 @@ complete -o bashdefault -o default -F _h COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur")) } +_hg_cmd_export() +{ + _hg_ext_mq_patchlist qapplied +} + # hbisect _hg_cmd_bisect() diff --git a/contrib/convert-repo b/contrib/convert-repo --- a/contrib/convert-repo +++ b/contrib/convert-repo @@ -28,7 +28,8 @@ class convert_git: self.path = path def getheads(self): - return [file(self.path + "/HEAD").read()[:-1]] + fh = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path) + return [fh.read()[:-1]] def catfile(self, rev, type): if rev == "0" * 40: raise IOError() diff --git a/contrib/darcs2hg.py b/contrib/darcs2hg.py --- a/contrib/darcs2hg.py +++ b/contrib/darcs2hg.py @@ -92,7 +92,7 @@ def darcs_tip(darcs_repo): def darcs_pull(hg_repo, darcs_repo, chash): old_tip = darcs_tip(darcs_repo) - res = cmd("darcs pull '%s' --all --match='hash %s'" % (darcs_repo, chash), hg_repo) + res = cmd("darcs pull \"%s\" --all --match=\"hash %s\"" % (darcs_repo, chash), hg_repo) print res new_tip = darcs_tip(darcs_repo) if not new_tip != old_tip + 1: @@ -110,7 +110,8 @@ def hg_commit( hg_repo, text, author, da old_tip = hg_tip(hg_repo) cmd("hg add -X _darcs", hg_repo) cmd("hg remove -X _darcs --after", hg_repo) - res = cmd("hg commit -l %s -u '%s' -d '%s 0'" % (tmpfile, author, date), hg_repo) + res = cmd("hg commit -l %s -u \"%s\" -d \"%s 0\"" % (tmpfile, author, date), hg_repo) + os.close(fd) os.unlink(tmpfile) new_tip = hg_tip(hg_repo) if not new_tip == old_tip + 1: @@ -156,7 +157,7 @@ if __name__ == "__main__": print "Given HG repository must not exist when no SKIP is specified." sys.exit(-1) if skip == None: - cmd("hg init '%s'" % (hg_repo)) + cmd("hg init \"%s\"" % (hg_repo)) cmd("darcs initialize", hg_repo) # Get the changes from the Darcs repository change_number = 0 diff --git a/contrib/macosx/Welcome.html b/contrib/macosx/Welcome.html --- a/contrib/macosx/Welcome.html +++ b/contrib/macosx/Welcome.html @@ -5,13 +5,73 @@

This is a prepackaged release of Mercurial for Mac OS X.


-

It is based on Mercurial 0.9.

+

It is based on Mercurial 0.9.1

+
+
+Release Notes
+-------------
+
+2006-07-24  v0.9.1
+
+Major changes between Mercurial 0.9 and 0.9.1:
+
+ New features:
+ - You can now configure your 'hgweb' server to let remote users
+   'push' changes over http.
+ - You can now 'import' a patch in a mail message by saving the mail
+   message, and importing it.  This works for patches sent either
+   inline or as attachments.
+ - The 'diff' command now accepts '-rA:B' syntax as a synonym for
+   '-r A -r B', and adds '-b' and '-B' options.
+
+ New contributions and extensions:
+ - The 'acl' extension lets you lock down parts of a repository
+   against incoming changes
+ - The 'extdiff' extension lets you run your favourite graphical
+   change viewer
+ - Comprehensive integration with the 'vim' editor
+ - A restricted shell for 'ssh'-hosted repositories
+ - An importer for 'darcs' repositories
+
+ New hooks added:
+ - 'preupdate' is run before an update or merge in the working
+   directory.
+ - 'update' is run after an update or merge in the working
+   directory.
+
+ Behaviour changes:
+ - NOTE: Mercurial as installed by the Windows binary
+   installer no longer performs automatic line-ending conversion for
+   Unix/Linux compatibility.  To re-enable this feature, edit your
+   'mercurial.ini' file after you upgrade.
+ - The Windows binary installer now automatically adds 'hg' to your
+   '%PATH%'.
+ - The 'backout' command now runs an editor by default, to let you
+   modify the commit message for a backed-out changeset.
+ - An earlier problem with parsing of tags has been fixed.
+   This makes tag parsing slower but more reliable.
+
+ Memory usage and performance improvements:
+ - The 'remove' command has been rewritten to be hundreds of times
+   faster in large repositories.
+ - It is now possible to 'clone' a repository very quickly over a
+   LAN, if the server is configured to allow it.  See the new 'server'
+   section in the 'hgrc' documentation.
+
+ Other changes of note:
+ - Mercurial will now print help for an extension if you type 'hg
+   help EXT_NAME'.
+ - The usual array of bug fixes and documentation improvements.
+ - The integrated web server is now more WSGI-compliant.
+ - Work has begun to solidify Mercurial's API for use by third-party
+   packages.
+
diff --git a/contrib/mercurial.el b/contrib/mercurial.el --- a/contrib/mercurial.el +++ b/contrib/mercurial.el @@ -380,7 +380,9 @@ Handle frickin' frackin' gratuitous even (save-excursion (while hg-prev-buffer (set-buffer hg-prev-buffer)) - (let ((path (or default (buffer-file-name) default-directory))) + (let ((path (or default + (buffer-file-name) + (expand-file-name default-directory)))) (if (or (not path) current-prefix-arg) (expand-file-name (eval (list* 'read-file-name @@ -716,7 +718,11 @@ code by typing `M-x find-library mercuri (goto-char pos) (end-of-line 1) (delete-region pos (point))) - (cd (hg-root)))) + (let ((hg-root-dir (hg-root))) + (if (not hg-root-dir) + (error "error: %s: directory is not part of a Mercurial repository." + default-directory) + (cd hg-root-dir))))) (defun hg-add (path) "Add PATH to the Mercurial repository on the next commit. @@ -972,7 +978,8 @@ With a prefix argument, prompt for the p (cd (hg-root path))) (when update (with-current-buffer buf - (set (make-local-variable 'backup-inhibited) nil) + (when (local-variable-p 'backup-inhibited) + (kill-local-variable 'backup-inhibited)) (hg-mode-line))))) (defun hg-incoming (&optional repo) diff --git a/contrib/vim/hgcommand.vim b/contrib/vim/hgcommand.vim --- a/contrib/vim/hgcommand.vim +++ b/contrib/vim/hgcommand.vim @@ -3,7 +3,7 @@ " Vim plugin to assist in working with HG-controlled files. " " Last Change: 2006/02/22 -" Version: 1.76 +" Version: 1.77 " Maintainer: Mathieu Clabaut " License: This file is placed in the public domain. " Credits: @@ -13,7 +13,7 @@ """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" " -" Section: Documentation +" Section: Documentation "---------------------------- " " Documentation should be available by ":help hgcommand" command, once the @@ -21,7 +21,7 @@ " " You still can read the documentation at the end of this file. Locate it by " searching the "hgcommand-contents" string (and set ft=help to have -" appropriate syntaxic coloration). +" appropriate syntaxic coloration). " Section: Plugin header {{{1 @@ -34,11 +34,33 @@ if exists("loaded_hgcommand") endif let loaded_hgcommand = 1 +" store 'compatible' settings +let s:save_cpo = &cpo +set cpo&vim + +" run checks +let s:script_name = expand(":t:r") + +function! s:HGCleanupOnFailure(err) + echohl WarningMsg + echomsg s:script_name . ":" a:err "Plugin not loaded" + echohl None + let loaded_hgcommand = "no" + unlet s:save_cpo s:script_name +endfunction + if v:version < 602 - echohl WarningMsg|echomsg "HGCommand 1.69 or later requires VIM 6.2 or later"|echohl None + call HGCleanupOnFailure("VIM 6.2 or later required.") finish endif +if !exists("*system") + call HGCleanupOnFailure("builtin system() function required.") + finish +endif + +let s:script_version = "v0.2" + " Section: Event group setup {{{1 augroup HGCommand @@ -63,7 +85,7 @@ unlet! s:vimDiffScratchList function! s:HGResolveLink(fileName) let resolved = resolve(a:fileName) if resolved != a:fileName - let resolved = s:HGResolveLink(resolved) + let resolved = HGResolveLink(resolved) endif return resolved endfunction @@ -74,7 +96,7 @@ endfunction function! s:HGChangeToCurrentFileDir(fileName) let oldCwd=getcwd() - let fileName=s:HGResolveLink(a:fileName) + let fileName=HGResolveLink(a:fileName) let newCwd=fnamemodify(fileName, ':h') if strlen(newCwd) > 0 execute 'cd' escape(newCwd, ' ') @@ -82,7 +104,7 @@ function! s:HGChangeToCurrentFileDir(fil return oldCwd endfunction -" Function: s:HGGetOption(name, default) {{{2 +" Function: HGGetOption(name, default) {{{2 " Grab a user-specified option to override the default provided. Options are " searched in the window, buffer, then global spaces. @@ -110,9 +132,9 @@ function! s:HGEditFile(name, origBuffNR) "Name parameter will be pasted into expression. let name = escape(a:name, ' *?\') - let editCommand = s:HGGetOption('HGCommandEdit', 'edit') + let editCommand = HGGetOption('HGCommandEdit', 'edit') if editCommand != 'edit' - if s:HGGetOption('HGCommandSplit', 'horizontal') == 'horizontal' + if HGGetOption('HGCommandSplit', 'horizontal') == 'horizontal' if name == "" let editCommand = 'rightbelow new' else @@ -154,8 +176,8 @@ function! s:HGCreateCommandBuffer(cmd, c let resultBufferName='' - if s:HGGetOption("HGCommandNameResultBuffers", 0) - let nameMarker = s:HGGetOption("HGCommandNameMarker", '_') + if HGGetOption("HGCommandNameResultBuffers", 0) + let nameMarker = HGGetOption("HGCommandNameMarker", '_') if strlen(a:statusText) > 0 let bufName=a:cmdName . ' -- ' . a:statusText else @@ -170,7 +192,7 @@ function! s:HGCreateCommandBuffer(cmd, c endwhile endif - let hgCommand = s:HGGetOption("HGCommandHGExec", "hg") . " " . a:cmd + let hgCommand = HGGetOption("HGCommandHGExec", "hg") . " " . a:cmd "echomsg "DBG :".hgCommand let hgOut = system(hgCommand) " HACK: diff command does not return proper error codes @@ -192,7 +214,7 @@ function! s:HGCreateCommandBuffer(cmd, c return -1 endif - if s:HGEditFile(resultBufferName, a:origBuffNR) == -1 + if HGEditFile(resultBufferName, a:origBuffNR) == -1 return -1 endif @@ -200,7 +222,7 @@ function! s:HGCreateCommandBuffer(cmd, c set noswapfile set filetype= - if s:HGGetOption("HGCommandDeleteOnHide", 0) + if HGGetOption("HGCommandDeleteOnHide", 0) set bufhidden=delete endif @@ -213,8 +235,8 @@ function! s:HGCreateCommandBuffer(cmd, c " This could be fixed by explicitly detecting whether the last line is " within a fold, but I prefer to simply unfold the result buffer altogether. - if has('folding') - normal zR + if has("folding") + setlocal nofoldenable endif $d @@ -243,7 +265,7 @@ function! s:HGBufferCheck(hgBuffer) return origBuffer else " Original buffer no longer exists. - return -1 + return -1 endif else " No original buffer @@ -256,7 +278,7 @@ endfunction " for the current buffer. function! s:HGCurrentBufferCheck() - return s:HGBufferCheck(bufnr("%")) + return HGBufferCheck(bufnr("%")) endfunction " Function: s:HGToggleDeleteOnHide() {{{2 @@ -275,8 +297,8 @@ endfunction " Returns: name of the new command buffer containing the command results function! s:HGDoCommand(cmd, cmdName, statusText) - let hgBufferCheck=s:HGCurrentBufferCheck() - if hgBufferCheck == -1 + let hgBufferCheck=HGCurrentBufferCheck() + if hgBufferCheck == -1 echo "Original buffer no longer exists, aborting." return -1 endif @@ -285,8 +307,8 @@ function! s:HGDoCommand(cmd, cmdName, st if isdirectory(fileName) let fileName=fileName . "/" . getline(".") endif - let realFileName = fnamemodify(s:HGResolveLink(fileName), ':t') - let oldCwd=s:HGChangeToCurrentFileDir(fileName) + let realFileName = fnamemodify(HGResolveLink(fileName), ':t') + let oldCwd=HGChangeToCurrentFileDir(fileName) try " TODO "if !filereadable('HG/Root') @@ -294,7 +316,7 @@ function! s:HGDoCommand(cmd, cmdName, st "endif let fullCmd = a:cmd . ' "' . realFileName . '"' "echomsg "DEBUG".fullCmd - let resultBuffer=s:HGCreateCommandBuffer(fullCmd, a:cmdName, a:statusText, hgBufferCheck) + let resultBuffer=HGCreateCommandBuffer(fullCmd, a:cmdName, a:statusText, hgBufferCheck) return resultBuffer catch echoerr v:exception @@ -314,17 +336,17 @@ endfunction " Returns: string to be exec'd that sets the multiple return values. function! s:HGGetStatusVars(revisionVar, branchVar, repositoryVar) - let hgBufferCheck=s:HGCurrentBufferCheck() + let hgBufferCheck=HGCurrentBufferCheck() "echomsg "DBG : in HGGetStatusVars" - if hgBufferCheck == -1 + if hgBufferCheck == -1 return "" endif let fileName=bufname(hgBufferCheck) - let fileNameWithoutLink=s:HGResolveLink(fileName) + let fileNameWithoutLink=HGResolveLink(fileName) let realFileName = fnamemodify(fileNameWithoutLink, ':t') - let oldCwd=s:HGChangeToCurrentFileDir(realFileName) + let oldCwd=HGChangeToCurrentFileDir(realFileName) try - let hgCommand = s:HGGetOption("HGCommandHGExec", "hg") . " root " + let hgCommand = HGGetOption("HGCommandHGExec", "hg") . " root " let roottext=system(hgCommand) " Suppress ending null char ! Does it work in window ? let roottext=substitute(roottext,'^.*/\([^/\n\r]*\)\n\_.*$','\1','') @@ -335,31 +357,31 @@ function! s:HGGetStatusVars(revisionVar, if a:repositoryVar != "" let returnExpression=returnExpression . " | let " . a:repositoryVar . "='" . roottext . "'" endif - let hgCommand = s:HGGetOption("HGCommandHGExec", "hg") . " status -mardui " . realFileName + let hgCommand = HGGetOption("HGCommandHGExec", "hg") . " status -mardui " . realFileName let statustext=system(hgCommand) if(v:shell_error) return "" endif - if match(statustext, '^[?I]') >= 0 + if match(statustext, '^[?I]') >= 0 let revision="NEW" - elseif match(statustext, '^[R]') >= 0 + elseif match(statustext, '^[R]') >= 0 let revision="REMOVED" - elseif match(statustext, '^[D]') >= 0 + elseif match(statustext, '^[D]') >= 0 let revision="DELETED" - elseif match(statustext, '^[A]') >= 0 + elseif match(statustext, '^[A]') >= 0 let revision="ADDED" else " The file is tracked, we can try to get is revision number - let hgCommand = s:HGGetOption("HGCommandHGExec", "hg") . " parents -b " + let hgCommand = HGGetOption("HGCommandHGExec", "hg") . " parents -b " let statustext=system(hgCommand) if(v:shell_error) - return "" + return "" endif let revision=substitute(statustext, '^changeset:\s*\(\d\+\):.*\_$\_.*$', '\1', "") if a:branchVar != "" && match(statustext, '^\_.*\_^branch:') >= 0 - let branch=substitute(statustext, '^\_.*\_^branch:\s*\(\S\+\)\n\_.*$', '\1', "") - let returnExpression=returnExpression . " | let " . a:branchVar . "='" . branch . "'" + let branch=substitute(statustext, '^\_.*\_^branch:\s*\(\S\+\)\n\_.*$', '\1', "") + let returnExpression=returnExpression . " | let " . a:branchVar . "='" . branch . "'" endif endif if (exists('revision')) @@ -381,7 +403,7 @@ function! s:HGSetupBuffer(...) return endif - if !s:HGGetOption("HGCommandEnableBufferSetup", 0) + if !HGGetOption("HGCommandEnableBufferSetup", 0) \ || @% == "" \ || s:HGCommandEditFileRunning > 0 \ || exists("b:HGOrigBuffNR") @@ -399,7 +421,7 @@ function! s:HGSetupBuffer(...) let branch="" let repository="" - exec s:HGGetStatusVars('revision', 'branch', 'repository') + exec HGGetStatusVars('revision', 'branch', 'repository') "echomsg "DBG ".revision."#".branch."#".repository if revision != "" let b:HGRevision=revision @@ -427,7 +449,7 @@ endfunction function! s:HGMarkOrigBufferForSetup(hgBuffer) checktime if a:hgBuffer != -1 - let origBuffer = s:HGBufferCheck(a:hgBuffer) + let origBuffer = HGBufferCheck(a:hgBuffer) "This should never not work, but I'm paranoid if origBuffer != a:hgBuffer call setbufvar(origBuffer, "HGBufferSetup", 0) @@ -436,7 +458,7 @@ function! s:HGMarkOrigBufferForSetup(hgB "We are presumably in the original buffer let b:HGBufferSetup = 0 "We do the setup now as now event will be triggered allowing it later. - call s:HGSetupBuffer() + call HGSetupBuffer() endif return a:hgBuffer endfunction @@ -478,111 +500,93 @@ endfunction " 1 if new document installed, 0 otherwise. " Note: Cleaned and generalized by guo-peng Wen "''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -function! s:HGInstallDocumentation(full_name, revision) - " Name of the document path based on the system we use: - if (has("unix")) - " On UNIX like system, using forward slash: - let l:slash_char = '/' - let l:mkdir_cmd = ':silent !mkdir -p ' - else - " On M$ system, use backslash. Also mkdir syntax is different. - " This should only work on W2K and up. - let l:slash_char = '\' - let l:mkdir_cmd = ':silent !mkdir ' - endif - - let l:doc_path = l:slash_char . 'doc' - let l:doc_home = l:slash_char . '.vim' . l:slash_char . 'doc' +" Helper function to make mkdir as portable as possible +function! s:HGFlexiMkdir(dir) + if exists("*mkdir") " we can use Vim's own mkdir() + call mkdir(a:dir) + elseif !exists("+shellslash") + call system("mkdir -p '".a:dir."'") + else " M$ + let l:ssl = &shellslash + try + set shellslash + " no single quotes? + call system('mkdir "'.a:dir.'"') + finally + let &shellslash = l:ssl + endtry + endif +endfunction - " Figure out document path based on full name of this script: - let l:vim_plugin_path = fnamemodify(a:full_name, ':h') - let l:vim_doc_path = fnamemodify(a:full_name, ':h:h') . l:doc_path - if (!(filewritable(l:vim_doc_path) == 2)) - echomsg "Doc path: " . l:vim_doc_path - execute l:mkdir_cmd . '"' . l:vim_doc_path . '"' - if (!(filewritable(l:vim_doc_path) == 2)) - " Try a default configuration in user home: - let l:vim_doc_path = expand("~") . l:doc_home - if (!(filewritable(l:vim_doc_path) == 2)) - execute l:mkdir_cmd . '"' . l:vim_doc_path . '"' - if (!(filewritable(l:vim_doc_path) == 2)) - " Put a warning: - echomsg "Unable to open documentation directory" - echomsg " type :help add-local-help for more informations." - return 0 - endif - endif +function! s:HGInstallDocumentation(full_name) + " Figure out document path based on full name of this script: + let l:vim_doc_path = fnamemodify(a:full_name, ":h:h") . "/doc" + if filewritable(l:vim_doc_path) != 2 + echomsg s:script_name . ": Trying to update docs at" l:vim_doc_path + silent! call HGFlexiMkdir(l:vim_doc_path) + if filewritable(l:vim_doc_path) != 2 + " Try first item in 'runtimepath': + let l:vim_doc_path = + \ substitute(&runtimepath, '^\([^,]*\).*', '\1/doc', 'e') + if filewritable(l:vim_doc_path) != 2 + echomsg s:script_name . ": Trying to update docs at" l:vim_doc_path + silent! call HGFlexiMkdir(l:vim_doc_path) + if filewritable(l:vim_doc_path) != 2 + " Put a warning: + echomsg "Unable to open documentation directory" + echomsg " type `:help add-local-help' for more information." + return 0 endif + endif endif - - " Exit if we have problem to access the document directory: - if (!isdirectory(l:vim_plugin_path) - \ || !isdirectory(l:vim_doc_path) - \ || filewritable(l:vim_doc_path) != 2) - return 0 - endif - - " Full name of script and documentation file: - let l:script_name = fnamemodify(a:full_name, ':t') - let l:doc_name = fnamemodify(a:full_name, ':t:r') . '.txt' - let l:plugin_file = l:vim_plugin_path . l:slash_char . l:script_name - let l:doc_file = l:vim_doc_path . l:slash_char . l:doc_name + endif - " Bail out if document file is still up to date: - if (filereadable(l:doc_file) && - \ getftime(l:plugin_file) < getftime(l:doc_file)) - return 0 - endif + " Full name of documentation file: + let l:doc_file = + \ l:vim_doc_path . "/" . s:script_name . ".txt" + " Bail out if document file is still up to date: + if filereadable(l:doc_file) && + \ getftime(a:full_name) < getftime(l:doc_file) + return 0 + endif - " Prepare window position restoring command: - if (strlen(@%)) - let l:go_back = 'b ' . bufnr("%") - else - let l:go_back = 'enew!' - endif - - " Create a new buffer & read in the plugin file (me): - setl nomodeline - exe 'enew!' - exe 'r ' . l:plugin_file - - setl modeline - let l:buf = bufnr("%") - setl noswapfile modifiable - - norm zR - norm gg + " temporary global settings + let l:lz = &lazyredraw + let l:hls = &hlsearch + set lazyredraw nohlsearch + " Create a new buffer & read in the plugin file (me): + 1 new + setlocal noswapfile modifiable nomodeline + if has("folding") + setlocal nofoldenable + endif + silent execute "read" escape(a:full_name, " ") + let l:doc_buf = bufnr("%") - " Delete from first line to a line starts with - " === START_DOC - 1,/^=\{3,}\s\+START_DOC\C/ d - - " Delete from a line starts with - " === END_DOC - " to the end of the documents: - /^=\{3,}\s\+END_DOC\C/,$ d - - " Remove fold marks: - %s/{\{3}[1-9]/ / + 1 + " Delete from first line to a line starts with + " === START_DOC + silent 1,/^=\{3,}\s\+START_DOC\C/ d + " Delete from a line starts with + " === END_DOC + " to the end of the documents: + silent /^=\{3,}\s\+END_DOC\C/,$ d - " Add modeline for help doc: the modeline string is mangled intentionally - " to avoid it be recognized by VIM: - call append(line('$'), '') - call append(line('$'), ' v' . 'im:tw=78:ts=8:ft=help:norl:') - - " Replace revision: - exe "normal :1s/#version#/ v" . a:revision . "/\" + " Add modeline for help doc: the modeline string is mangled intentionally + " to avoid it be recognized by VIM: + call append(line("$"), "") + call append(line("$"), " v" . "im:tw=78:ts=8:ft=help:norl:") - " Save the help document: - exe 'w! ' . l:doc_file - exe l:go_back - exe 'bw ' . l:buf + " Replace revision: + silent execute "normal :1s/#version#/" . s:script_version . "/\" + " Save the help document and wipe out buffer: + silent execute "wq!" escape(l:doc_file, " ") "| bw" l:doc_buf + " Build help tags: + silent execute "helptags" l:vim_doc_path - " Build help tags: - exe 'helptags ' . l:vim_doc_path - - return 1 + let &hlsearch = l:hls + let &lazyredraw = l:lz + return 1 endfunction " Section: Public functions {{{1 @@ -593,7 +597,7 @@ endfunction function! HGGetRevision() let revision="" - exec s:HGGetStatusVars('revision', '', '') + exec HGGetStatusVars('revision', '', '') return revision endfunction @@ -612,16 +616,16 @@ function! HGEnableBufferSetup() let g:HGCommandEnableBufferSetup=1 augroup HGCommandPlugin au! - au BufEnter * call s:HGSetupBuffer() - au BufWritePost * call s:HGSetupBuffer() + au BufEnter * call HGSetupBuffer() + au BufWritePost * call HGSetupBuffer() " Force resetting up buffer on external file change (HG update) - au FileChangedShell * call s:HGSetupBuffer(1) + au FileChangedShell * call HGSetupBuffer(1) augroup END " Only auto-load if the plugin is fully loaded. This gives other plugins a " chance to run. if g:loaded_hgcommand == 2 - call s:HGSetupBuffer() + call HGSetupBuffer() endif endfunction @@ -662,7 +666,7 @@ endfunction " Function: s:HGAdd() {{{2 function! s:HGAdd() - return s:HGMarkOrigBufferForSetup(s:HGDoCommand('add', 'hgadd', '')) + return HGMarkOrigBufferForSetup(HGDoCommand('add', 'hgadd', '')) endfunction " Function: s:HGAnnotate(...) {{{2 @@ -672,7 +676,7 @@ function! s:HGAnnotate(...) " This is a HGAnnotate buffer. Perform annotation of the version " indicated by the current line. let revision = substitute(getline("."),'\(^[0-9]*\):.*','\1','') - if s:HGGetOption('HGCommandAnnotateParent', 0) != 0 && revision > 0 + if HGGetOption('HGCommandAnnotateParent', 0) != 0 && revision > 0 let revision = revision - 1 endif else @@ -691,7 +695,7 @@ function! s:HGAnnotate(...) return -1 endif - let resultBuffer=s:HGDoCommand('annotate -ndu -r ' . revision, 'hgannotate', revision) + let resultBuffer=HGDoCommand('annotate -ndu -r ' . revision, 'hgannotate', revision) "echomsg "DBG: ".resultBuffer if resultBuffer != -1 set filetype=HGAnnotate @@ -706,10 +710,10 @@ function! s:HGCommit(...) " is used; if bang is supplied, an empty message is used; otherwise, the " user is provided a buffer from which to edit the commit message. if a:2 != "" || a:1 == "!" - return s:HGMarkOrigBufferForSetup(s:HGDoCommand('commit -m "' . a:2 . '"', 'hgcommit', '')) + return HGMarkOrigBufferForSetup(HGDoCommand('commit -m "' . a:2 . '"', 'hgcommit', '')) endif - let hgBufferCheck=s:HGCurrentBufferCheck() + let hgBufferCheck=HGCurrentBufferCheck() if hgBufferCheck == -1 echo "Original buffer no longer exists, aborting." return -1 @@ -725,7 +729,7 @@ function! s:HGCommit(...) let messageFileName = tempname() let fileName=bufname(hgBufferCheck) - let realFilePath=s:HGResolveLink(fileName) + let realFilePath=HGResolveLink(fileName) let newCwd=fnamemodify(realFilePath, ':h') if strlen(newCwd) == 0 " Account for autochdir being in effect, which will make this blank, but @@ -735,7 +739,7 @@ function! s:HGCommit(...) let realFileName=fnamemodify(realFilePath, ':t') - if s:HGEditFile(messageFileName, hgBufferCheck) == -1 + if HGEditFile(messageFileName, hgBufferCheck) == -1 return endif @@ -766,9 +770,9 @@ function! s:HGCommit(...) silent put =\"HG: Enter Log. Lines beginning with `HG:' are removed automatically\" silent put ='HG: Type cc (or your own HGCommit mapping)' - if s:HGGetOption('HGCommandCommitOnWrite', 1) == 1 + if HGGetOption('HGCommandCommitOnWrite', 1) == 1 execute 'au HGCommit BufWritePre' autoPattern 'g/^HG:/d' - execute 'au HGCommit BufWritePost' autoPattern 'call s:HGFinishCommit("' . messageFileName . '", "' . newCwd . '", "' . realFileName . '", ' . hgBufferCheck . ') | au! * ' autoPattern + execute 'au HGCommit BufWritePost' autoPattern 'call HGFinishCommit("' . messageFileName . '", "' . newCwd . '", "' . realFileName . '", ' . hgBufferCheck . ') | au! * ' autoPattern silent put ='HG: or write this buffer' endif @@ -797,7 +801,7 @@ function! s:HGDiff(...) let caption = '' endif - let hgdiffopt=s:HGGetOption('HGCommandDiffOpt', 'w') + let hgdiffopt=HGGetOption('HGCommandDiffOpt', 'w') if hgdiffopt == "" let diffoptionstring="" @@ -805,8 +809,8 @@ function! s:HGDiff(...) let diffoptionstring=" -" . hgdiffopt . " " endif - let resultBuffer = s:HGDoCommand('diff ' . diffoptionstring . revOptions , 'hgdiff', caption) - if resultBuffer != -1 + let resultBuffer = HGDoCommand('diff ' . diffoptionstring . revOptions , 'hgdiff', caption) + if resultBuffer != -1 set filetype=diff endif return resultBuffer @@ -815,7 +819,7 @@ endfunction " Function: s:HGGotoOriginal(["!]) {{{2 function! s:HGGotoOriginal(...) - let origBuffNR = s:HGCurrentBufferCheck() + let origBuffNR = HGCurrentBufferCheck() if origBuffNR > 0 let origWinNR = bufwinnr(origBuffNR) if origWinNR == -1 @@ -845,11 +849,11 @@ function! s:HGFinishCommit(messageFile, if strlen(a:targetDir) > 0 execute 'cd' escape(a:targetDir, ' ') endif - let resultBuffer=s:HGCreateCommandBuffer('commit -l "' . a:messageFile . '" "'. a:targetFile . '"', 'hgcommit', '', a:origBuffNR) + let resultBuffer=HGCreateCommandBuffer('commit -l "' . a:messageFile . '" "'. a:targetFile . '"', 'hgcommit', '', a:origBuffNR) execute 'cd' escape(oldCwd, ' ') execute 'bw' escape(a:messageFile, ' *?\') silent execute 'call delete("' . a:messageFile . '")' - return s:HGMarkOrigBufferForSetup(resultBuffer) + return HGMarkOrigBufferForSetup(resultBuffer) else echoerr "Can't read message file; no commit is possible." return -1 @@ -866,7 +870,7 @@ function! s:HGLog(...) let caption = a:1 endif - let resultBuffer=s:HGDoCommand('log' . versionOption, 'hglog', caption) + let resultBuffer=HGDoCommand('log' . versionOption, 'hglog', caption) if resultBuffer != "" set filetype=rcslog endif @@ -875,14 +879,14 @@ endfunction " Function: s:HGRevert() {{{2 function! s:HGRevert() - return s:HGMarkOrigBufferForSetup(s:HGDoCommand('revert', 'hgrevert', '')) + return HGMarkOrigBufferForSetup(HGDoCommand('revert', 'hgrevert', '')) endfunction " Function: s:HGReview(...) {{{2 function! s:HGReview(...) if a:0 == 0 let versiontag="" - if s:HGGetOption('HGCommandInteractive', 0) + if HGGetOption('HGCommandInteractive', 0) let versiontag=input('Revision: ') endif if versiontag == "" @@ -896,7 +900,7 @@ function! s:HGReview(...) let versionOption=" -r " . versiontag . " " endif - let resultBuffer = s:HGDoCommand('cat' . versionOption, 'hgreview', versiontag) + let resultBuffer = HGDoCommand('cat' . versionOption, 'hgreview', versiontag) if resultBuffer > 0 let &filetype=getbufvar(b:HGOrigBuffNR, '&filetype') endif @@ -906,18 +910,18 @@ endfunction " Function: s:HGStatus() {{{2 function! s:HGStatus() - return s:HGDoCommand('status', 'hgstatus', '') + return HGDoCommand('status', 'hgstatus', '') endfunction " Function: s:HGUpdate() {{{2 function! s:HGUpdate() - return s:HGMarkOrigBufferForSetup(s:HGDoCommand('update', 'update', '')) + return HGMarkOrigBufferForSetup(HGDoCommand('update', 'update', '')) endfunction " Function: s:HGVimDiff(...) {{{2 function! s:HGVimDiff(...) - let originalBuffer = s:HGCurrentBufferCheck() + let originalBuffer = HGCurrentBufferCheck() let s:HGCommandEditFileRunning = s:HGCommandEditFileRunning + 1 try " If there's already a VimDiff'ed window, restore it. @@ -925,16 +929,16 @@ function! s:HGVimDiff(...) if exists("s:vimDiffSourceBuffer") && s:vimDiffSourceBuffer != originalBuffer " Clear the existing vimdiff setup by removing the result buffers. - call s:HGWipeoutCommandBuffers(s:vimDiffSourceBuffer, 'vimdiff') + call HGWipeoutCommandBuffers(s:vimDiffSourceBuffer, 'vimdiff') endif " Split and diff if(a:0 == 2) " Reset the vimdiff system, as 2 explicit versions were provided. if exists('s:vimDiffSourceBuffer') - call s:HGWipeoutCommandBuffers(s:vimDiffSourceBuffer, 'vimdiff') + call HGWipeoutCommandBuffers(s:vimDiffSourceBuffer, 'vimdiff') endif - let resultBuffer = s:HGReview(a:1) + let resultBuffer = HGReview(a:1) if resultBuffer < 0 echomsg "Can't open HG revision " . a:1 return resultBuffer @@ -945,10 +949,10 @@ function! s:HGVimDiff(...) let s:vimDiffScratchList = '{'. resultBuffer . '}' " If no split method is defined, cheat, and set it to vertical. try - call s:HGOverrideOption('HGCommandSplit', s:HGGetOption('HGCommandDiffSplit', s:HGGetOption('HGCommandSplit', 'vertical'))) - let resultBuffer=s:HGReview(a:2) + call HGOverrideOption('HGCommandSplit', HGGetOption('HGCommandDiffSplit', HGGetOption('HGCommandSplit', 'vertical'))) + let resultBuffer=HGReview(a:2) finally - call s:HGOverrideOption('HGCommandSplit') + call HGOverrideOption('HGCommandSplit') endtry if resultBuffer < 0 echomsg "Can't open HG revision " . a:1 @@ -962,16 +966,16 @@ function! s:HGVimDiff(...) " Add new buffer try " Force splitting behavior, otherwise why use vimdiff? - call s:HGOverrideOption("HGCommandEdit", "split") - call s:HGOverrideOption("HGCommandSplit", s:HGGetOption('HGCommandDiffSplit', s:HGGetOption('HGCommandSplit', 'vertical'))) + call HGOverrideOption("HGCommandEdit", "split") + call HGOverrideOption("HGCommandSplit", HGGetOption('HGCommandDiffSplit', HGGetOption('HGCommandSplit', 'vertical'))) if(a:0 == 0) - let resultBuffer=s:HGReview() + let resultBuffer=HGReview() else - let resultBuffer=s:HGReview(a:1) + let resultBuffer=HGReview(a:1) endif finally - call s:HGOverrideOption("HGCommandEdit") - call s:HGOverrideOption("HGCommandSplit") + call HGOverrideOption("HGCommandEdit") + call HGOverrideOption("HGCommandSplit") endtry if resultBuffer < 0 echomsg "Can't open current HG revision" @@ -990,14 +994,14 @@ function! s:HGVimDiff(...) wincmd W execute 'buffer' originalBuffer " Store info for later original buffer restore - let s:vimDiffRestoreCmd = + let s:vimDiffRestoreCmd = \ "call setbufvar(".originalBuffer.", \"&diff\", ".getbufvar(originalBuffer, '&diff').")" \ . "|call setbufvar(".originalBuffer.", \"&foldcolumn\", ".getbufvar(originalBuffer, '&foldcolumn').")" \ . "|call setbufvar(".originalBuffer.", \"&foldenable\", ".getbufvar(originalBuffer, '&foldenable').")" \ . "|call setbufvar(".originalBuffer.", \"&foldmethod\", '".getbufvar(originalBuffer, '&foldmethod')."')" \ . "|call setbufvar(".originalBuffer.", \"&scrollbind\", ".getbufvar(originalBuffer, '&scrollbind').")" \ . "|call setbufvar(".originalBuffer.", \"&wrap\", ".getbufvar(originalBuffer, '&wrap').")" - \ . "|if &foldmethod=='manual'|execute 'normal zE'|endif" + \ . "|if &foldmethod=='manual'|execute 'normal! zE'|endif" diffthis wincmd w else @@ -1027,17 +1031,17 @@ endfunction " Section: Command definitions {{{1 " Section: Primary commands {{{2 -com! HGAdd call s:HGAdd() -com! -nargs=? HGAnnotate call s:HGAnnotate() -com! -bang -nargs=? HGCommit call s:HGCommit(, ) -com! -nargs=* HGDiff call s:HGDiff() -com! -bang HGGotoOriginal call s:HGGotoOriginal() -com! -nargs=? HGLog call s:HGLog() -com! HGRevert call s:HGRevert() -com! -nargs=? HGReview call s:HGReview() -com! HGStatus call s:HGStatus() -com! HGUpdate call s:HGUpdate() -com! -nargs=* HGVimDiff call s:HGVimDiff() +com! HGAdd call HGAdd() +com! -nargs=? HGAnnotate call HGAnnotate() +com! -bang -nargs=? HGCommit call HGCommit(, ) +com! -nargs=* HGDiff call HGDiff() +com! -bang HGGotoOriginal call HGGotoOriginal() +com! -nargs=? HGLog call HGLog() +com! HGRevert call HGRevert() +com! -nargs=? HGReview call HGReview() +com! HGStatus call HGStatus() +com! HGUpdate call HGUpdate() +com! -nargs=* HGVimDiff call HGVimDiff() " Section: HG buffer management commands {{{2 com! HGDisableBufferSetup call HGDisableBufferSetup() @@ -1173,7 +1177,7 @@ endfunction augroup HGVimDiffRestore au! - au BufUnload * call s:HGVimDiffRestore(expand("")) + au BufUnload * call HGVimDiffRestore(expand("")) augroup END " Section: Optional activation of buffer management {{{1 @@ -1183,20 +1187,24 @@ if s:HGGetOption('HGCommandEnableBufferS endif " Section: Doc installation {{{1 -" - let s:revision="0.1" - silent! let s:install_status = - \ s:HGInstallDocumentation(expand(':p'), s:revision) - if (s:install_status == 1) - echom expand(":t:r") . ' v' . s:revision . - \ ': Help-documentation installed.' - endif +if HGInstallDocumentation(expand(":p")) + echomsg s:script_name s:script_version . ": updated documentation" +endif " Section: Plugin completion {{{1 +" delete one-time vars and functions +delfunction HGInstallDocumentation +delfunction HGFlexiMkdir +delfunction HGCleanupOnFailure +unlet s:script_version s:script_name + let loaded_hgcommand=2 silent do HGCommand User HGPluginFinish + +let &cpo = s:save_cpo +unlet s:save_cpo " vim:se expandtab sts=2 sw=2: finish @@ -1228,16 +1236,16 @@ 1. Contents *hgcommand-contents* ============================================================================== 2. HGCommand Installation *hgcommand-install* - In order to install the plugin, place the hgcommand.vim file into a plugin' - directory in your runtime path (please see |add-global-plugin| and + In order to install the plugin, place the hgcommand.vim file into a plugin' + directory in your runtime path (please see |add-global-plugin| and |'runtimepath'|. - HGCommand may be customized by setting variables, creating maps, and + HGCommand may be customized by setting variables, creating maps, and specifying event handlers. Please see |hgcommand-customize| for more details. *hgcommand-auto-help* - The help file is automagically generated when the |hgcommand| script is + The help file is automagically generated when the |hgcommand| script is loaded for the first time. ============================================================================== @@ -1245,32 +1253,32 @@ 2. HGCommand Installation *hgcomma 3. HGCommand Intro *hgcommand* *hgcommand-intro* - The HGCommand plugin provides global ex commands for manipulating - HG-controlled source files. In general, each command operates on the - current buffer and accomplishes a separate hg function, such as update, + The HGCommand plugin provides global ex commands for manipulating + HG-controlled source files. In general, each command operates on the + current buffer and accomplishes a separate hg function, such as update, commit, log, and others (please see |hgcommand-commands| for a list of all - available commands). The results of each operation are displayed in a - scratch buffer. Several buffer variables are defined for those scratch + available commands). The results of each operation are displayed in a + scratch buffer. Several buffer variables are defined for those scratch buffers (please see |hgcommand-buffer-variables|). - The notion of "current file" means either the current buffer, or, in the + The notion of "current file" means either the current buffer, or, in the case of a directory buffer, the file on the current line within the buffer. - For convenience, any HGCommand invoked on a HGCommand scratch buffer acts - as though it was invoked on the original file and splits the screen so that + For convenience, any HGCommand invoked on a HGCommand scratch buffer acts + as though it was invoked on the original file and splits the screen so that the output appears in a new window. - Many of the commands accept revisions as arguments. By default, most - operate on the most recent revision on the current branch if no revision is + Many of the commands accept revisions as arguments. By default, most + operate on the most recent revision on the current branch if no revision is specified (though see |HGCommandInteractive| to prompt instead). - Each HGCommand is mapped to a key sequence starting with the - keystroke. The default mappings may be overridden by supplying different - mappings before the plugin is loaded, such as in the vimrc, in the standard - fashion for plugin mappings. For examples, please see + Each HGCommand is mapped to a key sequence starting with the + keystroke. The default mappings may be overridden by supplying different + mappings before the plugin is loaded, such as in the vimrc, in the standard + fashion for plugin mappings. For examples, please see |hgcommand-mappings-override|. - The HGCommand plugin may be configured in several ways. For more details, + The HGCommand plugin may be configured in several ways. For more details, please see |hgcommand-customize|. ============================================================================== @@ -1294,85 +1302,85 @@ 4.1 HGCommand commands *hgcommand- :HGAdd *:HGAdd* - This command performs "hg add" on the current file. Please note, this does + This command performs "hg add" on the current file. Please note, this does not commit the newly-added file. :HGAnnotate *:HGAnnotate* - This command performs "hg annotate" on the current file. If an argument is - given, the argument is used as a revision number to display. If not given - an argument, it uses the most recent version of the file on the current - branch. Additionally, if the current buffer is a HGAnnotate buffer + This command performs "hg annotate" on the current file. If an argument is + given, the argument is used as a revision number to display. If not given + an argument, it uses the most recent version of the file on the current + branch. Additionally, if the current buffer is a HGAnnotate buffer already, the version number on the current line is used. - If the |HGCommandAnnotateParent| variable is set to a non-zero value, the - version previous to the one on the current line is used instead. This + If the |HGCommandAnnotateParent| variable is set to a non-zero value, the + version previous to the one on the current line is used instead. This allows one to navigate back to examine the previous version of a line. - The filetype of the HGCommand scratch buffer is set to 'HGAnnotate', to + The filetype of the HGCommand scratch buffer is set to 'HGAnnotate', to take advantage of the bundled syntax file. :HGCommit[!] *:HGCommit* - If called with arguments, this performs "hg commit" using the arguments as + If called with arguments, this performs "hg commit" using the arguments as the log message. If '!' is used with no arguments, an empty log message is committed. - If called with no arguments, this is a two-step command. The first step - opens a buffer to accept a log message. When that buffer is written, it is - automatically closed and the file is committed using the information from - that log message. The commit can be abandoned if the log message buffer is + If called with no arguments, this is a two-step command. The first step + opens a buffer to accept a log message. When that buffer is written, it is + automatically closed and the file is committed using the information from + that log message. The commit can be abandoned if the log message buffer is deleted or wiped before being written. - Alternatively, the mapping that is used to invoke :HGCommit (by default - hgc) can be used in the log message buffer to immediately commit. - This is useful if the |HGCommandCommitOnWrite| variable is set to 0 to + Alternatively, the mapping that is used to invoke :HGCommit (by default + hgc) can be used in the log message buffer to immediately commit. + This is useful if the |HGCommandCommitOnWrite| variable is set to 0 to disable the normal commit-on-write behavior. :HGDiff *:HGDiff* - With no arguments, this performs "hg diff" on the current file against the + With no arguments, this performs "hg diff" on the current file against the current repository version. - With one argument, "hg diff" is performed on the current file against the + With one argument, "hg diff" is performed on the current file against the specified revision. - With two arguments, hg diff is performed between the specified revisions of + With two arguments, hg diff is performed between the specified revisions of the current file. - This command uses the 'HGCommandDiffOpt' variable to specify diff options. - If that variable does not exist, then 'wbBc' is assumed. If you wish to + This command uses the 'HGCommandDiffOpt' variable to specify diff options. + If that variable does not exist, then 'wbBc' is assumed. If you wish to have no options, then set it to the empty string. :HGGotoOriginal *:HGGotoOriginal* - This command returns the current window to the source buffer, if the + This command returns the current window to the source buffer, if the current buffer is a HG command output buffer. :HGGotoOriginal! - Like ":HGGotoOriginal" but also executes :bufwipeout on all HG command + Like ":HGGotoOriginal" but also executes :bufwipeout on all HG command output buffers for the source buffer. :HGLog *:HGLog* Performs "hg log" on the current file. - If an argument is given, it is passed as an argument to the "-r" option of + If an argument is given, it is passed as an argument to the "-r" option of "hg log". :HGRevert *:HGRevert* - Replaces the current file with the most recent version from the repository + Replaces the current file with the most recent version from the repository in order to wipe out any undesired changes. - + :HGReview *:HGReview* - Retrieves a particular version of the current file. If no argument is - given, the most recent version of the file on the current branch is + Retrieves a particular version of the current file. If no argument is + given, the most recent version of the file on the current branch is retrieved. Otherwise, the specified version is retrieved. :HGStatus *:HGStatus* @@ -1381,37 +1389,37 @@ 4.1 HGCommand commands *hgcommand- :HGUpdate *:HGUpdate* - Performs "hg update" on the current file. This intentionally does not - automatically reload the current buffer, though vim should prompt the user + Performs "hg update" on the current file. This intentionally does not + automatically reload the current buffer, though vim should prompt the user to do so if the underlying file is altered by this command. :HGVimDiff *:HGVimDiff* - With no arguments, this prompts the user for a revision and then uses - vimdiff to display the differences between the current file and the - specified revision. If no revision is specified, the most recent version + With no arguments, this prompts the user for a revision and then uses + vimdiff to display the differences between the current file and the + specified revision. If no revision is specified, the most recent version of the file on the current branch is used. - With one argument, that argument is used as the revision as above. With - two arguments, the differences between the two revisions is displayed using + With one argument, that argument is used as the revision as above. With + two arguments, the differences between the two revisions is displayed using vimdiff. - With either zero or one argument, the original buffer is used to perform - the vimdiff. When the other buffer is closed, the original buffer will be + With either zero or one argument, the original buffer is used to perform + the vimdiff. When the other buffer is closed, the original buffer will be returned to normal mode. - Once vimdiff mode is started using the above methods, additional vimdiff - buffers may be added by passing a single version argument to the command. + Once vimdiff mode is started using the above methods, additional vimdiff + buffers may be added by passing a single version argument to the command. There may be up to 4 vimdiff buffers total. - Using the 2-argument form of the command resets the vimdiff to only those 2 - versions. Additionally, invoking the command on a different file will + Using the 2-argument form of the command resets the vimdiff to only those 2 + versions. Additionally, invoking the command on a different file will close the previous vimdiff buffers. 4.2 Mappings *hgcommand-mappings* - By default, a mapping is defined for each command. These mappings execute + By default, a mapping is defined for each command. These mappings execute the default (no-argument) form of each command. hga HGAdd @@ -1428,20 +1436,20 @@ 4.2 Mappings *hgcommand-mappings* *hgcommand-mappings-override* - The default mappings can be overriden by user-provided instead by mapping - to CommandName. This is especially useful when these mappings - collide with other existing mappings (vim will warn of this during plugin + The default mappings can be overriden by user-provided instead by mapping + to CommandName. This is especially useful when these mappings + collide with other existing mappings (vim will warn of this during plugin initialization, but will not clobber the existing mappings). - For instance, to override the default mapping for :HGAdd to set it to + For instance, to override the default mapping for :HGAdd to set it to '\add', add the following to the vimrc: > nmap \add HGAdd < 4.3 Automatic buffer variables *hgcommand-buffer-variables* - Several buffer variables are defined in each HGCommand result buffer. - These may be useful for additional customization in callbacks defined in + Several buffer variables are defined in each HGCommand result buffer. + These may be useful for additional customization in callbacks defined in the event handlers (please see |hgcommand-events|). The following variables are automatically defined: @@ -1452,24 +1460,24 @@ b:hgOrigBuffNR *b:hgOrigBuffN b:hgcmd *b:hgcmd* - This variable is set to the name of the hg command that created the result + This variable is set to the name of the hg command that created the result buffer. ============================================================================== 5. Configuration and customization *hgcommand-customize* *hgcommand-config* - The HGCommand plugin can be configured in two ways: by setting - configuration variables (see |hgcommand-options|) or by defining HGCommand - event handlers (see |hgcommand-events|). Additionally, the HGCommand - plugin provides several option for naming the HG result buffers (see - |hgcommand-naming|) and supported a customized status line (see + The HGCommand plugin can be configured in two ways: by setting + configuration variables (see |hgcommand-options|) or by defining HGCommand + event handlers (see |hgcommand-events|). Additionally, the HGCommand + plugin provides several option for naming the HG result buffers (see + |hgcommand-naming|) and supported a customized status line (see |hgcommand-statusline| and |hgcommand-buffer-management|). 5.1 HGCommand configuration variables *hgcommand-options* - Several variables affect the plugin's behavior. These variables are - checked at time of execution, and may be defined at the window, buffer, or + Several variables affect the plugin's behavior. These variables are + checked at time of execution, and may be defined at the window, buffer, or global level and are checked in that order of precedence. @@ -1490,87 +1498,87 @@ 5.1 HGCommand configuration variables HGCommandAnnotateParent *HGCommandAnnotateParent* - This variable, if set to a non-zero value, causes the zero-argument form of - HGAnnotate when invoked on a HGAnnotate buffer to go to the version - previous to that displayed on the current line. If not set, it defaults to + This variable, if set to a non-zero value, causes the zero-argument form of + HGAnnotate when invoked on a HGAnnotate buffer to go to the version + previous to that displayed on the current line. If not set, it defaults to 0. HGCommandCommitOnWrite *HGCommandCommitOnWrite* - This variable, if set to a non-zero value, causes the pending hg commit to - take place immediately as soon as the log message buffer is written. If - set to zero, only the HGCommit mapping will cause the pending commit to + This variable, if set to a non-zero value, causes the pending hg commit to + take place immediately as soon as the log message buffer is written. If + set to zero, only the HGCommit mapping will cause the pending commit to occur. If not set, it defaults to 1. HGCommandHGExec *HGCommandHGExec* - This variable controls the executable used for all HG commands. If not + This variable controls the executable used for all HG commands. If not set, it defaults to "hg". HGCommandDeleteOnHide *HGCommandDeleteOnHide* - This variable, if set to a non-zero value, causes the temporary HG result + This variable, if set to a non-zero value, causes the temporary HG result buffers to automatically delete themselves when hidden. HGCommandDiffOpt *HGCommandDiffOpt* - This variable, if set, determines the options passed to the diff command of + This variable, if set, determines the options passed to the diff command of HG. If not set, it defaults to 'w'. HGCommandDiffSplit *HGCommandDiffSplit* - This variable overrides the |HGCommandSplit| variable, but only for buffers + This variable overrides the |HGCommandSplit| variable, but only for buffers created with |:HGVimDiff|. HGCommandEdit *HGCommandEdit* - This variable controls whether the original buffer is replaced ('edit') or + This variable controls whether the original buffer is replaced ('edit') or split ('split'). If not set, it defaults to 'edit'. HGCommandEnableBufferSetup *HGCommandEnableBufferSetup* - This variable, if set to a non-zero value, activates HG buffer management - mode see (|hgcommand-buffer-management|). This mode means that three - buffer variables, 'HGRepository', 'HGRevision' and 'HGBranch', are set if - the file is HG-controlled. This is useful for displaying version + This variable, if set to a non-zero value, activates HG buffer management + mode see (|hgcommand-buffer-management|). This mode means that three + buffer variables, 'HGRepository', 'HGRevision' and 'HGBranch', are set if + the file is HG-controlled. This is useful for displaying version information in the status bar. HGCommandInteractive *HGCommandInteractive* - This variable, if set to a non-zero value, causes appropriate commands (for - the moment, only |:HGReview|) to query the user for a revision to use + This variable, if set to a non-zero value, causes appropriate commands (for + the moment, only |:HGReview|) to query the user for a revision to use instead of the current revision if none is specified. HGCommandNameMarker *HGCommandNameMarker* - This variable, if set, configures the special attention-getting characters - that appear on either side of the hg buffer type in the buffer name. This - has no effect unless |HGCommandNameResultBuffers| is set to a true value. - If not set, it defaults to '_'. + This variable, if set, configures the special attention-getting characters + that appear on either side of the hg buffer type in the buffer name. This + has no effect unless |HGCommandNameResultBuffers| is set to a true value. + If not set, it defaults to '_'. HGCommandNameResultBuffers *HGCommandNameResultBuffers* - This variable, if set to a true value, causes the hg result buffers to be - named in the old way (' __'). If not set or + This variable, if set to a true value, causes the hg result buffers to be + named in the old way (' __'). If not set or set to a false value, the result buffer is nameless. HGCommandSplit *HGCommandSplit* - This variable controls the orientation of the various window splits that - may occur (such as with HGVimDiff, when using a HG command on a HG command - buffer, or when the |HGCommandEdit| variable is set to 'split'. If set to - 'horizontal', the resulting windows will be on stacked on top of one - another. If set to 'vertical', the resulting windows will be side-by-side. + This variable controls the orientation of the various window splits that + may occur (such as with HGVimDiff, when using a HG command on a HG command + buffer, or when the |HGCommandEdit| variable is set to 'split'. If set to + 'horizontal', the resulting windows will be on stacked on top of one + another. If set to 'vertical', the resulting windows will be side-by-side. If not set, it defaults to 'horizontal' for all but HGVimDiff windows. 5.2 HGCommand events *hgcommand-events* - For additional customization, HGCommand can trigger user-defined events. - Event handlers are provided by defining User event autocommands (see - |autocommand|, |User|) in the HGCommand group with patterns matching the + For additional customization, HGCommand can trigger user-defined events. + Event handlers are provided by defining User event autocommands (see + |autocommand|, |User|) in the HGCommand group with patterns matching the event name. - For instance, the following could be added to the vimrc to provide a 'q' + For instance, the following could be added to the vimrc to provide a 'q' mapping to quit a HGCommand scratch buffer: > augroup HGCommand @@ -1582,10 +1590,10 @@ 5.2 HGCommand events *hgc The following hooks are available: HGBufferCreated This event is fired just after a hg command result - buffer is created and filled with the result of a hg - command. It is executed within the context of the HG - command buffer. The HGCommand buffer variables may be - useful for handlers of this event (please see + buffer is created and filled with the result of a hg + command. It is executed within the context of the HG + command buffer. The HGCommand buffer variables may be + useful for handlers of this event (please see |hgcommand-buffer-variables|). HGBufferSetup This event is fired just after HG buffer setup occurs, @@ -1598,50 +1606,50 @@ HGPluginFinish This event is fired just loads. HGVimDiffFinish This event is fired just after the HGVimDiff command - executes to allow customization of, for instance, + executes to allow customization of, for instance, window placement and focus. 5.3 HGCommand buffer naming *hgcommand-naming* - By default, the buffers containing the result of HG commands are nameless - scratch buffers. It is intended that buffer variables of those buffers be - used to customize the statusline option so that the user may fully control + By default, the buffers containing the result of HG commands are nameless + scratch buffers. It is intended that buffer variables of those buffers be + used to customize the statusline option so that the user may fully control the display of result buffers. - If the old-style naming is desired, please enable the - |HGCommandNameResultBuffers| variable. Then, each result buffer will - receive a unique name that includes the source file name, the HG command, - and any extra data (such as revision numbers) that were part of the + If the old-style naming is desired, please enable the + |HGCommandNameResultBuffers| variable. Then, each result buffer will + receive a unique name that includes the source file name, the HG command, + and any extra data (such as revision numbers) that were part of the command. 5.4 HGCommand status line support *hgcommand-statusline* - It is intended that the user will customize the |'statusline'| option to - include HG result buffer attributes. A sample function that may be used in - the |'statusline'| option is provided by the plugin, HGGetStatusLine(). In - order to use that function in the status line, do something like the + It is intended that the user will customize the |'statusline'| option to + include HG result buffer attributes. A sample function that may be used in + the |'statusline'| option is provided by the plugin, HGGetStatusLine(). In + order to use that function in the status line, do something like the following: > set statusline=%<%f\ %{HGGetStatusLine()}\ %h%m%r%=%l,%c%V\ %P < of which %{HGGetStatusLine()} is the relevant portion. - The sample HGGetStatusLine() function handles both HG result buffers and - HG-managed files if HGCommand buffer management is enabled (please see + The sample HGGetStatusLine() function handles both HG result buffers and + HG-managed files if HGCommand buffer management is enabled (please see |hgcommand-buffer-management|). 5.5 HGCommand buffer management *hgcommand-buffer-management* - The HGCommand plugin can operate in buffer management mode, which means - that it attempts to set two buffer variables ('HGRevision' and 'HGBranch') - upon entry into a buffer. This is rather slow because it means that 'hg - status' will be invoked at each entry into a buffer (during the |BufEnter| + The HGCommand plugin can operate in buffer management mode, which means + that it attempts to set two buffer variables ('HGRevision' and 'HGBranch') + upon entry into a buffer. This is rather slow because it means that 'hg + status' will be invoked at each entry into a buffer (during the |BufEnter| autocommand). - This mode is enablmed by default. In order to disable it, set the - |HGCommandEnableBufferSetup| variable to a false (zero) value. Enabling - this mode simply provides the buffer variables mentioned above. The user - must explicitly include those in the |'statusline'| option if they are to + This mode is enabled by default. In order to disable it, set the + |HGCommandEnableBufferSetup| variable to a false (zero) value. Enabling + this mode simply provides the buffer variables mentioned above. The user + must explicitly include those in the |'statusline'| option if they are to appear in the status line (but see |hgcommand-statusline| for a simple way to do that). @@ -1655,10 +1663,10 @@ 9.1 Split window annotation, by Michael \:set nowrap < - This splits the buffer vertically, puts an annotation on the left (minus - the header) with the width set to 40. An editable/normal copy is placed on - the right. The two versions are scroll locked so they move as one. and - wrapping is turned off so that the lines line up correctly. The advantages + This splits the buffer vertically, puts an annotation on the left (minus + the header) with the width set to 40. An editable/normal copy is placed on + the right. The two versions are scroll locked so they move as one. and + wrapping is turned off so that the lines line up correctly. The advantages are... 1) You get a versioning on the right. @@ -1671,9 +1679,9 @@ 8. Known bugs *hgcommand-bugs Please let me know if you run across any. - HGVimDiff, when using the original (real) source buffer as one of the diff - buffers, uses some hacks to try to restore the state of the original buffer - when the scratch buffer containing the other version is destroyed. There + HGVimDiff, when using the original (real) source buffer as one of the diff + buffers, uses some hacks to try to restore the state of the original buffer + when the scratch buffer containing the other version is destroyed. There may still be bugs in here, depending on many configuration details. ============================================================================== @@ -1686,4 +1694,4 @@ 9. TODO *hgcommand-todo* """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" " v im:tw=78:ts=8:ft=help:norl: " vim600: set foldmethod=marker tabstop=8 shiftwidth=2 softtabstop=2 smartindent smarttab : -"fileencoding=iso-8859-15 +"fileencoding=iso-8859-15 diff --git a/contrib/win32/ReadMe.html b/contrib/win32/ReadMe.html --- a/contrib/win32/ReadMe.html +++ b/contrib/win32/ReadMe.html @@ -14,7 +14,7 @@ -

Mercurial version 0.9 for Windows

+

Mercurial version 0.9.1 for Windows

Welcome to Mercurial for Windows!

diff --git a/contrib/win32/mercurial.iss b/contrib/win32/mercurial.iss --- a/contrib/win32/mercurial.iss +++ b/contrib/win32/mercurial.iss @@ -4,7 +4,7 @@ [Setup] AppCopyright=Copyright 2005, 2006 Matt Mackall and others AppName=Mercurial -AppVerName=Mercurial version 0.9 +AppVerName=Mercurial version 0.9.1 InfoAfterFile=contrib/win32/postinstall.txt LicenseFile=COPYING ShowLanguageDialog=yes @@ -14,10 +14,10 @@ AppSupportURL=http://www.selenic.com/mer AppUpdatesURL=http://www.selenic.com/mercurial AppID={{4B95A5F1-EF59-4B08-BED8-C891C46121B3} AppContact=mercurial@selenic.com -OutputBaseFilename=Mercurial-0.9 +OutputBaseFilename=Mercurial-0.9.1 DefaultDirName={sd}\Mercurial SourceDir=C:\hg\hg-release -VersionInfoVersion=0.9 +VersionInfoVersion=0.9.1 VersionInfoDescription=Mercurial distributed SCM VersionInfoCopyright=Copyright 2005, 2006 Matt Mackall and others VersionInfoCompany=Matt Mackall and others diff --git a/contrib/win32/postinstall.txt b/contrib/win32/postinstall.txt --- a/contrib/win32/postinstall.txt +++ b/contrib/win32/postinstall.txt @@ -7,6 +7,62 @@ file that comes with this package. Release Notes ------------- +2006-07-24 v0.9.1 + +Major changes between Mercurial 0.9 and 0.9.1: + + New features: + - You can now configure your 'hgweb' server to let remote users + 'push' changes over http. + - You can now 'import' a patch in a mail message by saving the mail + message, and importing it. This works for patches sent either + inline or as attachments. + - The 'diff' command now accepts '-rA:B' syntax as a synonym for + '-r A -r B', and adds '-b' and '-B' options. + + New contributions and extensions: + - The 'acl' extension lets you lock down parts of a repository + against incoming changes + - The 'extdiff' extension lets you run your favourite graphical + change viewer + - Comprehensive integration with the 'vim' editor + - A restricted shell for 'ssh'-hosted repositories + - An importer for 'darcs' repositories + + New hooks added: + - 'preupdate' is run before an update or merge in the working + directory. + - 'update' is run after an update or merge in the working + directory. + + Behaviour changes: + - NOTE: Mercurial as installed by the Windows binary + installer no longer performs automatic line-ending conversion for + Unix/Linux compatibility. To re-enable this feature, edit your + 'mercurial.ini' file after you upgrade. + - The Windows binary installer now automatically adds 'hg' to your + '%PATH%'. + - The 'backout' command now runs an editor by default, to let you + modify the commit message for a backed-out changeset. + - An earlier problem with parsing of tags has been fixed. + This makes tag parsing slower but more reliable. + + Memory usage and performance improvements: + - The 'remove' command has been rewritten to be hundreds of times + faster in large repositories. + - It is now possible to 'clone' a repository very quickly over a + LAN, if the server is configured to allow it. See the new 'server' + section in the 'hgrc' documentation. + + Other changes of note: + - Mercurial will now print help for an extension if you type 'hg + help EXT_NAME'. + - The usual array of bug fixes and documentation improvements. + - The integrated web server is now more WSGI-compliant. + - Work has begun to solidify Mercurial's API for use by third-party + packages. + + 2006-05-10 v0.9 * Major changes between Mercurial 0.8.1 and 0.9: diff --git a/doc/hg.1.txt b/doc/hg.1.txt --- a/doc/hg.1.txt +++ b/doc/hg.1.txt @@ -216,6 +216,6 @@ http://selenic.com/mailman/listinfo/merc COPYING ------- -Copyright \(C) 2005 Matt Mackall. +Copyright \(C) 2005, 2006 Matt Mackall. Free use of this software is granted under the terms of the GNU General Public License (GPL). diff --git a/doc/hgmerge.1.txt b/doc/hgmerge.1.txt --- a/doc/hgmerge.1.txt +++ b/doc/hgmerge.1.txt @@ -30,6 +30,6 @@ hg(1) - the command line interface to Me COPYING ------- -Copyright \(C) 2005 Matt Mackall. +Copyright \(C) 2005, 2006 Matt Mackall. Free use of this software is granted under the terms of the GNU General Public License (GPL). diff --git a/doc/hgrc.5.txt b/doc/hgrc.5.txt --- a/doc/hgrc.5.txt +++ b/doc/hgrc.5.txt @@ -138,9 +138,17 @@ email:: from;; Optional. Email address to use in "From" header and SMTP envelope of outgoing messages. + to;; + Optional. Comma-separated list of recipients' email addresses. + cc;; + Optional. Comma-separated list of carbon copy recipients' + email addresses. + bcc;; + Optional. Comma-separated list of blind carbon copy + recipients' email addresses. Cannot be set interactively. method;; Optional. Method to use to send email messages. If value is - "smtp" (default), use SMTP (see section "[mail]" for + "smtp" (default), use SMTP (see section "[smtp]" for configuration). Otherwise, use as name of program to run that acts like sendmail (takes "-f" option for sender, list of recipients on command line, message on stdin). Normally, setting @@ -194,7 +202,8 @@ hooks:: changegroup;; Run after a changegroup has been added via push, pull or - unbundle. ID of the first new changeset is in $HG_NODE. + unbundle. ID of the first new changeset is in $HG_NODE. URL from + which changes came is in $HG_URL. commit;; Run after a changeset has been created in the local repository. ID of the newly created changeset is in $HG_NODE. Parent @@ -202,7 +211,7 @@ hooks:: incoming;; Run after a changeset has been pulled, pushed, or unbundled into the local repository. The ID of the newly arrived changeset is in - $HG_NODE. + $HG_NODE. URL that was source of changes came is in $HG_URL. outgoing;; Run after sending changes from local repository to another. ID of first changeset sent is in $HG_NODE. Source of operation is in @@ -210,7 +219,8 @@ hooks:: prechangegroup;; Run before a changegroup is added via push, pull or unbundle. Exit status 0 allows the changegroup to proceed. Non-zero status - will cause the push, pull or unbundle to fail. + will cause the push, pull or unbundle to fail. URL from which + changes will come is in $HG_URL. precommit;; Run before starting a local commit. Exit status 0 allows the commit to proceed. Non-zero status will cause the commit to fail. @@ -236,7 +246,8 @@ hooks:: before accepting them. Passed the ID of the first new changeset in $HG_NODE. Exit status 0 allows the transaction to commit. Non-zero status will cause the transaction to be rolled back and - the push, pull or unbundle will fail. + the push, pull or unbundle will fail. URL that was source of + changes is in $HG_URL. pretxncommit;; Run after a changeset has been created but the transaction not yet committed. Changeset is visible to hook program. This lets you @@ -295,7 +306,7 @@ http_proxy:: smtp:: Configuration for extensions that need to send email messages. host;; - Optional. Host name of mail server. Default: "mail". + Host name of mail server, e.g. "mail.example.com". port;; Optional. Port to connect to on mail server. Default: 25. tls;; @@ -440,6 +451,9 @@ web:: push_ssl;; Whether to require that inbound pushes be transported over SSL to prevent password sniffing. Default is true. + stripes;; + How many lines a "zebra stripe" should span in multiline output. + Default is 1; set to 0 to disable. style;; Which template map style to use. templates;; diff --git a/doc/ja/hg.1.ja.txt b/doc/ja/hg.1.ja.txt --- a/doc/ja/hg.1.ja.txt +++ b/doc/ja/hg.1.ja.txt @@ -862,6 +862,6 @@ http://selenic.com/mailman/listinfo/mercurial[メーリングリスト] 著作権情報 ----- -Copyright (C) 2005 Matt Mackall. +Copyright (C) 2005, 2006 Matt Mackall. このソフトウェアの自由な使用は GNU 一般公有使用許諾 (GPL) のもとで 認められます。 diff --git a/doc/ja/hgmerge.1.ja.txt b/doc/ja/hgmerge.1.ja.txt --- a/doc/ja/hgmerge.1.ja.txt +++ b/doc/ja/hgmerge.1.ja.txt @@ -32,6 +32,6 @@ hg(1) - Mercurial システムへのコマンドラインインターフェイス 著作権情報 ---- -Copyright (C) 2005 Matt Mackall. +Copyright (C) 2005, 2006 Matt Mackall. このソフトウェアの自由な使用は GNU 一般公有使用許諾 (GPL) のもとで 認められます。 diff --git a/hgext/extdiff.py b/hgext/extdiff.py --- a/hgext/extdiff.py +++ b/hgext/extdiff.py @@ -5,34 +5,49 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. # -# allow to use external programs to compare revisions, or revision -# with working dir. program is called with two arguments: paths to -# directories containing snapshots of files to compare. +# The `extdiff' Mercurial extension allows you to use external programs +# to compare revisions, or revision with working dir. The external diff +# programs are called with a configurable set of options and two +# non-option arguments: paths to directories containing snapshots of +# files to compare. # -# to enable: +# To enable this extension: # # [extensions] # hgext.extdiff = # -# also allows to configure new diff commands, so you do not need to -# type "hg extdiff -p kdiff3" always. +# The `extdiff' extension also allows to configure new diff commands, so +# you do not need to type "hg extdiff -p kdiff3" always. # # [extdiff] +# # add new command that runs GNU diff(1) in 'context diff' mode +# cmd.cdiff = gdiff +# opts.cdiff = -Nprc5 # # add new command called vdiff, runs kdiff3 # cmd.vdiff = kdiff3 # # add new command called meld, runs meld (no need to name twice) # cmd.meld = +# # add new command called vimdiff, runs gvimdiff with DirDiff plugin +# #(see http://www.vim.org/scripts/script.php?script_id=102) +# cmd.vimdiff = LC_ALL=C gvim -f '+bdel 1 2' '+ execute "DirDiff ".argv(0)." ".argv(1)' # -# you can use -I/-X and list of file or directory names like normal -# "hg diff" command. extdiff makes snapshots of only needed files, so -# compare program will be fast. +# Each custom diff commands can have two parts: a `cmd' and an `opts' +# part. The cmd.xxx option defines the name of an executable program +# that will be run, and opts.xxx defines a set of command-line options +# which will be inserted to the command between the program name and +# the files/directories to diff (i.e. the cdiff example above). +# +# You can use -I/-X and list of file or directory names like normal +# "hg diff" command. The `extdiff' extension makes snapshots of only +# needed files, so running the external diff program will actually be +# pretty fast (at least faster than having to compare the entire tree). from mercurial.demandload import demandload from mercurial.i18n import gettext as _ from mercurial.node import * -demandload(globals(), 'mercurial:commands,util os shutil tempfile') +demandload(globals(), 'mercurial:commands,cmdutil,util os shutil tempfile') -def dodiff(ui, repo, diffcmd, pats, opts): +def dodiff(ui, repo, diffcmd, diffopts, pats, opts): def snapshot_node(files, node): '''snapshot files as of some revision''' changes = repo.changelog.read(node) @@ -76,9 +91,9 @@ def dodiff(ui, repo, diffcmd, pats, opts return dirname node1, node2 = commands.revpair(ui, repo, opts['rev']) - files, matchfn, anypats = commands.matchpats(repo, pats, opts) - modified, added, removed, deleted, unknown = repo.changes( - node1, node2, files, match=matchfn) + files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) + modified, added, removed, deleted, unknown = repo.status( + node1, node2, files, match=matchfn)[:5] if not (modified or added or removed): return 0 @@ -89,9 +104,12 @@ def dodiff(ui, repo, diffcmd, pats, opts dir2 = snapshot_node(modified + added, node2) else: dir2 = snapshot_wdir(modified + added) - util.system('%s %s "%s" "%s"' % - (diffcmd, ' '.join(opts['option']), dir1, dir2), - cwd=tmproot) + cmdline = ('%s %s %s %s' % + (util.shellquote(diffcmd), + ' '.join(map(util.shellquote, diffopts)), + util.shellquote(dir1), util.shellquote(dir2))) + ui.debug('running %r in %s\n' % (cmdline, tmproot)) + util.system(cmdline, cwd=tmproot) return 1 finally: ui.note(_('cleaning up temp directory\n')) @@ -101,7 +119,9 @@ def extdiff(ui, repo, *pats, **opts): '''use external program to diff repository (or selected files) Show differences between revisions for the specified files, using - an external program. The default program used is "diff -Npru". + an external program. The default program used is diff, with + default options "-Npru". + To select a different program, use the -p option. The program will be passed the names of two directories to compare. To pass additional options to the program, use the -o option. These will @@ -112,7 +132,8 @@ def extdiff(ui, repo, *pats, **opts): specified then that revision is compared to the working directory, and, when no revisions are specified, the working directory files are compared to its parent.''' - return dodiff(ui, repo, opts['program'] or 'diff -Npru', pats, opts) + return dodiff(ui, repo, opts['program'] or 'diff', + opts['option'] or ['-Npru'], pats, opts) cmdtable = { "extdiff": @@ -130,20 +151,24 @@ def uisetup(ui): if not cmd.startswith('cmd.'): continue cmd = cmd[4:] if not path: path = cmd + diffopts = ui.config('extdiff', 'opts.' + cmd, '') + diffopts = diffopts and [diffopts] or [] def save(cmd, path): '''use closure to save diff command to use''' def mydiff(ui, repo, *pats, **opts): - return dodiff(ui, repo, path, pats, opts) - mydiff.__doc__ = '''use %s to diff repository (or selected files) + return dodiff(ui, repo, path, diffopts, pats, opts) + mydiff.__doc__ = '''use %(path)r to diff repository (or selected files) Show differences between revisions for the specified - files, using the %s program. + files, using the %(path)r program. When two revision arguments are given, then changes are shown between those revisions. If only one revision is specified then that revision is compared to the working directory, and, when no revisions are specified, the - working directory files are compared to its parent.''' % (cmd, cmd) + working directory files are compared to its parent.''' % { + 'path': path, + } return mydiff cmdtable[cmd] = (save(cmd, path), cmdtable['extdiff'][1][1:], diff --git a/hgext/fetch.py b/hgext/fetch.py new file mode 100644 --- /dev/null +++ b/hgext/fetch.py @@ -0,0 +1,99 @@ +# fetch.py - pull and merge remote changes +# +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from mercurial.demandload import * +from mercurial.i18n import gettext as _ +from mercurial.node import * +demandload(globals(), 'mercurial:commands,hg,node,util') + +def fetch(ui, repo, source='default', **opts): + '''Pull changes from a remote repository, merge new changes if needed. + + This finds all changes from the repository at the specified path + or URL and adds them to the local repository. + + If the pulled changes add a new head, the head is automatically + merged, and the result of the merge is committed. Otherwise, the + working directory is updated.''' + + def postincoming(other, modheads): + if modheads == 0: + return 0 + if modheads == 1: + return hg.clean(repo, repo.changelog.tip(), wlock=wlock) + newheads = repo.heads(parent) + newchildren = [n for n in repo.heads(parent) if n != parent] + newparent = parent + if newchildren: + newparent = newchildren[0] + hg.clean(repo, newparent, wlock=wlock) + newheads = [n for n in repo.heads() if n != newparent] + err = False + if newheads: + ui.status(_('merging with new head %d:%s\n') % + (repo.changelog.rev(newheads[0]), short(newheads[0]))) + err = hg.merge(repo, newheads[0], remind=False, wlock=wlock) + if not err and len(newheads) > 1: + ui.status(_('not merging with %d other new heads ' + '(use "hg heads" and "hg merge" to merge them)') % + (len(newheads) - 1)) + if not err: + mod, add, rem = repo.status(wlock=wlock)[:3] + message = (commands.logmessage(opts) or + (_('Automated merge with %s') % other.url())) + n = repo.commit(mod + add + rem, message, + opts['user'], opts['date'], lock=lock, wlock=wlock, + force_editor=opts.get('force_editor')) + ui.status(_('new changeset %d:%s merges remote changes ' + 'with local\n') % (repo.changelog.rev(n), + short(n))) + def pull(): + commands.setremoteconfig(ui, opts) + + other = hg.repository(ui, ui.expandpath(source)) + ui.status(_('pulling from %s\n') % ui.expandpath(source)) + revs = None + if opts['rev'] and not other.local(): + raise util.Abort(_("fetch -r doesn't work for remote repositories yet")) + elif opts['rev']: + revs = [other.lookup(rev) for rev in opts['rev']] + modheads = repo.pull(other, heads=revs, lock=lock) + return postincoming(other, modheads) + + parent, p2 = repo.dirstate.parents() + if parent != repo.changelog.tip(): + raise util.Abort(_('working dir not at tip ' + '(use "hg update" to check out tip)')) + if p2 != nullid: + raise util.Abort(_('outstanding uncommitted merge')) + wlock = repo.wlock() + lock = repo.lock() + try: + mod, add, rem = repo.status(wlock=wlock)[:3] + if mod or add or rem: + raise util.Abort(_('outstanding uncommitted changes')) + if len(repo.heads()) > 1: + raise util.Abort(_('multiple heads in this repository ' + '(use "hg heads" and "hg merge" to merge)')) + return pull() + finally: + lock.release() + wlock.release() + +cmdtable = { + 'fetch': + (fetch, + [('e', 'ssh', '', _('specify ssh command to use')), + ('m', 'message', '', _('use as commit message')), + ('l', 'logfile', '', _('read the commit message from ')), + ('d', 'date', '', _('record datecode as commit date')), + ('u', 'user', '', _('record user as commiter')), + ('r', 'rev', [], _('a specific revision you would like to pull')), + ('f', 'force-editor', None, _('edit commit message')), + ('', 'remotecmd', '', _('hg command to run on the remote side'))], + 'hg fetch [SOURCE]'), + } diff --git a/hgext/gpg.py b/hgext/gpg.py --- a/hgext/gpg.py +++ b/hgext/gpg.py @@ -221,7 +221,7 @@ def sign(ui, repo, *revs, **opts): repo.opener("localsigs", "ab").write(sigmessage) return - for x in repo.changes(): + for x in repo.status()[:5]: if ".hgsigs" in x and not opts["force"]: raise util.Abort(_("working copy of .hgsigs is changed " "(please commit .hgsigs manually " diff --git a/hgext/hbisect.py b/hgext/hbisect.py --- a/hgext/hbisect.py +++ b/hgext/hbisect.py @@ -23,10 +23,10 @@ def lookup_rev(ui, repo, rev=None): return parents.pop() def check_clean(ui, repo): - modified, added, removed, deleted, unknown = repo.changes() - if modified or added or removed: - ui.warn("Repository is not clean, please commit or revert\n") - sys.exit(1) + modified, added, removed, deleted, unknown = repo.status()[:5] + if modified or added or removed: + ui.warn("Repository is not clean, please commit or revert\n") + sys.exit(1) class bisect(object): """dichotomic search in the DAG of changesets""" @@ -50,7 +50,7 @@ class bisect(object): if r: self.badrev = hg.bin(r.pop(0)) - def __del__(self): + def write(self): if not os.path.isdir(self.path): return f = self.opener(self.good_path, "w") @@ -197,7 +197,7 @@ class bisect(object): check_clean(self.ui, self.repo) rev = self.next() if rev is not None: - return self.repo.update(rev, force=True) + return hg.clean(self.repo, rev) def good(self, rev): self.goodrevs.append(rev) @@ -288,7 +288,10 @@ for subcommands see "hg bisect help\" if len(args) > bisectcmdtable[cmd][1]: ui.warn(_("bisect: Too many arguments\n")) return help_() - return bisectcmdtable[cmd][0](*args) + try: + return bisectcmdtable[cmd][0](*args) + finally: + b.write() cmdtable = { "bisect": (bisect_run, [], _("hg bisect [help|init|reset|next|good|bad]")), diff --git a/hgext/hgk.py b/hgext/hgk.py --- a/hgext/hgk.py +++ b/hgext/hgk.py @@ -1,12 +1,13 @@ # Minimal support for git commands on an hg repository # -# Copyright 2005 Chris Mason +# Copyright 2005, 2006 Chris Mason # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -import time, sys, signal, os -from mercurial import hg, mdiff, fancyopts, commands, ui, util +from mercurial.demandload import * +demandload(globals(), 'time sys signal os') +demandload(globals(), 'mercurial:hg,mdiff,fancyopts,commands,ui,util') def dodiff(fp, ui, repo, node1, node2, files=None, match=util.always, changes=None, text=False): @@ -14,7 +15,7 @@ def dodiff(fp, ui, repo, node1, node2, f return time.asctime(time.gmtime(c[2][0])) if not changes: - changes = repo.changes(node1, node2, files, match=match) + changes = repo.status(node1, node2, files, match=match)[:5] modified, added, removed, deleted, unknown = changes if files: modified, added, removed = map(lambda x: filterfiles(files, x), @@ -67,12 +68,12 @@ def difftree(ui, repo, node1=None, node2 if node2: change = repo.changelog.read(node2) mmap2 = repo.manifest.read(change[0]) - modified, added, removed, deleted, unknown = repo.changes(node1, node2) + modified, added, removed, deleted, unknown = repo.status(node1, node2)[:5] def read(f): return repo.file(f).read(mmap2[f]) date2 = date(change) else: date2 = time.asctime() - modified, added, removed, deleted, unknown = repo.changes(node1) + modified, added, removed, deleted, unknown = repo.status(node1)[:5] if not node1: node1 = repo.dirstate.parents()[0] def read(f): return file(os.path.join(repo.root, f)).read() @@ -334,6 +335,3 @@ cmdtable = { ('n', 'max-count', 0, 'max-count')], "hg debug-rev-list [options] revs"), } - -def reposetup(ui, repo): - pass diff --git a/hgext/mq.py b/hgext/mq.py --- a/hgext/mq.py +++ b/hgext/mq.py @@ -1,6 +1,6 @@ # queue.py - patch queues for mercurial # -# Copyright 2005 Chris Mason +# Copyright 2005, 2006 Chris Mason # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -30,22 +30,30 @@ refresh contents of top applied patch ''' from mercurial.demandload import * +from mercurial.i18n import gettext as _ demandload(globals(), "os sys re struct traceback errno bz2") -from mercurial.i18n import gettext as _ -from mercurial import ui, hg, revlog, commands, util +demandload(globals(), "mercurial:cmdutil,commands,hg,patch,revlog,ui,util") + +commands.norepo += " qclone qversion" -versionstr = "0.45" +class statusentry: + def __init__(self, rev, name=None): + if not name: + fields = rev.split(':') + if len(fields) == 2: + self.rev, self.name = fields + else: + self.rev, self.name = None, None + else: + self.rev, self.name = rev, name -repomap = {} + def __str__(self): + return self.rev + ':' + self.name -commands.norepo += " qversion" class queue: def __init__(self, ui, path, patchdir=None): self.basepath = path - if patchdir: - self.path = patchdir - else: - self.path = os.path.join(path, "patches") + self.path = patchdir or os.path.join(path, "patches") self.opener = util.opener(self.path) self.ui = ui self.applied = [] @@ -54,13 +62,26 @@ class queue: self.series_dirty = 0 self.series_path = "series" self.status_path = "status" + self.guards_path = "guards" + self.active_guards = None + self.guards_dirty = False + self._diffopts = None - if os.path.exists(os.path.join(self.path, self.series_path)): + if os.path.exists(self.join(self.series_path)): self.full_series = self.opener(self.series_path).read().splitlines() - self.read_series(self.full_series) + self.parse_series() + + if os.path.exists(self.join(self.status_path)): + lines = self.opener(self.status_path).read().splitlines() + self.applied = [statusentry(l) for l in lines] - if os.path.exists(os.path.join(self.path, self.status_path)): - self.applied = self.opener(self.status_path).read().splitlines() + def diffopts(self): + if self._diffopts is None: + self._diffopts = patch.diffopts(self.ui) + return self._diffopts + + def join(self, *p): + return os.path.join(self.path, *p) def find_series(self, patch): pre = re.compile("(\s*)([^#]+)") @@ -75,34 +96,132 @@ class queue: index += 1 return None - def read_series(self, list): - def matcher(list): - pre = re.compile("(\s*)([^#]+)") - for l in list: - m = pre.match(l) - if m: - s = m.group(2) - s = s.rstrip() - if len(s) > 0: - yield s + guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)') + + def parse_series(self): self.series = [] - self.series = [ x for x in matcher(list) ] + self.series_guards = [] + for l in self.full_series: + h = l.find('#') + if h == -1: + patch = l + comment = '' + elif h == 0: + continue + else: + patch = l[:h] + comment = l[h:] + patch = patch.strip() + if patch: + self.series.append(patch) + self.series_guards.append(self.guard_re.findall(comment)) + + def check_guard(self, guard): + bad_chars = '# \t\r\n\f' + first = guard[0] + for c in '-+': + if first == c: + return (_('guard %r starts with invalid character: %r') % + (guard, c)) + for c in bad_chars: + if c in guard: + return _('invalid character in guard %r: %r') % (guard, c) + + def set_active(self, guards): + for guard in guards: + bad = self.check_guard(guard) + if bad: + raise util.Abort(bad) + guards = dict.fromkeys(guards).keys() + guards.sort() + self.ui.debug('active guards: %s\n' % ' '.join(guards)) + self.active_guards = guards + self.guards_dirty = True + + def active(self): + if self.active_guards is None: + self.active_guards = [] + try: + guards = self.opener(self.guards_path).read().split() + except IOError, err: + if err.errno != errno.ENOENT: raise + guards = [] + for i, guard in enumerate(guards): + bad = self.check_guard(guard) + if bad: + self.ui.warn('%s:%d: %s\n' % + (self.join(self.guards_path), i + 1, bad)) + else: + self.active_guards.append(guard) + return self.active_guards + + def set_guards(self, idx, guards): + for g in guards: + if len(g) < 2: + raise util.Abort(_('guard %r too short') % g) + if g[0] not in '-+': + raise util.Abort(_('guard %r starts with invalid char') % g) + bad = self.check_guard(g[1:]) + if bad: + raise util.Abort(bad) + drop = self.guard_re.sub('', self.full_series[idx]) + self.full_series[idx] = drop + ''.join([' #' + g for g in guards]) + self.parse_series() + self.series_dirty = True + + def pushable(self, idx): + if isinstance(idx, str): + idx = self.series.index(idx) + patchguards = self.series_guards[idx] + if not patchguards: + return True, None + default = False + guards = self.active() + exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards] + if exactneg: + return False, exactneg[0] + pos = [g for g in patchguards if g[0] == '+'] + exactpos = [g for g in pos if g[1:] in guards] + if pos: + if exactpos: + return True, exactpos[0] + return False, pos + return True, '' + + def explain_pushable(self, idx, all_patches=False): + write = all_patches and self.ui.write or self.ui.warn + if all_patches or self.ui.verbose: + if isinstance(idx, str): + idx = self.series.index(idx) + pushable, why = self.pushable(idx) + if all_patches and pushable: + if why is None: + write(_('allowing %s - no guards in effect\n') % + self.series[idx]) + else: + if not why: + write(_('allowing %s - no matching negative guards\n') % + self.series[idx]) + else: + write(_('allowing %s - guarded by %r\n') % + (self.series[idx], why)) + if not pushable: + if why: + write(_('skipping %s - guarded by %r\n') % + (self.series[idx], ' '.join(why))) + else: + write(_('skipping %s - no matching guards\n') % + self.series[idx]) def save_dirty(self): - if self.applied_dirty: - if len(self.applied) > 0: - nl = "\n" - else: - nl = "" - f = self.opener(self.status_path, "w") - f.write("\n".join(self.applied) + nl) - if self.series_dirty: - if len(self.full_series) > 0: - nl = "\n" - else: - nl = "" - f = self.opener(self.series_path, "w") - f.write("\n".join(self.full_series) + nl) + def write_list(items, path): + fp = self.opener(path, 'w') + for i in items: + print >> fp, i + fp.close() + if self.applied_dirty: write_list(map(str, self.applied), self.status_path) + if self.series_dirty: write_list(self.full_series, self.series_path) + if self.guards_dirty: write_list(self.active_guards, self.guards_path) def readheaders(self, patch): def eatdiff(lines): @@ -122,7 +241,7 @@ class queue: else: break - pf = os.path.join(self.path, patch) + pf = self.join(patch) message = [] comments = [] user = None @@ -133,6 +252,9 @@ class queue: for line in file(pf): line = line.rstrip() + if line.startswith('diff --git'): + diffstart = 2 + break if diffstart: if line.startswith('+++ '): diffstart = 2 @@ -178,6 +300,13 @@ class queue: message.insert(0, subject) return (message, comments, user, date, diffstart > 1) + def printdiff(self, repo, node1, node2=None, files=None, + fp=None, changes=None, opts={}): + fns, matchfn, anypats = cmdutil.matchpats(repo, files, opts) + + patch.diff(repo, node1, node2, fns, match=matchfn, + fp=fp, changes=changes, opts=self.diffopts()) + def mergeone(self, repo, mergeq, head, patch, rev, wlock): # first try just applying the patch (err, n) = self.apply(repo, [ patch ], update_status=False, @@ -187,35 +316,31 @@ class queue: return (err, n) if n is None: - self.ui.warn("apply failed for patch %s\n" % patch) - sys.exit(1) + raise util.Abort(_("apply failed for patch %s") % patch) self.ui.warn("patch didn't work out, merging %s\n" % patch) # apply failed, strip away that rev and merge. - repo.update(head, allow=False, force=True, wlock=wlock) + hg.clean(repo, head, wlock=wlock) self.strip(repo, n, update=False, backup='strip', wlock=wlock) c = repo.changelog.read(rev) - ret = repo.update(rev, allow=True, wlock=wlock) + ret = hg.merge(repo, rev, wlock=wlock) if ret: - self.ui.warn("update returned %d\n" % ret) - sys.exit(1) + raise util.Abort(_("update returned %d") % ret) n = repo.commit(None, c[4], c[1], force=1, wlock=wlock) if n == None: - self.ui.warn("repo commit failed\n") - sys.exit(1) + raise util.Abort(_("repo commit failed")) try: message, comments, user, date, patchfound = mergeq.readheaders(patch) except: - self.ui.warn("Unable to read %s\n" % patch) - sys.exit(1) + raise util.Abort(_("unable to read %s") % patch) patchf = self.opener(patch, "w") if comments: comments = "\n".join(comments) + '\n\n' patchf.write(comments) - commands.dodiff(patchf, self.ui, repo, head, n) + self.printdiff(repo, head, n, fp=patchf) patchf.close() return (0, n) @@ -226,12 +351,10 @@ class queue: return p1 if len(self.applied) == 0: return None - (top, patch) = self.applied[-1].split(':') - top = revlog.bin(top) - return top + return revlog.bin(self.applied[-1].rev) pp = repo.changelog.parents(rev) if pp[1] != revlog.nullid: - arevs = [ x.split(':')[0] for x in self.applied ] + arevs = [ x.rev for x in self.applied ] p0 = revlog.hex(pp[0]) p1 = revlog.hex(pp[1]) if p0 in arevs: @@ -251,17 +374,20 @@ class queue: pname = ".hg.patches.merge.marker" n = repo.commit(None, '[mq]: merge marker', user=None, force=1, wlock=wlock) - self.applied.append(revlog.hex(n) + ":" + pname) + self.applied.append(statusentry(revlog.hex(n), pname)) self.applied_dirty = 1 head = self.qparents(repo) for patch in series: - patch = mergeq.lookup(patch) + patch = mergeq.lookup(patch, strict=True) if not patch: self.ui.warn("patch %s does not exist\n" % patch) return (1, None) - + pushable, reason = self.pushable(patch) + if not pushable: + self.explain_pushable(patch, all_patches=True) + continue info = mergeq.isapplied(patch) if not info: self.ui.warn("patch %s is not applied\n" % patch) @@ -269,102 +395,80 @@ class queue: rev = revlog.bin(info[1]) (err, head) = self.mergeone(repo, mergeq, head, patch, rev, wlock) if head: - self.applied.append(revlog.hex(head) + ":" + patch) + self.applied.append(statusentry(revlog.hex(head), patch)) self.applied_dirty = 1 if err: return (err, head) return (0, head) + def patch(self, repo, patchfile): + '''Apply patchfile to the working directory. + patchfile: file name of patch''' + try: + (files, fuzz) = patch.patch(patchfile, self.ui, strip=1, + cwd=repo.root) + except Exception, inst: + self.ui.note(str(inst) + '\n') + if not self.ui.verbose: + self.ui.warn("patch failed, unable to continue (try -v)\n") + return (False, [], False) + + return (True, files, fuzz) + def apply(self, repo, series, list=False, update_status=True, strict=False, patchdir=None, merge=None, wlock=None): # TODO unify with commands.py if not patchdir: patchdir = self.path - pwd = os.getcwd() - os.chdir(repo.root) err = 0 if not wlock: wlock = repo.wlock() lock = repo.lock() tr = repo.transaction() n = None - for patch in series: - self.ui.warn("applying %s\n" % patch) - pf = os.path.join(patchdir, patch) + for patchname in series: + pushable, reason = self.pushable(patchname) + if not pushable: + self.explain_pushable(patchname, all_patches=True) + continue + self.ui.warn("applying %s\n" % patchname) + pf = os.path.join(patchdir, patchname) try: - message, comments, user, date, patchfound = self.readheaders(patch) + message, comments, user, date, patchfound = self.readheaders(patchname) except: - self.ui.warn("Unable to read %s\n" % pf) + self.ui.warn("Unable to read %s\n" % patchname) err = 1 break if not message: - message = "imported patch %s\n" % patch + message = "imported patch %s\n" % patchname else: if list: - message.append("\nimported patch %s" % patch) + message.append("\nimported patch %s" % patchname) message = '\n'.join(message) - try: - pp = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch') - f = os.popen("%s -p1 --no-backup-if-mismatch < '%s'" % (pp, pf)) - except: - self.ui.warn("patch failed, unable to continue (try -v)\n") - err = 1 - break - files = [] - fuzz = False - for l in f: - l = l.rstrip('\r\n'); - if self.ui.verbose: - self.ui.warn(l + "\n") - if l[:14] == 'patching file ': - pf = os.path.normpath(l[14:]) - # when patch finds a space in the file name, it puts - # single quotes around the filename. strip them off - if pf[0] == "'" and pf[-1] == "'": - pf = pf[1:-1] - if pf not in files: - files.append(pf) - printed_file = False - file_str = l - elif l.find('with fuzz') >= 0: - if not printed_file: - self.ui.warn(file_str + '\n') - printed_file = True - self.ui.warn(l + '\n') - fuzz = True - elif l.find('saving rejects to file') >= 0: - self.ui.warn(l + '\n') - elif l.find('FAILED') >= 0: - if not printed_file: - self.ui.warn(file_str + '\n') - printed_file = True - self.ui.warn(l + '\n') - patcherr = f.close() + (patcherr, files, fuzz) = self.patch(repo, pf) + patcherr = not patcherr - if merge and len(files) > 0: + if merge and files: # Mark as merged and update dirstate parent info - repo.dirstate.update(repo.dirstate.filterfiles(files), 'm') + repo.dirstate.update(repo.dirstate.filterfiles(files.keys()), 'm') p1, p2 = repo.dirstate.parents() repo.dirstate.setparents(p1, merge) - if len(files) > 0: - commands.addremove_lock(self.ui, repo, files, - opts={}, wlock=wlock) + files = patch.updatedir(self.ui, repo, files, wlock=wlock) n = repo.commit(files, message, user, date, force=1, lock=lock, wlock=wlock) if n == None: - self.ui.warn("repo commit failed\n") - sys.exit(1) + raise util.Abort(_("repo commit failed")) if update_status: - self.applied.append(revlog.hex(n) + ":" + patch) + self.applied.append(statusentry(revlog.hex(n), patchname)) if patcherr: if not patchfound: - self.ui.warn("patch %s is empty\n" % patch) + self.ui.warn("patch %s is empty\n" % patchname) err = 0 else: self.ui.warn("patch failed, rejects left in working dir\n") @@ -376,49 +480,58 @@ class queue: err = 1 break tr.close() - os.chdir(pwd) return (err, n) - def delete(self, repo, patch): - patch = self.lookup(patch) - info = self.isapplied(patch) - if info: - self.ui.warn("cannot delete applied patch %s\n" % patch) - sys.exit(1) - if patch not in self.series: - self.ui.warn("patch %s not in series file\n" % patch) - sys.exit(1) - i = self.find_series(patch) - del self.full_series[i] - self.read_series(self.full_series) + def delete(self, repo, patches, keep=False): + realpatches = [] + for patch in patches: + patch = self.lookup(patch, strict=True) + info = self.isapplied(patch) + if info: + raise util.Abort(_("cannot delete applied patch %s") % patch) + if patch not in self.series: + raise util.Abort(_("patch %s not in series file") % patch) + realpatches.append(patch) + + if not keep: + r = self.qrepo() + if r: + r.remove(realpatches, True) + else: + os.unlink(self.join(patch)) + + indices = [self.find_series(p) for p in realpatches] + indices.sort() + for i in indices[-1::-1]: + del self.full_series[i] + self.parse_series() self.series_dirty = 1 def check_toppatch(self, repo): if len(self.applied) > 0: - (top, patch) = self.applied[-1].split(':') - top = revlog.bin(top) + top = revlog.bin(self.applied[-1].rev) pp = repo.dirstate.parents() if top not in pp: - self.ui.warn("queue top not at dirstate parents. top %s dirstate %s %s\n" %( revlog.short(top), revlog.short(pp[0]), revlog.short(pp[1]))) - sys.exit(1) + raise util.Abort(_("queue top not at same revision as working directory")) return top return None - def check_localchanges(self, repo): - (c, a, r, d, u) = repo.changes(None, None) - if c or a or d or r: - self.ui.write("Local changes found, refresh first\n") - sys.exit(1) + def check_localchanges(self, repo, force=False, refresh=True): + m, a, r, d = repo.status()[:4] + if m or a or r or d: + if not force: + if refresh: + raise util.Abort(_("local changes found, refresh first")) + else: + raise util.Abort(_("local changes found")) + return m, a, r, d def new(self, repo, patch, msg=None, force=None): - commitfiles = [] - (c, a, r, d, u) = repo.changes(None, None) - if c or a or d or r: - if not force: - raise util.Abort(_("Local changes found, refresh first")) - else: - commitfiles = c + a + r + if os.path.exists(self.join(patch)): + raise util.Abort(_('patch "%s" already exists') % patch) + m, a, r, d = self.check_localchanges(repo, force) + commitfiles = m + a + r self.check_toppatch(repo) wlock = repo.wlock() - insert = self.series_end() + insert = self.full_series_end() if msg: n = repo.commit(commitfiles, "[mq]: %s" % msg, force=True, wlock=wlock) @@ -426,11 +539,10 @@ class queue: n = repo.commit(commitfiles, "New patch: %s" % patch, force=True, wlock=wlock) if n == None: - self.ui.warn("repo commit failed\n") - sys.exit(1) + raise util.Abort(_("repo commit failed")) self.full_series[insert:insert] = [patch] - self.applied.append(revlog.hex(n) + ":" + patch) - self.read_series(self.full_series) + self.applied.append(statusentry(revlog.hex(n), patch)) + self.parse_series() self.series_dirty = 1 self.applied_dirty = 1 p = self.opener(patch, "w") @@ -509,9 +621,9 @@ class queue: # we go in two steps here so the strip loop happens in a # sensible order. When stripping many files, this helps keep # our disk access patterns under control. - list = seen.keys() - list.sort() - for f in list: + seen_list = seen.keys() + seen_list.sort() + for f in seen_list: ff = repo.file(f) filerev = seen[f] if filerev != 0: @@ -530,8 +642,9 @@ class queue: revnum = chlog.rev(rev) if update: + self.check_localchanges(repo, refresh=False) urev = self.qparents(repo, rev) - repo.update(urev, allow=False, force=True, wlock=wlock) + hg.clean(repo, urev, wlock=wlock) repo.dirstate.write() # save is a list of all the branches we are truncating away @@ -540,7 +653,6 @@ class queue: saveheads = [] savebases = {} - tip = chlog.tip() heads = limitheads(chlog, rev) seen = {} @@ -571,7 +683,7 @@ class queue: savebases[x] = 1 # create a changegroup for all the branches we need to keep - if backup is "all": + if backup == "all": backupch = repo.changegroupsubset([rev], chlog.heads(), 'strip') bundle(backupch) if saveheads: @@ -586,37 +698,89 @@ class queue: if saveheads: self.ui.status("adding branch\n") commands.unbundle(self.ui, repo, chgrpfile, update=False) - if backup is not "strip": + if backup != "strip": os.unlink(chgrpfile) def isapplied(self, patch): """returns (index, rev, patch)""" for i in xrange(len(self.applied)): - p = self.applied[i] - a = p.split(':') - if a[1] == patch: - return (i, a[0], a[1]) + a = self.applied[i] + if a.name == patch: + return (i, a.rev, a.name) return None - def lookup(self, patch): + # if the exact patch name does not exist, we try a few + # variations. If strict is passed, we try only #1 + # + # 1) a number to indicate an offset in the series file + # 2) a unique substring of the patch name was given + # 3) patchname[-+]num to indicate an offset in the series file + def lookup(self, patch, strict=False): + patch = patch and str(patch) + + def partial_name(s): + if s in self.series: + return s + matches = [x for x in self.series if s in x] + if len(matches) > 1: + self.ui.warn(_('patch name "%s" is ambiguous:\n') % s) + for m in matches: + self.ui.warn(' %s\n' % m) + return None + if matches: + return matches[0] + if len(self.series) > 0 and len(self.applied) > 0: + if s == 'qtip': + return self.series[self.series_end()-1] + if s == 'qbase': + return self.series[0] + return None if patch == None: return None - if patch in self.series: - return patch - if not os.path.isfile(os.path.join(self.path, patch)): + + # we don't want to return a partial match until we make + # sure the file name passed in does not exist (checked below) + res = partial_name(patch) + if res and res == patch: + return res + + if not os.path.isfile(self.join(patch)): try: sno = int(patch) except(ValueError, OverflowError): - self.ui.warn("patch %s not in series\n" % patch) - sys.exit(1) - if sno >= len(self.series): - self.ui.warn("patch number %d is out of range\n" % sno) - sys.exit(1) - patch = self.series[sno] - else: - self.ui.warn("patch %s not in series\n" % patch) - sys.exit(1) - return patch + pass + else: + if sno < len(self.series): + return self.series[sno] + if not strict: + # return any partial match made above + if res: + return res + minus = patch.rsplit('-', 1) + if len(minus) > 1: + res = partial_name(minus[0]) + if res: + i = self.series.index(res) + try: + off = int(minus[1] or 1) + except(ValueError, OverflowError): + pass + else: + if i - off >= 0: + return self.series[i - off] + plus = patch.rsplit('+', 1) + if len(plus) > 1: + res = partial_name(plus[0]) + if res: + i = self.series.index(res) + try: + off = int(plus[1] or 1) + except(ValueError, OverflowError): + pass + else: + if i + off < len(self.series): + return self.series[i + off] + raise util.Abort(_("patch %s not in series") % patch) def push(self, repo, patch=None, force=False, list=False, mergeq=None, wlock=None): @@ -624,10 +788,10 @@ class queue: wlock = repo.wlock() patch = self.lookup(patch) if patch and self.isapplied(patch): - self.ui.warn("patch %s is already applied\n" % patch) + self.ui.warn(_("patch %s is already applied\n") % patch) sys.exit(1) if self.series_end() == len(self.series): - self.ui.warn("File series fully applied\n") + self.ui.warn(_("patch series fully applied\n")) sys.exit(1) if not force: self.check_localchanges(repo) @@ -646,7 +810,7 @@ class queue: ret = self.mergepatch(repo, mergeq, s, wlock) else: ret = self.apply(repo, s, list, wlock=wlock) - top = self.applied[-1].split(':')[1] + top = self.applied[-1].name if ret[0]: self.ui.write("Errors during apply, please fix and refresh %s\n" % top) @@ -654,7 +818,8 @@ class queue: self.ui.write("Now at: %s\n" % top) return ret[0] - def pop(self, repo, patch=None, force=False, update=True, wlock=None): + def pop(self, repo, patch=None, force=False, update=True, all=False, + wlock=None): def getfile(f, rev): t = repo.file(f).read(rev) try: @@ -675,15 +840,14 @@ class queue: patch = self.lookup(patch) info = self.isapplied(patch) if not info: - self.ui.warn("patch %s is not applied\n" % patch) - sys.exit(1) + raise util.Abort(_("patch %s is not applied") % patch) if len(self.applied) == 0: - self.ui.warn("No patches applied\n") + self.ui.warn(_("no patches applied\n")) sys.exit(1) if not update: parents = repo.dirstate.parents() - rr = [ revlog.bin(x.split(':')[0]) for x in self.applied ] + rr = [ revlog.bin(x.rev) for x in self.applied ] for p in parents: if p in rr: self.ui.warn("qpop: forcing dirstate update\n") @@ -695,7 +859,17 @@ class queue: self.applied_dirty = 1; end = len(self.applied) if not patch: - info = [len(self.applied) - 1] + self.applied[-1].split(':') + if all: + popi = 0 + else: + popi = len(self.applied) - 1 + else: + popi = info[0] + 1 + if popi >= end: + self.ui.warn("qpop: %s is already at the top\n" % patch) + return + info = [ popi ] + [self.applied[popi].rev, self.applied[popi].name] + start = info[0] rev = revlog.bin(info[1]) @@ -705,17 +879,16 @@ class queue: top = self.check_toppatch(repo) qp = self.qparents(repo, rev) changes = repo.changelog.read(qp) - mf1 = repo.manifest.readflags(changes[0]) mmap = repo.manifest.read(changes[0]) - (c, a, r, d, u) = repo.changes(qp, top) + m, a, r, d, u = repo.status(qp, top)[:5] if d: raise util.Abort("deletions found between repo revs") - for f in c: + for f in m: getfile(f, mmap[f]) for f in r: getfile(f, mmap[f]) - util.set_exec(repo.wjoin(f), mf1[f]) - repo.dirstate.update(c + r, 'n') + util.set_exec(repo.wjoin(f), mmap.execf(f)) + repo.dirstate.update(m + r, 'n') for f in a: try: os.unlink(repo.wjoin(f)) except: raise @@ -727,36 +900,46 @@ class queue: self.strip(repo, rev, update=False, backup='strip', wlock=wlock) del self.applied[start:end] if len(self.applied): - self.ui.write("Now at: %s\n" % self.applied[-1].split(':')[1]) + self.ui.write("Now at: %s\n" % self.applied[-1].name) else: self.ui.write("Patch queue now empty\n") - def diff(self, repo, files): + def diff(self, repo, pats, opts): top = self.check_toppatch(repo) if not top: self.ui.write("No patches applied\n") return qp = self.qparents(repo, top) - commands.dodiff(sys.stdout, self.ui, repo, qp, None, files) + self.printdiff(repo, qp, files=pats, opts=opts) - def refresh(self, repo, short=False): + def refresh(self, repo, pats=None, **opts): if len(self.applied) == 0: self.ui.write("No patches applied\n") return wlock = repo.wlock() self.check_toppatch(repo) - qp = self.qparents(repo) - (top, patch) = self.applied[-1].split(':') + (top, patch) = (self.applied[-1].rev, self.applied[-1].name) top = revlog.bin(top) cparents = repo.changelog.parents(top) patchparent = self.qparents(repo, top) message, comments, user, date, patchfound = self.readheaders(patch) patchf = self.opener(patch, "w") + msg = opts.get('msg', '').rstrip() + if msg: + if comments: + # Remove existing message. + ci = 0 + for mi in range(len(message)): + while message[mi] != comments[ci]: + ci += 1 + del comments[ci] + comments.append(msg) if comments: comments = "\n".join(comments) + '\n\n' patchf.write(comments) + fns, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) tip = repo.changelog.tip() if top == tip: # if the top of our patch queue is also the tip, there is an @@ -769,30 +952,30 @@ class queue: # patch already # # this should really read: - #(cc, dd, aa, aa2, uu) = repo.changes(tip, patchparent) + # mm, dd, aa, aa2, uu = repo.status(tip, patchparent)[:5] # but we do it backwards to take advantage of manifest/chlog - # caching against the next repo.changes call + # caching against the next repo.status call # - (cc, aa, dd, aa2, uu) = repo.changes(patchparent, tip) - if short: - filelist = cc + aa + dd + mm, aa, dd, aa2, uu = repo.status(patchparent, tip)[:5] + if opts.get('short'): + filelist = mm + aa + dd else: filelist = None - (c, a, r, d, u) = repo.changes(None, None, filelist) + m, a, r, d, u = repo.status(files=filelist)[:5] # we might end up with files that were added between tip and # the dirstate parent, but then changed in the local dirstate. # in this case, we want them to only show up in the added section - for x in c: + for x in m: if x not in aa: - cc.append(x) + mm.append(x) # we might end up with files added by the local dirstate that # were deleted by the patch. In this case, they should only # show up in the changed section. for x in a: if x in dd: del dd[dd.index(x)] - cc.append(x) + mm.append(x) else: aa.append(x) # make sure any files deleted in the local dirstate @@ -803,70 +986,97 @@ class queue: del aa[aa.index(x)] forget.append(x) continue - elif x in cc: - del cc[cc.index(x)] + elif x in mm: + del mm[mm.index(x)] dd.append(x) - c = list(util.unique(cc)) + m = list(util.unique(mm)) r = list(util.unique(dd)) a = list(util.unique(aa)) - filelist = list(util.unique(c + r + a )) - commands.dodiff(patchf, self.ui, repo, patchparent, None, - filelist, changes=(c, a, r, [], u)) + filelist = filter(matchfn, util.unique(m + r + a)) + self.printdiff(repo, patchparent, files=filelist, + changes=(m, a, r, [], u), fp=patchf) patchf.close() changes = repo.changelog.read(tip) repo.dirstate.setparents(*cparents) + copies = [(f, repo.dirstate.copied(f)) for f in a] repo.dirstate.update(a, 'a') + for dst, src in copies: + repo.dirstate.copy(src, dst) repo.dirstate.update(r, 'r') - repo.dirstate.update(c, 'n') + # if the patch excludes a modified file, mark that file with mtime=0 + # so status can see it. + mm = [] + for i in range(len(m)-1, -1, -1): + if not matchfn(m[i]): + mm.append(m[i]) + del m[i] + repo.dirstate.update(m, 'n') + repo.dirstate.update(mm, 'n', st_mtime=0) repo.dirstate.forget(forget) - if not message: - message = "patch queue: %s\n" % patch + if not msg: + if not message: + message = "patch queue: %s\n" % patch + else: + message = "\n".join(message) else: - message = "\n".join(message) + message = msg + self.strip(repo, top, update=False, backup='strip', wlock=wlock) n = repo.commit(filelist, message, changes[1], force=1, wlock=wlock) - self.applied[-1] = revlog.hex(n) + ':' + patch + self.applied[-1] = statusentry(revlog.hex(n), patch) self.applied_dirty = 1 else: - commands.dodiff(patchf, self.ui, repo, patchparent, None) + self.printdiff(repo, patchparent, fp=patchf) patchf.close() self.pop(repo, force=True, wlock=wlock) self.push(repo, force=True, wlock=wlock) def init(self, repo, create=False): if os.path.isdir(self.path): - raise util.Abort("patch queue directory already exists") + raise util.Abort(_("patch queue directory already exists")) os.mkdir(self.path) if create: return self.qrepo(create=True) def unapplied(self, repo, patch=None): if patch and patch not in self.series: - self.ui.warn("%s not in the series file\n" % patch) - sys.exit(1) + raise util.Abort(_("patch %s is not in series file") % patch) if not patch: start = self.series_end() else: start = self.series.index(patch) + 1 - for p in self.series[start:]: - self.ui.write("%s\n" % p) + unapplied = [] + for i in xrange(start, len(self.series)): + pushable, reason = self.pushable(i) + if pushable: + unapplied.append((i, self.series[i])) + self.explain_pushable(i) + return unapplied - def qseries(self, repo, missing=None): - start = self.series_end() + def qseries(self, repo, missing=None, summary=False): + start = self.series_end(all_patches=True) if not missing: - for p in self.series[:start]: + for i in range(len(self.series)): + patch = self.series[i] if self.ui.verbose: - self.ui.write("%d A " % self.series.index(p)) - self.ui.write("%s\n" % p) - for p in self.series[start:]: - if self.ui.verbose: - self.ui.write("%d U " % self.series.index(p)) - self.ui.write("%s\n" % p) + if i < start: + status = 'A' + elif self.pushable(i)[0]: + status = 'U' + else: + status = 'G' + self.ui.write('%d %s ' % (i, status)) + if summary: + msg = self.readheaders(patch)[0] + msg = msg and ': ' + msg[0] or ': ' + else: + msg = '' + self.ui.write('%s%s\n' % (patch, msg)) else: - list = [] + msng_list = [] for root, dirs, files in os.walk(self.path): d = root[len(self.path) + 1:] for f in files: @@ -874,21 +1084,19 @@ class queue: if (fl not in self.series and fl not in (self.status_path, self.series_path) and not fl.startswith('.')): - list.append(fl) - list.sort() - if list: - for x in list: - if self.ui.verbose: - self.ui.write("D ") - self.ui.write("%s\n" % x) + msng_list.append(fl) + msng_list.sort() + for x in msng_list: + if self.ui.verbose: + self.ui.write("D ") + self.ui.write("%s\n" % x) def issaveline(self, l): - name = l.split(':')[1] - if name == '.hg.patches.save.line': + if l.name == '.hg.patches.save.line': return True def qrepo(self, create=False): - if create or os.path.isdir(os.path.join(self.path, ".hg")): + if create or os.path.isdir(self.join(".hg")): return hg.repository(self.ui, path=self.path, create=create) def restore(self, repo, rev, delete=None, qupdate=None): @@ -909,19 +1117,18 @@ class queue: qpp = [ hg.bin(x) for x in l ] elif datastart != None: l = lines[i].rstrip() - index = l.index(':') - id = l[:index] - file = l[index + 1:] - if id: - applied.append(l) - series.append(file) + se = statusentry(l) + file_ = se.name + if se.rev: + applied.append(se) + series.append(file_) if datastart == None: self.ui.warn("No saved patch data found\n") return 1 self.ui.warn("restoring status: %s\n" % lines[0]) self.full_series = series self.applied = applied - self.read_series(self.full_series) + self.parse_series() self.series_dirty = 1 self.applied_dirty = 1 heads = repo.changelog.heads() @@ -945,7 +1152,7 @@ class queue: if not r: self.ui.warn("Unable to load queue repository\n") return 1 - r.update(qpp[0], allow=False, force=True) + hg.clean(r, qpp[0]) def save(self, repo, msg=None): if len(self.applied) == 0: @@ -965,30 +1172,49 @@ class queue: pp = r.dirstate.parents() msg += "\nDirstate: %s %s" % (hg.hex(pp[0]), hg.hex(pp[1])) msg += "\n\nPatch Data:\n" - text = msg + "\n".join(self.applied) + '\n' + (ar and "\n".join(ar) - + '\n' or "") + text = msg + "\n".join([str(x) for x in self.applied]) + '\n' + (ar and + "\n".join(ar) + '\n' or "") n = repo.commit(None, text, user=None, force=1) if not n: self.ui.warn("repo commit failed\n") return 1 - self.applied.append(revlog.hex(n) + ":" + '.hg.patches.save.line') + self.applied.append(statusentry(revlog.hex(n),'.hg.patches.save.line')) self.applied_dirty = 1 - def series_end(self): + def full_series_end(self): + if len(self.applied) > 0: + p = self.applied[-1].name + end = self.find_series(p) + if end == None: + return len(self.full_series) + return end + 1 + return 0 + + def series_end(self, all_patches=False): end = 0 + def next(start): + if all_patches: + return start + i = start + while i < len(self.series): + p, reason = self.pushable(i) + if p: + break + self.explain_pushable(i) + i += 1 + return i if len(self.applied) > 0: - (top, p) = self.applied[-1].split(':') + p = self.applied[-1].name try: end = self.series.index(p) except ValueError: return 0 - return end + 1 - return end + return next(end + 1) + return next(end) def qapplied(self, repo, patch=None): if patch and patch not in self.series: - self.ui.warn("%s not in the series file\n" % patch) - sys.exit(1) + raise util.Abort(_("patch %s is not in series file") % patch) if not patch: end = len(self.applied) else: @@ -998,9 +1224,11 @@ class queue: self.ui.write("%s\n" % p) def appliedname(self, index): - p = self.applied[index] + pname = self.applied[index].name if not self.ui.verbose: - p = p.split(':')[1] + p = pname + else: + p = str(self.series.index(pname)) + " " + pname return p def top(self, repo): @@ -1015,7 +1243,10 @@ class queue: if end == len(self.series): self.ui.write("All patches applied\n") else: - self.ui.write(self.series[end] + '\n') + p = self.series[end] + if self.ui.verbose: + self.ui.write("%d " % self.series.index(p)) + self.ui.write(p + '\n') def prev(self, repo): if len(self.applied) > 1: @@ -1028,36 +1259,33 @@ class queue: def qimport(self, repo, files, patch=None, existing=None, force=None): if len(files) > 1 and patch: - self.ui.warn("-n option not valid when importing multiple files\n") - sys.exit(1) + raise util.Abort(_('option "-n" not valid when importing multiple ' + 'files')) i = 0 added = [] for filename in files: if existing: if not patch: patch = filename - if not os.path.isfile(os.path.join(self.path, patch)): - self.ui.warn("patch %s does not exist\n" % patch) - sys.exit(1) + if not os.path.isfile(self.join(patch)): + raise util.Abort(_("patch %s does not exist") % patch) else: try: text = file(filename).read() except IOError: - self.ui.warn("Unable to read %s\n" % patch) - sys.exit(1) + raise util.Abort(_("unable to read %s") % patch) if not patch: patch = os.path.split(filename)[1] - if not force and os.path.isfile(os.path.join(self.path, patch)): - self.ui.warn("patch %s already exists\n" % patch) - sys.exit(1) + if not force and os.path.exists(self.join(patch)): + raise util.Abort(_('patch "%s" already exists') % patch) patchf = self.opener(patch, "w") patchf.write(text) if patch in self.series: - self.ui.warn("patch %s is already in the series file\n" % patch) - sys.exit(1) - index = self.series_end() + i + raise util.Abort(_('patch %s is already in the series file') + % patch) + index = self.full_series_end() + i self.full_series[index:index] = [patch] - self.read_series(self.full_series) + self.parse_series() self.ui.warn("adding %s to series file\n" % patch) i += 1 added.append(patch) @@ -1067,34 +1295,44 @@ class queue: if qrepo: qrepo.add(added) -def delete(ui, repo, patch, **opts): - """remove a patch from the series file""" - q = repomap[repo] - q.delete(repo, patch) +def delete(ui, repo, patch, *patches, **opts): + """remove patches from queue + + The patches must not be applied. + With -k, the patch files are preserved in the patch directory.""" + q = repo.mq + q.delete(repo, (patch,) + patches, keep=opts.get('keep')) q.save_dirty() return 0 def applied(ui, repo, patch=None, **opts): """print the patches already applied""" - repomap[repo].qapplied(repo, patch) + repo.mq.qapplied(repo, patch) return 0 def unapplied(ui, repo, patch=None, **opts): """print the patches not yet applied""" - repomap[repo].unapplied(repo, patch) - return 0 + for i, p in repo.mq.unapplied(repo, patch): + if ui.verbose: + ui.write("%d " % i) + ui.write("%s\n" % p) def qimport(ui, repo, *filename, **opts): """import a patch""" - q = repomap[repo] + q = repo.mq q.qimport(repo, filename, patch=opts['name'], existing=opts['existing'], force=opts['force']) q.save_dirty() return 0 def init(ui, repo, **opts): - """init a new queue repository""" - q = repomap[repo] + """init a new queue repository + + The queue repository is unversioned by default. If -c is + specified, qinit will create a separate nested repository + for patches. Use qcommit to commit changes to this queue + repository.""" + q = repo.mq r = q.init(repo, create=opts['create_repo']) q.save_dirty() if r: @@ -1106,68 +1344,254 @@ def init(ui, repo, **opts): r.add(['.hgignore', 'series']) return 0 +def clone(ui, source, dest=None, **opts): + '''clone main and patch repository at same time + + If source is local, destination will have no patches applied. If + source is remote, this command can not check if patches are + applied in source, so cannot guarantee that patches are not + applied in destination. If you clone remote repository, be sure + before that it has no patches applied. + + Source patch repository is looked for in /.hg/patches by + default. Use -p to change. + ''' + commands.setremoteconfig(ui, opts) + if dest is None: + dest = hg.defaultdest(source) + sr = hg.repository(ui, ui.expandpath(source)) + qbase, destrev = None, None + if sr.local(): + reposetup(ui, sr) + if sr.mq.applied: + qbase = revlog.bin(sr.mq.applied[0].rev) + if not hg.islocal(dest): + destrev = sr.parents(qbase)[0] + ui.note(_('cloning main repo\n')) + sr, dr = hg.clone(ui, sr, dest, + pull=opts['pull'], + rev=destrev, + update=False, + stream=opts['uncompressed']) + ui.note(_('cloning patch repo\n')) + spr, dpr = hg.clone(ui, opts['patches'] or (sr.url() + '/.hg/patches'), + dr.url() + '/.hg/patches', + pull=opts['pull'], + update=not opts['noupdate'], + stream=opts['uncompressed']) + if dr.local(): + if qbase: + ui.note(_('stripping applied patches from destination repo\n')) + reposetup(ui, dr) + dr.mq.strip(dr, qbase, update=False, backup=None) + if not opts['noupdate']: + ui.note(_('updating destination repo\n')) + hg.update(dr, dr.changelog.tip()) + def commit(ui, repo, *pats, **opts): """commit changes in the queue repository""" - q = repomap[repo] + q = repo.mq r = q.qrepo() if not r: raise util.Abort('no queue repository') commands.commit(r.ui, r, *pats, **opts) def series(ui, repo, **opts): """print the entire series file""" - repomap[repo].qseries(repo, missing=opts['missing']) + repo.mq.qseries(repo, missing=opts['missing'], summary=opts['summary']) return 0 def top(ui, repo, **opts): """print the name of the current patch""" - repomap[repo].top(repo) + repo.mq.top(repo) return 0 def next(ui, repo, **opts): """print the name of the next patch""" - repomap[repo].next(repo) + repo.mq.next(repo) return 0 def prev(ui, repo, **opts): """print the name of the previous patch""" - repomap[repo].prev(repo) + repo.mq.prev(repo) return 0 def new(ui, repo, patch, **opts): - """create a new patch""" - q = repomap[repo] - q.new(repo, patch, msg=opts['message'], force=opts['force']) + """create a new patch + + qnew creates a new patch on top of the currently-applied patch + (if any). It will refuse to run if there are any outstanding + changes unless -f is specified, in which case the patch will + be initialised with them. + + -e, -m or -l set the patch header as well as the commit message. + If none is specified, the patch header is empty and the + commit message is 'New patch: PATCH'""" + q = repo.mq + message = commands.logmessage(opts) + if opts['edit']: + message = ui.edit(message, ui.username()) + q.new(repo, patch, msg=message, force=opts['force']) + q.save_dirty() + return 0 + +def refresh(ui, repo, *pats, **opts): + """update the current patch + + If any file patterns are provided, the refreshed patch will contain only + the modifications that match those patterns; the remaining modifications + will remain in the working directory. + """ + q = repo.mq + message = commands.logmessage(opts) + if opts['edit']: + if message: + raise util.Abort(_('option "-e" incompatible with "-m" or "-l"')) + patch = q.applied[-1].name + (message, comment, user, date, hasdiff) = q.readheaders(patch) + message = ui.edit('\n'.join(message), user or ui.username()) + q.refresh(repo, pats, msg=message, **opts) q.save_dirty() return 0 -def refresh(ui, repo, **opts): - """update the current patch""" - q = repomap[repo] - q.refresh(repo, short=opts['short']) - q.save_dirty() +def diff(ui, repo, *pats, **opts): + """diff of the current patch""" + repo.mq.diff(repo, pats, opts) return 0 -def diff(ui, repo, *files, **opts): - """diff of the current patch""" - # deep in the dirstate code, the walkhelper method wants a list, not a tuple - repomap[repo].diff(repo, list(files)) - return 0 +def fold(ui, repo, *files, **opts): + """fold the named patches into the current patch + + Patches must not yet be applied. Each patch will be successively + applied to the current patch in the order given. If all the + patches apply successfully, the current patch will be refreshed + with the new cumulative patch, and the folded patches will + be deleted. With -k/--keep, the folded patch files will not + be removed afterwards. + + The header for each folded patch will be concatenated with + the current patch header, separated by a line of '* * *'.""" + + q = repo.mq + + if not files: + raise util.Abort(_('qfold requires at least one patch name')) + if not q.check_toppatch(repo): + raise util.Abort(_('No patches applied\n')) + + message = commands.logmessage(opts) + if opts['edit']: + if message: + raise util.Abort(_('option "-e" incompatible with "-m" or "-l"')) + + parent = q.lookup('qtip') + patches = [] + messages = [] + for f in files: + p = q.lookup(f) + if p in patches or p == parent: + ui.warn(_('Skipping already folded patch %s') % p) + if q.isapplied(p): + raise util.Abort(_('qfold cannot fold already applied patch %s') % p) + patches.append(p) + + for p in patches: + if not message: + messages.append(q.readheaders(p)[0]) + pf = q.join(p) + (patchsuccess, files, fuzz) = q.patch(repo, pf) + if not patchsuccess: + raise util.Abort(_('Error folding patch %s') % p) + patch.updatedir(ui, repo, files) + + if not message: + message, comments, user = q.readheaders(parent)[0:3] + for msg in messages: + message.append('* * *') + message.extend(msg) + message = '\n'.join(message) + + if opts['edit']: + message = ui.edit(message, user or ui.username()) + + q.refresh(repo, msg=message) + q.delete(repo, patches, keep=opts['keep']) + q.save_dirty() + +def guard(ui, repo, *args, **opts): + '''set or print guards for a patch + + Guards control whether a patch can be pushed. A patch with no + guards is always pushed. A patch with a positive guard ("+foo") is + pushed only if the qselect command has activated it. A patch with + a negative guard ("-foo") is never pushed if the qselect command + has activated it. + + With no arguments, print the currently active guards. + With arguments, set guards for the named patch. + + To set a negative guard "-foo" on topmost patch ("--" is needed so + hg will not interpret "-foo" as an option): + hg qguard -- -foo + + To set guards on another patch: + hg qguard other.patch +2.6.17 -stable + ''' + def status(idx): + guards = q.series_guards[idx] or ['unguarded'] + ui.write('%s: %s\n' % (q.series[idx], ' '.join(guards))) + q = repo.mq + patch = None + args = list(args) + if opts['list']: + if args or opts['none']: + raise util.Abort(_('cannot mix -l/--list with options or arguments')) + for i in xrange(len(q.series)): + status(i) + return + if not args or args[0][0:1] in '-+': + if not q.applied: + raise util.Abort(_('no patches applied')) + patch = q.applied[-1].name + if patch is None and args[0][0:1] not in '-+': + patch = args.pop(0) + if patch is None: + raise util.Abort(_('no patch to work with')) + if args or opts['none']: + q.set_guards(q.find_series(patch), args) + q.save_dirty() + else: + status(q.series.index(q.lookup(patch))) + +def header(ui, repo, patch=None): + """Print the header of the topmost or specified patch""" + q = repo.mq + + if patch: + patch = q.lookup(patch) + else: + if not q.applied: + ui.write('No patches applied\n') + return + patch = q.lookup('qtip') + message = repo.mq.readheaders(patch)[0] + + ui.write('\n'.join(message) + '\n') def lastsavename(path): - (dir, base) = os.path.split(path) - names = os.listdir(dir) + (directory, base) = os.path.split(path) + names = os.listdir(directory) namere = re.compile("%s.([0-9]+)" % base) - max = None + maxindex = None maxname = None for f in names: m = namere.match(f) if m: index = int(m.group(1)) - if max == None or index > max: - max = index + if maxindex == None or index > maxindex: + maxindex = index maxname = f if maxname: - return (os.path.join(dir, maxname), max) + return (os.path.join(directory, maxname), maxindex) return (None, None) def savename(path): @@ -1179,7 +1603,7 @@ def savename(path): def push(ui, repo, patch=None, **opts): """push the next patch onto the stack""" - q = repomap[repo] + q = repo.mq mergeq = None if opts['all']: @@ -1207,17 +1631,65 @@ def pop(ui, repo, patch=None, **opts): ui.warn('using patch queue: %s\n' % q.path) localupdate = False else: - q = repomap[repo] - if opts['all'] and len(q.applied) > 0: - patch = q.applied[0].split(':')[1] - q.pop(repo, patch, force=opts['force'], update=localupdate) + q = repo.mq + q.pop(repo, patch, force=opts['force'], update=localupdate, all=opts['all']) q.save_dirty() return 0 +def rename(ui, repo, patch, name=None, **opts): + """rename a patch + + With one argument, renames the current patch to PATCH1. + With two arguments, renames PATCH1 to PATCH2.""" + + q = repo.mq + + if not name: + name = patch + patch = None + + if name in q.series: + raise util.Abort(_('A patch named %s already exists in the series file') % name) + + absdest = q.join(name) + if os.path.exists(absdest): + raise util.Abort(_('%s already exists') % absdest) + + if patch: + patch = q.lookup(patch) + else: + if not q.applied: + ui.write(_('No patches applied\n')) + return + patch = q.lookup('qtip') + + if ui.verbose: + ui.write('Renaming %s to %s\n' % (patch, name)) + i = q.find_series(patch) + q.full_series[i] = name + q.parse_series() + q.series_dirty = 1 + + info = q.isapplied(patch) + if info: + q.applied[info[0]] = statusentry(info[1], name) + q.applied_dirty = 1 + + util.rename(q.join(patch), absdest) + r = q.qrepo() + if r: + wlock = r.wlock() + if r.dirstate.state(name) == 'r': + r.undelete([name], wlock) + r.copy(patch, name, wlock) + r.remove([patch], False, wlock) + + q.save_dirty() + def restore(ui, repo, rev, **opts): """restore the queue state saved by a rev""" rev = repo.lookup(rev) - q = repomap[repo] + q = repo.mq q.restore(repo, rev, delete=opts['delete'], qupdate=opts['update']) q.save_dirty() @@ -1225,8 +1697,9 @@ def restore(ui, repo, rev, **opts): def save(ui, repo, **opts): """save current queue state""" - q = repomap[repo] - ret = q.save(repo, msg=opts['message']) + q = repo.mq + message = commands.logmessage(opts) + ret = q.save(repo, msg=message) if ret: return ret q.save_dirty() @@ -1236,20 +1709,18 @@ def save(ui, repo, **opts): newpath = os.path.join(q.basepath, opts['name']) if os.path.exists(newpath): if not os.path.isdir(newpath): - ui.warn("destination %s exists and is not a directory\n" % - newpath) - sys.exit(1) + raise util.Abort(_('destination %s exists and is not ' + 'a directory') % newpath) if not opts['force']: - ui.warn("destination %s exists, use -f to force\n" % - newpath) - sys.exit(1) + raise util.Abort(_('destination %s exists, ' + 'use -f to force') % newpath) else: newpath = savename(path) ui.warn("copy %s to %s\n" % (path, newpath)) util.copyfiles(path, newpath) if opts['empty']: try: - os.unlink(os.path.join(q.path, q.status_path)) + os.unlink(q.join(q.status_path)) except: pass return 0 @@ -1262,25 +1733,196 @@ def strip(ui, repo, rev, **opts): backup = 'strip' elif opts['nobackup']: backup = 'none' - repomap[repo].strip(repo, rev, backup=backup) + repo.mq.strip(repo, rev, backup=backup) return 0 -def version(ui, q=None): - """print the version number""" - ui.write("mq version %s\n" % versionstr) - return 0 +def select(ui, repo, *args, **opts): + '''set or print guarded patches to push + + Use the qguard command to set or print guards on patch, then use + qselect to tell mq which guards to use. A patch will be pushed if it + has no guards or any positive guards match the currently selected guard, + but will not be pushed if any negative guards match the current guard. + For example: + + qguard foo.patch -stable (negative guard) + qguard bar.patch +stable (positive guard) + qselect stable + + This activates the "stable" guard. mq will skip foo.patch (because + it has a negative match) but push bar.patch (because it + has a positive match). + + With no arguments, prints the currently active guards. + With one argument, sets the active guard. + + Use -n/--none to deactivate guards (no other arguments needed). + When no guards are active, patches with positive guards are skipped + and patches with negative guards are pushed. + + qselect can change the guards on applied patches. It does not pop + guarded patches by default. Use --pop to pop back to the last applied + patch that is not guarded. Use --reapply (which implies --pop) to push + back to the current patch afterwards, but skip guarded patches. + + Use -s/--series to print a list of all guards in the series file (no + other arguments needed). Use -v for more information.''' + + q = repo.mq + guards = q.active() + if args or opts['none']: + old_unapplied = q.unapplied(repo) + old_guarded = [i for i in xrange(len(q.applied)) if + not q.pushable(i)[0]] + q.set_active(args) + q.save_dirty() + if not args: + ui.status(_('guards deactivated\n')) + if not opts['pop'] and not opts['reapply']: + unapplied = q.unapplied(repo) + guarded = [i for i in xrange(len(q.applied)) + if not q.pushable(i)[0]] + if len(unapplied) != len(old_unapplied): + ui.status(_('number of unguarded, unapplied patches has ' + 'changed from %d to %d\n') % + (len(old_unapplied), len(unapplied))) + if len(guarded) != len(old_guarded): + ui.status(_('number of guarded, applied patches has changed ' + 'from %d to %d\n') % + (len(old_guarded), len(guarded))) + elif opts['series']: + guards = {} + noguards = 0 + for gs in q.series_guards: + if not gs: + noguards += 1 + for g in gs: + guards.setdefault(g, 0) + guards[g] += 1 + if ui.verbose: + guards['NONE'] = noguards + guards = guards.items() + guards.sort(lambda a, b: cmp(a[0][1:], b[0][1:])) + if guards: + ui.note(_('guards in series file:\n')) + for guard, count in guards: + ui.note('%2d ' % count) + ui.write(guard, '\n') + else: + ui.note(_('no guards in series file\n')) + else: + if guards: + ui.note(_('active guards:\n')) + for g in guards: + ui.write(g, '\n') + else: + ui.write(_('no active guards\n')) + reapply = opts['reapply'] and q.applied and q.appliedname(-1) + popped = False + if opts['pop'] or opts['reapply']: + for i in xrange(len(q.applied)): + pushable, reason = q.pushable(i) + if not pushable: + ui.status(_('popping guarded patches\n')) + popped = True + if i == 0: + q.pop(repo, all=True) + else: + q.pop(repo, i-1) + break + if popped: + try: + if reapply: + ui.status(_('reapplying unguarded patches\n')) + q.push(repo, reapply) + finally: + q.save_dirty() def reposetup(ui, repo): - repomap[repo] = queue(ui, repo.join("")) + class mqrepo(repo.__class__): + def abort_if_wdir_patched(self, errmsg, force=False): + if self.mq.applied and not force: + parent = revlog.hex(self.dirstate.parents()[0]) + if parent in [s.rev for s in self.mq.applied]: + raise util.Abort(errmsg) + + def commit(self, *args, **opts): + if len(args) >= 6: + force = args[5] + else: + force = opts.get('force') + self.abort_if_wdir_patched( + _('cannot commit over an applied mq patch'), + force) + + return super(mqrepo, self).commit(*args, **opts) + + def push(self, remote, force=False, revs=None): + if self.mq.applied and not force: + raise util.Abort(_('source has mq patches applied')) + return super(mqrepo, self).push(remote, force, revs) + + def tags(self): + if self.tagscache: + return self.tagscache + + tagscache = super(mqrepo, self).tags() + + q = self.mq + if not q.applied: + return tagscache + + mqtags = [(patch.rev, patch.name) for patch in q.applied] + mqtags.append((mqtags[-1][0], 'qtip')) + mqtags.append((mqtags[0][0], 'qbase')) + for patch in mqtags: + if patch[1] in tagscache: + self.ui.warn('Tag %s overrides mq patch of the same name\n' % patch[1]) + else: + tagscache[patch[1]] = revlog.bin(patch[0]) + + return tagscache + + if repo.local(): + repo.__class__ = mqrepo + repo.mq = queue(ui, repo.join("")) cmdtable = { "qapplied": (applied, [], 'hg qapplied [PATCH]'), + "qclone": (clone, + [('', 'pull', None, _('use pull protocol to copy metadata')), + ('U', 'noupdate', None, _('do not update the new working directories')), + ('', 'uncompressed', None, + _('use uncompressed transfer (fast over LAN)')), + ('e', 'ssh', '', _('specify ssh command to use')), + ('p', 'patches', '', _('location of source patch repo')), + ('', 'remotecmd', '', + _('specify hg command to run on the remote side'))], + 'hg qclone [OPTION]... SOURCE [DEST]'), "qcommit|qci": (commit, commands.table["^commit|ci"][1], 'hg qcommit [OPTION]... [FILE]...'), - "^qdiff": (diff, [], 'hg qdiff [FILE]...'), - "qdelete": (delete, [], 'hg qdelete PATCH'), + "^qdiff": (diff, + [('I', 'include', [], _('include names matching the given patterns')), + ('X', 'exclude', [], _('exclude names matching the given patterns'))], + 'hg qdiff [-I] [-X] [FILE]...'), + "qdelete|qremove|qrm": + (delete, + [('k', 'keep', None, _('keep patch file'))], + 'hg qdelete [-k] PATCH'), + 'qfold': + (fold, + [('e', 'edit', None, _('edit patch header')), + ('k', 'keep', None, _('keep folded patch files')), + ('m', 'message', '', _('set patch header to ')), + ('l', 'logfile', '', _('set patch header to contents of '))], + 'hg qfold [-e] [-m ] [-l as commit message')), + ('l', 'logfile', '', _('read the commit message from ')), + ('f', 'force', None, _('import uncommitted changes into patch'))], + 'hg qnew [-e] [-m TEXT] [-l FILE] [-f] PATCH'), "qnext": (next, [], 'hg qnext'), "qprev": (prev, [], 'hg qprev'), "^qpop": @@ -1314,8 +1958,15 @@ cmdtable = { 'hg qpush [-f] [-l] [-a] [-m] [-n NAME] [PATCH | INDEX]'), "^qrefresh": (refresh, - [('s', 'short', None, 'short refresh')], - 'hg qrefresh [-s]'), + [('e', 'edit', None, _('edit commit message')), + ('m', 'message', '', _('change commit message with ')), + ('l', 'logfile', '', _('change commit message with content')), + ('s', 'short', None, 'short refresh'), + ('I', 'include', [], _('include names matching the given patterns')), + ('X', 'exclude', [], _('exclude names matching the given patterns'))], + 'hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] FILES...'), + 'qrename|qmv': + (rename, [], 'hg qrename PATCH1 [PATCH2]'), "qrestore": (restore, [('d', 'delete', None, 'delete save entry'), @@ -1323,15 +1974,24 @@ cmdtable = { 'hg qrestore [-d] [-u] REV'), "qsave": (save, - [('m', 'message', '', 'commit message'), + [('m', 'message', '', _('use as commit message')), + ('l', 'logfile', '', _('read the commit message from ')), ('c', 'copy', None, 'copy patch directory'), ('n', 'name', '', 'copy directory name'), ('e', 'empty', None, 'clear queue status file'), ('f', 'force', None, 'force copy')], - 'hg qsave [-m TEXT] [-c] [-n NAME] [-e] [-f]'), + 'hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'), + "qselect": (select, + [('n', 'none', None, _('disable all guards')), + ('s', 'series', None, _('list all guards in series file')), + ('', 'pop', None, + _('pop to before first guarded applied patch')), + ('', 'reapply', None, _('pop, then reapply patches'))], + 'hg qselect [OPTION...] [GUARD...]'), "qseries": (series, - [('m', 'missing', None, 'print patches not in series')], + [('m', 'missing', None, 'print patches not in series'), + ('s', 'summary', None, _('print first line of patch header'))], 'hg qseries [-m]'), "^strip": (strip, @@ -1341,6 +2001,4 @@ cmdtable = { 'hg strip [-f] [-b] [-n] REV'), "qtop": (top, [], 'hg qtop'), "qunapplied": (unapplied, [], 'hg qunapplied [PATCH]'), - "qversion": (version, [], 'hg qversion') } - diff --git a/hgext/notify.py b/hgext/notify.py --- a/hgext/notify.py +++ b/hgext/notify.py @@ -67,8 +67,8 @@ from mercurial.demandload import * from mercurial.i18n import gettext as _ from mercurial.node import * -demandload(globals(), 'email.Parser mercurial:commands,templater,util') -demandload(globals(), 'fnmatch socket time') +demandload(globals(), 'mercurial:commands,patch,templater,util,mail') +demandload(globals(), 'email.Parser fnmatch socket time') # template for single changeset can include email headers. single_template = ''' @@ -229,8 +229,8 @@ class notifier(object): else: self.ui.status(_('notify: sending %d subscribers %d changes\n') % (len(self.subs), count)) - mail = self.ui.sendmail() - mail.sendmail(templater.email(msg['From']), self.subs, msgtext) + mail.sendmail(self.ui, templater.email(msg['From']), + self.subs, msgtext) def diff(self, node, ref): maxdiff = int(self.ui.config('notify', 'maxdiff', 300)) @@ -238,7 +238,7 @@ class notifier(object): return fp = templater.stringio() prev = self.repo.changelog.parents(node)[0] - commands.dodiff(fp, self.ui, self.repo, prev, ref) + patch.diff(self.repo, fp, prev, ref) difflines = fp.getvalue().splitlines(1) if maxdiff > 0 and len(difflines) > maxdiff: self.sio.write(_('\ndiffs (truncated from %d to %d lines):\n\n') % @@ -255,7 +255,7 @@ def hook(ui, repo, hooktype, node=None, changegroup. else send one email per changeset.''' n = notifier(ui, repo, hooktype) if not n.subs: - ui.debug(_('notify: no subscribers to this repo\n')) + ui.debug(_('notify: no subscribers to repo %s\n' % n.root)) return if n.skipsource(source): ui.debug(_('notify: changes have source "%s" - skipping\n') % diff --git a/hgext/patchbomb.py b/hgext/patchbomb.py --- a/hgext/patchbomb.py +++ b/hgext/patchbomb.py @@ -23,27 +23,52 @@ # the changeset summary, so you can be sure you are sending the right # changes. # -# It is best to run this script with the "-n" (test only) flag before -# firing it up "for real", in which case it will use your pager to -# display each of the messages that it would send. +# To enable this extension: # -# The "-m" (mbox) option will create an mbox file instead of sending -# the messages directly. This can be reviewed e.g. with "mutt -R -f mbox", -# and finally sent with "formail -s sendmail -bm -t < mbox". +# [extensions] +# hgext.patchbomb = # # To configure other defaults, add a section like this to your hgrc # file: # -# [email] -# from = My Name -# to = recipient1, recipient2, ... -# cc = cc1, cc2, ... +# [email] +# from = My Name +# to = recipient1, recipient2, ... +# cc = cc1, cc2, ... +# bcc = bcc1, bcc2, ... +# +# Then you can use the "hg email" command to mail a series of changesets +# as a patchbomb. +# +# To avoid sending patches prematurely, it is a good idea to first run +# the "email" command with the "-n" option (test only). You will be +# prompted for an email recipient address, a subject an an introductory +# message describing the patches of your patchbomb. Then when all is +# done, your pager will be fired up once for each patchbomb message, so +# you can verify everything is alright. +# +# The "-m" (mbox) option is also very useful. Instead of previewing +# each patchbomb message in a pager or sending the messages directly, +# it will create a UNIX mailbox file with the patch emails. This +# mailbox file can be previewed with any mail user agent which supports +# UNIX mbox files, i.e. with mutt: +# +# % mutt -R -f mbox +# +# When you are previewing the patchbomb messages, you can use `formail' +# (a utility that is commonly installed as part of the procmail package), +# to send each message out: +# +# % formail -s sendmail -bm -t < mbox +# +# That should be all. Now your patchbomb is on its way out. from mercurial.demandload import * demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils - mercurial:commands,hg,ui + mercurial:commands,hg,mail,ui os errno popen2 socket sys tempfile time''') from mercurial.i18n import gettext as _ +from mercurial.node import * try: # readline gives raw_input editing capabilities, but is not @@ -129,8 +154,26 @@ def patchbomb(ui, repo, *revs, **opts): while patch and not patch[0].strip(): patch.pop(0) if opts['diffstat']: body += cdiffstat('\n'.join(desc), patch) + '\n\n' - body += '\n'.join(patch) - msg = email.MIMEText.MIMEText(body) + if opts['attach']: + msg = email.MIMEMultipart.MIMEMultipart() + if body: msg.attach(email.MIMEText.MIMEText(body, 'plain')) + p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch') + binnode = bin(node) + # if node is mq patch, it will have patch file name as tag + patchname = [t for t in repo.nodetags(binnode) + if t.endswith('.patch') or t.endswith('.diff')] + if patchname: + patchname = patchname[0] + elif total > 1: + patchname = commands.make_filename(repo, '%b-%n.patch', + binnode, idx, total) + else: + patchname = commands.make_filename(repo, '%b.patch', binnode) + p['Content-Disposition'] = 'inline; filename=' + patchname + msg.attach(p) + else: + body += '\n'.join(patch) + msg = email.MIMEText.MIMEText(body) if total == 1: subj = '[PATCH] ' + desc[0].strip() else: @@ -185,11 +228,14 @@ def patchbomb(ui, repo, *revs, **opts): to = getaddrs('to', 'To') cc = getaddrs('cc', 'Cc', '') + bcc = opts['bcc'] or (ui.config('email', 'bcc') or + ui.config('patchbomb', 'bcc') or '').split(',') + bcc = [a.strip() for a in bcc if a.strip()] + if len(patches) > 1: ui.write(_('\nWrite the introductory message for the patch series.\n\n')) - msg = email.MIMEMultipart.MIMEMultipart() - msg['Subject'] = '[PATCH 0 of %d] %s' % ( + subj = '[PATCH 0 of %d] %s' % ( len(patches), opts['subject'] or prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches))) @@ -204,18 +250,21 @@ def patchbomb(ui, repo, *revs, **opts): if l == '.': break body.append(l) - msg.attach(email.MIMEText.MIMEText('\n'.join(body) + '\n')) - if opts['diffstat']: d = cdiffstat(_('Final summary:\n'), jumbo) - if d: msg.attach(email.MIMEText.MIMEText(d)) + if d: body.append('\n' + d) + + body = '\n'.join(body) + '\n' + + msg = email.MIMEText.MIMEText(body) + msg['Subject'] = subj msgs.insert(0, msg) ui.write('\n') if not opts['test'] and not opts['mbox']: - mail = ui.sendmail() + mailer = mail.connect(ui) parent = None # Calculate UTC offset @@ -240,7 +289,8 @@ def patchbomb(ui, repo, *revs, **opts): start_time += 1 m['From'] = sender m['To'] = ', '.join(to) - if cc: m['Cc'] = ', '.join(cc) + if cc: m['Cc'] = ', '.join(cc) + if bcc: m['Bcc'] = ', '.join(bcc) if opts['test']: ui.status('Displaying ', m['Subject'], ' ...\n') fp = os.popen(os.getenv('PAGER', 'more'), 'w') @@ -261,12 +311,16 @@ def patchbomb(ui, repo, *revs, **opts): fp.close() else: ui.status('Sending ', m['Subject'], ' ...\n') - mail.sendmail(sender, to + cc, m.as_string(0)) + # Exim does not remove the Bcc field + del m['Bcc'] + mailer.sendmail(sender, to + bcc + cc, m.as_string(0)) cmdtable = { 'email': (patchbomb, - [('c', 'cc', [], 'email addresses of copy recipients'), + [('a', 'attach', None, 'send patches as inline attachments'), + ('', 'bcc', [], 'email addresses of blind copy recipients'), + ('c', 'cc', [], 'email addresses of copy recipients'), ('d', 'diffstat', None, 'add diffstat output to messages'), ('f', 'from', '', 'email address of sender'), ('', 'plain', None, 'omit hg patch header'), diff --git a/mercurial/archival.py b/mercurial/archival.py --- a/mercurial/archival.py +++ b/mercurial/archival.py @@ -163,12 +163,12 @@ def archive(repo, dest, node, kind, deco change = repo.changelog.read(node) mn = change[0] archiver = archivers[kind](dest, prefix, mtime or change[2][0]) - mf = repo.manifest.read(mn).items() - mff = repo.manifest.readflags(mn) - mf.sort() + m = repo.manifest.read(mn) + items = m.items() + items.sort() write('.hg_archival.txt', 0644, 'repo: %s\nnode: %s\n' % (hex(repo.changelog.node(0)), hex(node))) - for filename, filenode in mf: - write(filename, mff[filename] and 0755 or 0644, + for filename, filenode in items: + write(filename, m.execf(filename) and 0755 or 0644, repo.file(filename).read(filenode)) archiver.done() diff --git a/mercurial/bdiff.c b/mercurial/bdiff.c --- a/mercurial/bdiff.c +++ b/mercurial/bdiff.c @@ -1,7 +1,7 @@ /* bdiff.c - efficient binary diff extension for Mercurial - Copyright 2005 Matt Mackall + Copyright 2005, 2006 Matt Mackall This software may be used and distributed according to the terms of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/bundlerepo.py b/mercurial/bundlerepo.py --- a/mercurial/bundlerepo.py +++ b/mercurial/bundlerepo.py @@ -159,6 +159,10 @@ class bundlefilelog(bundlerevlog, filelo class bundlerepository(localrepo.localrepository): def __init__(self, ui, path, bundlename): localrepo.localrepository.__init__(self, ui, path) + + self._url = 'bundle:' + bundlename + if path: self._url += '+' + path + self.tempfile = None self.bundlefile = open(bundlename, "rb") header = self.bundlefile.read(6) @@ -208,6 +212,9 @@ class bundlerepository(localrepo.localre for c in changegroup.chunkiter(self.bundlefile): pass + def url(self): + return self._url + def dev(self): return -1 @@ -230,3 +237,18 @@ class bundlerepository(localrepo.localre self.bundlefile.close() if self.tempfile is not None: os.unlink(self.tempfile) + +def instance(ui, path, create): + if create: + raise util.Abort(_('cannot create new bundle repository')) + path = util.drop_scheme('file', path) + if path.startswith('bundle:'): + path = util.drop_scheme('bundle', path) + s = path.split("+", 1) + if len(s) == 1: + repopath, bundlename = "", s[0] + else: + repopath, bundlename = s + else: + repopath, bundlename = '', path + return bundlerepository(ui, repopath, bundlename) diff --git a/mercurial/changelog.py b/mercurial/changelog.py --- a/mercurial/changelog.py +++ b/mercurial/changelog.py @@ -1,6 +1,6 @@ # changelog.py - changelog class for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py new file mode 100644 --- /dev/null +++ b/mercurial/cmdutil.py @@ -0,0 +1,111 @@ +# commands.py - command processing for mercurial +# +# Copyright 2005, 2006 Matt Mackall +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from demandload import demandload +from node import * +from i18n import gettext as _ +demandload(globals(), 'util') +demandload(globals(), 'os sys') + +def make_filename(repo, pat, node, + total=None, seqno=None, revwidth=None, pathname=None): + node_expander = { + 'H': lambda: hex(node), + 'R': lambda: str(repo.changelog.rev(node)), + 'h': lambda: short(node), + } + expander = { + '%': lambda: '%', + 'b': lambda: os.path.basename(repo.root), + } + + try: + if node: + expander.update(node_expander) + if node and revwidth is not None: + expander['r'] = (lambda: + str(repo.changelog.rev(node)).zfill(revwidth)) + if total is not None: + expander['N'] = lambda: str(total) + if seqno is not None: + expander['n'] = lambda: str(seqno) + if total is not None and seqno is not None: + expander['n'] = lambda:str(seqno).zfill(len(str(total))) + if pathname is not None: + expander['s'] = lambda: os.path.basename(pathname) + expander['d'] = lambda: os.path.dirname(pathname) or '.' + expander['p'] = lambda: pathname + + newname = [] + patlen = len(pat) + i = 0 + while i < patlen: + c = pat[i] + if c == '%': + i += 1 + c = pat[i] + c = expander[c]() + newname.append(c) + i += 1 + return ''.join(newname) + except KeyError, inst: + raise util.Abort(_("invalid format spec '%%%s' in output file name"), + inst.args[0]) + +def make_file(repo, pat, node=None, + total=None, seqno=None, revwidth=None, mode='wb', pathname=None): + if not pat or pat == '-': + return 'w' in mode and sys.stdout or sys.stdin + if hasattr(pat, 'write') and 'w' in mode: + return pat + if hasattr(pat, 'read') and 'r' in mode: + return pat + return open(make_filename(repo, pat, node, total, seqno, revwidth, + pathname), + mode) + +def matchpats(repo, pats=[], opts={}, head=''): + cwd = repo.getcwd() + if not pats and cwd: + opts['include'] = [os.path.join(cwd, i) + for i in opts.get('include', [])] + opts['exclude'] = [os.path.join(cwd, x) + for x in opts.get('exclude', [])] + cwd = '' + return util.cmdmatcher(repo.root, cwd, pats or ['.'], opts.get('include'), + opts.get('exclude'), head) + +def makewalk(repo, pats=[], opts={}, node=None, head='', badmatch=None): + files, matchfn, anypats = matchpats(repo, pats, opts, head) + exact = dict(zip(files, files)) + def walk(): + for src, fn in repo.walk(node=node, files=files, match=matchfn, + badmatch=badmatch): + yield src, fn, util.pathto(repo.getcwd(), fn), fn in exact + return files, matchfn, walk() + +def walk(repo, pats=[], opts={}, node=None, head='', badmatch=None): + files, matchfn, results = makewalk(repo, pats, opts, node, head, badmatch) + for r in results: + yield r + +def addremove(repo, pats=[], opts={}, wlock=None, dry_run=None): + if dry_run is None: + dry_run = opts.get('dry_run') + add, remove = [], [] + for src, abs, rel, exact in walk(repo, pats, opts): + if src == 'f' and repo.dirstate.state(abs) == '?': + add.append(abs) + if repo.ui.verbose or not exact: + repo.ui.status(_('adding %s\n') % ((pats and rel) or abs)) + if repo.dirstate.state(abs) != 'r' and not os.path.exists(rel): + remove.append(abs) + if repo.ui.verbose or not exact: + repo.ui.status(_('removing %s\n') % ((pats and rel) or abs)) + if not dry_run: + repo.add(add, wlock=wlock) + repo.remove(remove, wlock=wlock) diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -1,6 +1,6 @@ # commands.py - command processing for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -10,10 +10,10 @@ from node import * from i18n import gettext as _ demandload(globals(), "os re sys signal shutil imp urllib pdb") demandload(globals(), "fancyopts ui hg util lock revlog templater bundlerepo") -demandload(globals(), "fnmatch mdiff random signal tempfile time") +demandload(globals(), "fnmatch difflib patch random signal tempfile time") demandload(globals(), "traceback errno socket version struct atexit sets bz2") -demandload(globals(), "archival cStringIO changegroup email.Parser") -demandload(globals(), "hgweb.server sshserver") +demandload(globals(), "archival cStringIO changegroup") +demandload(globals(), "cmdutil hgweb.server sshserver") class UnknownCommand(Exception): """Exception raised if command is not in the command table.""" @@ -21,47 +21,34 @@ class AmbiguousCommand(Exception): """Exception raised if command shortcut matches more than one command.""" def bail_if_changed(repo): - modified, added, removed, deleted, unknown = repo.changes() + modified, added, removed, deleted = repo.status()[:4] if modified or added or removed or deleted: raise util.Abort(_("outstanding uncommitted changes")) -def filterfiles(filters, files): - l = [x for x in files if x in filters] - - for t in filters: - if t and t[-1] != "/": - t += "/" - l += [x for x in files if x.startswith(t)] - return l - def relpath(repo, args): cwd = repo.getcwd() if cwd: return [util.normpath(os.path.join(cwd, x)) for x in args] return args -def matchpats(repo, pats=[], opts={}, head=''): - cwd = repo.getcwd() - if not pats and cwd: - opts['include'] = [os.path.join(cwd, i) for i in opts['include']] - opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']] - cwd = '' - return util.cmdmatcher(repo.root, cwd, pats or ['.'], opts.get('include'), - opts.get('exclude'), head) - -def makewalk(repo, pats, opts, node=None, head='', badmatch=None): - files, matchfn, anypats = matchpats(repo, pats, opts, head) - exact = dict(zip(files, files)) - def walk(): - for src, fn in repo.walk(node=node, files=files, match=matchfn, - badmatch=badmatch): - yield src, fn, util.pathto(repo.getcwd(), fn), fn in exact - return files, matchfn, walk() - -def walk(repo, pats, opts, node=None, head='', badmatch=None): - files, matchfn, results = makewalk(repo, pats, opts, node, head, badmatch) - for r in results: - yield r +def logmessage(opts): + """ get the log message according to -m and -l option """ + message = opts['message'] + logfile = opts['logfile'] + + if message and logfile: + raise util.Abort(_('options --message and --logfile are mutually ' + 'exclusive')) + if not message and logfile: + try: + if logfile == '-': + message = sys.stdin.read() + else: + message = open(logfile).read() + except IOError, inst: + raise util.Abort(_("can't read commit message '%s': %s") % + (logfile, inst.strerror)) + return message def walkchangerevs(ui, repo, pats, opts): '''Iterate over files and the revs they changed in. @@ -105,12 +92,23 @@ def walkchangerevs(ui, repo, pats, opts) windowsize *= 2 - files, matchfn, anypats = matchpats(repo, pats, opts) + files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) + follow = opts.get('follow') or opts.get('follow_first') if repo.changelog.count() == 0: return [], False, matchfn - revs = map(int, revrange(ui, repo, opts['rev'] or ['tip:0'])) + if follow: + p = repo.dirstate.parents()[0] + if p == nullid: + ui.warn(_('No working directory revision; defaulting to tip\n')) + start = 'tip' + else: + start = repo.changelog.rev(p) + defrange = '%s:0' % start + else: + defrange = 'tip:0' + revs = map(int, revrange(ui, repo, opts['rev'] or [defrange])) wanted = {} slowpath = anypats fncache = {} @@ -125,37 +123,54 @@ def walkchangerevs(ui, repo, pats, opts) if not slowpath and not files: # No files, no patterns. Display all revs. wanted = dict(zip(revs, revs)) + copies = [] if not slowpath: # Only files, no patterns. Check the history of each file. - def filerevgen(filelog): + def filerevgen(filelog, node): cl_count = repo.changelog.count() - for i, window in increasing_windows(filelog.count()-1, -1): + if node is None: + last = filelog.count() - 1 + else: + last = filelog.rev(node) + for i, window in increasing_windows(last, -1): revs = [] for j in xrange(i - window, i + 1): - revs.append(filelog.linkrev(filelog.node(j))) + n = filelog.node(j) + revs.append((filelog.linkrev(n), + follow and filelog.renamed(n))) revs.reverse() for rev in revs: # only yield rev for which we have the changelog, it can # happen while doing "hg log" during a pull or commit - if rev < cl_count: + if rev[0] < cl_count: yield rev - + def iterfiles(): + for filename in files: + yield filename, None + for filename_node in copies: + yield filename_node minrev, maxrev = min(revs), max(revs) - for file_ in files: + for file_, node in iterfiles(): filelog = repo.file(file_) # A zero count may be a directory or deleted file, so # try to find matching entries on the slow path. if filelog.count() == 0: slowpath = True break - for rev in filerevgen(filelog): + for rev, copied in filerevgen(filelog, node): if rev <= maxrev: if rev < minrev: break fncache.setdefault(rev, []) fncache[rev].append(file_) wanted[rev] = 1 + if follow and copied: + copies.append(copied) if slowpath: + if follow: + raise util.Abort(_('can only follow copies/renames for explicit ' + 'file names')) + # The slow path checks files modified in every changeset. def changerevgen(): for i, window in increasing_windows(repo.changelog.count()-1, -1): @@ -168,11 +183,66 @@ def walkchangerevs(ui, repo, pats, opts) fncache[rev] = matches wanted[rev] = 1 + class followfilter: + def __init__(self, onlyfirst=False): + self.startrev = -1 + self.roots = [] + self.onlyfirst = onlyfirst + + def match(self, rev): + def realparents(rev): + if self.onlyfirst: + return repo.changelog.parentrevs(rev)[0:1] + else: + return filter(lambda x: x != -1, repo.changelog.parentrevs(rev)) + + if self.startrev == -1: + self.startrev = rev + return True + + if rev > self.startrev: + # forward: all descendants + if not self.roots: + self.roots.append(self.startrev) + for parent in realparents(rev): + if parent in self.roots: + self.roots.append(rev) + return True + else: + # backwards: all parents + if not self.roots: + self.roots.extend(realparents(self.startrev)) + if rev in self.roots: + self.roots.remove(rev) + self.roots.extend(realparents(rev)) + return True + + return False + + # it might be worthwhile to do this in the iterator if the rev range + # is descending and the prune args are all within that range + for rev in opts.get('prune', ()): + rev = repo.changelog.rev(repo.lookup(rev)) + ff = followfilter() + stop = min(revs[0], revs[-1]) + for x in range(rev, stop-1, -1): + if ff.match(x) and wanted.has_key(x): + del wanted[x] + def iterate(): + if follow and not files: + ff = followfilter(onlyfirst=opts.get('follow_first')) + def want(rev): + if ff.match(rev) and rev in wanted: + return True + return False + else: + def want(rev): + return rev in wanted + for i, window in increasing_windows(0, len(revs)): yield 'window', revs[0] < revs[-1], revs[-1] - nrevs = [rev for rev in revs[i:i+window] - if rev in wanted] + nrevs = [rev for rev in revs[i:i+window] if want(rev)] srevs = list(nrevs) srevs.sort() for rev in srevs: @@ -252,62 +322,6 @@ def revrange(ui, repo, revs): seen[rev] = 1 yield str(rev) -def make_filename(repo, pat, node, - total=None, seqno=None, revwidth=None, pathname=None): - node_expander = { - 'H': lambda: hex(node), - 'R': lambda: str(repo.changelog.rev(node)), - 'h': lambda: short(node), - } - expander = { - '%': lambda: '%', - 'b': lambda: os.path.basename(repo.root), - } - - try: - if node: - expander.update(node_expander) - if node and revwidth is not None: - expander['r'] = lambda: str(r.rev(node)).zfill(revwidth) - if total is not None: - expander['N'] = lambda: str(total) - if seqno is not None: - expander['n'] = lambda: str(seqno) - if total is not None and seqno is not None: - expander['n'] = lambda:str(seqno).zfill(len(str(total))) - if pathname is not None: - expander['s'] = lambda: os.path.basename(pathname) - expander['d'] = lambda: os.path.dirname(pathname) or '.' - expander['p'] = lambda: pathname - - newname = [] - patlen = len(pat) - i = 0 - while i < patlen: - c = pat[i] - if c == '%': - i += 1 - c = pat[i] - c = expander[c]() - newname.append(c) - i += 1 - return ''.join(newname) - except KeyError, inst: - raise util.Abort(_("invalid format spec '%%%s' in output file name"), - inst.args[0]) - -def make_file(repo, pat, node=None, - total=None, seqno=None, revwidth=None, mode='wb', pathname=None): - if not pat or pat == '-': - return 'w' in mode and sys.stdout or sys.stdin - if hasattr(pat, 'write') and 'w' in mode: - return pat - if hasattr(pat, 'read') and 'r' in mode: - return pat - return open(make_filename(repo, pat, node, total, seqno, revwidth, - pathname), - mode) - def write_bundle(cg, filename=None, compress=True): """Write a bundle file and return its filename. @@ -360,83 +374,6 @@ def write_bundle(cg, filename=None, comp if cleanup is not None: os.unlink(cleanup) -def dodiff(fp, ui, repo, node1, node2, files=None, match=util.always, - changes=None, text=False, opts={}): - if not node1: - node1 = repo.dirstate.parents()[0] - # reading the data for node1 early allows it to play nicely - # with repo.changes and the revlog cache. - change = repo.changelog.read(node1) - mmap = repo.manifest.read(change[0]) - date1 = util.datestr(change[2]) - - if not changes: - changes = repo.changes(node1, node2, files, match=match) - modified, added, removed, deleted, unknown = changes - if files: - modified, added, removed = map(lambda x: filterfiles(files, x), - (modified, added, removed)) - - if not modified and not added and not removed: - return - - if node2: - change = repo.changelog.read(node2) - mmap2 = repo.manifest.read(change[0]) - _date2 = util.datestr(change[2]) - def date2(f): - return _date2 - def read(f): - return repo.file(f).read(mmap2[f]) - else: - tz = util.makedate()[1] - _date2 = util.datestr() - def date2(f): - try: - return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz)) - except OSError, err: - if err.errno != errno.ENOENT: raise - return _date2 - def read(f): - return repo.wread(f) - - if ui.quiet: - r = None - else: - hexfunc = ui.verbose and hex or short - r = [hexfunc(node) for node in [node1, node2] if node] - - diffopts = ui.diffopts() - showfunc = opts.get('show_function') or diffopts['showfunc'] - ignorews = opts.get('ignore_all_space') or diffopts['ignorews'] - ignorewsamount = opts.get('ignore_space_change') or \ - diffopts['ignorewsamount'] - ignoreblanklines = opts.get('ignore_blank_lines') or \ - diffopts['ignoreblanklines'] - for f in modified: - to = None - if f in mmap: - to = repo.file(f).read(mmap[f]) - tn = read(f) - fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, text=text, - showfunc=showfunc, ignorews=ignorews, - ignorewsamount=ignorewsamount, - ignoreblanklines=ignoreblanklines)) - for f in added: - to = None - tn = read(f) - fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, text=text, - showfunc=showfunc, ignorews=ignorews, - ignorewsamount=ignorewsamount, - ignoreblanklines=ignoreblanklines)) - for f in removed: - to = repo.file(f).read(mmap[f]) - tn = None - fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, text=text, - showfunc=showfunc, ignorews=ignorews, - ignorewsamount=ignorewsamount, - ignoreblanklines=ignoreblanklines)) - def trimuser(ui, name, rev, revcache): """trim the name of the user who committed a change""" user = revcache.get(rev) @@ -493,7 +430,7 @@ class changeset_printer(object): self.ui.status(_("date: %s\n") % date) if self.ui.debugflag: - files = self.repo.changes(log.parents(changenode)[0], changenode) + files = self.repo.status(log.parents(changenode)[0], changenode)[:3] for key, value in zip([_("files:"), _("files+:"), _("files-:")], files): if value: @@ -537,12 +474,19 @@ def show_changeset(ui, repo, opts): return t return changeset_printer(ui, repo) +def setremoteconfig(ui, opts): + "copy remote options to ui tree" + if opts.get('ssh'): + ui.setconfig("ui", "ssh", opts['ssh']) + if opts.get('remotecmd'): + ui.setconfig("ui", "remotecmd", opts['remotecmd']) + def show_version(ui): """output version and copyright information""" ui.write(_("Mercurial Distributed SCM (version %s)\n") % version.get_version()) ui.status(_( - "\nCopyright (C) 2005 Matt Mackall \n" + "\nCopyright (C) 2005, 2006 Matt Mackall \n" "This is free software; see the source for copying conditions. " "There is NO\nwarranty; " "not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n" @@ -696,7 +640,7 @@ def add(ui, repo, *pats, **opts): """ names = [] - for src, abs, rel, exact in walk(repo, pats, opts): + for src, abs, rel, exact in cmdutil.walk(repo, pats, opts): if exact: if ui.verbose: ui.status(_('adding %s\n') % rel) @@ -715,22 +659,7 @@ def addremove(ui, repo, *pats, **opts): New files are ignored if they match any of the patterns in .hgignore. As with add, these changes take effect at the next commit. """ - return addremove_lock(ui, repo, pats, opts) - -def addremove_lock(ui, repo, pats, opts, wlock=None): - add, remove = [], [] - for src, abs, rel, exact in walk(repo, pats, opts): - if src == 'f' and repo.dirstate.state(abs) == '?': - add.append(abs) - if ui.verbose or not exact: - ui.status(_('adding %s\n') % ((pats and rel) or abs)) - if repo.dirstate.state(abs) != 'r' and not os.path.exists(rel): - remove.append(abs) - if ui.verbose or not exact: - ui.status(_('removing %s\n') % ((pats and rel) or abs)) - if not opts.get('dry_run'): - repo.add(add, wlock=wlock) - repo.remove(remove, wlock=wlock) + return cmdutil.addremove(repo, pats, opts) def annotate(ui, repo, *pats, **opts): """show changeset information per file line @@ -773,7 +702,8 @@ def annotate(ui, repo, *pats, **opts): ctx = repo.changectx(opts['rev'] or repo.dirstate.parents()[0]) - for src, abs, rel, exact in walk(repo, pats, opts, node=ctx.node()): + for src, abs, rel, exact in cmdutil.walk(repo, pats, opts, + node=ctx.node()): fctx = ctx.filectx(abs) if not opts['text'] and util.binary(fctx.data()): ui.write(_("%s: binary file\n") % ((pats and rel) or abs)) @@ -825,10 +755,10 @@ def archive(ui, repo, dest, **opts): raise util.Abort(_('uncommitted merge - please provide a ' 'specific revision')) - dest = make_filename(repo, dest, node) + dest = cmdutil.make_filename(repo, dest, node) if os.path.realpath(dest) == repo.root: raise util.Abort(_('repository root cannot be destination')) - dummy, matchfn, dummy = matchpats(repo, [], opts) + dummy, matchfn, dummy = cmdutil.matchpats(repo, [], opts) kind = opts.get('type') or 'files' prefix = opts['prefix'] if dest == '-': @@ -836,7 +766,7 @@ def archive(ui, repo, dest, **opts): raise util.Abort(_('cannot archive plain files to stdout')) dest = sys.stdout if not prefix: prefix = os.path.basename(repo.root) + '-%h' - prefix = make_filename(repo, prefix, node) + prefix = cmdutil.make_filename(repo, prefix, node) archival.archive(repo, dest, node, kind, not opts['no_decode'], matchfn, prefix) @@ -879,7 +809,7 @@ def backout(ui, repo, rev, **opts): if opts['parent']: raise util.Abort(_('cannot use --parent on non-merge changeset')) parent = p1 - repo.update(node, force=True, show_stats=False) + hg.clean(repo, node, show_stats=False) revert_opts = opts.copy() revert_opts['rev'] = hex(parent) revert(ui, repo, **revert_opts) @@ -896,11 +826,13 @@ def backout(ui, repo, rev, **opts): if op1 != node: if opts['merge']: ui.status(_('merging with changeset %s\n') % nice(op1)) - doupdate(ui, repo, hex(op1), **opts) + n = _lookup(repo, hex(op1)) + hg.merge(repo, n) else: ui.status(_('the backout changeset is a new head - ' 'do not forget to merge\n')) - ui.status(_('(use "backout -m" if you want to auto-merge)\n')) + ui.status(_('(use "backout --merge" ' + 'if you want to auto-merge)\n')) def bundle(ui, repo, fname, dest=None, **opts): """create a changegroup file @@ -937,9 +869,10 @@ def cat(ui, repo, file1, *pats, **opts): %d dirname of file being printed, or '.' if in repo root %p root-relative path name of file being printed """ - ctx = repo.changectx(opts['rev'] or -1) - for src, abs, rel, exact in walk(repo, (file1,) + pats, opts, ctx.node()): - fp = make_file(repo, opts['output'], ctx.node(), pathname=abs) + ctx = repo.changectx(opts['rev'] or "-1") + for src, abs, rel, exact in cmdutil.walk(repo, (file1,) + pats, opts, + ctx.node()): + fp = cmdutil.make_file(repo, opts['output'], ctx.node(), pathname=abs) fp.write(ctx.filectx(abs).data()) def clone(ui, source, dest=None, **opts): @@ -954,10 +887,25 @@ def clone(ui, source, dest=None, **opts) .hg/hgrc file, as the default to be used for future pulls. For efficiency, hardlinks are used for cloning whenever the source - and destination are on the same filesystem. Some filesystems, - such as AFS, implement hardlinking incorrectly, but do not report - errors. In these cases, use the --pull option to avoid - hardlinking. + and destination are on the same filesystem (note this applies only + to the repository data, not to the checked out files). Some + filesystems, such as AFS, implement hardlinking incorrectly, but + do not report errors. In these cases, use the --pull option to + avoid hardlinking. + + You can safely clone repositories and checked out files using full + hardlinks with + + $ cp -al REPO REPOCLONE + + which is the fastest way to clone. However, the operation is not + atomic (making sure REPO is not modified during the operation is + up to you) and you have to make sure your editor breaks hardlinks + (Emacs and most Linux Kernel tools do so). + + If you use the -r option to clone up to a specific revision, no + subsequent revisions will be present in the cloned repository. + This option implies --pull, even on local repositories. See pull for valid source format details. @@ -965,7 +913,7 @@ def clone(ui, source, dest=None, **opts) .hg/hgrc will be created on the remote side. Look at the help text for the pull command for important details about ssh:// URLs. """ - ui.setconfig_remoteopts(**opts) + setremoteconfig(ui, opts) hg.clone(ui, ui.expandpath(source), dest, pull=opts['pull'], stream=opts['uncompressed'], @@ -983,28 +931,13 @@ def commit(ui, repo, *pats, **opts): If no commit message is specified, the editor configured in your hgrc or in the EDITOR environment variable is started to enter a message. """ - message = opts['message'] - logfile = opts['logfile'] - - if message and logfile: - raise util.Abort(_('options --message and --logfile are mutually ' - 'exclusive')) - if not message and logfile: - try: - if logfile == '-': - message = sys.stdin.read() - else: - message = open(logfile).read() - except IOError, inst: - raise util.Abort(_("can't read commit message '%s': %s") % - (logfile, inst.strerror)) + message = logmessage(opts) if opts['addremove']: - addremove_lock(ui, repo, pats, opts) - fns, match, anypats = matchpats(repo, pats, opts) + cmdutil.addremove(repo, pats, opts) + fns, match, anypats = cmdutil.matchpats(repo, pats, opts) if pats: - modified, added, removed, deleted, unknown = ( - repo.changes(files=fns, match=match)) + modified, added, removed = repo.status(files=fns, match=match)[:3] files = modified + added + removed else: files = [] @@ -1159,7 +1092,7 @@ def docopy(ui, repo, pats, opts, wlock): copylist = [] for pat in pats: srcs = [] - for tag, abssrc, relsrc, exact in walk(repo, [pat], opts): + for tag, abssrc, relsrc, exact in cmdutil.walk(repo, [pat], opts): origsrc = okaytocopy(abssrc, relsrc, exact) if origsrc: srcs.append((origsrc, abssrc, relsrc, exact)) @@ -1233,9 +1166,9 @@ def debugrebuildstate(ui, repo, rev=None rev = repo.lookup(rev) change = repo.changelog.read(rev) n = change[0] - files = repo.manifest.readflags(n) + files = repo.manifest.read(n) wlock = repo.wlock() - repo.dirstate.rebuild(rev, files.iteritems()) + repo.dirstate.rebuild(rev, files) def debugcheckstate(ui, repo): """validate the correctness of the current dirstate""" @@ -1376,7 +1309,7 @@ def debugrename(ui, repo, file, rev=None def debugwalk(ui, repo, *pats, **opts): """show how files match on given patterns""" - items = list(walk(repo, pats, opts)) + items = list(cmdutil.walk(repo, pats, opts)) if not items: return fmt = '%%s %%-%ds %%-%ds %%s' % ( @@ -1405,37 +1338,10 @@ def diff(ui, repo, *pats, **opts): """ node1, node2 = revpair(ui, repo, opts['rev']) - fns, matchfn, anypats = matchpats(repo, pats, opts) - - dodiff(sys.stdout, ui, repo, node1, node2, fns, match=matchfn, - text=opts['text'], opts=opts) - -def doexport(ui, repo, changeset, seqno, total, revwidth, opts): - node = repo.lookup(changeset) - parents = [p for p in repo.changelog.parents(node) if p != nullid] - if opts['switch_parent']: - parents.reverse() - prev = (parents and parents[0]) or nullid - change = repo.changelog.read(node) - - fp = make_file(repo, opts['output'], node, total=total, seqno=seqno, - revwidth=revwidth) - if fp != sys.stdout: - ui.note("%s\n" % fp.name) - - fp.write("# HG changeset patch\n") - fp.write("# User %s\n" % change[1]) - fp.write("# Date %d %d\n" % change[2]) - fp.write("# Node ID %s\n" % hex(node)) - fp.write("# Parent %s\n" % hex(prev)) - if len(parents) > 1: - fp.write("# Parent %s\n" % hex(parents[1])) - fp.write(change[4].rstrip()) - fp.write("\n\n") - - dodiff(fp, ui, repo, prev, node, text=opts['text']) - if fp != sys.stdout: - fp.close() + fns, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) + + patch.diff(repo, node1, node2, fns, match=matchfn, + opts=patch.diffopts(ui, opts)) def export(ui, repo, *changesets, **opts): """dump the header and diffs for one or more changesets @@ -1466,15 +1372,14 @@ def export(ui, repo, *changesets, **opts """ if not changesets: raise util.Abort(_("export requires at least one changeset")) - seqno = 0 revs = list(revrange(ui, repo, changesets)) - total = len(revs) - revwidth = max(map(len, revs)) - msg = len(revs) > 1 and _("Exporting patches:\n") or _("Exporting patch:\n") - ui.note(msg) - for cset in revs: - seqno += 1 - doexport(ui, repo, cset, seqno, total, revwidth, opts) + if len(revs) > 1: + ui.note(_('exporting patches:\n')) + else: + ui.note(_('exporting patch:\n')) + patch.export(repo, map(repo.lookup, revs), template=opts['output'], + switch_parent=opts['switch_parent'], + opts=patch.diffopts(ui, opts)) def forget(ui, repo, *pats, **opts): """don't add the specified files on the next commit (DEPRECATED) @@ -1487,7 +1392,7 @@ def forget(ui, repo, *pats, **opts): """ ui.warn(_("(the forget command is deprecated; use revert instead)\n")) forget = [] - for src, abs, rel, exact in walk(repo, pats, opts): + for src, abs, rel, exact in cmdutil.walk(repo, pats, opts): if repo.dirstate.state(abs) == 'a': forget.append(abs) if ui.verbose or not exact: @@ -1544,42 +1449,56 @@ def grep(ui, repo, pattern, *pats, **opt self.linenum = linenum self.colstart = colstart self.colend = colend + def __eq__(self, other): return self.line == other.line - def __hash__(self): - return hash(self.line) matches = {} + copies = {} def grepbody(fn, rev, body): - matches[rev].setdefault(fn, {}) + matches[rev].setdefault(fn, []) m = matches[rev][fn] for lnum, cstart, cend, line in matchlines(body): s = linestate(line, lnum, cstart, cend) - m[s] = s - - # FIXME: prev isn't used, why ? + m.append(s) + + def difflinestates(a, b): + sm = difflib.SequenceMatcher(None, a, b) + for tag, alo, ahi, blo, bhi in sm.get_opcodes(): + if tag == 'insert': + for i in range(blo, bhi): + yield ('+', b[i]) + elif tag == 'delete': + for i in range(alo, ahi): + yield ('-', a[i]) + elif tag == 'replace': + for i in range(alo, ahi): + yield ('-', a[i]) + for i in range(blo, bhi): + yield ('+', b[i]) + prev = {} ucache = {} def display(fn, rev, states, prevstates): - diff = list(sets.Set(states).symmetric_difference(sets.Set(prevstates))) - diff.sort(lambda x, y: cmp(x.linenum, y.linenum)) counts = {'-': 0, '+': 0} filerevmatches = {} - for l in diff: + if incrementing or not opts['all']: + a, b = prevstates, states + else: + a, b = states, prevstates + for change, l in difflinestates(a, b): if incrementing or not opts['all']: - change = ((l in prevstates) and '-') or '+' r = rev else: - change = ((l in states) and '-') or '+' r = prev[fn] - cols = [fn, str(rev)] + cols = [fn, str(r)] if opts['line_number']: cols.append(str(l.linenum)) if opts['all']: cols.append(change) if opts['user']: - cols.append(trimuser(ui, getchange(rev)[1], rev, - ucache)) + cols.append(trimuser(ui, getchange(r)[1], rev, + ucache)) if opts['files_with_matches']: c = (fn, rev) if c in filerevmatches: @@ -1596,6 +1515,7 @@ def grep(ui, repo, pattern, *pats, **opt changeiter, getchange, matchfn = walkchangerevs(ui, repo, pats, opts) count = 0 incrementing = False + follow = opts.get('follow') for st, rev, fns in changeiter: if st == 'window': incrementing = rev @@ -1610,20 +1530,31 @@ def grep(ui, repo, pattern, *pats, **opt fstate.setdefault(fn, {}) try: grepbody(fn, rev, getfile(fn).read(mf[fn])) + if follow: + copied = getfile(fn).renamed(mf[fn]) + if copied: + copies.setdefault(rev, {})[fn] = copied[0] except KeyError: pass elif st == 'iter': states = matches[rev].items() states.sort() for fn, m in states: + copy = copies.get(rev, {}).get(fn) if fn in skip: + if copy: + skip[copy] = True continue if incrementing or not opts['all'] or fstate[fn]: pos, neg = display(fn, rev, m, fstate[fn]) count += pos + neg if pos and not opts['all']: skip[fn] = True + if copy: + skip[copy] = True fstate[fn] = m + if copy: + fstate[copy] = m prev[fn] = rev if not incrementing: @@ -1632,7 +1563,8 @@ def grep(ui, repo, pattern, *pats, **opt for fn, state in fstate: if fn in skip: continue - display(fn, rev, {}, state) + if fn not in copies.get(prev[fn], {}): + display(fn, rev, {}, state) return (count == 0 and 1) or 0 def heads(ui, repo, **opts): @@ -1670,7 +1602,7 @@ def identify(ui, repo): return hexfunc = ui.verbose and hex or short - modified, added, removed, deleted, unknown = repo.changes() + modified, added, removed, deleted = repo.status()[:4] output = ["%s%s" % ('+'.join([hexfunc(parent) for parent in parents]), (modified or added or removed or deleted) and "+" or "")] @@ -1714,81 +1646,23 @@ def import_(ui, repo, patch1, *patches, d = opts["base"] strip = opts["strip"] - mailre = re.compile(r'(?:From |[\w-]+:)') - - # attempt to detect the start of a patch - # (this heuristic is borrowed from quilt) - diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' + - 'retrieving revision [0-9]+(\.[0-9]+)*$|' + - '(---|\*\*\*)[ \t])', re.MULTILINE) - - for patch in patches: - pf = os.path.join(d, patch) - - message = None - user = None - date = None - hgpatch = False - - p = email.Parser.Parser() + wlock = repo.wlock() + lock = repo.lock() + + for p in patches: + pf = os.path.join(d, p) + if pf == '-': - msg = p.parse(sys.stdin) ui.status(_("applying patch from stdin\n")) + tmpname, message, user, date = patch.extract(ui, sys.stdin) else: - msg = p.parse(file(pf)) - ui.status(_("applying %s\n") % patch) - - fd, tmpname = tempfile.mkstemp(prefix='hg-patch-') - tmpfp = os.fdopen(fd, 'w') + ui.status(_("applying %s\n") % p) + tmpname, message, user, date = patch.extract(ui, file(pf)) + + if tmpname is None: + raise util.Abort(_('no diffs found')) + try: - message = msg['Subject'] - if message: - message = message.replace('\n\t', ' ') - ui.debug('Subject: %s\n' % message) - user = msg['From'] - if user: - ui.debug('From: %s\n' % user) - diffs_seen = 0 - ok_types = ('text/plain', 'text/x-patch') - for part in msg.walk(): - content_type = part.get_content_type() - ui.debug('Content-Type: %s\n' % content_type) - if content_type not in ok_types: - continue - payload = part.get_payload(decode=True) - m = diffre.search(payload) - if m: - ui.debug(_('found patch at byte %d\n') % m.start(0)) - diffs_seen += 1 - hgpatch = False - fp = cStringIO.StringIO() - if message: - fp.write(message) - fp.write('\n') - for line in payload[:m.start(0)].splitlines(): - if line.startswith('# HG changeset patch'): - ui.debug(_('patch generated by hg export\n')) - hgpatch = True - # drop earlier commit message content - fp.seek(0) - fp.truncate() - elif hgpatch: - if line.startswith('# User '): - user = line[7:] - ui.debug('From: %s\n' % user) - elif line.startswith("# Date "): - date = line[7:] - if not line.startswith('# '): - fp.write(line) - fp.write('\n') - message = fp.getvalue() - if tmpfp: - tmpfp.write(payload) - if not payload.endswith('\n'): - tmpfp.write('\n') - elif not diffs_seen and message and content_type == 'text/plain': - message += '\n' + payload - if opts['message']: # pickup the cmdline msg message = opts['message'] @@ -1800,14 +1674,9 @@ def import_(ui, repo, patch1, *patches, message = None ui.debug(_('message:\n%s\n') % message) - tmpfp.close() - if not diffs_seen: - raise util.Abort(_('no diffs found')) - - files = util.patch(strip, tmpname, ui) - if len(files) > 0: - addremove_lock(ui, repo, files, {}) - repo.commit(files, message, user, date) + files, fuzz = patch.patch(tmpname, ui, strip=strip, cwd=repo.root) + files = patch.updatedir(ui, repo, files, wlock=wlock) + repo.commit(files, message, user, date, wlock=wlock, lock=lock) finally: os.unlink(tmpname) @@ -1824,7 +1693,7 @@ def incoming(ui, repo, source="default", See pull for valid source format details. """ source = ui.expandpath(source) - ui.setconfig_remoteopts(**opts) + setremoteconfig(ui, opts) other = hg.repository(ui, source) incoming = repo.findincoming(other, force=opts["force"]) @@ -1860,7 +1729,7 @@ def incoming(ui, repo, source="default", displayer.show(changenode=n) if opts['patch']: prev = (parents and parents[0]) or nullid - dodiff(ui, ui, other, prev, n) + patch.diff(repo, other, prev, n) ui.write("\n") finally: if hasattr(other, 'close'): @@ -1880,7 +1749,7 @@ def init(ui, dest=".", **opts): Look at the help text for the pull command for important details about ssh:// URLs. """ - ui.setconfig_remoteopts(**opts) + setremoteconfig(ui, opts) hg.repository(ui, dest, create=1) def locate(ui, repo, *pats, **opts): @@ -1908,8 +1777,8 @@ def locate(ui, repo, *pats, **opts): else: node = None - for src, abs, rel, exact in walk(repo, pats, opts, node=node, - head='(?:.*/|)'): + for src, abs, rel, exact in cmdutil.walk(repo, pats, opts, node=node, + head='(?:.*/|)'): if not node and repo.dirstate.state(abs) == '?': continue if opts['fullpath']: @@ -1920,7 +1789,18 @@ def locate(ui, repo, *pats, **opts): def log(ui, repo, *pats, **opts): """show revision history of entire repository or files - Print the revision history of the specified files or the entire project. + Print the revision history of the specified files or the entire + project. + + File history is shown without following rename or copy history of + files. Use -f/--follow with a file name to follow history across + renames and copies. --follow without a file name will only show + ancestors or descendants of the starting revision. --follow-first + only follows the first parent of merge revisions. + + If no revision range is specified, the default is tip:0 unless + --follow is set, in which case the working directory parent is + used as the starting revision. By default this command outputs: changeset id and hash, tags, non-trivial parents, user, date and time, and a summary for each @@ -2000,7 +1880,7 @@ def log(ui, repo, *pats, **opts): displayer.show(rev, brinfo=br) if opts['patch']: prev = (parents and parents[0]) or nullid - dodiff(du, du, repo, prev, changenode, match=matchfn) + patch.diff(repo, prev, changenode, match=matchfn, fp=du) du.write("\n\n") elif st == 'iter': if count == limit: break @@ -2031,22 +1911,44 @@ def manifest(ui, repo, rev=None): else: n = repo.manifest.tip() m = repo.manifest.read(n) - mf = repo.manifest.readflags(n) files = m.keys() files.sort() for f in files: - ui.write("%40s %3s %s\n" % (hex(m[f]), mf[f] and "755" or "644", f)) - -def merge(ui, repo, node=None, **opts): + ui.write("%40s %3s %s\n" % (hex(m[f]), + m.execf(f) and "755" or "644", f)) + +def merge(ui, repo, node=None, force=None, branch=None): """Merge working directory with another revision Merge the contents of the current working directory and the requested revision. Files that changed between either parent are marked as changed for the next commit and a commit must be performed before any further updates are allowed. + + If no revision is specified, the working directory's parent is a + head revision, and the repository contains exactly one other head, + the other head is merged with by default. Otherwise, an explicit + revision to merge with must be provided. """ - return doupdate(ui, repo, node=node, merge=True, **opts) + + if node: + node = _lookup(repo, node, branch) + else: + heads = repo.heads() + if len(heads) > 2: + raise util.Abort(_('repo has %d heads - ' + 'please merge with an explicit rev') % + len(heads)) + if len(heads) == 1: + raise util.Abort(_('there is nothing to merge - ' + 'use "hg update" instead')) + parent = repo.dirstate.parents()[0] + if parent not in heads: + raise util.Abort(_('working dir not at a head rev - ' + 'use "hg update" or merge with an explicit rev')) + node = parent == heads[0] and heads[-1] or heads[0] + return hg.merge(repo, node, force=force) def outgoing(ui, repo, dest=None, **opts): """show changesets not found in destination @@ -2058,7 +1960,7 @@ def outgoing(ui, repo, dest=None, **opts See pull for valid destination format details. """ dest = ui.expandpath(dest or 'default-push', dest or 'default') - ui.setconfig_remoteopts(**opts) + setremoteconfig(ui, opts) revs = None if opts['rev']: revs = [repo.lookup(rev) for rev in opts['rev']] @@ -2079,16 +1981,31 @@ def outgoing(ui, repo, dest=None, **opts displayer.show(changenode=n) if opts['patch']: prev = (parents and parents[0]) or nullid - dodiff(ui, ui, repo, prev, n) + patch.diff(repo, prev, n) ui.write("\n") -def parents(ui, repo, rev=None, branches=None, **opts): +def parents(ui, repo, file_=None, rev=None, branches=None, **opts): """show the parents of the working dir or revision Print the working directory's parent revisions. """ + # legacy + if file_ and not rev: + try: + rev = repo.lookup(file_) + file_ = None + except hg.RepoError: + pass + else: + ui.warn(_("'hg parent REV' is deprecated, " + "please use 'hg parents -r REV instead\n")) + if rev: - p = repo.changelog.parents(repo.lookup(rev)) + if file_: + ctx = repo.filectx(file_, changeid=rev) + else: + ctx = repo.changectx(rev) + p = [cp.node() for cp in ctx.parents()] else: p = repo.dirstate.parents() @@ -2125,7 +2042,7 @@ def postincoming(ui, repo, modheads, opt return if optupdate: if modheads == 1: - return doupdate(ui, repo) + return hg.update(repo, repo.changelog.tip()) # update else: ui.status(_("not updating, since new heads added\n")) if modheads > 1: @@ -2165,7 +2082,7 @@ def pull(ui, repo, source="default", **o with the --ssh command line option. """ source = ui.expandpath(source) - ui.setconfig_remoteopts(**opts) + setremoteconfig(ui, opts) other = hg.repository(ui, source) ui.status(_('pulling from %s\n') % (source)) @@ -2203,7 +2120,7 @@ def push(ui, repo, dest=None, **opts): feature is enabled on the remote Mercurial server. """ dest = ui.expandpath(dest or 'default-push', dest or 'default') - ui.setconfig_remoteopts(**opts) + setremoteconfig(ui, opts) other = hg.repository(ui, dest) ui.status('pushing to %s\n' % (dest)) @@ -2257,7 +2174,7 @@ def recover(ui, repo): operation. It should only be necessary when Mercurial suggests it. """ if repo.recover(): - return repo.verify() + return hg.verify(repo) return 1 def remove(ui, repo, *pats, **opts): @@ -2277,12 +2194,12 @@ def remove(ui, repo, *pats, **opts): names = [] if not opts['after'] and not pats: raise util.Abort(_('no files specified')) - files, matchfn, anypats = matchpats(repo, pats, opts) + files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) exact = dict.fromkeys(files) - mardu = map(dict.fromkeys, repo.changes(files=files, match=matchfn)) + mardu = map(dict.fromkeys, repo.status(files=files, match=matchfn))[:5] modified, added, removed, deleted, unknown = mardu remove, forget = [], [] - for src, abs, rel, exact in walk(repo, pats, opts): + for src, abs, rel, exact in cmdutil.walk(repo, pats, opts): reason = None if abs not in deleted and opts['after']: reason = _('is still present') @@ -2389,20 +2306,21 @@ def revert(ui, repo, *pats, **opts): # walk dirstate. - for src, abs, rel, exact in walk(repo, pats, opts, badmatch=mf.has_key): + for src, abs, rel, exact in cmdutil.walk(repo, pats, opts, + badmatch=mf.has_key): names[abs] = (rel, exact) if src == 'b': target_only[abs] = True # walk target manifest. - for src, abs, rel, exact in walk(repo, pats, opts, node=node, - badmatch=names.has_key): + for src, abs, rel, exact in cmdutil.walk(repo, pats, opts, node=node, + badmatch=names.has_key): if abs in names: continue names[abs] = (rel, exact) target_only[abs] = True - changes = repo.changes(match=names.has_key, wlock=wlock) + changes = repo.status(match=names.has_key, wlock=wlock)[:5] modified, added, removed, deleted, unknown = map(dict.fromkeys, changes) revert = ([], _('reverting %s\n')) @@ -2474,8 +2392,7 @@ def revert(ui, repo, *pats, **opts): if not opts.get('dry_run'): repo.dirstate.forget(forget[0]) - r = repo.update(node, False, True, update.has_key, False, wlock=wlock, - show_stats=False) + r = hg.revert(repo, node, update.has_key, wlock) repo.dirstate.update(add[0], 'a') repo.dirstate.update(undelete[0], 'n') repo.dirstate.update(remove[0], 'r') @@ -2593,37 +2510,44 @@ def serve(ui, repo, **opts): def status(ui, repo, *pats, **opts): """show changed files in the working directory - Show changed files in the repository. If names are - given, only files that match are shown. + Show status of files in the repository. If names are given, only + files that match are shown. Files that are clean or ignored, are + not listed unless -c (clean), -i (ignored) or -A is given. The codes used to show the status of files are: M = modified A = added R = removed + C = clean ! = deleted, but still tracked ? = not tracked I = ignored (not shown by default) = the previous added file was copied from here """ - show_ignored = opts['ignored'] and True or False - files, matchfn, anypats = matchpats(repo, pats, opts) + all = opts['all'] + + files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) cwd = (pats and repo.getcwd()) or '' - modified, added, removed, deleted, unknown, ignored = [ + modified, added, removed, deleted, unknown, ignored, clean = [ [util.pathto(cwd, x) for x in n] - for n in repo.changes(files=files, match=matchfn, - show_ignored=show_ignored)] - - changetypes = [('modified', 'M', modified), + for n in repo.status(files=files, match=matchfn, + list_ignored=all or opts['ignored'], + list_clean=all or opts['clean'])] + + changetypes = (('modified', 'M', modified), ('added', 'A', added), ('removed', 'R', removed), ('deleted', '!', deleted), ('unknown', '?', unknown), - ('ignored', 'I', ignored)] + ('ignored', 'I', ignored)) + + explicit_changetypes = changetypes + (('clean', 'C', clean),) end = opts['print0'] and '\0' or '\n' - for opt, char, changes in ([ct for ct in changetypes if opts[ct[0]]] + for opt, char, changes in ([ct for ct in explicit_changetypes + if all or opts[ct[0]]] or changetypes): if opts['no_status']: format = "%%s%s" % end @@ -2632,7 +2556,7 @@ def status(ui, repo, *pats, **opts): for f in changes: ui.write(format % f) - if (opts.get('copies') and not opts.get('no_status') + if ((all or opts.get('copies')) and not opts.get('no_status') and opt == 'added' and repo.dirstate.copies.has_key(f)): ui.write(' %s%s' % (repo.dirstate.copies[f], end)) @@ -2645,7 +2569,7 @@ def tag(ui, repo, name, rev_=None, **opt very useful to compare different revision, to go back to significant earlier versions or to mark branch points as releases, etc. - If no revision is given, the tip is used. + If no revision is given, the parent of the working directory is used. To facilitate version control, distribution, and merging of tags, they are stored as a file named ".hgtags" which is managed @@ -2653,8 +2577,8 @@ def tag(ui, repo, name, rev_=None, **opt necessary. The file '.hg/localtags' is used for local tags (not shared among repositories). """ - if name == "tip": - raise util.Abort(_("the name 'tip' is reserved")) + if name in ['tip', '.']: + raise util.Abort(_("the name '%s' is reserved") % name) if rev_ is not None: ui.warn(_("use of 'hg tag NAME [REV]' is deprecated, " "please use 'hg tag [-r REV] NAME' instead\n")) @@ -2665,7 +2589,12 @@ def tag(ui, repo, name, rev_=None, **opt if rev_: r = hex(repo.lookup(rev_)) else: - r = hex(repo.changelog.tip()) + p1, p2 = repo.dirstate.parents() + if p1 == nullid: + raise util.Abort(_('no revision to tag')) + if p2 != nullid: + raise util.Abort(_('outstanding uncommitted merges')) + r = hex(p1) repo.tag(name, r, opts['local'], opts['message'], opts['user'], opts['date']) @@ -2701,7 +2630,7 @@ def tip(ui, repo, **opts): br = repo.branchlookup([n]) show_changeset(ui, repo, opts).show(changenode=n, brinfo=br) if opts['patch']: - dodiff(ui, ui, repo, repo.changelog.parents(n)[0], n) + patch.diff(repo, repo.changelog.parents(n)[0], n) def unbundle(ui, repo, fname, **opts): """apply a changegroup file @@ -2730,7 +2659,8 @@ def unbundle(ui, repo, fname, **opts): raise util.Abort(_("%s: unknown bundle compression type") % fname) gen = generator(util.filechunkiter(f, 4096)) - modheads = repo.addchangegroup(util.chunkbuffer(gen), 'unbundle') + modheads = repo.addchangegroup(util.chunkbuffer(gen), 'unbundle', + 'bundle:' + fname) return postincoming(ui, repo, modheads, opts['update']) def undo(ui, repo): @@ -2745,7 +2675,7 @@ def undo(ui, repo): repo.rollback() def update(ui, repo, node=None, merge=False, clean=False, force=None, - branch=None, **opts): + branch=None): """update or merge working directory Update the working directory to the specified revision. @@ -2760,13 +2690,17 @@ def update(ui, repo, node=None, merge=Fa By default, update will refuse to run if doing so would require merging or discarding local changes. """ + node = _lookup(repo, node, branch) if merge: ui.warn(_('(the -m/--merge option is deprecated; ' 'use the merge command instead)\n')) - return doupdate(ui, repo, node, merge, clean, force, branch, **opts) - -def doupdate(ui, repo, node=None, merge=False, clean=False, force=None, - branch=None, **opts): + return hg.merge(repo, node, force=force) + elif clean: + return hg.clean(repo, node) + else: + return hg.update(repo, node) + +def _lookup(repo, node, branch=None): if branch: br = repo.branchlookup(branch=branch) found = [] @@ -2774,19 +2708,19 @@ def doupdate(ui, repo, node=None, merge= if branch in br[x]: found.append(x) if len(found) > 1: - ui.warn(_("Found multiple heads for %s\n") % branch) + repo.ui.warn(_("Found multiple heads for %s\n") % branch) for x in found: - show_changeset(ui, repo, opts).show(changenode=x, brinfo=br) - return 1 + show_changeset(ui, repo, {}).show(changenode=x, brinfo=br) + raise util.Abort("") if len(found) == 1: node = found[0] - ui.warn(_("Using head %s for branch %s\n") % (short(node), branch)) + repo.ui.warn(_("Using head %s for branch %s\n") + % (short(node), branch)) else: - ui.warn(_("branch %s not found\n") % (branch)) - return 1 + raise util.Abort(_("branch %s not found\n") % (branch)) else: node = node and repo.lookup(node) or repo.changelog.tip() - return repo.update(node, allow=merge, force=clean, forcemerge=force) + return node def verify(ui, repo): """verify the integrity of the repository @@ -2798,7 +2732,7 @@ def verify(ui, repo): the changelog, manifest, and tracked files, as well as the integrity of their crosslinks and indices. """ - return repo.verify() + return hg.verify(repo) # Command options and aliases are listed here, alphabetically @@ -2919,6 +2853,7 @@ table = { ('a', 'text', None, _('treat all files as text')), ('p', 'show-function', None, _('show which function each change is in')), + ('g', 'git', None, _('use git extended diff format')), ('w', 'ignore-all-space', None, _('ignore white space when comparing lines')), ('b', 'ignore-space-change', None, @@ -2943,6 +2878,8 @@ table = { (grep, [('0', 'print0', None, _('end fields with NUL')), ('', 'all', None, _('print all revisions that match')), + ('f', 'follow', None, + _('follow changeset history, or file history across copies and renames')), ('i', 'ignore-case', None, _('ignore case when matching')), ('l', 'files-with-matches', None, _('print only filenames and revs that match')), @@ -2979,7 +2916,7 @@ table = { ('n', 'newest-first', None, _('show newest record first')), ('', 'bundle', '', _('file to store the bundles into')), ('p', 'patch', None, _('show patch')), - ('r', 'rev', [], _('a specific revision you would like to pull')), + ('r', 'rev', [], _('a specific revision up to which you would like to pull')), ('', 'template', '', _('display with template')), ('e', 'ssh', '', _('specify ssh command to use')), ('', 'remotecmd', '', @@ -3005,6 +2942,10 @@ table = { "^log|history": (log, [('b', 'branches', None, _('show branches')), + ('f', 'follow', None, + _('follow changeset history, or file history across copies and renames')), + ('', 'follow-first', None, + _('only follow the first parent of merge changesets')), ('k', 'keyword', [], _('search for a keyword')), ('l', 'limit', '', _('limit number of changes displayed')), ('r', 'rev', [], _('show the specified revision or range')), @@ -3012,6 +2953,7 @@ table = { ('', 'style', '', _('display using template map file')), ('m', 'only-merges', None, _('show only merges')), ('p', 'patch', None, _('show patch')), + ('P', 'prune', [], _('do not display revision or any of its ancestors')), ('', 'template', '', _('display with template')), ('I', 'include', [], _('include names matching the given patterns')), ('X', 'exclude', [], _('exclude names matching the given patterns'))], @@ -3038,9 +2980,10 @@ table = { "^parents": (parents, [('b', 'branches', None, _('show branches')), + ('r', 'rev', '', _('show parents from the specified rev')), ('', 'style', '', _('display using template map file')), ('', 'template', '', _('display with template'))], - _('hg parents [-b] [REV]')), + _('hg parents [-b] [-r REV] [FILE]')), "paths": (paths, [], _('hg paths [NAME]')), "^pull": (pull, @@ -3049,7 +2992,7 @@ table = { ('e', 'ssh', '', _('specify ssh command to use')), ('f', 'force', None, _('run even when remote repository is unrelated')), - ('r', 'rev', [], _('a specific revision you would like to pull')), + ('r', 'rev', [], _('a specific revision up to which you would like to pull')), ('', 'remotecmd', '', _('specify hg command to run on the remote side'))], _('hg pull [-u] [-r REV]... [-e FILE] [--remotecmd FILE] [SOURCE]')), @@ -3117,10 +3060,12 @@ table = { _('hg serve [OPTION]...')), "^status|st": (status, - [('m', 'modified', None, _('show only modified files')), + [('A', 'all', None, _('show status of all files')), + ('m', 'modified', None, _('show only modified files')), ('a', 'added', None, _('show only added files')), ('r', 'removed', None, _('show only removed files')), ('d', 'deleted', None, _('show only deleted (but tracked) files')), + ('c', 'clean', None, _('show only files without changes')), ('u', 'unknown', None, _('show only unknown (not tracked) files')), ('i', 'ignored', None, _('show ignored files')), ('n', 'no-status', None, _('hide status prefix')), @@ -3286,24 +3231,16 @@ def findext(name): try: return sys.modules[external[name]] except KeyError: - dotname = '.' + name for k, v in external.iteritems(): - if k.endswith('.' + name) or v == name: + if k.endswith('.' + name) or k.endswith('/' + name) or v == name: return sys.modules[v] raise KeyError(name) -def dispatch(args): - for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM': - num = getattr(signal, name, None) - if num: signal.signal(num, catchterm) - - try: - u = ui.ui(traceback='--traceback' in sys.argv[1:]) - except util.Abort, inst: - sys.stderr.write(_("abort: %s\n") % inst) - return -1 - - for ext_name, load_from_name in u.extensions(): +def load_extensions(ui): + added = [] + for ext_name, load_from_name in ui.extensions(): + if ext_name in external: + continue try: if load_from_name: # the module will be loaded in sys.modules @@ -3323,23 +3260,36 @@ def dispatch(args): except ImportError: mod = importh(ext_name) external[ext_name] = mod.__name__ + added.append((mod, ext_name)) except (util.SignalInterrupt, KeyboardInterrupt): raise except Exception, inst: - u.warn(_("*** failed to import extension %s: %s\n") % (ext_name, inst)) - if u.print_exc(): + ui.warn(_("*** failed to import extension %s: %s\n") % + (ext_name, inst)) + if ui.print_exc(): return 1 - for name in external.itervalues(): - mod = sys.modules[name] + for mod, name in added: uisetup = getattr(mod, 'uisetup', None) if uisetup: - uisetup(u) + uisetup(ui) cmdtable = getattr(mod, 'cmdtable', {}) for t in cmdtable: if t in table: - u.warn(_("module %s overrides %s\n") % (name, t)) + ui.warn(_("module %s overrides %s\n") % (name, t)) table.update(cmdtable) + +def dispatch(args): + for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM': + num = getattr(signal, name, None) + if num: signal.signal(num, catchterm) + + try: + u = ui.ui(traceback='--traceback' in sys.argv[1:], + readhooks=[load_extensions]) + except util.Abort, inst: + sys.stderr.write(_("abort: %s\n") % inst) + return -1 try: cmd, func, args, options, cmdoptions = parse(u, args) @@ -3391,6 +3341,7 @@ def dispatch(args): mod = sys.modules[name] if hasattr(mod, 'reposetup'): mod.reposetup(u, repo) + hg.repo_setup_hooks.append(mod.reposetup) except hg.RepoError: if cmd not in optionalrepo.split(): raise @@ -3398,6 +3349,11 @@ def dispatch(args): else: d = lambda: func(u, *args, **cmdoptions) + # reupdate the options, repo/.hg/hgrc may have changed them + u.updateopts(options["verbose"], options["debug"], options["quiet"], + not options["noninteractive"], options["traceback"], + options["config"]) + try: if options['profile']: import hotshot, hotshot.stats diff --git a/mercurial/context.py b/mercurial/context.py --- a/mercurial/context.py +++ b/mercurial/context.py @@ -1,6 +1,6 @@ # context.py - changeset and file context objects for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -11,9 +11,8 @@ class changectx(object): def __init__(self, repo, changeid): """changeid is a revision number, node, or tag""" self._repo = repo - self._id = changeid - self._node = self._repo.lookup(self._id) + self._node = self._repo.lookup(changeid) self._rev = self._repo.changelog.rev(self._node) def changeset(self): @@ -74,39 +73,40 @@ class filectx(object): fileid can be a file revision or node.""" self._repo = repo self._path = path - self._id = changeid - self._fileid = fileid - if self._id: + assert changeid or fileid + + if not fileid: # if given a changeset id, go ahead and look up the file - self._changeset = self._repo.changelog.read(self._id) - node, flag = self._repo.manifest.find(self._changeset[0], path) + self._changeid = changeid + self._changectx = self.changectx() self._filelog = self._repo.file(self._path) - self._filenode = node - elif self._fileid: + self._filenode = self._changectx.filenode(self._path) + else: # else be lazy self._filelog = self._repo.file(self._path) - self._filenode = self._filelog.lookup(self._fileid) + self._filenode = self._filelog.lookup(fileid) + self._changeid = self._filelog.linkrev(self._filenode) self._filerev = self._filelog.rev(self._filenode) - def changeset(self): + def changectx(self): try: - return self._changeset + return self._changectx except AttributeError: - self._changeset = self._repo.changelog.read(self.node()) - return self._changeset + self._changectx = changectx(self._repo, self._changeid) + return self._changectx def filerev(self): return self._filerev def filenode(self): return self._filenode def filelog(self): return self._filelog - def rev(self): return self.changeset().rev() - def node(self): return self.changeset().node() - def user(self): return self.changeset().user() - def date(self): return self.changeset().date() - def files(self): return self.changeset().files() - def description(self): return self.changeset().description() - def manifest(self): return self.changeset().manifest() + def rev(self): return self.changectx().rev() + def node(self): return self.changectx().node() + def user(self): return self.changectx().user() + def date(self): return self.changectx().date() + def files(self): return self.changectx().files() + def description(self): return self.changectx().description() + def manifest(self): return self.changectx().manifest() def data(self): return self._filelog.read(self._filenode) def metadata(self): return self._filelog.readmeta(self._filenode) diff --git a/mercurial/demandload.py b/mercurial/demandload.py --- a/mercurial/demandload.py +++ b/mercurial/demandload.py @@ -96,6 +96,7 @@ def demandload(scope, modules): foo import foo foo bar import foo, bar + foo@bar import foo as bar foo.bar import foo.bar foo:bar from foo import bar foo:bar,quux from foo import bar, quux @@ -108,6 +109,9 @@ def demandload(scope, modules): mod = mod[:col] else: fromlist = [] + as = None + if '@' in mod: + mod, as = mod.split("@") importer = _importer(scope, mod, fromlist) if fromlist: for name in fromlist: @@ -126,4 +130,6 @@ def demandload(scope, modules): continue else: basemod = mod - scope[basemod] = _replacer(importer, basemod) + if not as: + as = basemod + scope[as] = _replacer(importer, as) diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py --- a/mercurial/dirstate.py +++ b/mercurial/dirstate.py @@ -1,7 +1,7 @@ """ dirstate.py - working directory tracking for mercurial -Copyright 2005 Matt Mackall +Copyright 2005, 2006 Matt Mackall This software may be used and distributed according to the terms of the GNU General Public License, incorporated herein by reference. @@ -10,7 +10,7 @@ of the GNU General Public License, incor from node import * from i18n import gettext as _ from demandload import * -demandload(globals(), "struct os time bisect stat util re errno") +demandload(globals(), "struct os time bisect stat strutil util re errno") class dirstate(object): format = ">cllll" @@ -22,6 +22,7 @@ class dirstate(object): self.ui = ui self.map = None self.pl = None + self.dirs = None self.copies = {} self.ignorefunc = None self.blockignore = False @@ -197,6 +198,38 @@ class dirstate(object): def copied(self, file): return self.copies.get(file, None) + def initdirs(self): + if self.dirs is None: + self.dirs = {} + for f in self.map: + self.updatedirs(f, 1) + + def updatedirs(self, path, delta): + if self.dirs is not None: + for c in strutil.findall(path, '/'): + pc = path[:c] + self.dirs.setdefault(pc, 0) + self.dirs[pc] += delta + + def checkshadows(self, files): + def prefixes(f): + for c in strutil.rfindall(f, '/'): + yield f[:c] + self.lazyread() + self.initdirs() + seendirs = {} + for f in files: + if self.dirs.get(f): + raise util.Abort(_('directory named %r already in dirstate') % + f) + for d in prefixes(f): + if d in seendirs: + break + if d in self.map: + raise util.Abort(_('file named %r already in dirstate') % + d) + seendirs[d] = True + def update(self, files, state, **kw): ''' current states: n normal @@ -207,10 +240,16 @@ class dirstate(object): if not files: return self.lazyread() self.markdirty() + if state == "a": + self.initdirs() + self.checkshadows(files) for f in files: if state == "r": self.map[f] = ('r', 0, 0, 0) + self.updatedirs(f, -1) else: + if state == "a": + self.updatedirs(f, 1) s = os.lstat(self.wjoin(f)) st_size = kw.get('st_size', s.st_size) st_mtime = kw.get('st_mtime', s.st_mtime) @@ -222,9 +261,11 @@ class dirstate(object): if not files: return self.lazyread() self.markdirty() + self.initdirs() for f in files: try: del self.map[f] + self.updatedirs(f, -1) except KeyError: self.ui.warn(_("not in dirstate: %s!\n") % f) pass @@ -232,14 +273,15 @@ class dirstate(object): def clear(self): self.map = {} self.copies = {} + self.dirs = None self.markdirty() def rebuild(self, parent, files): self.clear() umask = os.umask(0) os.umask(umask) - for f, mode in files: - if mode: + for f in files: + if files.execf(f): self.map[f] = ('n', ~umask, -1, 0) else: self.map[f] = ('n', ~umask & 0666, -1, 0) @@ -344,6 +386,10 @@ class dirstate(object): # directly by this function, but might be modified by your statmatch call. # def walkhelper(self, files, statmatch, dc, badmatch=None): + # self.root may end with a path separator when self.root == '/' + common_prefix_len = len(self.root) + if not self.root.endswith('/'): + common_prefix_len += 1 # recursion free walker, faster than os.walk. def findfiles(s): work = [s] @@ -352,7 +398,7 @@ class dirstate(object): names = os.listdir(top) names.sort() # nd is the top of the repository dir tree - nd = util.normpath(top[len(self.root) + 1:]) + nd = util.normpath(top[common_prefix_len:]) if nd == '.': nd = '' else: @@ -434,15 +480,16 @@ class dirstate(object): if not seen(k) and (statmatch(k, None)): yield 'm', k, None - def changes(self, files=None, match=util.always, show_ignored=None): + def status(self, files=None, match=util.always, list_ignored=False, + list_clean=False): lookup, modified, added, unknown, ignored = [], [], [], [], [] - removed, deleted = [], [] + removed, deleted, clean = [], [], [] - for src, fn, st in self.statwalk(files, match, ignored=show_ignored): + for src, fn, st in self.statwalk(files, match, ignored=list_ignored): try: type_, mode, size, time = self[fn] except KeyError: - if show_ignored and self.ignore(fn): + if list_ignored and self.ignore(fn): ignored.append(fn) else: unknown.append(fn) @@ -473,6 +520,8 @@ class dirstate(object): modified.append(fn) elif time != st.st_mtime: lookup.append(fn) + elif list_clean: + clean.append(fn) elif type_ == 'm': modified.append(fn) elif type_ == 'a': @@ -480,4 +529,5 @@ class dirstate(object): elif type_ == 'r': removed.append(fn) - return (lookup, modified, added, removed, deleted, unknown, ignored) + return (lookup, modified, added, removed, deleted, unknown, ignored, + clean) diff --git a/mercurial/filelog.py b/mercurial/filelog.py --- a/mercurial/filelog.py +++ b/mercurial/filelog.py @@ -1,6 +1,6 @@ # filelog.py - file history class for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -65,6 +65,26 @@ class filelog(revlog): return (m["copy"], bin(m["copyrev"])) return False + def size(self, rev): + """return the size of a given revision""" + + # for revisions with renames, we have to go the slow way + node = self.node(rev) + if self.renamed(node): + return len(self.read(node)) + + return revlog.size(self, rev) + + def cmp(self, node, text): + """compare text with a given file revision""" + + # for renames, we have to go the slow way + if self.renamed(node): + t2 = self.read(node) + return t2 != text + + return revlog.cmp(self, node, text) + def annotate(self, node): def decorate(text, rev): @@ -76,31 +96,59 @@ class filelog(revlog): return child # find all ancestors - needed = {node:1} - visit = [node] + needed = {(self, node):1} + files = [self] + visit = [(self, node)] while visit: - n = visit.pop(0) - for p in self.parents(n): - if p not in needed: - needed[p] = 1 - visit.append(p) + f, n = visit.pop(0) + rn = f.renamed(n) + if rn: + f, n = rn + f = filelog(self.opener, f, self.defversion) + files.insert(0, f) + if (f, n) not in needed: + needed[(f, n)] = 1 + else: + needed[(f, n)] += 1 + for p in f.parents(n): + if p == nullid: + continue + if (f, p) not in needed: + needed[(f, p)] = 1 + visit.append((f, p)) else: # count how many times we'll use this - needed[p] += 1 + needed[(f, p)] += 1 - # sort by revision which is a topological order - visit = [ (self.rev(n), n) for n in needed.keys() ] - visit.sort() + # sort by revision (per file) which is a topological order + visit = [] + for f in files: + fn = [(f.rev(n[1]), f, n[1]) for n in needed.keys() if n[0] == f] + fn.sort() + visit.extend(fn) hist = {} - for r,n in visit: - curr = decorate(self.read(n), self.linkrev(n)) - for p in self.parents(n): + for i in range(len(visit)): + r, f, n = visit[i] + curr = decorate(f.read(n), f.linkrev(n)) + if r == -1: + continue + parents = f.parents(n) + # follow parents across renames + if r < 1 and i > 0: + j = i + while j > 0 and visit[j][1] == f: + j -= 1 + parents = (visit[j][2],) + f = visit[j][1] + else: + parents = f.parents(n) + for p in parents: if p != nullid: curr = pair(hist[p], curr) # trim the history of unneeded revs - needed[p] -= 1 - if not needed[p]: + needed[(f, p)] -= 1 + if not needed[(f, p)]: del hist[p] hist[n] = curr diff --git a/mercurial/hg.py b/mercurial/hg.py --- a/mercurial/hg.py +++ b/mercurial/hg.py @@ -1,6 +1,7 @@ # hg.py - repository classes for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall +# Copyright 2006 Vadim Gelfer # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -10,69 +11,56 @@ from repo import * from demandload import * from i18n import gettext as _ demandload(globals(), "localrepo bundlerepo httprepo sshrepo statichttprepo") -demandload(globals(), "errno lock os shutil util") - -def bundle(ui, path): - if path.startswith('bundle://'): - path = path[9:] - else: - path = path[7:] - s = path.split("+", 1) - if len(s) == 1: - repopath, bundlename = "", s[0] - else: - repopath, bundlename = s - return bundlerepo.bundlerepository(ui, repopath, bundlename) - -def hg(ui, path): - ui.warn(_("hg:// syntax is deprecated, please use http:// instead\n")) - return httprepo.httprepository(ui, path.replace("hg://", "http://")) +demandload(globals(), "errno lock os shutil util merge@_merge verify@_verify") -def local_(ui, path, create=0): - if path.startswith('file:'): - path = path[5:] - return localrepo.localrepository(ui, path, create) - -def ssh_(ui, path, create=0): - return sshrepo.sshrepository(ui, path, create) - -def old_http(ui, path): - ui.warn(_("old-http:// syntax is deprecated, " - "please use static-http:// instead\n")) - return statichttprepo.statichttprepository( - ui, path.replace("old-http://", "http://")) - -def static_http(ui, path): - return statichttprepo.statichttprepository( - ui, path.replace("static-http://", "http://")) +def _local(path): + return (os.path.isfile(path and util.drop_scheme('file', path)) and + bundlerepo or localrepo) schemes = { - 'bundle': bundle, - 'file': local_, - 'hg': hg, - 'http': lambda ui, path: httprepo.httprepository(ui, path), - 'https': lambda ui, path: httprepo.httpsrepository(ui, path), - 'old-http': old_http, - 'ssh': ssh_, - 'static-http': static_http, + 'bundle': bundlerepo, + 'file': _local, + 'hg': httprepo, + 'http': httprepo, + 'https': httprepo, + 'old-http': statichttprepo, + 'ssh': sshrepo, + 'static-http': statichttprepo, } -def repository(ui, path=None, create=0): - scheme = None +def _lookup(path): + scheme = 'file' if path: c = path.find(':') if c > 0: - scheme = schemes.get(path[:c]) - else: - path = '' - ctor = scheme or schemes['file'] - if create: + scheme = path[:c] + thing = schemes.get(scheme) or schemes['file'] + try: + return thing(path) + except TypeError: + return thing + +def islocal(repo): + '''return true if repo or path is local''' + if isinstance(repo, str): try: - return ctor(ui, path, create) - except TypeError: - raise util.Abort(_('cannot create new repository over "%s" protocol') % - scheme) - return ctor(ui, path) + return _lookup(repo).islocal(repo) + except AttributeError: + return False + return repo.local() + +repo_setup_hooks = [] + +def repository(ui, path=None, create=False): + """return a repository object for the specified path""" + repo = _lookup(path).instance(ui, path, create) + for hook in repo_setup_hooks: + hook(ui, repo) + return repo + +def defaultdest(source): + '''return default destination of clone if none is given''' + return os.path.basename(os.path.normpath(source)) def clone(ui, source, dest=None, pull=False, rev=None, update=True, stream=False): @@ -90,7 +78,9 @@ def clone(ui, source, dest=None, pull=Fa If an exception is raised, the partly cloned/updated destination repository will be deleted. - Keyword arguments: + Arguments: + + source: repository object or URL dest: URL of destination repository to create (defaults to base name of source repository) @@ -105,8 +95,24 @@ def clone(ui, source, dest=None, pull=Fa update: update working directory after clone completes, if destination is local repository """ + if isinstance(source, str): + src_repo = repository(ui, source) + else: + src_repo = source + source = src_repo.url() + if dest is None: - dest = os.path.basename(os.path.normpath(source)) + dest = defaultdest(source) + + def localpath(path): + if path.startswith('file://'): + return path[7:] + if path.startswith('file:'): + return path[5:] + return path + + dest = localpath(dest) + source = localpath(source) if os.path.exists(dest): raise util.Abort(_("destination '%s' already exists"), dest) @@ -121,8 +127,6 @@ def clone(ui, source, dest=None, pull=Fa if self.dir_: self.rmtree(self.dir_, True) - src_repo = repository(ui, source) - dest_repo = None try: dest_repo = repository(ui, dest) @@ -133,7 +137,7 @@ def clone(ui, source, dest=None, pull=Fa dest_path = None dir_cleanup = None if dest_repo.local(): - dest_path = os.path.realpath(dest) + dest_path = os.path.realpath(dest_repo.root) dir_cleanup = DirCleanup(dest_path) abspath = source @@ -202,8 +206,31 @@ def clone(ui, source, dest=None, pull=Fa dest_lock.release() if update: - dest_repo.update(dest_repo.changelog.tip()) + _merge.update(dest_repo, dest_repo.changelog.tip()) if dir_cleanup: dir_cleanup.close() return src_repo, dest_repo + +def update(repo, node): + """update the working directory to node, merging linear changes""" + return _merge.update(repo, node) + +def clean(repo, node, wlock=None, show_stats=True): + """forcibly switch the working directory to node, clobbering changes""" + return _merge.update(repo, node, force=True, wlock=wlock, + show_stats=show_stats) + +def merge(repo, node, force=None, remind=True, wlock=None): + """branch merge with node, resolving changes""" + return _merge.update(repo, node, branchmerge=True, force=force, + remind=remind, wlock=wlock) + +def revert(repo, node, choose, wlock): + """revert changes to revision in node without updating dirstate""" + return _merge.update(repo, node, force=True, partial=choose, + show_stats=False, wlock=wlock) + +def verify(repo): + """verify the consistency of a repository""" + return _verify.verify(repo) diff --git a/mercurial/hgweb/common.py b/mercurial/hgweb/common.py --- a/mercurial/hgweb/common.py +++ b/mercurial/hgweb/common.py @@ -1,7 +1,7 @@ # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod # # Copyright 21 May 2005 - (c) 2005 Jake Edge -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py --- a/mercurial/hgweb/hgweb_mod.py +++ b/mercurial/hgweb/hgweb_mod.py @@ -1,7 +1,7 @@ # hgweb/hgweb_mod.py - Web interface for a repository. # # Copyright 21 May 2005 - (c) 2005 Jake Edge -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -11,7 +11,7 @@ import os.path import mimetypes from mercurial.demandload import demandload demandload(globals(), "re zlib ConfigParser mimetools cStringIO sys tempfile") -demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,streamclone") +demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,streamclone,patch") demandload(globals(), "mercurial:templater") demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile") from mercurial.node import * @@ -37,6 +37,7 @@ class hgweb(object): self.mtime = -1 self.reponame = name self.archives = 'zip', 'gz', 'bz2' + self.stripecount = 1 self.templatepath = self.repo.ui.config("web", "templates", templater.templatepath()) @@ -46,6 +47,8 @@ class hgweb(object): self.mtime = mtime self.repo = hg.repository(self.repo.ui, self.repo.root) self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10)) + self.stripecount = int(self.repo.ui.config("web", "stripes", 1)) + self.maxshortchanges = int(self.repo.ui.config("web", "maxshortchanges", 60)) self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10)) self.allowpull = self.repo.ui.configbool("web", "allowpull", True) @@ -126,39 +129,29 @@ class hgweb(object): date1 = util.datestr(change1[2]) date2 = util.datestr(change2[2]) - modified, added, removed, deleted, unknown = r.changes(node1, node2) + modified, added, removed, deleted, unknown = r.status(node1, node2)[:5] if files: modified, added, removed = map(lambda x: filterfiles(files, x), (modified, added, removed)) - diffopts = self.repo.ui.diffopts() - showfunc = diffopts['showfunc'] - ignorews = diffopts['ignorews'] - ignorewsamount = diffopts['ignorewsamount'] - ignoreblanklines = diffopts['ignoreblanklines'] + diffopts = patch.diffopts(self.repo.ui) for f in modified: to = r.file(f).read(mmap1[f]) tn = r.file(f).read(mmap2[f]) yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, - showfunc=showfunc, ignorews=ignorews, - ignorewsamount=ignorewsamount, - ignoreblanklines=ignoreblanklines), f, tn) + opts=diffopts), f, tn) for f in added: to = None tn = r.file(f).read(mmap2[f]) yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, - showfunc=showfunc, ignorews=ignorews, - ignorewsamount=ignorewsamount, - ignoreblanklines=ignoreblanklines), f, tn) + opts=diffopts), f, tn) for f in removed: to = r.file(f).read(mmap1[f]) tn = None yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, - showfunc=showfunc, ignorews=ignorews, - ignorewsamount=ignorewsamount, - ignoreblanklines=ignoreblanklines), f, tn) + opts=diffopts), f, tn) - def changelog(self, pos): + def changelog(self, pos, shortlog=False): def changenav(**map): def seq(factor, maxchanges=None): if maxchanges: @@ -173,8 +166,9 @@ class hgweb(object): l = [] last = 0 - for f in seq(1, self.maxchanges): - if f < self.maxchanges or f <= last: + maxchanges = shortlog and self.maxshortchanges or self.maxchanges + for f in seq(1, maxchanges): + if f < maxchanges or f <= last: continue if f > count: break @@ -219,14 +213,15 @@ class hgweb(object): for e in l: yield e + maxchanges = shortlog and self.maxshortchanges or self.maxchanges cl = self.repo.changelog mf = cl.read(cl.tip())[0] count = cl.count() - start = max(0, pos - self.maxchanges + 1) - end = min(count, start + self.maxchanges) + start = max(0, pos - maxchanges + 1) + end = min(count, start + maxchanges) pos = end - 1 - yield self.t('changelog', + yield self.t(shortlog and 'shortlog' or 'changelog', changenav=changenav, manifest=hex(mf), rev=pos, changesets=count, entries=changelist, @@ -265,7 +260,7 @@ class hgweb(object): hn = hex(n) yield self.t('searchentry', - parity=count & 1, + parity=self.stripes(count), author=changes[1], parent=self.siblings(cl.parents(n), cl.rev), child=self.siblings(cl.children(n), cl.rev), @@ -376,7 +371,7 @@ class hgweb(object): for l, t in enumerate(text.splitlines(1)): yield {"line": t, "linenumber": "% 6d" % (l + 1), - "parity": l & 1} + "parity": self.stripes(l)} yield self.t("filerevision", file=f, @@ -393,7 +388,7 @@ class hgweb(object): parent=self.siblings(fl.parents(n), fl.rev, file=f), child=self.siblings(fl.children(n), fl.rev, file=f), rename=self.renamelink(fl, n), - permissions=self.repo.manifest.readflags(mfn)[f]) + permissions=self.repo.manifest.read(mfn).execf(f)) def fileannotate(self, f, node): bcache = {} @@ -409,7 +404,7 @@ class hgweb(object): mfn = cs[0] def annotate(**map): - parity = 1 + parity = 0 last = None for r, l in fl.annotate(n): try: @@ -447,7 +442,7 @@ class hgweb(object): rename=self.renamelink(fl, n), parent=self.siblings(fl.parents(n), fl.rev, file=f), child=self.siblings(fl.children(n), fl.rev, file=f), - permissions=self.repo.manifest.readflags(mfn)[f]) + permissions=self.repo.manifest.read(mfn).execf(f)) def manifest(self, mnode, path): man = self.repo.manifest @@ -457,7 +452,6 @@ class hgweb(object): rev = man.rev(mn) changerev = man.linkrev(mn) node = self.repo.changelog.node(changerev) - mff = man.readflags(mn) files = {} @@ -489,10 +483,10 @@ class hgweb(object): yield {"file": full, "manifest": mnode, "filenode": hex(fnode), - "parity": parity, + "parity": self.stripes(parity), "basename": f, - "permissions": mff[full]} - parity = 1 - parity + "permissions": mf.execf(full)} + parity += 1 def dirlist(**map): parity = 0 @@ -503,11 +497,11 @@ class hgweb(object): if fnode: continue - yield {"parity": parity, + yield {"parity": self.stripes(parity), "path": os.path.join(path, f), "manifest": mnode, "basename": f[:-1]} - parity = 1 - parity + parity += 1 yield self.t("manifest", manifest=mnode, @@ -530,12 +524,12 @@ class hgweb(object): parity = 0 for k,n in i: if notip and k == "tip": continue - yield {"parity": parity, + yield {"parity": self.stripes(parity), "tag": k, "tagmanifest": hex(cl.read(n)[0]), "date": cl.read(n)[2], "node": hex(n)} - parity = 1 - parity + parity += 1 yield self.t("tags", manifest=hex(mf), @@ -565,12 +559,12 @@ class hgweb(object): t = c[2] yield self.t("tagentry", - parity = parity, + parity = self.stripes(parity), tag = k, node = hex(n), date = t, tagmanifest = hex(m)) - parity = 1 - parity + parity += 1 def changelist(**map): parity = 0 @@ -609,7 +603,8 @@ class hgweb(object): lastchange = (0, 0), # FIXME manifest = hex(mf), tags = tagentries, - shortlog = changelist) + shortlog = changelist, + archives=self.archivelist("tip")) def filediff(self, file, changeset): cl = self.repo.changelog @@ -689,6 +684,7 @@ class hgweb(object): def expand_form(form): shortcuts = { 'cl': [('cmd', ['changelog']), ('rev', None)], + 'sl': [('cmd', ['shortlog']), ('rev', None)], 'cs': [('cmd', ['changeset']), ('node', None)], 'f': [('cmd', ['file']), ('filenode', None)], 'fl': [('cmd', ['filelog']), ('filenode', None)], @@ -752,6 +748,13 @@ class hgweb(object): else: req.write(self.t("error")) + def stripes(self, parity): + "make horizontal stripes for easier reading" + if self.stripecount: + return (1 + parity / self.stripecount) & 1 + else: + return 0 + def do_changelog(self, req): hi = self.repo.changelog.count() - 1 if req.form.has_key('rev'): @@ -764,6 +767,18 @@ class hgweb(object): req.write(self.changelog(hi)) + def do_shortlog(self, req): + hi = self.repo.changelog.count() - 1 + if req.form.has_key('rev'): + hi = req.form['rev'][0] + try: + hi = self.repo.changelog.rev(self.repo.lookup(hi)) + except hg.RepoError: + req.write(self.search(hi)) # XXX redirect to 404 page? + return + + req.write(self.changelog(hi, shortlog = True)) + def do_changeset(self, req): req.write(self.changeset(req.form['node'][0])) @@ -895,9 +910,13 @@ class hgweb(object): # require ssl by default, auth info cannot be sniffed and # replayed ssl_req = self.repo.ui.configbool('web', 'push_ssl', True) - if ssl_req and not req.env.get('HTTPS'): - bail(_('ssl required\n')) - return + if ssl_req: + if not req.env.get('HTTPS'): + bail(_('ssl required\n')) + return + proto = 'https' + else: + proto = 'http' # do not allow push unless explicitly allowed if not self.check_perm(req, 'push', False): @@ -943,7 +962,9 @@ class hgweb(object): sys.stdout = cStringIO.StringIO() try: - ret = self.repo.addchangegroup(fp, 'serve') + url = 'remote:%s:%s' % (proto, + req.env.get('REMOTE_HOST', '')) + ret = self.repo.addchangegroup(fp, 'serve', url) finally: val = sys.stdout.getvalue() sys.stdout = old_stdout diff --git a/mercurial/hgweb/hgwebdir_mod.py b/mercurial/hgweb/hgwebdir_mod.py --- a/mercurial/hgweb/hgwebdir_mod.py +++ b/mercurial/hgweb/hgwebdir_mod.py @@ -1,7 +1,7 @@ # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories. # # Copyright 21 May 2005 - (c) 2005 Jake Edge -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/hgweb/request.py b/mercurial/hgweb/request.py --- a/mercurial/hgweb/request.py +++ b/mercurial/hgweb/request.py @@ -1,7 +1,7 @@ # hgweb/request.py - An http request from either CGI or the standalone server. # # Copyright 21 May 2005 - (c) 2005 Jake Edge -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/hgweb/server.py b/mercurial/hgweb/server.py --- a/mercurial/hgweb/server.py +++ b/mercurial/hgweb/server.py @@ -1,7 +1,7 @@ # hgweb/server.py - The standalone hg web server. # # Copyright 21 May 2005 - (c) 2005 Jake Edge -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -198,6 +198,7 @@ def create_server(ui, repo): self.webdirmaker = hgwebdir self.repoviewmaker = hgweb self.reqmaker = wsgiapplication(self.make_handler) + self.daemon_threads = True def make_handler(self): if self.webdir_conf: diff --git a/mercurial/httprangereader.py b/mercurial/httprangereader.py --- a/mercurial/httprangereader.py +++ b/mercurial/httprangereader.py @@ -1,6 +1,6 @@ # httprangereader.py - just what it says # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/httprepo.py b/mercurial/httprepo.py --- a/mercurial/httprepo.py +++ b/mercurial/httprepo.py @@ -1,6 +1,7 @@ # httprepo.py - HTTP repository proxy classes for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall +# Copyright 2006 Vadim Gelfer # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -115,6 +116,7 @@ else: class httprepository(remoterepository): def __init__(self, ui, path): + self.path = path self.caps = None scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path) if query or frag: @@ -124,8 +126,8 @@ class httprepository(remoterepository): host, port, user, passwd = netlocsplit(netloc) # urllib cannot handle URLs with embedded user or passwd - self.url = urlparse.urlunsplit((scheme, netlocunsplit(host, port), - urlpath, '', '')) + self._url = urlparse.urlunsplit((scheme, netlocunsplit(host, port), + urlpath, '', '')) self.ui = ui proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy') @@ -189,6 +191,9 @@ class httprepository(remoterepository): opener.addheaders = [('User-agent', 'mercurial/proto-1.0')] urllib2.install_opener(opener) + def url(self): + return self.path + # look up capabilities only when needed def get_caps(self): @@ -213,7 +218,7 @@ class httprepository(remoterepository): q = {"cmd": cmd} q.update(args) qs = urllib.urlencode(q) - cu = "%s?%s" % (self.url, qs) + cu = "%s?%s" % (self._url, qs) try: resp = urllib2.urlopen(urllib2.Request(cu, data, headers)) except urllib2.HTTPError, inst: @@ -234,13 +239,13 @@ class httprepository(remoterepository): not proto.startswith('text/plain') and \ not proto.startswith('application/hg-changegroup'): raise hg.RepoError(_("'%s' does not appear to be an hg repository") % - self.url) + self._url) if proto.startswith('application/mercurial'): version = proto[22:] if float(version) > 0.1: raise hg.RepoError(_("'%s' uses newer protocol %s") % - (self.url, version)) + (self._url, version)) return resp @@ -335,3 +340,13 @@ class httpsrepository(httprepository): raise util.Abort(_('Python support for SSL and HTTPS ' 'is not installed')) httprepository.__init__(self, ui, path) + +def instance(ui, path, create): + if create: + raise util.Abort(_('cannot create new http repository')) + if path.startswith('hg:'): + ui.warn(_("hg:// syntax is deprecated, please use http:// instead\n")) + path = 'http:' + path[3:] + if path.startswith('https:'): + return httpsrepository(ui, path) + return httprepository(ui, path) diff --git a/mercurial/i18n.py b/mercurial/i18n.py --- a/mercurial/i18n.py +++ b/mercurial/i18n.py @@ -1,7 +1,7 @@ """ i18n.py - internationalization support for mercurial -Copyright 2005 Matt Mackall +Copyright 2005, 2006 Matt Mackall This software may be used and distributed according to the terms of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -1,6 +1,6 @@ # localrepo.py - read/write repository class for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -83,6 +83,9 @@ class localrepository(repo.repository): self.dirstate = dirstate.dirstate(self.opener, self.ui, self.root) + def url(self): + return 'file:' + self.root + def hook(self, name, throw=False, **args): def callhook(hname, funcname): '''call python hook. hook is callable object, looked up as @@ -195,7 +198,7 @@ class localrepository(repo.repository): self.hook('tag', node=node, tag=name, local=local) return - for x in self.changes(): + for x in self.status()[:5]: if '.hgtags' in x: raise util.Abort(_('working copy of .hgtags is changed ' '(please commit .hgtags manually)')) @@ -289,6 +292,10 @@ class localrepository(repo.repository): try: return self.tags()[key] except KeyError: + if key == '.': + key = self.dirstate.parents()[0] + if key == nullid: + raise repo.RepoError(_("no revision checked out")) try: return self.changelog.lookup(key) except: @@ -463,8 +470,7 @@ class localrepository(repo.repository): p2 = p2 or self.dirstate.parents()[1] or nullid c1 = self.changelog.read(p1) c2 = self.changelog.read(p2) - m1 = self.manifest.read(c1[0]) - mf1 = self.manifest.readflags(c1[0]) + m1 = self.manifest.read(c1[0]).copy() m2 = self.manifest.read(c2[0]) changed = [] @@ -477,36 +483,32 @@ class localrepository(repo.repository): wlock = self.wlock() l = self.lock() tr = self.transaction() - mm = m1.copy() - mfm = mf1.copy() linkrev = self.changelog.count() for f in files: try: t = self.wread(f) - tm = util.is_exec(self.wjoin(f), mfm.get(f, False)) + m1.set(f, util.is_exec(self.wjoin(f), m1.execf(f))) r = self.file(f) - mfm[f] = tm (entry, fp1, fp2) = self.checkfilemerge(f, t, r, m1, m2) if entry: - mm[f] = entry + m1[f] = entry continue - mm[f] = r.add(t, {}, tr, linkrev, fp1, fp2) + m1[f] = r.add(t, {}, tr, linkrev, fp1, fp2) changed.append(f) if update_dirstate: self.dirstate.update([f], "n") except IOError: try: - del mm[f] - del mfm[f] + del m1[f] if update_dirstate: self.dirstate.forget([f]) except: # deleted from p2? pass - mnode = self.manifest.add(mm, mfm, tr, linkrev, c1[0], c2[0]) + mnode = self.manifest.add(m1, tr, linkrev, c1[0], c2[0]) user = user or self.ui.username() n = self.changelog.add(mnode, changed, text, tr, p1, p2, user, date) tr.close() @@ -530,15 +532,14 @@ class localrepository(repo.repository): else: self.ui.warn(_("%s not tracked!\n") % f) else: - modified, added, removed, deleted, unknown = self.changes(match=match) + modified, added, removed, deleted, unknown = self.status(match=match)[:5] commit = modified + added remove = removed p1, p2 = self.dirstate.parents() c1 = self.changelog.read(p1) c2 = self.changelog.read(p2) - m1 = self.manifest.read(c1[0]) - mf1 = self.manifest.readflags(c1[0]) + m1 = self.manifest.read(c1[0]).copy() m2 = self.manifest.read(c2[0]) if not commit and not remove and not force and p2 == nullid: @@ -564,7 +565,7 @@ class localrepository(repo.repository): for f in commit: self.ui.note(f + "\n") try: - mf1[f] = util.is_exec(self.wjoin(f), mf1.get(f, False)) + m1.set(f, util.is_exec(self.wjoin(f), m1.execf(f))) t = self.wread(f) except IOError: self.ui.warn(_("trouble committing %s!\n") % f) @@ -591,12 +592,11 @@ class localrepository(repo.repository): changed.append(f) # update manifest - m1 = m1.copy() m1.update(new) for f in remove: if f in m1: del m1[f] - mn = self.manifest.add(m1, mf1, tr, linkrev, c1[0], c2[0], + mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0], (new, remove)) # add changeset @@ -658,9 +658,9 @@ class localrepository(repo.repository): for src, fn in self.dirstate.walk(files, match, badmatch=badmatch): yield src, fn - def changes(self, node1=None, node2=None, files=[], match=util.always, - wlock=None, show_ignored=None): - """return changes between two nodes or node and working directory + def status(self, node1=None, node2=None, files=[], match=util.always, + wlock=None, list_ignored=False, list_clean=False): + """return status of files between two nodes or node and working directory If node1 is None, use the first dirstate parent instead. If node2 is None, compare node1 with working directory. @@ -668,8 +668,7 @@ class localrepository(repo.repository): def fcmp(fn, mf): t1 = self.wread(fn) - t2 = self.file(fn).read(mf.get(fn, nullid)) - return cmp(t1, t2) + return self.file(fn).cmp(mf.get(fn, nullid), t1) def mfmatches(node): change = self.changelog.read(node) @@ -679,7 +678,9 @@ class localrepository(repo.repository): del mf[fn] return mf - modified, added, removed, deleted, unknown, ignored = [],[],[],[],[],[] + modified, added, removed, deleted, unknown = [], [], [], [], [] + ignored, clean = [], [] + compareworking = False if not node1 or (not node2 and node1 == self.dirstate.parents()[0]): compareworking = True @@ -697,8 +698,9 @@ class localrepository(repo.repository): wlock = self.wlock(wait=0) except lock.LockException: wlock = None - lookup, modified, added, removed, deleted, unknown, ignored = ( - self.dirstate.changes(files, match, show_ignored)) + (lookup, modified, added, removed, deleted, unknown, + ignored, clean) = self.dirstate.status(files, match, + list_ignored, list_clean) # are we comparing working dir against its parent? if compareworking: @@ -721,12 +723,11 @@ class localrepository(repo.repository): del mf2[f] else: # we are comparing two revisions - deleted, unknown, ignored = [], [], [] mf2 = mfmatches(node2) if not compareworking: # flush lists from dirstate before comparing manifests - modified, added = [], [] + modified, added, clean = [], [], [] # make sure to sort the files so we talk to the disk in a # reasonable order @@ -736,6 +737,8 @@ class localrepository(repo.repository): if mf1.has_key(fn): if mf1[fn] != mf2[fn] and (mf2[fn] != "" or fcmp(fn, mf1)): modified.append(fn) + elif list_clean: + clean.append(fn) del mf1[fn] else: added.append(fn) @@ -743,12 +746,9 @@ class localrepository(repo.repository): removed = mf1.keys() # sort and return results: - for l in modified, added, removed, deleted, unknown, ignored: + for l in modified, added, removed, deleted, unknown, ignored, clean: l.sort() - if show_ignored is None: - return (modified, added, removed, deleted, unknown) - else: - return (modified, added, removed, deleted, unknown, ignored) + return (modified, added, removed, deleted, unknown, ignored, clean) def add(self, list, wlock=None): if not wlock: @@ -798,7 +798,6 @@ class localrepository(repo.repository): def undelete(self, list, wlock=None): p = self.dirstate.parents()[0] mn = self.changelog.read(p)[0] - mf = self.manifest.readflags(mn) m = self.manifest.read(mn) if not wlock: wlock = self.wlock() @@ -808,7 +807,7 @@ class localrepository(repo.repository): else: t = self.file(f).read(m[f]) self.wwrite(f, t) - util.set_exec(self.wjoin(f), mf[f]) + util.set_exec(self.wjoin(f), m.execf(f)) self.dirstate.update([f], "n") def copy(self, source, dest, wlock=None): @@ -1159,22 +1158,29 @@ class localrepository(repo.repository): else: return subset - def pull(self, remote, heads=None, force=False): - l = self.lock() + def pull(self, remote, heads=None, force=False, lock=None): + mylock = False + if not lock: + lock = self.lock() + mylock = True - fetch = self.findincoming(remote, force=force) - if fetch == [nullid]: - self.ui.status(_("requesting all changes\n")) + try: + fetch = self.findincoming(remote, force=force) + if fetch == [nullid]: + self.ui.status(_("requesting all changes\n")) - if not fetch: - self.ui.status(_("no changes found\n")) - return 0 + if not fetch: + self.ui.status(_("no changes found\n")) + return 0 - if heads is None: - cg = remote.changegroup(fetch, 'pull') - else: - cg = remote.changegroupsubset(fetch, heads, 'pull') - return self.addchangegroup(cg, 'pull') + if heads is None: + cg = remote.changegroup(fetch, 'pull') + else: + cg = remote.changegroupsubset(fetch, heads, 'pull') + return self.addchangegroup(cg, 'pull', remote.url()) + finally: + if mylock: + lock.release() def push(self, remote, force=False, revs=None): # there are two ways to push to remote repo: @@ -1230,7 +1236,7 @@ class localrepository(repo.repository): ret = self.prepush(remote, force, revs) if ret[0] is not None: cg, remote_heads = ret - return remote.addchangegroup(cg, 'push') + return remote.addchangegroup(cg, 'push', self.url()) return ret[1] def push_unbundle(self, remote, force, revs): @@ -1583,7 +1589,7 @@ class localrepository(repo.repository): return util.chunkbuffer(gengroup()) - def addchangegroup(self, source, srctype): + def addchangegroup(self, source, srctype, url): """add changegroup to repo. returns number of heads modified or added + 1.""" @@ -1597,7 +1603,7 @@ class localrepository(repo.repository): if not source: return 0 - self.hook('prechangegroup', throw=True, source=srctype) + self.hook('prechangegroup', throw=True, source=srctype, url=url) changesets = files = revisions = 0 @@ -1664,544 +1670,21 @@ class localrepository(repo.repository): if changesets > 0: self.hook('pretxnchangegroup', throw=True, - node=hex(self.changelog.node(cor+1)), source=srctype) + node=hex(self.changelog.node(cor+1)), source=srctype, + url=url) tr.close() if changesets > 0: self.hook("changegroup", node=hex(self.changelog.node(cor+1)), - source=srctype) + source=srctype, url=url) for i in range(cor + 1, cnr + 1): self.hook("incoming", node=hex(self.changelog.node(i)), - source=srctype) + source=srctype, url=url) return newheads - oldheads + 1 - def update(self, node, allow=False, force=False, choose=None, - moddirstate=True, forcemerge=False, wlock=None, show_stats=True): - pl = self.dirstate.parents() - if not force and pl[1] != nullid: - raise util.Abort(_("outstanding uncommitted merges")) - - err = False - - p1, p2 = pl[0], node - pa = self.changelog.ancestor(p1, p2) - m1n = self.changelog.read(p1)[0] - m2n = self.changelog.read(p2)[0] - man = self.manifest.ancestor(m1n, m2n) - m1 = self.manifest.read(m1n) - mf1 = self.manifest.readflags(m1n) - m2 = self.manifest.read(m2n).copy() - mf2 = self.manifest.readflags(m2n) - ma = self.manifest.read(man) - mfa = self.manifest.readflags(man) - - modified, added, removed, deleted, unknown = self.changes() - - # is this a jump, or a merge? i.e. is there a linear path - # from p1 to p2? - linear_path = (pa == p1 or pa == p2) - - if allow and linear_path: - raise util.Abort(_("there is nothing to merge, just use " - "'hg update' or look at 'hg heads'")) - if allow and not forcemerge: - if modified or added or removed: - raise util.Abort(_("outstanding uncommitted changes")) - - if not forcemerge and not force: - for f in unknown: - if f in m2: - t1 = self.wread(f) - t2 = self.file(f).read(m2[f]) - if cmp(t1, t2) != 0: - raise util.Abort(_("'%s' already exists in the working" - " dir and differs from remote") % f) - - # resolve the manifest to determine which files - # we care about merging - self.ui.note(_("resolving manifests\n")) - self.ui.debug(_(" force %s allow %s moddirstate %s linear %s\n") % - (force, allow, moddirstate, linear_path)) - self.ui.debug(_(" ancestor %s local %s remote %s\n") % - (short(man), short(m1n), short(m2n))) - - merge = {} - get = {} - remove = [] - - # construct a working dir manifest - mw = m1.copy() - mfw = mf1.copy() - umap = dict.fromkeys(unknown) - - for f in added + modified + unknown: - mw[f] = "" - mfw[f] = util.is_exec(self.wjoin(f), mfw.get(f, False)) - - if moddirstate and not wlock: - wlock = self.wlock() - - for f in deleted + removed: - if f in mw: - del mw[f] - - # If we're jumping between revisions (as opposed to merging), - # and if neither the working directory nor the target rev has - # the file, then we need to remove it from the dirstate, to - # prevent the dirstate from listing the file when it is no - # longer in the manifest. - if moddirstate and linear_path and f not in m2: - self.dirstate.forget((f,)) - - # Compare manifests - for f, n in mw.iteritems(): - if choose and not choose(f): - continue - if f in m2: - s = 0 - - # is the wfile new since m1, and match m2? - if f not in m1: - t1 = self.wread(f) - t2 = self.file(f).read(m2[f]) - if cmp(t1, t2) == 0: - n = m2[f] - del t1, t2 - - # are files different? - if n != m2[f]: - a = ma.get(f, nullid) - # are both different from the ancestor? - if n != a and m2[f] != a: - self.ui.debug(_(" %s versions differ, resolve\n") % f) - # merge executable bits - # "if we changed or they changed, change in merge" - a, b, c = mfa.get(f, 0), mfw[f], mf2[f] - mode = ((a^b) | (a^c)) ^ a - merge[f] = (m1.get(f, nullid), m2[f], mode) - s = 1 - # are we clobbering? - # is remote's version newer? - # or are we going back in time? - elif force or m2[f] != a or (p2 == pa and mw[f] == m1[f]): - self.ui.debug(_(" remote %s is newer, get\n") % f) - get[f] = m2[f] - s = 1 - elif f in umap or f in added: - # this unknown file is the same as the checkout - # we need to reset the dirstate if the file was added - get[f] = m2[f] - - if not s and mfw[f] != mf2[f]: - if force: - self.ui.debug(_(" updating permissions for %s\n") % f) - util.set_exec(self.wjoin(f), mf2[f]) - else: - a, b, c = mfa.get(f, 0), mfw[f], mf2[f] - mode = ((a^b) | (a^c)) ^ a - if mode != b: - self.ui.debug(_(" updating permissions for %s\n") - % f) - util.set_exec(self.wjoin(f), mode) - del m2[f] - elif f in ma: - if n != ma[f]: - r = _("d") - if not force and (linear_path or allow): - r = self.ui.prompt( - (_(" local changed %s which remote deleted\n") % f) + - _("(k)eep or (d)elete?"), _("[kd]"), _("k")) - if r == _("d"): - remove.append(f) - else: - self.ui.debug(_("other deleted %s\n") % f) - remove.append(f) # other deleted it - else: - # file is created on branch or in working directory - if force and f not in umap: - self.ui.debug(_("remote deleted %s, clobbering\n") % f) - remove.append(f) - elif n == m1.get(f, nullid): # same as parent - if p2 == pa: # going backwards? - self.ui.debug(_("remote deleted %s\n") % f) - remove.append(f) - else: - self.ui.debug(_("local modified %s, keeping\n") % f) - else: - self.ui.debug(_("working dir created %s, keeping\n") % f) - - for f, n in m2.iteritems(): - if choose and not choose(f): - continue - if f[0] == "/": - continue - if f in ma and n != ma[f]: - r = _("k") - if not force and (linear_path or allow): - r = self.ui.prompt( - (_("remote changed %s which local deleted\n") % f) + - _("(k)eep or (d)elete?"), _("[kd]"), _("k")) - if r == _("k"): - get[f] = n - elif f not in ma: - self.ui.debug(_("remote created %s\n") % f) - get[f] = n - else: - if force or p2 == pa: # going backwards? - self.ui.debug(_("local deleted %s, recreating\n") % f) - get[f] = n - else: - self.ui.debug(_("local deleted %s\n") % f) - - del mw, m1, m2, ma - - if force: - for f in merge: - get[f] = merge[f][1] - merge = {} - - if linear_path or force: - # we don't need to do any magic, just jump to the new rev - branch_merge = False - p1, p2 = p2, nullid - else: - if not allow: - self.ui.status(_("this update spans a branch" - " affecting the following files:\n")) - fl = merge.keys() + get.keys() - fl.sort() - for f in fl: - cf = "" - if f in merge: - cf = _(" (resolve)") - self.ui.status(" %s%s\n" % (f, cf)) - self.ui.warn(_("aborting update spanning branches!\n")) - self.ui.status(_("(use 'hg merge' to merge across branches" - " or 'hg update -C' to lose changes)\n")) - return 1 - branch_merge = True - - xp1 = hex(p1) - xp2 = hex(p2) - if p2 == nullid: xxp2 = '' - else: xxp2 = xp2 - - self.hook('preupdate', throw=True, parent1=xp1, parent2=xxp2) - - # get the files we don't need to change - files = get.keys() - files.sort() - for f in files: - if f[0] == "/": - continue - self.ui.note(_("getting %s\n") % f) - t = self.file(f).read(get[f]) - self.wwrite(f, t) - util.set_exec(self.wjoin(f), mf2[f]) - if moddirstate: - if branch_merge: - self.dirstate.update([f], 'n', st_mtime=-1) - else: - self.dirstate.update([f], 'n') - - # merge the tricky bits - failedmerge = [] - files = merge.keys() - files.sort() - for f in files: - self.ui.status(_("merging %s\n") % f) - my, other, flag = merge[f] - ret = self.merge3(f, my, other, xp1, xp2) - if ret: - err = True - failedmerge.append(f) - util.set_exec(self.wjoin(f), flag) - if moddirstate: - if branch_merge: - # We've done a branch merge, mark this file as merged - # so that we properly record the merger later - self.dirstate.update([f], 'm') - else: - # We've update-merged a locally modified file, so - # we set the dirstate to emulate a normal checkout - # of that file some time in the past. Thus our - # merge will appear as a normal local file - # modification. - f_len = len(self.file(f).read(other)) - self.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1) - - remove.sort() - for f in remove: - self.ui.note(_("removing %s\n") % f) - util.audit_path(f) - try: - util.unlink(self.wjoin(f)) - except OSError, inst: - if inst.errno != errno.ENOENT: - self.ui.warn(_("update failed to remove %s: %s!\n") % - (f, inst.strerror)) - if moddirstate: - if branch_merge: - self.dirstate.update(remove, 'r') - else: - self.dirstate.forget(remove) - - if moddirstate: - self.dirstate.setparents(p1, p2) - - if show_stats: - stats = ((len(get), _("updated")), - (len(merge) - len(failedmerge), _("merged")), - (len(remove), _("removed")), - (len(failedmerge), _("unresolved"))) - note = ", ".join([_("%d files %s") % s for s in stats]) - self.ui.status("%s\n" % note) - if moddirstate: - if branch_merge: - if failedmerge: - self.ui.status(_("There are unresolved merges," - " you can redo the full merge using:\n" - " hg update -C %s\n" - " hg merge %s\n" - % (self.changelog.rev(p1), - self.changelog.rev(p2)))) - else: - self.ui.status(_("(branch merge, don't forget to commit)\n")) - elif failedmerge: - self.ui.status(_("There are unresolved merges with" - " locally modified files.\n")) - - self.hook('update', parent1=xp1, parent2=xxp2, error=int(err)) - return err - - def merge3(self, fn, my, other, p1, p2): - """perform a 3-way merge in the working directory""" - - def temp(prefix, node): - pre = "%s~%s." % (os.path.basename(fn), prefix) - (fd, name) = tempfile.mkstemp(prefix=pre) - f = os.fdopen(fd, "wb") - self.wwrite(fn, fl.read(node), f) - f.close() - return name - - fl = self.file(fn) - base = fl.ancestor(my, other) - a = self.wjoin(fn) - b = temp("base", base) - c = temp("other", other) - - self.ui.note(_("resolving %s\n") % fn) - self.ui.debug(_("file %s: my %s other %s ancestor %s\n") % - (fn, short(my), short(other), short(base))) - - cmd = (os.environ.get("HGMERGE") or self.ui.config("ui", "merge") - or "hgmerge") - r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=self.root, - environ={'HG_FILE': fn, - 'HG_MY_NODE': p1, - 'HG_OTHER_NODE': p2, - 'HG_FILE_MY_NODE': hex(my), - 'HG_FILE_OTHER_NODE': hex(other), - 'HG_FILE_BASE_NODE': hex(base)}) - if r: - self.ui.warn(_("merging %s failed!\n") % fn) - - os.unlink(b) - os.unlink(c) - return r - - def verify(self): - filelinkrevs = {} - filenodes = {} - changesets = revisions = files = 0 - errors = [0] - warnings = [0] - neededmanifests = {} - - def err(msg): - self.ui.warn(msg + "\n") - errors[0] += 1 - - def warn(msg): - self.ui.warn(msg + "\n") - warnings[0] += 1 - - def checksize(obj, name): - d = obj.checksize() - if d[0]: - err(_("%s data length off by %d bytes") % (name, d[0])) - if d[1]: - err(_("%s index contains %d extra bytes") % (name, d[1])) - - def checkversion(obj, name): - if obj.version != revlog.REVLOGV0: - if not revlogv1: - warn(_("warning: `%s' uses revlog format 1") % name) - elif revlogv1: - warn(_("warning: `%s' uses revlog format 0") % name) - - revlogv1 = self.revlogversion != revlog.REVLOGV0 - if self.ui.verbose or revlogv1 != self.revlogv1: - self.ui.status(_("repository uses revlog format %d\n") % - (revlogv1 and 1 or 0)) - - seen = {} - self.ui.status(_("checking changesets\n")) - checksize(self.changelog, "changelog") - - for i in range(self.changelog.count()): - changesets += 1 - n = self.changelog.node(i) - l = self.changelog.linkrev(n) - if l != i: - err(_("incorrect link (%d) for changeset revision %d") %(l, i)) - if n in seen: - err(_("duplicate changeset at revision %d") % i) - seen[n] = 1 - - for p in self.changelog.parents(n): - if p not in self.changelog.nodemap: - err(_("changeset %s has unknown parent %s") % - (short(n), short(p))) - try: - changes = self.changelog.read(n) - except KeyboardInterrupt: - self.ui.warn(_("interrupted")) - raise - except Exception, inst: - err(_("unpacking changeset %s: %s") % (short(n), inst)) - continue - - neededmanifests[changes[0]] = n - - for f in changes[3]: - filelinkrevs.setdefault(f, []).append(i) - - seen = {} - self.ui.status(_("checking manifests\n")) - checkversion(self.manifest, "manifest") - checksize(self.manifest, "manifest") - - for i in range(self.manifest.count()): - n = self.manifest.node(i) - l = self.manifest.linkrev(n) - - if l < 0 or l >= self.changelog.count(): - err(_("bad manifest link (%d) at revision %d") % (l, i)) - - if n in neededmanifests: - del neededmanifests[n] - - if n in seen: - err(_("duplicate manifest at revision %d") % i) - - seen[n] = 1 - - for p in self.manifest.parents(n): - if p not in self.manifest.nodemap: - err(_("manifest %s has unknown parent %s") % - (short(n), short(p))) - - try: - delta = mdiff.patchtext(self.manifest.delta(n)) - except KeyboardInterrupt: - self.ui.warn(_("interrupted")) - raise - except Exception, inst: - err(_("unpacking manifest %s: %s") % (short(n), inst)) - continue - - try: - ff = [ l.split('\0') for l in delta.splitlines() ] - for f, fn in ff: - filenodes.setdefault(f, {})[bin(fn[:40])] = 1 - except (ValueError, TypeError), inst: - err(_("broken delta in manifest %s: %s") % (short(n), inst)) - - self.ui.status(_("crosschecking files in changesets and manifests\n")) - - for m, c in neededmanifests.items(): - err(_("Changeset %s refers to unknown manifest %s") % - (short(m), short(c))) - del neededmanifests - - for f in filenodes: - if f not in filelinkrevs: - err(_("file %s in manifest but not in changesets") % f) - - for f in filelinkrevs: - if f not in filenodes: - err(_("file %s in changeset but not in manifest") % f) - - self.ui.status(_("checking files\n")) - ff = filenodes.keys() - ff.sort() - for f in ff: - if f == "/dev/null": - continue - files += 1 - if not f: - err(_("file without name in manifest %s") % short(n)) - continue - fl = self.file(f) - checkversion(fl, f) - checksize(fl, f) - - nodes = {nullid: 1} - seen = {} - for i in range(fl.count()): - revisions += 1 - n = fl.node(i) - - if n in seen: - err(_("%s: duplicate revision %d") % (f, i)) - if n not in filenodes[f]: - err(_("%s: %d:%s not in manifests") % (f, i, short(n))) - else: - del filenodes[f][n] - - flr = fl.linkrev(n) - if flr not in filelinkrevs.get(f, []): - err(_("%s:%s points to unexpected changeset %d") - % (f, short(n), flr)) - else: - filelinkrevs[f].remove(flr) - - # verify contents - try: - t = fl.read(n) - except KeyboardInterrupt: - self.ui.warn(_("interrupted")) - raise - except Exception, inst: - err(_("unpacking file %s %s: %s") % (f, short(n), inst)) - - # verify parents - (p1, p2) = fl.parents(n) - if p1 not in nodes: - err(_("file %s:%s unknown parent 1 %s") % - (f, short(n), short(p1))) - if p2 not in nodes: - err(_("file %s:%s unknown parent 2 %s") % - (f, short(n), short(p1))) - nodes[n] = 1 - - # cross-check - for node in filenodes[f]: - err(_("node %s in manifests not in %s") % (hex(node), f)) - - self.ui.status(_("%d files, %d changesets, %d total revisions\n") % - (files, changesets, revisions)) - - if warnings[0]: - self.ui.warn(_("%d warnings encountered!\n") % warnings[0]) - if errors[0]: - self.ui.warn(_("%d integrity errors encountered!\n") % errors[0]) - return 1 def stream_in(self, remote): fp = remote.stream_out() @@ -2227,7 +1710,7 @@ class localrepository(repo.repository): util.bytecount(total_bytes / elapsed))) self.reload() return len(self.heads()) + 1 - + def clone(self, remote, heads=[], stream=False): '''clone remote repository. @@ -2256,3 +1739,8 @@ def aftertrans(base): os.path.join(p, "undo.dirstate")) return a +def instance(ui, path, create): + return localrepository(ui, util.drop_scheme('file', path), create) + +def islocal(path): + return True diff --git a/mercurial/lock.py b/mercurial/lock.py --- a/mercurial/lock.py +++ b/mercurial/lock.py @@ -1,6 +1,6 @@ # lock.py - simple locking scheme for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/mail.py b/mercurial/mail.py new file mode 100644 --- /dev/null +++ b/mercurial/mail.py @@ -0,0 +1,68 @@ +# mail.py - mail sending bits for mercurial +# +# Copyright 2006 Matt Mackall +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from i18n import gettext as _ +from demandload import * +demandload(globals(), "os re smtplib templater util") + +def _smtp(ui): + '''send mail using smtp.''' + + local_hostname = ui.config('smtp', 'local_hostname') + s = smtplib.SMTP(local_hostname=local_hostname) + mailhost = ui.config('smtp', 'host') + if not mailhost: + raise util.Abort(_('no [smtp]host in hgrc - cannot send mail')) + mailport = int(ui.config('smtp', 'port', 25)) + self.note(_('sending mail: smtp host %s, port %s\n') % + (mailhost, mailport)) + s.connect(host=mailhost, port=mailport) + if ui.configbool('smtp', 'tls'): + ui.note(_('(using tls)\n')) + s.ehlo() + s.starttls() + s.ehlo() + username = ui.config('smtp', 'username') + password = ui.config('smtp', 'password') + if username and password: + ui.note(_('(authenticating to mail server as %s)\n') % + (username)) + s.login(username, password) + return s + +class _sendmail(object): + '''send mail using sendmail.''' + + def __init__(self, ui, program): + self.ui = ui + self.program = program + + def sendmail(self, sender, recipients, msg): + cmdline = '%s -f %s %s' % ( + self.program, templater.email(sender), + ' '.join(map(templater.email, recipients))) + self.ui.note(_('sending mail: %s\n') % cmdline) + fp = os.popen(cmdline, 'w') + fp.write(msg) + ret = fp.close() + if ret: + raise util.Abort('%s %s' % ( + os.path.basename(self.program.split(None, 1)[0]), + util.explain_exit(ret)[0])) + +def connect(ui): + '''make a mail connection. object returned has one method, sendmail. + call as sendmail(sender, list-of-recipients, msg).''' + + method = ui.config('email', 'method', 'smtp') + if method == 'smtp': + return _smtp(ui) + + return _sendmail(ui, method) + +def sendmail(ui, sender, recipients, msg): + return connect(ui).sendmail(sender, recipients, msg) diff --git a/mercurial/manifest.py b/mercurial/manifest.py --- a/mercurial/manifest.py +++ b/mercurial/manifest.py @@ -1,6 +1,6 @@ # manifest.py - manifest revision class for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -10,6 +10,31 @@ from i18n import gettext as _ from demandload import * demandload(globals(), "array bisect struct") +class manifestdict(dict): + def __init__(self, mapping=None, flags=None): + if mapping is None: mapping = {} + if flags is None: flags = {} + dict.__init__(self, mapping) + self._flags = flags + def flags(self, f): + return self._flags.get(f, "") + def execf(self, f): + "test for executable in manifest flags" + return "x" in self.flags(f) + def linkf(self, f): + "test for symlink in manifest flags" + return "l" in self.flags(f) + def rawset(self, f, entry): + self[f] = bin(entry[:40]) + fl = entry[40:-1] + if fl: self._flags[f] = fl + def set(self, f, execf=False, linkf=False): + if linkf: self._flags[f] = "l" + elif execf: self._flags[f] = "x" + else: self._flags[f] = "" + def copy(self): + return manifestdict(dict.copy(self), dict.copy(self._flags)) + class manifest(revlog): def __init__(self, opener, defversion=REVLOGV0): self.mapcache = None @@ -18,26 +43,18 @@ class manifest(revlog): defversion) def read(self, node): - if node == nullid: return {} # don't upset local cache + if node == nullid: return manifestdict() # don't upset local cache if self.mapcache and self.mapcache[0] == node: return self.mapcache[1] text = self.revision(node) - map = {} - flag = {} self.listcache = array.array('c', text) lines = text.splitlines(1) + mapping = manifestdict() for l in lines: (f, n) = l.split('\0') - map[f] = bin(n[:40]) - flag[f] = (n[40:-1] == "x") - self.mapcache = (node, map, flag) - return map - - def readflags(self, node): - if node == nullid: return {} # don't upset local cache - if not self.mapcache or self.mapcache[0] != node: - self.read(node) - return self.mapcache[2] + mapping.rawset(f, n) + self.mapcache = (node, mapping) + return mapping def diff(self, a, b): return mdiff.textdiff(str(a), str(b)) @@ -86,7 +103,7 @@ class manifest(revlog): '''look up entry for a single file efficiently. return (node, flag) pair if found, (None, None) if not.''' if self.mapcache and node == self.mapcache[0]: - return self.mapcache[1].get(f), self.mapcache[2].get(f) + return self.mapcache[1].get(f), self.mapcache[1].flags(f) text = self.revision(node) start, end = self._search(text, f) if start == end: @@ -95,7 +112,7 @@ class manifest(revlog): f, n = l.split('\0') return bin(n[:40]), n[40:-1] == 'x' - def add(self, map, flags, transaction, link, p1=None, p2=None, + def add(self, map, transaction, link, p1=None, p2=None, changed=None): # apply the changes collected during the bisect loop to our addlist # return a delta suitable for addrevision @@ -123,9 +140,7 @@ class manifest(revlog): # if this is changed to support newlines in filenames, # be sure to check the templates/ dir again (especially *-raw.tmpl) - text = ["%s\000%s%s\n" % - (f, hex(map[f]), flags[f] and "x" or '') - for f in files] + text = ["%s\000%s%s\n" % (f, hex(map[f]), map.flags(f)) for f in files] self.listcache = array.array('c', "".join(text)) cachedelta = None else: @@ -151,8 +166,7 @@ class manifest(revlog): # bs will either be the index of the item or the insert point start, end = self._search(addbuf, f, start) if w[1] == 0: - l = "%s\000%s%s\n" % (f, hex(map[f]), - flags[f] and "x" or '') + l = "%s\000%s%s\n" % (f, hex(map[f]), map.flags(f)) else: l = "" if start == end and w[1] == 1: @@ -183,6 +197,6 @@ class manifest(revlog): n = self.addrevision(buffer(self.listcache), transaction, link, p1, \ p2, cachedelta) - self.mapcache = (n, map, flags) + self.mapcache = (n, map) return n diff --git a/mercurial/mdiff.py b/mercurial/mdiff.py --- a/mercurial/mdiff.py +++ b/mercurial/mdiff.py @@ -1,6 +1,6 @@ # mdiff.py - diff and patch routines for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -19,14 +19,41 @@ def splitnewlines(text): lines[-1] = lines[-1][:-1] return lines -def unidiff(a, ad, b, bd, fn, r=None, text=False, - showfunc=False, ignorews=False, ignorewsamount=False, - ignoreblanklines=False): +class diffopts(object): + '''context is the number of context lines + text treats all files as text + showfunc enables diff -p output + git enables the git extended patch format + ignorews ignores all whitespace changes in the diff + ignorewsamount ignores changes in the amount of whitespace + ignoreblanklines ignores changes whose lines are all blank''' + defaults = { + 'context': 3, + 'text': False, + 'showfunc': True, + 'git': False, + 'ignorews': False, + 'ignorewsamount': False, + 'ignoreblanklines': False, + } + + __slots__ = defaults.keys() + + def __init__(self, **opts): + for k in self.__slots__: + v = opts.get(k) + if v is None: + v = self.defaults[k] + setattr(self, k, v) + +defaultopts = diffopts() + +def unidiff(a, ad, b, bd, fn, r=None, opts=defaultopts): if not a and not b: return "" epoch = util.datestr((0, 0)) - if not text and (util.binary(a) or util.binary(b)): + if not opts.text and (util.binary(a) or util.binary(b)): l = ['Binary file %s has changed\n' % fn] elif not a: b = splitnewlines(b) @@ -49,10 +76,7 @@ def unidiff(a, ad, b, bd, fn, r=None, te else: al = splitnewlines(a) bl = splitnewlines(b) - l = list(bunidiff(a, b, al, bl, "a/" + fn, "b/" + fn, - showfunc=showfunc, ignorews=ignorews, - ignorewsamount=ignorewsamount, - ignoreblanklines=ignoreblanklines)) + l = list(bunidiff(a, b, al, bl, "a/" + fn, "b/" + fn, opts=opts)) if not l: return "" # difflib uses a space, rather than a tab l[0] = "%s\t%s\n" % (l[0][:-2], ad) @@ -72,21 +96,15 @@ def unidiff(a, ad, b, bd, fn, r=None, te # t1 and t2 are the text to be diffed # l1 and l2 are the text broken up into lines # header1 and header2 are the filenames for the diff output -# context is the number of context lines -# showfunc enables diff -p output -# ignorews ignores all whitespace changes in the diff -# ignorewsamount ignores changes in the amount of whitespace -# ignoreblanklines ignores changes whose lines are all blank -def bunidiff(t1, t2, l1, l2, header1, header2, context=3, showfunc=False, - ignorews=False, ignorewsamount=False, ignoreblanklines=False): +def bunidiff(t1, t2, l1, l2, header1, header2, opts=defaultopts): def contextend(l, len): - ret = l + context + ret = l + opts.context if ret > len: ret = len return ret def contextstart(l): - ret = l - context + ret = l - opts.context if ret < 0: return 0 return ret @@ -101,7 +119,7 @@ def bunidiff(t1, t2, l1, l2, header1, he blen = b2 - bstart + aend - a2 func = "" - if showfunc: + if opts.showfunc: # walk backwards from the start of the context # to find a line starting with an alphanumeric char. for x in xrange(astart, -1, -1): @@ -119,14 +137,14 @@ def bunidiff(t1, t2, l1, l2, header1, he header = [ "--- %s\t\n" % header1, "+++ %s\t\n" % header2 ] - if showfunc: + if opts.showfunc: funcre = re.compile('\w') - if ignorewsamount: + if opts.ignorewsamount: wsamountre = re.compile('[ \t]+') wsappendedre = re.compile(' \n') - if ignoreblanklines: + if opts.ignoreblanklines: wsblanklinesre = re.compile('\n') - if ignorews: + if opts.ignorews: wsre = re.compile('[ \t]') # bdiff.blocks gives us the matching sequences in the files. The loop @@ -159,13 +177,13 @@ def bunidiff(t1, t2, l1, l2, header1, he if not old and not new: continue - if ignoreblanklines: + if opts.ignoreblanklines: wsold = wsblanklinesre.sub('', "".join(old)) wsnew = wsblanklinesre.sub('', "".join(new)) if wsold == wsnew: continue - if ignorewsamount: + if opts.ignorewsamount: wsold = wsamountre.sub(' ', "".join(old)) wsold = wsappendedre.sub('\n', wsold) wsnew = wsamountre.sub(' ', "".join(new)) @@ -173,7 +191,7 @@ def bunidiff(t1, t2, l1, l2, header1, he if wsold == wsnew: continue - if ignorews: + if opts.ignorews: wsold = wsre.sub('', "".join(old)) wsnew = wsre.sub('', "".join(new)) if wsold == wsnew: @@ -184,7 +202,7 @@ def bunidiff(t1, t2, l1, l2, header1, he prev = None if hunk: # join with the previous hunk if it falls inside the context - if astart < hunk[1] + context + 1: + if astart < hunk[1] + opts.context + 1: prev = hunk astart = hunk[1] bstart = hunk[3] diff --git a/mercurial/merge.py b/mercurial/merge.py new file mode 100644 --- /dev/null +++ b/mercurial/merge.py @@ -0,0 +1,339 @@ +# merge.py - directory-level update/merge handling for Mercurial +# +# Copyright 2006 Matt Mackall +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from node import * +from i18n import gettext as _ +from demandload import * +demandload(globals(), "util os tempfile") + +def fmerge(f, local, other, ancestor): + """merge executable flags""" + a, b, c = ancestor.execf(f), local.execf(f), other.execf(f) + return ((a^b) | (a^c)) ^ a + +def merge3(repo, fn, my, other, p1, p2): + """perform a 3-way merge in the working directory""" + + def temp(prefix, node): + pre = "%s~%s." % (os.path.basename(fn), prefix) + (fd, name) = tempfile.mkstemp(prefix=pre) + f = os.fdopen(fd, "wb") + repo.wwrite(fn, fl.read(node), f) + f.close() + return name + + fl = repo.file(fn) + base = fl.ancestor(my, other) + a = repo.wjoin(fn) + b = temp("base", base) + c = temp("other", other) + + repo.ui.note(_("resolving %s\n") % fn) + repo.ui.debug(_("file %s: my %s other %s ancestor %s\n") % + (fn, short(my), short(other), short(base))) + + cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge") + or "hgmerge") + r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root, + environ={'HG_FILE': fn, + 'HG_MY_NODE': p1, + 'HG_OTHER_NODE': p2, + 'HG_FILE_MY_NODE': hex(my), + 'HG_FILE_OTHER_NODE': hex(other), + 'HG_FILE_BASE_NODE': hex(base)}) + if r: + repo.ui.warn(_("merging %s failed!\n") % fn) + + os.unlink(b) + os.unlink(c) + return r + +def update(repo, node, branchmerge=False, force=False, partial=None, + wlock=None, show_stats=True, remind=True): + + overwrite = force and not branchmerge + forcemerge = force and branchmerge + + if not wlock: + wlock = repo.wlock() + + ### check phase + + pl = repo.dirstate.parents() + if not overwrite and pl[1] != nullid: + raise util.Abort(_("outstanding uncommitted merges")) + + p1, p2 = pl[0], node + pa = repo.changelog.ancestor(p1, p2) + + # is there a linear path from p1 to p2? + linear_path = (pa == p1 or pa == p2) + if branchmerge and linear_path: + raise util.Abort(_("there is nothing to merge, just use " + "'hg update' or look at 'hg heads'")) + + if not overwrite and not linear_path and not branchmerge: + raise util.Abort(_("update spans branches, use 'hg merge' " + "or 'hg update -C' to lose changes")) + + modified, added, removed, deleted, unknown = repo.status()[:5] + if branchmerge and not forcemerge: + if modified or added or removed: + raise util.Abort(_("outstanding uncommitted changes")) + + m1n = repo.changelog.read(p1)[0] + m2n = repo.changelog.read(p2)[0] + man = repo.manifest.ancestor(m1n, m2n) + m1 = repo.manifest.read(m1n) + m2 = repo.manifest.read(m2n).copy() + ma = repo.manifest.read(man) + + if not force: + for f in unknown: + if f in m2: + if repo.file(f).cmp(m2[f], repo.wread(f)): + raise util.Abort(_("'%s' already exists in the working" + " dir and differs from remote") % f) + + # resolve the manifest to determine which files + # we care about merging + repo.ui.note(_("resolving manifests\n")) + repo.ui.debug(_(" overwrite %s branchmerge %s partial %s linear %s\n") % + (overwrite, branchmerge, bool(partial), linear_path)) + repo.ui.debug(_(" ancestor %s local %s remote %s\n") % + (short(man), short(m1n), short(m2n))) + + merge = {} + get = {} + remove = [] + forget = [] + + # construct a working dir manifest + mw = m1.copy() + umap = dict.fromkeys(unknown) + + for f in added + modified + unknown: + mw[f] = "" + # is the wfile new and matches m2? + if (f not in m1 and f in m2 and + not repo.file(f).cmp(m2[f], repo.wread(f))): + mw[f] = m2[f] + + mw.set(f, util.is_exec(repo.wjoin(f), mw.execf(f))) + + for f in deleted + removed: + if f in mw: + del mw[f] + + # If we're jumping between revisions (as opposed to merging), + # and if neither the working directory nor the target rev has + # the file, then we need to remove it from the dirstate, to + # prevent the dirstate from listing the file when it is no + # longer in the manifest. + if linear_path and f not in m2: + forget.append(f) + + # Compare manifests + for f, n in mw.iteritems(): + if partial and not partial(f): + continue + if f in m2: + s = 0 + + # are files different? + if n != m2[f]: + a = ma.get(f, nullid) + # are both different from the ancestor? + if n != a and m2[f] != a: + repo.ui.debug(_(" %s versions differ, resolve\n") % f) + merge[f] = (fmerge(f, mw, m2, ma), m1.get(f, nullid), m2[f]) + s = 1 + # are we clobbering? + # is remote's version newer? + # or are we going back in time? + elif overwrite or m2[f] != a or (p2 == pa and mw[f] == m1[f]): + repo.ui.debug(_(" remote %s is newer, get\n") % f) + get[f] = (m2.execf(f), m2[f]) + s = 1 + elif f in umap or f in added: + # this unknown file is the same as the checkout + # we need to reset the dirstate if the file was added + get[f] = (m2.execf(f), m2[f]) + + if not s and mw.execf(f) != m2.execf(f): + if overwrite: + repo.ui.debug(_(" updating permissions for %s\n") % f) + util.set_exec(repo.wjoin(f), m2.execf(f)) + else: + if fmerge(f, mw, m2, ma) != mw.execf(f): + repo.ui.debug(_(" updating permissions for %s\n") + % f) + util.set_exec(repo.wjoin(f), mode) + del m2[f] + elif f in ma: + if n != ma[f]: + r = _("d") + if not overwrite and (linear_path or branchmerge): + r = repo.ui.prompt( + (_(" local changed %s which remote deleted\n") % f) + + _("(k)eep or (d)elete?"), _("[kd]"), _("k")) + if r == _("d"): + remove.append(f) + else: + repo.ui.debug(_("other deleted %s\n") % f) + remove.append(f) # other deleted it + else: + # file is created on branch or in working directory + if overwrite and f not in umap: + repo.ui.debug(_("remote deleted %s, clobbering\n") % f) + remove.append(f) + elif n == m1.get(f, nullid): # same as parent + if p2 == pa: # going backwards? + repo.ui.debug(_("remote deleted %s\n") % f) + remove.append(f) + else: + repo.ui.debug(_("local modified %s, keeping\n") % f) + else: + repo.ui.debug(_("working dir created %s, keeping\n") % f) + + for f, n in m2.iteritems(): + if partial and not partial(f): + continue + if f[0] == "/": + continue + if f in ma and n != ma[f]: + r = _("k") + if not overwrite and (linear_path or branchmerge): + r = repo.ui.prompt( + (_("remote changed %s which local deleted\n") % f) + + _("(k)eep or (d)elete?"), _("[kd]"), _("k")) + if r == _("k"): + get[f] = (m2.execf(f), n) + elif f not in ma: + repo.ui.debug(_("remote created %s\n") % f) + get[f] = (m2.execf(f), n) + else: + if overwrite or p2 == pa: # going backwards? + repo.ui.debug(_("local deleted %s, recreating\n") % f) + get[f] = (m2.execf(f), n) + else: + repo.ui.debug(_("local deleted %s\n") % f) + + del mw, m1, m2, ma + + ### apply phase + + if overwrite: + for f in merge: + get[f] = merge[f][:2] + merge = {} + + if linear_path or overwrite: + # we don't need to do any magic, just jump to the new rev + p1, p2 = p2, nullid + + xp1 = hex(p1) + xp2 = hex(p2) + if p2 == nullid: xxp2 = '' + else: xxp2 = xp2 + + repo.hook('preupdate', throw=True, parent1=xp1, parent2=xxp2) + + # get the files we don't need to change + files = get.keys() + files.sort() + for f in files: + flag, node = get[f] + if f[0] == "/": + continue + repo.ui.note(_("getting %s\n") % f) + t = repo.file(f).read(node) + repo.wwrite(f, t) + util.set_exec(repo.wjoin(f), flag) + + # merge the tricky bits + unresolved = [] + files = merge.keys() + files.sort() + for f in files: + repo.ui.status(_("merging %s\n") % f) + flag, my, other = merge[f] + ret = merge3(repo, f, my, other, xp1, xp2) + if ret: + unresolved.append(f) + util.set_exec(repo.wjoin(f), flag) + + remove.sort() + for f in remove: + repo.ui.note(_("removing %s\n") % f) + util.audit_path(f) + try: + util.unlink(repo.wjoin(f)) + except OSError, inst: + if inst.errno != errno.ENOENT: + repo.ui.warn(_("update failed to remove %s: %s!\n") % + (f, inst.strerror)) + + # update dirstate + if not partial: + repo.dirstate.setparents(p1, p2) + repo.dirstate.forget(forget) + if branchmerge: + repo.dirstate.update(remove, 'r') + else: + repo.dirstate.forget(remove) + + files = get.keys() + files.sort() + for f in files: + if branchmerge: + repo.dirstate.update([f], 'n', st_mtime=-1) + else: + repo.dirstate.update([f], 'n') + + files = merge.keys() + files.sort() + for f in files: + if branchmerge: + # We've done a branch merge, mark this file as merged + # so that we properly record the merger later + repo.dirstate.update([f], 'm') + else: + # We've update-merged a locally modified file, so + # we set the dirstate to emulate a normal checkout + # of that file some time in the past. Thus our + # merge will appear as a normal local file + # modification. + fl = repo.file(f) + f_len = fl.size(fl.rev(other)) + repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1) + + if show_stats: + stats = ((len(get), _("updated")), + (len(merge) - len(unresolved), _("merged")), + (len(remove), _("removed")), + (len(unresolved), _("unresolved"))) + note = ", ".join([_("%d files %s") % s for s in stats]) + repo.ui.status("%s\n" % note) + if not partial: + if branchmerge: + if unresolved: + repo.ui.status(_("There are unresolved merges," + " you can redo the full merge using:\n" + " hg update -C %s\n" + " hg merge %s\n" + % (repo.changelog.rev(p1), + repo.changelog.rev(p2)))) + elif remind: + repo.ui.status(_("(branch merge, don't forget to commit)\n")) + elif unresolved: + repo.ui.status(_("There are unresolved merges with" + " locally modified files.\n")) + + repo.hook('update', parent1=xp1, parent2=xxp2, error=len(unresolved)) + return len(unresolved) + diff --git a/mercurial/mpatch.c b/mercurial/mpatch.c --- a/mercurial/mpatch.c +++ b/mercurial/mpatch.c @@ -14,7 +14,7 @@ allocation of intermediate Python objects. Working memory is about 2x the total number of hunks. - Copyright 2005 Matt Mackall + Copyright 2005, 2006 Matt Mackall This software may be used and distributed according to the terms of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/node.py b/mercurial/node.py --- a/mercurial/node.py +++ b/mercurial/node.py @@ -1,7 +1,7 @@ """ node.py - basic nodeid manipulation for mercurial -Copyright 2005 Matt Mackall +Copyright 2005, 2006 Matt Mackall This software may be used and distributed according to the terms of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/packagescan.py b/mercurial/packagescan.py --- a/mercurial/packagescan.py +++ b/mercurial/packagescan.py @@ -2,7 +2,7 @@ # Used for the py2exe distutil. # This module must be the first mercurial module imported in setup.py # -# Copyright 2005 Volker Kleinfeld +# Copyright 2005, 2006 Volker Kleinfeld # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/patch.py b/mercurial/patch.py new file mode 100644 --- /dev/null +++ b/mercurial/patch.py @@ -0,0 +1,539 @@ +# patch.py - patch file parsing routines +# +# Copyright 2006 Brendan Cully +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from demandload import demandload +from i18n import gettext as _ +from node import * +demandload(globals(), "cmdutil mdiff util") +demandload(globals(), "cStringIO email.Parser errno os re shutil sys tempfile") + +# helper functions + +def copyfile(src, dst, basedir=None): + if not basedir: + basedir = os.getcwd() + + abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)] + if os.path.exists(absdst): + raise util.Abort(_("cannot create %s: destination already exists") % + dst) + + targetdir = os.path.dirname(absdst) + if not os.path.isdir(targetdir): + os.makedirs(targetdir) + try: + shutil.copyfile(abssrc, absdst) + shutil.copymode(abssrc, absdst) + except shutil.Error, inst: + raise util.Abort(str(inst)) + +# public functions + +def extract(ui, fileobj): + '''extract patch from data read from fileobj. + + patch can be normal patch or contained in email message. + + return tuple (filename, message, user, date). any item in returned + tuple can be None. if filename is None, fileobj did not contain + patch. caller must unlink filename when done.''' + + # attempt to detect the start of a patch + # (this heuristic is borrowed from quilt) + diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' + + 'retrieving revision [0-9]+(\.[0-9]+)*$|' + + '(---|\*\*\*)[ \t])', re.MULTILINE) + + fd, tmpname = tempfile.mkstemp(prefix='hg-patch-') + tmpfp = os.fdopen(fd, 'w') + try: + hgpatch = False + + msg = email.Parser.Parser().parse(fileobj) + + message = msg['Subject'] + user = msg['From'] + # should try to parse msg['Date'] + date = None + + if message: + message = message.replace('\n\t', ' ') + ui.debug('Subject: %s\n' % message) + if user: + ui.debug('From: %s\n' % user) + diffs_seen = 0 + ok_types = ('text/plain', 'text/x-diff', 'text/x-patch') + + for part in msg.walk(): + content_type = part.get_content_type() + ui.debug('Content-Type: %s\n' % content_type) + if content_type not in ok_types: + continue + payload = part.get_payload(decode=True) + m = diffre.search(payload) + if m: + ui.debug(_('found patch at byte %d\n') % m.start(0)) + diffs_seen += 1 + cfp = cStringIO.StringIO() + if message: + cfp.write(message) + cfp.write('\n') + for line in payload[:m.start(0)].splitlines(): + if line.startswith('# HG changeset patch'): + ui.debug(_('patch generated by hg export\n')) + hgpatch = True + # drop earlier commit message content + cfp.seek(0) + cfp.truncate() + elif hgpatch: + if line.startswith('# User '): + user = line[7:] + ui.debug('From: %s\n' % user) + elif line.startswith("# Date "): + date = line[7:] + if not line.startswith('# '): + cfp.write(line) + cfp.write('\n') + message = cfp.getvalue() + if tmpfp: + tmpfp.write(payload) + if not payload.endswith('\n'): + tmpfp.write('\n') + elif not diffs_seen and message and content_type == 'text/plain': + message += '\n' + payload + except: + tmpfp.close() + os.unlink(tmpname) + raise + + tmpfp.close() + if not diffs_seen: + os.unlink(tmpname) + return None, message, user, date + return tmpname, message, user, date + +def readgitpatch(patchname): + """extract git-style metadata about patches from """ + class gitpatch: + "op is one of ADD, DELETE, RENAME, MODIFY or COPY" + def __init__(self, path): + self.path = path + self.oldpath = None + self.mode = None + self.op = 'MODIFY' + self.copymod = False + self.lineno = 0 + + # Filter patch for git information + gitre = re.compile('diff --git a/(.*) b/(.*)') + pf = file(patchname) + gp = None + gitpatches = [] + # Can have a git patch with only metadata, causing patch to complain + dopatch = False + + lineno = 0 + for line in pf: + lineno += 1 + if line.startswith('diff --git'): + m = gitre.match(line) + if m: + if gp: + gitpatches.append(gp) + src, dst = m.group(1,2) + gp = gitpatch(dst) + gp.lineno = lineno + elif gp: + if line.startswith('--- '): + if gp.op in ('COPY', 'RENAME'): + gp.copymod = True + dopatch = 'filter' + gitpatches.append(gp) + gp = None + if not dopatch: + dopatch = True + continue + if line.startswith('rename from '): + gp.op = 'RENAME' + gp.oldpath = line[12:].rstrip() + elif line.startswith('rename to '): + gp.path = line[10:].rstrip() + elif line.startswith('copy from '): + gp.op = 'COPY' + gp.oldpath = line[10:].rstrip() + elif line.startswith('copy to '): + gp.path = line[8:].rstrip() + elif line.startswith('deleted file'): + gp.op = 'DELETE' + elif line.startswith('new file mode '): + gp.op = 'ADD' + gp.mode = int(line.rstrip()[-3:], 8) + elif line.startswith('new mode '): + gp.mode = int(line.rstrip()[-3:], 8) + if gp: + gitpatches.append(gp) + + if not gitpatches: + dopatch = True + + return (dopatch, gitpatches) + +def dogitpatch(patchname, gitpatches): + """Preprocess git patch so that vanilla patch can handle it""" + pf = file(patchname) + pfline = 1 + + fd, patchname = tempfile.mkstemp(prefix='hg-patch-') + tmpfp = os.fdopen(fd, 'w') + + try: + for i in range(len(gitpatches)): + p = gitpatches[i] + if not p.copymod: + continue + + copyfile(p.oldpath, p.path) + + # rewrite patch hunk + while pfline < p.lineno: + tmpfp.write(pf.readline()) + pfline += 1 + tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path)) + line = pf.readline() + pfline += 1 + while not line.startswith('--- a/'): + tmpfp.write(line) + line = pf.readline() + pfline += 1 + tmpfp.write('--- a/%s\n' % p.path) + + line = pf.readline() + while line: + tmpfp.write(line) + line = pf.readline() + except: + tmpfp.close() + os.unlink(patchname) + raise + + tmpfp.close() + return patchname + +def patch(patchname, ui, strip=1, cwd=None): + """apply the patch to the working directory. + a list of patched files is returned""" + + (dopatch, gitpatches) = readgitpatch(patchname) + + files = {} + fuzz = False + if dopatch: + if dopatch == 'filter': + patchname = dogitpatch(patchname, gitpatches) + patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch') + args = [] + if cwd: + args.append('-d %s' % util.shellquote(cwd)) + fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, + util.shellquote(patchname))) + + if dopatch == 'filter': + False and os.unlink(patchname) + + for line in fp: + line = line.rstrip() + ui.note(line + '\n') + if line.startswith('patching file '): + pf = util.parse_patch_output(line) + printed_file = False + files.setdefault(pf, (None, None)) + elif line.find('with fuzz') >= 0: + fuzz = True + if not printed_file: + ui.warn(pf + '\n') + printed_file = True + ui.warn(line + '\n') + elif line.find('saving rejects to file') >= 0: + ui.warn(line + '\n') + elif line.find('FAILED') >= 0: + if not printed_file: + ui.warn(pf + '\n') + printed_file = True + ui.warn(line + '\n') + + code = fp.close() + if code: + raise util.Abort(_("patch command failed: %s") % + util.explain_exit(code)[0]) + + for gp in gitpatches: + files[gp.path] = (gp.op, gp) + + return (files, fuzz) + +def diffopts(ui, opts={}): + return mdiff.diffopts( + text=opts.get('text'), + git=(opts.get('git') or + ui.configbool('diff', 'git', None)), + showfunc=(opts.get('show_function') or + ui.configbool('diff', 'showfunc', None)), + ignorews=(opts.get('ignore_all_space') or + ui.configbool('diff', 'ignorews', None)), + ignorewsamount=(opts.get('ignore_space_change') or + ui.configbool('diff', 'ignorewsamount', None)), + ignoreblanklines=(opts.get('ignore_blank_lines') or + ui.configbool('diff', 'ignoreblanklines', None))) + +def updatedir(ui, repo, patches, wlock=None): + '''Update dirstate after patch application according to metadata''' + if not patches: + return + copies = [] + removes = [] + cfiles = patches.keys() + copts = {'after': False, 'force': False} + cwd = repo.getcwd() + if cwd: + cfiles = [util.pathto(cwd, f) for f in patches.keys()] + for f in patches: + ctype, gp = patches[f] + if ctype == 'RENAME': + copies.append((gp.oldpath, gp.path, gp.copymod)) + removes.append(gp.oldpath) + elif ctype == 'COPY': + copies.append((gp.oldpath, gp.path, gp.copymod)) + elif ctype == 'DELETE': + removes.append(gp.path) + for src, dst, after in copies: + if not after: + copyfile(src, dst, repo.root) + repo.copy(src, dst, wlock=wlock) + if removes: + repo.remove(removes, True, wlock=wlock) + for f in patches: + ctype, gp = patches[f] + if gp and gp.mode: + x = gp.mode & 0100 != 0 + dst = os.path.join(repo.root, gp.path) + util.set_exec(dst, x) + cmdutil.addremove(repo, cfiles, wlock=wlock) + files = patches.keys() + files.extend([r for r in removes if r not in files]) + files.sort() + + return files + +def diff(repo, node1=None, node2=None, files=None, match=util.always, + fp=None, changes=None, opts=None): + '''print diff of changes to files between two nodes, or node and + working directory. + + if node1 is None, use first dirstate parent instead. + if node2 is None, compare node1 with working directory.''' + + if opts is None: + opts = mdiff.defaultopts + if fp is None: + fp = repo.ui + + if not node1: + node1 = repo.dirstate.parents()[0] + + clcache = {} + def getchangelog(n): + if n not in clcache: + clcache[n] = repo.changelog.read(n) + return clcache[n] + mcache = {} + def getmanifest(n): + if n not in mcache: + mcache[n] = repo.manifest.read(n) + return mcache[n] + fcache = {} + def getfile(f): + if f not in fcache: + fcache[f] = repo.file(f) + return fcache[f] + + # reading the data for node1 early allows it to play nicely + # with repo.status and the revlog cache. + change = getchangelog(node1) + mmap = getmanifest(change[0]) + date1 = util.datestr(change[2]) + + if not changes: + changes = repo.status(node1, node2, files, match=match)[:5] + modified, added, removed, deleted, unknown = changes + if files: + def filterfiles(filters): + l = [x for x in filters if x in files] + + for t in files: + if not t.endswith("/"): + t += "/" + l += [x for x in filters if x.startswith(t)] + return l + + modified, added, removed = map(filterfiles, (modified, added, removed)) + + if not modified and not added and not removed: + return + + def renamedbetween(f, n1, n2): + r1, r2 = map(repo.changelog.rev, (n1, n2)) + src = None + while r2 > r1: + cl = getchangelog(n2)[0] + m = getmanifest(cl) + try: + src = getfile(f).renamed(m[f]) + except KeyError: + return None + if src: + f = src[0] + n2 = repo.changelog.parents(n2)[0] + r2 = repo.changelog.rev(n2) + return src + + if node2: + change = getchangelog(node2) + mmap2 = getmanifest(change[0]) + _date2 = util.datestr(change[2]) + def date2(f): + return _date2 + def read(f): + return getfile(f).read(mmap2[f]) + def renamed(f): + return renamedbetween(f, node1, node2) + else: + tz = util.makedate()[1] + _date2 = util.datestr() + def date2(f): + try: + return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz)) + except OSError, err: + if err.errno != errno.ENOENT: raise + return _date2 + def read(f): + return repo.wread(f) + def renamed(f): + src = repo.dirstate.copies.get(f) + parent = repo.dirstate.parents()[0] + if src: + f = src[0] + of = renamedbetween(f, node1, parent) + if of: + return of + elif src: + cl = getchangelog(parent)[0] + return (src, getmanifest(cl)[src]) + else: + return None + + if repo.ui.quiet: + r = None + else: + hexfunc = repo.ui.verbose and hex or short + r = [hexfunc(node) for node in [node1, node2] if node] + + if opts.git: + copied = {} + for f in added: + src = renamed(f) + if src: + copied[f] = src + srcs = [x[1][0] for x in copied.items()] + + all = modified + added + removed + all.sort() + for f in all: + to = None + tn = None + dodiff = True + if f in mmap: + to = getfile(f).read(mmap[f]) + if f not in removed: + tn = read(f) + if opts.git: + def gitmode(x): + return x and '100755' or '100644' + def addmodehdr(header, omode, nmode): + if omode != nmode: + header.append('old mode %s\n' % omode) + header.append('new mode %s\n' % nmode) + + a, b = f, f + header = [] + if f in added: + if node2: + mode = gitmode(mmap2.execf(f)) + else: + mode = gitmode(util.is_exec(repo.wjoin(f), None)) + if f in copied: + a, arev = copied[f] + omode = gitmode(mmap.execf(a)) + addmodehdr(header, omode, mode) + op = a in removed and 'rename' or 'copy' + header.append('%s from %s\n' % (op, a)) + header.append('%s to %s\n' % (op, f)) + to = getfile(a).read(arev) + else: + header.append('new file mode %s\n' % mode) + elif f in removed: + if f in srcs: + dodiff = False + else: + mode = gitmode(mmap.execf(f)) + header.append('deleted file mode %s\n' % mode) + else: + omode = gitmode(mmap.execf(f)) + nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f))) + addmodehdr(header, omode, nmode) + r = None + if dodiff: + header.insert(0, 'diff --git a/%s b/%s\n' % (a, b)) + fp.write(''.join(header)) + if dodiff: + fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts)) + +def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False, + opts=None): + '''export changesets as hg patches.''' + + total = len(revs) + revwidth = max(map(len, revs)) + + def single(node, seqno, fp): + parents = [p for p in repo.changelog.parents(node) if p != nullid] + if switch_parent: + parents.reverse() + prev = (parents and parents[0]) or nullid + change = repo.changelog.read(node) + + if not fp: + fp = cmdutil.make_file(repo, template, node, total=total, + seqno=seqno, revwidth=revwidth) + if fp not in (sys.stdout, repo.ui): + repo.ui.note("%s\n" % fp.name) + + fp.write("# HG changeset patch\n") + fp.write("# User %s\n" % change[1]) + fp.write("# Date %d %d\n" % change[2]) + fp.write("# Node ID %s\n" % hex(node)) + fp.write("# Parent %s\n" % hex(prev)) + if len(parents) > 1: + fp.write("# Parent %s\n" % hex(parents[1])) + fp.write(change[4].rstrip()) + fp.write("\n\n") + + diff(repo, prev, node, fp=fp, opts=opts) + if fp not in (sys.stdout, repo.ui): + fp.close() + + for seqno, cset in enumerate(revs): + single(cset, seqno, fp) diff --git a/mercurial/remoterepo.py b/mercurial/remoterepo.py --- a/mercurial/remoterepo.py +++ b/mercurial/remoterepo.py @@ -1,6 +1,6 @@ -# remoterepo - remote repositort proxy classes for mercurial +# remoterepo - remote repository proxy classes for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/repo.py b/mercurial/repo.py --- a/mercurial/repo.py +++ b/mercurial/repo.py @@ -1,6 +1,7 @@ # repo.py - repository base classes for mercurial # # Copyright 2005 Matt Mackall +# Copyright 2006 Vadim Gelfer # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/revlog.py b/mercurial/revlog.py --- a/mercurial/revlog.py +++ b/mercurial/revlog.py @@ -4,7 +4,7 @@ revlog.py - storage back-end for mercuri This provides efficient delta storage with O(1) retrieve and append and O(changes) merge between branches -Copyright 2005 Matt Mackall +Copyright 2005, 2006 Matt Mackall This software may be used and distributed according to the terms of the GNU General Public License, incorporated herein by reference. @@ -469,7 +469,8 @@ class revlog(object): return self.nodemap[node] except KeyError: raise RevlogError(_('%s: no node %s') % (self.indexfile, hex(node))) - def linkrev(self, node): return self.index[self.rev(node)][-4] + def linkrev(self, node): + return (node == nullid) and -1 or self.index[self.rev(node)][-4] def parents(self, node): if node == nullid: return (nullid, nullid) r = self.rev(node) @@ -743,13 +744,8 @@ class revlog(object): def lookup(self, id): """locate a node based on revision number or subset of hex nodeid""" - if id in self.nodemap: - return id if type(id) == type(0): - rev = id - if rev < 0: rev = self.count() + rev - if rev < 0 or rev >= self.count(): return None - return self.node(rev) + return self.node(id) try: rev = int(id) if str(rev) != id: raise ValueError @@ -762,10 +758,26 @@ class revlog(object): if hex(n).startswith(id): c.append(n) if len(c) > 1: raise RevlogError(_("Ambiguous identifier")) - if len(c) < 1: raise RevlogError(_("No match found")) - return c[0] + if len(c) == 1: return c[0] + + # might need fixing if we change hash lengths + if len(id) == 20 and id in self.nodemap: + return id + + raise RevlogError(_("No match found")) - return None + def cmp(self, node, text): + """compare text with a given file revision""" + p1, p2 = self.parents(node) + return hash(text, p1, p2) != node + + def makenode(self, node, text): + """calculate a file nodeid for text, descended or possibly + unchanged from node""" + + if self.cmp(node, text): + return hash(text, node, nullid) + return node def diff(self, a, b): """return a delta between two revisions""" diff --git a/mercurial/sshrepo.py b/mercurial/sshrepo.py --- a/mercurial/sshrepo.py +++ b/mercurial/sshrepo.py @@ -1,6 +1,6 @@ # sshrepo.py - ssh repository proxy class for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -13,7 +13,7 @@ demandload(globals(), "hg os re stat uti class sshrepository(remoterepository): def __init__(self, ui, path, create=0): - self.url = path + self._url = path self.ui = ui m = re.match(r'ssh://(([^@]+)@)?([^:/]+)(:(\d+))?(/(.*))?', path) @@ -48,6 +48,9 @@ class sshrepository(remoterepository): self.validate_repo(ui, sshcmd, args, remotecmd) + def url(self): + return self._url + def validate_repo(self, ui, sshcmd, args, remotecmd): cmd = '%s %s "%s -R %s serve --stdio"' cmd = cmd % (sshcmd, args, remotecmd, self.path) @@ -180,7 +183,7 @@ class sshrepository(remoterepository): return 1 return int(r) - def addchangegroup(self, cg, source): + def addchangegroup(self, cg, source, url): d = self.call("addchangegroup") if d: raise hg.RepoError(_("push refused: %s") % d) @@ -201,3 +204,5 @@ class sshrepository(remoterepository): def stream_out(self): return self.do_cmd('stream_out') + +instance = sshrepository diff --git a/mercurial/sshserver.py b/mercurial/sshserver.py --- a/mercurial/sshserver.py +++ b/mercurial/sshserver.py @@ -1,6 +1,7 @@ # sshserver.py - ssh protocol server support for mercurial # # Copyright 2005 Matt Mackall +# Copyright 2006 Vadim Gelfer # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. @@ -117,9 +118,13 @@ class sshserver(object): return self.respond("") - r = self.repo.addchangegroup(self.fin, 'serve') + r = self.repo.addchangegroup(self.fin, 'serve', self.client_url()) self.respond(str(r)) + def client_url(self): + client = os.environ.get('SSH_CLIENT', '').split(' ', 1)[0] + return 'remote:ssh:' + client + def do_unbundle(self): their_heads = self.getarg()[1].split() @@ -159,7 +164,7 @@ class sshserver(object): # push can proceed fp.seek(0) - r = self.repo.addchangegroup(fp, 'serve') + r = self.repo.addchangegroup(fp, 'serve', self.client_url()) self.respond(str(r)) finally: if not was_locked: diff --git a/mercurial/statichttprepo.py b/mercurial/statichttprepo.py --- a/mercurial/statichttprepo.py +++ b/mercurial/statichttprepo.py @@ -2,14 +2,15 @@ # # This provides read-only repo access to repositories exported via static http # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -from demandload import demandload +from demandload import * +from i18n import gettext as _ demandload(globals(), "changelog filelog httprangereader") -demandload(globals(), "localrepo manifest os urllib urllib2") +demandload(globals(), "localrepo manifest os urllib urllib2 util") class rangereader(httprangereader.httprangereader): def read(self, size=None): @@ -30,6 +31,7 @@ def opener(base): class statichttprepository(localrepo.localrepository): def __init__(self, ui, path): + self._url = path self.path = (path + "/.hg") self.ui = ui self.revlogversion = 0 @@ -41,8 +43,22 @@ class statichttprepository(localrepo.loc self.encodepats = None self.decodepats = None + def url(self): + return 'static-' + self._url + def dev(self): return -1 def local(self): return False + +def instance(ui, path, create): + if create: + raise util.Abort(_('cannot create new static-http repository')) + if path.startswith('old-http:'): + ui.warn(_("old-http:// syntax is deprecated, " + "please use static-http:// instead\n")) + path = path[4:] + else: + path = path[7:] + return statichttprepository(ui, path) diff --git a/mercurial/strutil.py b/mercurial/strutil.py new file mode 100644 --- /dev/null +++ b/mercurial/strutil.py @@ -0,0 +1,34 @@ +# strutil.py - string utilities for Mercurial +# +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +def findall(haystack, needle, start=0, end=None): + if end is None: + end = len(haystack) + if end < 0: + end += len(haystack) + if start < 0: + start += len(haystack) + while start < end: + c = haystack.find(needle, start, end) + if c == -1: + break + yield c + start = c + 1 + +def rfindall(haystack, needle, start=0, end=None): + if end is None: + end = len(haystack) + if end < 0: + end += len(haystack) + if start < 0: + start += len(haystack) + while end >= 0: + c = haystack.rfind(needle, start, end) + if c == -1: + break + yield c + end = c - 1 diff --git a/mercurial/templater.py b/mercurial/templater.py --- a/mercurial/templater.py +++ b/mercurial/templater.py @@ -241,6 +241,7 @@ def nl2br(text): return text.replace('\n', '
\n') def obfuscate(text): + text = unicode(text, 'utf-8', 'replace') return ''.join(['&#%d;' % ord(c) for c in text]) def domain(author): @@ -458,7 +459,7 @@ class changeset_templater(object): yield x if self.ui.debugflag: - files = self.repo.changes(log.parents(changenode)[0], changenode) + files = self.repo.status(log.parents(changenode)[0], changenode)[:3] def showfiles(**args): for x in showlist('file', files[0], **args): yield x def showadds(**args): diff --git a/mercurial/transaction.py b/mercurial/transaction.py --- a/mercurial/transaction.py +++ b/mercurial/transaction.py @@ -6,7 +6,7 @@ # effectively log-structured, this should amount to simply truncating # anything that isn't referenced in the changelog. # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/mercurial/ui.py b/mercurial/ui.py --- a/mercurial/ui.py +++ b/mercurial/ui.py @@ -1,22 +1,24 @@ # ui.py - user interface bits for mercurial # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from i18n import gettext as _ from demandload import * -demandload(globals(), "errno getpass os re smtplib socket sys tempfile") -demandload(globals(), "ConfigParser templater traceback util") +demandload(globals(), "errno getpass os re socket sys tempfile") +demandload(globals(), "ConfigParser mdiff templater traceback util") class ui(object): def __init__(self, verbose=False, debug=False, quiet=False, - interactive=True, traceback=False, parentui=None): + interactive=True, traceback=False, parentui=None, + readhooks=[]): self.overlay = {} if parentui is None: # this is the parent of all ui children self.parentui = None + self.readhooks = list(readhooks) self.cdata = ConfigParser.SafeConfigParser() self.readconfig(util.rcpath()) @@ -34,6 +36,7 @@ class ui(object): else: # parentui may point to an ui object which is already a child self.parentui = parentui.parentui or parentui + self.readhooks = list(parentui.readhooks or readhooks) parent_cdata = self.parentui.cdata self.cdata = ConfigParser.SafeConfigParser(parent_cdata.defaults()) # make interpolation work @@ -78,6 +81,8 @@ class ui(object): for name, path in self.configitems("paths"): if path and "://" not in path and not os.path.isabs(path): self.cdata.set("paths", name, os.path.join(root, path)) + for hook in self.readhooks: + hook(self) def setconfig(self, section, name, val): self.overlay[(section, name)] = val @@ -169,17 +174,6 @@ class ui(object): result[key.lower()] = value return result - def diffopts(self): - if self.diffcache: - return self.diffcache - result = {'showfunc': True, 'ignorews': False, - 'ignorewsamount': False, 'ignoreblanklines': False} - for key, value in self.configitems("diff"): - if value: - result[key.lower()] = (value.lower() == 'true') - self.diffcache = result - return result - def username(self): """Return default username to be used in commits. @@ -197,7 +191,7 @@ class ui(object): user = os.environ.get("EMAIL") if user is None: try: - user = '%s@%s' % (getpass.getuser(), socket.getfqdn()) + user = '%s@%s' % (util.getuser(), socket.getfqdn()) except KeyError: raise util.Abort(_("Please specify a username.")) return user @@ -217,12 +211,6 @@ class ui(object): path = self.config("paths", default) return path or loc - def setconfig_remoteopts(self, **opts): - if opts.get('ssh'): - self.setconfig("ui", "ssh", opts['ssh']) - if opts.get('remotecmd'): - self.setconfig("ui", "remotecmd", opts['remotecmd']) - def write(self, *args): if self.header: if self.header != self.prev_header: @@ -298,62 +286,6 @@ class ui(object): return t - def sendmail(self): - '''send mail message. object returned has one method, sendmail. - call as sendmail(sender, list-of-recipients, msg).''' - - def smtp(): - '''send mail using smtp.''' - - local_hostname = self.config('smtp', 'local_hostname') - s = smtplib.SMTP(local_hostname=local_hostname) - mailhost = self.config('smtp', 'host') - if not mailhost: - raise util.Abort(_('no [smtp]host in hgrc - cannot send mail')) - mailport = int(self.config('smtp', 'port', 25)) - self.note(_('sending mail: smtp host %s, port %s\n') % - (mailhost, mailport)) - s.connect(host=mailhost, port=mailport) - if self.configbool('smtp', 'tls'): - self.note(_('(using tls)\n')) - s.ehlo() - s.starttls() - s.ehlo() - username = self.config('smtp', 'username') - password = self.config('smtp', 'password') - if username and password: - self.note(_('(authenticating to mail server as %s)\n') % - (username)) - s.login(username, password) - return s - - class sendmail(object): - '''send mail using sendmail.''' - - def __init__(self, ui, program): - self.ui = ui - self.program = program - - def sendmail(self, sender, recipients, msg): - cmdline = '%s -f %s %s' % ( - self.program, templater.email(sender), - ' '.join(map(templater.email, recipients))) - self.ui.note(_('sending mail: %s\n') % cmdline) - fp = os.popen(cmdline, 'w') - fp.write(msg) - ret = fp.close() - if ret: - raise util.Abort('%s %s' % ( - os.path.basename(self.program.split(None, 1)[0]), - util.explain_exit(ret)[0])) - - method = self.config('email', 'method', 'smtp') - if method == 'smtp': - mail = smtp() - else: - mail = sendmail(self, method) - return mail - def print_exc(self): '''print exception traceback if traceback printing enabled. only to call in exception handler. returns true if traceback diff --git a/mercurial/util.py b/mercurial/util.py --- a/mercurial/util.py +++ b/mercurial/util.py @@ -2,6 +2,8 @@ util.py - Mercurial utility functions and platform specfic implementations Copyright 2005 K. Thananchayan + Copyright 2005, 2006 Matt Mackall + Copyright 2006 Vadim Gelfer This software may be used and distributed according to the terms of the GNU General Public License, incorporated herein by reference. @@ -12,7 +14,7 @@ platform-specific details from the core. from i18n import gettext as _ from demandload import * -demandload(globals(), "cStringIO errno popen2 re shutil sys tempfile") +demandload(globals(), "cStringIO errno getpass popen2 re shutil sys tempfile") demandload(globals(), "os threading time") # used by parsedate @@ -93,23 +95,6 @@ def find_in_path(name, path, default=Non return p_name return default -def patch(strip, patchname, ui): - """apply the patch to the working directory. - a list of patched files is returned""" - patcher = find_in_path('gpatch', os.environ.get('PATH', ''), 'patch') - fp = os.popen('%s -p%d < "%s"' % (patcher, strip, patchname)) - files = {} - for line in fp: - line = line.rstrip() - ui.status("%s\n" % line) - if line.startswith('patching file '): - pf = parse_patch_output(line) - files.setdefault(pf, 1) - code = fp.close() - if code: - raise Abort(_("patch command failed: %s") % explain_exit(code)[0]) - return files.keys() - def binary(s): """return true if a string is binary data using diff's heuristic""" if s and '\0' in s[:4096]: @@ -510,6 +495,20 @@ def is_win_9x(): except AttributeError: return os.name == 'nt' and 'command' in os.environ.get('comspec', '') +getuser_fallback = None + +def getuser(): + '''return name of current user''' + try: + return getpass.getuser() + except ImportError: + # import of pwd will fail on windows - try fallback + if getuser_fallback: + return getuser_fallback() + # raised if win32api not available + raise Abort(_('user name not available - set USERNAME ' + 'environment variable')) + # Platform specific variants if os.name == 'nt': demandload(globals(), "msvcrt") @@ -593,6 +592,9 @@ if os.name == 'nt': def samestat(s1, s2): return False + def shellquote(s): + return '"%s"' % s.replace('"', '\\"') + def explain_exit(code): return _("exited with status %d") % code, code @@ -682,6 +684,9 @@ else: else: raise + def shellquote(s): + return "'%s'" % s.replace("'", "'\\''") + def testpid(pid): '''return False if pid dead, True if running or not sure''' try: @@ -982,3 +987,11 @@ def bytecount(nbytes): if nbytes >= divisor * multiplier: return format % (nbytes / float(divisor)) return units[-1][2] % nbytes + +def drop_scheme(scheme, path): + sc = scheme + ':' + if path.startswith(sc): + path = path[len(sc):] + if path.startswith('//'): + path = path[2:] + return path diff --git a/mercurial/util_win32.py b/mercurial/util_win32.py --- a/mercurial/util_win32.py +++ b/mercurial/util_win32.py @@ -297,3 +297,5 @@ class posixfile_nt(object): win32file.SetEndOfFile(self.handle) except pywintypes.error, err: raise WinIOError(err) + +getuser_fallback = win32api.GetUserName diff --git a/mercurial/verify.py b/mercurial/verify.py new file mode 100644 --- /dev/null +++ b/mercurial/verify.py @@ -0,0 +1,200 @@ +# verify.py - repository integrity checking for Mercurial +# +# Copyright 2006 Matt Mackall +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from node import * +from i18n import gettext as _ +import revlog, mdiff + +def verify(repo): + filelinkrevs = {} + filenodes = {} + changesets = revisions = files = 0 + errors = [0] + warnings = [0] + neededmanifests = {} + + def err(msg): + repo.ui.warn(msg + "\n") + errors[0] += 1 + + def warn(msg): + repo.ui.warn(msg + "\n") + warnings[0] += 1 + + def checksize(obj, name): + d = obj.checksize() + if d[0]: + err(_("%s data length off by %d bytes") % (name, d[0])) + if d[1]: + err(_("%s index contains %d extra bytes") % (name, d[1])) + + def checkversion(obj, name): + if obj.version != revlog.REVLOGV0: + if not revlogv1: + warn(_("warning: `%s' uses revlog format 1") % name) + elif revlogv1: + warn(_("warning: `%s' uses revlog format 0") % name) + + revlogv1 = repo.revlogversion != revlog.REVLOGV0 + if repo.ui.verbose or revlogv1 != repo.revlogv1: + repo.ui.status(_("repository uses revlog format %d\n") % + (revlogv1 and 1 or 0)) + + seen = {} + repo.ui.status(_("checking changesets\n")) + checksize(repo.changelog, "changelog") + + for i in range(repo.changelog.count()): + changesets += 1 + n = repo.changelog.node(i) + l = repo.changelog.linkrev(n) + if l != i: + err(_("incorrect link (%d) for changeset revision %d") %(l, i)) + if n in seen: + err(_("duplicate changeset at revision %d") % i) + seen[n] = 1 + + for p in repo.changelog.parents(n): + if p not in repo.changelog.nodemap: + err(_("changeset %s has unknown parent %s") % + (short(n), short(p))) + try: + changes = repo.changelog.read(n) + except KeyboardInterrupt: + repo.ui.warn(_("interrupted")) + raise + except Exception, inst: + err(_("unpacking changeset %s: %s") % (short(n), inst)) + continue + + neededmanifests[changes[0]] = n + + for f in changes[3]: + filelinkrevs.setdefault(f, []).append(i) + + seen = {} + repo.ui.status(_("checking manifests\n")) + checkversion(repo.manifest, "manifest") + checksize(repo.manifest, "manifest") + + for i in range(repo.manifest.count()): + n = repo.manifest.node(i) + l = repo.manifest.linkrev(n) + + if l < 0 or l >= repo.changelog.count(): + err(_("bad manifest link (%d) at revision %d") % (l, i)) + + if n in neededmanifests: + del neededmanifests[n] + + if n in seen: + err(_("duplicate manifest at revision %d") % i) + + seen[n] = 1 + + for p in repo.manifest.parents(n): + if p not in repo.manifest.nodemap: + err(_("manifest %s has unknown parent %s") % + (short(n), short(p))) + + try: + delta = mdiff.patchtext(repo.manifest.delta(n)) + except KeyboardInterrupt: + repo.ui.warn(_("interrupted")) + raise + except Exception, inst: + err(_("unpacking manifest %s: %s") % (short(n), inst)) + continue + + try: + ff = [ l.split('\0') for l in delta.splitlines() ] + for f, fn in ff: + filenodes.setdefault(f, {})[bin(fn[:40])] = 1 + except (ValueError, TypeError), inst: + err(_("broken delta in manifest %s: %s") % (short(n), inst)) + + repo.ui.status(_("crosschecking files in changesets and manifests\n")) + + for m, c in neededmanifests.items(): + err(_("Changeset %s refers to unknown manifest %s") % + (short(m), short(c))) + del neededmanifests + + for f in filenodes: + if f not in filelinkrevs: + err(_("file %s in manifest but not in changesets") % f) + + for f in filelinkrevs: + if f not in filenodes: + err(_("file %s in changeset but not in manifest") % f) + + repo.ui.status(_("checking files\n")) + ff = filenodes.keys() + ff.sort() + for f in ff: + if f == "/dev/null": + continue + files += 1 + if not f: + err(_("file without name in manifest %s") % short(n)) + continue + fl = repo.file(f) + checkversion(fl, f) + checksize(fl, f) + + nodes = {nullid: 1} + seen = {} + for i in range(fl.count()): + revisions += 1 + n = fl.node(i) + + if n in seen: + err(_("%s: duplicate revision %d") % (f, i)) + if n not in filenodes[f]: + err(_("%s: %d:%s not in manifests") % (f, i, short(n))) + else: + del filenodes[f][n] + + flr = fl.linkrev(n) + if flr not in filelinkrevs.get(f, []): + err(_("%s:%s points to unexpected changeset %d") + % (f, short(n), flr)) + else: + filelinkrevs[f].remove(flr) + + # verify contents + try: + t = fl.read(n) + except KeyboardInterrupt: + repo.ui.warn(_("interrupted")) + raise + except Exception, inst: + err(_("unpacking file %s %s: %s") % (f, short(n), inst)) + + # verify parents + (p1, p2) = fl.parents(n) + if p1 not in nodes: + err(_("file %s:%s unknown parent 1 %s") % + (f, short(n), short(p1))) + if p2 not in nodes: + err(_("file %s:%s unknown parent 2 %s") % + (f, short(n), short(p1))) + nodes[n] = 1 + + # cross-check + for node in filenodes[f]: + err(_("node %s in manifests not in %s") % (hex(node), f)) + + repo.ui.status(_("%d files, %d changesets, %d total revisions\n") % + (files, changesets, revisions)) + + if warnings[0]: + repo.ui.warn(_("%d warnings encountered!\n") % warnings[0]) + if errors[0]: + repo.ui.warn(_("%d integrity errors encountered!\n") % errors[0]) + return 1 + diff --git a/mercurial/version.py b/mercurial/version.py --- a/mercurial/version.py +++ b/mercurial/version.py @@ -1,4 +1,4 @@ -# Copyright (C) 2005 by Intevation GmbH +# Copyright (C) 2005, 2006 by Intevation GmbH # Author(s): # Thomas Arendsen Hein # diff --git a/templates/changelog-gitweb.tmpl b/templates/changelog-gitweb.tmpl --- a/templates/changelog-gitweb.tmpl +++ b/templates/changelog-gitweb.tmpl @@ -20,7 +20,7 @@ diff --git a/templates/changelog.tmpl b/templates/changelog.tmpl --- a/templates/changelog.tmpl +++ b/templates/changelog.tmpl @@ -6,6 +6,7 @@
+shortlog tags manifest #archives%archiveentry# diff --git a/templates/changeset-gitweb.tmpl b/templates/changeset-gitweb.tmpl --- a/templates/changeset-gitweb.tmpl +++ b/templates/changeset-gitweb.tmpl @@ -10,7 +10,7 @@
diff --git a/templates/changeset.tmpl b/templates/changeset.tmpl --- a/templates/changeset.tmpl +++ b/templates/changeset.tmpl @@ -5,6 +5,7 @@
changelog +shortlog tags manifest raw diff --git a/templates/error-gitweb.tmpl b/templates/error-gitweb.tmpl --- a/templates/error-gitweb.tmpl +++ b/templates/error-gitweb.tmpl @@ -10,7 +10,7 @@
diff --git a/templates/fileannotate-gitweb.tmpl b/templates/fileannotate-gitweb.tmpl --- a/templates/fileannotate-gitweb.tmpl +++ b/templates/fileannotate-gitweb.tmpl @@ -10,7 +10,7 @@
#file|escape#
diff --git a/templates/fileannotate.tmpl b/templates/fileannotate.tmpl --- a/templates/fileannotate.tmpl +++ b/templates/fileannotate.tmpl @@ -5,6 +5,7 @@
changelog +shortlog tags changeset manifest diff --git a/templates/filediff.tmpl b/templates/filediff.tmpl --- a/templates/filediff.tmpl +++ b/templates/filediff.tmpl @@ -5,6 +5,7 @@
changelog +shortlog tags changeset file diff --git a/templates/filelog-gitweb.tmpl b/templates/filelog-gitweb.tmpl --- a/templates/filelog-gitweb.tmpl +++ b/templates/filelog-gitweb.tmpl @@ -10,7 +10,7 @@
#file|urlescape#
diff --git a/templates/filelog.tmpl b/templates/filelog.tmpl --- a/templates/filelog.tmpl +++ b/templates/filelog.tmpl @@ -8,6 +8,7 @@
changelog +shortlog tags file annotate diff --git a/templates/filerevision-gitweb.tmpl b/templates/filerevision-gitweb.tmpl --- a/templates/filerevision-gitweb.tmpl +++ b/templates/filerevision-gitweb.tmpl @@ -10,7 +10,7 @@
#file|escape#
diff --git a/templates/filerevision.tmpl b/templates/filerevision.tmpl --- a/templates/filerevision.tmpl +++ b/templates/filerevision.tmpl @@ -5,6 +5,7 @@
changelog +shortlog tags changeset manifest diff --git a/templates/manifest-gitweb.tmpl b/templates/manifest-gitweb.tmpl --- a/templates/manifest-gitweb.tmpl +++ b/templates/manifest-gitweb.tmpl @@ -10,7 +10,7 @@
#path|escape#
diff --git a/templates/manifest.tmpl b/templates/manifest.tmpl --- a/templates/manifest.tmpl +++ b/templates/manifest.tmpl @@ -5,6 +5,7 @@
changelog +shortlog tags changeset #archives%archiveentry# diff --git a/templates/map b/templates/map --- a/templates/map +++ b/templates/map @@ -3,7 +3,10 @@ header = header.tmpl footer = footer.tmpl search = search.tmpl changelog = changelog.tmpl +shortlog = shortlog.tmpl +shortlogentry = shortlogentry.tmpl naventry = '#label|escape# ' +navshortentry = '#label|escape# ' filedifflink = '#file|escape# ' filenodelink = '#file|escape# ' fileellipses = '...' diff --git a/templates/search-gitweb.tmpl b/templates/search-gitweb.tmpl --- a/templates/search-gitweb.tmpl +++ b/templates/search-gitweb.tmpl @@ -1,6 +1,6 @@ #header#

searching for #query|escape#

diff --git a/templates/search.tmpl b/templates/search.tmpl --- a/templates/search.tmpl +++ b/templates/search.tmpl @@ -5,6 +5,7 @@ diff --git a/templates/shortlog-gitweb.tmpl b/templates/shortlog-gitweb.tmpl --- a/templates/shortlog-gitweb.tmpl +++ b/templates/shortlog-gitweb.tmpl @@ -1,13 +1,32 @@ #header# +#repo|escape#: Shortlog + + + + + +
+ +
+
-#entries# +#entries%shortlogentry#
#footer# diff --git a/templates/shortlog.tmpl b/templates/shortlog.tmpl new file mode 100644 --- /dev/null +++ b/templates/shortlog.tmpl @@ -0,0 +1,38 @@ +#header# +#repo|escape#: shortlog + + + + +
+changelog +tags +manifest +#archives%archiveentry# +rss +
+ +

shortlog for #repo|escape#

+ +
+

+ + + +navigate: #changenav%navshortentry# +

+
+ +#entries%shortlogentry# + +
+

+ + + +navigate: #changenav%navshortentry# +

+
+ +#footer# diff --git a/templates/shortlogentry.tmpl b/templates/shortlogentry.tmpl new file mode 100644 --- /dev/null +++ b/templates/shortlogentry.tmpl @@ -0,0 +1,7 @@ + + + + + + +
#date|age##author|obfuscate##desc|strip|firstline|escape#
diff --git a/templates/static/style.css b/templates/static/style.css --- a/templates/static/style.css +++ b/templates/static/style.css @@ -57,6 +57,12 @@ pre { margin: 0; } .logEntry th.age, .logEntry th.firstline { font-weight: bold; } .logEntry th.firstline { text-align: left; width: inherit; } +/* Shortlog entries */ +.slogEntry { width: 100%; font-size: smaller; } +.slogEntry .age { width: 7%; } +.slogEntry td { font-weight: normal; text-align: left; vertical-align: top; } +.slogEntry td.author { width: 35%; } + /* Tag entries */ #tagEntries { list-style: none; margin: 0; padding: 0; } #tagEntries .tagEntry { list-style: none; margin: 0; padding: 0; } diff --git a/templates/summary-gitweb.tmpl b/templates/summary-gitweb.tmpl --- a/templates/summary-gitweb.tmpl +++ b/templates/summary-gitweb.tmpl @@ -9,7 +9,8 @@
Mercurial
#repo|escape# / summary
 
diff --git a/templates/tags-gitweb.tmpl b/templates/tags-gitweb.tmpl --- a/templates/tags-gitweb.tmpl +++ b/templates/tags-gitweb.tmpl @@ -10,7 +10,7 @@
diff --git a/templates/tags.tmpl b/templates/tags.tmpl --- a/templates/tags.tmpl +++ b/templates/tags.tmpl @@ -7,6 +7,7 @@ diff --git a/tests/README b/tests/README --- a/tests/README +++ b/tests/README @@ -28,6 +28,6 @@ writing tests: - diff will show the current time - use hg diff | sed "s/\(\(---\|+++\) [a-zA-Z0-9_/.-]*\).*/\1/" to strip - dates - + use hg diff | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" + to strip dates diff --git a/tests/run-tests.py b/tests/run-tests.py --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -25,7 +25,7 @@ parser = optparse.OptionParser("%prog [o parser.add_option("-v", "--verbose", action="store_true", help="output verbose messages") parser.add_option("-t", "--timeout", type="int", - help="output verbose messages") + help="kill errant tests after TIMEOUT seconds") parser.add_option("-c", "--cover", action="store_true", help="print a test coverage report") parser.add_option("-s", "--cover_stdlib", action="store_true", @@ -201,6 +201,11 @@ def run(cmd): return ret, splitnewlines(output) def run_one(test): + '''tristate output: + None -> skipped + True -> passed + False -> failed''' + vlog("# Test", test) if not verbose: sys.stdout.write('.') @@ -217,15 +222,28 @@ def run_one(test): os.mkdir(tmpd) os.chdir(tmpd) - if test.endswith(".py"): - cmd = '%s "%s"' % (sys.executable, os.path.join(TESTDIR, test)) - else: - cmd = '"%s"' % (os.path.join(TESTDIR, test)) + lctest = test.lower() - # To reliably get the error code from batch files on WinXP, - # the "cmd /c call" prefix is needed. Grrr - if os.name == 'nt' and test.endswith(".bat"): + if lctest.endswith('.py'): + cmd = '%s "%s"' % (sys.executable, os.path.join(TESTDIR, test)) + elif lctest.endswith('.bat'): + # do not run batch scripts on non-windows + if os.name != 'nt': + print '\nSkipping %s: batch script' % test + return None + # To reliably get the error code from batch files on WinXP, + # the "cmd /c call" prefix is needed. Grrr cmd = 'cmd /c call "%s"' % (os.path.join(TESTDIR, test)) + else: + # do not run shell scripts on windows + if os.name == 'nt': + print '\nSkipping %s: shell script' % test + return None + # do not try to run non-executable programs + if not os.access(os.path.join(TESTDIR, test), os.X_OK): + print '\nSkipping %s: not executable' % test + return None + cmd = '"%s"' % (os.path.join(TESTDIR, test)) if options.timeout > 0: signal.alarm(options.timeout) @@ -244,7 +262,7 @@ def run_one(test): ref_out = splitnewlines(f.read()) f.close() else: - ref_out = [''] + ref_out = [] if out != ref_out: diffret = 1 print "\nERROR: %s output changed" % (test) @@ -330,16 +348,23 @@ try: tests = 0 failed = 0 + skipped = 0 if len(args) == 0: args = os.listdir(".") for test in args: - if test.startswith("test-") and not '~' in test and not '.' in test: - if not run_one(test): + if (test.startswith("test-") and '~' not in test and + ('.' not in test or test.endswith('.py') or + test.endswith('.bat'))): + ret = run_one(test) + if ret is None: + skipped += 1 + elif not ret: failed += 1 tests += 1 - print "\n# Ran %d tests, %d failed." % (tests, failed) + print "\n# Ran %d tests, %d skipped, %d failed." % (tests, skipped, + failed) if coverage: output_coverage() except KeyboardInterrupt: diff --git a/tests/test-abort-checkin b/tests/test-abort-checkin new file mode 100755 --- /dev/null +++ b/tests/test-abort-checkin @@ -0,0 +1,22 @@ +#!/bin/sh + +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "mq=" >> $HGTMP/.hgrc +cat > $HGTMP/false < foo +hg add foo + +# mq may keep a reference to the repository so __del__ will not be called +# and .hg/journal.dirstate will not be deleted: +HGEDITOR=$HGTMP/false hg ci +HGEDITOR=$HGTMP/false hg ci + +exit 0 diff --git a/tests/test-abort-checkin.out b/tests/test-abort-checkin.out new file mode 100644 --- /dev/null +++ b/tests/test-abort-checkin.out @@ -0,0 +1,6 @@ +abort: edit failed: false exited with status 1 +transaction abort! +rollback completed +abort: edit failed: false exited with status 1 +transaction abort! +rollback completed diff --git a/tests/test-annotate b/tests/test-annotate new file mode 100755 --- /dev/null +++ b/tests/test-annotate @@ -0,0 +1,23 @@ +#!/bin/sh + +echo % init +hg init + +echo % commit +echo 'a' > a +hg ci -A -m test -u nobody -d '1 0' + +echo % annotate -c +hg annotate -c a + +echo % annotate -d +hg annotate -d a + +echo % annotate -n +hg annotate -n a + +echo % annotate -u +hg annotate -u a + +echo % annotate -cdnu +hg annotate -cdnu a diff --git a/tests/test-annotate.out b/tests/test-annotate.out new file mode 100644 --- /dev/null +++ b/tests/test-annotate.out @@ -0,0 +1,13 @@ +% init +% commit +adding a +% annotate -c +8435f90966e4: a +% annotate -d +Thu Jan 01 00:00:01 1970 +0000: a +% annotate -n +0: a +% annotate -u +nobody: a +% annotate -cdnu +nobody 0 8435f90966e4 Thu Jan 01 00:00:01 1970 +0000: a diff --git a/tests/test-backout.out b/tests/test-backout.out --- a/tests/test-backout.out +++ b/tests/test-backout.out @@ -27,7 +27,7 @@ adding b reverting a changeset 3:4cbb1e70196a backs out changeset 1:22bca4c721e5 the backout changeset is a new head - do not forget to merge -(use "backout -m" if you want to auto-merge) +(use "backout --merge" if you want to auto-merge) b: No such file or directory adding a adding b diff --git a/tests/test-bisect b/tests/test-bisect new file mode 100755 --- /dev/null +++ b/tests/test-bisect @@ -0,0 +1,37 @@ +#!/bin/sh + +set -e + +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "hbisect=" >> $HGTMP/.hgrc + +echo % init +hg init + +echo % committing changes +count=0 +echo > a +while test $count -lt 32 ; do + echo 'a' >> a + test $count -eq 0 && hg add + hg ci -m "msg $count" -d "$count 0" + echo % committed changeset $count + count=`expr $count + 1` +done + +echo % log +hg log + +echo % hg up -C +hg up -C + +echo % bisect test +hg bisect init +hg bisect bad +hg bisect good 1 +hg bisect good +hg bisect good +hg bisect good +hg bisect bad +hg bisect good diff --git a/tests/test-bisect.out b/tests/test-bisect.out new file mode 100644 --- /dev/null +++ b/tests/test-bisect.out @@ -0,0 +1,216 @@ +% init +% committing changes +adding a +% committed changeset 0 +% committed changeset 1 +% committed changeset 2 +% committed changeset 3 +% committed changeset 4 +% committed changeset 5 +% committed changeset 6 +% committed changeset 7 +% committed changeset 8 +% committed changeset 9 +% committed changeset 10 +% committed changeset 11 +% committed changeset 12 +% committed changeset 13 +% committed changeset 14 +% committed changeset 15 +% committed changeset 16 +% committed changeset 17 +% committed changeset 18 +% committed changeset 19 +% committed changeset 20 +% committed changeset 21 +% committed changeset 22 +% committed changeset 23 +% committed changeset 24 +% committed changeset 25 +% committed changeset 26 +% committed changeset 27 +% committed changeset 28 +% committed changeset 29 +% committed changeset 30 +% committed changeset 31 +% log +changeset: 31:58c80a7c8a40 +tag: tip +user: test +date: Thu Jan 01 00:00:31 1970 +0000 +summary: msg 31 + +changeset: 30:ed2d2f24b11c +user: test +date: Thu Jan 01 00:00:30 1970 +0000 +summary: msg 30 + +changeset: 29:b5bd63375ab9 +user: test +date: Thu Jan 01 00:00:29 1970 +0000 +summary: msg 29 + +changeset: 28:8e0c2264c8af +user: test +date: Thu Jan 01 00:00:28 1970 +0000 +summary: msg 28 + +changeset: 27:288867a866e9 +user: test +date: Thu Jan 01 00:00:27 1970 +0000 +summary: msg 27 + +changeset: 26:3efc6fd51aeb +user: test +date: Thu Jan 01 00:00:26 1970 +0000 +summary: msg 26 + +changeset: 25:02a84173a97a +user: test +date: Thu Jan 01 00:00:25 1970 +0000 +summary: msg 25 + +changeset: 24:10e0acd3809e +user: test +date: Thu Jan 01 00:00:24 1970 +0000 +summary: msg 24 + +changeset: 23:5ec79163bff4 +user: test +date: Thu Jan 01 00:00:23 1970 +0000 +summary: msg 23 + +changeset: 22:06c7993750ce +user: test +date: Thu Jan 01 00:00:22 1970 +0000 +summary: msg 22 + +changeset: 21:e5db6aa3fe2a +user: test +date: Thu Jan 01 00:00:21 1970 +0000 +summary: msg 21 + +changeset: 20:7128fb4fdbc9 +user: test +date: Thu Jan 01 00:00:20 1970 +0000 +summary: msg 20 + +changeset: 19:52798545b482 +user: test +date: Thu Jan 01 00:00:19 1970 +0000 +summary: msg 19 + +changeset: 18:86977a90077e +user: test +date: Thu Jan 01 00:00:18 1970 +0000 +summary: msg 18 + +changeset: 17:03515f4a9080 +user: test +date: Thu Jan 01 00:00:17 1970 +0000 +summary: msg 17 + +changeset: 16:a2e6ea4973e9 +user: test +date: Thu Jan 01 00:00:16 1970 +0000 +summary: msg 16 + +changeset: 15:e7fa0811edb0 +user: test +date: Thu Jan 01 00:00:15 1970 +0000 +summary: msg 15 + +changeset: 14:ce8f0998e922 +user: test +date: Thu Jan 01 00:00:14 1970 +0000 +summary: msg 14 + +changeset: 13:9d7d07bc967c +user: test +date: Thu Jan 01 00:00:13 1970 +0000 +summary: msg 13 + +changeset: 12:1941b52820a5 +user: test +date: Thu Jan 01 00:00:12 1970 +0000 +summary: msg 12 + +changeset: 11:7b4cd9578619 +user: test +date: Thu Jan 01 00:00:11 1970 +0000 +summary: msg 11 + +changeset: 10:7c5eff49a6b6 +user: test +date: Thu Jan 01 00:00:10 1970 +0000 +summary: msg 10 + +changeset: 9:eb44510ef29a +user: test +date: Thu Jan 01 00:00:09 1970 +0000 +summary: msg 9 + +changeset: 8:453eb4dba229 +user: test +date: Thu Jan 01 00:00:08 1970 +0000 +summary: msg 8 + +changeset: 7:03750880c6b5 +user: test +date: Thu Jan 01 00:00:07 1970 +0000 +summary: msg 7 + +changeset: 6:a3d5c6fdf0d3 +user: test +date: Thu Jan 01 00:00:06 1970 +0000 +summary: msg 6 + +changeset: 5:7874a09ea728 +user: test +date: Thu Jan 01 00:00:05 1970 +0000 +summary: msg 5 + +changeset: 4:9b2ba8336a65 +user: test +date: Thu Jan 01 00:00:04 1970 +0000 +summary: msg 4 + +changeset: 3:b53bea5e2fcb +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +summary: msg 3 + +changeset: 2:db07c04beaca +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: msg 2 + +changeset: 1:5cd978ea5149 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: msg 1 + +changeset: 0:b99c7b9c8e11 +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +summary: msg 0 + +% hg up -C +0 files updated, 0 files merged, 0 files removed, 0 files unresolved +% bisect test +Testing changeset 16:a2e6ea4973e9 (30 changesets remaining, ~4 tests) +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +Testing changeset 23:5ec79163bff4 (15 changesets remaining, ~3 tests) +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +Testing changeset 27:288867a866e9 (8 changesets remaining, ~3 tests) +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +Testing changeset 29:b5bd63375ab9 (4 changesets remaining, ~2 tests) +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +Testing changeset 28:8e0c2264c8af (2 changesets remaining, ~1 tests) +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +The first bad revision is: +changeset: 29:b5bd63375ab9 +user: test +date: Thu Jan 01 00:00:29 1970 +0000 +summary: msg 29 + diff --git a/tests/test-bundle b/tests/test-bundle --- a/tests/test-bundle +++ b/tests/test-bundle @@ -30,14 +30,20 @@ cd .. hg init empty hg -R test bundle full.hg empty hg -R test unbundle full.hg -hg -R empty unbundle full.hg hg -R empty heads hg -R empty verify +hg --cwd test pull ../full.hg +hg --cwd empty pull ../full.hg +hg -R empty rollback +hg --cwd empty pull ../full.hg + rm -rf empty hg init empty cd empty hg -R bundle://../full.hg log +echo '[hooks]' >> .hg/hgrc +echo 'changegroup = echo changegroup: u=$HG_URL' >> .hg/hgrc #doesn't work (yet ?) #hg -R bundle://../full.hg verify hg pull bundle://../full.hg diff --git a/tests/test-bundle.out b/tests/test-bundle.out --- a/tests/test-bundle.out +++ b/tests/test-bundle.out @@ -11,28 +11,34 @@ adding manifests adding file changes added 0 changesets with 0 changes to 4 files (run 'hg update' to get a working copy) +changeset: -1:000000000000 +tag: tip +user: +date: Thu Jan 01 00:00:00 1970 +0000 + +checking changesets +checking manifests +crosschecking files in changesets and manifests +checking files +0 files, 0 changesets, 0 total revisions +pulling from ../full.hg +searching for changes +no changes found +pulling from ../full.hg +requesting all changes adding changesets adding manifests adding file changes added 9 changesets with 7 changes to 4 files (+1 heads) (run 'hg heads' to see heads, 'hg merge' to merge) -changeset: 8:836ac62537ab -tag: tip -parent: 3:ac69c658229d -user: test -date: Mon Jan 12 13:46:40 1970 +0000 -summary: 0.3m - -changeset: 7:80fe151401c2 -user: test -date: Mon Jan 12 13:46:40 1970 +0000 -summary: 1.3m - -checking changesets -checking manifests -crosschecking files in changesets and manifests -checking files -4 files, 9 changesets, 7 total revisions +rolling back last transaction +pulling from ../full.hg +requesting all changes +adding changesets +adding manifests +adding file changes +added 9 changesets with 7 changes to 4 files (+1 heads) +(run 'hg heads' to see heads, 'hg merge' to merge) changeset: 8:836ac62537ab tag: tip parent: 3:ac69c658229d @@ -81,6 +87,7 @@ user: test date: Mon Jan 12 13:46:40 1970 +0000 summary: 0.0 +changegroup: u=bundle:../full.hg pulling from bundle://../full.hg requesting all changes adding changesets diff --git a/tests/test-diff-subdir b/tests/test-diff-subdir new file mode 100755 --- /dev/null +++ b/tests/test-diff-subdir @@ -0,0 +1,27 @@ +#!/bin/sh + +hg init + +mkdir alpha +touch alpha/one +mkdir beta +touch beta/two + +hg add alpha/one beta/two +hg ci -m "start" -d "1000000 0" + +echo 1 > alpha/one +echo 2 > beta/two + +echo EVERYTHING +hg diff | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" + +echo BETA ONLY +hg diff beta | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" + +echo INSIDE BETA +cd beta +hg diff . | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" diff --git a/tests/test-diff-subdir.out b/tests/test-diff-subdir.out new file mode 100644 --- /dev/null +++ b/tests/test-diff-subdir.out @@ -0,0 +1,23 @@ +EVERYTHING +diff -r ec612a6291f1 alpha/one +--- a/alpha/one ++++ b/alpha/one +@@ -0,0 +1,1 @@ ++1 +diff -r ec612a6291f1 beta/two +--- a/beta/two ++++ b/beta/two +@@ -0,0 +1,1 @@ ++2 +BETA ONLY +diff -r ec612a6291f1 beta/two +--- a/beta/two ++++ b/beta/two +@@ -0,0 +1,1 @@ ++2 +INSIDE BETA +diff -r ec612a6291f1 beta/two +--- a/beta/two ++++ b/beta/two +@@ -0,0 +1,1 @@ ++2 diff --git a/tests/test-extdiff b/tests/test-extdiff new file mode 100755 --- /dev/null +++ b/tests/test-extdiff @@ -0,0 +1,30 @@ +#!/bin/sh + +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "extdiff=" >> $HGTMP/.hgrc + +hg init a +cd a +echo a > a +hg add +diff -N /dev/null /dev/null 2> /dev/null +if [ $? -ne 0 ]; then + opt="-p gdiff" +fi +hg extdiff -o -Nr $opt + +echo "[extdiff]" >> $HGTMP/.hgrc +echo "cmd.falabala=echo" >> $HGTMP/.hgrc +echo "opts.falabala=diffing" >> $HGTMP/.hgrc + +hg falabala + +hg help falabala + +hg ci -d '0 0' -mtest1 + +echo b >> a +hg ci -d '1 0' -mtest2 + +hg falabala -r 0:1 || echo "diff-like tools yield a non-zero exit code" diff --git a/tests/test-extdiff.out b/tests/test-extdiff.out new file mode 100644 --- /dev/null +++ b/tests/test-extdiff.out @@ -0,0 +1,32 @@ +adding a +making snapshot of 0 files from rev 000000000000 +making snapshot of 1 files from working dir +diff -Nr a.000000000000/a a/a +0a1 +> a +making snapshot of 0 files from rev 000000000000 +making snapshot of 1 files from working dir +diffing a.000000000000 a +hg falabala [OPT]... [FILE]... + +use 'echo' to diff repository (or selected files) + + Show differences between revisions for the specified + files, using the 'echo' program. + + When two revision arguments are given, then changes are + shown between those revisions. If only one revision is + specified then that revision is compared to the working + directory, and, when no revisions are specified, the + working directory files are compared to its parent. + +options: + + -o --option pass option to comparison program + -r --rev revision + -I --include include names matching the given patterns + -X --exclude exclude names matching the given patterns +making snapshot of 1 files from rev e27a2475d60a +making snapshot of 1 files from rev 5e49ec8d3f05 +diffing a.e27a2475d60a a.5e49ec8d3f05 +diff-like tools yield a non-zero exit code diff --git a/tests/test-fetch b/tests/test-fetch new file mode 100755 --- /dev/null +++ b/tests/test-fetch @@ -0,0 +1,25 @@ +#!/bin/sh + +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "fetch=" >> $HGTMP/.hgrc + +hg init a +echo a > a/a +hg --cwd a commit -d '1 0' -Ama + +hg clone a b +hg clone a c + +echo b > a/b +hg --cwd a commit -d '2 0' -Amb +hg --cwd a parents -q + +echo % should pull one change +hg --cwd b fetch ../a +hg --cwd b parents -q + +echo c > c/c +hg --cwd c commit -d '3 0' -Amc +hg --cwd c fetch -d '4 0' -m 'automated merge' ../a +ls c diff --git a/tests/test-fetch.out b/tests/test-fetch.out new file mode 100644 --- /dev/null +++ b/tests/test-fetch.out @@ -0,0 +1,27 @@ +adding a +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +adding b +1:97d72e5f12c7 +% should pull one change +pulling from ../a +searching for changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +1:97d72e5f12c7 +adding c +pulling from ../a +searching for changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files (+1 heads) +merging with new head 2:97d72e5f12c7 +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +new changeset 3:cd3a41621cf0 merges remote changes with local +a +b +c diff --git a/tests/test-git-export b/tests/test-git-export new file mode 100755 --- /dev/null +++ b/tests/test-git-export @@ -0,0 +1,52 @@ +#!/bin/sh + +hg init a +cd a + +echo start > start +hg ci -Amstart -d '0 0' +echo new > new +hg ci -Amnew -d '0 0' +echo '% new file' +hg diff --git -r 0 | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" + +hg cp new copy +hg ci -mcopy -d '0 0' +echo '% copy' +hg diff --git -r 1:tip | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" + +hg mv copy rename +hg ci -mrename -d '0 0' +echo '% rename' +hg diff --git -r 2:tip | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" + +hg rm rename +hg ci -mdelete -d '0 0' +echo '% delete' +hg diff --git -r 3:tip | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" + +cat > src <> dst +hg ci -mrenamemod -d '0 0' +echo '% rename+mod+chmod' +hg diff --git -r 6:tip | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" diff --git a/tests/test-git-export.out b/tests/test-git-export.out new file mode 100644 --- /dev/null +++ b/tests/test-git-export.out @@ -0,0 +1,42 @@ +adding start +adding new +% new file +diff --git a/new b/new +new file mode 100644 +--- /dev/null ++++ b/new +@@ -0,0 +1,1 @@ ++new +% copy +diff --git a/new b/copy +copy from new +copy to copy +% rename +diff --git a/copy b/rename +rename from copy +rename to rename +% delete +diff --git a/rename b/rename +deleted file mode 100644 +--- a/rename ++++ /dev/null +@@ -1,1 +0,0 @@ +-new +adding src +% chmod 644 +diff --git a/src b/src +old mode 100644 +new mode 100755 +% rename+mod+chmod +diff --git a/src b/dst +old mode 100755 +new mode 100644 +rename from src +rename to dst +--- a/dst ++++ b/dst +@@ -3,3 +3,4 @@ 3 + 3 + 4 + 5 ++a diff --git a/tests/test-git-import b/tests/test-git-import new file mode 100755 --- /dev/null +++ b/tests/test-git-import @@ -0,0 +1,122 @@ +#!/bin/sh + +hg init a +cd a + +echo % new file +hg import -mnew - < /dev/null && echo quuxfoo hg --cwd c --config '' tip -q hg --cwd c --config a.b tip -q hg --cwd c --config a tip -q diff --git a/tests/test-grep b/tests/test-grep --- a/tests/test-grep +++ b/tests/test-grep @@ -18,6 +18,13 @@ head -n 3 port > port1 mv port1 port hg commit -m 4 -u spam -d '4 0' hg grep port port -echo 'FIXME: history is wrong here' hg grep --all -nu port port hg grep import port + +hg cp port port2 +hg commit -m 4 -u spam -d '5 0' +echo '% follow' +hg grep -f 'import$' port2 +echo deport >> port2 +hg commit -m 5 -u eggs -d '6 0' +hg grep -f --all -nu port port2 diff --git a/tests/test-grep.out b/tests/test-grep.out --- a/tests/test-grep.out +++ b/tests/test-grep.out @@ -1,10 +1,25 @@ port:4:export port:4:vaportight port:4:import/export -FIXME: history is wrong here -port:1:1:-:eggs:import -port:1:2:+:eggs:vaportight -port:1:3:+:eggs:import/export -port:0:2:+:spam:export -port:0:1:+:spam:import +port:4:4:-:spam:import/export +port:3:4:+:eggs:import/export +port:2:1:-:spam:import +port:2:2:-:spam:export +port:2:1:+:spam:export +port:2:2:+:spam:vaportight +port:2:3:+:spam:import/export +port:1:2:+:eggs:export +port:0:1:+:eggs:import port:4:import/export +% follow +port:0:import +port2:6:4:+:eggs:deport +port:4:4:-:spam:import/export +port:3:4:+:eggs:import/export +port:2:1:-:spam:import +port:2:2:-:spam:export +port:2:1:+:spam:export +port:2:2:+:spam:vaportight +port:2:3:+:spam:import/export +port:1:2:+:eggs:export +port:0:1:+:eggs:import diff --git a/tests/test-help.out b/tests/test-help.out --- a/tests/test-help.out +++ b/tests/test-help.out @@ -178,6 +178,7 @@ options: -r --rev revision -a --text treat all files as text -p --show-function show which function each change is in + -g --git use git extended diff format -w --ignore-all-space ignore white space when comparing lines -b --ignore-space-change ignore changes in the amount of white space -B --ignore-blank-lines ignore changes whose lines are all blank @@ -187,13 +188,15 @@ hg status [OPTION]... [FILE]... show changed files in the working directory - Show changed files in the repository. If names are - given, only files that match are shown. + Show status of files in the repository. If names are given, only + files that match are shown. Files that are clean or ignored, are + not listed unless -c (clean), -i (ignored) or -A is given. The codes used to show the status of files are: M = modified A = added R = removed + C = clean ! = deleted, but still tracked ? = not tracked I = ignored (not shown by default) @@ -203,10 +206,12 @@ aliases: st options: + -A --all show status of all files -m --modified show only modified files -a --added show only added files -r --removed show only removed files -d --deleted show only deleted (but tracked) files + -c --clean show only files without changes -u --unknown show only unknown (not tracked) files -i --ignored show ignored files -n --no-status hide status prefix diff --git a/tests/test-hook b/tests/test-hook --- a/tests/test-hook +++ b/tests/test-hook @@ -17,9 +17,9 @@ cd ../b # changegroup hooks can see env vars echo '[hooks]' > .hg/hgrc -echo 'prechangegroup = echo prechangegroup hook' >> .hg/hgrc -echo 'changegroup = echo changegroup hook: n=$HG_NODE' >> .hg/hgrc -echo 'incoming = echo incoming hook: n=$HG_NODE' >> .hg/hgrc +echo 'prechangegroup = echo prechangegroup hook: u=`echo $HG_URL | sed s,file:.*,file:,`' >> .hg/hgrc +echo 'changegroup = echo changegroup hook: n=$HG_NODE u=`echo $HG_URL | sed s,file:.*,file:,`' >> .hg/hgrc +echo 'incoming = echo incoming hook: n=$HG_NODE u=`echo $HG_URL | sed s,file:.*,file:,`' >> .hg/hgrc # pretxncommit and commit hooks can see both parents of merge cd ../a diff --git a/tests/test-hook.out b/tests/test-hook.out --- a/tests/test-hook.out +++ b/tests/test-hook.out @@ -22,11 +22,11 @@ pretxncommit hook: n=4c52fb2e402287dd5dc 3:4c52fb2e4022 commit hook: n=4c52fb2e402287dd5dc052090682536c8406c321 p1=1324a5531bac09b329c3845d35ae6a7526874edb p2=b702efe9688826e3a91283852b328b84dbf37bc2 commit hook b -prechangegroup hook -changegroup hook: n=b702efe9688826e3a91283852b328b84dbf37bc2 -incoming hook: n=b702efe9688826e3a91283852b328b84dbf37bc2 -incoming hook: n=1324a5531bac09b329c3845d35ae6a7526874edb -incoming hook: n=4c52fb2e402287dd5dc052090682536c8406c321 +prechangegroup hook: u=file: +changegroup hook: n=b702efe9688826e3a91283852b328b84dbf37bc2 u=file: +incoming hook: n=b702efe9688826e3a91283852b328b84dbf37bc2 u=file: +incoming hook: n=1324a5531bac09b329c3845d35ae6a7526874edb u=file: +incoming hook: n=4c52fb2e402287dd5dc052090682536c8406c321 u=file: pulling from ../a searching for changes adding changesets diff --git a/tests/test-http b/tests/test-http --- a/tests/test-http +++ b/tests/test-http @@ -4,22 +4,31 @@ hg init test cd test echo foo>foo hg commit -A -d '0 0' -m 1 -hg --config server.uncompressed=True serve -p 20059 -d --pid-file=hg1.pid -cat hg1.pid >> $DAEMON_PIDS -hg serve -p 20060 -d --pid-file=hg2.pid -cat hg2.pid >> $DAEMON_PIDS +hg --config server.uncompressed=True serve -p 20059 -d --pid-file=../hg1.pid +hg serve -p 20060 -d --pid-file=../hg2.pid cd .. +cat hg1.pid hg2.pid >> $DAEMON_PIDS echo % clone via stream http_proxy= hg clone --uncompressed http://localhost:20059/ copy 2>&1 | \ sed -e 's/[0-9][0-9.]*/XXX/g' -cd copy -hg verify +hg verify -R copy echo % try to clone via stream, should use pull instead http_proxy= hg clone --uncompressed http://localhost:20060/ copy2 echo % clone via pull http_proxy= hg clone http://localhost:20059/ copy-pull +hg verify -R copy-pull + +cd test +echo bar > bar +hg commit -A -d '1 0' -m 2 +cd .. + +echo % pull cd copy-pull -hg verify +echo '[hooks]' >> .hg/hgrc +echo 'changegroup = echo changegroup: u=$HG_URL' >> .hg/hgrc +hg pull +cd .. diff --git a/tests/test-http.out b/tests/test-http.out --- a/tests/test-http.out +++ b/tests/test-http.out @@ -28,3 +28,13 @@ checking manifests crosschecking files in changesets and manifests checking files 1 files, 1 changesets, 1 total revisions +adding bar +% pull +changegroup: u=http://localhost:20059/ +pulling from http://localhost:20059/ +searching for changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +(run 'hg update' to get a working copy) diff --git a/tests/test-import b/tests/test-import --- a/tests/test-import +++ b/tests/test-import @@ -1,7 +1,10 @@ #!/bin/sh hg init a +mkdir a/d1 +mkdir a/d1/d2 echo line 1 > a/a +echo line 1 > a/d1/d2/a hg --cwd a ci -d '0 0' -Ama echo line 2 >> a/a @@ -69,7 +72,7 @@ rm -rf b echo % plain diff in email, no subject, no message body, should fail hg clone -r0 a b -grep -v '^\(Subject\|email\)' msg.patch | hg --cwd b import - +egrep -v '^(Subject|email)' msg.patch | hg --cwd b import - rm -rf b echo % hg export in email, should use patch header @@ -79,3 +82,20 @@ python mkmsg.py | hg --cwd b import - hg --cwd b tip | grep second rm -rf b +# bug non regression test +# importing a patch in a subdirectory failed at the commit stage +echo line 2 >> a/d1/d2/a +hg --cwd a ci -u someoneelse -d '1 0' -m'subdir change' +echo % hg import in a subdirectory +hg clone -r0 a b +hg --cwd a export tip | sed -e 's/d1\/d2\///' > tip.patch +dir=`pwd` +cd b/d1/d2 2>&1 > /dev/null +hg import ../../../tip.patch +cd $dir +echo "% message should be 'subdir change'" +hg --cwd b tip | grep 'subdir change' +echo "% committer should be 'someoneelse'" +hg --cwd b tip | grep someoneelse +echo "% should be empty" +hg --cwd b status diff --git a/tests/test-import.out b/tests/test-import.out --- a/tests/test-import.out +++ b/tests/test-import.out @@ -1,13 +1,13 @@ adding a +adding d1/d2/a % import exported patch requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying ../tip.patch -patching file a % message should be same summary: second change % committer should be same @@ -17,10 +17,9 @@ requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying ../tip.patch -patching file a transaction abort! rollback completed % import of plain diff should be ok with message @@ -28,38 +27,34 @@ requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying ../tip.patch -patching file a % import from stdin requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying patch from stdin -patching file a % override commit message requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying patch from stdin -patching file a summary: override % plain diff in email, subject, message body requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying ../msg.patch -patching file a user: email patcher summary: email patch % plain diff in email, no subject, message body @@ -67,28 +62,25 @@ requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying patch from stdin -patching file a % plain diff in email, subject, no message body requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying patch from stdin -patching file a % plain diff in email, no subject, no message body, should fail requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying patch from stdin -patching file a transaction abort! rollback completed % hg export in email, should use patch header @@ -96,8 +88,20 @@ requesting all changes adding changesets adding manifests adding file changes -added 1 changesets with 1 changes to 1 files -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying patch from stdin -patching file a summary: second change +% hg import in a subdirectory +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying ../../../tip.patch +% message should be 'subdir change' +summary: subdir change +% committer should be 'someoneelse' +user: someoneelse +% should be empty diff --git a/tests/test-issue322 b/tests/test-issue322 new file mode 100755 --- /dev/null +++ b/tests/test-issue322 @@ -0,0 +1,49 @@ +#!/bin/sh +# http://www.selenic.com/mercurial/bts/issue322 + +echo % file replaced with directory + +hg init a +cd a +echo a > a +hg commit -Ama +rm a +mkdir a +echo a > a/a + +echo % should fail - would corrupt dirstate +hg add a/a + +cd .. + +echo % directory replaced with file + +hg init c +cd c +mkdir a +echo a > a/a +hg commit -Ama + +rm -rf a +echo a > a + +echo % should fail - would corrupt dirstate +hg add a + +cd .. + +echo % directory replaced with file + +hg init d +cd d +mkdir b +mkdir b/c +echo a > b/c/d +hg commit -Ama +rm -rf b +echo a > b + +echo % should fail - would corrupt dirstate +hg add b + +exit 0 diff --git a/tests/test-issue322.out b/tests/test-issue322.out new file mode 100644 --- /dev/null +++ b/tests/test-issue322.out @@ -0,0 +1,12 @@ +% file replaced with directory +adding a +% should fail - would corrupt dirstate +abort: file named 'a' already in dirstate +% directory replaced with file +adding a/a +% should fail - would corrupt dirstate +abort: directory named 'a' already in dirstate +% directory replaced with file +adding b/c/d +% should fail - would corrupt dirstate +abort: directory named 'b' already in dirstate diff --git a/tests/test-log b/tests/test-log new file mode 100755 --- /dev/null +++ b/tests/test-log @@ -0,0 +1,68 @@ +#!/bin/sh + +hg init a + +cd a +echo a > a +hg ci -Ama -d '1 0' + +hg cp a b +hg ci -mb -d '2 0' + +mkdir dir +hg mv b dir +hg ci -mc -d '3 0' + +hg mv a b +hg ci -md -d '4 0' + +hg mv dir/b e +hg ci -me -d '5 0' + +hg log a +echo % -f, directory +hg log -f dir +echo % -f, but no args +hg log -f +echo % one rename +hg log -vf a +echo % many renames +hg log -vf e + +# log --follow tests +hg init ../follow +cd ../follow +echo base > base +hg ci -Ambase -d '1 0' + +echo r1 >> base +hg ci -Amr1 -d '1 0' +echo r2 >> base +hg ci -Amr2 -d '1 0' + +hg up -C 1 +echo b1 > b1 +hg ci -Amb1 -d '1 0' + +echo % log -f +hg log -f + +hg up -C 0 +echo b2 > b2 +hg ci -Amb2 -d '1 0' + +echo % log -f -r 1:tip +hg log -f -r 1:tip + +hg up -C 3 +hg merge tip +hg ci -mm12 -d '1 0' + +echo postm >> b1 +hg ci -Amb1.1 -d'1 0' + +echo % log --follow-first +hg log --follow-first + +echo % log -P 2 +hg log -P 2 diff --git a/tests/test-log.out b/tests/test-log.out new file mode 100644 --- /dev/null +++ b/tests/test-log.out @@ -0,0 +1,177 @@ +adding a +changeset: 0:8580ff50825a +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: a + +% -f, directory +abort: can only follow copies/renames for explicit file names +% -f, but no args +changeset: 4:8c1c8408f737 +tag: tip +user: test +date: Thu Jan 01 00:00:05 1970 +0000 +summary: e + +changeset: 3:c4ba038c90ce +user: test +date: Thu Jan 01 00:00:04 1970 +0000 +summary: d + +changeset: 2:21fba396af4c +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +summary: c + +changeset: 1:c0296dabce9b +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: b + +changeset: 0:8580ff50825a +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: a + +% one rename +changeset: 0:8580ff50825a50c8f716709acdf8de0deddcd6ab +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +files: a +description: +a + + +% many renames +changeset: 4:8c1c8408f7371319750ea2d4fa7969828effbcf4 +tag: tip +user: test +date: Thu Jan 01 00:00:05 1970 +0000 +files: dir/b e +description: +e + + +changeset: 2:21fba396af4c801f9717de6c415b6cc9620437e8 +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +files: b dir/b +description: +c + + +changeset: 1:c0296dabce9bf0cd3fdd608de26693c91cd6bbf4 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +files: b +description: +b + + +changeset: 0:8580ff50825a50c8f716709acdf8de0deddcd6ab +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +files: a +description: +a + + +adding base +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +adding b1 +% log -f +changeset: 3:e62f78d544b4 +tag: tip +parent: 1:3d5bf5654eda +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: b1 + +changeset: 1:3d5bf5654eda +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: r1 + +changeset: 0:67e992f2c4f3 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: base + +1 files updated, 0 files merged, 1 files removed, 0 files unresolved +adding b2 +% log -f -r 1:tip +changeset: 1:3d5bf5654eda +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: r1 + +changeset: 2:60c670bf5b30 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: r2 + +changeset: 3:e62f78d544b4 +parent: 1:3d5bf5654eda +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: b1 + +2 files updated, 0 files merged, 1 files removed, 0 files unresolved +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +% log --follow-first +changeset: 6:2404bbcab562 +tag: tip +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: b1.1 + +changeset: 5:302e9dd6890d +parent: 3:e62f78d544b4 +parent: 4:ddb82e70d1a1 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: m12 + +changeset: 3:e62f78d544b4 +parent: 1:3d5bf5654eda +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: b1 + +changeset: 1:3d5bf5654eda +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: r1 + +changeset: 0:67e992f2c4f3 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: base + +% log -P 2 +changeset: 6:2404bbcab562 +tag: tip +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: b1.1 + +changeset: 5:302e9dd6890d +parent: 3:e62f78d544b4 +parent: 4:ddb82e70d1a1 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: m12 + +changeset: 4:ddb82e70d1a1 +parent: 0:67e992f2c4f3 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: b2 + +changeset: 3:e62f78d544b4 +parent: 1:3d5bf5654eda +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: b1 + diff --git a/tests/test-merge-default b/tests/test-merge-default new file mode 100755 --- /dev/null +++ b/tests/test-merge-default @@ -0,0 +1,40 @@ +#!/bin/sh + +hg init +echo a > a +hg commit -A -ma + +echo a >> a +hg commit -mb + +echo a >> a +hg commit -mc + +hg up 1 +echo a >> a +hg commit -md + +hg up 1 +echo a >> a +hg commit -me + +hg up 1 +echo % should fail because not at a head +hg merge + +hg up +echo % should fail because \> 2 heads +hg merge + +echo % should succeed +hg merge 2 +hg commit -mm1 + +echo % should succeed - 2 heads +hg merge +hg commit -mm2 + +echo % should fail because 1 head +hg merge + +true diff --git a/tests/test-merge-default.out b/tests/test-merge-default.out new file mode 100644 --- /dev/null +++ b/tests/test-merge-default.out @@ -0,0 +1,17 @@ +adding a +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +% should fail because not at a head +abort: repo has 3 heads - please merge with an explicit rev +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +% should fail because > 2 heads +abort: repo has 3 heads - please merge with an explicit rev +% should succeed +0 files updated, 0 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +% should succeed - 2 heads +0 files updated, 0 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +% should fail because 1 head +abort: there is nothing to merge - use "hg update" instead diff --git a/tests/test-merge5.out b/tests/test-merge5.out --- a/tests/test-merge5.out +++ b/tests/test-merge5.out @@ -1,6 +1,3 @@ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved removing b -this update spans a branch affecting the following files: - b -aborting update spanning branches! -(use 'hg merge' to merge across branches or 'hg update -C' to lose changes) +abort: update spans branches, use 'hg merge' or 'hg update -C' to lose changes diff --git a/tests/test-merge7.out b/tests/test-merge7.out --- a/tests/test-merge7.out +++ b/tests/test-merge7.out @@ -22,7 +22,7 @@ added 1 changesets with 1 changes to 1 f (run 'hg heads' to see heads, 'hg merge' to merge) merge: warning: conflicts during merge resolving manifests - force False allow True moddirstate True linear False + overwrite None branchmerge True partial False linear False ancestor 055d847dd401 local 2eded9ab0a5c remote 84cf5750dd20 test.txt versions differ, resolve merging test.txt diff --git a/tests/test-mq b/tests/test-mq new file mode 100755 --- /dev/null +++ b/tests/test-mq @@ -0,0 +1,155 @@ +#!/bin/sh + +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "mq=" >> $HGTMP/.hgrc + +echo % help +hg help mq + +hg init a +cd a +echo a > a +hg ci -Ama + +hg clone . ../k + +mkdir b +echo z > b/z +hg ci -Ama + +echo % qinit + +hg qinit + +cd .. +hg init b + +echo % -R qinit + +hg -R b qinit + +hg init c + +echo % qinit -c + +hg --cwd c qinit -c +hg -R c/.hg/patches st + +echo % qnew implies add + +hg -R c qnew test.patch +hg -R c/.hg/patches st + +cd a + +echo % qnew -m + +hg qnew -m 'foo bar' test.patch +cat .hg/patches/test.patch + +echo % qrefresh + +echo a >> a +hg qrefresh +sed -e "s/^\(diff -r \)\([a-f0-9]* \)/\1 x/" \ + -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/test.patch + +echo % qpop + +hg qpop + +echo % qpush + +hg qpush + +cd .. + +echo % pop/push outside repo + +hg -R a qpop +hg -R a qpush + +cd a +hg qnew test2.patch + +echo % qrefresh in subdir + +cd b +echo a > a +hg add a +hg qrefresh + +echo % pop/push -a in subdir + +hg qpop -a +hg --traceback qpush -a + +echo % qseries +hg qseries + +echo % qapplied +hg qapplied + +echo % qtop +hg qtop + +echo % qprev +hg qprev + +echo % qnext +hg qnext + +echo % pop, qnext, qprev, qapplied +hg qpop +hg qnext +hg qprev +hg qapplied + +echo % commit should fail +hg commit + +echo % push should fail +hg push ../../k + +echo % qunapplied +hg qunapplied + +echo % push should succeed +hg qpop -a +hg push ../../k + +echo % strip +cd ../../b +echo x>x +hg ci -Ama +hg strip tip 2>&1 | sed 's/\(saving bundle to \).*/\1/' +hg unbundle .hg/strip-backup/* + +cat >>$HGTMP/.hgrc < new +chmod +x new +hg add new +hg qrefresh +sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/new + +hg qnew -m'copy file' copy +hg cp new copy +hg qrefresh +sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/copy + +hg qpop +hg qpush +hg qdiff diff --git a/tests/test-mq-guards b/tests/test-mq-guards new file mode 100755 --- /dev/null +++ b/tests/test-mq-guards @@ -0,0 +1,101 @@ +#!/bin/sh + +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "mq=" >> $HGTMP/.hgrc + +hg init +hg qinit + +echo x > x +hg ci -Ama + +hg qnew a.patch +echo a > a +hg add a +hg qrefresh + +hg qnew b.patch +echo b > b +hg add b +hg qrefresh + +hg qnew c.patch +echo c > c +hg add c +hg qrefresh + +hg qpop -a + +echo % should fail +hg qguard +fail + +hg qpush +echo % should guard a.patch +hg qguard +a +echo % should print +a +hg qguard +hg qpop + +hg qguard a.patch +echo % should push b.patch +hg qpush + +hg qpop +hg qselect a +echo % should push a.patch +hg qpush + +hg qguard c.patch -a +echo % should print -a +hg qguard c.patch + +echo % should skip c.patch +hg qpush -a + +hg qguard -n c.patch +echo % should push c.patch +hg qpush -a + +hg qpop -a +hg qselect -n +echo % should push all +hg qpush -a + +hg qpop -a +hg qguard a.patch +1 +hg qguard b.patch +2 +hg qselect 1 +echo % should push a.patch, not b.patch +hg qpush +hg qpush +hg qpop -a + +hg qselect 2 +echo % should push b.patch +hg qpush +hg qpop -a + +hg qselect 1 2 +echo % should push a.patch, b.patch +hg qpush +hg qpush +hg qpop -a + +hg qguard a.patch +1 +2 -3 +hg qselect 1 2 3 +echo % list patches and guards +hg qguard -l +echo % list series +hg qseries -v +echo % list guards +hg qselect +echo % should push b.patch +hg qpush + +hg qpush -a +hg qselect -n --reapply +echo % guards in series file: +1 +2 -3 +hg qselect -s +echo % should show c.patch +hg qapplied diff --git a/tests/test-mq-guards.out b/tests/test-mq-guards.out new file mode 100644 --- /dev/null +++ b/tests/test-mq-guards.out @@ -0,0 +1,84 @@ +adding x +Patch queue now empty +% should fail +abort: no patches applied +applying a.patch +Now at: a.patch +% should guard a.patch +% should print +a +a.patch: +a +Patch queue now empty +a.patch: +a +% should push b.patch +applying b.patch +Now at: b.patch +Patch queue now empty +number of unguarded, unapplied patches has changed from 2 to 3 +% should push a.patch +applying a.patch +Now at: a.patch +% should print -a +c.patch: -a +% should skip c.patch +applying b.patch +skipping c.patch - guarded by '- a' +Now at: b.patch +% should push c.patch +applying c.patch +Now at: c.patch +Patch queue now empty +guards deactivated +number of unguarded, unapplied patches has changed from 3 to 2 +% should push all +applying b.patch +applying c.patch +Now at: c.patch +Patch queue now empty +number of unguarded, unapplied patches has changed from 1 to 2 +% should push a.patch, not b.patch +applying a.patch +Now at: a.patch +applying c.patch +Now at: c.patch +Patch queue now empty +% should push b.patch +applying b.patch +Now at: b.patch +Patch queue now empty +number of unguarded, unapplied patches has changed from 2 to 3 +% should push a.patch, b.patch +applying a.patch +Now at: a.patch +applying b.patch +Now at: b.patch +Patch queue now empty +number of unguarded, unapplied patches has changed from 3 to 2 +% list patches and guards +a.patch: +1 +2 -3 +b.patch: +2 +c.patch: unguarded +% list series +0 G a.patch +1 U b.patch +2 U c.patch +% list guards +1 +2 +3 +% should push b.patch +applying b.patch +Now at: b.patch +applying c.patch +Now at: c.patch +guards deactivated +popping guarded patches +Patch queue now empty +reapplying unguarded patches +applying c.patch +Now at: c.patch +% guards in series file: +1 +2 -3 ++1 ++2 +-3 +% should show c.patch +c.patch diff --git a/tests/test-mq-qdiff b/tests/test-mq-qdiff new file mode 100755 --- /dev/null +++ b/tests/test-mq-qdiff @@ -0,0 +1,28 @@ +#!/bin/sh + +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "mq=" >> $HGTMP/.hgrc + +echo % init +hg init a +cd a + +echo % commit +echo 'base' > base +hg ci -Ambase -d '1 0' + +echo % qnew mqbase +hg qnew -mmqbase mqbase + +echo % qrefresh +echo 'patched' > base +hg qrefresh + +echo % qdiff +hg qdiff | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" + +echo % qdiff dirname +hg qdiff . | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" diff --git a/tests/test-mq-qdiff.out b/tests/test-mq-qdiff.out new file mode 100644 --- /dev/null +++ b/tests/test-mq-qdiff.out @@ -0,0 +1,19 @@ +% init +% commit +adding base +% qnew mqbase +% qrefresh +% qdiff +diff -r 67e992f2c4f3 base +--- a/base ++++ b/base +@@ -1,1 +1,1 @@ base +-base ++patched +% qdiff dirname +diff -r 67e992f2c4f3 base +--- a/base ++++ b/base +@@ -1,1 +1,1 @@ base +-base ++patched diff --git a/tests/test-mq-qnew-twice b/tests/test-mq-qnew-twice new file mode 100755 --- /dev/null +++ b/tests/test-mq-qnew-twice @@ -0,0 +1,15 @@ +#!/bin/sh + +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "mq=" >> $HGTMP/.hgrc + +hg init a +cd a +hg qnew first.patch +hg qnew first.patch + +touch ../first.patch +hg qimport ../first.patch + +exit 0 diff --git a/tests/test-mq-qnew-twice.out b/tests/test-mq-qnew-twice.out new file mode 100644 --- /dev/null +++ b/tests/test-mq-qnew-twice.out @@ -0,0 +1,2 @@ +abort: patch "first.patch" already exists +abort: patch "first.patch" already exists diff --git a/tests/test-mq-qrefresh-replace-log-message b/tests/test-mq-qrefresh-replace-log-message new file mode 100755 --- /dev/null +++ b/tests/test-mq-qrefresh-replace-log-message @@ -0,0 +1,51 @@ +#!/bin/sh + +# Environement setup for MQ +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "mq=" >> $HGTMP/.hgrc + +#Repo init +hg init +hg qinit + +hg qnew -m "First commit message" first-patch +echo aaaa > file +hg add file +hg qrefresh +echo ======================= +echo "Should display 'First commit message'" +hg log -l1 -v | sed -n '/description/,$p' +echo + +# Testing changing message with -m +echo bbbb > file +hg qrefresh -m "Second commit message" +echo ======================= +echo "Should display 'Second commit message'" +hg log -l1 -v | sed -n '/description/,$p' +echo + + +# Testing changing message with -l +echo "Third commit message" > logfile +echo " This is the 3rd log message" >> logfile +echo bbbb > file +hg qrefresh -l logfile +echo ======================= +printf "Should display 'Third commit message\\\n This is the 3rd log message'\n" +hg log -l1 -v | sed -n '/description/,$p' +echo + +# Testing changing message with -l- +hg qnew -m "First commit message" second-patch +echo aaaa > file2 +hg add file2 +echo bbbb > file2 +(echo "Fifth commit message" +echo " This is the 5th log message" >> logfile) |\ +hg qrefresh -l- +echo ======================= +printf "Should display 'Fifth commit message\\\n This is the 5th log message'\n" +hg log -l1 -v | sed -n '/description/,$p' +echo diff --git a/tests/test-mq-qrefresh-replace-log-message.out b/tests/test-mq-qrefresh-replace-log-message.out new file mode 100644 --- /dev/null +++ b/tests/test-mq-qrefresh-replace-log-message.out @@ -0,0 +1,29 @@ +======================= +Should display 'First commit message' +description: +First commit message + + + +======================= +Should display 'Second commit message' +description: +Second commit message + + + +======================= +Should display 'Third commit message\n This is the 3rd log message' +description: +Third commit message + This is the 3rd log message + + + +======================= +Should display 'Fifth commit message\n This is the 5th log message' +description: +Fifth commit message + + + diff --git a/tests/test-mq-qsave b/tests/test-mq-qsave new file mode 100755 --- /dev/null +++ b/tests/test-mq-qsave @@ -0,0 +1,16 @@ +#!/bin/sh + +HGRCPATH=$HGTMP/.hgrc; export HGRCPATH +echo "[extensions]" >> $HGTMP/.hgrc +echo "mq=" >> $HGTMP/.hgrc + +hg init a +cd a + +echo 'base' > base +hg ci -Ambase -d '1 0' + +hg qnew -mmqbase mqbase + +hg qsave +hg qrestore 2 diff --git a/tests/test-mq-qsave.out b/tests/test-mq-qsave.out new file mode 100644 --- /dev/null +++ b/tests/test-mq-qsave.out @@ -0,0 +1,2 @@ +adding base +restoring status: hg patches saved state diff --git a/tests/test-mq.out b/tests/test-mq.out new file mode 100644 --- /dev/null +++ b/tests/test-mq.out @@ -0,0 +1,148 @@ +% help +mq extension - patch management and development + +This extension lets you work with a stack of patches in a Mercurial +repository. It manages two stacks of patches - all known patches, and +applied patches (subset of known patches). + +Known patches are represented as patch files in the .hg/patches +directory. Applied patches are both patch files and changesets. + +Common tasks (use "hg help command" for more details): + +prepare repository to work with patches qinit +create new patch qnew +import existing patch qimport + +print patch series qseries +print applied patches qapplied +print name of top applied patch qtop + +add known patch to applied stack qpush +remove patch from applied stack qpop +refresh contents of top applied patch qrefresh + +list of commands (use "hg help -v mq" to show aliases and global options): + + qapplied print the patches already applied + qclone clone main and patch repository at same time + qcommit commit changes in the queue repository + qdelete remove patches from queue + qdiff diff of the current patch + qfold fold the named patches into the current patch + qguard set or print guards for a patch + qheader Print the header of the topmost or specified patch + qimport import a patch + qinit init a new queue repository + qnew create a new patch + qnext print the name of the next patch + qpop pop the current patch off the stack + qprev print the name of the previous patch + qpush push the next patch onto the stack + qrefresh update the current patch + qrename rename a patch + qrestore restore the queue state saved by a rev + qsave save current queue state + qselect set or print guarded patches to push + qseries print the entire series file + qtop print the name of the current patch + qunapplied print the patches not yet applied + strip strip a revision and all later revs on the same branch +adding a +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +adding b/z +% qinit +% -R qinit +% qinit -c +A .hgignore +A series +% qnew implies add +A .hgignore +A series +A test.patch +% qnew -m +foo bar +% qrefresh +foo bar + +diff -r xa +--- a/a ++++ b/a +@@ -1,1 +1,2 @@ a + a ++a +% qpop +Patch queue now empty +% qpush +applying test.patch +Now at: test.patch +% pop/push outside repo +Patch queue now empty +applying test.patch +Now at: test.patch +% qrefresh in subdir +% pop/push -a in subdir +Patch queue now empty +applying test.patch +applying test2.patch +Now at: test2.patch +% qseries +test.patch +test2.patch +% qapplied +test.patch +test2.patch +% qtop +test2.patch +% qprev +test.patch +% qnext +All patches applied +% pop, qnext, qprev, qapplied +Now at: test.patch +test2.patch +Only one patch applied +test.patch +% commit should fail +abort: cannot commit over an applied mq patch +% push should fail +pushing to ../../k +abort: source has mq patches applied +% qunapplied +test2.patch +% push should succeed +Patch queue now empty +pushing to ../../k +searching for changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +% strip +adding x +0 files updated, 0 files merged, 1 files removed, 0 files unresolved +saving bundle to +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +(run 'hg update' to get a working copy) +new file + +diff --git a/new b/new +new file mode 100755 +--- /dev/null ++++ b/new +@@ -0,0 +1,1 @@ ++foo +copy file + +diff --git a/new b/copy +copy from new +copy to copy +Now at: new +applying copy +Now at: copy +diff --git a/new b/copy +copy from new +copy to copy diff --git a/tests/test-push-http b/tests/test-push-http --- a/tests/test-push-http +++ b/tests/test-push-http @@ -36,13 +36,19 @@ kill `cat hg.pid` echo % expect success echo 'allow_push = *' >> .hg/hgrc +echo '[hooks]' >> .hg/hgrc +echo 'changegroup = echo changegroup: u=$HG_URL >> $HGTMP/urls' >> .hg/hgrc hg serve -p 20059 -d --pid-file=hg.pid cat hg.pid >> $DAEMON_PIDS hg --cwd ../test2 push http://localhost:20059/ kill `cat hg.pid` hg rollback +sed 's/\(remote:http.*\):.*/\1/' $HGTMP/urls + echo % expect authorization error: all users denied +echo '[web]' > .hg/hgrc +echo 'push_ssl = false' >> .hg/hgrc echo 'deny_push = *' >> .hg/hgrc hg serve -p 20059 -d --pid-file=hg.pid cat hg.pid >> $DAEMON_PIDS diff --git a/tests/test-push-http.out b/tests/test-push-http.out --- a/tests/test-push-http.out +++ b/tests/test-push-http.out @@ -20,6 +20,7 @@ adding manifests adding file changes added 1 changesets with 1 changes to 1 files rolling back last transaction +changegroup: u=remote:http % expect authorization error: all users denied pushing to http://localhost:20059/ searching for changes diff --git a/tests/test-ssh b/tests/test-ssh --- a/tests/test-ssh +++ b/tests/test-ssh @@ -17,6 +17,8 @@ if [ ! -x dummyssh ] ; then exit -1 fi +SSH_CLIENT='127.0.0.1 1 2' +export SSH_CLIENT echo Got arguments 1:$1 2:$2 3:$3 4:$4 5:$5 >> dummylog $2 EOF @@ -29,6 +31,8 @@ echo this > foo hg ci -A -m "init" -d "1000000 0" foo echo '[server]' > .hg/hgrc echo 'uncompressed = True' >> .hg/hgrc +echo '[hooks]' >> .hg/hgrc +echo 'changegroup = echo changegroup in remote: u=$HG_URL >> ../dummylog' >> .hg/hgrc cd .. @@ -46,6 +50,9 @@ echo "# verify" cd local hg verify +echo '[hooks]' >> .hg/hgrc +echo 'changegroup = echo changegroup in local: u=$HG_URL >> ../dummylog' >> .hg/hgrc + echo "# empty default pull" hg paths hg pull -e ../dummyssh diff --git a/tests/test-ssh.out b/tests/test-ssh.out --- a/tests/test-ssh.out +++ b/tests/test-ssh.out @@ -83,5 +83,7 @@ Got arguments 1:user@dummy 2:hg -R remot Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: Got arguments 1:user@dummy 2:hg -R local serve --stdio 3: 4: 5: Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: +changegroup in remote: u=remote:ssh:127.0.0.1 Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: +changegroup in remote: u=remote:ssh:127.0.0.1 diff --git a/tests/test-static-http b/tests/test-static-http --- a/tests/test-static-http +++ b/tests/test-static-http @@ -37,6 +37,14 @@ http_proxy= hg clone static-http://local cd local hg verify cat bar + +cd ../remote +echo baz > quux +hg commit -A -mtest2 -d '100000000 0' + +cd ../local +echo '[hooks]' >> .hg/hgrc +echo 'changegroup = echo changegroup: u=$HG_URL' >> .hg/hgrc http_proxy= hg pull kill $! diff --git a/tests/test-static-http.out b/tests/test-static-http.out --- a/tests/test-static-http.out +++ b/tests/test-static-http.out @@ -19,6 +19,12 @@ crosschecking files in changesets and ma checking files 1 files, 1 changesets, 1 total revisions foo +adding quux +changegroup: u=static-http://localhost:20059/remote pulling from static-http://localhost:20059/remote searching for changes -no changes found +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +(run 'hg update' to get a working copy) diff --git a/tests/test-status b/tests/test-status --- a/tests/test-status +++ b/tests/test-status @@ -35,3 +35,8 @@ hg status modified added removed deleted hg copy modified copied echo "hg status -C:" hg status -C + +echo "hg status -t:" +hg status -t +echo "hg status -A:" +hg status -A diff --git a/tests/test-status.out b/tests/test-status.out --- a/tests/test-status.out +++ b/tests/test-status.out @@ -108,3 +108,50 @@ A copied R removed ! deleted ? unknown +hg status -t: +hg status: option -t not recognized +hg status [OPTION]... [FILE]... + +show changed files in the working directory + + Show status of files in the repository. If names are given, only + files that match are shown. Files that are clean or ignored, are + not listed unless -c (clean), -i (ignored) or -A is given. + + The codes used to show the status of files are: + M = modified + A = added + R = removed + C = clean + ! = deleted, but still tracked + ? = not tracked + I = ignored (not shown by default) + = the previous added file was copied from here + +aliases: st + +options: + + -A --all show status of all files + -m --modified show only modified files + -a --added show only added files + -r --removed show only removed files + -d --deleted show only deleted (but tracked) files + -c --clean show only files without changes + -u --unknown show only unknown (not tracked) files + -i --ignored show ignored files + -n --no-status hide status prefix + -C --copies show source of copied files + -0 --print0 end filenames with NUL, for use with xargs + -I --include include names matching the given patterns + -X --exclude exclude names matching the given patterns +hg status -A: +A added +A copied + modified +R removed +! deleted +? unknown +I ignored +C .hgignore +C modified diff --git a/tests/test-tag b/tests/test-tag --- a/tests/test-tag +++ b/tests/test-tag @@ -19,6 +19,11 @@ hg tag -l -d "1000000 0" "bleah1" 1 cat .hgtags cat .hg/localtags +hg update 0 +hg tag -d "1000000 0" "foobar" +cat .hgtags +cat .hg/localtags + hg tag -l 'xx newline' hg tag -l 'xx:xx' diff --git a/tests/test-tag.out b/tests/test-tag.out --- a/tests/test-tag.out +++ b/tests/test-tag.out @@ -25,5 +25,8 @@ use of 'hg tag NAME [REV]' is deprecated 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 bleah 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 bleah0 c5c60883086f5526bd3e36814b94a73a4e75e172 bleah1 +0 files updated, 0 files merged, 1 files removed, 0 files unresolved +0acdaf8983679e0aac16e811534eb49d7ee1f2b4 foobar +c5c60883086f5526bd3e36814b94a73a4e75e172 bleah1 abort: '\n' cannot be used in a tag name abort: ':' cannot be used in a tag name diff --git a/tests/test-up-local-change.out b/tests/test-up-local-change.out --- a/tests/test-up-local-change.out +++ b/tests/test-up-local-change.out @@ -15,7 +15,7 @@ date: Mon Jan 12 13:46:40 1970 +0 summary: 1 resolving manifests - force None allow None moddirstate True linear True + overwrite False branchmerge False partial False linear True ancestor a0c8bcbbb45c local a0c8bcbbb45c remote 1165e8bd193e a versions differ, resolve remote created b @@ -31,7 +31,7 @@ date: Mon Jan 12 13:46:40 1970 +0 summary: 2 resolving manifests - force None allow None moddirstate True linear True + overwrite False branchmerge False partial False linear True ancestor a0c8bcbbb45c local 1165e8bd193e remote a0c8bcbbb45c remote deleted b removing b @@ -41,7 +41,7 @@ user: test date: Mon Jan 12 13:46:40 1970 +0000 summary: 1 -abort: there is nothing to merge, just use 'hg update' or look at 'hg heads' +abort: there is nothing to merge - use "hg update" instead failed changeset: 0:33aaa84a386b user: test @@ -49,7 +49,7 @@ date: Mon Jan 12 13:46:40 1970 +0 summary: 1 resolving manifests - force None allow None moddirstate True linear True + overwrite False branchmerge False partial False linear True ancestor a0c8bcbbb45c local a0c8bcbbb45c remote 1165e8bd193e a versions differ, resolve remote created b @@ -95,21 +95,12 @@ user: test date: Mon Jan 12 13:46:40 1970 +0000 summary: 2 -resolving manifests - force None allow None moddirstate True linear False - ancestor a0c8bcbbb45c local 1165e8bd193e remote 4096f2872392 - a versions differ, resolve - b versions differ, resolve -this update spans a branch affecting the following files: - a (resolve) - b (resolve) -aborting update spanning branches! -(use 'hg merge' to merge across branches or 'hg update -C' to lose changes) +abort: update spans branches, use 'hg merge' or 'hg update -C' to lose changes failed abort: outstanding uncommitted changes failed resolving manifests - force False allow True moddirstate True linear False + overwrite False branchmerge True partial False linear False ancestor a0c8bcbbb45c local 1165e8bd193e remote 4096f2872392 a versions differ, resolve b versions differ, resolve diff --git a/tests/test-update-reverse.out b/tests/test-update-reverse.out --- a/tests/test-update-reverse.out +++ b/tests/test-update-reverse.out @@ -40,7 +40,7 @@ a side1 side2 resolving manifests - force 1 allow None moddirstate True linear False + overwrite True branchmerge False partial False linear False ancestor 8515d4bfda76 local 1c0f48f8ece6 remote 0594b9004bae remote deleted side2, clobbering remote deleted side1, clobbering