# HG changeset patch # User Thomas Arendsen Hein # Date 1161983386 -7200 # Node ID 23f7d96217831943b19eb5734a76c29f3afd3e1f # Parent 3bab1fc0ab7545bb18109db151560c5eca05a7c0# Parent ece5c53577eb38693dc43ed27f5098dc4be80554 Merge with upstream diff --git a/contrib/vim/hgcommand.vim b/contrib/vim/hgcommand.vim --- a/contrib/vim/hgcommand.vim +++ b/contrib/vim/hgcommand.vim @@ -29,10 +29,10 @@ " completes. This allows various actions to only be taken by functions after " system initialization. -if exists("loaded_hgcommand") +if exists("g:loaded_hgcommand") finish endif -let loaded_hgcommand = 1 +let g:loaded_hgcommand = 1 " store 'compatible' settings let s:save_cpo = &cpo @@ -45,7 +45,7 @@ function! s:HGCleanupOnFailure(err) echohl WarningMsg echomsg s:script_name . ":" a:err "Plugin not loaded" echohl None - let loaded_hgcommand = "no" + let g:loaded_hgcommand = "no" unlet s:save_cpo s:script_name endfunction @@ -372,7 +372,7 @@ function! s:HGGetStatusVars(revisionVar, let revision="ADDED" else " The file is tracked, we can try to get is revision number - let hgCommand = HGGetOption("HGCommandHGExec", "hg") . " parents -b " + let hgCommand = HGGetOption("HGCommandHGExec", "hg") . " parents " let statustext=system(hgCommand) if(v:shell_error) return "" @@ -566,11 +566,11 @@ function! s:HGInstallDocumentation(full_ 1 " Delete from first line to a line starts with " === START_DOC - silent 1,/^=\{3,}\s\+START_DOC\C/ d + silent 1,/^=\{3,}\s\+START_DOC\C/ delete _ " Delete from a line starts with " === END_DOC " to the end of the documents: - silent /^=\{3,}\s\+END_DOC\C/,$ d + silent /^=\{3,}\s\+END_DOC\C/,$ delete _ " Add modeline for help doc: the modeline string is mangled intentionally " to avoid it be recognized by VIM: @@ -1048,7 +1048,7 @@ com! HGDisableBufferSetup call HGDisable com! HGEnableBufferSetup call HGEnableBufferSetup() " Allow reloading hgcommand.vim -com! HGReload unlet! loaded_hgcommand | runtime plugin/hgcommand.vim +com! HGReload unlet! g:loaded_hgcommand | runtime plugin/hgcommand.vim " Section: Plugin command mappings {{{1 nnoremap HGAdd :HGAdd @@ -1200,7 +1200,7 @@ delfunction HGFlexiMkdir delfunction HGCleanupOnFailure unlet s:script_version s:script_name -let loaded_hgcommand=2 +let g:loaded_hgcommand=2 silent do HGCommand User HGPluginFinish let &cpo = s:save_cpo diff --git a/contrib/zsh_completion b/contrib/zsh_completion --- a/contrib/zsh_completion +++ b/contrib/zsh_completion @@ -5,6 +5,7 @@ # instance) # # Copyright (C) 2005 Steve Borho +# Copyright (C) 2006 Brendan Cully # # This is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free @@ -13,499 +14,663 @@ # local curcontext="$curcontext" state line -typeset -A opt_args -local repos newFiles addedFiles includeExclude commitMessage -local ridx _hgroot +typeset -A _hg_cmd_globals + +_hg() { + local cmd + integer i=2 + _hg_cmd_globals=() + + while (( i < $#words )) + do + case "$words[$i]" in + -R|--repository|--cwd|--config) + # pass along arguments to hg completer + _hg_cmd_globals+="$words[$i]" + _hg_cmd_globals+="$words[$i+1]" + (( i += 2 )) + continue + ;; + -R*) + _hg_cmd_globals+="$words[$i]" + (( i++ )) + continue + ;; + -*) + # skip option + (( i++ )) + continue + ;; + esac + if [[ -z "$cmd" ]] + then + cmd="$words[$i]" + words[$i]=() + (( CURRENT-- )) + fi + (( i++ )) + done + + if [[ -z "$cmd" ]] + then + _arguments -s -w : $_hg_global_opts \ + ':mercurial command:_hg_commands' + return + fi + + # resolve abbreviations and aliases + if ! (( $+functions[_hg_cmd_${cmd}] )) + then + local cmdexp + (( $#_hg_cmd_list )) || _hg_get_commands + + cmdexp=$_hg_cmd_list[(r)${cmd}*] + if [[ $cmdexp == $_hg_cmd_list[(R)${cmd}*] ]] + then + # might be nice to rewrite the command line with the expansion + cmd="$cmdexp" + fi + if [[ -n $_hg_alias_list[$cmd] ]] + then + cmd=$_hg_alias_list[$cmd] + fi + fi + + if (( $+functions[_hg_cmd_${cmd}] )) + then + curcontext="${curcontext%:*:*}:hg-${cmd}:" + _hg_cmd_${cmd} + return + fi +} -# FIXME: why isn't opt_args available? -[[ -d .hg ]] && _hgroot="$PWD" -ridx=$words[(i)-R] -(( $ridx < $#words )) && _hgroot="${words[$ridx+1]}" +_hg_get_commands() { + typeset -ga _hg_cmd_list + typeset -gA _hg_alias_list + local hline cmd cmdalias + _call_program help hg --verbose help | while read -A hline + do + cmd="$hline[1]" + case $cmd in + *:) + cmd=${cmd%:} + _hg_cmd_list+=($cmd) + ;; + *,) + cmd=${cmd%,} + _hg_cmd_list+=($cmd) + integer i=2 + while (( i <= $#hline )) + do + cmdalias=${hline[$i]%(:|,)} + _hg_cmd_list+=($cmdalias) + _hg_alias_list+=($cmdalias $cmd) + (( i++ )) + done + ;; + esac + done +} + +_hg_commands() { + (( $#_hg_cmd_list )) || _hg_get_commands + _describe -t commands 'mercurial command' _hg_cmd_list +} + +_hg_revrange() { + compset -P 1 '*:' + _hg_tags "$@" +} + +_hg_tags() { + typeset -a tags + local tag rev -# hg dispatch (borrowed from _cvs) -(( $+functions[_hg_cmd] )) || -_hg_cmd () { - _call_program hg hg -R "$_hgroot" "$@" + _hg_cmd tags 2> /dev/null | while read tag rev + do + tags+=($tag) + done + (( $#tags )) && _describe -t tags 'tags' tags +} + +_hg_status() { + status_files=(${(ps:\0:)"$(_hg_cmd status -0n$1 . 2>/dev/null)"}) +} + +_hg_unknown() { + typeset -a status_files + _hg_status u + (( $#status_files )) && _describe -t files 'unknown files' status_files +} + +_hg_missing() { + typeset -a status_files + _hg_status d + (( $#status_files )) && _describe -t files 'missing files' status_files +} + +_hg_addremove() { + _alternative 'files:unknown files:_hg_unknown' \ + 'files:missing files:_hg_missing' +} + +_hg_urls() { + if compset -P bundle:// + then + _files + elif [[ -prefix *: ]] + then + _urls + else + local expl + compset -S '[^:]*' + _wanted url-schemas expl 'URL schema' compadd -S '' - \ + http:// https:// ssh:// bundle:// + fi } -(( $+functions[_hg_state] )) || -_hg_state () { - case "$state" in - (tags) - compadd $(_hg_cmd tags 2> /dev/null | - sed -e 's/[0-9]*:[a-f0-9]*$//; s/ *$//') - ;; - (qapplied) - compadd $(_hg_cmd qapplied) - ;; - (qunapplied) - compadd $(_hg_cmd qunapplied) - ;; - esac +_hg_paths() { + typeset -a paths pnames + _hg_cmd paths 2> /dev/null | while read -A pnames + do + paths+=($pnames[1]) + done + (( $#paths )) && _describe -t path-aliases 'repository alias' paths +} + +_hg_remote() { + _alternative 'path-aliases:repository alias:_hg_paths' \ + 'directories:directory:_files -/' \ + 'urls:URL schema:_hg_urls' +} + +# Common options +_hg_global_opts=( + '(--repository -R)'{-R+,--repository}'[repository root directory]:repository:_files -/' + '--cwd[change working directory]:new working directory:_files -/' + '(--noninteractive -y)'{-y,--noninteractive}'[do not prompt, assume yes for any required answers]' + '(--verbose -v)'{-v,--verbose}'[enable additional output]' + '(--quiet -q)'{-q,--quiet}'[suppress output]' + '(--help -h)'{-h,--help}'[display help and exit]' + '--debug[debug mode]' + '--debugger[start debugger]' + '--traceback[print traceback on exception]' + '--time[time how long the command takes]' + '--profile[profile]' + '--version[output version information and exit]' +) + +_hg_pat_opts=( + '*'{-I+,--include}'[include names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/' + '*'{-X+,--exclude}'[exclude names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/') + +_hg_diff_opts=( + '(--text -a)'{-a,--text}'[treat all files as text]' + '(--git -g)'{-g,--git}'[use git extended diff format]' + "--nodates[don't include dates in diff headers]") + +_hg_dryrun_opts=( + '(--dry-run -n)'{-n,--dry-run}'[do not perform actions, just print output]') + +_hg_style_opts=( + '--style[display using template map file]:' + '--template[display with template]:') + +_hg_commit_opts=( + '(-m --message -l --logfile --edit -e)'{-e,--edit}'[edit commit message]' + '(-e --edit -l --logfile --message -m)'{-m+,--message}'[use as commit message]:message:' + '(-e --edit -m --message --logfile -l)'{-l+,--logfile}'[read the commit message from ]:log file:_files') + +_hg_remote_opts=( + '(--ssh -e)'{-e+,--ssh}'[specify ssh command to use]:' + '--remotecmd[specify hg command to run on the remote side]:') + +_hg_cmd() { + _call_program hg hg "$_hg_cmd_globals[@]" "$@" +} + +_hg_cmd_add() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \ + '*:unknown files:_hg_unknown' +} + +_hg_cmd_addremove() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \ + '(--similarity -s)'{-s+,--similarity}'[guess renamed files by similarity (0<=s<=100)]:' \ + '*:unknown or missing files:_hg_addremove' +} + +_hg_cmd_annotate() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '(--rev -r)'{-r+,--rev}'[annotate the specified revision]:revision:_hg_tags' \ + '(--follow -f)'{-f,--follow}'[follow file copies and renames]' \ + '(--text -a)'{-a,--text}'[treat all files as text]' \ + '(--user -u)'{-u,--user}'[list the author]' \ + '(--date -d)'{-d,--date}'[list the date]' \ + '(--number -n)'{-n,--number}'[list the revision number (default)]' \ + '(--changeset -c)'{-c,--changeset}'[list the changeset]' \ + '*:files:_files -W $(_hg_cmd root)' +} + +_hg_cmd_archive() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '--no-decode[do not pass files through decoders]' \ + '(--prefix -p)'{-p+,--prefix}'[directory prefix for files in archive]:' \ + '(--rev -r)'{-r+,--rev}'[revision to distribute]:revision:_hg_tags' \ + '(--type -t)'{-t+,--type}'[type of distribution to create]:archive type:(files tar tbz2 tgz uzip zip)' \ + '*:destination:_files' +} + +_hg_cmd_bundle() { + _arguments -s -w : $_hg_global_opts $_hg_remote_opts \ + '(--force -f)'{-f,--force}'[run even when remote repository is unrelated]' \ + '(2)*--base[a base changeset to specify instead of a destination]:revision:_hg_tags' \ + ':output file:_files' \ + ':destination repository:_files -/' +} + +_hg_cmd_cat() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '(--output -o)'{-o+,--output}'[print output to file with formatted name]:filespec:' \ + '(--rev -r)'{-r+,--rev}'[revision]:revision:_hg_tags' \ + '*:file:_files -W $(_hg_cmd root)' +} + +_hg_cmd_clone() { + _arguments -s -w : $_hg_global_opts $_hg_remote_opts \ + '(--noupdate -U)'{-U,--noupdate}'[do not update the new working directory]' \ + '(--rev -r)'{-r+,--rev}'[a changeset you would like to have after cloning]:' \ + '--uncompressed[use uncompressed transfer (fast over LAN)]' \ + ':source repository:_hg_remote' \ + ':destination:_files -/' +} + +_hg_cmd_commit() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '(--addremove -A)'{-A,--addremove}'[mark new/missing files as added/removed before committing]' + '(--message -m)'{-m+,--message}'[use as commit message]:text:' \ + '(--logfile -l)'{-l+,--logfile}'[read commit message from ]:log file:_file -g \*.txt' \ + '(--date -d)'{-d+,--date}'[record datecode as commit date]:date code:' \ + '(--user -u)'{-u+,--user}'[record user as commiter]:user:' \ + '*:file:_files -W $(_hg_cmd root)' +} + +_hg_cmd_copy() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \ + '(--after -A)'{-A,--after}'[record a copy that has already occurred]' \ + '(--force -f)'{-f,--force}'[forcibly copy over an existing managed file]' \ + '*:file:_files -W $(_hg_cmd root)' +} + +_hg_cmd_diff() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_diff_opts \ + '*'{-r,--rev}'+[revision]:revision:_hg_revrange' \ + '(--show-function -p)'{-p,--show-function}'[show which function each change is in]' \ + '(--ignore-all-space -w)'{-w,--ignore-all-space}'[ignore white space when comparing lines]' \ + '(--ignore-space-change -b)'{-b,--ignore-space-change}'[ignore changes in the amount of white space]' \ + '(--ignore-blank-lines -B)'{-B,--ignore-blank-lines}'[ignore changes whose lines are all blank]' \ + '*:file:_files -W $(_hg_cmd root)' +} + +_hg_cmd_export() { + _arguments -s -w : $_hg_global_opts $_hg_diff_opts \ + '(--outout -o)'{-o+,--output}'[print output to file with formatted name]:filespec:' \ + '--switch-parent[diff against the second parent]' \ + '*:revision:_hg_tags' +} + +_hg_cmd_grep() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '(--print0 -0)'{-0,--print0}'[end filenames with NUL]' \ + '--all[print all revisions with matches]' \ + '(--follow -f)'{-f,--follow}'[follow changeset or file history]' \ + '(--ignore-case -i)'{-i,--ignore-case}'[ignore case when matching]' \ + '(--files-with-matches -l)'{-l,--files-with-matches}'[print only filenames and revs that match]' \ + '(--line-number -n)'{-n,--line-number}'[print matching line numbers]' \ + '*'{-r+,--rev}'[search in given revision range]:revision:_hg_revrange' \ + '(--user -u)'{-u,--user}'[print user who committed change]' \ + '*:search pattern:_files -W $(_hg_cmd root)' +} + +_hg_cmd_heads() { + _arguments -s -w : $_hg_global_opts $_hg_style_opts \ + '(--rev -r)'{-r+,--rev}'[show only heads which are descendants of rev]:revision:_hg_tags' +} + +_hg_cmd_help() { + _arguments -s -w : $_hg_global_opts \ + '*:mercurial command:_hg_commands' } -(( $+_hg_commands )) || -_hg_commands=($(hg -v help | sed -e '1,/^list of commands:/d' \ - -e '/^global options:/,$d' -e '/^ [^ ]/!d; s/[,:].*//g;')) - -# A lot of commands have these arguments -includeExclude=( - '*-I-[include names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/' - '*--include-[include names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/' - '*-X-[exclude names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/' - '*--exclude-[exclude names matching the given patterns]:dir:_files -W $(_hg_cmd root) -/') - -styleOpts=( - '--style[display using template map file]:' - '--template[display with template]:') - -commitMessage=( - '(-m --message -l --logfile --edit)-e[edit commit message]' - '(-m --message -l --logfile -e)--edit[edit commit message]' - '(-e --edit -l --logfile --message)-m[use as commit message]:message:' - '(-e --edit -l --logfile -m)--message[use as commit message]:message:' - '(-e --edit -m --message --logfile)-l[read the commit message from ]:log file:_files' - '(-e --edit -m --message -l)--logfile[read the commit message from ]:log file:_files') +_hg_cmd_import() { + _arguments -s -w : $_hg_global_opts \ + '(--strip -p)'{-p+,--strip}'[directory strip option for patch (default: 1)]:count:' \ + '(--message -m)'{-m+,--message}'[use as commit message]:text:' \ + '(--force -f)'{-f,--force}'[skip check for outstanding uncommitted changes]' \ + '*:patch:_files' +} -if [[ $service == "hg" ]]; then - _arguments -C -A "-*" \ - '(--repository)-R[repository root directory]:root:_files -/' \ - '(-R)--repository[repository root directory]:root:_files -/' \ - '--cwd[change working directory]:new working directory:_files -/' \ - '(--noninteractive)-y[do not prompt, assume yes for any required answers]' \ - '(-y)--noninteractive[do not prompt, assume yes for any required answers]' \ - '(--verbose)-v[enable additional output]' \ - '(-v)--verbose[enable additional output]' \ - '(--quiet)-q[suppress output]' \ - '(-q)--quiet[suppress output]' \ - '(--help)-h[display help and exit]' \ - '(-h)--help[display help and exit]' \ - '--debug[debug mode]' \ - '--debugger[start debugger]' \ - '--traceback[print traceback on exception]' \ - '--time[time how long the command takes]' \ - '--profile[profile]' \ - '--version[output version information and exit]' \ - '*::command:->subcmd' && return 0 +_hg_cmd_incoming() { + _arguments -s -w : $_hg_global_opts $_hg_remote_opts $_hg_style_opts \ + '(--no-merges -M)'{-M,--no-merges}'[do not show merge revisions]' \ + '(--force -f)'{-f,--force}'[run even when the remote repository is unrelated]' \ + '(--patch -p)'{-p,--patch}'[show patch]' \ + '(--rev -r)'{-r+,--rev}'[a specific revision up to which you would like to pull]' \ + '(--newest-first -n)'{-n,--newest-first}'[show newest record first]' \ + '--bundle[file to store the bundles into]:bundle file:_files' \ + ':source:_hg_remote' +} - if (( CURRENT == 1 )); then - _wanted commands expl 'hg command' compadd -a _hg_commands - return - fi - service="$words[1]" - curcontext="${curcontext%:*}=$service:" -fi +_hg_cmd_init() { + _arguments -s -w : $_hg_global_opts $_hg_remote_opts \ + ':dir:_files -/' +} + +_hg_cmd_locate() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '(--rev -r)'{-r+,--rev}'[search repository as it stood at revision]:revision:_hg_tags' \ + '(--print0 -0)'{-0,--print0}'[end filenames with NUL, for use with xargs]' \ + '(--fullpath -f)'{-f,--fullpath}'[print complete paths]' \ + '*:search pattern:_files -W $(_hg_cmd root)' +} -case $service in - (add) - newFiles=(${(ps:\0:)"$(_hg_cmd status -0un .)"}) - _arguments $includeExclude \ - '*:file:->unknown' - _wanted files expl 'unknown files' compadd -a newFiles - ;; +_hg_cmd_log() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_style_opts \ + '(--follow --follow-first -f)'{-f,--follow}'[follow changeset or history]' \ + '(-f --follow)--follow-first[only follow the first parent of merge changesets]' \ + '(--copies -C)'{-C,--copies}'[show copied files]' \ + '(--keyword -k)'{-k+,--keyword}'[search for a keyword]:' \ + '(--limit -l)'{-l+,--limit}'[limit number of changes displayed]:' \ + '*'{-r,--rev}'[show the specified revision or range]:revision:_hg_revrange' \ + '(--no-merges -M)'{-M,--no-merges}'[do not show merges]' \ + '(--only-merges -m)'{-m,--only-merges}'[show only merges]' \ + '(--patch -p)'{-p,--patch}'[show patch]' \ + '(--prune -P)'{-P+,--prune}'[do not display revision or any of its ancestors]:revision:_hg_tags' \ + '*:files:_files -W $(_hg_cmd root)' +} - (addremove) - _arguments $includeExclude \ - '*:directories:_files -/' # assume they want to add/remove a dir - ;; - - (forget) - addedFiles=(${(ps:\0:)"$(_hg_cmd status -0an .)"}) - _arguments $includeExclude \ - '*:file:->added' - _wanted files expl 'newly added files' compadd -a addedFiles - ;; - - (remove|rm) - _arguments $includeExclude \ - '*:file:_files' - ;; +_hg_cmd_manifest() { + _arguments -s -w : $_hg_global_opts \ + ':revision:_hg_tags' +} - (copy|cp) - _arguments $includeExclude \ - '(--after)-A[record a copy that has already occurred]' \ - '(-A)--after[record a copy that has already occurred]' \ - '(--force)-f[forcibly copy over an existing managed file]' \ - '(-f)--force[forcibly copy over an existing managed file]' \ - '(--parents)-p[append source path to dest]' \ - '(-p)--parents[append source path to dest]' \ - '*:files:_files' - ;; +_hg_cmd_outgoing() { + _arguments -s -w : $_hg_global_opts $_hg_remote_opts $_hg_style_opts \ + '(--no-merges -M)'{-M,--no-merges}'[do not show merge revisions]' \ + '(--force -f)'{-f,--force}'[run even when the remote repository is unrelated]' \ + '(--patch -p)'{-p,--patch}'[show patch]' \ + '(--rev -r)'{-r+,--rev}'[a specific revision you would like to push]' \ + '(--newest-first -n)'{-n,--newest-first}'[show newest record first]' \ + ':destination:_hg_remote' +} - (rename|mv) - if (( CURRENT == 2 )); then - _arguments $includeExclude \ - '(--after)-A[record a rename that has already occurred]' \ - '(-A)--after[record a rename that has already occurred]' \ - '(--force)-f[replace destination if it exists]' \ - '(-F)--force[replace destination if it exists]' \ - '(--parents)-p[append source path to dest]' \ - '(-p)--parents[append source path to dest]' \ - '*:files:_files' - else - _arguments '*:destination:_files' - fi - ;; +_hg_cmd_parents() { + _arguments -s -w : $_hg_global_opts $_hg_style_opts \ + '(--rev -r)'{-r+,--rev}'[show parents of the specified rev]:revision:_hg_tags' \ + ':revision:_hg_tags' +} - (diff) - _arguments $includeExclude \ - '*-r[revision]:revision:->tags' \ - '*--rev[revision]:revision:->tags' \ - '(--text)-a[treat all files as text]' \ - '(-a)--text[treat all files as text]' \ - '*:file:_files' - ;; +_hg_cmd_paths() { + _arguments -s -w : $_hg_global_opts \ + ':path:_hg_paths' +} + +_hg_cmd_pull() { + _arguments -s -w : $_hg_global_opts $_hg_remote_opts \ + '(--force -f)'{-f,--force}'[run even when the remote repository is unrelated]' \ + '(--update -u)'{-u,--update}'[update to new tip if changesets were pulled]' \ + ':source:_hg_remote' +} - (status|st) - _arguments $includeExclude \ - '(--no-status)-n[hide status prefix]' \ - '(-n)--no-status[hide status prefix]' \ - '(--print0)-0[end filenames with NUL, for use with xargs]' \ - '(-0)--print0[end filenames with NUL, for use with xargs]' \ - '(--modified)-m[show only modified files]' \ - '(-m)--modified[show only modified files]' \ - '(--added)-a[show only added files]' \ - '(-a)--added[show only added files]' \ - '(--removed)-r[show only removed files]' \ - '(-r)--removed[show only removed files]' \ - '(--unknown)-u[show only unknown files]' \ - '(-u)--unknown[show only unknown files]' \ - '*:search pattern, then files:_files' - ;; +_hg_cmd_push() { + _arguments -s -w : $_hg_global_opts $_hg_remote_opts \ + '(--force -f)'{-f,--force}'[force push]' \ + '(--rev -r)'{-r+,--rev}'[a specific revision you would like to push]' \ + ':destination:_hg_remote' +} + +_hg_cmd_remove() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '(--after -A)'{-A,--after}'[record remove that has already occurred]' \ + '(--force -f)'{-f,--force}'[remove file even if modified]' \ + '*:file:_files -W $(_hg_cmd root)' +} - (revert) - addedFiles=(${(ps:\0:)"$(_hg_cmd status -0amrn .)"}) - _arguments \ - '(--rev)-r[revision to revert to]:revision:->tags' \ - '(-r)--rev[revision to revert to]:revision:->tags' \ - '(--nonrecursive)-n[do not recurse into subdirectories]' \ - '(-n)--nonrecursive[do not recurse into subdirectories]' \ - '*:file:->modified' - _wanted files expl 'mofified files' compadd -a addedFiles - ;; +_hg_cmd_rename() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \ + '(--after -A)'{-A,--after}'[record a rename that has already occurred]' \ + '(--force -f)'{-f,--force}'[forcibly copy over an existing managed file]' \ + '*:file:_files -W $(_hg_cmd root)' +} - (commit|ci) - addedFiles=(${(ps:\0:)"$(_hg_cmd status -0amrn .)"}) - _arguments $includeExclude \ - '(--addremove)-A[run addremove during commit]' \ - '(-A)--addremove[run addremove during commit]' \ - '(--message)-m[use as commit message]:string:' \ - '(-m)--message[use as commit message]:string:' \ - '(--logfile)-l[read commit message from ]:.log file:_file -g \*.txt' \ - '(-l)--logfile[read commit message from ]:.log file:_file -g \*.txt' \ - '(--date)-d[record datecode as commit date]:date code:' \ - '(-d)--date[record datecode as commit date]:date code:' \ - '(--user)-u[record user as commiter]:user:' \ - '(-u)--user[record user as commiter]:user:' \ - '*:file:->modified' - _wanted files expl 'mofified files' compadd -a addedFiles - ;; - - (cat) - _arguments $includeExclude \ - '(--output)-o[print output to file with formatted name]:filespec:' \ - '(-o)--output[print output to file with formatted name]:filespec:' \ - '(--rev)-r[revision]:revision:->tags' \ - '(-r)--rev[revision]:revision:->tags' \ - '*:file:_files' - ;; +_hg_cmd_revert() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_dryrun_opts \ + '(--all -a :)'{-a,--all}'[revert all changes when no arguments given]' \ + '(--rev -r)'{-r+,--rev}'[revision to revert to]:revision:_hg_tags' \ + '--no-backup[do not save backup copies of files]' \ + '*:file:_files -W $(_hg_cmd root)' +} - (annotate) - _arguments $includeExclude \ - '(--rev)-r[annotate the specified revision]:revision:->tags' \ - '(-r)--rev[annotate the specified revision]:revision:->tags' \ - '(--text)-a[treat all files as text]' \ - '(-a)--text[treat all files as text]' \ - '(--user)-u[list the author]' \ - '(-u)--user[list the author]' \ - '(--changeset)-c[list the changeset]' \ - '(-c)--changeset[list the changeset]' \ - '(--number)-n[list the revision number (default)]' \ - '(-n)--number[list the revision number (default)]' \ - '*:files:_files' - ;; +_hg_cmd_serve() { + _arguments -s -w : $_hg_global_opts \ + '(--accesslog -A)'{-A+,--accesslog}'[name of access log file]:log file:_files' \ + '(--errorlog -E)'{-E+,--errorlog}'[name of error log file]:log file:_files' \ + '(--daemon -d)'{-d,--daemon}'[run server in background]' \ + '(--port -p)'{-p+,--port}'[listen port]:listen port:' \ + '(--address -a)'{-a+,--address}'[interface address]:interface address:' \ + '(--name -n)'{-n+,--name}'[name to show in web pages]:repository name:' \ + '(--templates -t)'{-t,--templates}'[web template directory]:template dir:_files -/' \ + '--style[web template style]:style' \ + '--stdio[for remote clients]' \ + '(--ipv6 -6)'{-6,--ipv6}'[use IPv6 in addition to IPv4]' +} - (grep) - _arguments $includeExclude \ - '*-r[search in given revision range]:revision:->tags' \ - '*--rev[search in given revision range]:revision:->tags' \ - '--all[print all revisions with matches]' \ - '(-print0)-0[end filenames with NUL, for use with xargs]' \ - '(-0)--print0[end filenames with NUL, for use with xargs]' \ - '(--ignore-case)-i[ignore case when matching]' \ - '(-i)--ignore-case[ignore case when matching]' \ - '(--files-with-matches)-l[print names of files and revs that match]' \ - '(-l)--files-with-matches[print names of files and revs that match]' \ - '(--line-number)-n[print matching line numbers]' \ - '(-n)--line-number[print matching line numbers]' \ - '(--user)-u[print user who committed change]' \ - '(-u)--user[print user who committed change]' \ - '*:search pattern:' - ;; +_hg_cmd_status() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '(--all -A)'{-A,--all}'[show status of all files]' \ + '(--modified -m)'{-m,--modified}'[show only modified files]' \ + '(--added -a)'{-a,--added}'[show only added files]' \ + '(--removed -r)'{-r,--removed}'[show only removed files]' \ + '(--deleted -d)'{-d,--deleted}'[show only deleted (but tracked) files]' \ + '(--clean -c)'{-c,--clean}'[show only files without changes]' \ + '(--unknown -u)'{-u,--unknown}'[show only unknown files]' \ + '(--ignored -i)'{-i,--ignored}'[show ignored files]' \ + '(--no-status -n)'{-n,--no-status}'[hide status prefix]' \ + '(--copies -C)'{-C,--copies}'[show source of copied files]' \ + '(--print0 -0)'{-0,--print0}'[end filenames with NUL, for use with xargs]' \ + '--rev[show difference from revision]:revision:_hg_tags' \ + '*:files:_files' +} - (locate) - _arguments $includeExclude \ - '(--rev)-r[search repository as it stood at revision]:revision:->tags' \ - '(-r)--rev[search repository as it stood at revision]:revision:->tags' \ - '(--print0)-0[end filenames with NUL, for use with xargs]' \ - '(-0)--print0[end filenames with NUL, for use with xargs]' \ - '(--fullpath)-f[print complete paths]' \ - '(-f)--fullpath[print complete paths]' \ - '*:search pattern:' - ;; +_hg_cmd_tag() { + _arguments -s -w : $_hg_global_opts \ + '(--local -l)'{-l,--local}'[make the tag local]' \ + '(--message -m)'{-m+,--message}'[message for tag commit log entry]:message:' \ + '(--date -d)'{-d+,--date}'[record datecode as commit date]:date code:' \ + '(--user -u)'{-u+,--user}'[record user as commiter]:user:' \ + '(--rev -r)'{-r+,--rev}'[revision to tag]:revision:_hg_tags' \ + ':tag name:' +} - (log|history) - _arguments $includeExclude $styleOpts \ - '*-r[show the specified revision or range]:revision:->tags' \ - '*--rev[show the specified revision or range]:revision:->tags' \ - '(--no-merges -M --only-merges)-m[show only merge revisions]' \ - '(--no-merges -M -m)--only-merges[show only merge revisions]' \ - '(--only-merges -m --no-merges)-M[do not show merge revisions]' \ - '(--only-merges -m -M)--no-merges[do not show merge revisions]' \ - '(--keyword)-k[search for a keyword]:keyword:' \ - '(-k)--keyword[search for a keyword]:keyword:' \ - '(--branch)-b[show branches]' \ - '(-b)--branch[show branches]' \ - '(--patch)-p[show patch]' \ - '(-p)--patch[show patch]' \ - '*:file:_files' - ;; +_hg_cmd_tip() { + _arguments -s -w : $_hg_global_opts $_hg_style_opts \ + '(--patch -p)'{-p,--patch}'[show patch]' +} + +_hg_cmd_unbundle() { + _arguments -s -w : $_hg_global_opts \ + '(--update -u)'{-u,--update}'[update to new tip if changesets were unbundled]' \ + ':files:_files' +} - (update|checkout|co) - _arguments \ - '(--branch)-b[checkout the head of a specific branch]' \ - '(-b)--branch[checkout the head of a specific branch]' \ - '(-C --clean --merge)-m[allow merging of branches]' \ - '(-C --clean -m)--merge[allow merging of branches]' \ - '(-m --merge --clean)-C[overwrite locally modified files]' \ - '(-m --merge -C)--clean[overwrite locally modified files]' \ - '*:revision or tag:->tags' - ;; +_hg_cmd_update() { + _arguments -s -w : $_hg_global_opts \ + '(--clean -C)'{-C,--clean}'[overwrite locally modified files]' \ + '(--force -f)'{-f,--force}'[force a merge with outstanding changes]' \ + ':revision:_hg_tags' +} - (tag) - _arguments \ - '(--local)-l[make the tag local]' \ - '(-l)--local[make the tag local]' \ - '(--message)-m[message for tag commit log entry]:string:' \ - '(-m)--message[message for tag commit log entry]:string:' \ - '(--date)-d[record datecode as commit date]:date code:' \ - '(-d)--date[record datecode as commit date]:date code:' \ - '(--user)-u[record user as commiter]:user:' \ - '(-u)--user[record user as commiter]:user:' \ - '*:name, then revision:->tags' - ;; +# HGK +_hg_cmd_view() { + _arguments -s -w : $_hg_global_opts \ + '(--limit -l)'{-l+,--limit}'[limit number of changes displayed]:' \ + ':revision range:_hg_tags' +} - (clone) - if (( CURRENT == 2 )); then - repos=( $(_hg_cmd paths | sed -e 's/^.*= //') ) - _arguments \ - '(--no-update)-U[do not update the new working directory]' \ - '(-U)--no-update[do not update the new working directory]' \ - '(--ssh)-e[specify ssh command to use]:string:' \ - '(-e)--ssh[specify ssh command to use]:string:' \ - '--pull[use pull protocol to copy metadata]' \ - '--remotecmd[specify hg command to run on the remote side]:remote hg:' \ - '*:local repo:_files -/' - _wanted source expl 'source repository' compadd -a repos - elif (( CURRENT == 3 )); then - _arguments '*:dest repo:_files -/' - fi - ;; +# MQ +_hg_qseries() { + typeset -a patches + patches=($(_hg_cmd qseries 2>/dev/null)) + (( $#patches )) && _describe -t hg-patches 'patches' patches +} - (rawcommit) - _arguments \ - '(--parent)-p[parent revision]:revision:->tags' \ - '(-p)--parent[parent revision]:revision:->tags' \ - '(--date)-d[record datecode as commit date]:date code:' \ - '(-d)--date[record datecode as commit date]:date code:' \ - '(--user)-u[record user as commiter]:user:' \ - '(-u)--user[record user as commiter]:user:' \ - '(--message)-m[use as commit message]:string:' \ - '(-m)--message[use as commit message]:string:' \ - '(--logfile)-l[read commit message from ]:.log file:_file -g \*.txt' \ - '(-l)--logfile[read commit message from ]:.log file:_file -g \*.txt' \ - '(--files)-F[file list]:file list:_files' \ - '(-F)--files[file list]:file list:_files' \ - '*:files to commit:_files' - ;; +_hg_qapplied() { + typeset -a patches + patches=($(_hg_cmd qapplied 2>/dev/null)) + if (( $#patches )) + then + patches+=(qbase qtip) + _describe -t hg-applied-patches 'applied patches' patches + fi +} + +_hg_qunapplied() { + typeset -a patches + patches=($(_hg_cmd qunapplied 2>/dev/null)) + (( $#patches )) && _describe -t hg-unapplied-patches 'unapplied patches' patches +} - (bundle) - if (( CURRENT == 2 )); then - _arguments '*:changegroup file:_files -g \*.hg' - elif (( CURRENT == 3 )); then - _arguments '*:other repo:_files -/' - fi - ;; +_hg_qguards() { + typeset -a guards + local guard + compset -P "+|-" + _hg_cmd qselect -s 2>/dev/null | while read guard + do + guards+=(${guard#(+|-)}) + done + (( $#guards )) && _describe -t hg-guards 'guards' guards +} - (unbundle) - _arguments '*:changegroup .hg file:_files -g \*.hg' - ;; +_hg_qseries_opts=( + '(--summary -s)'{-s,--summary}'[print first line of patch header]') - (incoming) - _arguments $styleOpts \ - '(--patch)-p[show patch]' \ - '(-p)--patch[show patch]' \ - '(--no-merges)-M[do not show merge revisions]' \ - '(-M)--no-merges[do not show merge revisions]' \ - '(--newest-first)-n[show newest record first]' \ - '(-n)--newest-first[show newest record first]' \ - '*:mercurial repository:_files -/' - ;; +_hg_cmd_qapplied() { + _arguments -s -w : $_hg_global_opts $_hg_qseries_opts +} - (import|patch) - _arguments \ - '(--strip)-p[directory strip option for patch (default: 1)]:count:' \ - '(-p)--strip[directory strip option for patch (default: 1)]:count:' \ - '(--force)-f[skip check for outstanding uncommitted changes]' \ - '(-f)--force[skip check for outstanding uncommitted changes]' \ - '(--base)-b[base directory to read patches from]:file:_files -W $(_hg_cmd root) -/' \ - '(-b)--base[base directory to read patches from]:file:_files -W $(_hg_cmd root) -/' \ - '*:patch file:_files' - ;; +_hg_cmd_qdelete() { + _arguments -s -w : $_hg_global_opts \ + '(--keep -k)'{-k,--keep}'[keep patch file]' \ + '*'{-r+,--rev}'[stop managing a revision]:applied patch:_hg_revrange' \ + '*:unapplied patch:_hg_qunapplied' +} + +_hg_cmd_qdiff() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '*:pattern:_files -W $(_hg_cmd root)' +} - (pull) - repos=( $(_hg_cmd paths | sed -e 's/^.*= //') ) - _arguments \ - '(--update)-u[update working directory to tip after pull]' \ - '(-u)--update[update working directory to tip after pull]' \ - '(--ssh)-e[specify ssh command to use]:ssh command:' \ - '(-e)--ssh[specify ssh command to use]:ssh command:' \ - '--remotecmd[specify hg command to run on the remote side]:remote hg:' \ - '*:local repo:_files -/' - _wanted source expl 'source repository' compadd -a repos - ;; +_hg_cmd_qfold() { + _arguments -s -w : $_hg_global_opts $_h_commit_opts \ + '(--keep,-k)'{-k,--keep}'[keep folded patch files]' \ + '*:unapplied patch:_hg_qunapplied' +} - (outgoing) - _arguments $styleOpts \ - '(--patch)-p[show patch]' \ - '(-p)--patch[show patch]' \ - '(--no-merges)-M[do not show merge revisions]' \ - '(-M)--no-merges[do not show merge revisions]' \ - '(--newest-first)-n[show newest record first]' \ - '(-n)--newest-first[show newest record first]' \ - '*:local repo:_files -/' - _wanted source expl 'source repository' compadd -a repos - ;; +_hg_cmd_qguard() { + _arguments -s -w : $_hg_global_opts \ + '(--list -l)'{-l,--list}'[list all patches and guards]' \ + '(--none -n)'{-n,--none}'[drop all guards]' \ + ':patch:_hg_qseries' \ + '*:guards:_hg_qguards' +} + +_hg_cmd_qheader() { + _arguments -s -w : $_hg_global_opts \ + ':patch:_hg_qseries' +} - (export) - _arguments \ - '(--outout)-o[print output to file with formatted name]:filespec:' \ - '(-o)--output[print output to file with formatted name]:filespec:' \ - '(--text)-a[treat all files as text]' \ - '(-a)--text[treat all files as text]' \ - '*:revision:->revs' - _wanted revs expl 'revision or tag' compadd -a tags - ;; +_hg_cmd_qimport() { + _arguments -s -w : $_hg_global_opts \ + '(--existing -e)'{-e,--existing}'[import file in patch dir]' \ + '(--name -n 2)'{-n+,--name}'[patch file name]:name:' \ + '(--force -f)'{-f,--force}'[overwrite existing files]' \ + '*'{-r+,--rev}'[place existing revisions under mq control]:revision:_hg_revrange' \ + '*:patch:_files' +} - (push) - repos=( $(_hg_cmd paths | sed -e 's/^.*= //') ) - _arguments \ - '(--force)-f[force push]' \ - '(-f)--force[force push]' \ - '(--ssh)-e[specify ssh command to use]:ssh command:' \ - '(-e)--ssh[specify ssh command to use]:ssh command:' \ - '--remotecmd[specify hg command to run on the remote side]:remote hg:' \ - '*:local repo:_files -/' - _wanted source expl 'source repository' compadd -a repos - ;; +_hg_cmd_qnew() { + _arguments -s -w : $_hg_global_opts $_hg_commit_opts \ + '(--force -f)'{-f,--force}'[import uncommitted changes into patch]' \ + ':patch:' +} + +_hg_cmd_qnext() { + _arguments -s -w : $_hg_global_opts $_hg_qseries_opts +} - (serve) - _arguments \ - '(--accesslog)-A[name of access log file]:log file:_files' \ - '(-A)--accesslog[name of access log file]:log file:_files' \ - '(--errorlog)-E[name of error log file]:log file:_files' \ - '(-E)--errorlog[name of error log file]:log file:_files' \ - '(--port)-p[listen port]:listen port:' \ - '(-p)--port[listen port]:listen port:' \ - '(--address)-a[interface address]:interface address:' \ - '(-a)--address[interface address]:interface address:' \ - '(--name)-n[name to show in web pages]:repository name:' \ - '(-n)--name[name to show in web pages]:repository name:' \ - '(--templates)-t[web template directory]:template dir:_files -/' \ - '(-t)--templates[web template directory]:template dir:_files -/' \ - '--style[web template style]:style' \ - '--stdio[for remote clients]' \ - '(--ipv6)-6[use IPv6 in addition to IPv4]' \ - '(-6)--ipv6[use IPv6 in addition to IPv4]' - ;; +_hg_cmd_qpop() { + _arguments -s -w : $_hg_global_opts \ + '(--all -a :)'{-a,--all}'[pop all patches]' \ + '(--name -n)'{-n+,--name}'[queue name to pop]:' \ + '(--force -f)'{-f,--force}'[forget any local changes]' \ + ':patch:_hg_qapplied' +} - (help) - _wanted commands expl 'hg command' compadd -a _hg_commands - ;; +_hg_cmd_qprev() { + _arguments -s -w : $_hg_global_opts $_hg_qseries_opts +} - (heads) - _arguments $styleOpts \ - '(--branches)-b[find branch info]' \ - '(-b)--branches[find branch info]' - ;; - - (paths) - _arguments '*:symbolic name:(default default-push)' - ;; - - (init) - _arguments '*:new repo directory:_files -/' - ;; +_hg_cmd_qpush() { + _arguments -s -w : $_hg_global_opts \ + '(--all -a :)'{-a,--all}'[apply all patches]' \ + '(--list -l)'{-l,--list}'[list patch name in commit text]' \ + '(--merge -m)'{-m+,--merge}'[merge from another queue]:' \ + '(--name -n)'{-n+,--name}'[merge queue name]:' \ + '(--force -f)'{-f,--force}'[apply if the patch has rejects]' \ + ':patch:_hg_qunapplied' +} - (manifest) - _arguments '*:revision:->tags' - ;; - - (par*) - _arguments $styleOpts \ - '(--rev 1)-r[show parents of the specified rev]:revision:->tags' \ - '(-r 1)--rev[show parents of the specified rev]:revision:->tags' \ - '::revision:->tags' - ;; - - (identify|recover|root|undo|verify|version|ct|tags) - # no arguments for these commands - ;; +_hg_cmd_qrefresh() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts $_hg_commit_opts \ + '(--git -g)'{-g,--git}'[use git extended diff format]' \ + '(--short -s)'{-s,--short}'[short refresh]' \ + '*:files:_files -W $(_hg_cmd root)' +} - # HGK - (vi*) - _arguments \ - '(--limit)-l[limit number of changes displayed]:' \ - '(-l)--limit[limit number of changes displayed]:' \ - '::revision range:->tags' - ;; +_hg_cmd_qrename() { + _arguments -s -w : $_hg_global_opts \ + ':patch:_hg_qseries' \ + ':destination:' +} - # MQ commands - (qdel*|qrm|qrem*) - _arguments \ - {-k,--keep}'[keep patch file]' \ - {-r,--rev}'[revision]:applied patch:->qapplied' \ - '*:unapplied patches:->qunapplied' - ;; +_hg_cmd_qselect() { + _arguments -s -w : $_hg_global_opts \ + '(--none -n :)'{-n,--none}'[disable all guards]' \ + '(--series -s :)'{-s,--series}'[list all guards in series file]' \ + '--pop[pop to before first guarded applied patch]' \ + '--reapply[pop and reapply patches]' \ + '*:guards:_hg_qguards' +} - (qnew) - _arguments $commitMessage \ - {-f,--force}'[import uncommitted changes into patch]' \ - ':patch name:' - ;; +_hg_cmd_qseries() { + _arguments -s -w : $_hg_global_opts $_hg_qseries_opts \ + '(--missing -m)'{-m,--missing}'[print patches not in series]' +} - (qpo*) - _arguments \ - (1){-a,--all}'[pop all patches]' \ - {-f,--force}'[forget any local changes]' \ - ':applied patch:->qapplied' - ;; +_hg_cmd_qunapplied() { + _arguments -s -w : $_hg_global_opts $_hg_qseries_opts +} - (qpu*) - _arguments \ - (1){-a,--all}'[apply all patches]' \ - {-f,--force}'[apply if the patch has rejects]' \ - ':unapplied patch:->qunapplied' - ;; - (qref*) - _arguments $commitMessage $includeExclude \ - {-g,--git}'[use git extended diff format]' \ - {-s,--short}'[short refresh]' - ;; +_hg_cmd_qtop() { + _arguments -s -w : $_hg_global_opts $_hg_qseries_opts +} - (*) - _message "unknown hg command completion: $service" - ;; -esac +_hg_cmd_strip() { + _arguments -s -w : $_hg_global_opts \ + '(--force -f)'{-f,--force}'[force multi-head removal]' \ + '(--backup -b)'{-b,--backup}'[bundle unrelated changesets]' \ + '(--nobackup -n)'{-n,--nobackup}'[no backups]' \ + ':revision:_hg_tags' +} -_hg_state +_hg "$@" diff --git a/doc/hg.1.txt b/doc/hg.1.txt --- a/doc/hg.1.txt +++ b/doc/hg.1.txt @@ -8,7 +8,7 @@ hg - Mercurial source code management sy SYNOPSIS -------- -'hg' [-v -d -q -y] [command options] [files] +'hg' [global option]... [command/global option]... [argument]... DESCRIPTION ----------- diff --git a/doc/hgrc.5.txt b/doc/hgrc.5.txt --- a/doc/hgrc.5.txt +++ b/doc/hgrc.5.txt @@ -50,6 +50,9 @@ installed. particular repository. This file is not version-controlled, and will not get transferred during a "clone" operation. Options in this file override options in all other configuration files. + On Unix, most of this file will be ignored if it doesn't belong + to a trusted user or to a trusted group. See the documentation + for the trusted section below for more details. SYNTAX ------ @@ -364,6 +367,22 @@ server:: 6Mbps), uncompressed streaming is slower, because of the extra data transfer overhead. Default is False. +trusted:: + For security reasons, Mercurial will not use the settings in + the .hg/hgrc file from a repository if it doesn't belong to a + trusted user or to a trusted group. The main exception is the + web interface, which automatically uses some safe settings, since + it's common to serve repositories from different users. + + This section specifies what users and groups are trusted. The + current user is always trusted. To trust everybody, list a user + or a group with name "*". + + users;; + Comma-separated list of trusted users. + groups;; + Comma-separated list of trusted groups. + ui:: User interface controls. debug;; diff --git a/hgext/mq.py b/hgext/mq.py --- a/hgext/mq.py +++ b/hgext/mq.py @@ -1337,13 +1337,20 @@ class queue: for filename in files: if existing: + if filename == '-': + raise util.Abort(_('-e is incompatible with import from -')) if not patchname: patchname = filename if not os.path.isfile(self.join(patchname)): raise util.Abort(_("patch %s does not exist") % patchname) else: try: - text = file(filename).read() + if filename == '-': + if not patchname: + raise util.Abort(_('need --name to import a patch from -')) + text = sys.stdin.read() + else: + text = file(filename).read() except IOError: raise util.Abort(_("unable to read %s") % patchname) if not patchname: diff --git a/mercurial/bdiff.c b/mercurial/bdiff.c --- a/mercurial/bdiff.c +++ b/mercurial/bdiff.c @@ -13,11 +13,7 @@ #include #include -#ifdef __hpux -#define inline -#endif - -#ifdef __SUNPRO_C +#if defined __hpux || defined __SUNPRO_C || defined _AIX # define inline #endif diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -1196,7 +1196,7 @@ def debugcheckstate(ui, repo): error = _(".hg/dirstate inconsistent with current parent's manifest") raise util.Abort(error) -def showconfig(ui, repo, *values): +def showconfig(ui, repo, *values, **opts): """show combined config settings from all hgrc files With no args, print names and values of all config items. @@ -1207,10 +1207,11 @@ def showconfig(ui, repo, *values): With multiple args, print names and values of all config items with matching section names.""" + untrusted = bool(opts.get('untrusted')) if values: if len([v for v in values if '.' in v]) > 1: raise util.Abort(_('only one config item permitted')) - for section, name, value in ui.walkconfig(): + for section, name, value in ui.walkconfig(untrusted=untrusted): sectname = section + '.' + name if values: for v in values: @@ -2091,10 +2092,17 @@ def pull(ui, repo, source="default", **o Valid URLs are of the form: - local/filesystem/path + local/filesystem/path (or file://local/filesystem/path) http://[user@]host[:port]/[path] https://[user@]host[:port]/[path] ssh://[user@]host[:port]/[path] + static-http://host[:port]/[path] + + Paths in the local filesystem can either point to Mercurial + repositories or to bundle files (as created by 'hg bundle' or + 'hg incoming --bundle'). The static-http:// protocol, albeit slow, + allows access to a Mercurial repository where you simply use a web + server to publish the .hg directory as static content. Some notes about using SSH with Mercurial: - SSH requires an accessible shell account on the destination machine @@ -2142,14 +2150,16 @@ def push(ui, repo, dest=None, **opts): Valid URLs are of the form: - local/filesystem/path + local/filesystem/path (or file://local/filesystem/path) ssh://[user@]host[:port]/[path] + http://[user@]host[:port]/[path] + https://[user@]host[:port]/[path] Look at the help text for the pull command for important details about ssh:// URLs. - Pushing to http:// and https:// URLs is possible, too, if this - feature is enabled on the remote Mercurial server. + Pushing to http:// and https:// URLs is only possible, if this + feature is explicitly enabled on the remote Mercurial server. """ dest = ui.expandpath(dest or 'default-push', dest or 'default') setremoteconfig(ui, opts) @@ -3074,7 +3084,10 @@ table = { _('hg revert [-r REV] [NAME]...')), "rollback": (rollback, [], _('hg rollback')), "root": (root, [], _('hg root')), - "showconfig|debugconfig": (showconfig, [], _('showconfig [NAME]...')), + "showconfig|debugconfig": + (showconfig, + [('u', 'untrusted', None, _('show untrusted configuration options'))], + _('showconfig [-u] [NAME]...')), "^serve": (serve, [('A', 'accesslog', '', _('name of access log file to write to')), @@ -3486,6 +3499,15 @@ def dispatch(args): u.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename)) else: u.warn(_("abort: %s\n") % inst.strerror) + except util.UnexpectedOutput, inst: + u.warn(_("abort: %s") % inst[0]) + if not isinstance(inst[1], basestring): + u.warn(" %r\n" % (inst[1],)) + elif not inst[1]: + u.warn(_(" empty string\n")) + else: + u.warn("\n%r%s\n" % + (inst[1][:400], len(inst[1]) > 400 and '...' or '')) except util.Abort, inst: u.warn(_("abort: %s\n") % inst) except TypeError, inst: 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 @@ -69,7 +69,7 @@ def revnavgen(pos, pagelen, limit, nodef class hgweb(object): def __init__(self, repo, name=None): if type(repo) == type(""): - self.repo = hg.repository(ui.ui(), repo) + self.repo = hg.repository(ui.ui(report_untrusted=False), repo) else: self.repo = repo @@ -77,24 +77,41 @@ class hgweb(object): self.reponame = name self.archives = 'zip', 'gz', 'bz2' self.stripecount = 1 - self.templatepath = self.repo.ui.config("web", "templates", - templater.templatepath()) + # a repo owner may set web.templates in .hg/hgrc to get any file + # readable by the user running the CGI script + self.templatepath = self.config("web", "templates", + templater.templatepath(), + untrusted=False) + + # The CGI scripts are often run by a user different from the repo owner. + # Trust the settings from the .hg/hgrc files by default. + def config(self, section, name, default=None, untrusted=True): + return self.repo.ui.config(section, name, default, + untrusted=untrusted) + + def configbool(self, section, name, default=False, untrusted=True): + return self.repo.ui.configbool(section, name, default, + untrusted=untrusted) + + def configlist(self, section, name, default=None, untrusted=True): + return self.repo.ui.configlist(section, name, default, + untrusted=untrusted) def refresh(self): mtime = get_mtime(self.repo.root) if mtime != self.mtime: self.mtime = mtime self.repo = hg.repository(self.repo.ui, self.repo.root) - self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10)) - self.stripecount = int(self.repo.ui.config("web", "stripes", 1)) - self.maxshortchanges = int(self.repo.ui.config("web", "maxshortchanges", 60)) - self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10)) - self.allowpull = self.repo.ui.configbool("web", "allowpull", True) + self.maxchanges = int(self.config("web", "maxchanges", 10)) + self.stripecount = int(self.config("web", "stripes", 1)) + self.maxshortchanges = int(self.config("web", "maxshortchanges", 60)) + self.maxfiles = int(self.config("web", "maxfiles", 10)) + self.allowpull = self.configbool("web", "allowpull", True) def archivelist(self, nodeid): - allowed = self.repo.ui.configlist("web", "allow_archive") + allowed = self.configlist("web", "allow_archive") for i, spec in self.archive_specs.iteritems(): - if i in allowed or self.repo.ui.configbool("web", "allow" + i): + if i in allowed or self.configbool("web", "allow" + i): yield {"type" : i, "extension" : spec[2], "node" : nodeid} def listfilediffs(self, files, changeset): @@ -169,7 +186,7 @@ class hgweb(object): modified, added, removed = map(lambda x: filterfiles(files, x), (modified, added, removed)) - diffopts = patch.diffopts(self.repo.ui) + diffopts = patch.diffopts(self.repo.ui, untrusted=True) for f in modified: to = r.file(f).read(mmap1[f]) tn = r.file(f).read(mmap2[f]) @@ -571,10 +588,10 @@ class hgweb(object): end = min(count, start + self.maxchanges) yield self.t("summary", - desc = self.repo.ui.config("web", "description", "unknown"), - owner = (self.repo.ui.config("ui", "username") or # preferred - self.repo.ui.config("web", "contact") or # deprecated - self.repo.ui.config("web", "author", "unknown")), # also + desc = self.config("web", "description", "unknown"), + owner = (self.config("ui", "username") or # preferred + self.config("web", "contact") or # deprecated + self.config("web", "author", "unknown")), # also lastchange = cl.read(cl.tip())[2], tags = tagentries, heads = heads, @@ -650,7 +667,7 @@ class hgweb(object): yield self.t("footer", **map) def motd(**map): - yield self.repo.ui.config("web", "motd", "") + yield self.config("web", "motd", "") def expand_form(form): shortcuts = { @@ -748,7 +765,7 @@ class hgweb(object): fields = [] if req.form.has_key('style'): style = req.form['style'][0] - if style != self.repo.ui.config('web', 'style', ''): + if style != self.config('web', 'style', ''): fields.append(('style', style)) separator = req.url[-1] == '?' and ';' or '?' @@ -761,7 +778,7 @@ class hgweb(object): expand_form(req.form) rewrite_request(req) - style = self.repo.ui.config("web", "style", "") + style = self.config("web", "style", "") if req.form.has_key('style'): style = req.form['style'][0] mapfile = style_map(self.templatepath, style) @@ -771,7 +788,7 @@ class hgweb(object): urlbase = 'http://%s%s' % (req.env['SERVER_NAME'], port) if not self.reponame: - self.reponame = (self.repo.ui.config("web", "name") + self.reponame = (self.config("web", "name") or req.env.get('REPO_NAME') or req.url.strip('/') or self.repo.root) @@ -985,9 +1002,9 @@ class hgweb(object): def do_archive(self, req): changeset = self.repo.lookup(req.form['node'][0]) type_ = req.form['type'][0] - allowed = self.repo.ui.configlist("web", "allow_archive") + allowed = self.configlist("web", "allow_archive") if (type_ in self.archives and (type_ in allowed or - self.repo.ui.configbool("web", "allow" + type_, False))): + self.configbool("web", "allow" + type_, False))): self.archive(req, changeset, type_) return @@ -995,15 +1012,17 @@ class hgweb(object): def do_static(self, req): fname = req.form['file'][0] - static = self.repo.ui.config("web", "static", - os.path.join(self.templatepath, - "static")) + # a repo owner may set web.static in .hg/hgrc to get any file + # readable by the user running the CGI script + static = self.config("web", "static", + os.path.join(self.templatepath, "static"), + untrusted=False) req.write(staticfile(static, fname, req) or self.t("error", error="%r not found" % fname)) def do_capabilities(self, req): caps = ['unbundle', 'lookup', 'changegroupsubset'] - if self.repo.ui.configbool('server', 'uncompressed'): + if self.configbool('server', 'uncompressed'): caps.append('stream=%d' % self.repo.revlogversion) resp = ' '.join(caps) req.httphdr("application/mercurial-0.1", length=len(resp)) @@ -1016,11 +1035,11 @@ class hgweb(object): user = req.env.get('REMOTE_USER') - deny = self.repo.ui.configlist('web', 'deny_' + op) + deny = self.configlist('web', 'deny_' + op) if deny and (not user or deny == ['*'] or user in deny): return False - allow = self.repo.ui.configlist('web', 'allow_' + op) + allow = self.configlist('web', 'allow_' + op) return (allow and (allow == ['*'] or user in allow)) or default def do_unbundle(self, req): @@ -1036,7 +1055,7 @@ class hgweb(object): # require ssl by default, auth info cannot be sniffed and # replayed - ssl_req = self.repo.ui.configbool('web', 'push_ssl', True) + ssl_req = self.configbool('web', 'push_ssl', True) if ssl_req: if not req.env.get('HTTPS'): bail(_('ssl required\n')) 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 @@ -87,9 +87,10 @@ class hgwebdir(object): "url": url}) def archivelist(ui, nodeid, url): - allowed = ui.configlist("web", "allow_archive") + allowed = ui.configlist("web", "allow_archive", untrusted=True) for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]: - if i[0] in allowed or ui.configbool("web", "allow" + i[0]): + if i[0] in allowed or ui.configbool("web", "allow" + i[0], + untrusted=True): yield {"type" : i[0], "extension": i[1], "node": nodeid, "url": url} @@ -109,12 +110,13 @@ class hgwebdir(object): rows = [] parity = 0 for name, path in self.repos: - u = ui.ui() + u = ui.ui(report_untrusted=False) try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) except IOError: pass - get = u.config + def get(section, name, default=None): + return u.config(section, name, default, untrusted=True) url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name]) .replace("//", "/")) + '/' diff --git a/mercurial/httprepo.py b/mercurial/httprepo.py --- a/mercurial/httprepo.py +++ b/mercurial/httprepo.py @@ -218,9 +218,15 @@ class httprepository(remoterepository): self.ui.debug(_("sending %s command\n") % cmd) q = {"cmd": cmd} q.update(args) - qs = urllib.urlencode(q) - cu = "%s?%s" % (self._url, qs) + qs = '?%s' % urllib.urlencode(q) + cu = "%s%s" % (self._url, qs) try: + if data: + if isinstance(data, file): + # urllib2 needs string or buffer when using a proxy + data.seek(0) + data = data.read() + self.ui.debug(_("sending %d bytes\n") % len(data)) resp = urllib2.urlopen(urllib2.Request(cu, data, headers)) except urllib2.HTTPError, inst: if inst.code == 401: @@ -233,6 +239,8 @@ class httprepository(remoterepository): except IndexError: # this only happens with Python 2.3, later versions raise URLError raise util.Abort(_('http error, possibly caused by proxy setting')) + # record the url we got redirected to + self._url = resp.geturl().rstrip(qs) try: proto = resp.getheader('content-type') except AttributeError: @@ -273,8 +281,7 @@ class httprepository(remoterepository): try: return map(bin, d[:-1].split(" ")) except: - self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n") - raise + raise util.UnexpectedOutput(_("unexpected response:"), d) def branches(self, nodes): n = " ".join(map(hex, nodes)) @@ -283,8 +290,7 @@ class httprepository(remoterepository): br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ] return br except: - self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n") - raise + raise util.UnexpectedOutput(_("unexpected response:"), d) def between(self, pairs): n = "\n".join(["-".join(map(hex, p)) for p in pairs]) @@ -293,8 +299,7 @@ class httprepository(remoterepository): p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ] return p except: - self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n") - raise + raise util.UnexpectedOutput(_("unexpected response:"), d) def changegroup(self, nodes, kind): n = " ".join(map(hex, nodes)) @@ -340,7 +345,7 @@ class httprepository(remoterepository): try: rfp = self.do_cmd( 'unbundle', data=fp, - headers={'content-length': length, + headers={'content-length': str(length), 'content-type': 'application/octet-stream'}, heads=' '.join(map(hex, heads))) try: diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -1795,17 +1795,32 @@ class localrepository(repo.repository): def stream_in(self, remote): fp = remote.stream_out() - resp = int(fp.readline()) + l = fp.readline() + try: + resp = int(l) + except ValueError: + raise util.UnexpectedOutput( + _('Unexpected response from remote server:'), l) if resp != 0: raise util.Abort(_('operation forbidden by server')) self.ui.status(_('streaming all changes\n')) - total_files, total_bytes = map(int, fp.readline().split(' ', 1)) + l = fp.readline() + try: + total_files, total_bytes = map(int, l.split(' ', 1)) + except ValueError, TypeError: + raise util.UnexpectedOutput( + _('Unexpected response from remote server:'), l) self.ui.status(_('%d files to transfer, %s of data\n') % (total_files, util.bytecount(total_bytes))) start = time.time() for i in xrange(total_files): - name, size = fp.readline().split('\0', 1) - size = int(size) + l = fp.readline() + try: + name, size = l.split('\0', 1) + size = int(size) + except ValueError, TypeError: + raise util.UnexpectedOutput( + _('Unexpected response from remote server:'), l) self.ui.debug('adding %s (%s)\n' % (name, util.bytecount(size))) ofp = self.sopener(name, 'w') for chunk in util.filechunkiter(fp, limit=size): diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -326,21 +326,18 @@ def patch(patchname, ui, strip=1, cwd=No return fuzz -def diffopts(ui, opts={}): +def diffopts(ui, opts={}, untrusted=False): + def get(key, name=None): + return (opts.get(key) or + ui.configbool('diff', name or key, None, untrusted=untrusted)) return mdiff.diffopts( text=opts.get('text'), - git=(opts.get('git') or - ui.configbool('diff', 'git', None)), - nodates=(opts.get('nodates') or - ui.configbool('diff', 'nodates', 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))) + git=get('git'), + nodates=get('nodates'), + showfunc=get('show_function', 'showfunc'), + ignorews=get('ignore_all_space', 'ignorews'), + ignorewsamount=get('ignore_space_change', 'ignorewsamount'), + ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines')) def updatedir(ui, repo, patches, wlock=None): '''Update dirstate after patch application according to metadata''' diff --git a/mercurial/ui.py b/mercurial/ui.py --- a/mercurial/ui.py +++ b/mercurial/ui.py @@ -26,7 +26,8 @@ def updateconfig(source, dest, sections= class ui(object): def __init__(self, verbose=False, debug=False, quiet=False, - interactive=True, traceback=False, parentui=None): + interactive=True, traceback=False, report_untrusted=True, + parentui=None): self.overlay = None self.header = [] self.prev_header = [] @@ -39,14 +40,23 @@ class ui(object): self.debugflag = debug self.interactive = interactive self.traceback = traceback + self.report_untrusted = report_untrusted + self.trusted_users = {} + self.trusted_groups = {} + # if ucdata is not None, its keys must be a superset of cdata's self.cdata = util.configparser() + self.ucdata = None self.readconfig(util.rcpath()) self.updateopts(verbose, debug, quiet, interactive) else: # parentui may point to an ui object which is already a child self.parentui = parentui.parentui or parentui self.readhooks = self.parentui.readhooks[:] + self.trusted_users = parentui.trusted_users.copy() + self.trusted_groups = parentui.trusted_groups.copy() self.cdata = dupconfig(self.parentui.cdata) + if self.parentui.ucdata: + self.ucdata = dupconfig(self.parentui.ucdata) if self.parentui.overlay: self.overlay = dupconfig(self.parentui.overlay) @@ -82,14 +92,52 @@ class ui(object): elif self.verbose and self.quiet: self.quiet = self.verbose = False + def _is_trusted(self, fp, f, warn=True): + tusers = self.trusted_users + tgroups = self.trusted_groups + if (tusers or tgroups) and '*' not in tusers and '*' not in tgroups: + st = util.fstat(fp) + user = util.username(st.st_uid) + group = util.groupname(st.st_gid) + if user not in tusers and group not in tgroups: + if warn and self.report_untrusted: + self.warn(_('Not trusting file %s from untrusted ' + 'user %s, group %s\n') % (f, user, group)) + return False + return True + def readconfig(self, fn, root=None): if isinstance(fn, basestring): fn = [fn] for f in fn: try: - self.cdata.read(f) + fp = open(f) + except IOError: + continue + cdata = self.cdata + trusted = self._is_trusted(fp, f) + if not trusted: + if self.ucdata is None: + self.ucdata = dupconfig(self.cdata) + cdata = self.ucdata + elif self.ucdata is not None: + # use a separate configparser, so that we don't accidentally + # override ucdata settings later on. + cdata = util.configparser() + + try: + cdata.readfp(fp, f) except ConfigParser.ParsingError, inst: - raise util.Abort(_("Failed to parse %s\n%s") % (f, inst)) + msg = _("Failed to parse %s\n%s") % (f, inst) + if trusted: + raise util.Abort(msg) + self.warn(_("Ignored: %s\n") % msg) + + if trusted: + if cdata != self.cdata: + updateconfig(cdata, self.cdata) + if self.ucdata is not None: + updateconfig(cdata, self.ucdata) # override data from config files with data set with ui.setconfig if self.overlay: updateconfig(self.overlay, self.cdata) @@ -103,7 +151,10 @@ class ui(object): self.readhooks.append(hook) def readsections(self, filename, *sections): - "read filename and add only the specified sections to the config data" + """Read filename and add only the specified sections to the config data + + The settings are added to the trusted config data. + """ if not sections: return @@ -119,6 +170,8 @@ class ui(object): cdata.add_section(section) updateconfig(cdata, self.cdata, sections) + if self.ucdata: + updateconfig(cdata, self.ucdata, sections) def fixconfig(self, section=None, name=None, value=None, root=None): # translate paths relative to root (or home) into absolute paths @@ -126,7 +179,7 @@ class ui(object): if root is None: root = os.getcwd() items = section and [(name, value)] or [] - for cdata in self.cdata, self.overlay: + for cdata in self.cdata, self.ucdata, self.overlay: if not cdata: continue if not items and cdata.has_section('paths'): pathsitems = cdata.items('paths') @@ -144,62 +197,111 @@ class ui(object): if name is None or name == 'interactive': self.interactive = self.configbool("ui", "interactive", True) + # update trust information + if section is None or section == 'trusted': + user = util.username() + if user is not None: + self.trusted_users[user] = 1 + for user in self.configlist('trusted', 'users'): + self.trusted_users[user] = 1 + for group in self.configlist('trusted', 'groups'): + self.trusted_groups[group] = 1 + def setconfig(self, section, name, value): if not self.overlay: self.overlay = util.configparser() - for cdata in (self.overlay, self.cdata): + for cdata in (self.overlay, self.cdata, self.ucdata): + if not cdata: continue if not cdata.has_section(section): cdata.add_section(section) cdata.set(section, name, value) self.fixconfig(section, name, value) - def _config(self, section, name, default, funcname): - if self.cdata.has_option(section, name): + def _get_cdata(self, untrusted): + if untrusted and self.ucdata: + return self.ucdata + return self.cdata + + def _config(self, section, name, default, funcname, untrusted, abort): + cdata = self._get_cdata(untrusted) + if cdata.has_option(section, name): try: - func = getattr(self.cdata, funcname) + func = getattr(cdata, funcname) return func(section, name) except ConfigParser.InterpolationError, inst: - raise util.Abort(_("Error in configuration section [%s] " - "parameter '%s':\n%s") - % (section, name, inst)) + msg = _("Error in configuration section [%s] " + "parameter '%s':\n%s") % (section, name, inst) + if abort: + raise util.Abort(msg) + self.warn(_("Ignored: %s\n") % msg) return default - def config(self, section, name, default=None): - return self._config(section, name, default, 'get') + def _configcommon(self, section, name, default, funcname, untrusted): + value = self._config(section, name, default, funcname, + untrusted, abort=True) + if self.debugflag and not untrusted and self.ucdata: + uvalue = self._config(section, name, None, funcname, + untrusted=True, abort=False) + if uvalue is not None and uvalue != value: + self.warn(_("Ignoring untrusted configuration option " + "%s.%s = %s\n") % (section, name, uvalue)) + return value - def configbool(self, section, name, default=False): - return self._config(section, name, default, 'getboolean') + def config(self, section, name, default=None, untrusted=False): + return self._configcommon(section, name, default, 'get', untrusted) - def configlist(self, section, name, default=None): + def configbool(self, section, name, default=False, untrusted=False): + return self._configcommon(section, name, default, 'getboolean', + untrusted) + + def configlist(self, section, name, default=None, untrusted=False): """Return a list of comma/space separated strings""" - result = self.config(section, name) + result = self.config(section, name, untrusted=untrusted) if result is None: result = default or [] if isinstance(result, basestring): result = result.replace(",", " ").split() return result - def has_config(self, section): + def has_config(self, section, untrusted=False): '''tell whether section exists in config.''' - return self.cdata.has_section(section) + cdata = self._get_cdata(untrusted) + return cdata.has_section(section) - def configitems(self, section): + def _configitems(self, section, untrusted, abort): items = {} - if self.cdata.has_section(section): + cdata = self._get_cdata(untrusted) + if cdata.has_section(section): try: - items.update(dict(self.cdata.items(section))) + items.update(dict(cdata.items(section))) except ConfigParser.InterpolationError, inst: - raise util.Abort(_("Error in configuration section [%s]:\n%s") - % (section, inst)) + msg = _("Error in configuration section [%s]:\n" + "%s") % (section, inst) + if abort: + raise util.Abort(msg) + self.warn(_("Ignored: %s\n") % msg) + return items + + def configitems(self, section, untrusted=False): + items = self._configitems(section, untrusted=untrusted, abort=True) + if self.debugflag and not untrusted and self.ucdata: + uitems = self._configitems(section, untrusted=True, abort=False) + keys = uitems.keys() + keys.sort() + for k in keys: + if uitems[k] != items.get(k): + self.warn(_("Ignoring untrusted configuration option " + "%s.%s = %s\n") % (section, k, uitems[k])) x = items.items() x.sort() return x - def walkconfig(self): - sections = self.cdata.sections() + def walkconfig(self, untrusted=False): + cdata = self._get_cdata(untrusted) + sections = cdata.sections() sections.sort() for section in sections: - for name, value in self.configitems(section): + for name, value in self.configitems(section, untrusted): yield section, name, value.replace('\n', '\\n') def extensions(self): diff --git a/mercurial/util.py b/mercurial/util.py --- a/mercurial/util.py +++ b/mercurial/util.py @@ -138,6 +138,9 @@ def unique(g): class Abort(Exception): """Raised if a command needs to print an error and exit.""" +class UnexpectedOutput(Abort): + """Raised to print an error with part of output and exit.""" + def always(fn): return True def never(fn): return False @@ -521,6 +524,36 @@ def is_win_9x(): except AttributeError: return os.name == 'nt' and 'command' in os.environ.get('comspec', '') +def username(uid=None): + """Return the name of the user with the given uid. + + If uid is None, return the name of the current user.""" + try: + import pwd + if uid is None: + uid = os.getuid() + try: + return pwd.getpwuid(uid)[0] + except KeyError: + return str(uid) + except ImportError: + return None + +def groupname(gid=None): + """Return the name of the group with the given gid. + + If gid is None, return the name of the current group.""" + try: + import grp + if gid is None: + gid = os.getgid() + try: + return grp.getgrgid(gid)[0] + except KeyError: + return str(gid) + except ImportError: + return None + # Platform specific variants if os.name == 'nt': demandload(globals(), "msvcrt") diff --git a/tests/test-trusted.py b/tests/test-trusted.py new file mode 100644 --- /dev/null +++ b/tests/test-trusted.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +# Since it's not easy to write a test that portably deals +# with files from different users/groups, we cheat a bit by +# monkey-patching some functions in the util module + +import os +from mercurial import ui, util + +hgrc = os.environ['HGRCPATH'] + +def testui(user='foo', group='bar', tusers=(), tgroups=(), + cuser='foo', cgroup='bar', debug=False, silent=False): + # user, group => owners of the file + # tusers, tgroups => trusted users/groups + # cuser, cgroup => user/group of the current process + + # write a global hgrc with the list of trusted users/groups and + # some setting so that we can be sure it was read + f = open(hgrc, 'w') + f.write('[paths]\n') + f.write('global = /some/path\n\n') + + if tusers or tgroups: + f.write('[trusted]\n') + if tusers: + f.write('users = %s\n' % ', '.join(tusers)) + if tgroups: + f.write('groups = %s\n' % ', '.join(tgroups)) + f.close() + + # override the functions that give names to uids and gids + def username(uid=None): + if uid is None: + return cuser + return user + util.username = username + + def groupname(gid=None): + if gid is None: + return 'bar' + return group + util.groupname = groupname + + # try to read everything + #print '# File belongs to user %s, group %s' % (user, group) + #print '# trusted users = %s; trusted groups = %s' % (tusers, tgroups) + kind = ('different', 'same') + who = ('', 'user', 'group', 'user and the group') + trusted = who[(user in tusers) + 2*(group in tgroups)] + if trusted: + trusted = ', but we trust the ' + trusted + print '# %s user, %s group%s' % (kind[user == cuser], kind[group == cgroup], + trusted) + + parentui = ui.ui() + parentui.updateopts(debug=debug) + u = ui.ui(parentui=parentui) + u.readconfig('.hg/hgrc') + if silent: + return u + print 'trusted' + for name, path in u.configitems('paths'): + print ' ', name, '=', path + print 'untrusted' + for name, path in u.configitems('paths', untrusted=True): + print '.', + u.config('paths', name) # warning with debug=True + print '.', + u.config('paths', name, untrusted=True) # no warnings + print name, '=', path + print + + return u + +os.mkdir('repo') +os.chdir('repo') +os.mkdir('.hg') +f = open('.hg/hgrc', 'w') +f.write('[paths]\n') +f.write('local = /another/path\n\n') +f.write('interpolated = %(global)s%(local)s\n\n') +f.close() + +#print '# Everything is run by user foo, group bar\n' + +# same user, same group +testui() +# same user, different group +testui(group='def') +# different user, same group +testui(user='abc') +# ... but we trust the group +testui(user='abc', tgroups=['bar']) +# different user, different group +testui(user='abc', group='def') +# ... but we trust the user +testui(user='abc', group='def', tusers=['abc']) +# ... but we trust the group +testui(user='abc', group='def', tgroups=['def']) +# ... but we trust the user and the group +testui(user='abc', group='def', tusers=['abc'], tgroups=['def']) +# ... but we trust all users +print '# we trust all users' +testui(user='abc', group='def', tusers=['*']) +# ... but we trust all groups +print '# we trust all groups' +testui(user='abc', group='def', tgroups=['*']) +# ... but we trust the whole universe +print '# we trust all users and groups' +testui(user='abc', group='def', tusers=['*'], tgroups=['*']) +# ... check that users and groups are in different namespaces +print "# we don't get confused by users and groups with the same name" +testui(user='abc', group='def', tusers=['def'], tgroups=['abc']) +# ... lists of user names work +print "# list of user names" +testui(user='abc', group='def', tusers=['foo', 'xyz', 'abc', 'bleh'], + tgroups=['bar', 'baz', 'qux']) +# ... lists of group names work +print "# list of group names" +testui(user='abc', group='def', tusers=['foo', 'xyz', 'bleh'], + tgroups=['bar', 'def', 'baz', 'qux']) + +print "# Can't figure out the name of the user running this process" +testui(user='abc', group='def', cuser=None) + +print "# prints debug warnings" +u = testui(user='abc', group='def', cuser='foo', debug=True) + +print "# ui.readsections" +filename = 'foobar' +f = open(filename, 'w') +f.write('[foobar]\n') +f.write('baz = quux\n') +f.close() +u.readsections(filename, 'foobar') +print u.config('foobar', 'baz') + +print +print "# read trusted, untrusted, new ui, trusted" +u = ui.ui() +u.updateopts(debug=True) +u.readconfig(filename) +u2 = ui.ui(parentui=u) +def username(uid=None): + return 'foo' +util.username = username +u2.readconfig('.hg/hgrc') +print 'trusted:' +print u2.config('foobar', 'baz') +print u2.config('paths', 'interpolated') +print 'untrusted:' +print u2.config('foobar', 'baz', untrusted=True) +print u2.config('paths', 'interpolated', untrusted=True) + +print +print "# error handling" + +def assertraises(f, exc=util.Abort): + try: + f() + except exc, inst: + print 'raised', inst.__class__.__name__ + else: + print 'no exception?!' + +print "# file doesn't exist" +os.unlink('.hg/hgrc') +assert not os.path.exists('.hg/hgrc') +testui(debug=True, silent=True) +testui(user='abc', group='def', debug=True, silent=True) + +print +print "# parse error" +f = open('.hg/hgrc', 'w') +f.write('foo = bar') +f.close() +testui(user='abc', group='def', silent=True) +assertraises(lambda: testui(debug=True, silent=True)) + +print +print "# interpolation error" +f = open('.hg/hgrc', 'w') +f.write('[foo]\n') +f.write('bar = %(') +f.close() +u = testui(debug=True, silent=True) +print '# regular config:' +print ' trusted', +assertraises(lambda: u.config('foo', 'bar')) +print 'untrusted', +assertraises(lambda: u.config('foo', 'bar', untrusted=True)) + +u = testui(user='abc', group='def', debug=True, silent=True) +print ' trusted ', +print u.config('foo', 'bar') +print 'untrusted', +assertraises(lambda: u.config('foo', 'bar', untrusted=True)) + +print '# configitems:' +print ' trusted ', +print u.configitems('foo') +print 'untrusted', +assertraises(lambda: u.configitems('foo', untrusted=True)) + diff --git a/tests/test-trusted.py.out b/tests/test-trusted.py.out new file mode 100644 --- /dev/null +++ b/tests/test-trusted.py.out @@ -0,0 +1,212 @@ +# same user, same group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# same user, different group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# different user, same group +Not trusting file .hg/hgrc from untrusted user abc, group bar +trusted + global = /some/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# different user, same group, but we trust the group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# different user, different group +Not trusting file .hg/hgrc from untrusted user abc, group def +trusted + global = /some/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# different user, different group, but we trust the user +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# different user, different group, but we trust the group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# different user, different group, but we trust the user and the group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# we trust all users +# different user, different group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# we trust all groups +# different user, different group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# we trust all users and groups +# different user, different group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# we don't get confused by users and groups with the same name +# different user, different group +Not trusting file .hg/hgrc from untrusted user abc, group def +trusted + global = /some/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# list of user names +# different user, different group, but we trust the user +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# list of group names +# different user, different group, but we trust the group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# Can't figure out the name of the user running this process +# different user, different group +trusted + global = /some/path + interpolated = /some/path/another/path + local = /another/path +untrusted +. . global = /some/path +. . interpolated = /some/path/another/path +. . local = /another/path + +# prints debug warnings +# different user, different group +Not trusting file .hg/hgrc from untrusted user abc, group def +trusted +Ignoring untrusted configuration option paths.interpolated = /some/path/another/path +Ignoring untrusted configuration option paths.local = /another/path + global = /some/path +untrusted +. . global = /some/path +.Ignoring untrusted configuration option paths.interpolated = /some/path/another/path + . interpolated = /some/path/another/path +.Ignoring untrusted configuration option paths.local = /another/path + . local = /another/path + +# ui.readsections +quux + +# read trusted, untrusted, new ui, trusted +Not trusting file foobar from untrusted user abc, group def +trusted: +Ignoring untrusted configuration option foobar.baz = quux +None +/some/path/another/path +untrusted: +quux +/some/path/another/path + +# error handling +# file doesn't exist +# same user, same group +# different user, different group + +# parse error +# different user, different group +Not trusting file .hg/hgrc from untrusted user abc, group def +Ignored: Failed to parse .hg/hgrc +File contains no section headers. +file: .hg/hgrc, line: 1 +'foo = bar' +# same user, same group +raised Abort + +# interpolation error +# same user, same group +# regular config: + trusted raised Abort +untrusted raised Abort +# different user, different group +Not trusting file .hg/hgrc from untrusted user abc, group def + trusted Ignored: Error in configuration section [foo] parameter 'bar': +bad interpolation variable reference '%(' + None +untrusted raised Abort +# configitems: + trusted Ignored: Error in configuration section [foo]: +bad interpolation variable reference '%(' + [] +untrusted raised Abort