# HG changeset patch # User Josef "Jeff" Sipek # Date 1157277962 14400 # Node ID fcadf7a3242548f808c5587e65cc88c57b67637e # Parent 2a4d4aecb2b47fe1e70bcd996ca388d8aa664495# Parent 962b9c7df6416d411214f23498f2b5a312813577 Merge with mpm 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 @@ -10,6 +10,7 @@ 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 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/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/mercurial.el b/contrib/mercurial.el --- a/contrib/mercurial.el +++ b/contrib/mercurial.el @@ -718,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. 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/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 @@ -139,12 +139,13 @@ email:: Optional. Email address to use in "From" header and SMTP envelope of outgoing messages. to;; - Optional. Email address(es) of recipient(s). + Optional. Comma-separated list of recipients' email addresses. cc;; - Optional. Email address(es) to send carbon copies to. + Optional. Comma-separated list of carbon copy recipients' + email addresses. bcc;; - Optional. Email address(es) to send blind carbon copies to. - Cannot be set interactively. + 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 "[smtp]" for @@ -305,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;; 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,19 +5,24 @@ # 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) @@ -26,16 +31,23 @@ # #(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) @@ -79,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 @@ -92,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')) @@ -104,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 @@ -115,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": @@ -133,21 +151,25 @@ def uisetup(ui): if not cmd.startswith('cmd.'): continue cmd = cmd[4:] if not path: path = cmd - def save(cmd, path): + diffopts = ui.config('extdiff', 'opts.' + cmd, '') + diffopts = diffopts and [diffopts] or [] + def save(cmd, path, diffopts): '''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[cmd] = (save(cmd, path, diffopts), cmdtable['extdiff'][1][1:], _('hg %s [OPT]... [FILE]...') % cmd) 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,72 +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:]: - if self.ui.verbose: - self.ui.write("%d " % self.series.index(p)) - 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: @@ -876,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): @@ -911,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() @@ -947,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: @@ -967,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: @@ -1000,12 +1224,11 @@ class queue: self.ui.write("%s\n" % p) def appliedname(self, index): - p = self.applied[index] - pname = p.split(':')[1] + pname = self.applied[index].name if not self.ui.verbose: p = pname else: - p = str(self.series.index(pname)) + " " + p + p = str(self.series.index(pname)) + " " + pname return p def top(self, repo): @@ -1036,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) @@ -1075,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: @@ -1114,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): @@ -1187,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']: @@ -1215,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() @@ -1233,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() @@ -1244,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 @@ -1270,49 +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("")) - oldtags = repo.tags + 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 qtags(): - if repo.tagscache: - return repo.tagscache + 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() - tagscache = oldtags() + q = self.mq + if not q.applied: + return tagscache - q = repomap[repo] - if len(q.applied) == 0: + 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 - mqtags = [patch.split(':') 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: - repo.ui.warn('Tag %s overrides mq patch of the same name\n' % patch[1]) - else: - tagscache[patch[1]] = revlog.bin(patch[0]) - - return tagscache - - repo.tags = qtags + 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": @@ -1346,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'), @@ -1355,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, @@ -1373,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,28 +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, ... -# bcc = bcc1, bcc2, ... +# [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 @@ -130,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: @@ -193,8 +235,7 @@ def patchbomb(ui, repo, *revs, **opts): 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))) @@ -209,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 @@ -267,12 +311,15 @@ def patchbomb(ui, repo, *revs, **opts): fp.close() else: ui.status('Sending ', m['Subject'], ' ...\n') - mail.sendmail(sender, to + bcc + 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, - [('', 'bcc', [], 'email addresses of blind 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'), 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 @@ -237,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,145 @@ +# cmdutil.py - help for command processing in 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(), 'mdiff 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 findrenames(repo, added=None, removed=None, threshold=0.5): + if added is None or removed is None: + added, removed = repo.status()[1:3] + changes = repo.changelog.read(repo.dirstate.parents()[0]) + mf = repo.manifest.read(changes[0]) + for a in added: + aa = repo.wread(a) + bestscore, bestname = None, None + for r in removed: + rr = repo.file(r).read(mf[r]) + delta = mdiff.textdiff(aa, rr) + if len(delta) < len(aa): + myscore = 1.0 - (float(len(delta)) / len(aa)) + if bestscore is None or myscore > bestscore: + bestscore, bestname = myscore, r + if bestname and bestscore >= threshold: + yield bestname, a, bestscore + +def addremove(repo, pats=[], opts={}, wlock=None, dry_run=None, + similarity=None): + if dry_run is None: + dry_run = opts.get('dry_run') + if similarity is None: + similarity = float(opts.get('similarity') or 0) + add, remove = [], [] + mapping = {} + for src, abs, rel, exact in walk(repo, pats, opts): + if src == 'f' and repo.dirstate.state(abs) == '?': + add.append(abs) + mapping[abs] = rel, exact + 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) + mapping[abs] = rel, exact + 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) + if similarity > 0: + for old, new, score in findrenames(repo, add, remove, similarity): + oldrel, oldexact = mapping[old] + newrel, newexact = mapping[new] + if repo.ui.verbose or not oldexact or not newexact: + repo.ui.status(_('recording removal of %s as rename to %s ' + '(%d%% similar)\n') % + (oldrel, newrel, score * 100)) + if not dry_run: + repo.copy(old, new, 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) @@ -466,17 +403,15 @@ class changeset_printer(object): changes = log.read(changenode) date = util.datestr(changes[2]) - parents = [(log.rev(p), self.ui.verbose and hex(p) or short(p)) - for p in log.parents(changenode) + hexfunc = self.ui.debugflag and hex or short + + parents = [(log.rev(p), hexfunc(p)) for p in log.parents(changenode) if self.ui.debugflag or p != nullid] if (not self.ui.debugflag and len(parents) == 1 and parents[0][0] == rev-1): parents = [] - if self.ui.verbose: - self.ui.write(_("changeset: %d:%s\n") % (rev, hex(changenode))) - else: - self.ui.write(_("changeset: %d:%s\n") % (rev, short(changenode))) + self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode))) for tag in self.repo.nodetags(changenode): self.ui.status(_("tag: %s\n") % tag) @@ -493,7 +428,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 +472,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 +638,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) @@ -710,33 +652,21 @@ def add(ui, repo, *pats, **opts): def addremove(ui, repo, *pats, **opts): """add all new files, delete all missing files (DEPRECATED) - (DEPRECATED) Add all new files and remove all missing files from the repository. New files are ignored if they match any of the patterns in .hgignore. As with add, these changes take effect at the next commit. - This command is now deprecated and will be removed in a future - release. Please use add and remove --after instead. + Use the -s option to detect renamed files. With a parameter > 0, + this compares every removed file with every added file and records + those similar enough as renames. This option takes a percentage + between 0 (disabled) and 100 (files must be identical) as its + parameter. Detecting renamed files this way can be expensive. """ - ui.warn(_('(the addremove command is deprecated; use add and remove ' - '--after instead)\n')) - 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) + sim = float(opts.get('similarity') or 0) + if sim < 0 or sim > 100: + raise util.Abort(_('similarity must be between 0 and 100')) + return cmdutil.addremove(repo, pats, opts, similarity=sim/100.) def annotate(ui, repo, *pats, **opts): """show changeset information per file line @@ -779,7 +709,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)) @@ -831,10 +762,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 == '-': @@ -842,7 +773,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) @@ -885,7 +816,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) @@ -902,11 +833,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 @@ -944,8 +877,9 @@ def cat(ui, repo, file1, *pats, **opts): %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) + 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): @@ -960,10 +894,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. @@ -971,7 +920,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'], @@ -989,28 +938,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 = [] @@ -1165,7 +1099,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)) @@ -1239,9 +1173,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""" @@ -1382,7 +1316,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' % ( @@ -1411,37 +1345,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 @@ -1472,15 +1379,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) @@ -1493,7 +1399,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: @@ -1550,42 +1456,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: @@ -1602,6 +1522,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 @@ -1616,20 +1537,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: @@ -1638,7 +1570,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): @@ -1675,8 +1608,8 @@ def identify(ui, repo): ui.write(_("unknown\n")) return - hexfunc = ui.verbose and hex or short - modified, added, removed, deleted, unknown = repo.changes() + hexfunc = ui.debugflag and hex or short + 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 "")] @@ -1720,81 +1653,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'] @@ -1806,14 +1681,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) @@ -1830,7 +1700,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"]) @@ -1866,7 +1736,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(other, prev, n, fp=repo.ui) ui.write("\n") finally: if hasattr(other, 'close'): @@ -1886,7 +1756,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): @@ -1914,8 +1784,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']: @@ -1926,7 +1796,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 @@ -2006,7 +1887,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 @@ -2037,22 +1918,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 @@ -2064,7 +1967,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']] @@ -2085,7 +1988,7 @@ 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, file_=None, rev=None, branches=None, **opts): @@ -2146,7 +2049,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: @@ -2186,7 +2089,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)) @@ -2224,7 +2127,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)) @@ -2278,7 +2181,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): @@ -2298,12 +2201,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') @@ -2410,20 +2313,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')) @@ -2495,8 +2399,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') @@ -2631,7 +2534,7 @@ def status(ui, repo, *pats, **opts): all = opts['all'] - files, matchfn, anypats = matchpats(repo, pats, opts) + files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) cwd = (pats and repo.getcwd()) or '' modified, added, removed, deleted, unknown, ignored, clean = [ [util.pathto(cwd, x) for x in n] @@ -2681,8 +2584,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")) @@ -2691,17 +2594,20 @@ def tag(ui, repo, name, rev_=None, **opt if opts['rev']: rev_ = opts['rev'] if rev_: - r = hex(repo.lookup(rev_)) + r = repo.lookup(rev_) else: 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']) + r = p1 + + message = opts['message'] + if not message: + message = _('Added tag %s for changeset %s') % (name, short(r)) + + repo.tag(name, r, message, opts['local'], opts['user'], opts['date']) def tags(ui, repo): """list repository tags @@ -2713,9 +2619,10 @@ def tags(ui, repo): l = repo.tagslist() l.reverse() + hexfunc = ui.debugflag and hex or short for t, n in l: try: - r = "%5d:%s" % (repo.changelog.rev(n), hex(n)) + r = "%5d:%s" % (repo.changelog.rev(n), hexfunc(n)) except KeyError: r = " ?:?" if ui.quiet: @@ -2734,7 +2641,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 @@ -2779,7 +2686,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. @@ -2794,13 +2701,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 = [] @@ -2808,19 +2719,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 @@ -2832,7 +2743,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 @@ -2843,11 +2754,14 @@ table = { ('X', 'exclude', [], _('exclude names matching the given patterns')), ('n', 'dry-run', None, _('do not perform actions, just print output'))], _('hg add [OPTION]... [FILE]...')), - "debugaddremove|addremove": + "addremove": (addremove, [('I', 'include', [], _('include names matching the given patterns')), ('X', 'exclude', [], _('exclude names matching the given patterns')), - ('n', 'dry-run', None, _('do not perform actions, just print output'))], + ('n', 'dry-run', None, + _('do not perform actions, just print output')), + ('s', 'similarity', '', + _('guess renamed files by similarity (0<=s<=1)'))], _('hg addremove [OPTION]... [FILE]...')), "^annotate": (annotate, @@ -2953,6 +2867,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, @@ -2977,6 +2892,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')), @@ -3013,7 +2930,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', '', @@ -3039,6 +2956,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')), @@ -3046,6 +2967,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'))], @@ -3084,7 +3006,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]')), @@ -3323,24 +3245,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 @@ -3360,23 +3274,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) @@ -3428,6 +3355,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 @@ -3435,6 +3363,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. 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) @@ -476,7 +518,7 @@ class dirstate(object): if size >= 0 and (size != st.st_size or (mode ^ st.st_mode) & 0100): modified.append(fn) - elif time != st.st_mtime: + elif time != int(st.st_mtime): lookup.append(fn) elif list_clean: clean.append(fn) 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 * @@ -48,6 +48,7 @@ class hgweb(object): 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) @@ -128,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: @@ -175,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 @@ -221,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, @@ -395,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 = {} @@ -449,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 @@ -459,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 = {} @@ -493,7 +485,7 @@ class hgweb(object): "filenode": hex(fnode), "parity": self.stripes(parity), "basename": f, - "permissions": mff[full]} + "permissions": mf.execf(full)} parity += 1 def dirlist(**map): @@ -611,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 @@ -691,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)], @@ -773,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])) 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. 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. @@ -339,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. @@ -169,7 +169,7 @@ class localrepository(repo.repository): tag_disallowed = ':\r\n' - def tag(self, name, node, local=False, message=None, user=None, date=None): + def tag(self, name, node, message, local, user, date): '''tag a revision with a symbolic name. if local is True, the tag is stored in a per-repository file. @@ -191,27 +191,24 @@ class localrepository(repo.repository): if c in name: raise util.Abort(_('%r cannot be used in a tag name') % c) - self.hook('pretag', throw=True, node=node, tag=name, local=local) + self.hook('pretag', throw=True, node=hex(node), tag=name, local=local) if local: - self.opener('localtags', 'a').write('%s %s\n' % (node, name)) - self.hook('tag', node=node, tag=name, local=local) + self.opener('localtags', 'a').write('%s %s\n' % (hex(node), name)) + self.hook('tag', node=hex(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)')) - self.wfile('.hgtags', 'ab').write('%s %s\n' % (node, name)) + self.wfile('.hgtags', 'ab').write('%s %s\n' % (hex(node), name)) if self.dirstate.state('.hgtags') == '?': self.add(['.hgtags']) - if not message: - message = _('Added tag %s for changeset %s') % (name, node) - self.commit(['.hgtags'], message, user, date) - self.hook('tag', node=node, tag=name, local=local) + self.hook('tag', node=hex(node), tag=name, local=local) def tags(self): '''return a mapping of tag to node''' @@ -292,6 +289,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: @@ -466,8 +467,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 = [] @@ -480,36 +480,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() @@ -533,15 +529,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: @@ -567,7 +562,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) @@ -594,12 +589,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 @@ -671,8 +665,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) @@ -714,8 +707,10 @@ class localrepository(repo.repository): for f in lookup: if fcmp(f, mf2): modified.append(f) - elif wlock is not None: - self.dirstate.update([f], "n") + else: + clean.append(f) + if wlock is not None: + self.dirstate.update([f], "n") else: # we are comparing working dir against non-parent # generate a pseudo-manifest for the working dir @@ -754,16 +749,6 @@ class localrepository(repo.repository): l.sort() return (modified, added, removed, deleted, unknown, ignored, clean) - def changes(self, node1=None, node2=None, files=[], match=util.always, - wlock=None, list_ignored=False, list_clean=False): - '''DEPRECATED - use status instead''' - marduit = self.status(node1, node2, files, match, wlock, - list_ignored, list_clean) - if list_ignored: - return marduit[:-1] - else: - return marduit[:-2] - def add(self, list, wlock=None): if not wlock: wlock = self.wlock() @@ -812,7 +797,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() @@ -822,7 +806,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): @@ -1118,7 +1102,7 @@ class localrepository(repo.repository): else: raise util.Abort(_("repository is unrelated")) - self.ui.note(_("found new changesets starting at ") + + self.ui.debug(_("found new changesets starting at ") + " ".join([short(f) for f in fetch]) + "\n") self.ui.debug(_("%d total queries\n") % reqcnt) @@ -1173,22 +1157,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', remote.url()) + 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: @@ -1693,530 +1684,6 @@ class localrepository(repo.repository): 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() @@ -2242,7 +1709,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. @@ -2271,3 +1738,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)) + ui.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,327 @@ +# 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) + + # are we going backwards? + backwards = (pa == 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 linear_path and not (overwrite or 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).copy() + 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))) + + action = {} + forget = [] + + # update m1 from working dir + umap = dict.fromkeys(unknown) + + for f in added + modified + unknown: + m1[f] = m1.get(f, nullid) + "+" + m1.set(f, util.is_exec(repo.wjoin(f), m1.execf(f))) + + for f in deleted + removed: + del m1[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) + + if partial: + for f in m1.keys(): + if not partial(f): del m1[f] + for f in m2.keys(): + if not partial(f): del m2[f] + + # Compare manifests + for f, n in m1.iteritems(): + if f in m2: + queued = 0 + + # are files different? + if n != m2[f]: + a = ma.get(f, nullid) + # are both different from the ancestor? + if not overwrite and n != a and m2[f] != a: + repo.ui.debug(_(" %s versions differ, resolve\n") % f) + action[f] = (fmerge(f, m1, m2, ma), n[:20], m2[f]) + queued = 1 + # are we clobbering? + # is remote's version newer? + # or are we going back in time and clean? + elif overwrite or m2[f] != a or (backwards and not n[20:]): + repo.ui.debug(_(" remote %s is newer, get\n") % f) + action[f] = (m2.execf(f), m2[f], None) + queued = 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 + action[f] = (m2.execf(f), m2[f], None) + + # do we still need to look at mode bits? + if not queued and m1.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, m1, m2, ma) != m1.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: + r = repo.ui.prompt( + (_(" local changed %s which remote deleted\n") % f) + + _("(k)eep or (d)elete?"), _("[kd]"), _("k")) + if r == _("d"): + action[f] = (None, None, None) + else: + repo.ui.debug(_("other deleted %s\n") % f) + action[f] = (None, None, None) + 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) + action[f] = (None, None, None) + elif not n[20:]: # same as parent + if backwards: + repo.ui.debug(_("remote deleted %s\n") % f) + action[f] = (None, None, None) + 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 f[0] == "/": + continue + if f in ma and n != ma[f]: + r = _("k") + if not overwrite: + r = repo.ui.prompt( + (_("remote changed %s which local deleted\n") % f) + + _("(k)eep or (d)elete?"), _("[kd]"), _("k")) + if r == _("k"): + action[f] = (m2.execf(f), n, None) + elif f not in ma: + repo.ui.debug(_("remote created %s\n") % f) + action[f] = (m2.execf(f), n, None) + else: + if overwrite or backwards: + repo.ui.debug(_("local deleted %s, recreating\n") % f) + action[f] = (m2.execf(f), n, None) + else: + repo.ui.debug(_("local deleted %s\n") % f) + + del m1, m2, ma + + ### apply phase + + 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) + + # update files + unresolved = [] + updated, merged, removed = 0, 0, 0 + files = action.keys() + files.sort() + for f in files: + flag, my, other = action[f] + if f[0] == "/": + continue + if not my: + 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)) + removed +=1 + elif other: + repo.ui.status(_("merging %s\n") % f) + if merge3(repo, f, my, other, xp1, xp2): + unresolved.append(f) + util.set_exec(repo.wjoin(f), flag) + merged += 1 + else: + repo.ui.note(_("getting %s\n") % f) + t = repo.file(f).read(my) + repo.wwrite(f, t) + util.set_exec(repo.wjoin(f), flag) + updated += 1 + + # update dirstate + if not partial: + repo.dirstate.setparents(p1, p2) + repo.dirstate.forget(forget) + files = action.keys() + files.sort() + for f in files: + flag, my, other = action[f] + if not my: + if branchmerge: + repo.dirstate.update([f], 'r') + else: + repo.dirstate.forget([f]) + elif not other: + if branchmerge: + repo.dirstate.update([f], 'n', st_mtime=-1) + else: + repo.dirstate.update([f], 'n') + else: + 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 = ((updated, _("updated")), + (merged - len(unresolved), _("merged")), + (removed, _("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. @@ -744,8 +744,6 @@ 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): return self.node(id) try: @@ -760,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. @@ -204,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. 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): @@ -50,3 +51,14 @@ class statichttprepository(localrepo.loc 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 @@ -459,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. @@ -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. @@ -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]: @@ -607,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 @@ -696,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: @@ -996,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/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 @@ -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-addremove b/tests/test-addremove --- a/tests/test-addremove +++ b/tests/test-addremove @@ -10,3 +10,17 @@ cd dir/ touch ../foo_2 bar_2 hg -v addremove hg -v commit -m "add 2" -d "1000000 0" + +cd .. +hg init sim +cd sim +echo a > a +echo a >> a +echo a >> a +echo c > c +hg commit -Ama +mv a b +rm c +echo d > d +hg addremove -s 0.5 +hg commit -mb diff --git a/tests/test-addremove.out b/tests/test-addremove.out --- a/tests/test-addremove.out +++ b/tests/test-addremove.out @@ -1,10 +1,15 @@ -(the addremove command is deprecated; use add and remove --after instead) adding dir/bar adding foo dir/bar foo -(the addremove command is deprecated; use add and remove --after instead) adding dir/bar_2 adding foo_2 dir/bar_2 foo_2 +adding a +adding c +adding b +adding d +removing a +removing c +recording removal of a as rename to b (100% similar) 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-archive.out b/tests/test-archive.out --- a/tests/test-archive.out +++ b/tests/test-archive.out @@ -1,8 +1,5 @@ -(the addremove command is deprecated; use add and remove --after instead) adding foo -(the addremove command is deprecated; use add and remove --after instead) adding bar -(the addremove command is deprecated; use add and remove --after instead) adding baz/bletch test-archive-TIP/.hg_archival.txt test-archive-TIP/bar 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,10 +30,14 @@ 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 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 diff --git a/tests/test-command-template.out b/tests/test-command-template.out --- a/tests/test-command-template.out +++ b/tests/test-command-template.out @@ -1,4 +1,20 @@ # default style is like normal output +1c1 +< changeset: 3:10e46f2dcbf4 +--- +> changeset: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 +10c10 +< changeset: 2:97054abb4ab8 +--- +> changeset: 2:97054abb4ab824450e9164180baf491ae0078465 +18c18 +< changeset: 1:b608e9d1a3f0 +--- +> changeset: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965 +29c29 +< changeset: 0:1e4e1b8f71e0 +--- +> changeset: 0:1e4e1b8f71e05681d422154f5421e385fec3454f 18a19 > files: 29a31 diff --git a/tests/test-copy.out b/tests/test-copy.out --- a/tests/test-copy.out +++ b/tests/test-copy.out @@ -2,7 +2,7 @@ A b b b: copy a:b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3 we should see two history entries -changeset: 1:386a3cc01532710ca78aed9a54fa2f459c04f29c +changeset: 1:386a3cc01532 tag: tip user: test date: Mon Jan 12 13:46:40 1970 +0000 @@ -11,7 +11,7 @@ description: 2 -changeset: 0:33aaa84a386bd609094aeb21a97c09436c482ef1 +changeset: 0:33aaa84a386b user: test date: Mon Jan 12 13:46:40 1970 +0000 files: a 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-filebranch.out b/tests/test-filebranch.out --- a/tests/test-filebranch.out +++ b/tests/test-filebranch.out @@ -31,10 +31,10 @@ main: we should have a merge here 2 150 71 2 2 a6aef98656b7 c36078bec30d 000000000000 3 221 72 3 3 0c2cc6fc80e2 182b283965f1 a6aef98656b7 log should show foo and quux changed -changeset: 3:0c2cc6fc80e2d4ee289bb658dbbe9ad932380fe9 +changeset: 3:0c2cc6fc80e2 tag: tip -parent: 1:182b283965f1069c0112784e30e7755ad1c0dd52 -parent: 2:a6aef98656b71154cae9d87408abe6d0218c8045 +parent: 1:182b283965f1 +parent: 2:a6aef98656b7 user: test date: Mon Jan 12 13:46:40 1970 +0000 files: foo quux 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-globalopts.out b/tests/test-globalopts.out --- a/tests/test-globalopts.out +++ b/tests/test-globalopts.out @@ -47,7 +47,7 @@ 0:b6c483daf290 0:8580ff50825a 1:b6c483daf290 %% -v/--verbose -changeset: 1:b6c483daf2907ce5825c0bb50f5716226281cc1a +changeset: 1:b6c483daf290 tag: tip user: test date: Thu Jan 01 00:00:01 1970 +0000 @@ -56,7 +56,7 @@ description: b -changeset: 0:8580ff50825a50c8f716709acdf8de0deddcd6ab +changeset: 0:8580ff50825a user: test date: Thu Jan 01 00:00:01 1970 +0000 files: a @@ -64,7 +64,7 @@ description: a -changeset: 0:b6c483daf2907ce5825c0bb50f5716226281cc1a +changeset: 0:b6c483daf290 tag: tip user: test date: Thu Jan 01 00:00:01 1970 +0000 @@ -114,92 +114,94 @@ Mercurial Distributed SCM list of commands (use "hg help -v" to show aliases and global options): - add add the specified files on the next commit - annotate show changeset information per file line - archive create unversioned archive of a repository revision - backout reverse effect of earlier changeset - bundle create a changegroup file - cat output the latest or given revisions of files - clone make a copy of an existing repository - commit commit the specified files or all outstanding changes - copy mark files as copied for the next commit - diff diff repository (or selected files) - export dump the header and diffs for one or more changesets - grep search for a pattern in specified files and revisions - heads show current repository heads - help show help for a command, extension, or list of commands - identify print information about the working copy - import import an ordered set of patches - incoming show new changesets found in source - init create a new repository in the given directory - locate locate files matching specific patterns - log show revision history of entire repository or files - manifest output the latest or given revision of the project manifest - merge Merge working directory with another revision - outgoing show changesets not found in destination - parents show the parents of the working dir or revision - paths show definition of symbolic path names - pull pull changes from the specified source - push push changes to the specified destination - recover roll back an interrupted transaction - remove remove the specified files on the next commit - rename rename files; equivalent of copy + remove - revert revert files or dirs to their states as of some revision - rollback roll back the last transaction in this repository - root print the root (top) of the current working dir - serve export the repository via HTTP - status show changed files in the working directory - tag add a tag for the current tip or a given revision - tags list repository tags - tip show the tip revision - unbundle apply a changegroup file - update update or merge working directory - verify verify the integrity of the repository - version output version and copyright information + add add the specified files on the next commit + addremove add all new files, delete all missing files (DEPRECATED) + annotate show changeset information per file line + archive create unversioned archive of a repository revision + backout reverse effect of earlier changeset + bundle create a changegroup file + cat output the latest or given revisions of files + clone make a copy of an existing repository + commit commit the specified files or all outstanding changes + copy mark files as copied for the next commit + diff diff repository (or selected files) + export dump the header and diffs for one or more changesets + grep search for a pattern in specified files and revisions + heads show current repository heads + help show help for a command, extension, or list of commands + identify print information about the working copy + import import an ordered set of patches + incoming show new changesets found in source + init create a new repository in the given directory + locate locate files matching specific patterns + log show revision history of entire repository or files + manifest output the latest or given revision of the project manifest + merge Merge working directory with another revision + outgoing show changesets not found in destination + parents show the parents of the working dir or revision + paths show definition of symbolic path names + pull pull changes from the specified source + push push changes to the specified destination + recover roll back an interrupted transaction + remove remove the specified files on the next commit + rename rename files; equivalent of copy + remove + revert revert files or dirs to their states as of some revision + rollback roll back the last transaction in this repository + root print the root (top) of the current working dir + serve export the repository via HTTP + status show changed files in the working directory + tag add a tag for the current tip or a given revision + tags list repository tags + tip show the tip revision + unbundle apply a changegroup file + update update or merge working directory + verify verify the integrity of the repository + version output version and copyright information Mercurial Distributed SCM list of commands (use "hg help -v" to show aliases and global options): - add add the specified files on the next commit - annotate show changeset information per file line - archive create unversioned archive of a repository revision - backout reverse effect of earlier changeset - bundle create a changegroup file - cat output the latest or given revisions of files - clone make a copy of an existing repository - commit commit the specified files or all outstanding changes - copy mark files as copied for the next commit - diff diff repository (or selected files) - export dump the header and diffs for one or more changesets - grep search for a pattern in specified files and revisions - heads show current repository heads - help show help for a command, extension, or list of commands - identify print information about the working copy - import import an ordered set of patches - incoming show new changesets found in source - init create a new repository in the given directory - locate locate files matching specific patterns - log show revision history of entire repository or files - manifest output the latest or given revision of the project manifest - merge Merge working directory with another revision - outgoing show changesets not found in destination - parents show the parents of the working dir or revision - paths show definition of symbolic path names - pull pull changes from the specified source - push push changes to the specified destination - recover roll back an interrupted transaction - remove remove the specified files on the next commit - rename rename files; equivalent of copy + remove - revert revert files or dirs to their states as of some revision - rollback roll back the last transaction in this repository - root print the root (top) of the current working dir - serve export the repository via HTTP - status show changed files in the working directory - tag add a tag for the current tip or a given revision - tags list repository tags - tip show the tip revision - unbundle apply a changegroup file - update update or merge working directory - verify verify the integrity of the repository - version output version and copyright information + add add the specified files on the next commit + addremove add all new files, delete all missing files (DEPRECATED) + annotate show changeset information per file line + archive create unversioned archive of a repository revision + backout reverse effect of earlier changeset + bundle create a changegroup file + cat output the latest or given revisions of files + clone make a copy of an existing repository + commit commit the specified files or all outstanding changes + copy mark files as copied for the next commit + diff diff repository (or selected files) + export dump the header and diffs for one or more changesets + grep search for a pattern in specified files and revisions + heads show current repository heads + help show help for a command, extension, or list of commands + identify print information about the working copy + import import an ordered set of patches + incoming show new changesets found in source + init create a new repository in the given directory + locate locate files matching specific patterns + log show revision history of entire repository or files + manifest output the latest or given revision of the project manifest + merge Merge working directory with another revision + outgoing show changesets not found in destination + parents show the parents of the working dir or revision + paths show definition of symbolic path names + pull pull changes from the specified source + push push changes to the specified destination + recover roll back an interrupted transaction + remove remove the specified files on the next commit + rename rename files; equivalent of copy + remove + revert revert files or dirs to their states as of some revision + rollback roll back the last transaction in this repository + root print the root (top) of the current working dir + serve export the repository via HTTP + status show changed files in the working directory + tag add a tag for the current tip or a given revision + tags list repository tags + tip show the tip revision + unbundle apply a changegroup file + update update or merge working directory + verify verify the integrity of the repository + version output version and copyright information %% not tested: --debugger 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 @@ -38,90 +38,92 @@ Mercurial Distributed SCM list of commands (use "hg help -v" to show aliases and global options): - add add the specified files on the next commit - annotate show changeset information per file line - archive create unversioned archive of a repository revision - backout reverse effect of earlier changeset - bundle create a changegroup file - cat output the latest or given revisions of files - clone make a copy of an existing repository - commit commit the specified files or all outstanding changes - copy mark files as copied for the next commit - diff diff repository (or selected files) - export dump the header and diffs for one or more changesets - grep search for a pattern in specified files and revisions - heads show current repository heads - help show help for a command, extension, or list of commands - identify print information about the working copy - import import an ordered set of patches - incoming show new changesets found in source - init create a new repository in the given directory - locate locate files matching specific patterns - log show revision history of entire repository or files - manifest output the latest or given revision of the project manifest - merge Merge working directory with another revision - outgoing show changesets not found in destination - parents show the parents of the working dir or revision - paths show definition of symbolic path names - pull pull changes from the specified source - push push changes to the specified destination - recover roll back an interrupted transaction - remove remove the specified files on the next commit - rename rename files; equivalent of copy + remove - revert revert files or dirs to their states as of some revision - rollback roll back the last transaction in this repository - root print the root (top) of the current working dir - serve export the repository via HTTP - status show changed files in the working directory - tag add a tag for the current tip or a given revision - tags list repository tags - tip show the tip revision - unbundle apply a changegroup file - update update or merge working directory - verify verify the integrity of the repository - version output version and copyright information - add add the specified files on the next commit - annotate show changeset information per file line - archive create unversioned archive of a repository revision - backout reverse effect of earlier changeset - bundle create a changegroup file - cat output the latest or given revisions of files - clone make a copy of an existing repository - commit commit the specified files or all outstanding changes - copy mark files as copied for the next commit - diff diff repository (or selected files) - export dump the header and diffs for one or more changesets - grep search for a pattern in specified files and revisions - heads show current repository heads - help show help for a command, extension, or list of commands - identify print information about the working copy - import import an ordered set of patches - incoming show new changesets found in source - init create a new repository in the given directory - locate locate files matching specific patterns - log show revision history of entire repository or files - manifest output the latest or given revision of the project manifest - merge Merge working directory with another revision - outgoing show changesets not found in destination - parents show the parents of the working dir or revision - paths show definition of symbolic path names - pull pull changes from the specified source - push push changes to the specified destination - recover roll back an interrupted transaction - remove remove the specified files on the next commit - rename rename files; equivalent of copy + remove - revert revert files or dirs to their states as of some revision - rollback roll back the last transaction in this repository - root print the root (top) of the current working dir - serve export the repository via HTTP - status show changed files in the working directory - tag add a tag for the current tip or a given revision - tags list repository tags - tip show the tip revision - unbundle apply a changegroup file - update update or merge working directory - verify verify the integrity of the repository - version output version and copyright information + add add the specified files on the next commit + addremove add all new files, delete all missing files (DEPRECATED) + annotate show changeset information per file line + archive create unversioned archive of a repository revision + backout reverse effect of earlier changeset + bundle create a changegroup file + cat output the latest or given revisions of files + clone make a copy of an existing repository + commit commit the specified files or all outstanding changes + copy mark files as copied for the next commit + diff diff repository (or selected files) + export dump the header and diffs for one or more changesets + grep search for a pattern in specified files and revisions + heads show current repository heads + help show help for a command, extension, or list of commands + identify print information about the working copy + import import an ordered set of patches + incoming show new changesets found in source + init create a new repository in the given directory + locate locate files matching specific patterns + log show revision history of entire repository or files + manifest output the latest or given revision of the project manifest + merge Merge working directory with another revision + outgoing show changesets not found in destination + parents show the parents of the working dir or revision + paths show definition of symbolic path names + pull pull changes from the specified source + push push changes to the specified destination + recover roll back an interrupted transaction + remove remove the specified files on the next commit + rename rename files; equivalent of copy + remove + revert revert files or dirs to their states as of some revision + rollback roll back the last transaction in this repository + root print the root (top) of the current working dir + serve export the repository via HTTP + status show changed files in the working directory + tag add a tag for the current tip or a given revision + tags list repository tags + tip show the tip revision + unbundle apply a changegroup file + update update or merge working directory + verify verify the integrity of the repository + version output version and copyright information + add add the specified files on the next commit + addremove add all new files, delete all missing files (DEPRECATED) + annotate show changeset information per file line + archive create unversioned archive of a repository revision + backout reverse effect of earlier changeset + bundle create a changegroup file + cat output the latest or given revisions of files + clone make a copy of an existing repository + commit commit the specified files or all outstanding changes + copy mark files as copied for the next commit + diff diff repository (or selected files) + export dump the header and diffs for one or more changesets + grep search for a pattern in specified files and revisions + heads show current repository heads + help show help for a command, extension, or list of commands + identify print information about the working copy + import import an ordered set of patches + incoming show new changesets found in source + init create a new repository in the given directory + locate locate files matching specific patterns + log show revision history of entire repository or files + manifest output the latest or given revision of the project manifest + merge Merge working directory with another revision + outgoing show changesets not found in destination + parents show the parents of the working dir or revision + paths show definition of symbolic path names + pull pull changes from the specified source + push push changes to the specified destination + recover roll back an interrupted transaction + remove remove the specified files on the next commit + rename rename files; equivalent of copy + remove + revert revert files or dirs to their states as of some revision + rollback roll back the last transaction in this repository + root print the root (top) of the current working dir + serve export the repository via HTTP + status show changed files in the working directory + tag add a tag for the current tip or a given revision + tags list repository tags + tip show the tip revision + unbundle apply a changegroup file + update update or merge working directory + verify verify the integrity of the repository + version output version and copyright information hg add [OPTION]... [FILE]... add the specified files on the next commit @@ -176,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 diff --git a/tests/test-hook.out b/tests/test-hook.out --- a/tests/test-hook.out +++ b/tests/test-hook.out @@ -36,43 +36,43 @@ added 3 changesets with 2 changes to 2 f (run 'hg update' to get a working copy) pretag hook: t=a n=4c52fb2e402287dd5dc052090682536c8406c321 l=0 precommit hook: p1=4c52fb2e402287dd5dc052090682536c8406c321 p2= -pretxncommit hook: n=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 p1=4c52fb2e402287dd5dc052090682536c8406c321 p2= -4:4f92e785b90a -commit hook: n=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 p1=4c52fb2e402287dd5dc052090682536c8406c321 p2= +pretxncommit hook: n=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 p1=4c52fb2e402287dd5dc052090682536c8406c321 p2= +4:8ea2ef7ad3e8 +commit hook: n=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 p1=4c52fb2e402287dd5dc052090682536c8406c321 p2= commit hook b tag hook: t=a n=4c52fb2e402287dd5dc052090682536c8406c321 l=0 -pretag hook: t=la n=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 l=1 -tag hook: t=la n=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 l=1 -pretag hook: t=fa n=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 l=0 +pretag hook: t=la n=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 l=1 +tag hook: t=la n=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 l=1 +pretag hook: t=fa n=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 l=0 pretag.forbid hook abort: pretag.forbid hook exited with status 1 -pretag hook: t=fla n=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 l=1 +pretag hook: t=fla n=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 l=1 pretag.forbid hook abort: pretag.forbid hook exited with status 1 -4:4f92e785b90a -precommit hook: p1=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 p2= -pretxncommit hook: n=7792358308a2026661cea44f9d47c072813004cb p1=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 p2= -5:7792358308a2 -pretxncommit.forbid hook: tip=5:7792358308a2 +4:8ea2ef7ad3e8 +precommit hook: p1=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 p2= +pretxncommit hook: n=fad284daf8c032148abaffcd745dafeceefceb61 p1=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 p2= +5:fad284daf8c0 +pretxncommit.forbid hook: tip=5:fad284daf8c0 abort: pretxncommit.forbid hook exited with status 1 transaction abort! rollback completed -4:4f92e785b90a -precommit hook: p1=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 p2= +4:8ea2ef7ad3e8 +precommit hook: p1=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 p2= precommit.forbid hook abort: precommit.forbid hook exited with status 1 -4:4f92e785b90a +4:8ea2ef7ad3e8 preupdate hook: p1=b702efe9688826e3a91283852b328b84dbf37bc2 p2= 0 files updated, 0 files merged, 2 files removed, 0 files unresolved -preupdate hook: p1=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 p2= -update hook: p1=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 p2= err=0 +preupdate hook: p1=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 p2= +update hook: p1=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 p2= err=0 2 files updated, 0 files merged, 0 files removed, 0 files unresolved 3:4c52fb2e4022 prechangegroup.forbid hook pulling from ../a searching for changes abort: prechangegroup.forbid hook exited with status 1 -pretxnchangegroup.forbid hook: tip=4:4f92e785b90a +pretxnchangegroup.forbid hook: tip=4:8ea2ef7ad3e8 pulling from ../a searching for changes adding changesets @@ -84,7 +84,7 @@ transaction abort! rollback completed 3:4c52fb2e4022 preoutgoing hook: s=pull -outgoing hook: n=4f92e785b90ae8995dfe156e39dd4fbc3b346a24 s=pull +outgoing hook: n=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 s=pull pulling from ../a searching for changes adding changesets 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:8580ff50825a +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +files: a +description: +a + + +% many renames +changeset: 4:8c1c8408f737 +tag: tip +user: test +date: Thu Jan 01 00:00:05 1970 +0000 +files: dir/b e +description: +e + + +changeset: 2:21fba396af4c +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +files: b dir/b +description: +c + + +changeset: 1:c0296dabce9b +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +files: b +description: +b + + +changeset: 0:8580ff50825a +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-merge1.out b/tests/test-merge1.out --- a/tests/test-merge1.out +++ b/tests/test-merge1.out @@ -1,6 +1,8 @@ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved %% no merges expected -1 files updated, 0 files merged, 0 files removed, 0 files unresolved +merging for b +merging b +0 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) 0 files updated, 0 files merged, 1 files removed, 0 files unresolved %% merge should fail 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-parse-date.out b/tests/test-parse-date.out --- a/tests/test-parse-date.out +++ b/tests/test-parse-date.out @@ -1,6 +1,6 @@ reverting a changeset 3:107ce1ee2b43 backs out changeset 1:25a1420a55f8 -merging with changeset 2:99a1acecff55 +merging with changeset 2:e6c3abc120e7 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) abort: invalid date: 'should fail' diff --git a/tests/test-pull.out b/tests/test-pull.out --- a/tests/test-pull.out +++ b/tests/test-pull.out @@ -1,4 +1,3 @@ -(the addremove command is deprecated; use add and remove --after instead) adding foo checking changesets checking manifests diff --git a/tests/test-simple-update.out b/tests/test-simple-update.out --- a/tests/test-simple-update.out +++ b/tests/test-simple-update.out @@ -1,4 +1,3 @@ -(the addremove command is deprecated; use add and remove --after instead) adding foo checking changesets checking manifests diff --git a/tests/test-symlinks.out b/tests/test-symlinks.out --- a/tests/test-symlinks.out +++ b/tests/test-symlinks.out @@ -1,6 +1,4 @@ -(the addremove command is deprecated; use add and remove --after instead) adding foo -(the addremove command is deprecated; use add and remove --after instead) adding bomb adding a.c adding dir/a.o diff --git a/tests/test-tag.out b/tests/test-tag.out --- a/tests/test-tag.out +++ b/tests/test-tag.out @@ -4,11 +4,11 @@ user: test date: Mon Jan 12 13:46:40 1970 +0000 summary: test -changeset: 1:c5c60883086f +changeset: 1:3ecf002a1c57 tag: tip user: test date: Mon Jan 12 13:46:40 1970 +0000 -summary: Added tag bleah for changeset 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 +summary: Added tag bleah for changeset 0acdaf898367 changeset: 0:0acdaf898367 tag: bleah @@ -24,9 +24,9 @@ failed use of 'hg tag NAME [REV]' is deprecated, please use 'hg tag [-r REV] NAME' instead 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 bleah 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 bleah0 -c5c60883086f5526bd3e36814b94a73a4e75e172 bleah1 +3ecf002a1c572a2f3bb4e665417e60fca65bbd42 bleah1 0 files updated, 0 files merged, 1 files removed, 0 files unresolved 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 foobar -c5c60883086f5526bd3e36814b94a73a4e75e172 bleah1 +3ecf002a1c572a2f3bb4e665417e60fca65bbd42 bleah1 abort: '\n' cannot be used in a tag name abort: ':' cannot be used in a tag name diff --git a/tests/test-tags b/tests/test-tags --- a/tests/test-tags +++ b/tests/test-tags @@ -9,7 +9,7 @@ hg add a hg commit -m "test" -d "1000000 0" hg co hg identify -T=`hg tip -v | head -n 1 | cut -d : -f 3` +T=`hg tip --debug | head -n 1 | cut -d : -f 3` echo "$T first" > .hgtags cat .hgtags hg add .hgtags diff --git a/tests/test-tags.out b/tests/test-tags.out --- a/tests/test-tags.out +++ b/tests/test-tags.out @@ -2,35 +2,35 @@ unknown 0 files updated, 0 files merged, 0 files removed, 0 files unresolved 0acdaf898367 tip 0acdaf8983679e0aac16e811534eb49d7ee1f2b4 first -tip 1:8a3ca90d111dc784e6575d373105be12570e8776 -first 0:0acdaf8983679e0aac16e811534eb49d7ee1f2b4 +tip 1:8a3ca90d111d +first 0:0acdaf898367 8a3ca90d111d tip M a 8a3ca90d111d+ tip 0 files updated, 0 files merged, 1 files removed, 0 files unresolved 0acdaf898367+ first -0acdaf8983679e0aac16e811534eb49d7ee1f2b4+ first +0acdaf898367+ first M a 8216907a933d tip 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) 8216907a933d+8a3ca90d111d+ tip M .hgtags -tip 6:c6af9d771a81bb9c7f267ec03491224a9f8ba1cd -first 0:0acdaf8983679e0aac16e811534eb49d7ee1f2b4 -.hgtags (rev 7:39bba1bbbc4c), line 2: cannot parse entry -.hgtags (rev 7:39bba1bbbc4c), line 4: node 'foo' is not well formed +tip 6:e2174d339386 +first 0:0acdaf898367 +.hgtags (rev 7:c071f74ab5eb), line 2: cannot parse entry +.hgtags (rev 7:c071f74ab5eb), line 4: node 'foo' is not well formed localtags, line 1: tag 'invalid' refers to unknown node 1 files updated, 0 files merged, 0 files removed, 0 files unresolved -.hgtags (rev 7:39bba1bbbc4c), line 2: cannot parse entry -.hgtags (rev 7:39bba1bbbc4c), line 4: node 'foo' is not well formed +.hgtags (rev 7:c071f74ab5eb), line 2: cannot parse entry +.hgtags (rev 7:c071f74ab5eb), line 4: node 'foo' is not well formed .hgtags (rev 8:4ca6f1b1a68c), line 2: node 'x' is not well formed localtags, line 1: tag 'invalid' refers to unknown node -tip 8:4ca6f1b1a68c77be687a03aaeb1614671ba59b20 -first 0:0acdaf8983679e0aac16e811534eb49d7ee1f2b4 +tip 8:4ca6f1b1a68c +first 0:0acdaf898367 changeset: 8:4ca6f1b1a68c -.hgtags (rev 7:39bba1bbbc4c), line 2: cannot parse entry -.hgtags (rev 7:39bba1bbbc4c), line 4: node 'foo' is not well formed +.hgtags (rev 7:c071f74ab5eb), line 2: cannot parse entry +.hgtags (rev 7:c071f74ab5eb), line 4: node 'foo' is not well formed .hgtags (rev 8:4ca6f1b1a68c), line 2: node 'x' is not well formed localtags, line 1: tag 'invalid' refers to unknown node tag: tip 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 @@ -1,4 +1,3 @@ -(the addremove command is deprecated; use add and remove --after instead) adding a 1 files updated, 0 files merged, 0 files removed, 0 files unresolved 0 files updated, 0 files merged, 0 files removed, 0 files unresolved @@ -8,7 +7,6 @@ diff -r 33aaa84a386b a @@ -1,1 +1,1 @@ a -a +abc -(the addremove command is deprecated; use add and remove --after instead) adding b M a changeset: 0:33aaa84a386b @@ -17,14 +15,14 @@ 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 -getting b merging a resolving a file a: my b789fdd96dc2 other d730145abbf9 ancestor b789fdd96dc2 +getting b 1 files updated, 1 files merged, 0 files removed, 0 files unresolved changeset: 1:802f095af299 tag: tip @@ -33,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 @@ -43,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 @@ -51,14 +49,14 @@ 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 -getting b merging a resolving a file a: my b789fdd96dc2 other d730145abbf9 ancestor b789fdd96dc2 +getting b 1 files updated, 1 files merged, 0 files removed, 0 files unresolved changeset: 1:802f095af299 tag: tip @@ -66,7 +64,7 @@ user: test date: Mon Jan 12 13:46:40 1970 +0000 summary: 2 -changeset: 1:802f095af299cde27a85b2f056aef3829870956c +changeset: 1:802f095af299 tag: tip user: test date: Mon Jan 12 13:46:40 1970 +0000 @@ -75,7 +73,7 @@ description: 2 -changeset: 0:33aaa84a386bd609094aeb21a97c09436c482ef1 +changeset: 0:33aaa84a386b user: test date: Mon Jan 12 13:46:40 1970 +0000 files: a @@ -90,7 +88,6 @@ diff -r 802f095af299 a -a2 +abc 1 files updated, 0 files merged, 1 files removed, 0 files unresolved -(the addremove command is deprecated; use add and remove --after instead) adding b M a changeset: 1:802f095af299 @@ -98,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 @@ -149,4 +137,5 @@ 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 +merging a +0 files updated, 1 files merged, 0 files removed, 0 files unresolved 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 diff --git a/tests/test-walk.out b/tests/test-walk.out --- a/tests/test-walk.out +++ b/tests/test-walk.out @@ -1,4 +1,3 @@ -(the addremove command is deprecated; use add and remove --after instead) adding beans/black adding beans/borlotti adding beans/kidney