# HG changeset patch # User Bryan O'Sullivan # Date 1191124536 25200 # Node ID c8d6f8510bf4685ce415aa14d1998c89abe9d68c # Parent f87685355c9c168eabeaacf74ef4f9e7c4a1100d# Parent bd706eb8bc258f6fffbfa68337b0ea4588f19064 Merge with crew. diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -4,6 +4,7 @@ syntax: glob *.orig *.rej *~ +*.mergebackup *.o *.so *.pyc diff --git a/contrib/buildrpm b/contrib/buildrpm --- a/contrib/buildrpm +++ b/contrib/buildrpm @@ -29,7 +29,7 @@ tmpspec=/tmp/`basename "$specfile"`.$$ version=`hg tags | perl -e 'while(){if(/^(\d\S+)/){print$1;exit}}'` # Compute the release number as the difference in revision numbers # between the tip and the most recent tag. -release=`hg tags | perl -e 'while(){/^(\S+)\s+(\d+)/;if($1eq"tip"){$t=$2}else{print$t-$2+1;exit}}'` +release=`hg tags | perl -e 'while(){($tag,$id)=/^(\S+)\s+(\d+)/;if($tag eq "tip"){$tip = $id}elsif($tag=~/^\d/){print $tip-$id+1;exit}}'` tip=`hg -q tip` # Beat up the spec file @@ -40,6 +40,19 @@ sed -e 's,^Source:.*,Source: /dev/null,' -e 's,^%setup.*,,' \ $specfile > $tmpspec +cat <> $tmpspec +%changelog +* `date +'%a %b %d %Y'` `hg showconfig ui.username` $version-$release +- Automatically built via $0 + +EOF +hg log \ + --template '* {date|rfc822date} {author}\n- {desc|firstline}\n\n' \ + .hgtags \ + | sed -e 's/^\(\* [MTWFS][a-z][a-z]\), \([0-3][0-9]\) \([A-Z][a-z][a-z]\) /\1 \3 \2 /' \ + -e '/^\* [MTWFS][a-z][a-z] /{s/ [012][0-9]:[0-9][0-9]:[0-9][0-9] [+-][0-9]\{4\}//}' \ + >> $tmpspec + rpmbuild --define "_topdir $rpmdir" -bb $tmpspec if [ $? = 0 ]; then rm -rf $tmpspec $rpmdir/BUILD diff --git a/contrib/churn.py b/contrib/churn.py --- a/contrib/churn.py +++ b/contrib/churn.py @@ -11,9 +11,34 @@ # # -import sys from mercurial.i18n import gettext as _ from mercurial import hg, mdiff, cmdutil, ui, util, templater, node +import os, sys + +def get_tty_width(): + if 'COLUMNS' in os.environ: + try: + return int(os.environ['COLUMNS']) + except ValueError: + pass + try: + import termios, fcntl, struct + buf = 'abcd' + for dev in (sys.stdout, sys.stdin): + try: + if buf != 'abcd': + break + fd = dev.fileno() + if not os.isatty(fd): + continue + buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf) + except ValueError: + pass + if buf != 'abcd': + return struct.unpack('hh', buf)[1] + except ImportError: + pass + return 80 def __gather(ui, repo, node1, node2): def dirtywork(f, mmap1, mmap2): @@ -159,8 +184,9 @@ def churn(ui, repo, **opts): maximum = ordered[0][1] - ui.note("Assuming 80 character terminal\n") - width = 80 - 1 + width = get_tty_width() + ui.note(_("assuming %i character terminal\n") % width) + width -= 1 for i in ordered: person = i[0] diff --git a/contrib/hg-ssh b/contrib/hg-ssh --- a/contrib/hg-ssh +++ b/contrib/hg-ssh @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright 2005, 2006 by Intevation GmbH +# Copyright 2005-2007 by Intevation GmbH # Author(s): # Thomas Arendsen Hein # @@ -25,7 +25,10 @@ You can use pattern matching of your nor command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}" """ -from mercurial import commands +# enable importing on demand to reduce startup time +from mercurial import demandimport; demandimport.enable() + +from mercurial import dispatch import sys, os @@ -38,7 +41,7 @@ if orig_cmd.startswith('hg -R ') and ori path = orig_cmd[6:-14] repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path))) if repo in allowed_paths: - commands.dispatch(['-R', repo, 'serve', '--stdio']) + dispatch.dispatch(['-R', repo, 'serve', '--stdio']) else: sys.stderr.write("Illegal repository %r\n" % repo) sys.exit(-1) diff --git a/contrib/hgk b/contrib/hgk --- a/contrib/hgk +++ b/contrib/hgk @@ -5,6 +5,74 @@ # and distributed under the terms of the GNU General Public Licence, # either version 2, or (at your option) any later version. + +# Modified version of Tip 171: +# http://www.tcl.tk/cgi-bin/tct/tip/171.html +# +# The in_mousewheel global was added to fix strange reentrancy issues. +# The whole snipped is activated only under windows, mouse wheel +# bindings working already under MacOSX and Linux. + +if {[tk windowingsystem] eq "win32"} { + +set mw_classes [list Text Listbox Table TreeCtrl] + foreach class $mw_classes { bind $class {} } + +set in_mousewheel 0 + +proc ::tk::MouseWheel {wFired X Y D {shifted 0}} { + global in_mousewheel + if { $in_mousewheel != 0 } { return } + # Set event to check based on call + set evt "<[expr {$shifted?{Shift-}:{}}]MouseWheel>" + # do not double-fire in case the class already has a binding + if {[bind [winfo class $wFired] $evt] ne ""} { return } + # obtain the window the mouse is over + set w [winfo containing $X $Y] + # if we are outside the app, try and scroll the focus widget + if {![winfo exists $w]} { catch {set w [focus]} } + if {[winfo exists $w]} { + + if {[bind $w $evt] ne ""} { + # Awkward ... this widget has a MouseWheel binding, but to + # trigger successfully in it, we must give it focus. + catch {focus} old + if {$w ne $old} { focus $w } + set in_mousewheel 1 + event generate $w $evt -rootx $X -rooty $Y -delta $D + set in_mousewheel 0 + if {$w ne $old} { focus $old } + return + } + + # aqua and x11/win32 have different delta handling + if {[tk windowingsystem] ne "aqua"} { + set delta [expr {- ($D / 30)}] + } else { + set delta [expr {- ($D)}] + } + # scrollbars have different call conventions + if {[string match "*Scrollbar" [winfo class $w]]} { + catch {tk::ScrollByUnits $w \ + [string index [$w cget -orient] 0] $delta} + } else { + set cmd [list $w [expr {$shifted ? "xview" : "yview"}] \ + scroll $delta units] + # Walking up to find the proper widget (handles cases like + # embedded widgets in a canvas) + while {[catch $cmd] && [winfo toplevel $w] ne $w} { + set w [winfo parent $w] + } + } + } +} + +bind all [list ::tk::MouseWheel %W %X %Y %D 0] + +# end of win32 section +} + + proc gitdir {} { global env if {[info exists env(GIT_DIR)]} { @@ -299,6 +367,11 @@ proc readotherrefs {base dname excl} { } } +proc allcansmousewheel {delta} { + set delta [expr -5*(int($delta)/abs($delta))] + allcanvs yview scroll $delta units +} + proc error_popup msg { set w .error toplevel $w @@ -470,6 +543,7 @@ proc makewindow {} { bindall <1> {selcanvline %W %x %y} #bindall {selcanvline %W %x %y} + bindall "allcansmousewheel %D" bindall "allcanvs yview scroll -5 units" bindall "allcanvs yview scroll 5 units" bindall <2> "allcanvs scan mark 0 %y" @@ -3681,4 +3755,6 @@ set patchnum 0 setcoords makewindow readrefs +set hgroot [exec $env(HG) root] +wm title . "hgk $hgroot" getcommits $revtreeargs diff --git a/contrib/hgwebdir.fcgi b/contrib/hgwebdir.fcgi --- a/contrib/hgwebdir.fcgi +++ b/contrib/hgwebdir.fcgi @@ -2,14 +2,17 @@ # # An example CGI script to export multiple hgweb repos, edit as necessary +# adjust python path if not a system-wide install: +#import sys +#sys.path.insert(0, "/path/to/python/lib") + +# enable demandloading to reduce startup time +from mercurial import demandimport; demandimport.enable() + # send python tracebacks to the browser if an error occurs: import cgitb cgitb.enable() -# adjust python path if not a system-wide install: -#import sys -#sys.path.insert(0, "/path/to/python/lib") - # If you'd like to serve pages with UTF-8 instead of your default # locale charset, you can do so by uncommenting the following lines. # Note that this will cause your .hgrc files to be interpreted in diff --git a/contrib/macosx/Readme.html b/contrib/macosx/Readme.html --- a/contrib/macosx/Readme.html +++ b/contrib/macosx/Readme.html @@ -19,10 +19,14 @@


This is not a stand-alone version of Mercurial.


-

To use it, you must have the Universal MacPython 2.4.3 from www.python.org installed.

+

To use it, you must have the appropriate version of Universal MacPython from www.python.org installed.


-

You can download MacPython 2.4.3 from here:

-

http://www.python.org/ftp/python/2.4.3/Universal-MacPython-2.4.3-2006-04-07.dmg

+

You can find more information and download MacPython from here:

+

http://www.python.org/download

+


+

Or direct links to the latest version are:

+

Python 2.5.1 for Macintosh OS X

+

Python 2.4.4 for Macintosh OS X


After you install


diff --git a/contrib/mercurial.el b/contrib/mercurial.el --- a/contrib/mercurial.el +++ b/contrib/mercurial.el @@ -1261,9 +1261,22 @@ Names are displayed relative to the repo (interactive) (error "not implemented")) -(defun hg-version-other-window () - (interactive) - (error "not implemented")) +(defun hg-version-other-window (rev) + "Visit version REV of the current file in another window. +If the current file is named `F', the version is named `F.~REV~'. +If `F.~REV~' already exists, use it instead of checking it out again." + (interactive "sVersion to visit (default is workfile version): ") + (let* ((file buffer-file-name) + (version (if (string-equal rev "") + "tip" + rev)) + (automatic-backup (vc-version-backup-file-name file version)) + (manual-backup (vc-version-backup-file-name file version 'manual))) + (unless (file-exists-p manual-backup) + (if (file-exists-p automatic-backup) + (rename-file automatic-backup manual-backup nil) + (hg-run0 "-q" "cat" "-r" version "-o" manual-backup file))) + (find-file-other-window manual-backup))) (provide 'mercurial) diff --git a/contrib/mercurial.spec b/contrib/mercurial.spec old mode 100644 new mode 100755 --- a/contrib/mercurial.spec +++ b/contrib/mercurial.spec @@ -8,6 +8,17 @@ Source: http://www.selenic.com/mercurial URL: http://www.selenic.com/mercurial BuildRoot: /tmp/build.%{name}-%{version}-%{release} +# From the README: +# +# Note: some distributions fails to include bits of distutils by +# default, you'll need python-dev to install. You'll also need a C +# compiler and a 3-way merge tool like merge, tkdiff, or kdiff3. +# +# python-devel provides an adequate python-dev. The merge tool is a +# run-time dependency. +# +BuildRequires: python >= 2.3, python-devel, make, gcc, asciidoc, xmlto + %define pythonver %(python -c 'import sys;print ".".join(map(str, sys.version_info[:2]))') %define pythonlib %{_libdir}/python%{pythonver}/site-packages/%{name} %define hgext %{_libdir}/python%{pythonver}/site-packages/hgext @@ -21,23 +32,51 @@ rm -rf $RPM_BUILD_ROOT %setup -q %build -python setup.py build +make all %install -python setup.py install --root $RPM_BUILD_ROOT +python setup.py install --root $RPM_BUILD_ROOT --prefix %{_prefix} +make install-doc DESTDIR=$RPM_BUILD_ROOT MANDIR=%{_mandir} + +install contrib/hgk $RPM_BUILD_ROOT%{_bindir} +install contrib/convert-repo $RPM_BUILD_ROOT%{_bindir}/mercurial-convert-repo +install contrib/hg-ssh $RPM_BUILD_ROOT%{_bindir} +install contrib/git-viz/{hg-viz,git-rev-tree} $RPM_BUILD_ROOT%{_bindir} + +bash_completion_dir=$RPM_BUILD_ROOT%{_sysconfdir}/bash_completion.d +mkdir -p $bash_completion_dir +install contrib/bash_completion $bash_completion_dir/mercurial.sh + +zsh_completion_dir=$RPM_BUILD_ROOT%{_datadir}/zsh/site-functions +mkdir -p $zsh_completion_dir +install contrib/zsh_completion $zsh_completion_dir/_mercurial + +lisp_dir=$RPM_BUILD_ROOT%{_datadir}/emacs/site-lisp +mkdir -p $lisp_dir +install contrib/mercurial.el $lisp_dir %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-,root,root,-) -%doc doc/* *.cgi +%doc CONTRIBUTORS COPYING doc/README doc/hg*.txt doc/hg*.html doc/ja *.cgi +%{_mandir}/man?/hg*.gz %dir %{pythonlib} %dir %{hgext} +%{_sysconfdir}/bash_completion.d/mercurial.sh +%{_datadir}/zsh/site-functions/_mercurial +%{_datadir}/emacs/site-lisp/mercurial.el +%{_bindir}/hg +%{_bindir}/hgk %{_bindir}/hgmerge -%{_bindir}/hg +%{_bindir}/hg-ssh +%{_bindir}/hg-viz +%{_bindir}/git-rev-tree +%{_bindir}/mercurial-convert-repo %{pythonlib}/templates %{pythonlib}/*.py* %{pythonlib}/hgweb/*.py* %{pythonlib}/*.so %{hgext}/*.py* +%{hgext}/convert/*.py* diff --git a/contrib/win32/mercurial.ini b/contrib/win32/mercurial.ini --- a/contrib/win32/mercurial.ini +++ b/contrib/win32/mercurial.ini @@ -1,41 +1,41 @@ -; System-wide Mercurial config file. To override these settings on a -; per-user basis, please edit the following file instead, where -; USERNAME is your Windows user name: -; C:\Documents and Settings\USERNAME\Mercurial.ini - -[ui] -editor = notepad - -; By default, we try to encode and decode all files that do not -; contain ASCII NUL characters. What this means is that we try to set -; line endings to Windows style on update, and to Unix style on -; commit. This lets us cooperate with Linux and Unix users, so -; everybody sees files with their native line endings. - -[extensions] -; The win32text extension is available and installed by default. It -; provides built-in Python hooks to perform line ending conversions. -; This is normally much faster than running an external program. -hgext.win32text = - - -[encode] -; Encode files that don't contain NUL characters. - -; ** = cleverencode: - -; Alternatively, you can explicitly specify each file extension that -; you want encoded (any you omit will be left untouched), like this: - -; *.txt = dumbencode: - - -[decode] -; Decode files that don't contain NUL characters. - -; ** = cleverdecode: - -; Alternatively, you can explicitly specify each file extension that -; you want decoded (any you omit will be left untouched), like this: - -; **.txt = dumbdecode: +; System-wide Mercurial config file. To override these settings on a +; per-user basis, please edit the following file instead, where +; USERNAME is your Windows user name: +; C:\Documents and Settings\USERNAME\Mercurial.ini + +[ui] +editor = notepad + +; By default, we try to encode and decode all files that do not +; contain ASCII NUL characters. What this means is that we try to set +; line endings to Windows style on update, and to Unix style on +; commit. This lets us cooperate with Linux and Unix users, so +; everybody sees files with their native line endings. + +[extensions] +; The win32text extension is available and installed by default. It +; provides built-in Python hooks to perform line ending conversions. +; This is normally much faster than running an external program. +hgext.win32text = + + +[encode] +; Encode files that don't contain NUL characters. + +; ** = cleverencode: + +; Alternatively, you can explicitly specify each file extension that +; you want encoded (any you omit will be left untouched), like this: + +; *.txt = dumbencode: + + +[decode] +; Decode files that don't contain NUL characters. + +; ** = cleverdecode: + +; Alternatively, you can explicitly specify each file extension that +; you want decoded (any you omit will be left untouched), like this: + +; **.txt = dumbdecode: diff --git a/contrib/zsh_completion b/contrib/zsh_completion --- a/contrib/zsh_completion +++ b/contrib/zsh_completion @@ -200,6 +200,13 @@ typeset -A _hg_cmd_globals _wanted files expl 'modified files' _multi_parts / status_files } +_hg_config() { + typeset -a items + local line + items=(${${(%f)"$(_hg_cmd showconfig)"}%%\=*}) + (( $#items )) && _describe -t config 'config item' items +} + _hg_addremove() { _alternative 'files:unknown files:_hg_unknown' \ 'files:missing files:_hg_missing' @@ -352,6 +359,17 @@ typeset -A _hg_cmd_globals '*:destination:_files' } +_hg_cmd_backout() { + _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ + '--merge[merge with old dirstate parent after backout]' \ + '(--date -d)'{-d+,--date}'[record datecode as commit date]:date code:' \ + '--parent[parent to choose when backing out merge]' \ + '(--user -u)'{-u+,--user}'[record user as commiter]:user:' \ + '(--rev -r)'{-r+,--rev}'[revision]:revision:_hg_tags' \ + '(--message -m)'{-m+,--message}'[use as commit message]:text:' \ + '(--logfile -l)'{-l+,--logfile}'[read commit message from ]:log file:_files -g \*.txt' +} + _hg_cmd_bundle() { _arguments -s -w : $_hg_global_opts $_hg_remote_opts \ '(--force -f)'{-f,--force}'[run even when remote repository is unrelated]' \ @@ -431,7 +449,8 @@ typeset -A _hg_cmd_globals '(--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:_hg_files' + '1:search pattern:' \ + '*:files:_hg_files' } _hg_cmd_heads() { @@ -444,6 +463,15 @@ typeset -A _hg_cmd_globals '*:mercurial command:_hg_commands' } +_hg_cmd_identify() { + _arguments -s -w : $_hg_global_opts \ + '(--rev -r)'{-r+,--rev}'[identify the specified rev]:revision:_hg_tags' \ + '(--num -n)'{-n+,--num}'[show local revision number]' \ + '(--id -i)'{-i+,--id}'[show global revision id]' \ + '(--branch -b)'{-b+,--branch}'[show branch]' \ + '(--tags -t)'{-t+,--tags}'[show tags]' +} + _hg_cmd_import() { _arguments -s -w : $_hg_global_opts \ '(--strip -p)'{-p+,--strip}'[directory strip option for patch (default: 1)]:count:' \ @@ -457,7 +485,7 @@ typeset -A _hg_cmd_globals '(--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]' \ + '(--rev -r)'{-r+,--rev}'[a specific revision up to which you would like to pull]:revision:_hg_tags' \ '(--newest-first -n)'{-n,--newest-first}'[show newest record first]' \ '--bundle[file to store the bundles into]:bundle file:_files' \ ':source:_hg_remote' @@ -509,7 +537,7 @@ typeset -A _hg_cmd_globals _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' + ':last modified file:_hg_files' } _hg_cmd_paths() { @@ -521,13 +549,14 @@ typeset -A _hg_cmd_globals _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]' \ + '(--rev -r)'{-r+,--rev}'[a specific revision up to which you would like to pull]:revision:' \ ':source:_hg_remote' } _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]' \ + '(--rev -r)'{-r+,--rev}'[a specific revision you would like to push]:revision:_hg_tags' \ ':destination:_hg_remote' } @@ -579,6 +608,12 @@ typeset -A _hg_cmd_globals '(--ipv6 -6)'{-6,--ipv6}'[use IPv6 in addition to IPv4]' } +_hg_cmd_showconfig() { + _arguments -s -w : $_hg_global_opts \ + '(--untrusted -u)'{-u+,--untrusted}'[show untrusted configuration options]' \ + ':config item:_hg_config' +} + _hg_cmd_status() { _arguments -s -w : $_hg_global_opts $_hg_pat_opts \ '(--all -A)'{-A,--all}'[show status of all files]' \ @@ -620,9 +655,15 @@ typeset -A _hg_cmd_globals _hg_cmd_update() { _arguments -s -w : $_hg_global_opts \ '(--clean -C)'{-C,--clean}'[overwrite locally modified files]' \ + '(--rev -r)'{-r+,--rev}'[revision]:revision:_hg_tags' \ ':revision:_hg_tags' } +# bisect extension +_hg_cmd_bisect() { + _arguments -s -w : $_hg_global_opts ':evaluation:(help init reset next good bad)' +} + # HGK _hg_cmd_view() { _arguments -s -w : $_hg_global_opts \ diff --git a/doc/gendoc.py b/doc/gendoc.py --- a/doc/gendoc.py +++ b/doc/gendoc.py @@ -1,6 +1,7 @@ import sys, textwrap # import from the live mercurial repo sys.path.insert(0, "..") +from mercurial import demandimport; demandimport.enable() from mercurial.commands import table, globalopts from mercurial.i18n import gettext as _ from mercurial.help import helptable diff --git a/doc/hg.1.txt b/doc/hg.1.txt --- a/doc/hg.1.txt +++ b/doc/hg.1.txt @@ -62,6 +62,14 @@ SPECIFYING SINGLE REVISIONS The reserved name "tip" is a special tag that always identifies the most recent revision. + The reserved name "null" indicates the null revision. This is the + revision of an empty repository, and the parent of revision 0. + + The reserved name "." indicates the working directory parent. If + no working directory is checked out, it is equivalent to null. + If an uncommitted merge is in progress, "." is the revision of + the first parent. + SPECIFYING MULTIPLE REVISIONS ----------------------------- diff --git a/hg b/hg --- a/hg +++ b/hg @@ -7,5 +7,8 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -import mercurial.commands -mercurial.commands.run() +# enable importing on demand to reduce startup time +from mercurial import demandimport; demandimport.enable() + +import mercurial.dispatch +mercurial.dispatch.run() diff --git a/hgext/alias.py b/hgext/alias.py new file mode 100644 --- /dev/null +++ b/hgext/alias.py @@ -0,0 +1,76 @@ +# Copyright (C) 2007 Brendan Cully +# This file is published under the GNU GPL. + +'''allow user-defined command aliases + +To use, create entries in your hgrc of the form + +[alias] +mycmd = cmd --args +''' + +from mercurial.cmdutil import findcmd, UnknownCommand, AmbiguousCommand +from mercurial import commands + +cmdtable = {} + +class RecursiveCommand(Exception): pass + +class lazycommand(object): + '''defer command lookup until needed, so that extensions loaded + after alias can be aliased''' + def __init__(self, ui, name, target): + self._ui = ui + self._name = name + self._target = target + self._cmd = None + + def __len__(self): + self._resolve() + return len(self._cmd) + + def __getitem__(self, key): + self._resolve() + return self._cmd[key] + + def __iter__(self): + self._resolve() + return self._cmd.__iter__() + + def _resolve(self): + if self._cmd is not None: + return + + try: + self._cmd = findcmd(self._ui, self._target, commands.table)[1] + if self._cmd == self: + raise RecursiveCommand() + if self._target in commands.norepo.split(' '): + commands.norepo += ' %s' % self._name + return + except UnknownCommand: + msg = '*** [alias] %s: command %s is unknown' % \ + (self._name, self._target) + except AmbiguousCommand: + msg = '*** [alias] %s: command %s is ambiguous' % \ + (self._name, self._target) + except RecursiveCommand: + msg = '*** [alias] %s: circular dependency on %s' % \ + (self._name, self._target) + def nocmd(*args, **opts): + self._ui.warn(msg + '\n') + return 1 + nocmd.__doc__ = msg + self._cmd = (nocmd, [], '') + commands.norepo += ' %s' % self._name + +def uisetup(ui): + for cmd, target in ui.configitems('alias'): + if not target: + ui.warn('*** [alias] %s: no definition\n' % cmd) + continue + args = target.split(' ') + tcmd = args.pop(0) + if args: + ui.setconfig('defaults', cmd, ' '.join(args)) + cmdtable[cmd] = lazycommand(ui, cmd, tcmd) diff --git a/hgext/children.py b/hgext/children.py new file mode 100644 --- /dev/null +++ b/hgext/children.py @@ -0,0 +1,41 @@ +# Mercurial extension to provide the 'hg children' command +# +# Copyright 2007 by Intevation GmbH +# Author(s): +# Thomas Arendsen Hein +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from mercurial import cmdutil +from mercurial.i18n import _ + + +def children(ui, repo, file_=None, **opts): + """show the children of the given or working dir revision + + Print the children of the working directory's revisions. + If a revision is given via --rev, the children of that revision + will be printed. If a file argument is given, revision in + which the file was last changed (after the working directory + revision or the argument to --rev if given) is printed. + """ + rev = opts.get('rev') + if file_: + ctx = repo.filectx(file_, changeid=rev) + else: + ctx = repo.changectx(rev) + + displayer = cmdutil.show_changeset(ui, repo, opts) + for node in [cp.node() for cp in ctx.children()]: + displayer.show(changenode=node) + + +cmdtable = { + "children": + (children, + [('r', 'rev', '', _('show children of the specified rev')), + ('', 'style', '', _('display using template map file')), + ('', 'template', '', _('display with template'))], + _('hg children [-r REV] [FILE]')), +} diff --git a/hgext/convert/__init__.py b/hgext/convert/__init__.py --- a/hgext/convert/__init__.py +++ b/hgext/convert/__init__.py @@ -5,48 +5,60 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -from common import NoRepo +from common import NoRepo, converter_source, converter_sink from cvs import convert_cvs from git import convert_git -from hg import convert_mercurial +from hg import mercurial_source, mercurial_sink +from subversion import convert_svn, debugsvnlog -import os +import os, shlex, shutil from mercurial import hg, ui, util, commands +from mercurial.i18n import _ + +commands.norepo += " convert debugsvnlog" -commands.norepo += " convert" +converters = [convert_cvs, convert_git, convert_svn, mercurial_source, + mercurial_sink] -converters = [convert_cvs, convert_git, convert_mercurial] +def convertsource(ui, path, **opts): + for c in converters: + try: + return c.getcommit and c(ui, path, **opts) + except (AttributeError, NoRepo): + pass + raise util.Abort('%s: unknown repository type' % path) -def converter(ui, path): +def convertsink(ui, path): if not os.path.isdir(path): raise util.Abort("%s: not a directory" % path) for c in converters: try: - return c(ui, path) - except NoRepo: + return c.putcommit and c(ui, path) + except (AttributeError, NoRepo): pass - raise util.Abort("%s: unknown repository type" % path) + raise util.Abort('%s: unknown repository type' % path) -class convert(object): - def __init__(self, ui, source, dest, mapfile, opts): +class converter(object): + def __init__(self, ui, source, dest, revmapfile, filemapper, opts): self.source = source self.dest = dest self.ui = ui self.opts = opts self.commitcache = {} - self.mapfile = mapfile - self.mapfilefd = None + self.revmapfile = revmapfile + self.revmapfilefd = None self.authors = {} self.authorfile = None + self.mapfile = filemapper self.map = {} try: - origmapfile = open(self.mapfile, 'r') - for l in origmapfile: + origrevmapfile = open(self.revmapfile, 'r') + for l in origrevmapfile: sv, dv = l[:-1].split() self.map[sv] = dv - origmapfile.close() + origrevmapfile.close() except IOError: pass @@ -69,10 +81,9 @@ class convert(object): n = visit.pop(0) if n in known or n in self.map: continue known[n] = 1 - self.commitcache[n] = self.source.getcommit(n) - cp = self.commitcache[n].parents + commit = self.cachecommit(n) parents[n] = [] - for p in cp: + for p in commit.parents: parents[n].append(p) visit.append(p) @@ -138,14 +149,14 @@ class convert(object): return s def mapentry(self, src, dst): - if self.mapfilefd is None: + if self.revmapfilefd is None: try: - self.mapfilefd = open(self.mapfile, "a") + self.revmapfilefd = open(self.revmapfile, "a") except IOError, (errno, strerror): - raise util.Abort("Could not open map file %s: %s, %s\n" % (self.mapfile, errno, strerror)) + raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror)) self.map[src] = dst - self.mapfilefd.write("%s %s\n" % (src, dst)) - self.mapfilefd.flush() + self.revmapfilefd.write("%s %s\n" % (src, dst)) + self.revmapfilefd.flush() def writeauthormap(self): authorfile = self.authorfile @@ -176,26 +187,56 @@ class convert(object): % (authorfile, line)) afile.close() + def cachecommit(self, rev): + commit = self.source.getcommit(rev) + commit.author = self.authors.get(commit.author, commit.author) + self.commitcache[rev] = commit + return commit + def copy(self, rev): - c = self.commitcache[rev] - files = self.source.getchanges(rev) + commit = self.commitcache[rev] + do_copies = hasattr(self.dest, 'copyfile') + filenames = [] + files, copies = self.source.getchanges(rev) + parents = [self.map[r] for r in commit.parents] + if commit.parents: + prev = commit.parents[0] + if prev not in self.commitcache: + self.cachecommit(prev) + pbranch = self.commitcache[prev].branch + else: + pbranch = None + self.dest.setbranch(commit.branch, pbranch, parents) for f, v in files: + newf = self.mapfile(f) + if not newf: + continue + filenames.append(newf) try: data = self.source.getfile(f, v) except IOError, inst: - self.dest.delfile(f) + self.dest.delfile(newf) else: e = self.source.getmode(f, v) - self.dest.putfile(f, e, data) + self.dest.putfile(newf, e, data) + if do_copies: + if f in copies: + copyf = self.mapfile(copies[f]) + if copyf: + # Merely marks that a copy happened. + self.dest.copyfile(copyf, newf) - r = [self.map[v] for v in c.parents] - f = [f for f, v in files] - newnode = self.dest.putcommit(f, r, c) + if not filenames and self.mapfile.active(): + newnode = parents[0] + else: + newnode = self.dest.putcommit(filenames, parents, commit) self.mapentry(rev, newnode) def convert(self): try: + self.dest.before() + self.source.setrevmap(self.map) self.ui.status("scanning source...\n") heads = self.source.getheads() parents = self.walktree(heads) @@ -210,9 +251,6 @@ class convert(object): desc = self.commitcache[c].desc if "\n" in desc: desc = desc.splitlines()[0] - author = self.commitcache[c].author - author = self.authors.get(author, author) - self.commitcache[c].author = author self.ui.status("%d %s\n" % (num, desc)) self.copy(c) @@ -235,25 +273,117 @@ class convert(object): self.cleanup() def cleanup(self): - if self.mapfilefd: - self.mapfilefd.close() + self.dest.after() + if self.revmapfilefd: + self.revmapfilefd.close() + +def rpairs(name): + e = len(name) + while e != -1: + yield name[:e], name[e+1:] + e = name.rfind('/', 0, e) + +class filemapper(object): + '''Map and filter filenames when importing. + A name can be mapped to itself, a new name, or None (omit from new + repository).''' + + def __init__(self, ui, path=None): + self.ui = ui + self.include = {} + self.exclude = {} + self.rename = {} + if path: + if self.parse(path): + raise util.Abort(_('errors in filemap')) -def _convert(ui, src, dest=None, mapfile=None, **opts): - '''Convert a foreign SCM repository to a Mercurial one. + def parse(self, path): + errs = 0 + def check(name, mapping, listname): + if name in mapping: + self.ui.warn(_('%s:%d: %r already in %s list\n') % + (lex.infile, lex.lineno, name, listname)) + return 1 + return 0 + lex = shlex.shlex(open(path), path, True) + lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?' + cmd = lex.get_token() + while cmd: + if cmd == 'include': + name = lex.get_token() + errs += check(name, self.exclude, 'exclude') + self.include[name] = name + elif cmd == 'exclude': + name = lex.get_token() + errs += check(name, self.include, 'include') + errs += check(name, self.rename, 'rename') + self.exclude[name] = name + elif cmd == 'rename': + src = lex.get_token() + dest = lex.get_token() + errs += check(src, self.exclude, 'exclude') + self.rename[src] = dest + elif cmd == 'source': + errs += self.parse(lex.get_token()) + else: + self.ui.warn(_('%s:%d: unknown directive %r\n') % + (lex.infile, lex.lineno, cmd)) + errs += 1 + cmd = lex.get_token() + return errs + + def lookup(self, name, mapping): + for pre, suf in rpairs(name): + try: + return mapping[pre], pre, suf + except KeyError, err: + pass + return '', name, '' + + def __call__(self, name): + if self.include: + inc = self.lookup(name, self.include)[0] + else: + inc = name + if self.exclude: + exc = self.lookup(name, self.exclude)[0] + else: + exc = '' + if not inc or exc: + return None + newpre, pre, suf = self.lookup(name, self.rename) + if newpre: + if newpre == '.': + return suf + if suf: + return newpre + '/' + suf + return newpre + return name + + def active(self): + return bool(self.include or self.exclude or self.rename) + +def convert(ui, src, dest=None, revmapfile=None, **opts): + """Convert a foreign SCM repository to a Mercurial one. Accepted source formats: - GIT - CVS + - SVN Accepted destination formats: - Mercurial + If no revision is given, all revisions will be converted. Otherwise, + convert will only import up to the named revision (given in a format + understood by the source). + If no destination directory name is specified, it defaults to the basename of the source with '-hg' appended. If the destination repository doesn't exist, it will be created. - If isn't given, it will be put in a default location - (/.hg/shamap by default). The is a simple text + If isn't given, it will be put in a default location + (/.hg/shamap by default). The is a simple text file that maps each source commit ID to the destination ID for that revision, like so: @@ -267,19 +397,33 @@ def _convert(ui, src, dest=None, mapfile that use unix logins to identify authors (eg: CVS). One line per author mapping and the line format is: srcauthor=whatever string you want - ''' + + The filemap is a file that allows filtering and remapping of files + and directories. Comment lines start with '#'. Each line can + contain one of the following directives: + + include path/to/file + + exclude path/to/file + + rename from/file to/file + + The 'include' directive causes a file, or all files under a + directory, to be included in the destination repository. The + 'exclude' directive causes files or directories to be omitted. + The 'rename' directive renames a file or directory. To rename + from a subdirectory into the root of the repository, use '.' as + the path to rename to. + """ util._encoding = 'UTF-8' - srcc = converter(ui, src) - if not hasattr(srcc, "getcommit"): - raise util.Abort("%s: can't read from this repo type" % src) - if not dest: dest = hg.defaultdest(src) + "-hg" ui.status("assuming destination %s\n" % dest) # Try to be smart and initalize things when required + created = False if os.path.isdir(dest): if len(os.listdir(dest)) > 0: try: @@ -294,29 +438,46 @@ def _convert(ui, src, dest=None, mapfile else: ui.status("initializing destination %s repository\n" % dest) hg.repository(ui, dest, create=True) + created = True elif os.path.exists(dest): raise util.Abort("destination %s exists and is not a directory" % dest) else: ui.status("initializing destination %s repository\n" % dest) hg.repository(ui, dest, create=True) + created = True - destc = converter(ui, dest) - if not hasattr(destc, "putcommit"): - raise util.Abort("%s: can't write to this repo type" % src) + destc = convertsink(ui, dest) - if not mapfile: + try: + srcc = convertsource(ui, src, rev=opts.get('rev')) + except Exception: + if created: + shutil.rmtree(dest, True) + raise + + if not revmapfile: try: - mapfile = destc.mapfile() + revmapfile = destc.revmapfile() except: - mapfile = os.path.join(destc, "map") + revmapfile = os.path.join(destc, "map") + - c = convert(ui, srcc, destc, mapfile, opts) + c = converter(ui, srcc, destc, revmapfile, filemapper(ui, opts['filemap']), + opts) c.convert() + cmdtable = { "convert": - (_convert, + (convert, [('A', 'authors', '', 'username mapping filename'), + ('', 'filemap', '', 'remap file names using contents of file'), + ('r', 'rev', '', 'import up to target revision REV'), ('', 'datesort', None, 'try to sort changesets by date')], 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'), + "debugsvnlog": + (debugsvnlog, + [], + 'hg debugsvnlog'), } + diff --git a/hgext/convert/common.py b/hgext/convert/common.py --- a/hgext/convert/common.py +++ b/hgext/convert/common.py @@ -1,21 +1,46 @@ # common code for the convert extension +import base64 +import cPickle as pickle + +def encodeargs(args): + def encodearg(s): + lines = base64.encodestring(s) + lines = [l.splitlines()[0] for l in lines] + return ''.join(lines) + + s = pickle.dumps(args) + return encodearg(s) + +def decodeargs(s): + s = base64.decodestring(s) + return pickle.loads(s) class NoRepo(Exception): pass class commit(object): - def __init__(self, **parts): - for x in "author date desc parents".split(): - if not x in parts: - raise util.Abort("commit missing field %s" % x) - self.__dict__.update(parts) + def __init__(self, author, date, desc, parents, branch=None, rev=None): + self.author = author + self.date = date + self.desc = desc + self.parents = parents + self.branch = branch + self.rev = rev class converter_source(object): """Conversion source interface""" - def __init__(self, ui, path): + def __init__(self, ui, path, rev=None): """Initialize conversion source (or raise NoRepo("message") exception if path is not a valid repository)""" - raise NotImplementedError() + self.ui = ui + self.path = path + self.rev = rev + + self.encoding = 'utf-8' + + def setrevmap(self, revmap): + """set the map of already-converted revisions""" + pass def getheads(self): """Return a list of this repository's heads""" @@ -30,10 +55,12 @@ class converter_source(object): raise NotImplementedError() def getchanges(self, version): - """Return sorted list of (filename, id) tuples for all files changed in rev. + """Returns a tuple of (files, copies) + Files is a sorted list of (filename, id) tuples for all files changed + in version, where id is the source revision id of the file. - id just tells us which revision to return in getfile(), e.g. in - git it's an object hash.""" + copies is a dictionary of dest: source + """ raise NotImplementedError() def getcommit(self, version): @@ -44,6 +71,20 @@ class converter_source(object): """Return the tags as a dictionary of name: revision""" raise NotImplementedError() + def recode(self, s, encoding=None): + if not encoding: + encoding = self.encoding or 'utf-8' + + if isinstance(s, unicode): + return s.encode("utf-8") + try: + return s.decode(encoding).encode("utf-8") + except: + try: + return s.decode("latin-1").encode("utf-8") + except: + return s.decode(encoding, "replace").encode("utf-8") + class converter_sink(object): """Conversion sink (target) interface""" @@ -56,7 +97,7 @@ class converter_sink(object): """Return a list of this repository's heads""" raise NotImplementedError() - def mapfile(self): + def revmapfile(self): """Path to a file that will contain lines source_rev_id sink_rev_id mapping equivalent revision identifiers for each system.""" @@ -94,3 +135,11 @@ class converter_sink(object): """Put tags into sink. tags: {tagname: sink_rev_id, ...}""" raise NotImplementedError() + + def setbranch(self, branch, pbranch, parents): + """Set the current branch name. Called before the first putfile + on the branch. + branch: branch name for subsequent commits + pbranch: branch name of parent commit + parents: destination revisions of parent""" + pass diff --git a/hgext/convert/cvs.py b/hgext/convert/cvs.py --- a/hgext/convert/cvs.py +++ b/hgext/convert/cvs.py @@ -6,9 +6,9 @@ from mercurial import util from common import NoRepo, commit, converter_source class convert_cvs(converter_source): - def __init__(self, ui, path): - self.path = path - self.ui = ui + def __init__(self, ui, path, rev=None): + super(convert_cvs, self).__init__(ui, path, rev=rev) + cvs = os.path.join(path, "CVS") if not os.path.exists(cvs): raise NoRepo("couldn't open CVS repo %s" % path) @@ -29,15 +29,33 @@ class convert_cvs(converter_source): if self.changeset: return + maxrev = 0 + cmd = 'cvsps -A -u --cvs-direct -q' + if self.rev: + # TODO: handle tags + try: + # patchset number? + maxrev = int(self.rev) + except ValueError: + try: + # date + util.parsedate(self.rev, ['%Y/%m/%d %H:%M:%S']) + cmd = '%s -d "1970/01/01 00:00:01" -d "%s"' % (cmd, self.rev) + except util.Abort: + raise util.Abort('revision %s is not a patchset number or date' % self.rev) + cmd += " 2>&1" + d = os.getcwd() try: os.chdir(self.path) id = None state = 0 - for l in os.popen("cvsps -A -u --cvs-direct -q"): + for l in os.popen(cmd): if state == 0: # header if l.startswith("PatchSet"): id = l[9:-2] + if maxrev and int(id) > maxrev: + state = 3 elif l.startswith("Date"): date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"]) date = util.datestr(date) @@ -62,8 +80,6 @@ class convert_cvs(converter_source): if l == "Members: \n": files = {} log = self.recode(log[:-1]) - if log.isspace(): - log = "*** empty log message ***\n" state = 2 else: log += l @@ -85,6 +101,8 @@ class convert_cvs(converter_source): rev = l[colon+1:-2] rev = rev.split("->")[1] files[file] = rev + elif state == 3: + continue self.heads = self.lastbranch.values() finally: @@ -136,7 +154,7 @@ class convert_cvs(converter_source): sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw, "END AUTH REQUEST", ""])) if sck.recv(128) != "I LOVE YOU\n": - raise NoRepo("CVS pserver authentication failed") + raise util.Abort("CVS pserver authentication failed") self.writep = self.readp = sck.makefile('r+') @@ -149,7 +167,8 @@ class convert_cvs(converter_source): if root.startswith(":ext:"): root = root[5:] m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root) - if not m: + # Do not take Windows path "c:\foo\bar" for a connection strings + if os.path.isdir(root) or not m: conntype = "local" else: conntype = "rsh" @@ -163,7 +182,10 @@ class convert_cvs(converter_source): else: cmd = [rsh, host] + cmd - self.writep, self.readp = os.popen2(cmd) + # popen2 does not support argument lists under Windows + cmd = [util.shellquote(arg) for arg in cmd] + cmd = util.quotecommand(' '.join(cmd)) + self.writep, self.readp = os.popen2(cmd, 'b') self.realroot = root @@ -189,7 +211,7 @@ class convert_cvs(converter_source): raise IOError args = ("-N -P -kk -r %s --" % rev).split() - args.append(os.path.join(self.cvsrepo, name)) + args.append(self.cvsrepo + '/' + name) for x in args: self.writep.write("Argument %s\n" % x) self.writep.write("Directory .\n%s\nco\n" % self.realroot) @@ -237,10 +259,7 @@ class convert_cvs(converter_source): files = self.files[rev] cl = files.items() cl.sort() - return cl - - def recode(self, text): - return text.decode(self.encoding, "replace").encode("utf-8") + return (cl, {}) def getcommit(self, rev): return self.changeset[rev] diff --git a/hgext/convert/git.py b/hgext/convert/git.py --- a/hgext/convert/git.py +++ b/hgext/convert/git.py @@ -5,15 +5,6 @@ from mercurial import util from common import NoRepo, commit, converter_source -def recode(s): - try: - return s.decode("utf-8").encode("utf-8") - except: - try: - return s.decode("latin-1").encode("utf-8") - except: - return s.decode("utf-8", "replace").encode("utf-8") - class convert_git(converter_source): # Windows does not support GIT_DIR= construct while other systems # cannot remove environment variable. Just assume none have @@ -32,18 +23,22 @@ class convert_git(converter_source): else: def gitcmd(self, s): return os.popen('GIT_DIR=%s %s' % (self.path, s)) - - def __init__(self, ui, path): + + def __init__(self, ui, path, rev=None): + super(convert_git, self).__init__(ui, path, rev=rev) + if os.path.isdir(path + "/.git"): path += "/.git" - self.path = path - self.ui = ui if not os.path.exists(path + "/objects"): raise NoRepo("couldn't open GIT repo %s" % path) + self.path = path def getheads(self): - fh = self.gitcmd("git-rev-parse --verify HEAD") - return [fh.read()[:-1]] + if not self.rev: + return self.gitcmd('git-rev-parse --branches').read().splitlines() + else: + fh = self.gitcmd("git-rev-parse --verify %s" % self.rev) + return [fh.read()[:-1]] def catfile(self, rev, type): if rev == "0" * 40: raise IOError() @@ -61,22 +56,27 @@ class convert_git(converter_source): self.modecache = {} fh = self.gitcmd("git-diff-tree --root -m -r %s" % version) changes = [] + seen = {} for l in fh: - if "\t" not in l: continue + if "\t" not in l: + continue m, f = l[:-1].split("\t") + if f in seen: + continue + seen[f] = 1 m = m.split() h = m[3] p = (m[1] == "100755") s = (m[1] == "120000") self.modecache[(f, h)] = (p and "x") or (s and "l") or "" changes.append((f, h)) - return changes + return (changes, {}) def getcommit(self, version): c = self.catfile(version, "commit") # read the commit hash end = c.find("\n\n") message = c[end+2:] - message = recode(message) + message = self.recode(message) l = c[:end].splitlines() manifest = l[0].split()[1] parents = [] @@ -87,13 +87,13 @@ class convert_git(converter_source): tm, tz = p[-2:] author = " ".join(p[:-2]) if author[0] == "<": author = author[1:-1] - author = recode(author) + author = self.recode(author) if n == "committer": p = v.split() tm, tz = p[-2:] committer = " ".join(p[:-2]) if committer[0] == "<": committer = committer[1:-1] - committer = recode(committer) + committer = self.recode(committer) message += "\ncommitter: %s\n" % committer if n == "parent": parents.append(v) @@ -102,7 +102,8 @@ class convert_git(converter_source): date = tm + " " + str(tz) author = author or "unknown" - c = commit(parents=parents, date=date, author=author, desc=message) + c = commit(parents=parents, date=date, author=author, desc=message, + rev=version) return c def gettags(self): diff --git a/hgext/convert/hg.py b/hgext/convert/hg.py --- a/hgext/convert/hg.py +++ b/hgext/convert/hg.py @@ -1,20 +1,45 @@ # hg backend for convert extension +# Note for hg->hg conversion: Old versions of Mercurial didn't trim +# the whitespace from the ends of commit messages, but new versions +# do. Changesets created by those older versions, then converted, may +# thus have different hashes for changesets that are otherwise +# identical. + + import os, time -from mercurial import hg +from mercurial.i18n import _ +from mercurial.node import * +from mercurial import hg, lock, revlog, util -from common import NoRepo, converter_sink +from common import NoRepo, commit, converter_source, converter_sink -class convert_mercurial(converter_sink): +class mercurial_sink(converter_sink): def __init__(self, ui, path): self.path = path self.ui = ui + self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True) + self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False) + self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default') + self.lastbranch = None try: self.repo = hg.repository(self.ui, path) except: - raise NoRepo("could open hg repo %s" % path) + raise NoRepo("could not open hg repo %s as sink" % path) + self.lock = None + self.wlock = None - def mapfile(self): + def before(self): + self.wlock = self.repo.wlock() + self.lock = self.repo.lock() + self.repo.dirstate.clear() + + def after(self): + self.repo.dirstate.invalidate() + self.lock = None + self.wlock = None + + def revmapfile(self): return os.path.join(self.path, ".hg", "shamap") def authorfile(self): @@ -22,12 +47,15 @@ class convert_mercurial(converter_sink): def getheads(self): h = self.repo.changelog.heads() - return [ hg.hex(x) for x in h ] + return [ hex(x) for x in h ] def putfile(self, f, e, data): self.repo.wwrite(f, data, e) - if self.repo.dirstate.state(f) == '?': - self.repo.dirstate.update([f], "a") + if f not in self.repo.dirstate: + self.repo.dirstate.normallookup(f) + + def copyfile(self, source, dest): + self.repo.copy(source, dest) def delfile(self, f): try: @@ -36,6 +64,30 @@ class convert_mercurial(converter_sink): except: pass + def setbranch(self, branch, pbranch, parents): + if (not self.clonebranches) or (branch == self.lastbranch): + return + + self.lastbranch = branch + self.after() + if not branch: + branch = 'default' + if not pbranch: + pbranch = 'default' + + branchpath = os.path.join(self.path, branch) + try: + self.repo = hg.repository(self.ui, branchpath) + except: + if not parents: + self.repo = hg.repository(self.ui, branchpath, create=True) + else: + self.ui.note(_('cloning branch %s to %s\n') % (pbranch, branch)) + hg.clone(self.ui, os.path.join(self.path, pbranch), + branchpath, rev=parents, update=False, + stream=True) + self.repo = hg.repository(self.ui, branchpath) + def putcommit(self, files, parents, commit): seen = {} pl = [] @@ -51,16 +103,17 @@ class convert_mercurial(converter_sink): text = commit.desc extra = {} - try: - extra["branch"] = commit.branch - except AttributeError: - pass + if self.branchnames and commit.branch: + extra['branch'] = commit.branch + if commit.rev: + extra['convert_revision'] = commit.rev while parents: p1 = p2 p2 = parents.pop(0) a = self.repo.rawcommit(files, text, commit.author, commit.date, - hg.bin(p1), hg.bin(p2), extra=extra) + bin(p1), bin(p2), extra=extra) + self.repo.dirstate.clear() text = "(octopus merge fixup)\n" p2 = hg.hex(self.repo.changelog.tip()) @@ -89,6 +142,69 @@ class convert_mercurial(converter_sink): f.close() if not oldlines: self.repo.add([".hgtags"]) date = "%s 0" % int(time.mktime(time.gmtime())) + extra = {} + if self.tagsbranch != 'default': + extra['branch'] = self.tagsbranch + try: + tagparent = self.repo.changectx(self.tagsbranch).node() + except hg.RepoError, inst: + tagparent = nullid self.repo.rawcommit([".hgtags"], "update tags", "convert-repo", - date, self.repo.changelog.tip(), hg.nullid) - return hg.hex(self.repo.changelog.tip()) + date, tagparent, nullid) + return hex(self.repo.changelog.tip()) + +class mercurial_source(converter_source): + def __init__(self, ui, path, rev=None): + converter_source.__init__(self, ui, path, rev) + self.repo = hg.repository(self.ui, path) + self.lastrev = None + self.lastctx = None + + def changectx(self, rev): + if self.lastrev != rev: + self.lastctx = self.repo.changectx(rev) + self.lastrev = rev + return self.lastctx + + def getheads(self): + if self.rev: + return [hex(self.repo.changectx(self.rev).node())] + else: + return [hex(node) for node in self.repo.heads()] + + def getfile(self, name, rev): + try: + return self.changectx(rev).filectx(name).data() + except revlog.LookupError, err: + raise IOError(err) + + def getmode(self, name, rev): + m = self.changectx(rev).manifest() + return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '') + + def getchanges(self, rev): + ctx = self.changectx(rev) + m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3] + changes = [(name, rev) for name in m + a + r] + changes.sort() + return (changes, self.getcopies(ctx, m + a)) + + def getcopies(self, ctx, files): + copies = {} + for name in files: + try: + copies[name] = ctx.filectx(name).renamed()[0] + except TypeError: + pass + return copies + + def getcommit(self, rev): + ctx = self.changectx(rev) + parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid] + return commit(author=ctx.user(), date=util.datestr(ctx.date()), + desc=ctx.description(), parents=parents, + branch=ctx.branch()) + + def gettags(self): + tags = [t for t in self.repo.tagslist() if t[0] != 'tip'] + return dict([(name, hex(node)) for name, node in tags]) diff --git a/hgext/convert/subversion.py b/hgext/convert/subversion.py new file mode 100644 --- /dev/null +++ b/hgext/convert/subversion.py @@ -0,0 +1,646 @@ +# Subversion 1.4/1.5 Python API backend +# +# Copyright(C) 2007 Daniel Holth et al +# +# Configuration options: +# +# convert.svn.trunk +# Relative path to the trunk (default: "trunk") +# convert.svn.branches +# Relative path to tree of branches (default: "branches") +# +# Set these in a hgrc, or on the command line as follows: +# +# hg convert --config convert.svn.trunk=wackoname [...] + +import locale +import os +import sys +import cPickle as pickle +from mercurial import util + +# Subversion stuff. Works best with very recent Python SVN bindings +# e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing +# these bindings. + +from cStringIO import StringIO + +from common import NoRepo, commit, converter_source, encodeargs, decodeargs + +try: + from svn.core import SubversionException, Pool + import svn + import svn.client + import svn.core + import svn.ra + import svn.delta + import transport +except ImportError: + pass + +def geturl(path): + try: + return svn.client.url_from_path(svn.core.svn_path_canonicalize(path)) + except SubversionException: + pass + if os.path.isdir(path): + return 'file://%s' % os.path.normpath(os.path.abspath(path)) + return path + +def optrev(number): + optrev = svn.core.svn_opt_revision_t() + optrev.kind = svn.core.svn_opt_revision_number + optrev.value.number = number + return optrev + +class changedpath(object): + def __init__(self, p): + self.copyfrom_path = p.copyfrom_path + self.copyfrom_rev = p.copyfrom_rev + self.action = p.action + +def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True, + strict_node_history=False): + protocol = -1 + def receiver(orig_paths, revnum, author, date, message, pool): + if orig_paths is not None: + for k, v in orig_paths.iteritems(): + orig_paths[k] = changedpath(v) + pickle.dump((orig_paths, revnum, author, date, message), + fp, protocol) + + try: + # Use an ra of our own so that our parent can consume + # our results without confusing the server. + t = transport.SvnRaTransport(url=url) + svn.ra.get_log(t.ra, paths, start, end, limit, + discover_changed_paths, + strict_node_history, + receiver) + except SubversionException, (inst, num): + pickle.dump(num, fp, protocol) + else: + pickle.dump(None, fp, protocol) + fp.close() + +def debugsvnlog(ui, **opts): + """Fetch SVN log in a subprocess and channel them back to parent to + avoid memory collection issues. + """ + util.set_binary(sys.stdin) + util.set_binary(sys.stdout) + args = decodeargs(sys.stdin.read()) + get_log_child(sys.stdout, *args) + +# SVN conversion code stolen from bzr-svn and tailor +class convert_svn(converter_source): + def __init__(self, ui, url, rev=None): + super(convert_svn, self).__init__(ui, url, rev=rev) + + try: + SubversionException + except NameError: + msg = 'subversion python bindings could not be loaded\n' + ui.warn(msg) + raise NoRepo(msg) + + self.encoding = locale.getpreferredencoding() + self.lastrevs = {} + + latest = None + try: + # Support file://path@rev syntax. Useful e.g. to convert + # deleted branches. + at = url.rfind('@') + if at >= 0: + latest = int(url[at+1:]) + url = url[:at] + except ValueError, e: + pass + self.url = geturl(url) + self.encoding = 'UTF-8' # Subversion is always nominal UTF-8 + try: + self.transport = transport.SvnRaTransport(url=self.url) + self.ra = self.transport.ra + self.ctx = self.transport.client + self.base = svn.ra.get_repos_root(self.ra) + self.module = self.url[len(self.base):] + self.modulemap = {} # revision, module + self.commits = {} + self.paths = {} + self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding) + except SubversionException, e: + raise NoRepo("couldn't open SVN repo %s" % self.url) + + if rev: + try: + latest = int(rev) + except ValueError: + raise util.Abort('svn: revision %s is not an integer' % rev) + + try: + self.get_blacklist() + except IOError, e: + pass + + self.last_changed = self.latest(self.module, latest) + + self.head = self.revid(self.last_changed) + + def setrevmap(self, revmap): + lastrevs = {} + for revid in revmap.keys(): + uuid, module, revnum = self.revsplit(revid) + lastrevnum = lastrevs.setdefault(module, revnum) + if revnum > lastrevnum: + lastrevs[module] = revnum + self.lastrevs = lastrevs + + def exists(self, path, optrev): + try: + return svn.client.ls(self.url.rstrip('/') + '/' + path, + optrev, False, self.ctx) + except SubversionException, err: + return [] + + def getheads(self): + # detect standard /branches, /tags, /trunk layout + rev = optrev(self.last_changed) + rpath = self.url.strip('/') + cfgtrunk = self.ui.config('convert', 'svn.trunk') + cfgbranches = self.ui.config('convert', 'svn.branches') + trunk = (cfgtrunk or 'trunk').strip('/') + branches = (cfgbranches or 'branches').strip('/') + if self.exists(trunk, rev) and self.exists(branches, rev): + self.ui.note('found trunk at %r and branches at %r\n' % + (trunk, branches)) + oldmodule = self.module + self.module += '/' + trunk + lt = self.latest(self.module, self.last_changed) + self.head = self.revid(lt) + self.heads = [self.head] + branchnames = svn.client.ls(rpath + '/' + branches, rev, False, + self.ctx) + for branch in branchnames.keys(): + if oldmodule: + module = '/' + oldmodule + '/' + branches + '/' + branch + else: + module = '/' + branches + '/' + branch + brevnum = self.latest(module, self.last_changed) + brev = self.revid(brevnum, module) + self.ui.note('found branch %s at %d\n' % (branch, brevnum)) + self.heads.append(brev) + elif cfgtrunk or cfgbranches: + raise util.Abort('trunk/branch layout expected, but not found') + else: + self.ui.note('working with one branch\n') + self.heads = [self.head] + return self.heads + + def getfile(self, file, rev): + data, mode = self._getfile(file, rev) + self.modecache[(file, rev)] = mode + return data + + def getmode(self, file, rev): + return self.modecache[(file, rev)] + + def getchanges(self, rev): + self.modecache = {} + (paths, parents) = self.paths[rev] + files, copies = self.expandpaths(rev, paths, parents) + files.sort() + files = zip(files, [rev] * len(files)) + + # caller caches the result, so free it here to release memory + del self.paths[rev] + return (files, copies) + + def getcommit(self, rev): + if rev not in self.commits: + uuid, module, revnum = self.revsplit(rev) + self.module = module + self.reparent(module) + stop = self.lastrevs.get(module, 0) + self._fetch_revisions(from_revnum=revnum, to_revnum=stop) + commit = self.commits[rev] + # caller caches the result, so free it here to release memory + del self.commits[rev] + return commit + + def get_log(self, paths, start, end, limit=0, discover_changed_paths=True, + strict_node_history=False): + + def parent(fp): + while True: + entry = pickle.load(fp) + try: + orig_paths, revnum, author, date, message = entry + except: + if entry is None: + break + raise SubversionException("child raised exception", entry) + yield entry + + args = [self.url, paths, start, end, limit, discover_changed_paths, + strict_node_history] + arg = encodeargs(args) + hgexe = util.hgexecutable() + cmd = '%s debugsvnlog' % util.shellquote(hgexe) + stdin, stdout = os.popen2(cmd, 'b') + + stdin.write(arg) + stdin.close() + + for p in parent(stdout): + yield p + + def gettags(self): + tags = {} + start = self.revnum(self.head) + try: + for entry in self.get_log(['/tags'], 0, start): + orig_paths, revnum, author, date, message = entry + for path in orig_paths: + if not path.startswith('/tags/'): + continue + ent = orig_paths[path] + source = ent.copyfrom_path + rev = ent.copyfrom_rev + tag = path.split('/', 2)[2] + tags[tag] = self.revid(rev, module=source) + except SubversionException, (inst, num): + self.ui.note('no tags found at revision %d\n' % start) + return tags + + # -- helper functions -- + + def revid(self, revnum, module=None): + if not module: + module = self.module + return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding), + revnum) + + def revnum(self, rev): + return int(rev.split('@')[-1]) + + def revsplit(self, rev): + url, revnum = rev.encode(self.encoding).split('@', 1) + revnum = int(revnum) + parts = url.split('/', 1) + uuid = parts.pop(0)[4:] + mod = '' + if parts: + mod = '/' + parts[0] + return uuid, mod, revnum + + def latest(self, path, stop=0): + 'find the latest revision affecting path, up to stop' + if not stop: + stop = svn.ra.get_latest_revnum(self.ra) + try: + self.reparent('') + dirent = svn.ra.stat(self.ra, path.strip('/'), stop) + self.reparent(self.module) + except SubversionException: + dirent = None + if not dirent: + raise util.Abort('%s not found up to revision %d' % (path, stop)) + + return dirent.created_rev + + def get_blacklist(self): + """Avoid certain revision numbers. + It is not uncommon for two nearby revisions to cancel each other + out, e.g. 'I copied trunk into a subdirectory of itself instead + of making a branch'. The converted repository is significantly + smaller if we ignore such revisions.""" + self.blacklist = util.set() + blacklist = self.blacklist + for line in file("blacklist.txt", "r"): + if not line.startswith("#"): + try: + svn_rev = int(line.strip()) + blacklist.add(svn_rev) + except ValueError, e: + pass # not an integer or a comment + + def is_blacklisted(self, svn_rev): + return svn_rev in self.blacklist + + def reparent(self, module): + svn_url = self.base + module + self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding)) + svn.ra.reparent(self.ra, svn_url.encode(self.encoding)) + + def expandpaths(self, rev, paths, parents): + def get_entry_from_path(path, module=self.module): + # Given the repository url of this wc, say + # "http://server/plone/CMFPlone/branches/Plone-2_0-branch" + # extract the "entry" portion (a relative path) from what + # svn log --xml says, ie + # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py" + # that is to say "tests/PloneTestCase.py" + if path.startswith(module): + relative = path[len(module):] + if relative.startswith('/'): + return relative[1:] + else: + return relative + + # The path is outside our tracked tree... + self.ui.debug('%r is not under %r, ignoring\n' % (path, module)) + return None + + entries = [] + copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions. + copies = {} + revnum = self.revnum(rev) + + if revnum in self.modulemap: + new_module = self.modulemap[revnum] + if new_module != self.module: + self.module = new_module + self.reparent(self.module) + + for path, ent in paths: + entrypath = get_entry_from_path(path, module=self.module) + entry = entrypath.decode(self.encoding) + + kind = svn.ra.check_path(self.ra, entrypath, revnum) + if kind == svn.core.svn_node_file: + if ent.copyfrom_path: + copyfrom_path = get_entry_from_path(ent.copyfrom_path) + if copyfrom_path: + self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev)) + # It's probably important for hg that the source + # exists in the revision's parent, not just the + # ent.copyfrom_rev + fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev) + if fromkind != 0: + copies[self.recode(entry)] = self.recode(copyfrom_path) + entries.append(self.recode(entry)) + elif kind == 0: # gone, but had better be a deleted *file* + self.ui.debug("gone from %s\n" % ent.copyfrom_rev) + + # if a branch is created but entries are removed in the same + # changeset, get the right fromrev + if parents: + uuid, old_module, fromrev = self.revsplit(parents[0]) + else: + fromrev = revnum - 1 + # might always need to be revnum - 1 in these 3 lines? + old_module = self.modulemap.get(fromrev, self.module) + + basepath = old_module + "/" + get_entry_from_path(path, module=self.module) + entrypath = old_module + "/" + get_entry_from_path(path, module=self.module) + + def lookup_parts(p): + rc = None + parts = p.split("/") + for i in range(len(parts)): + part = "/".join(parts[:i]) + info = part, copyfrom.get(part, None) + if info[1] is not None: + self.ui.debug("Found parent directory %s\n" % info[1]) + rc = info + return rc + + self.ui.debug("base, entry %s %s\n" % (basepath, entrypath)) + + frompath, froment = lookup_parts(entrypath) or (None, revnum - 1) + + # need to remove fragment from lookup_parts and replace with copyfrom_path + if frompath is not None: + self.ui.debug("munge-o-matic\n") + self.ui.debug(entrypath + '\n') + self.ui.debug(entrypath[len(frompath):] + '\n') + entrypath = froment.copyfrom_path + entrypath[len(frompath):] + fromrev = froment.copyfrom_rev + self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath)) + + fromkind = svn.ra.check_path(self.ra, entrypath, fromrev) + if fromkind == svn.core.svn_node_file: # a deleted file + entries.append(self.recode(entry)) + elif fromkind == svn.core.svn_node_dir: + # print "Deleted/moved non-file:", revnum, path, ent + # children = self._find_children(path, revnum - 1) + # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action) + # Sometimes this is tricky. For example: in + # The Subversion Repository revision 6940 a dir + # was copied and one of its files was deleted + # from the new location in the same commit. This + # code can't deal with that yet. + if ent.action == 'C': + children = self._find_children(path, fromrev) + else: + oroot = entrypath.strip('/') + nroot = path.strip('/') + children = self._find_children(oroot, fromrev) + children = [s.replace(oroot,nroot) for s in children] + # Mark all [files, not directories] as deleted. + for child in children: + # Can we move a child directory and its + # parent in the same commit? (probably can). Could + # cause problems if instead of revnum -1, + # we have to look in (copyfrom_path, revnum - 1) + entrypath = get_entry_from_path("/" + child, module=old_module) + if entrypath: + entry = self.recode(entrypath.decode(self.encoding)) + if entry in copies: + # deleted file within a copy + del copies[entry] + else: + entries.append(entry) + else: + self.ui.debug('unknown path in revision %d: %s\n' % \ + (revnum, path)) + elif kind == svn.core.svn_node_dir: + # Should probably synthesize normal file entries + # and handle as above to clean up copy/rename handling. + + # If the directory just had a prop change, + # then we shouldn't need to look for its children. + # Also this could create duplicate entries. Not sure + # whether this will matter. Maybe should make entries a set. + # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev + # This will fail if a directory was copied + # from another branch and then some of its files + # were deleted in the same transaction. + children = self._find_children(path, revnum) + children.sort() + for child in children: + # Can we move a child directory and its + # parent in the same commit? (probably can). Could + # cause problems if instead of revnum -1, + # we have to look in (copyfrom_path, revnum - 1) + entrypath = get_entry_from_path("/" + child, module=self.module) + # print child, self.module, entrypath + if entrypath: + # Need to filter out directories here... + kind = svn.ra.check_path(self.ra, entrypath, revnum) + if kind != svn.core.svn_node_dir: + entries.append(self.recode(entrypath)) + + # Copies here (must copy all from source) + # Probably not a real problem for us if + # source does not exist + + # Can do this with the copy command "hg copy" + # if ent.copyfrom_path: + # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding), + # module=self.module) + # copyto_entry = entrypath + # + # print "copy directory", copyfrom_entry, 'to', copyto_entry + # + # copies.append((copyfrom_entry, copyto_entry)) + + if ent.copyfrom_path: + copyfrom_path = ent.copyfrom_path.decode(self.encoding) + copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module) + if copyfrom_entry: + copyfrom[path] = ent + self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path])) + + # Good, /probably/ a regular copy. Really should check + # to see whether the parent revision actually contains + # the directory in question. + children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev) + children.sort() + for child in children: + entrypath = get_entry_from_path("/" + child, module=self.module) + if entrypath: + entry = entrypath.decode(self.encoding) + # print "COPY COPY From", copyfrom_entry, entry + copyto_path = path + entry[len(copyfrom_entry):] + copyto_entry = get_entry_from_path(copyto_path, module=self.module) + # print "COPY", entry, "COPY To", copyto_entry + copies[self.recode(copyto_entry)] = self.recode(entry) + # copy from quux splort/quuxfile + + return (entries, copies) + + def _fetch_revisions(self, from_revnum = 0, to_revnum = 347): + self.child_cset = None + def parselogentry(orig_paths, revnum, author, date, message): + self.ui.debug("parsing revision %d (%d changes)\n" % + (revnum, len(orig_paths))) + + if revnum in self.modulemap: + new_module = self.modulemap[revnum] + if new_module != self.module: + self.module = new_module + self.reparent(self.module) + + rev = self.revid(revnum) + # branch log might return entries for a parent we already have + if (rev in self.commits or + (revnum < self.lastrevs.get(self.module, 0))): + return + + parents = [] + # check whether this revision is the start of a branch + if self.module in orig_paths: + ent = orig_paths[self.module] + if ent.copyfrom_path: + # ent.copyfrom_rev may not be the actual last revision + prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev) + self.modulemap[prev] = ent.copyfrom_path + parents = [self.revid(prev, ent.copyfrom_path)] + self.ui.note('found parent of branch %s at %d: %s\n' % \ + (self.module, prev, ent.copyfrom_path)) + else: + self.ui.debug("No copyfrom path, don't know what to do.\n") + + self.modulemap[revnum] = self.module # track backwards in time + + orig_paths = orig_paths.items() + orig_paths.sort() + paths = [] + # filter out unrelated paths + for path, ent in orig_paths: + if not path.startswith(self.module): + self.ui.debug("boring@%s: %s\n" % (revnum, path)) + continue + paths.append((path, ent)) + + self.paths[rev] = (paths, parents) + + # Example SVN datetime. Includes microseconds. + # ISO-8601 conformant + # '2007-01-04T17:35:00.902377Z' + date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"]) + + log = message and self.recode(message) + author = author and self.recode(author) or '' + try: + branch = self.module.split("/")[-1] + if branch == 'trunk': + branch = '' + except IndexError: + branch = None + + cset = commit(author=author, + date=util.datestr(date), + desc=log, + parents=parents, + branch=branch, + rev=rev.encode('utf-8')) + + self.commits[rev] = cset + if self.child_cset and not self.child_cset.parents: + self.child_cset.parents = [rev] + self.child_cset = cset + + self.ui.note('fetching revision log for "%s" from %d to %d\n' % + (self.module, from_revnum, to_revnum)) + + try: + for entry in self.get_log([self.module], from_revnum, to_revnum): + orig_paths, revnum, author, date, message = entry + if self.is_blacklisted(revnum): + self.ui.note('skipping blacklisted revision %d\n' % revnum) + continue + if orig_paths is None: + self.ui.debug('revision %d has no entries\n' % revnum) + continue + parselogentry(orig_paths, revnum, author, date, message) + except SubversionException, (inst, num): + if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION: + raise NoSuchRevision(branch=self, + revision="Revision number %d" % to_revnum) + raise + + def _getfile(self, file, rev): + io = StringIO() + # TODO: ra.get_file transmits the whole file instead of diffs. + mode = '' + try: + revnum = self.revnum(rev) + if self.module != self.modulemap[revnum]: + self.module = self.modulemap[revnum] + self.reparent(self.module) + info = svn.ra.get_file(self.ra, file, revnum, io) + if isinstance(info, list): + info = info[-1] + mode = ("svn:executable" in info) and 'x' or '' + mode = ("svn:special" in info) and 'l' or mode + except SubversionException, e: + notfound = (svn.core.SVN_ERR_FS_NOT_FOUND, + svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND) + if e.apr_err in notfound: # File not found + raise IOError() + raise + data = io.getvalue() + if mode == 'l': + link_prefix = "link " + if data.startswith(link_prefix): + data = data[len(link_prefix):] + return data, mode + + def _find_children(self, path, revnum): + path = path.strip('/') + pool = Pool() + rpath = '/'.join([self.base, path]).strip('/') + return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()] diff --git a/hgext/convert/transport.py b/hgext/convert/transport.py new file mode 100644 --- /dev/null +++ b/hgext/convert/transport.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2007 Daniel Holth +# This is a stripped-down version of the original bzr-svn transport.py, +# Copyright (C) 2006 Jelmer Vernooij + +# This program 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 Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from cStringIO import StringIO +import os +from tempfile import mktemp + +from svn.core import SubversionException, Pool +import svn.ra +import svn.client +import svn.core + +# Some older versions of the Python bindings need to be +# explicitly initialized. But what we want to do probably +# won't work worth a darn against those libraries anyway! +svn.ra.initialize() + +svn_config = svn.core.svn_config_get_config(None) + + +def _create_auth_baton(pool): + """Create a Subversion authentication baton. """ + import svn.client + # Give the client context baton a suite of authentication + # providers.h + providers = [ + svn.client.get_simple_provider(pool), + svn.client.get_username_provider(pool), + svn.client.get_ssl_client_cert_file_provider(pool), + svn.client.get_ssl_client_cert_pw_file_provider(pool), + svn.client.get_ssl_server_trust_file_provider(pool), + ] + # Platform-dependant authentication methods + if hasattr(svn.client, 'get_windows_simple_provider'): + providers.append(svn.client.get_windows_simple_provider(pool)) + + return svn.core.svn_auth_open(providers, pool) + +class NotBranchError(SubversionException): + pass + +class SvnRaTransport(object): + """ + Open an ra connection to a Subversion repository. + """ + def __init__(self, url="", ra=None): + self.pool = Pool() + self.svn_url = url + self.username = '' + self.password = '' + + # Only Subversion 1.4 has reparent() + if ra is None or not hasattr(svn.ra, 'reparent'): + self.client = svn.client.create_context(self.pool) + ab = _create_auth_baton(self.pool) + if False: + svn.core.svn_auth_set_parameter( + ab, svn.core.SVN_AUTH_PARAM_DEFAULT_USERNAME, self.username) + svn.core.svn_auth_set_parameter( + ab, svn.core.SVN_AUTH_PARAM_DEFAULT_PASSWORD, self.password) + self.client.auth_baton = ab + self.client.config = svn_config + try: + self.ra = svn.client.open_ra_session( + self.svn_url.encode('utf8'), + self.client, self.pool) + except SubversionException, (inst, num): + if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL, + svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED, + svn.core.SVN_ERR_BAD_URL): + raise NotBranchError(url) + raise + else: + self.ra = ra + svn.ra.reparent(self.ra, self.svn_url.encode('utf8')) + + class Reporter: + def __init__(self, (reporter, report_baton)): + self._reporter = reporter + self._baton = report_baton + + def set_path(self, path, revnum, start_empty, lock_token, pool=None): + svn.ra.reporter2_invoke_set_path(self._reporter, self._baton, + path, revnum, start_empty, lock_token, pool) + + def delete_path(self, path, pool=None): + svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton, + path, pool) + + def link_path(self, path, url, revision, start_empty, lock_token, + pool=None): + svn.ra.reporter2_invoke_link_path(self._reporter, self._baton, + path, url, revision, start_empty, lock_token, + pool) + + def finish_report(self, pool=None): + svn.ra.reporter2_invoke_finish_report(self._reporter, + self._baton, pool) + + def abort_report(self, pool=None): + svn.ra.reporter2_invoke_abort_report(self._reporter, + self._baton, pool) + + def do_update(self, revnum, path, *args, **kwargs): + return self.Reporter(svn.ra.do_update(self.ra, revnum, path, *args, **kwargs)) + + def clone(self, offset=None): + """See Transport.clone().""" + if offset is None: + return self.__class__(self.base) + + return SvnRaTransport(urlutils.join(self.base, offset), ra=self.ra) diff --git a/hgext/extdiff.py b/hgext/extdiff.py --- a/hgext/extdiff.py +++ b/hgext/extdiff.py @@ -4,106 +4,103 @@ # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -# -# 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 this extension: -# -# [extensions] -# hgext.extdiff = -# -# 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 + +''' +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 this extension: + + [extensions] + hgext.extdiff = + +The `extdiff' extension also allows to configure new diff commands, so +you do not need to type "hg extdiff -p kdiff3" always. -# # add new command called vdiff, runs kdiff3 -# cmd.vdiff = kdiff3 + [extdiff] + # add new command that runs GNU diff(1) in 'context diff' mode + cdiff = gdiff -Nprc5 + ## or the old way: + #cmd.cdiff = gdiff + #opts.cdiff = -Nprc5 -# # add new command called meld, runs meld (no need to name twice) -# cmd.meld = + # add new command called vdiff, runs kdiff3 + vdiff = kdiff3 -# # add new command called vimdiff, runs gvimdiff with DirDiff plugin -# #(see http://www.vim.org/scripts/script.php?script_id=102) -# # Non english user, be sure to put "let g:DirDiffDynamicDiffText = 1" in -# # your .vimrc -# cmd.vimdiff = gvim -# opts.vimdiff = -f '+next' '+execute "DirDiff" argv(0) argv(1)' -# -# 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). + # add new command called meld, runs meld (no need to name twice) + meld = + + # add new command called vimdiff, runs gvimdiff with DirDiff plugin + #(see http://www.vim.org/scripts/script.php?script_id=102) + # Non english user, be sure to put "let g:DirDiffDynamicDiffText = 1" in + # your .vimrc + vimdiff = gvim -f '+next' '+execute "DirDiff" argv(0) argv(1)' + +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.i18n import _ from mercurial.node import * -from mercurial import cmdutil, util -import os, shutil, tempfile +from mercurial import cmdutil, util, commands +import os, shlex, shutil, tempfile + +def snapshot_node(ui, repo, files, node, tmproot): + '''snapshot files as of some revision''' + mf = repo.changectx(node).manifest() + dirname = os.path.basename(repo.root) + if dirname == "": + dirname = "root" + dirname = '%s.%s' % (dirname, short(node)) + base = os.path.join(tmproot, dirname) + os.mkdir(base) + ui.note(_('making snapshot of %d files from rev %s\n') % + (len(files), short(node))) + for fn in files: + if not fn in mf: + # skipping new file after a merge ? + continue + wfn = util.pconvert(fn) + ui.note(' %s\n' % wfn) + dest = os.path.join(base, wfn) + destdir = os.path.dirname(dest) + if not os.path.isdir(destdir): + os.makedirs(destdir) + data = repo.wwritedata(wfn, repo.file(wfn).read(mf[wfn])) + open(dest, 'wb').write(data) + return dirname + + +def snapshot_wdir(ui, repo, files, tmproot): + '''snapshot files from working directory. + if not using snapshot, -I/-X does not work and recursive diff + in tools like kdiff3 and meld displays too many files.''' + dirname = os.path.basename(repo.root) + if dirname == "": + dirname = "root" + base = os.path.join(tmproot, dirname) + os.mkdir(base) + ui.note(_('making snapshot of %d files from working dir\n') % + (len(files))) + for fn in files: + wfn = util.pconvert(fn) + ui.note(' %s\n' % wfn) + dest = os.path.join(base, wfn) + destdir = os.path.dirname(dest) + if not os.path.isdir(destdir): + os.makedirs(destdir) + fp = open(dest, 'wb') + for chunk in util.filechunkiter(repo.wopener(wfn)): + fp.write(chunk) + return dirname + def dodiff(ui, repo, diffcmd, diffopts, pats, opts): - def snapshot_node(files, node): - '''snapshot files as of some revision''' - mf = repo.changectx(node).manifest() - dirname = os.path.basename(repo.root) - if dirname == "": - dirname = "root" - dirname = '%s.%s' % (dirname, short(node)) - base = os.path.join(tmproot, dirname) - os.mkdir(base) - if not ui.quiet: - ui.write_err(_('making snapshot of %d files from rev %s\n') % - (len(files), short(node))) - for fn in files: - if not fn in mf: - # skipping new file after a merge ? - continue - wfn = util.pconvert(fn) - ui.note(' %s\n' % wfn) - dest = os.path.join(base, wfn) - destdir = os.path.dirname(dest) - if not os.path.isdir(destdir): - os.makedirs(destdir) - data = repo.wwritedata(wfn, repo.file(wfn).read(mf[wfn])) - open(dest, 'wb').write(data) - return dirname - - def snapshot_wdir(files): - '''snapshot files from working directory. - if not using snapshot, -I/-X does not work and recursive diff - in tools like kdiff3 and meld displays too many files.''' - dirname = os.path.basename(repo.root) - if dirname == "": - dirname = "root" - base = os.path.join(tmproot, dirname) - os.mkdir(base) - if not ui.quiet: - ui.write_err(_('making snapshot of %d files from working dir\n') % - (len(files))) - for fn in files: - wfn = util.pconvert(fn) - ui.note(' %s\n' % wfn) - dest = os.path.join(base, wfn) - destdir = os.path.dirname(dest) - if not os.path.isdir(destdir): - os.makedirs(destdir) - fp = open(dest, 'wb') - for chunk in util.filechunkiter(repo.wopener(wfn)): - fp.write(chunk) - return dirname - node1, node2 = cmdutil.revpair(repo, opts['rev']) files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) modified, added, removed, deleted, unknown = repo.status( @@ -112,12 +109,34 @@ def dodiff(ui, repo, diffcmd, diffopts, return 0 tmproot = tempfile.mkdtemp(prefix='extdiff.') + dir2root = '' try: - dir1 = snapshot_node(modified + removed, node1) + # Always make a copy of node1 + dir1 = snapshot_node(ui, repo, modified + removed, node1, tmproot) + changes = len(modified) + len(removed) + len(added) + + # If node2 in not the wc or there is >1 change, copy it if node2: - dir2 = snapshot_node(modified + added, node2) + dir2 = snapshot_node(ui, repo, modified + added, node2, tmproot) + elif changes > 1: + dir2 = snapshot_wdir(ui, repo, modified + added, tmproot) else: - dir2 = snapshot_wdir(modified + added) + # This lets the diff tool open the changed file directly + dir2 = '' + dir2root = repo.root + + # If only one change, diff the files instead of the directories + if changes == 1 : + if len(modified): + dir1 = os.path.join(dir1, util.localpath(modified[0])) + dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0])) + elif len(removed) : + dir1 = os.path.join(dir1, util.localpath(removed[0])) + dir2 = os.devnull + else: + dir1 = os.devnull + dir2 = os.path.join(dir2root, dir2, util.localpath(added[0])) + cmdline = ('%s %s %s %s' % (util.shellquote(diffcmd), ' '.join(diffopts), util.shellquote(dir1), util.shellquote(dir2))) @@ -158,33 +177,41 @@ cmdtable = { [('p', 'program', '', _('comparison program to run')), ('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'))], + ] + commands.walkopts, _('hg extdiff [OPT]... [FILE]...')), } def uisetup(ui): for cmd, path in ui.configitems('extdiff'): - if not cmd.startswith('cmd.'): continue - cmd = cmd[4:] - if not path: path = cmd - diffopts = ui.config('extdiff', 'opts.' + cmd, '') - diffopts = diffopts and [diffopts] or [] + if cmd.startswith('cmd.'): + cmd = cmd[4:] + if not path: path = cmd + diffopts = ui.config('extdiff', 'opts.' + cmd, '') + diffopts = diffopts and [diffopts] or [] + elif cmd.startswith('opts.'): + continue + else: + # command = path opts + if path: + diffopts = shlex.split(path) + path = diffopts.pop(0) + else: + path, diffopts = cmd, [] def save(cmd, path, diffopts): '''use closure to save diff command to use''' def mydiff(ui, repo, *pats, **opts): return dodiff(ui, repo, path, diffopts, pats, opts) - mydiff.__doc__ = '''use %(path)r to diff repository (or selected files) + mydiff.__doc__ = '''use %(path)s to diff repository (or selected files) Show differences between revisions for the specified - files, using the %(path)r program. + files, using the %(path)s 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.''' % { - 'path': path, + 'path': util.uirepr(path), } return mydiff cmdtable[cmd] = (save(cmd, path, diffopts), diff --git a/hgext/fetch.py b/hgext/fetch.py --- a/hgext/fetch.py +++ b/hgext/fetch.py @@ -23,29 +23,29 @@ def fetch(ui, repo, source='default', ** if modheads == 0: return 0 if modheads == 1: - return hg.clean(repo, repo.changelog.tip(), wlock=wlock) + return hg.clean(repo, repo.changelog.tip()) 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) + hg.clean(repo, newparent) 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) + err = hg.merge(repo, newheads[0], remind=False) 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] + mod, add, rem = repo.status()[:3] message = (cmdutil.logmessage(opts) or (_('Automated merge with %s') % other.url())) n = repo.commit(mod + add + rem, message, - opts['user'], opts['date'], lock=lock, wlock=wlock, + opts['user'], opts['date'], force_editor=opts.get('force_editor')) ui.status(_('new changeset %d:%s merges remote changes ' 'with local\n') % (repo.changelog.rev(n), @@ -60,7 +60,7 @@ def fetch(ui, repo, source='default', ** 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) + modheads = repo.pull(other, heads=revs) return postincoming(other, modheads) parent, p2 = repo.dirstate.parents() @@ -69,10 +69,11 @@ def fetch(ui, repo, source='default', ** '(use "hg update" to check out tip)')) if p2 != nullid: raise util.Abort(_('outstanding uncommitted merge')) - wlock = repo.wlock() - lock = repo.lock() + wlock = lock = None try: - mod, add, rem = repo.status(wlock=wlock)[:3] + wlock = repo.wlock() + lock = repo.lock() + mod, add, rem = repo.status()[:3] if mod or add or rem: raise util.Abort(_('outstanding uncommitted changes')) if len(repo.heads()) > 1: @@ -80,19 +81,13 @@ def fetch(ui, repo, source='default', ** '(use "hg heads" and "hg merge" to merge)')) return pull() finally: - lock.release() - wlock.release() + del lock, wlock 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')), + [('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'))], + ] + commands.commitopts + commands.commitopts2 + commands.remoteopts, _('hg fetch [SOURCE]')), } diff --git a/hgext/gpg.py b/hgext/gpg.py --- a/hgext/gpg.py +++ b/hgext/gpg.py @@ -6,7 +6,7 @@ # of the GNU General Public License, incorporated herein by reference. import os, tempfile, binascii -from mercurial import util +from mercurial import util, commands from mercurial import node as hgnode from mercurial.i18n import _ @@ -240,7 +240,7 @@ def sign(ui, repo, *revs, **opts): repo.wfile(".hgsigs", "ab").write(sigmessage) - if repo.dirstate.state(".hgsigs") == '?': + if '.hgsigs' not in repo.dirstate: repo.add([".hgsigs"]) if opts["no_commit"]: @@ -269,10 +269,9 @@ cmdtable = { [('l', 'local', None, _('make the signature local')), ('f', 'force', None, _('sign even if the sigfile is modified')), ('', 'no-commit', None, _('do not commit the sigfile after signing')), + ('k', 'key', '', _('the key id to sign with')), ('m', 'message', '', _('commit message')), - ('d', 'date', '', _('date code')), - ('u', 'user', '', _('user')), - ('k', 'key', '', _('the key id to sign with'))], + ] + commands.commitopts2, _('hg sign [OPTION]... [REVISION]...')), "sigcheck": (check, [], _('hg sigcheck REVISION')), "sigs": (sigs, [], _('hg sigs')), diff --git a/hgext/hbisect.py b/hgext/hbisect.py --- a/hgext/hbisect.py +++ b/hgext/hbisect.py @@ -37,10 +37,9 @@ class bisect(object): self.ui = ui self.goodrevs = [] self.badrev = None - self.good_dirty = 0 - self.bad_dirty = 0 self.good_path = "good" self.bad_path = "bad" + self.is_reset = False if os.path.exists(os.path.join(self.path, self.good_path)): self.goodrevs = self.opener(self.good_path).read().splitlines() @@ -51,8 +50,10 @@ class bisect(object): self.badrev = hg.bin(r.pop(0)) def write(self): + if self.is_reset: + return if not os.path.isdir(self.path): - return + os.mkdir(self.path) f = self.opener(self.good_path, "w") f.write("\n".join([hg.hex(r) for r in self.goodrevs])) if len(self.goodrevs) > 0: @@ -81,6 +82,7 @@ class bisect(object): # Not sure about this #self.ui.write("Going back to tip\n") #self.repo.update(self.repo.changelog.tip()) + self.is_reset = True return 0 def num_ancestors(self, head=None, stop=None): @@ -301,10 +303,9 @@ For subcommands see "hg bisect help\" if len(args) > bisectcmdtable[cmd][1]: ui.warn(_("bisect: Too many arguments\n")) return help_() - try: - return bisectcmdtable[cmd][0](*args) - finally: - b.write() + ret = bisectcmdtable[cmd][0](*args) + b.write() + return ret cmdtable = { "bisect": (bisect_run, [], _("hg bisect [help|init|reset|next|good|bad]")), diff --git a/hgext/imerge.py b/hgext/imerge.py new file mode 100644 --- /dev/null +++ b/hgext/imerge.py @@ -0,0 +1,405 @@ +# Copyright (C) 2007 Brendan Cully +# Published under the GNU GPL + +''' +imerge - interactive merge +''' + +from mercurial.i18n import _ +from mercurial.node import * +from mercurial import commands, cmdutil, dispatch, fancyopts, hg, merge, util +import os, tarfile + +class InvalidStateFileException(Exception): pass + +class ImergeStateFile(object): + def __init__(self, im): + self.im = im + + def save(self, dest): + tf = tarfile.open(dest, 'w:gz') + + st = os.path.join(self.im.path, 'status') + tf.add(st, os.path.join('.hg', 'imerge', 'status')) + + for f in self.im.resolved: + (fd, fo) = self.im.conflicts[f] + abssrc = self.im.repo.wjoin(fd) + tf.add(abssrc, fd) + + tf.close() + + def load(self, source): + wlock = self.im.repo.wlock() + lock = self.im.repo.lock() + + tf = tarfile.open(source, 'r') + contents = tf.getnames() + # tarfile normalizes path separators to '/' + statusfile = '.hg/imerge/status' + if statusfile not in contents: + raise InvalidStateFileException('no status file') + + tf.extract(statusfile, self.im.repo.root) + p1, p2 = self.im.load() + if self.im.repo.dirstate.parents()[0] != p1.node(): + hg.clean(self.im.repo, p1.node()) + self.im.start(p2.node()) + for tarinfo in tf: + tf.extract(tarinfo, self.im.repo.root) + self.im.load() + +class Imerge(object): + def __init__(self, ui, repo): + self.ui = ui + self.repo = repo + + self.path = repo.join('imerge') + self.opener = util.opener(self.path) + + self.wctx = self.repo.workingctx() + self.conflicts = {} + self.resolved = [] + + def merging(self): + return len(self.wctx.parents()) > 1 + + def load(self): + # status format. \0-delimited file, fields are + # p1, p2, conflict count, conflict filenames, resolved filenames + # conflict filenames are tuples of localname, remoteorig, remotenew + + statusfile = self.opener('status') + + status = statusfile.read().split('\0') + if len(status) < 3: + raise util.Abort('invalid imerge status file') + + try: + parents = [self.repo.changectx(n) for n in status[:2]] + except LookupError: + raise util.Abort('merge parent %s not in repository' % short(p)) + + status = status[2:] + conflicts = int(status.pop(0)) * 3 + self.resolved = status[conflicts:] + for i in xrange(0, conflicts, 3): + self.conflicts[status[i]] = (status[i+1], status[i+2]) + + return parents + + def save(self): + lock = self.repo.lock() + + if not os.path.isdir(self.path): + os.mkdir(self.path) + statusfile = self.opener('status', 'wb') + + out = [hex(n.node()) for n in self.wctx.parents()] + out.append(str(len(self.conflicts))) + conflicts = self.conflicts.items() + conflicts.sort() + for fw, fd_fo in conflicts: + out.append(fw) + out.extend(fd_fo) + out.extend(self.resolved) + + statusfile.write('\0'.join(out)) + + def remaining(self): + return [f for f in self.conflicts if f not in self.resolved] + + def filemerge(self, fn, interactive=True): + wlock = self.repo.wlock() + + (fd, fo) = self.conflicts[fn] + p1, p2 = self.wctx.parents() + + # this could be greatly improved + realmerge = os.environ.get('HGMERGE') + if not interactive: + os.environ['HGMERGE'] = 'merge' + + # The filemerge ancestor algorithm does not work if self.wctx + # already has two parents (in normal merge it doesn't yet). But + # this is very dirty. + self.wctx._parents.pop() + try: + # TODO: we should probably revert the file if merge fails + return merge.filemerge(self.repo, fn, fd, fo, self.wctx, p2) + finally: + self.wctx._parents.append(p2) + if realmerge: + os.environ['HGMERGE'] = realmerge + elif not interactive: + del os.environ['HGMERGE'] + + def start(self, rev=None): + _filemerge = merge.filemerge + def filemerge(repo, fw, fd, fo, wctx, mctx): + self.conflicts[fw] = (fd, fo) + + merge.filemerge = filemerge + commands.merge(self.ui, self.repo, rev=rev) + merge.filemerge = _filemerge + + self.wctx = self.repo.workingctx() + self.save() + + def resume(self): + self.load() + + dp = self.repo.dirstate.parents() + p1, p2 = self.wctx.parents() + if p1.node() != dp[0] or p2.node() != dp[1]: + raise util.Abort('imerge state does not match working directory') + + def next(self): + remaining = self.remaining() + return remaining and remaining[0] + + def resolve(self, files): + resolved = dict.fromkeys(self.resolved) + for fn in files: + if fn not in self.conflicts: + raise util.Abort('%s is not in the merge set' % fn) + resolved[fn] = True + self.resolved = resolved.keys() + self.resolved.sort() + self.save() + return 0 + + def unresolve(self, files): + resolved = dict.fromkeys(self.resolved) + for fn in files: + if fn not in resolved: + raise util.Abort('%s is not resolved' % fn) + del resolved[fn] + self.resolved = resolved.keys() + self.resolved.sort() + self.save() + return 0 + + def pickle(self, dest): + '''write current merge state to file to be resumed elsewhere''' + state = ImergeStateFile(self) + return state.save(dest) + + def unpickle(self, source): + '''read merge state from file''' + state = ImergeStateFile(self) + return state.load(source) + +def load(im, source): + if im.merging(): + raise util.Abort('there is already a merge in progress ' + '(update -C to abort it)' ) + m, a, r, d = im.repo.status()[:4] + if m or a or r or d: + raise util.Abort('working directory has uncommitted changes') + + rc = im.unpickle(source) + if not rc: + status(im) + return rc + +def merge_(im, filename=None, auto=False): + success = True + if auto and not filename: + for fn in im.remaining(): + rc = im.filemerge(fn, interactive=False) + if rc: + success = False + else: + im.resolve([fn]) + if success: + im.ui.write('all conflicts resolved\n') + else: + status(im) + return 0 + + if not filename: + filename = im.next() + if not filename: + im.ui.write('all conflicts resolved\n') + return 0 + + rc = im.filemerge(filename, interactive=not auto) + if not rc: + im.resolve([filename]) + if not im.next(): + im.ui.write('all conflicts resolved\n') + return rc + +def next(im): + n = im.next() + if n: + im.ui.write('%s\n' % n) + else: + im.ui.write('all conflicts resolved\n') + return 0 + +def resolve(im, *files): + if not files: + raise util.Abort('resolve requires at least one filename') + return im.resolve(files) + +def save(im, dest): + return im.pickle(dest) + +def status(im, **opts): + if not opts.get('resolved') and not opts.get('unresolved'): + opts['resolved'] = True + opts['unresolved'] = True + + if im.ui.verbose: + p1, p2 = [short(p.node()) for p in im.wctx.parents()] + im.ui.note(_('merging %s and %s\n') % (p1, p2)) + + conflicts = im.conflicts.keys() + conflicts.sort() + remaining = dict.fromkeys(im.remaining()) + st = [] + for fn in conflicts: + if opts.get('no_status'): + mode = '' + elif fn in remaining: + mode = 'U ' + else: + mode = 'R ' + if ((opts.get('resolved') and fn not in remaining) + or (opts.get('unresolved') and fn in remaining)): + st.append((mode, fn)) + st.sort() + for (mode, fn) in st: + if im.ui.verbose: + fo, fd = im.conflicts[fn] + if fd != fn: + fn = '%s (%s)' % (fn, fd) + im.ui.write('%s%s\n' % (mode, fn)) + if opts.get('unresolved') and not remaining: + im.ui.write(_('all conflicts resolved\n')) + + return 0 + +def unresolve(im, *files): + if not files: + raise util.Abort('unresolve requires at least one filename') + return im.unresolve(files) + +subcmdtable = { + 'load': (load, []), + 'merge': + (merge_, + [('a', 'auto', None, _('automatically resolve if possible'))]), + 'next': (next, []), + 'resolve': (resolve, []), + 'save': (save, []), + 'status': + (status, + [('n', 'no-status', None, _('hide status prefix')), + ('', 'resolved', None, _('only show resolved conflicts')), + ('', 'unresolved', None, _('only show unresolved conflicts'))]), + 'unresolve': (unresolve, []) +} + +def dispatch_(im, args, opts): + def complete(s, choices): + candidates = [] + for choice in choices: + if choice.startswith(s): + candidates.append(choice) + return candidates + + c, args = args[0], list(args[1:]) + cmd = complete(c, subcmdtable.keys()) + if not cmd: + raise cmdutil.UnknownCommand('imerge ' + c) + if len(cmd) > 1: + cmd.sort() + raise cmdutil.AmbiguousCommand('imerge ' + c, cmd) + cmd = cmd[0] + + func, optlist = subcmdtable[cmd] + opts = {} + try: + args = fancyopts.fancyopts(args, optlist, opts) + return func(im, *args, **opts) + except fancyopts.getopt.GetoptError, inst: + raise dispatch.ParseError('imerge', '%s: %s' % (cmd, inst)) + except TypeError: + raise dispatch.ParseError('imerge', _('%s: invalid arguments') % cmd) + +def imerge(ui, repo, *args, **opts): + '''interactive merge + + imerge lets you split a merge into pieces. When you start a merge + with imerge, the names of all files with conflicts are recorded. + You can then merge any of these files, and if the merge is + successful, they will be marked as resolved. When all files are + resolved, the merge is complete. + + If no merge is in progress, hg imerge [rev] will merge the working + directory with rev (defaulting to the other head if the repository + only has two heads). You may also resume a saved merge with + hg imerge load . + + If a merge is in progress, hg imerge will default to merging the + next unresolved file. + + The following subcommands are available: + + status: + show the current state of the merge + options: + -n --no-status: do not print the status prefix + --resolved: only print resolved conflicts + --unresolved: only print unresolved conflicts + next: + show the next unresolved file merge + merge []: + merge . If the file merge is successful, the file will be + recorded as resolved. If no file is given, the next unresolved + file will be merged. + resolve ...: + mark files as successfully merged + unresolve ...: + mark files as requiring merging. + save : + save the state of the merge to a file to be resumed elsewhere + load : + load the state of the merge from a file created by save + ''' + + im = Imerge(ui, repo) + + if im.merging(): + im.resume() + else: + rev = opts.get('rev') + if rev and args: + raise util.Abort('please specify just one revision') + + if len(args) == 2 and args[0] == 'load': + pass + else: + if args: + rev = args[0] + im.start(rev=rev) + if opts.get('auto'): + args = ['merge', '--auto'] + else: + args = ['status'] + + if not args: + args = ['merge'] + + return dispatch_(im, args, opts) + +cmdtable = { + '^imerge': + (imerge, + [('r', 'rev', '', _('revision to merge')), + ('a', 'auto', None, _('automatically merge where possible'))], + 'hg imerge [command]') +} diff --git a/hgext/interhg.py b/hgext/interhg.py new file mode 100644 --- /dev/null +++ b/hgext/interhg.py @@ -0,0 +1,83 @@ +# interhg.py - interhg +# +# Copyright 2007 OHASHI Hideya +# +# Contributor(s): +# Edward Lee +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +# +# The `interhg' Mercurial extension allows you to change changelog and +# summary text just like InterWiki way. +# +# To enable this extension: +# +# [extensions] +# interhg = +# +# These are some example patterns (link to bug tracking, etc.) +# +# [interhg] +# issues = s!issue(\d+)!issue\1<\/a>! +# bugzilla = s!((?:bug|b=|(?=#?\d{4,}))(?:\s*#?)(\d+))!\1!i +# boldify = s/(^|\s)#(\d+)\b/ #\2<\/b>/ +# +# Add any number of names and patterns to match + +import re +from mercurial.hgweb import hgweb_mod +from mercurial import templater + +orig_escape = templater.common_filters["escape"] + +interhg_table = [] + +def interhg_escape(x): + escstr = orig_escape(x) + for regexp, format in interhg_table: + escstr = regexp.sub(format, escstr) + return escstr + +templater.common_filters["escape"] = interhg_escape + +orig_refresh = hgweb_mod.hgweb.refresh + +def interhg_refresh(self): + interhg_table[:] = [] + for key, pattern in self.repo.ui.configitems('interhg'): + # grab the delimiter from the character after the "s" + unesc = pattern[1] + delim = re.escape(unesc) + + # identify portions of the pattern, taking care to avoid escaped + # delimiters. the replace format and flags are optional, but delimiters + # are required. + match = re.match(r'^s%s(.+)(?:(?<=\\\\)|(? 0: - self.check_toppatch(repo) - if not patch: - patch = self.series[start] - end = start + 1 - else: - end = self.series.index(patch, start) + 1 - s = self.series[start:end] - all_files = {} - try: - if mergeq: - ret = self.mergepatch(repo, mergeq, s, wlock) + self.applied_dirty = 1; + start = self.series_end() + if start > 0: + self.check_toppatch(repo) + if not patch: + patch = self.series[start] + end = start + 1 else: - ret = self.apply(repo, s, list, wlock=wlock, - all_files=all_files) - except: - self.ui.warn(_('cleaning up working directory...')) - node = repo.dirstate.parents()[0] - hg.revert(repo, node, None, wlock) - unknown = repo.status(wlock=wlock)[4] - # only remove unknown files that we know we touched or - # created while patching - for f in unknown: - if f in all_files: - util.unlink(repo.wjoin(f)) - self.ui.warn(_('done\n')) - raise - top = self.applied[-1].name - if ret[0]: - self.ui.write("Errors during apply, please fix and refresh %s\n" % - top) - else: - self.ui.write("Now at: %s\n" % top) - return ret[0] + end = self.series.index(patch, start) + 1 + s = self.series[start:end] + all_files = {} + try: + if mergeq: + ret = self.mergepatch(repo, mergeq, s) + else: + ret = self.apply(repo, s, list, all_files=all_files) + except: + self.ui.warn(_('cleaning up working directory...')) + node = repo.dirstate.parents()[0] + hg.revert(repo, node, None) + unknown = repo.status()[4] + # only remove unknown files that we know we touched or + # created while patching + for f in unknown: + if f in all_files: + util.unlink(repo.wjoin(f)) + self.ui.warn(_('done\n')) + raise + top = self.applied[-1].name + if ret[0]: + self.ui.write( + "Errors during apply, please fix and refresh %s\n" % top) + else: + self.ui.write("Now at: %s\n" % top) + return ret[0] + finally: + del wlock - def pop(self, repo, patch=None, force=False, update=True, all=False, - wlock=None): - def getfile(f, rev): + def pop(self, repo, patch=None, force=False, update=True, all=False): + def getfile(f, rev, flags): t = repo.file(f).read(rev) - repo.wfile(f, "w").write(t) + repo.wwrite(f, t, flags) - if not wlock: - wlock = repo.wlock() - if patch: - # index, rev, patch - info = self.isapplied(patch) - if not info: - patch = self.lookup(patch) - info = self.isapplied(patch) - if not info: - raise util.Abort(_("patch %s is not applied") % patch) + wlock = repo.wlock() + try: + if patch: + # index, rev, patch + info = self.isapplied(patch) + if not info: + patch = self.lookup(patch) + info = self.isapplied(patch) + if not info: + raise util.Abort(_("patch %s is not applied") % patch) - if len(self.applied) == 0: - # Allow qpop -a to work repeatedly, - # but not qpop without an argument - self.ui.warn(_("no patches applied\n")) - return not all + if len(self.applied) == 0: + # Allow qpop -a to work repeatedly, + # but not qpop without an argument + self.ui.warn(_("no patches applied\n")) + return not all - if not update: - parents = repo.dirstate.parents() - 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") - update = True + if not update: + parents = repo.dirstate.parents() + 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") + update = True - if not force and update: - self.check_localchanges(repo) + if not force and update: + self.check_localchanges(repo) - self.applied_dirty = 1; - end = len(self.applied) - if not patch: - if all: - popi = 0 + self.applied_dirty = 1; + end = len(self.applied) + if not patch: + if all: + popi = 0 + else: + popi = len(self.applied) - 1 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] + 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]) + start = info[0] + rev = revlog.bin(info[1]) - # we know there are no local changes, so we can make a simplified - # form of hg.update. - if update: - top = self.check_toppatch(repo) - qp = self.qparents(repo, rev) - changes = repo.changelog.read(qp) - mmap = repo.manifest.read(changes[0]) - m, a, r, d, u = repo.status(qp, top)[:5] - if d: - raise util.Abort("deletions found between repo revs") - for f in m: - getfile(f, mmap[f]) - for f in r: - getfile(f, mmap[f]) - 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 OSError, e: - if e.errno != errno.ENOENT: - raise - try: os.removedirs(os.path.dirname(repo.wjoin(f))) - except: pass - if a: - repo.dirstate.forget(a) - repo.dirstate.setparents(qp, revlog.nullid) - 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].name) - else: - self.ui.write("Patch queue now empty\n") + # we know there are no local changes, so we can make a simplified + # form of hg.update. + if update: + top = self.check_toppatch(repo) + qp = self.qparents(repo, rev) + changes = repo.changelog.read(qp) + mmap = repo.manifest.read(changes[0]) + m, a, r, d, u = repo.status(qp, top)[:5] + if d: + raise util.Abort("deletions found between repo revs") + for f in m: + getfile(f, mmap[f], mmap.flags(f)) + for f in r: + getfile(f, mmap[f], mmap.flags(f)) + for f in m + r: + repo.dirstate.normal(f) + for f in a: + try: + os.unlink(repo.wjoin(f)) + except OSError, e: + if e.errno != errno.ENOENT: + raise + try: os.removedirs(os.path.dirname(repo.wjoin(f))) + except: pass + repo.dirstate.forget(f) + repo.dirstate.setparents(qp, revlog.nullid) + self.strip(repo, rev, update=False, backup='strip') + del self.applied[start:end] + if len(self.applied): + self.ui.write("Now at: %s\n" % self.applied[-1].name) + else: + self.ui.write("Patch queue now empty\n") + finally: + del wlock def diff(self, repo, pats, opts): top = self.check_toppatch(repo) @@ -902,177 +909,192 @@ class queue: self.ui.write("No patches applied\n") return 1 wlock = repo.wlock() - self.check_toppatch(repo) - (top, patchfn) = (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(patchfn) + try: + self.check_toppatch(repo) + (top, patchfn) = (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(patchfn) - patchf = self.opener(patchfn, 'r+') + patchf = self.opener(patchfn, 'r+') + + # if the patch was a git patch, refresh it as a git patch + for line in patchf: + if line.startswith('diff --git'): + self.diffopts().git = True + break - # if the patch was a git patch, refresh it as a git patch - for line in patchf: - if line.startswith('diff --git'): - self.diffopts().git = True - break - - msg = opts.get('msg', '').rstrip() - if msg: - if comments: - # Remove existing message. + msg = opts.get('msg', '').rstrip() + if msg and comments: + # Remove existing message, keeping the rest of the comments + # fields. + # If comments contains 'subject: ', message will prepend + # the field and a blank line. + if message: + subj = 'subject: ' + message[0].lower() + for i in xrange(len(comments)): + if subj == comments[i].lower(): + del comments[i] + message = message[2:] + break ci = 0 - subj = None for mi in xrange(len(message)): - if comments[ci].lower().startswith('subject: '): - subj = comments[ci][9:] - while message[mi] != comments[ci] and message[mi] != subj: + while message[mi] != comments[ci]: ci += 1 del comments[ci] - comments.append(msg) + if msg: + comments.append(msg) - patchf.seek(0) - patchf.truncate() + patchf.seek(0) + patchf.truncate() - if comments: - comments = "\n".join(comments) + '\n\n' - patchf.write(comments) + if comments: + comments = "\n".join(comments) + '\n\n' + patchf.write(comments) - if opts.get('git'): - self.diffopts().git = True - 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 - # optimization here. We update the dirstate in place and strip - # off the tip commit. Then just commit the current directory - # tree. We can also send repo.commit the list of files - # changed to speed up the diff - # - # in short mode, we only diff the files included in the - # patch already - # - # this should really read: - # 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.status call - # - mm, aa, dd, aa2, uu = repo.status(patchparent, tip)[:5] - changes = repo.changelog.read(tip) - man = repo.manifest.read(changes[0]) - aaa = aa[:] - if opts.get('short'): - filelist = mm + aa + dd - match = dict.fromkeys(filelist).__contains__ - else: - filelist = None - match = util.always - m, a, r, d, u = repo.status(files=filelist, match=match)[:5] + if opts.get('git'): + self.diffopts().git = True + 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 + # optimization here. We update the dirstate in place and strip + # off the tip commit. Then just commit the current directory + # tree. We can also send repo.commit the list of files + # changed to speed up the diff + # + # in short mode, we only diff the files included in the + # patch already + # + # this should really read: + # 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.status call + # + mm, aa, dd, aa2, uu = repo.status(patchparent, tip)[:5] + changes = repo.changelog.read(tip) + man = repo.manifest.read(changes[0]) + aaa = aa[:] + if opts.get('short'): + filelist = mm + aa + dd + match = dict.fromkeys(filelist).__contains__ + else: + filelist = None + match = util.always + m, a, r, d, u = repo.status(files=filelist, match=match)[: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 m: - if x not in aa: - 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)] - mm.append(x) - else: - aa.append(x) - # make sure any files deleted in the local dirstate - # are not in the add or change column of the patch - forget = [] - for x in d + r: - if x in aa: - del aa[aa.index(x)] - forget.append(x) - continue - elif x in mm: - del mm[mm.index(x)] - dd.append(x) + # 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 m: + if x not in aa: + 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)] + mm.append(x) + else: + aa.append(x) + # make sure any files deleted in the local dirstate + # are not in the add or change column of the patch + forget = [] + for x in d + r: + if x in aa: + del aa[aa.index(x)] + forget.append(x) + continue + elif x in mm: + del mm[mm.index(x)] + dd.append(x) - m = util.unique(mm) - r = util.unique(dd) - a = util.unique(aa) - c = [filter(matchfn, l) for l in (m, a, r, [], u)] - filelist = util.unique(c[0] + c[1] + c[2]) - patch.diff(repo, patchparent, files=filelist, match=matchfn, - fp=patchf, changes=c, opts=self.diffopts()) - patchf.close() + m = util.unique(mm) + r = util.unique(dd) + a = util.unique(aa) + c = [filter(matchfn, l) for l in (m, a, r, [], u)] + filelist = util.unique(c[0] + c[1] + c[2]) + patch.diff(repo, patchparent, files=filelist, match=matchfn, + fp=patchf, changes=c, opts=self.diffopts()) + patchf.close() - repo.dirstate.setparents(*cparents) - copies = {} - for dst in a: - src = repo.dirstate.copied(dst) - if src is None: - continue - copies.setdefault(src, []).append(dst) - repo.dirstate.update(a, 'a') - # remember the copies between patchparent and tip - # this may be slow, so don't do it if we're not tracking copies - if self.diffopts().git: - for dst in aaa: - f = repo.file(dst) - src = f.renamed(man[dst]) - if src: - copies[src[0]] = copies.get(dst, []) - if dst in a: - copies[src[0]].append(dst) - # we can't copy a file created by the patch itself - if dst in copies: - del copies[dst] - for src, dsts in copies.iteritems(): - for dst in dsts: - repo.dirstate.copy(src, dst) - repo.dirstate.update(r, 'r') - # if the patch excludes a modified file, mark that file with mtime=0 - # so status can see it. - mm = [] - for i in xrange(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=-1, st_size=-1) - repo.dirstate.forget(forget) + repo.dirstate.setparents(*cparents) + copies = {} + for dst in a: + src = repo.dirstate.copied(dst) + if src is not None: + copies.setdefault(src, []).append(dst) + repo.dirstate.add(dst) + # remember the copies between patchparent and tip + # this may be slow, so don't do it if we're not tracking copies + if self.diffopts().git: + for dst in aaa: + f = repo.file(dst) + src = f.renamed(man[dst]) + if src: + copies[src[0]] = copies.get(dst, []) + if dst in a: + copies[src[0]].append(dst) + # we can't copy a file created by the patch itself + if dst in copies: + del copies[dst] + for src, dsts in copies.iteritems(): + for dst in dsts: + repo.dirstate.copy(src, dst) + for f in r: + repo.dirstate.remove(f) + # if the patch excludes a modified file, mark that + # file with mtime=0 so status can see it. + mm = [] + for i in xrange(len(m)-1, -1, -1): + if not matchfn(m[i]): + mm.append(m[i]) + del m[i] + for f in m: + repo.dirstate.normal(f) + for f in mm: + repo.dirstate.normallookup(f) + for f in forget: + repo.dirstate.forget(f) - if not msg: - if not message: - message = "[mq]: %s\n" % patchfn + if not msg: + if not message: + message = "[mq]: %s\n" % patchfn + else: + message = "\n".join(message) else: - message = "\n".join(message) - else: - message = msg + message = msg - self.strip(repo, top, update=False, backup='strip', wlock=wlock) - n = repo.commit(filelist, message, changes[1], match=matchfn, - force=1, wlock=wlock) - self.applied[-1] = statusentry(revlog.hex(n), patchfn) - self.applied_dirty = 1 - self.removeundo(repo) - else: - self.printdiff(repo, patchparent, fp=patchf) - patchf.close() - added = repo.status()[1] - for a in added: - f = repo.wjoin(a) - try: - os.unlink(f) - except OSError, e: - if e.errno != errno.ENOENT: - raise - try: os.removedirs(os.path.dirname(f)) - except: pass - # forget the file copies in the dirstate - # push should readd the files later on - repo.dirstate.forget(added) - self.pop(repo, force=True, wlock=wlock) - self.push(repo, force=True, wlock=wlock) + self.strip(repo, top, update=False, + backup='strip') + n = repo.commit(filelist, message, changes[1], match=matchfn, + force=1) + self.applied[-1] = statusentry(revlog.hex(n), patchfn) + self.applied_dirty = 1 + self.removeundo(repo) + else: + self.printdiff(repo, patchparent, fp=patchf) + patchf.close() + added = repo.status()[1] + for a in added: + f = repo.wjoin(a) + try: + os.unlink(f) + except OSError, e: + if e.errno != errno.ENOENT: + raise + try: os.removedirs(os.path.dirname(f)) + except: pass + # forget the file copies in the dirstate + # push should readd the files later on + repo.dirstate.forget(a) + self.pop(repo, force=True) + self.push(repo, force=True) + finally: + del wlock def init(self, repo, create=False): if not create and os.path.isdir(self.path): @@ -1489,6 +1511,9 @@ def clone(ui, source, dest=None, **opts) Source patch repository is looked for in /.hg/patches by default. Use -p to change. + + The patch directory must be a nested mercurial repository, as + would be created by qinit -c. ''' def patchdir(repo): url = repo.url() @@ -1499,6 +1524,12 @@ def clone(ui, source, dest=None, **opts) if dest is None: dest = hg.defaultdest(source) sr = hg.repository(ui, ui.expandpath(source)) + patchespath = opts['patches'] or patchdir(sr) + try: + pr = hg.repository(ui, patchespath) + except hg.RepoError: + raise util.Abort(_('versioned patch repository not found' + ' (see qinit -c)')) qbase, destrev = None, None if sr.local(): if sr.mq.applied: @@ -1607,6 +1638,9 @@ def refresh(ui, repo, *pats, **opts): q = repo.mq message = cmdutil.logmessage(opts) if opts['edit']: + if not q.applied: + ui.write(_("No patches applied\n")) + return 1 if message: raise util.Abort(_('option "-e" incompatible with "-m" or "-l"')) patch = q.applied[-1].name @@ -1862,10 +1896,13 @@ def rename(ui, repo, patch, name=None, * 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) + try: + if r.dirstate[name] == 'r': + r.undelete([name]) + r.copy(patch, name) + r.remove([patch], False) + finally: + del wlock q.save_dirty() @@ -2107,10 +2144,8 @@ cmdtable = { ('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'))], + ] + commands.remoteopts, _('hg qclone [OPTION]... SOURCE [DEST]')), "qcommit|qci": (commit, @@ -2119,8 +2154,7 @@ cmdtable = { "^qdiff": (diff, [('g', 'git', None, _('use git extended diff format')), - ('I', 'include', [], _('include names matching the given patterns')), - ('X', 'exclude', [], _('exclude names matching the given patterns'))], + ] + commands.walkopts, _('hg qdiff [-I] [-X] [-g] [FILE]...')), "qdelete|qremove|qrm": (delete, @@ -2159,9 +2193,8 @@ cmdtable = { (new, [('e', 'edit', None, _('edit commit message')), ('f', 'force', None, _('import uncommitted changes into patch')), - ('I', 'include', [], _('include names matching the given patterns')), - ('X', 'exclude', [], _('exclude names matching the given patterns')), - ] + commands.commitopts, + ('g', 'git', None, _('use git extended diff format')), + ] + commands.walkopts + commands.commitopts, _('hg qnew [-e] [-m TEXT] [-l FILE] [-f] PATCH [FILE]...')), "qnext": (next, [] + seriesopts, _('hg qnext [-s]')), "qprev": (prev, [] + seriesopts, _('hg qprev [-s]')), @@ -2184,9 +2217,7 @@ cmdtable = { [('e', 'edit', None, _('edit commit message')), ('g', 'git', None, _('use git extended diff format')), ('s', 'short', None, _('refresh only files already in the patch')), - ('I', 'include', [], _('include names matching the given patterns')), - ('X', 'exclude', [], _('exclude names matching the given patterns')), - ] + commands.commitopts, + ] + commands.walkopts + commands.commitopts, _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...')), 'qrename|qmv': (rename, [], _('hg qrename PATCH1 [PATCH2]')), diff --git a/hgext/parentrevspec.py b/hgext/parentrevspec.py new file mode 100644 --- /dev/null +++ b/hgext/parentrevspec.py @@ -0,0 +1,96 @@ +# Mercurial extension to make it easy to refer to the parent of a revision +# +# Copyright (C) 2007 Alexis S. L. Carvalho +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +'''\ +use suffixes to refer to ancestor revisions + +This extension allows you to use git-style suffixes to refer to +the ancestors of a specific revision. + +For example, if you can refer to a revision as "foo", then: + +- foo^N = Nth parent of foo: + foo^0 = foo + foo^1 = first parent of foo + foo^2 = second parent of foo + foo^ = foo^1 + +- foo~N = Nth first grandparent of foo + foo~0 = foo + foo~1 = foo^1 = foo^ = first parent of foo + foo~2 = foo^1^1 = foo^^ = first parent of first parent of foo +''' +import mercurial.repo + +def reposetup(ui, repo): + if not repo.local(): + return + + class parentrevspecrepo(repo.__class__): + def lookup(self, key): + try: + _super = super(parentrevspecrepo, self) + return _super.lookup(key) + except mercurial.repo.RepoError: + pass + + circ = key.find('^') + tilde = key.find('~') + if circ < 0 and tilde < 0: + raise + elif circ >= 0 and tilde >= 0: + end = min(circ, tilde) + else: + end = max(circ, tilde) + + cl = self.changelog + base = key[:end] + try: + node = _super.lookup(base) + except mercurial.repo.RepoError: + # eek - reraise the first error + return _super.lookup(key) + + rev = cl.rev(node) + suffix = key[end:] + i = 0 + while i < len(suffix): + # foo^N => Nth parent of foo + # foo^0 == foo + # foo^1 == foo^ == 1st parent of foo + # foo^2 == 2nd parent of foo + if suffix[i] == '^': + j = i + 1 + p = cl.parentrevs(rev) + if j < len(suffix) and suffix[j].isdigit(): + j += 1 + n = int(suffix[i+1:j]) + if n > 2 or n == 2 and p[1] == -1: + raise + else: + n = 1 + if n: + rev = p[n - 1] + i = j + # foo~N => Nth first grandparent of foo + # foo~0 = foo + # foo~1 = foo^1 == foo^ == 1st parent of foo + # foo~2 = foo^1^1 == foo^^ == 1st parent of 1st parent of foo + elif suffix[i] == '~': + j = i + 1 + while j < len(suffix) and suffix[j].isdigit(): + j += 1 + if j == i + 1: + raise + n = int(suffix[i+1:j]) + for k in xrange(n): + rev = cl.parentrevs(rev)[0] + i = j + else: + raise + return cl.node(rev) + + repo.__class__ = parentrevspecrepo diff --git a/hgext/patchbomb.py b/hgext/patchbomb.py --- a/hgext/patchbomb.py +++ b/hgext/patchbomb.py @@ -306,8 +306,12 @@ def patchbomb(ui, repo, *revs, **opts): d = cdiffstat(_('Final summary:\n'), jumbo) if d: body = '\n' + d - ui.write(_('\nWrite the introductory message for the patch series.\n\n')) - body = ui.edit(body, sender) + if opts['desc']: + body = open(opts['desc']).read() + else: + ui.write(_('\nWrite the introductory message for the ' + 'patch series.\n\n')) + body = ui.edit(body, sender) msg = email.MIMEText.MIMEText(body) msg['Subject'] = subj @@ -417,6 +421,7 @@ cmdtable = { ('c', 'cc', [], _('email addresses of copy recipients')), ('d', 'diffstat', None, _('add diffstat output to messages')), ('', 'date', '', _('use the given date as the sending date')), + ('', 'desc', '', _('use the given file as the series description')), ('g', 'git', None, _('use git extended diff format')), ('f', 'from', '', _('email address of sender')), ('', 'plain', None, _('omit hg patch header')), diff --git a/hgext/purge.py b/hgext/purge.py --- a/hgext/purge.py +++ b/hgext/purge.py @@ -27,7 +27,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -from mercurial import hg, util +from mercurial import hg, util, commands from mercurial.i18n import _ import os @@ -162,7 +162,6 @@ cmdtable = { ('p', 'print', None, _('print the file names instead of deleting them')), ('0', 'print0', None, _('end filenames with NUL, for use with xargs' ' (implies -p)')), - ('I', 'include', [], _('include names matching the given patterns')), - ('X', 'exclude', [], _('exclude names matching the given patterns'))], + ] + commands.walkopts, _('hg purge [OPTION]... [DIR]...')) } diff --git a/hgext/record.py b/hgext/record.py new file mode 100644 --- /dev/null +++ b/hgext/record.py @@ -0,0 +1,415 @@ +# record.py +# +# Copyright 2007 Bryan O'Sullivan +# +# This software may be used and distributed according to the terms of +# the GNU General Public License, incorporated herein by reference. + +'''interactive change selection during commit''' + +from mercurial.i18n import _ +from mercurial import cmdutil, commands, cmdutil, hg, mdiff, patch, revlog +from mercurial import util +import copy, cStringIO, errno, operator, os, re, shutil, tempfile + +lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)') + +def scanpatch(fp): + lr = patch.linereader(fp) + + def scanwhile(first, p): + lines = [first] + while True: + line = lr.readline() + if not line: + break + if p(line): + lines.append(line) + else: + lr.push(line) + break + return lines + + while True: + line = lr.readline() + if not line: + break + if line.startswith('diff --git a/'): + def notheader(line): + s = line.split(None, 1) + return not s or s[0] not in ('---', 'diff') + header = scanwhile(line, notheader) + fromfile = lr.readline() + if fromfile.startswith('---'): + tofile = lr.readline() + header += [fromfile, tofile] + else: + lr.push(fromfile) + yield 'file', header + elif line[0] == ' ': + yield 'context', scanwhile(line, lambda l: l[0] in ' \\') + elif line[0] in '-+': + yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\') + else: + m = lines_re.match(line) + if m: + yield 'range', m.groups() + else: + raise patch.PatchError('unknown patch content: %r' % line) + +class header(object): + diff_re = re.compile('diff --git a/(.*) b/(.*)$') + allhunks_re = re.compile('(?:index|new file|deleted file) ') + pretty_re = re.compile('(?:new file|deleted file) ') + special_re = re.compile('(?:index|new|deleted|copy|rename) ') + + def __init__(self, header): + self.header = header + self.hunks = [] + + def binary(self): + for h in self.header: + if h.startswith('index '): + return True + + def pretty(self, fp): + for h in self.header: + if h.startswith('index '): + fp.write(_('this modifies a binary file (all or nothing)\n')) + break + if self.pretty_re.match(h): + fp.write(h) + if self.binary(): + fp.write(_('this is a binary file\n')) + break + if h.startswith('---'): + fp.write(_('%d hunks, %d lines changed\n') % + (len(self.hunks), + sum([h.added + h.removed for h in self.hunks]))) + break + fp.write(h) + + def write(self, fp): + fp.write(''.join(self.header)) + + def allhunks(self): + for h in self.header: + if self.allhunks_re.match(h): + return True + + def files(self): + fromfile, tofile = self.diff_re.match(self.header[0]).groups() + if fromfile == tofile: + return [fromfile] + return [fromfile, tofile] + + def filename(self): + return self.files()[-1] + + def __repr__(self): + return '
' % (' '.join(map(repr, self.files()))) + + def special(self): + for h in self.header: + if self.special_re.match(h): + return True + +def countchanges(hunk): + add = len([h for h in hunk if h[0] == '+']) + rem = len([h for h in hunk if h[0] == '-']) + return add, rem + +class hunk(object): + maxcontext = 3 + + def __init__(self, header, fromline, toline, proc, before, hunk, after): + def trimcontext(number, lines): + delta = len(lines) - self.maxcontext + if False and delta > 0: + return number + delta, lines[:self.maxcontext] + return number, lines + + self.header = header + self.fromline, self.before = trimcontext(fromline, before) + self.toline, self.after = trimcontext(toline, after) + self.proc = proc + self.hunk = hunk + self.added, self.removed = countchanges(self.hunk) + + def write(self, fp): + delta = len(self.before) + len(self.after) + fromlen = delta + self.removed + tolen = delta + self.added + fp.write('@@ -%d,%d +%d,%d @@%s\n' % + (self.fromline, fromlen, self.toline, tolen, + self.proc and (' ' + self.proc))) + fp.write(''.join(self.before + self.hunk + self.after)) + + pretty = write + + def filename(self): + return self.header.filename() + + def __repr__(self): + return '' % (self.filename(), self.fromline) + +def parsepatch(fp): + class parser(object): + def __init__(self): + self.fromline = 0 + self.toline = 0 + self.proc = '' + self.header = None + self.context = [] + self.before = [] + self.hunk = [] + self.stream = [] + + def addrange(self, (fromstart, fromend, tostart, toend, proc)): + self.fromline = int(fromstart) + self.toline = int(tostart) + self.proc = proc + + def addcontext(self, context): + if self.hunk: + h = hunk(self.header, self.fromline, self.toline, self.proc, + self.before, self.hunk, context) + self.header.hunks.append(h) + self.stream.append(h) + self.fromline += len(self.before) + h.removed + self.toline += len(self.before) + h.added + self.before = [] + self.hunk = [] + self.proc = '' + self.context = context + + def addhunk(self, hunk): + if self.context: + self.before = self.context + self.context = [] + self.hunk = data + + def newfile(self, hdr): + self.addcontext([]) + h = header(hdr) + self.stream.append(h) + self.header = h + + def finished(self): + self.addcontext([]) + return self.stream + + transitions = { + 'file': {'context': addcontext, + 'file': newfile, + 'hunk': addhunk, + 'range': addrange}, + 'context': {'file': newfile, + 'hunk': addhunk, + 'range': addrange}, + 'hunk': {'context': addcontext, + 'file': newfile, + 'range': addrange}, + 'range': {'context': addcontext, + 'hunk': addhunk}, + } + + p = parser() + + state = 'context' + for newstate, data in scanpatch(fp): + try: + p.transitions[state][newstate](p, data) + except KeyError: + raise patch.PatchError('unhandled transition: %s -> %s' % + (state, newstate)) + state = newstate + return p.finished() + +def filterpatch(ui, chunks): + chunks = list(chunks) + chunks.reverse() + seen = {} + def consumefile(): + consumed = [] + while chunks: + if isinstance(chunks[-1], header): + break + else: + consumed.append(chunks.pop()) + return consumed + resp_all = [None] + resp_file = [None] + applied = {} + def prompt(query): + if resp_all[0] is not None: + return resp_all[0] + if resp_file[0] is not None: + return resp_file[0] + while True: + r = (ui.prompt(query + _(' [Ynsfdaq?] '), '[Ynsfdaq?]?$', + matchflags=re.I) or 'y').lower() + if r == '?': + c = record.__doc__.find('y - record this change') + for l in record.__doc__[c:].splitlines(): + if l: ui.write(_(l.strip()), '\n') + continue + elif r == 's': + r = resp_file[0] = 'n' + elif r == 'f': + r = resp_file[0] = 'y' + elif r == 'd': + r = resp_all[0] = 'n' + elif r == 'a': + r = resp_all[0] = 'y' + elif r == 'q': + raise util.Abort(_('user quit')) + return r + while chunks: + chunk = chunks.pop() + if isinstance(chunk, header): + resp_file = [None] + fixoffset = 0 + hdr = ''.join(chunk.header) + if hdr in seen: + consumefile() + continue + seen[hdr] = True + if resp_all[0] is None: + chunk.pretty(ui) + r = prompt(_('examine changes to %s?') % + _(' and ').join(map(repr, chunk.files()))) + if r == 'y': + applied[chunk.filename()] = [chunk] + if chunk.allhunks(): + applied[chunk.filename()] += consumefile() + else: + consumefile() + else: + if resp_file[0] is None and resp_all[0] is None: + chunk.pretty(ui) + r = prompt(_('record this change to %r?') % + chunk.filename()) + if r == 'y': + if fixoffset: + chunk = copy.copy(chunk) + chunk.toline += fixoffset + applied[chunk.filename()].append(chunk) + else: + fixoffset += chunk.removed - chunk.added + return reduce(operator.add, [h for h in applied.itervalues() + if h[0].special() or len(h) > 1], []) + +def record(ui, repo, *pats, **opts): + '''interactively select changes to commit + + If a list of files is omitted, all changes reported by "hg status" + will be candidates for recording. + + You will be prompted for whether to record changes to each + modified file, and for files with multiple changes, for each + change to use. For each query, the following responses are + possible: + + y - record this change + n - skip this change + + s - skip remaining changes to this file + f - record remaining changes to this file + + d - done, skip remaining changes and files + a - record all changes to all remaining files + q - quit, recording no changes + + ? - display help''' + + if not ui.interactive: + raise util.Abort(_('running non-interactively, use commit instead')) + + def recordfunc(ui, repo, files, message, match, opts): + if files: + changes = None + else: + changes = repo.status(files=files, match=match)[:5] + modified, added, removed = changes[:3] + files = modified + added + removed + diffopts = mdiff.diffopts(git=True, nodates=True) + fp = cStringIO.StringIO() + patch.diff(repo, repo.dirstate.parents()[0], files=files, + match=match, changes=changes, opts=diffopts, fp=fp) + fp.seek(0) + + chunks = filterpatch(ui, parsepatch(fp)) + del fp + + contenders = {} + for h in chunks: + try: contenders.update(dict.fromkeys(h.files())) + except AttributeError: pass + + newfiles = [f for f in files if f in contenders] + + if not newfiles: + ui.status(_('no changes to record\n')) + return 0 + + if changes is None: + changes = repo.status(files=newfiles, match=match)[:5] + modified = dict.fromkeys(changes[0]) + + backups = {} + backupdir = repo.join('record-backups') + try: + os.mkdir(backupdir) + except OSError, err: + if err.errno != errno.EEXIST: + raise + try: + for f in newfiles: + if f not in modified: + continue + fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', + dir=backupdir) + os.close(fd) + ui.debug('backup %r as %r\n' % (f, tmpname)) + util.copyfile(repo.wjoin(f), tmpname) + backups[f] = tmpname + + fp = cStringIO.StringIO() + for c in chunks: + if c.filename() in backups: + c.write(fp) + dopatch = fp.tell() + fp.seek(0) + + if backups: + hg.revert(repo, repo.dirstate.parents()[0], backups.has_key) + + if dopatch: + ui.debug('applying patch\n') + ui.debug(fp.getvalue()) + patch.internalpatch(fp, ui, 1, repo.root) + del fp + + repo.commit(newfiles, message, opts['user'], opts['date'], match, + force_editor=opts.get('force_editor')) + return 0 + finally: + try: + for realname, tmpname in backups.iteritems(): + ui.debug('restoring %r to %r\n' % (tmpname, realname)) + util.copyfile(tmpname, repo.wjoin(realname)) + os.unlink(tmpname) + os.rmdir(backupdir) + except OSError: + pass + return cmdutil.commit(ui, repo, recordfunc, pats, opts) + +cmdtable = { + "record": + (record, + [('A', 'addremove', None, + _('mark new/missing files as added/removed before committing')), + ] + commands.walkopts + commands.commitopts + commands.commitopts2, + _('hg record [OPTION]... [FILE]...')), +} diff --git a/hgext/transplant.py b/hgext/transplant.py --- a/hgext/transplant.py +++ b/hgext/transplant.py @@ -96,9 +96,10 @@ class transplanter: diffopts = patch.diffopts(self.ui, opts) diffopts.git = True - wlock = repo.wlock() - lock = repo.lock() + lock = wlock = None try: + wlock = repo.wlock() + lock = repo.lock() for rev in revs: node = revmap[rev] revstr = '%s:%s' % (rev, revlog.short(node)) @@ -118,9 +119,8 @@ class transplanter: continue if pulls: if source != repo: - repo.pull(source, heads=pulls, lock=lock) - merge.update(repo, pulls[-1], False, False, None, - wlock=wlock) + repo.pull(source, heads=pulls) + merge.update(repo, pulls[-1], False, False, None) p1, p2 = repo.dirstate.parents() pulls = [] @@ -131,7 +131,7 @@ class transplanter: # fail. domerge = True if not hasnode(repo, node): - repo.pull(source, heads=[node], lock=lock) + repo.pull(source, heads=[node]) if parents[1] != revlog.nullid: self.ui.note(_('skipping merge changeset %s:%s\n') @@ -146,11 +146,11 @@ class transplanter: del revmap[rev] if patchfile or domerge: try: - n = self.applyone(repo, node, source.changelog.read(node), + n = self.applyone(repo, node, + source.changelog.read(node), patchfile, merge=domerge, log=opts.get('log'), - filter=opts.get('filter'), - lock=lock, wlock=wlock) + filter=opts.get('filter')) if n and domerge: self.ui.status(_('%s merged at %s\n') % (revstr, revlog.short(n))) @@ -161,11 +161,12 @@ class transplanter: if patchfile: os.unlink(patchfile) if pulls: - repo.pull(source, heads=pulls, lock=lock) - merge.update(repo, pulls[-1], False, False, None, wlock=wlock) + repo.pull(source, heads=pulls) + merge.update(repo, pulls[-1], False, False, None) finally: self.saveseries(revmap, merges) self.transplants.write() + del lock, wlock def filter(self, filter, changelog, patchfile): '''arbitrarily rewrite changeset before applying it''' @@ -193,7 +194,7 @@ class transplanter: return (user, date, msg) def applyone(self, repo, node, cl, patchfile, merge=False, log=False, - filter=None, lock=None, wlock=None): + filter=None): '''apply the patch in patchfile to the repository as a transplant''' (manifest, user, (time, timezone), files, message) = cl[:5] date = "%d %d" % (time, timezone) @@ -219,7 +220,7 @@ class transplanter: self.ui.warn(_('%s: empty changeset') % revlog.hex(node)) return None finally: - files = patch.updatedir(self.ui, repo, files, wlock=wlock) + files = patch.updatedir(self.ui, repo, files) except Exception, inst: if filter: os.unlink(patchfile) @@ -237,8 +238,7 @@ class transplanter: p1, p2 = repo.dirstate.parents() repo.dirstate.setparents(p1, node) - n = repo.commit(files, message, user, date, lock=lock, wlock=wlock, - extra=extra) + n = repo.commit(files, message, user, date, extra=extra) if not merge: self.transplants.set(n, node) @@ -272,20 +272,24 @@ class transplanter: extra = {'transplant_source': node} wlock = repo.wlock() - p1, p2 = repo.dirstate.parents() - if p1 != parents[0]: - raise util.Abort(_('working dir not at transplant parent %s') % - revlog.hex(parents[0])) - if merge: - repo.dirstate.setparents(p1, parents[1]) - n = repo.commit(None, message, user, date, wlock=wlock, extra=extra) - if not n: - raise util.Abort(_('commit failed')) - if not merge: - self.transplants.set(n, node) - self.unlog() + try: + p1, p2 = repo.dirstate.parents() + if p1 != parents[0]: + raise util.Abort( + _('working dir not at transplant parent %s') % + revlog.hex(parents[0])) + if merge: + repo.dirstate.setparents(p1, parents[1]) + n = repo.commit(None, message, user, date, extra=extra) + if not n: + raise util.Abort(_('commit failed')) + if not merge: + self.transplants.set(n, node) + self.unlog() - return n, node + return n, node + finally: + del wlock def readseries(self): nodes = [] diff --git a/hgext/win32text.py b/hgext/win32text.py --- a/hgext/win32text.py +++ b/hgext/win32text.py @@ -1,7 +1,24 @@ -import mercurial.util +from mercurial import util, ui +from mercurial.i18n import gettext as _ +import re + +# regexp for single LF without CR preceding. +re_single_lf = re.compile('(^|[^\r])\n', re.MULTILINE) def dumbdecode(s, cmd): - return s.replace('\n', '\r\n') + # warn if already has CRLF in repository. + # it might cause unexpected eol conversion. + # see issue 302: + # http://www.selenic.com/mercurial/bts/issue302 + if '\r\n' in s: + u = ui.ui() + u.warn(_('WARNING: file in repository already has CRLF line ending \n' + ' which does not need eol conversion by win32text plugin.\n' + ' Please reconsider encode/decode setting in' + ' mercurial.ini or .hg/hgrc\n' + ' before next commit.\n')) + # replace single LF to CRLF + return re_single_lf.sub('\\1\r\n', s) def dumbencode(s, cmd): return s.replace('\r\n', '\n') @@ -20,7 +37,7 @@ def cleverencode(s, cmd): return dumbencode(s, cmd) return s -mercurial.util.filtertable.update({ +util.filtertable.update({ 'dumbdecode:': dumbdecode, 'dumbencode:': dumbencode, 'cleverdecode:': cleverdecode, diff --git a/hgmerge b/hgmerge --- a/hgmerge +++ b/hgmerge @@ -96,6 +96,20 @@ ask_if_merged() { done } +# Check if conflict markers are present and ask if the merge was successful +conflicts_or_success() { + while egrep '^(<<<<<<< .*|=======|>>>>>>> .*)$' "$LOCAL" >/dev/null; do + echo "$LOCAL contains conflict markers." + echo "Keep this version? [y/n]" + read answer + case "$answer" in + y*|Y*) success;; + n*|N*) failure;; + esac + done + success +} + # Clean up when interrupted trap "failure" 1 2 3 6 15 # HUP INT QUIT ABRT TERM @@ -123,20 +137,20 @@ if [ -n "$FILEMERGE" ]; then # filemerge prefers the right by default $FILEMERGE -left "$OTHER" -right "$LOCAL" -ancestor "$BASE" -merge "$LOCAL" [ $? -ne 0 ] && echo "FileMerge failed to launch" && failure - $TEST "$LOCAL" -nt "$CHGTEST" && success || ask_if_merged + $TEST "$LOCAL" -nt "$CHGTEST" && conflicts_or_success || ask_if_merged fi if [ -n "$DISPLAY" ]; then # try using kdiff3, which is fairly nice if [ -n "$KDIFF3" ]; then $KDIFF3 --auto "$BASE" "$BACKUP" "$OTHER" -o "$LOCAL" || failure - success + conflicts_or_success fi # try using tkdiff, which is a bit less sophisticated if [ -n "$TKDIFF" ]; then $TKDIFF "$BACKUP" "$OTHER" -a "$BASE" -o "$LOCAL" || failure - success + conflicts_or_success fi if [ -n "$MELD" ]; then @@ -147,7 +161,7 @@ if [ -n "$DISPLAY" ]; then # use the file with conflicts $MELD "$LOCAL.tmp.$RAND" "$LOCAL" "$OTHER" || failure # Also it doesn't return good error code - $TEST "$LOCAL" -nt "$CHGTEST" && success || ask_if_merged + $TEST "$LOCAL" -nt "$CHGTEST" && conflicts_or_success || ask_if_merged fi fi @@ -155,10 +169,17 @@ fi if [ -n "$MERGE" -o -n "$DIFF3" ]; then echo "conflicts detected in $LOCAL" cp "$BACKUP" "$CHGTEST" - $EDITOR "$LOCAL" || failure + case "$EDITOR" in + "emacs") + $EDITOR "$LOCAL" --eval '(condition-case nil (smerge-mode 1) (error nil))' || failure + ;; + *) + $EDITOR "$LOCAL" || failure + ;; + esac # Some editors do not return meaningful error codes # Do not take any chances - $TEST "$LOCAL" -nt "$CHGTEST" && success || ask_if_merged + $TEST "$LOCAL" -nt "$CHGTEST" && conflicts_or_success || ask_if_merged fi # attempt to manually merge with diff and patch diff --git a/hgweb.cgi b/hgweb.cgi --- a/hgweb.cgi +++ b/hgweb.cgi @@ -2,14 +2,17 @@ # # An example CGI script to use hgweb, edit as necessary +# adjust python path if not a system-wide install: +#import sys +#sys.path.insert(0, "/path/to/python/lib") + +# enable importing on demand to reduce startup time +from mercurial import demandimport; demandimport.enable() + # send python tracebacks to the browser if an error occurs: import cgitb cgitb.enable() -# adjust python path if not a system-wide install: -#import sys -#sys.path.insert(0, "/path/to/python/lib") - # If you'd like to serve pages with UTF-8 instead of your default # locale charset, you can do so by uncommenting the following lines. # Note that this will cause your .hgrc files to be interpreted in diff --git a/hgwebdir.cgi b/hgwebdir.cgi --- a/hgwebdir.cgi +++ b/hgwebdir.cgi @@ -2,14 +2,17 @@ # # An example CGI script to export multiple hgweb repos, edit as necessary +# adjust python path if not a system-wide install: +#import sys +#sys.path.insert(0, "/path/to/python/lib") + +# enable importing on demand to reduce startup time +from mercurial import demandimport; demandimport.enable() + # send python tracebacks to the browser if an error occurs: import cgitb cgitb.enable() -# adjust python path if not a system-wide install: -#import sys -#sys.path.insert(0, "/path/to/python/lib") - # If you'd like to serve pages with UTF-8 instead of your default # locale charset, you can do so by uncommenting the following lines. # Note that this will cause your .hgrc files to be interpreted in diff --git a/mercurial/bundlerepo.py b/mercurial/bundlerepo.py --- a/mercurial/bundlerepo.py +++ b/mercurial/bundlerepo.py @@ -12,8 +12,7 @@ of the GNU General Public License, incor from node import * from i18n import _ -import changegroup, util, os, struct, bz2, tempfile - +import changegroup, util, os, struct, bz2, tempfile, mdiff import localrepo, changelog, manifest, filelog, revlog class bundlerevlog(revlog.revlog): @@ -57,14 +56,11 @@ class bundlerevlog(revlog.revlog): if not prev: prev = p1 - # start, size, base is not used, link, p1, p2, delta ref - if self.version == revlog.REVLOGV0: - e = (start, size, None, link, p1, p2, node) - else: - e = (self.offset_type(start, 0), size, -1, None, link, - self.rev(p1), self.rev(p2), node) + # start, size, full unc. size, base (unused), link, p1, p2, node + e = (revlog.offset_type(start, 0), size, -1, -1, link, + self.rev(p1), self.rev(p2), node) self.basemap[n] = prev - self.index.append(e) + self.index.insert(-1, e) self.nodemap[node] = n prev = node n += 1 @@ -80,7 +76,7 @@ class bundlerevlog(revlog.revlog): # not against rev - 1 # XXX: could use some caching if not self.bundle(rev): - return revlog.revlog.chunk(self, rev, df, cachelen) + return revlog.revlog.chunk(self, rev, df) self.bundlefile.seek(self.start(rev)) return self.bundlefile.read(self.length(rev)) @@ -94,7 +90,7 @@ class bundlerevlog(revlog.revlog): elif not self.bundle(rev1) and not self.bundle(rev2): return revlog.revlog.revdiff(self, rev1, rev2) - return self.diff(self.revision(self.node(rev1)), + return mdiff.textdiff(self.revision(self.node(rev1)), self.revision(self.node(rev2))) def revision(self, node): @@ -107,8 +103,8 @@ class bundlerevlog(revlog.revlog): rev = self.rev(iter_node) # reconstruct the revision if it is from a changegroup while self.bundle(rev): - if self.cache and self.cache[0] == iter_node: - text = self.cache[2] + if self._cache and self._cache[0] == iter_node: + text = self._cache[2] break chain.append(rev) iter_node = self.bundlebase(rev) @@ -118,14 +114,14 @@ class bundlerevlog(revlog.revlog): while chain: delta = self.chunk(chain.pop()) - text = self.patches(text, [delta]) + text = mdiff.patches(text, [delta]) p1, p2 = self.parents(node) if node != revlog.hash(text, p1, p2): raise revlog.RevlogError(_("integrity check failed on %s:%d") % (self.datafile, self.rev(node))) - self.cache = (node, self.rev(node), text) + self._cache = (node, self.rev(node), text) return text def addrevision(self, text, transaction, link, p1=None, p2=None, d=None): @@ -197,18 +193,27 @@ class bundlerepository(localrepo.localre else: raise util.Abort(_("%s: unknown bundle compression type") % bundlename) - self.changelog = bundlechangelog(self.sopener, self.bundlefile) - self.manifest = bundlemanifest(self.sopener, self.bundlefile, - self.changelog.rev) # dict with the mapping 'filename' -> position in the bundle self.bundlefilespos = {} - while 1: - f = changegroup.getchunk(self.bundlefile) - if not f: - break - self.bundlefilespos[f] = self.bundlefile.tell() - for c in changegroup.chunkiter(self.bundlefile): - pass + + def __getattr__(self, name): + if name == 'changelog': + self.changelog = bundlechangelog(self.sopener, self.bundlefile) + self.manstart = self.bundlefile.tell() + return self.changelog + if name == 'manifest': + self.bundlefile.seek(self.manstart) + self.manifest = bundlemanifest(self.sopener, self.bundlefile, + self.changelog.rev) + self.filestart = self.bundlefile.tell() + return self.manifest + if name == 'manstart': + self.changelog + return self.manstart + if name == 'filestart': + self.manifest + return self.filestart + return localrepo.localrepository.__getattr__(self, name) def url(self): return self._url @@ -217,6 +222,16 @@ class bundlerepository(localrepo.localre return -1 def file(self, f): + if not self.bundlefilespos: + self.bundlefile.seek(self.filestart) + while 1: + chunk = changegroup.getchunk(self.bundlefile) + if not chunk: + break + self.bundlefilespos[chunk] = self.bundlefile.tell() + for c in changegroup.chunkiter(self.bundlefile): + pass + if f[0] == '/': f = f[1:] if f in self.bundlefilespos: diff --git a/mercurial/changelog.py b/mercurial/changelog.py --- a/mercurial/changelog.py +++ b/mercurial/changelog.py @@ -58,7 +58,6 @@ class appender: def read(self, count=-1): '''only trick here is reads that span real file and data''' ret = "" - old_offset = self.offset if self.offset < self.size: s = self.fp.read(count) ret = s @@ -131,7 +130,10 @@ class changelog(revlog): return extra def encode_extra(self, d): - items = [_string_escape(":".join(t)) for t in d.iteritems()] + # keys must be sorted to produce a deterministic changelog entry + keys = d.keys() + keys.sort() + items = [_string_escape('%s:%s' % (k, d[k])) for k in keys] return "\0".join(items) def extract(self, text): diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py --- a/mercurial/cmdutil.py +++ b/mercurial/cmdutil.py @@ -7,9 +7,8 @@ from node import * from i18n import _ -import os, sys, atexit, signal, pdb, traceback, socket, errno, shlex -import mdiff, bdiff, util, templater, patch, commands, hg, lock, time -import fancyopts, revlog, version, extensions, hook +import os, sys, bisect, stat +import mdiff, bdiff, util, templater, patch revrangesep = ':' @@ -17,130 +16,8 @@ class UnknownCommand(Exception): """Exception raised if command is not in the command table.""" class AmbiguousCommand(Exception): """Exception raised if command shortcut matches more than one command.""" -class ParseError(Exception): - """Exception raised on errors in parsing the command line.""" -def runcatch(ui, args): - def catchterm(*args): - raise util.SignalInterrupt - - for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM': - num = getattr(signal, name, None) - if num: signal.signal(num, catchterm) - - try: - try: - # enter the debugger before command execution - if '--debugger' in args: - pdb.set_trace() - try: - return dispatch(ui, args) - finally: - ui.flush() - except: - # enter the debugger when we hit an exception - if '--debugger' in args: - pdb.post_mortem(sys.exc_info()[2]) - ui.print_exc() - raise - - except ParseError, inst: - if inst.args[0]: - ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1])) - commands.help_(ui, inst.args[0]) - else: - ui.warn(_("hg: %s\n") % inst.args[1]) - commands.help_(ui, 'shortlist') - except AmbiguousCommand, inst: - ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") % - (inst.args[0], " ".join(inst.args[1]))) - except UnknownCommand, inst: - ui.warn(_("hg: unknown command '%s'\n") % inst.args[0]) - commands.help_(ui, 'shortlist') - except hg.RepoError, inst: - ui.warn(_("abort: %s!\n") % inst) - except lock.LockHeld, inst: - if inst.errno == errno.ETIMEDOUT: - reason = _('timed out waiting for lock held by %s') % inst.locker - else: - reason = _('lock held by %s') % inst.locker - ui.warn(_("abort: %s: %s\n") % (inst.desc or inst.filename, reason)) - except lock.LockUnavailable, inst: - ui.warn(_("abort: could not lock %s: %s\n") % - (inst.desc or inst.filename, inst.strerror)) - except revlog.RevlogError, inst: - ui.warn(_("abort: %s!\n") % inst) - except util.SignalInterrupt: - ui.warn(_("killed!\n")) - except KeyboardInterrupt: - try: - ui.warn(_("interrupted!\n")) - except IOError, inst: - if inst.errno == errno.EPIPE: - if ui.debugflag: - ui.warn(_("\nbroken pipe\n")) - else: - raise - except socket.error, inst: - ui.warn(_("abort: %s\n") % inst[1]) - except IOError, inst: - if hasattr(inst, "code"): - ui.warn(_("abort: %s\n") % inst) - elif hasattr(inst, "reason"): - try: # usually it is in the form (errno, strerror) - reason = inst.reason.args[1] - except: # it might be anything, for example a string - reason = inst.reason - ui.warn(_("abort: error: %s\n") % reason) - elif hasattr(inst, "args") and inst[0] == errno.EPIPE: - if ui.debugflag: - ui.warn(_("broken pipe\n")) - elif getattr(inst, "strerror", None): - if getattr(inst, "filename", None): - ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename)) - else: - ui.warn(_("abort: %s\n") % inst.strerror) - else: - raise - except OSError, inst: - if getattr(inst, "filename", None): - ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename)) - else: - ui.warn(_("abort: %s\n") % inst.strerror) - except util.UnexpectedOutput, inst: - ui.warn(_("abort: %s") % inst[0]) - if not isinstance(inst[1], basestring): - ui.warn(" %r\n" % (inst[1],)) - elif not inst[1]: - ui.warn(_(" empty string\n")) - else: - ui.warn("\n%r\n" % util.ellipsis(inst[1])) - except ImportError, inst: - m = str(inst).split()[-1] - ui.warn(_("abort: could not import module %s!\n" % m)) - if m in "mpatch bdiff".split(): - ui.warn(_("(did you forget to compile extensions?)\n")) - elif m in "zlib".split(): - ui.warn(_("(is your Python install correct?)\n")) - - except util.Abort, inst: - ui.warn(_("abort: %s\n") % inst) - except SystemExit, inst: - # Commands shouldn't sys.exit directly, but give a return code. - # Just in case catch this and and pass exit code to caller. - return inst.code - except: - ui.warn(_("** unknown exception encountered, details follow\n")) - ui.warn(_("** report bug details to " - "http://www.selenic.com/mercurial/bts\n")) - ui.warn(_("** or mercurial@selenic.com\n")) - ui.warn(_("** Mercurial Distributed SCM (version %s)\n") - % version.get_version()) - raise - - return -1 - -def findpossible(ui, cmd): +def findpossible(ui, cmd, table): """ Return cmd -> (aliases, command table entry) for each matching command. @@ -148,7 +25,7 @@ def findpossible(ui, cmd): """ choice = {} debugchoice = {} - for e in commands.table.keys(): + for e in table.keys(): aliases = e.lstrip("^").split("|") found = None if cmd in aliases: @@ -160,18 +37,18 @@ def findpossible(ui, cmd): break if found is not None: if aliases[0].startswith("debug") or found.startswith("debug"): - debugchoice[found] = (aliases, commands.table[e]) + debugchoice[found] = (aliases, table[e]) else: - choice[found] = (aliases, commands.table[e]) + choice[found] = (aliases, table[e]) if not choice and debugchoice: choice = debugchoice return choice -def findcmd(ui, cmd): +def findcmd(ui, cmd, table): """Return (aliases, command table entry) for command string.""" - choice = findpossible(ui, cmd) + choice = findpossible(ui, cmd, table) if choice.has_key(cmd): return choice[cmd] @@ -186,247 +63,6 @@ def findcmd(ui, cmd): raise UnknownCommand(cmd) -def findrepo(): - p = os.getcwd() - while not os.path.isdir(os.path.join(p, ".hg")): - oldp, p = p, os.path.dirname(p) - if p == oldp: - return None - - return p - -def parse(ui, args): - options = {} - cmdoptions = {} - - try: - args = fancyopts.fancyopts(args, commands.globalopts, options) - except fancyopts.getopt.GetoptError, inst: - raise ParseError(None, inst) - - if args: - cmd, args = args[0], args[1:] - aliases, i = findcmd(ui, cmd) - cmd = aliases[0] - defaults = ui.config("defaults", cmd) - if defaults: - args = shlex.split(defaults) + args - c = list(i[1]) - else: - cmd = None - c = [] - - # combine global options into local - for o in commands.globalopts: - c.append((o[0], o[1], options[o[1]], o[3])) - - try: - args = fancyopts.fancyopts(args, c, cmdoptions) - except fancyopts.getopt.GetoptError, inst: - raise ParseError(cmd, inst) - - # separate global options back out - for o in commands.globalopts: - n = o[1] - options[n] = cmdoptions[n] - del cmdoptions[n] - - return (cmd, cmd and i[0] or None, args, options, cmdoptions) - -def parseconfig(config): - """parse the --config options from the command line""" - parsed = [] - for cfg in config: - try: - name, value = cfg.split('=', 1) - section, name = name.split('.', 1) - if not section or not name: - raise IndexError - parsed.append((section, name, value)) - except (IndexError, ValueError): - raise util.Abort(_('malformed --config option: %s') % cfg) - return parsed - -def earlygetopt(aliases, args): - """Return list of values for an option (or aliases). - - The values are listed in the order they appear in args. - The options and values are removed from args. - """ - try: - argcount = args.index("--") - except ValueError: - argcount = len(args) - shortopts = [opt for opt in aliases if len(opt) == 2] - values = [] - pos = 0 - while pos < argcount: - if args[pos] in aliases: - if pos + 1 >= argcount: - # ignore and let getopt report an error if there is no value - break - del args[pos] - values.append(args.pop(pos)) - argcount -= 2 - elif args[pos][:2] in shortopts: - # short option can have no following space, e.g. hg log -Rfoo - values.append(args.pop(pos)[2:]) - argcount -= 1 - else: - pos += 1 - return values - -def dispatch(ui, args): - # read --config before doing anything else - # (e.g. to change trust settings for reading .hg/hgrc) - config = earlygetopt(['--config'], args) - if config: - ui.updateopts(config=parseconfig(config)) - - # check for cwd - cwd = earlygetopt(['--cwd'], args) - if cwd: - os.chdir(cwd[-1]) - - # read the local repository .hgrc into a local ui object - path = findrepo() or "" - if not path: - lui = ui - if path: - try: - lui = commands.ui.ui(parentui=ui) - lui.readconfig(os.path.join(path, ".hg", "hgrc")) - except IOError: - pass - - # now we can expand paths, even ones in .hg/hgrc - rpath = earlygetopt(["-R", "--repository", "--repo"], args) - if rpath: - path = lui.expandpath(rpath[-1]) - lui = commands.ui.ui(parentui=ui) - lui.readconfig(os.path.join(path, ".hg", "hgrc")) - - extensions.loadall(lui) - # check for fallback encoding - fallback = lui.config('ui', 'fallbackencoding') - if fallback: - util._fallbackencoding = fallback - - fullargs = args - cmd, func, args, options, cmdoptions = parse(lui, args) - - if options["config"]: - raise util.Abort(_("Option --config may not be abbreviated!")) - if options["cwd"]: - raise util.Abort(_("Option --cwd may not be abbreviated!")) - if options["repository"]: - raise util.Abort(_( - "Option -R has to be separated from other options (i.e. not -qR) " - "and --repository may only be abbreviated as --repo!")) - - if options["encoding"]: - util._encoding = options["encoding"] - if options["encodingmode"]: - util._encodingmode = options["encodingmode"] - if options["time"]: - def get_times(): - t = os.times() - if t[4] == 0.0: # Windows leaves this as zero, so use time.clock() - t = (t[0], t[1], t[2], t[3], time.clock()) - return t - s = get_times() - def print_time(): - t = get_times() - ui.warn(_("Time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") % - (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3])) - atexit.register(print_time) - - ui.updateopts(options["verbose"], options["debug"], options["quiet"], - not options["noninteractive"], options["traceback"]) - - if options['help']: - return commands.help_(ui, cmd, options['version']) - elif options['version']: - return commands.version_(ui) - elif not cmd: - return commands.help_(ui, 'shortlist') - - repo = None - if cmd not in commands.norepo.split(): - try: - repo = hg.repository(ui, path=path) - ui = repo.ui - if not repo.local(): - raise util.Abort(_("repository '%s' is not local") % path) - except hg.RepoError: - if cmd not in commands.optionalrepo.split(): - if not path: - raise hg.RepoError(_("There is no Mercurial repository here" - " (.hg not found)")) - raise - d = lambda: func(ui, repo, *args, **cmdoptions) - else: - d = lambda: func(ui, *args, **cmdoptions) - - # run pre-hook, and abort if it fails - ret = hook.hook(ui, repo, "pre-%s" % cmd, False, args=" ".join(fullargs)) - if ret: - return ret - ret = runcommand(ui, options, cmd, d) - # run post-hook, passing command result - hook.hook(ui, repo, "post-%s" % cmd, False, args=" ".join(fullargs), - result = ret) - return ret - -def runcommand(ui, options, cmd, cmdfunc): - def checkargs(): - try: - return cmdfunc() - except TypeError, inst: - # was this an argument error? - tb = traceback.extract_tb(sys.exc_info()[2]) - if len(tb) != 2: # no - raise - raise ParseError(cmd, _("invalid arguments")) - - if options['profile']: - import hotshot, hotshot.stats - prof = hotshot.Profile("hg.prof") - try: - try: - return prof.runcall(checkargs) - except: - try: - ui.warn(_('exception raised - generating ' - 'profile anyway\n')) - except: - pass - raise - finally: - prof.close() - stats = hotshot.stats.load("hg.prof") - stats.strip_dirs() - stats.sort_stats('time', 'calls') - stats.print_stats(40) - elif options['lsprof']: - try: - from mercurial import lsprof - except ImportError: - raise util.Abort(_( - 'lsprof not available - install from ' - 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/')) - p = lsprof.Profiler() - p.enable(subcalls=True) - try: - return checkargs() - finally: - p.disable() - stats = lsprof.Stats(p.getstats()) - stats.sort() - stats.pprint(top=10, file=sys.stderr, climit=5) - else: - return checkargs() - def bail_if_changed(repo): modified, added, removed, deleted = repo.status()[:4] if modified or added or removed or deleted: @@ -458,15 +94,6 @@ def setremoteconfig(ui, opts): if opts.get('remotecmd'): ui.setconfig("ui", "remotecmd", opts['remotecmd']) -def parseurl(url, revs): - '''parse url#branch, returning url, branch + revs''' - - if '#' not in url: - return url, (revs or None), None - - url, rev = url.split('#', 1) - return url, revs + [rev], rev - def revpair(repo, revs): '''return pair of nodes, given list of revisions. second item can be None, meaning use working dir.''' @@ -625,8 +252,7 @@ def findrenames(repo, added=None, remove if bestname: yield bestname, a, bestscore -def addremove(repo, pats=[], opts={}, wlock=None, dry_run=None, - similarity=None): +def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None): if dry_run is None: dry_run = opts.get('dry_run') if similarity is None: @@ -635,19 +261,19 @@ def addremove(repo, pats=[], opts={}, wl mapping = {} for src, abs, rel, exact in walk(repo, pats, opts): target = repo.wjoin(abs) - if src == 'f' and repo.dirstate.state(abs) == '?': + if src == 'f' and abs not in repo.dirstate: 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 util.lexists(target): + if repo.dirstate[abs] != 'r' and not util.lexists(target): 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) + repo.add(add) + repo.remove(remove) if similarity > 0: for old, new, score in findrenames(repo, add, remove, similarity): oldrel, oldexact = mapping[old] @@ -657,7 +283,7 @@ def addremove(repo, pats=[], opts={}, wl '(%d%% similar)\n') % (oldrel, newrel, score * 100)) if not dry_run: - repo.copy(old, new, wlock=wlock) + repo.copy(old, new) def service(opts, parentfn=None, initfn=None, runfn=None): '''Run a command as a service.''' @@ -1273,3 +899,45 @@ def walkchangerevs(ui, repo, pats, chang for rev in nrevs: yield 'iter', rev, None return iterate(), matchfn + +def commit(ui, repo, commitfunc, pats, opts): + '''commit the specified files or all outstanding changes''' + message = logmessage(opts) + + if opts['addremove']: + addremove(repo, pats, opts) + fns, match, anypats = matchpats(repo, pats, opts) + if pats: + status = repo.status(files=fns, match=match) + modified, added, removed, deleted, unknown = status[:5] + files = modified + added + removed + slist = None + for f in fns: + if f == '.': + continue + if f not in files: + rf = repo.wjoin(f) + try: + mode = os.lstat(rf)[stat.ST_MODE] + except OSError: + raise util.Abort(_("file %s not found!") % rf) + if stat.S_ISDIR(mode): + name = f + '/' + if slist is None: + slist = list(files) + slist.sort() + i = bisect.bisect(slist, name) + if i >= len(slist) or not slist[i].startswith(name): + raise util.Abort(_("no match under directory %s!") + % rf) + elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)): + raise util.Abort(_("can't commit %s: " + "unsupported file type!") % rf) + elif f not in repo.dirstate: + raise util.Abort(_("file %s not tracked!") % rf) + else: + files = [] + try: + return commitfunc(ui, repo, files, message, match, opts) + except ValueError, inst: + raise util.Abort(str(inst)) diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -5,11 +5,10 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -import demandimport; demandimport.enable() from node import * from i18n import _ -import bisect, os, re, sys, urllib, shlex, stat -import ui, hg, util, revlog, bundlerepo, extensions +import os, re, sys, urllib +import hg, util, revlog, bundlerepo, extensions import difflib, patch, time, help, mdiff, tempfile import errno, version, socket import archival, changegroup, cmdutil, hgweb.server, sshserver @@ -33,7 +32,7 @@ def add(ui, repo, *pats, **opts): if ui.verbose: ui.status(_('adding %s\n') % rel) names.append(abs) - elif repo.dirstate.state(abs) == '?': + elif abs not in repo.dirstate: ui.status(_('adding %s\n') % rel) names.append(abs) if not opts.get('dry_run'): @@ -73,19 +72,31 @@ def annotate(ui, repo, *pats, **opts): detects as binary. With -a, annotate will generate an annotation anyway, probably with undesirable results. """ - getdate = util.cachefunc(lambda x: util.datestr(x.date())) + getdate = util.cachefunc(lambda x: util.datestr(x[0].date())) if not pats: raise util.Abort(_('at least one file name or pattern required')) - opmap = [['user', lambda x: ui.shortuser(x.user())], - ['number', lambda x: str(x.rev())], - ['changeset', lambda x: short(x.node())], - ['date', getdate], ['follow', lambda x: x.path()]] + opmap = [('user', lambda x: ui.shortuser(x[0].user())), + ('number', lambda x: str(x[0].rev())), + ('changeset', lambda x: short(x[0].node())), + ('date', getdate), + ('follow', lambda x: x[0].path()), + ] + if (not opts['user'] and not opts['changeset'] and not opts['date'] and not opts['follow']): opts['number'] = 1 + linenumber = opts.get('line_number') is not None + if (linenumber and (not opts['changeset']) and (not opts['number'])): + raise util.Abort(_('at least one of -n/-c is required for -l')) + + funcmap = [func for op, func in opmap if opts.get(op)] + if linenumber: + lastfunc = funcmap[-1] + funcmap[-1] = lambda x: "%s:%s" % (lastfunc(x), x[1]) + ctx = repo.changectx(opts['rev']) for src, abs, rel, exact in cmdutil.walk(repo, pats, opts, @@ -95,15 +106,15 @@ def annotate(ui, repo, *pats, **opts): ui.write(_("%s: binary file\n") % ((pats and rel) or abs)) continue - lines = fctx.annotate(follow=opts.get('follow')) + lines = fctx.annotate(follow=opts.get('follow'), + linenumber=linenumber) pieces = [] - for o, f in opmap: - if opts[o]: - l = [f(n) for n, dummy in lines] - if l: - m = max(map(len, l)) - pieces.append(["%*s" % (m, x) for x in l]) + for f in funcmap: + l = [f(n) for n, dummy in lines] + if l: + m = max(map(len, l)) + pieces.append(["%*s" % (m, x) for x in l]) if pieces: for p, l in zip(zip(*pieces), lines): @@ -324,7 +335,7 @@ def bundle(ui, repo, fname, dest=None, * visit.append(p) else: cmdutil.setremoteconfig(ui, opts) - dest, revs, checkout = cmdutil.parseurl( + dest, revs, checkout = hg.parseurl( ui.expandpath(dest or 'default-push', dest or 'default'), revs) other = hg.repository(ui, dest) o = repo.findoutgoing(other, force=opts['force']) @@ -416,48 +427,12 @@ 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 = cmdutil.logmessage(opts) - - if opts['addremove']: - cmdutil.addremove(repo, pats, opts) - fns, match, anypats = cmdutil.matchpats(repo, pats, opts) - if pats: - status = repo.status(files=fns, match=match) - modified, added, removed, deleted, unknown = status[:5] - files = modified + added + removed - slist = None - for f in fns: - if f == '.': - continue - if f not in files: - rf = repo.wjoin(f) - try: - mode = os.lstat(rf)[stat.ST_MODE] - except OSError: - raise util.Abort(_("file %s not found!") % rf) - if stat.S_ISDIR(mode): - name = f + '/' - if slist is None: - slist = list(files) - slist.sort() - i = bisect.bisect(slist, name) - if i >= len(slist) or not slist[i].startswith(name): - raise util.Abort(_("no match under directory %s!") - % rf) - elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)): - raise util.Abort(_("can't commit %s: " - "unsupported file type!") % rf) - elif repo.dirstate.state(f) == '?': - raise util.Abort(_("file %s not tracked!") % rf) - else: - files = [] - try: - repo.commit(files, message, opts['user'], opts['date'], match, - force_editor=opts.get('force_editor')) - except ValueError, inst: - raise util.Abort(str(inst)) - -def docopy(ui, repo, pats, opts, wlock): + def commitfunc(ui, repo, files, message, match, opts): + return repo.commit(files, message, opts['user'], opts['date'], match, + force_editor=opts.get('force_editor')) + cmdutil.commit(ui, repo, commitfunc, pats, opts) + +def docopy(ui, repo, pats, opts): # called with the repo lock held # # hgsep => pathname that uses "/" to separate directories @@ -473,7 +448,7 @@ def docopy(ui, repo, pats, opts, wlock): def okaytocopy(abs, rel, exact): reasons = {'?': _('is not managed'), 'r': _('has been marked for remove')} - state = repo.dirstate.state(abs) + state = repo.dirstate[abs] reason = reasons.get(state) if reason: if exact: @@ -501,7 +476,7 @@ def docopy(ui, repo, pats, opts, wlock): repo.pathto(prevsrc, cwd))) return if (not opts['after'] and os.path.exists(target) or - opts['after'] and repo.dirstate.state(abstarget) not in '?ar'): + opts['after'] and repo.dirstate[abstarget] in 'mn'): if not opts['force']: ui.warn(_('%s: not overwriting - file exists\n') % reltarget) @@ -516,16 +491,16 @@ def docopy(ui, repo, pats, opts, wlock): if not os.path.isdir(targetdir) and not opts.get('dry_run'): os.makedirs(targetdir) try: - restore = repo.dirstate.state(abstarget) == 'r' + restore = repo.dirstate[abstarget] == 'r' if restore and not opts.get('dry_run'): - repo.undelete([abstarget], wlock) + repo.undelete([abstarget]) try: if not opts.get('dry_run'): util.copyfile(src, target) restore = False finally: if restore: - repo.remove([abstarget], wlock=wlock) + repo.remove([abstarget]) except IOError, inst: if inst.errno == errno.ENOENT: ui.warn(_('%s: deleted in working copy\n') % relsrc) @@ -538,15 +513,15 @@ def docopy(ui, repo, pats, opts, wlock): ui.status(_('copying %s to %s\n') % (relsrc, reltarget)) targets[abstarget] = abssrc if abstarget != origsrc: - if repo.dirstate.state(origsrc) == 'a': + if repo.dirstate[origsrc] == 'a': if not ui.quiet: ui.warn(_("%s has not been committed yet, so no copy " "data will be stored for %s.\n") % (repo.pathto(origsrc, cwd), reltarget)) if abstarget not in repo.dirstate and not opts.get('dry_run'): - repo.add([abstarget], wlock) + repo.add([abstarget]) elif not opts.get('dry_run'): - repo.copy(origsrc, abstarget, wlock) + repo.copy(origsrc, abstarget) copied.append((abssrc, relsrc, exact)) # pat: ossep @@ -623,9 +598,12 @@ def docopy(ui, repo, pats, opts, wlock): raise util.Abort(_('no destination specified')) dest = pats.pop() destdirexists = os.path.isdir(dest) - if (len(pats) > 1 or util.patkind(pats[0], None)[0]) and not destdirexists: - raise util.Abort(_('with multiple sources, destination must be an ' - 'existing directory')) + if not destdirexists: + if len(pats) > 1 or util.patkind(pats[0], None)[0]: + raise util.Abort(_('with multiple sources, destination must be an ' + 'existing directory')) + if dest.endswith(os.sep) or os.altsep and dest.endswith(os.altsep): + raise util.Abort(_('destination %s is not a directory') % dest) if opts['after']: tfn = targetpathafterfn else: @@ -666,8 +644,11 @@ def copy(ui, repo, *pats, **opts): This command takes effect in the next commit. To undo a copy before that, see hg revert. """ - wlock = repo.wlock(0) - errs, copied = docopy(ui, repo, pats, opts, wlock) + wlock = repo.wlock(False) + try: + errs, copied = docopy(ui, repo, pats, opts) + finally: + del wlock return errs def debugancestor(ui, index, rev1, rev2): @@ -683,7 +664,7 @@ def debugcomplete(ui, cmd='', **opts): options = [] otables = [globalopts] if cmd: - aliases, entry = cmdutil.findcmd(ui, cmd) + aliases, entry = cmdutil.findcmd(ui, cmd, table) otables.append(entry[1]) for t in otables: for o in t: @@ -693,7 +674,7 @@ def debugcomplete(ui, cmd='', **opts): ui.write("%s\n" % "\n".join(options)) return - clist = cmdutil.findpossible(ui, cmd).keys() + clist = cmdutil.findpossible(ui, cmd, table).keys() clist.sort() ui.write("%s\n" % "\n".join(clist)) @@ -704,17 +685,19 @@ def debugrebuildstate(ui, repo, rev=""): ctx = repo.changectx(rev) files = ctx.manifest() wlock = repo.wlock() - repo.dirstate.rebuild(rev, files) + try: + repo.dirstate.rebuild(rev, files) + finally: + del wlock def debugcheckstate(ui, repo): """validate the correctness of the current dirstate""" parent1, parent2 = repo.dirstate.parents() - dc = repo.dirstate m1 = repo.changectx(parent1).manifest() m2 = repo.changectx(parent2).manifest() errors = 0 - for f in dc: - state = repo.dirstate.state(f) + for f in repo.dirstate: + state = repo.dirstate[f] if state in "nr" and f not in m1: ui.warn(_("%s in state %s, but not in manifest1\n") % (f, state)) errors += 1 @@ -726,7 +709,7 @@ def debugcheckstate(ui, repo): (f, state)) errors += 1 for f in m1: - state = repo.dirstate.state(f) + state = repo.dirstate[f] if state not in "nrm": ui.warn(_("%s in manifest1, but listed as state %s") % (f, state)) errors += 1 @@ -774,22 +757,25 @@ def debugsetparents(ui, repo, rev1, rev2 try: repo.dirstate.setparents(repo.lookup(rev1), repo.lookup(rev2)) finally: - wlock.release() + del wlock def debugstate(ui, repo): """show the contents of the current dirstate""" - dc = repo.dirstate - for file_ in dc: - if dc[file_][3] == -1: + k = repo.dirstate._map.items() + k.sort() + for file_, ent in k: + if ent[3] == -1: # Pad or slice to locale representation locale_len = len(time.strftime("%x %X", time.localtime(0))) timestr = 'unset' timestr = timestr[:locale_len] + ' '*(locale_len - len(timestr)) else: - timestr = time.strftime("%x %X", time.localtime(dc[file_][3])) - ui.write("%c %3o %10d %s %s\n" - % (dc[file_][0], dc[file_][1] & 0777, dc[file_][2], - timestr, file_)) + timestr = time.strftime("%x %X", time.localtime(ent[3])) + if ent[1] & 020000: + mode = 'lnk' + else: + mode = '%3o' % (ent[1] & 0777) + ui.write("%c %s %10d %s %s\n" % (ent[0], mode, ent[2], timestr, file_)) for f in repo.dirstate.copies(): ui.write(_("copy: %s -> %s\n") % (repo.dirstate.copied(f), f)) @@ -820,7 +806,10 @@ def debugindex(ui, file_): " nodeid p1 p2\n") for i in xrange(r.count()): node = r.node(i) - pp = r.parents(node) + try: + pp = r.parents(node) + except: + pp = [nullid, nullid] ui.write("% 6d % 9d % 7d % 6d % 7d %s %s %s\n" % ( i, r.start(i), r.length(i), r.base(i), r.linkrev(node), short(node), short(pp[0]), short(pp[1]))) @@ -841,7 +830,7 @@ def debuginstall(ui): '''test Mercurial installation''' def writetemp(contents): - (fd, name) = tempfile.mkstemp() + (fd, name) = tempfile.mkstemp(prefix="hg-debuginstall-") f = os.fdopen(fd, "wb") f.write(contents) f.close() @@ -880,42 +869,40 @@ def debuginstall(ui): # patch ui.status(_("Checking patch...\n")) - patcher = ui.config('ui', 'patch') - patcher = ((patcher and util.find_exe(patcher)) or - util.find_exe('gpatch') or - util.find_exe('patch')) - if not patcher: - ui.write(_(" Can't find patch or gpatch in PATH\n")) - ui.write(_(" (specify a patch utility in your .hgrc file)\n")) - problems += 1 + patchproblems = 0 + a = "1\n2\n3\n4\n" + b = "1\n2\n3\ninsert\n4\n" + fa = writetemp(a) + d = mdiff.unidiff(a, None, b, None, os.path.basename(fa)) + fd = writetemp(d) + + files = {} + try: + patch.patch(fd, ui, cwd=os.path.dirname(fa), files=files) + except util.Abort, e: + ui.write(_(" patch call failed:\n")) + ui.write(" " + str(e) + "\n") + patchproblems += 1 else: - # actually attempt a patch here - a = "1\n2\n3\n4\n" - b = "1\n2\n3\ninsert\n4\n" - fa = writetemp(a) - d = mdiff.unidiff(a, None, b, None, os.path.basename(fa)) - fd = writetemp(d) - - files = {} - try: - patch.patch(fd, ui, cwd=os.path.dirname(fa), files=files) - except util.Abort, e: - ui.write(_(" patch call failed:\n")) - ui.write(" " + str(e) + "\n") - problems += 1 + if list(files) != [os.path.basename(fa)]: + ui.write(_(" unexpected patch output!\n")) + patchproblems += 1 + a = file(fa).read() + if a != b: + ui.write(_(" patch test failed!\n")) + patchproblems += 1 + + if patchproblems: + if ui.config('ui', 'patch'): + ui.write(_(" (Current patch tool may be incompatible with patch," + " or misconfigured. Please check your .hgrc file)\n")) else: - if list(files) != [os.path.basename(fa)]: - ui.write(_(" unexpected patch output!")) - ui.write(_(" (you may have an incompatible version of patch)\n")) - problems += 1 - a = file(fa).read() - if a != b: - ui.write(_(" patch test failed!")) - ui.write(_(" (you may have an incompatible version of patch)\n")) - problems += 1 - - os.unlink(fa) - os.unlink(fd) + ui.write(_(" Internal patcher failure, please report this error" + " to http://www.selenic.com/mercurial/bts\n")) + problems += patchproblems + + os.unlink(fa) + os.unlink(fd) # merge helper ui.status(_("Checking merge helper...\n")) @@ -1323,7 +1310,7 @@ def help_(ui, name=None, with_version=Fa if with_version: version_(ui) ui.write('\n') - aliases, i = cmdutil.findcmd(ui, name) + aliases, i = cmdutil.findcmd(ui, name, table) # synopsis ui.write("%s\n\n" % i[2]) @@ -1486,12 +1473,16 @@ def identify(ui, repo, source=None, name for non-default branches. """ + if not repo and not source: + raise util.Abort(_("There is no Mercurial repository here " + "(.hg not found)")) + hexfunc = ui.debugflag and hex or short default = not (num or id or branch or tags) output = [] if source: - source, revs, checkout = cmdutil.parseurl(ui.expandpath(source), []) + source, revs, checkout = hg.parseurl(ui.expandpath(source), []) srepo = hg.repository(ui, source) if not rev and revs: rev = revs[0] @@ -1572,70 +1563,77 @@ def import_(ui, repo, patch1, *patches, d = opts["base"] strip = opts["strip"] - - wlock = repo.wlock() - lock = repo.lock() - - for p in patches: - pf = os.path.join(d, p) - - if pf == '-': - ui.status(_("applying patch from stdin\n")) - tmpname, message, user, date, branch, nodeid, p1, p2 = patch.extract(ui, sys.stdin) - else: - ui.status(_("applying %s\n") % p) - tmpname, message, user, date, branch, nodeid, p1, p2 = patch.extract(ui, file(pf, 'rb')) - - if tmpname is None: - raise util.Abort(_('no diffs found')) - - try: - cmdline_message = cmdutil.logmessage(opts) - if cmdline_message: - # pickup the cmdline msg - message = cmdline_message - elif message: - # pickup the patch msg - message = message.strip() + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + for p in patches: + pf = os.path.join(d, p) + + if pf == '-': + ui.status(_("applying patch from stdin\n")) + data = patch.extract(ui, sys.stdin) else: - # launch the editor - message = None - ui.debug(_('message:\n%s\n') % message) - - wp = repo.workingctx().parents() - if opts.get('exact'): - if not nodeid or not p1: - raise util.Abort(_('not a mercurial patch')) - p1 = repo.lookup(p1) - p2 = repo.lookup(p2 or hex(nullid)) - - if p1 != wp[0].node(): - hg.clean(repo, p1, wlock=wlock) - repo.dirstate.setparents(p1, p2) - elif p2: - try: + ui.status(_("applying %s\n") % p) + if os.path.exists(pf): + data = patch.extract(ui, file(pf, 'rb')) + else: + data = patch.extract(ui, urllib.urlopen(pf)) + tmpname, message, user, date, branch, nodeid, p1, p2 = data + + if tmpname is None: + raise util.Abort(_('no diffs found')) + + try: + cmdline_message = cmdutil.logmessage(opts) + if cmdline_message: + # pickup the cmdline msg + message = cmdline_message + elif message: + # pickup the patch msg + message = message.strip() + else: + # launch the editor + message = None + ui.debug(_('message:\n%s\n') % message) + + wp = repo.workingctx().parents() + if opts.get('exact'): + if not nodeid or not p1: + raise util.Abort(_('not a mercurial patch')) p1 = repo.lookup(p1) - p2 = repo.lookup(p2) - if p1 == wp[0].node(): - repo.dirstate.setparents(p1, p2) - except hg.RepoError: - pass - if opts.get('exact') or opts.get('import_branch'): - repo.dirstate.setbranch(branch or 'default') - - files = {} - try: - fuzz = patch.patch(tmpname, ui, strip=strip, cwd=repo.root, - files=files) + p2 = repo.lookup(p2 or hex(nullid)) + + if p1 != wp[0].node(): + hg.clean(repo, p1) + repo.dirstate.setparents(p1, p2) + elif p2: + try: + p1 = repo.lookup(p1) + p2 = repo.lookup(p2) + if p1 == wp[0].node(): + repo.dirstate.setparents(p1, p2) + except hg.RepoError: + pass + if opts.get('exact') or opts.get('import_branch'): + repo.dirstate.setbranch(branch or 'default') + + files = {} + try: + fuzz = patch.patch(tmpname, ui, strip=strip, cwd=repo.root, + files=files) + finally: + files = patch.updatedir(ui, repo, files) + n = repo.commit(files, message, user, date) + if opts.get('exact'): + if hex(n) != nodeid: + repo.rollback() + raise util.Abort(_('patch is damaged' + + ' or loses information')) finally: - files = patch.updatedir(ui, repo, files, wlock=wlock) - n = repo.commit(files, message, user, date, wlock=wlock, lock=lock) - if opts.get('exact'): - if hex(n) != nodeid: - repo.rollback(wlock=wlock, lock=lock) - raise util.Abort(_('patch is damaged or loses information')) - finally: - os.unlink(tmpname) + os.unlink(tmpname) + finally: + del lock, wlock def incoming(ui, repo, source="default", **opts): """show new changesets found in source @@ -1649,18 +1647,13 @@ def incoming(ui, repo, source="default", See pull for valid source format details. """ - source, revs, checkout = cmdutil.parseurl(ui.expandpath(source), - opts['rev']) + source, revs, checkout = hg.parseurl(ui.expandpath(source), opts['rev']) cmdutil.setremoteconfig(ui, opts) other = hg.repository(ui, source) ui.status(_('comparing with %s\n') % source) if revs: - if 'lookup' in other.capabilities: - revs = [other.lookup(rev) for rev in revs] - else: - error = _("Other repository doesn't support revision lookup, so a rev cannot be specified.") - raise util.Abort(error) + revs = [other.lookup(rev) for rev in revs] incoming = repo.findincoming(other, heads=revs, force=opts["force"]) if not incoming: try: @@ -1678,8 +1671,6 @@ def incoming(ui, repo, source="default", if revs is None: cg = other.changegroup(incoming, "incoming") else: - if 'changegroupsubset' not in other.capabilities: - raise util.Abort(_("Partial incoming cannot be done because other repository doesn't support changegroupsubset.")) cg = other.changegroupsubset(incoming, revs, 'incoming') bundletype = other.local() and "HG10BZ" or "HG10UN" fname = cleanup = changegroup.writebundle(cg, fname, bundletype) @@ -1751,7 +1742,7 @@ def locate(ui, repo, *pats, **opts): default='relglob'): if src == 'b': continue - if not node and repo.dirstate.state(abs) == '?': + if not node and abs not in repo.dirstate: continue if opts['fullpath']: ui.write(os.path.join(repo.root, abs), end) @@ -1883,7 +1874,7 @@ def log(ui, repo, *pats, **opts): if displayer.flush(rev): count += 1 -def manifest(ui, repo, rev=None): +def manifest(ui, repo, node=None, rev=None): """output the current or given revision of the project manifest Print a list of version controlled files for the given revision. @@ -1897,7 +1888,13 @@ def manifest(ui, repo, rev=None): file revision hashes. """ - m = repo.changectx(rev).manifest() + if rev and node: + raise util.Abort(_("please specify just one revision")) + + if not node: + node = rev + + m = repo.changectx(node).manifest() files = m.keys() files.sort() @@ -1924,7 +1921,6 @@ def merge(ui, repo, node=None, force=Non if rev and node: raise util.Abort(_("please specify just one revision")) - if not node: node = rev @@ -1934,10 +1930,13 @@ def merge(ui, repo, node=None, force=Non raise util.Abort(_('repo has %d heads - ' 'please merge with an explicit rev') % len(heads)) + parent = repo.dirstate.parents()[0] if len(heads) == 1: - raise util.Abort(_('there is nothing to merge - ' - 'use "hg update" instead')) - parent = repo.dirstate.parents()[0] + msg = _('there is nothing to merge') + if parent != repo.lookup(repo.workingctx().branch()): + msg = _('%s - use "hg update" instead' % msg) + raise util.Abort(msg) + if parent not in heads: raise util.Abort(_('working dir not at a head rev - ' 'use "hg update" or merge with an explicit rev')) @@ -1953,7 +1952,7 @@ def outgoing(ui, repo, dest=None, **opts See pull for valid destination format details. """ - dest, revs, checkout = cmdutil.parseurl( + dest, revs, checkout = hg.parseurl( ui.expandpath(dest or 'default-push', dest or 'default'), opts['rev']) cmdutil.setremoteconfig(ui, opts) if revs: @@ -1985,16 +1984,30 @@ def parents(ui, repo, file_=None, **opts revision or the argument to --rev if given) is printed. """ rev = opts.get('rev') + if rev: + ctx = repo.changectx(rev) + else: + ctx = repo.workingctx() + if file_: files, match, anypats = cmdutil.matchpats(repo, (file_,), opts) if anypats or len(files) != 1: raise util.Abort(_('can only specify an explicit file name')) - ctx = repo.filectx(files[0], changeid=rev) - elif rev: - ctx = repo.changectx(rev) + file_ = files[0] + filenodes = [] + for cp in ctx.parents(): + if not cp: + continue + try: + filenodes.append(cp.filenode(file_)) + except revlog.LookupError: + pass + if not filenodes: + raise util.Abort(_("'%s' not found in manifest!") % file_) + fl = repo.file(file_) + p = [repo.lookup(fl.linkrev(fn)) for fn in filenodes] else: - ctx = repo.workingctx() - p = [cp.node() for cp in ctx.parents()] + p = [cp.node() for cp in ctx.parents()] displayer = cmdutil.show_changeset(ui, repo, opts) for n in p: @@ -2075,17 +2088,17 @@ def pull(ui, repo, source="default", **o Alternatively specify "ssh -C" as your ssh command in your hgrc or with the --ssh command line option. """ - source, revs, checkout = cmdutil.parseurl(ui.expandpath(source), - opts['rev']) + source, revs, checkout = hg.parseurl(ui.expandpath(source), opts['rev']) cmdutil.setremoteconfig(ui, opts) other = hg.repository(ui, source) ui.status(_('pulling from %s\n') % (source)) if revs: - if 'lookup' in other.capabilities: + try: revs = [other.lookup(rev) for rev in revs] - else: - error = _("Other repository doesn't support revision lookup, so a rev cannot be specified.") + except repo.NoCapability: + error = _("Other repository doesn't support revision lookup, " + "so a rev cannot be specified.") raise util.Abort(error) modheads = repo.pull(other, heads=revs, force=opts['force']) @@ -2121,7 +2134,7 @@ def push(ui, repo, dest=None, **opts): Pushing to http:// and https:// URLs is only possible, if this feature is explicitly enabled on the remote Mercurial server. """ - dest, revs, checkout = cmdutil.parseurl( + dest, revs, checkout = hg.parseurl( ui.expandpath(dest or 'default-push', dest or 'default'), opts['rev']) cmdutil.setremoteconfig(ui, opts) @@ -2190,7 +2203,6 @@ def remove(ui, repo, *pats, **opts): Modified files and added files are not removed by default. To remove them, use the -f/--force option. """ - names = [] if not opts['after'] and not pats: raise util.Abort(_('no files specified')) files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) @@ -2207,7 +2219,7 @@ def remove(ui, repo, *pats, **opts): forget.append(abs) continue reason = _('has been marked for add (use -f to force removal)') - elif repo.dirstate.state(abs) == '?': + elif abs not in repo.dirstate: reason = _('is not managed') elif opts['after'] and not exact and abs not in deleted: continue @@ -2237,16 +2249,19 @@ def rename(ui, repo, *pats, **opts): This command takes effect in the next commit. To undo a rename before that, see hg revert. """ - wlock = repo.wlock(0) - errs, copied = docopy(ui, repo, pats, opts, wlock) - names = [] - for abs, rel, exact in copied: - if ui.verbose or not exact: - ui.status(_('removing %s\n') % rel) - names.append(abs) - if not opts.get('dry_run'): - repo.remove(names, True, wlock=wlock) - return errs + wlock = repo.wlock(False) + try: + errs, copied = docopy(ui, repo, pats, opts) + names = [] + for abs, rel, exact in copied: + if ui.verbose or not exact: + ui.status(_('removing %s\n') % rel) + names.append(abs) + if not opts.get('dry_run'): + repo.remove(names, True) + return errs + finally: + del wlock def revert(ui, repo, *pats, **opts): """revert files or dirs to their states as of some revision @@ -2300,8 +2315,6 @@ def revert(ui, repo, *pats, **opts): else: pmf = None - wlock = repo.wlock() - # need all matching names in dirstate and manifest of target rev, # so have to walk both. do not print errors if files exist in one # but not other. @@ -2309,117 +2322,124 @@ def revert(ui, repo, *pats, **opts): names = {} target_only = {} - # walk dirstate. - - 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. - - def badmatch(path): - if path in names: - return True - path_ = path + '/' - for f in names: - if f.startswith(path_): + wlock = repo.wlock() + try: + # walk dirstate. + 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. + + def badmatch(path): + if path in names: return True - return False - - for src, abs, rel, exact in cmdutil.walk(repo, pats, opts, node=node, - badmatch=badmatch): - if abs in names or src == 'b': - continue - names[abs] = (rel, exact) - target_only[abs] = True - - changes = repo.status(match=names.has_key, wlock=wlock)[:5] - modified, added, removed, deleted, unknown = map(dict.fromkeys, changes) - - # if f is a rename, also revert the source - cwd = repo.getcwd() - for f in added: - src = repo.dirstate.copied(f) - if src and src not in names and repo.dirstate[src][0] == 'r': - removed[src] = None - names[src] = (repo.pathto(src, cwd), True) - - revert = ([], _('reverting %s\n')) - add = ([], _('adding %s\n')) - remove = ([], _('removing %s\n')) - forget = ([], _('forgetting %s\n')) - undelete = ([], _('undeleting %s\n')) - update = {} - - disptable = ( - # dispatch table: - # file state - # action if in target manifest - # action if not in target manifest - # make backup if in target manifest - # make backup if not in target manifest - (modified, revert, remove, True, True), - (added, revert, forget, True, False), - (removed, undelete, None, False, False), - (deleted, revert, remove, False, False), - (unknown, add, None, True, False), - (target_only, add, None, False, False), - ) - - entries = names.items() - entries.sort() - - for abs, (rel, exact) in entries: - mfentry = mf.get(abs) - target = repo.wjoin(abs) - def handle(xlist, dobackup): - xlist[0].append(abs) - update[abs] = 1 - if dobackup and not opts['no_backup'] and util.lexists(target): - bakname = "%s.orig" % rel - ui.note(_('saving current version of %s as %s\n') % - (rel, bakname)) - if not opts.get('dry_run'): - util.copyfile(target, bakname) - if ui.verbose or not exact: - ui.status(xlist[1] % rel) - for table, hitlist, misslist, backuphit, backupmiss in disptable: - if abs not in table: continue - # file has changed in dirstate - if mfentry: - handle(hitlist, backuphit) - elif misslist is not None: - handle(misslist, backupmiss) + path_ = path + '/' + for f in names: + if f.startswith(path_): + return True + return False + + for src, abs, rel, exact in cmdutil.walk(repo, pats, opts, node=node, + badmatch=badmatch): + if abs in names or src == 'b': + continue + names[abs] = (rel, exact) + target_only[abs] = True + + changes = repo.status(match=names.has_key)[:5] + modified, added, removed, deleted, unknown = map(dict.fromkeys, changes) + + # if f is a rename, also revert the source + cwd = repo.getcwd() + for f in added: + src = repo.dirstate.copied(f) + if src and src not in names and repo.dirstate[src] == 'r': + removed[src] = None + names[src] = (repo.pathto(src, cwd), True) + + revert = ([], _('reverting %s\n')) + add = ([], _('adding %s\n')) + remove = ([], _('removing %s\n')) + forget = ([], _('forgetting %s\n')) + undelete = ([], _('undeleting %s\n')) + update = {} + + disptable = ( + # dispatch table: + # file state + # action if in target manifest + # action if not in target manifest + # make backup if in target manifest + # make backup if not in target manifest + (modified, revert, remove, True, True), + (added, revert, forget, True, False), + (removed, undelete, None, False, False), + (deleted, revert, remove, False, False), + (unknown, add, None, True, False), + (target_only, add, None, False, False), + ) + + entries = names.items() + entries.sort() + + for abs, (rel, exact) in entries: + mfentry = mf.get(abs) + target = repo.wjoin(abs) + def handle(xlist, dobackup): + xlist[0].append(abs) + update[abs] = 1 + if dobackup and not opts['no_backup'] and util.lexists(target): + bakname = "%s.orig" % rel + ui.note(_('saving current version of %s as %s\n') % + (rel, bakname)) + if not opts.get('dry_run'): + util.copyfile(target, bakname) + if ui.verbose or not exact: + ui.status(xlist[1] % rel) + for table, hitlist, misslist, backuphit, backupmiss in disptable: + if abs not in table: continue + # file has changed in dirstate + if mfentry: + handle(hitlist, backuphit) + elif misslist is not None: + handle(misslist, backupmiss) + else: + if exact: ui.warn(_('file not managed: %s\n') % rel) + break else: - if exact: ui.warn(_('file not managed: %s\n') % rel) - break - else: - # file has not changed in dirstate - if node == parent: - if exact: ui.warn(_('no changes needed to %s\n') % rel) - continue - if pmf is None: - # only need parent manifest in this unlikely case, - # so do not read by default - pmf = repo.changectx(parent).manifest() - if abs in pmf: - if mfentry: - # if version of file is same in parent and target - # manifests, do nothing - if pmf[abs] != mfentry: - handle(revert, False) - else: - handle(remove, False) - - if not opts.get('dry_run'): - repo.dirstate.forget(forget[0]) - 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') - return r + # file has not changed in dirstate + if node == parent: + if exact: ui.warn(_('no changes needed to %s\n') % rel) + continue + if pmf is None: + # only need parent manifest in this unlikely case, + # so do not read by default + pmf = repo.changectx(parent).manifest() + if abs in pmf: + if mfentry: + # if version of file is same in parent and target + # manifests, do nothing + if pmf[abs] != mfentry: + handle(revert, False) + else: + handle(remove, False) + + if not opts.get('dry_run'): + for f in forget[0]: + repo.dirstate.forget(f) + r = hg.revert(repo, node, update.has_key) + for f in add[0]: + repo.dirstate.add(f) + for f in undelete[0]: + repo.dirstate.normal(f) + for f in remove[0]: + repo.dirstate.remove(f) + return r + finally: + del wlock def rollback(ui, repo): """roll back the last transaction in this repository @@ -2477,7 +2497,7 @@ def serve(ui, repo, **opts): parentui = ui.parentui or ui optlist = ("name templates style address port ipv6" - " accesslog errorlog webdir_conf") + " accesslog errorlog webdir_conf certificate") for o in optlist.split(): if opts[o]: parentui.setconfig("web", o, str(opts[o])) @@ -2660,7 +2680,6 @@ def unbundle(ui, repo, fname1, *fnames, bundle command. """ fnames = (fname1,) + fnames - result = None for fname in fnames: if os.path.exists(fname): f = open(fname, "rb") @@ -2767,6 +2786,11 @@ commitopts = [ ('l', 'logfile', '', _('read commit message from ')), ] +commitopts2 = [ + ('d', 'date', '', _('record datecode as commit date')), + ('u', 'user', '', _('record user as committer')), +] + table = { "^add": (add, walkopts + dryrunopts, _('hg add [OPTION]... [FILE]...')), "addremove": @@ -2784,8 +2808,10 @@ table = { ('d', 'date', None, _('list the date')), ('n', 'number', None, _('list the revision number (default)')), ('c', 'changeset', None, _('list the changeset')), + ('l', 'line-number', None, + _('show line number at the first appearance')) ] + walkopts, - _('hg annotate [-r REV] [-f] [-a] [-u] [-d] [-n] [-c] FILE...')), + _('hg annotate [-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...')), "archive": (archive, [('', 'no-decode', None, _('do not pass files through decoders')), @@ -2798,11 +2824,9 @@ table = { (backout, [('', 'merge', None, _('merge with old dirstate parent after backout')), - ('d', 'date', '', _('record datecode as commit date')), ('', 'parent', '', _('parent to choose when backing out merge')), - ('u', 'user', '', _('record user as committer')), ('r', 'rev', '', _('revision to backout')), - ] + walkopts + commitopts, + ] + walkopts + commitopts + commitopts2, _('hg backout [OPTION]... [-r] REV')), "branch": (branch, @@ -2844,9 +2868,7 @@ table = { (commit, [('A', 'addremove', None, _('mark new/missing files as added/removed before committing')), - ('d', 'date', '', _('record datecode as commit date')), - ('u', 'user', '', _('record user as commiter')), - ] + walkopts + commitopts, + ] + walkopts + commitopts + commitopts2, _('hg commit [OPTION]... [FILE]...')), "copy|cp": (copy, @@ -2992,7 +3014,8 @@ table = { ('', 'template', '', _('display with template')), ] + walkopts, _('hg log [OPTION]... [FILE]')), - "manifest": (manifest, [], _('hg manifest [REV]')), + "manifest": (manifest, [('r', 'rev', '', _('revision to display'))], + _('hg manifest [-r REV]')), "^merge": (merge, [('f', 'force', None, _('force a merge with outstanding changes')), @@ -3036,10 +3059,8 @@ table = { "debugrawcommit|rawcommit": (rawcommit, [('p', 'parent', [], _('parent')), - ('d', 'date', '', _('date code')), - ('u', 'user', '', _('user')), ('F', 'files', '', _('file list')) - ] + commitopts, + ] + commitopts + commitopts2, _('hg debugrawcommit [OPTION]... [FILE]...')), "recover": (recover, [], _('hg recover')), "^remove|rm": @@ -3085,7 +3106,8 @@ table = { ('', 'stdio', None, _('for remote clients')), ('t', 'templates', '', _('web templates to use')), ('', 'style', '', _('template style to use')), - ('6', 'ipv6', None, _('use IPv6 in addition to IPv4'))], + ('6', 'ipv6', None, _('use IPv6 in addition to IPv4')), + ('', 'certificate', '', _('SSL certificate file'))], _('hg serve [OPTION]...')), "^status|st": (status, @@ -3108,11 +3130,11 @@ table = { (tag, [('f', 'force', None, _('replace existing tag')), ('l', 'local', None, _('make the tag local')), - ('m', 'message', '', _('message for tag commit log entry')), - ('d', 'date', '', _('record datecode as commit date')), - ('u', 'user', '', _('record user as commiter')), ('r', 'rev', '', _('revision to tag')), - ('', 'remove', None, _('remove a tag'))], + ('', 'remove', None, _('remove a tag')), + # -l/--local is already there, commitopts cannot be used + ('m', 'message', '', _('use as commit message')), + ] + commitopts2, _('hg tag [-l] [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME')), "tags": (tags, [], _('hg tags')), "tip": @@ -3138,15 +3160,4 @@ table = { norepo = ("clone init version help debugancestor debugcomplete debugdata" " debugindex debugindexdot debugdate debuginstall") -optionalrepo = ("paths serve showconfig") - -def dispatch(args): - try: - u = ui.ui(traceback='--traceback' in args) - except util.Abort, inst: - sys.stderr.write(_("abort: %s\n") % inst) - return -1 - return cmdutil.runcatch(u, args) - -def run(): - sys.exit(dispatch(sys.argv[1:])) +optionalrepo = ("identify paths serve showconfig") diff --git a/mercurial/context.py b/mercurial/context.py --- a/mercurial/context.py +++ b/mercurial/context.py @@ -60,6 +60,18 @@ class changectx(object): else: raise AttributeError, name + def __contains__(self, key): + return key in self._manifest + + def __getitem__(self, key): + return self.filectx(key) + + def __iter__(self): + a = self._manifest.keys() + a.sort() + for f in a: + return f + def changeset(self): return self._changeset def manifest(self): return self._manifest @@ -184,7 +196,7 @@ class filectx(object): def __eq__(self, other): try: return (self._path == other._path - and self._changeid == other._changeid) + and self._fileid == other._fileid) except AttributeError: return False @@ -240,14 +252,32 @@ class filectx(object): return [filectx(self._repo, self._path, fileid=x, filelog=self._filelog) for x in c] - def annotate(self, follow=False): + def annotate(self, follow=False, linenumber=None): '''returns a list of tuples of (ctx, line) for each line in the file, where ctx is the filectx of the node where - that line was last changed''' + that line was last changed. + This returns tuples of ((ctx, linenumber), line) for each line, + if "linenumber" parameter is NOT "None". + In such tuples, linenumber means one at the first appearance + in the managed file. + To reduce annotation cost, + this returns fixed value(False is used) as linenumber, + if "linenumber" parameter is "False".''' - def decorate(text, rev): + def decorate_compat(text, rev): return ([rev] * len(text.splitlines()), text) + def without_linenumber(text, rev): + return ([(rev, False)] * len(text.splitlines()), text) + + def with_linenumber(text, rev): + size = len(text.splitlines()) + return ([(rev, i) for i in xrange(1, size + 1)], text) + + decorate = (((linenumber is None) and decorate_compat) or + (linenumber and with_linenumber) or + without_linenumber) + def pair(parent, child): for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]): child[0][b1:b2] = parent[0][a1:a2] diff --git a/mercurial/diffhelpers.c b/mercurial/diffhelpers.c new file mode 100644 --- /dev/null +++ b/mercurial/diffhelpers.c @@ -0,0 +1,150 @@ +/* + * diffhelpers.c - helper routines for mpatch + * + * Copyright 2007 Chris Mason + * + * This software may be used and distributed according to the terms + * of the GNU General Public License v2, incorporated herein by reference. + */ + +#include +#include +#include + +static char diffhelpers_doc[] = "Efficient diff parsing"; +static PyObject *diffhelpers_Error; + + +/* fixup the last lines of a and b when the patch has no newline at eof */ +static void _fix_newline(PyObject *hunk, PyObject *a, PyObject *b) +{ + int hunksz = PyList_Size(hunk); + PyObject *s = PyList_GET_ITEM(hunk, hunksz-1); + char *l = PyString_AS_STRING(s); + int sz = PyString_GET_SIZE(s); + int alen = PyList_Size(a); + int blen = PyList_Size(b); + char c = l[0]; + + PyObject *hline = PyString_FromStringAndSize(l, sz-1); + if (c == ' ' || c == '+') { + PyObject *rline = PyString_FromStringAndSize(l+1, sz-2); + PyList_SetItem(b, blen-1, rline); + } + if (c == ' ' || c == '-') { + Py_INCREF(hline); + PyList_SetItem(a, alen-1, hline); + } + PyList_SetItem(hunk, hunksz-1, hline); +} + +/* python callable form of _fix_newline */ +static PyObject * +fix_newline(PyObject *self, PyObject *args) +{ + PyObject *hunk, *a, *b; + if (!PyArg_ParseTuple(args, "OOO", &hunk, &a, &b)) + return NULL; + _fix_newline(hunk, a, b); + return Py_BuildValue("l", 0); +} + +/* + * read lines from fp into the hunk. The hunk is parsed into two arrays + * a and b. a gets the old state of the text, b gets the new state + * The control char from the hunk is saved when inserting into a, but not b + * (for performance while deleting files) + */ +static PyObject * +addlines(PyObject *self, PyObject *args) +{ + + PyObject *fp, *hunk, *a, *b, *x; + int i; + int lena, lenb; + int num; + int todoa, todob; + char *s, c; + PyObject *l; + if (!PyArg_ParseTuple(args, "OOiiOO", &fp, &hunk, &lena, &lenb, &a, &b)) + return NULL; + + while(1) { + todoa = lena - PyList_Size(a); + todob = lenb - PyList_Size(b); + num = todoa > todob ? todoa : todob; + if (num == 0) + break; + for (i = 0 ; i < num ; i++) { + x = PyFile_GetLine(fp, 0); + s = PyString_AS_STRING(x); + c = *s; + if (strcmp(s, "\\ No newline at end of file\n") == 0) { + _fix_newline(hunk, a, b); + continue; + } + PyList_Append(hunk, x); + if (c == '+') { + l = PyString_FromString(s + 1); + PyList_Append(b, l); + Py_DECREF(l); + } else if (c == '-') { + PyList_Append(a, x); + } else { + l = PyString_FromString(s + 1); + PyList_Append(b, l); + Py_DECREF(l); + PyList_Append(a, x); + } + Py_DECREF(x); + } + } + return Py_BuildValue("l", 0); +} + +/* + * compare the lines in a with the lines in b. a is assumed to have + * a control char at the start of each line, this char is ignored in the + * compare + */ +static PyObject * +testhunk(PyObject *self, PyObject *args) +{ + + PyObject *a, *b; + long bstart; + int alen, blen; + int i; + char *sa, *sb; + + if (!PyArg_ParseTuple(args, "OOl", &a, &b, &bstart)) + return NULL; + alen = PyList_Size(a); + blen = PyList_Size(b); + if (alen > blen - bstart) { + return Py_BuildValue("l", -1); + } + for (i = 0 ; i < alen ; i++) { + sa = PyString_AS_STRING(PyList_GET_ITEM(a, i)); + sb = PyString_AS_STRING(PyList_GET_ITEM(b, i + bstart)); + if (strcmp(sa+1, sb) != 0) + return Py_BuildValue("l", -1); + } + return Py_BuildValue("l", 0); +} + +static PyMethodDef methods[] = { + {"addlines", addlines, METH_VARARGS, "add lines to a hunk\n"}, + {"fix_newline", fix_newline, METH_VARARGS, "fixup newline counters\n"}, + {"testhunk", testhunk, METH_VARARGS, "test lines in a hunk\n"}, + {NULL, NULL} +}; + +PyMODINIT_FUNC +initdiffhelpers(void) +{ + Py_InitModule3("diffhelpers", methods, diffhelpers_doc); + diffhelpers_Error = PyErr_NewException("diffhelpers.diffhelpersError", + NULL, NULL); +} + diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py --- a/mercurial/dirstate.py +++ b/mercurial/dirstate.py @@ -20,8 +20,8 @@ class dirstate(object): def __init__(self, opener, ui, root): self._opener = opener self._root = root - self._dirty = 0 - self._dirtypl = 0 + self._dirty = False + self._dirtypl = False self._ui = ui def __getattr__(self, name): @@ -53,7 +53,7 @@ class dirstate(object): self._incpath(f) return self._dirs elif name == '_ignore': - files = [self.wjoin('.hgignore')] + files = [self._join('.hgignore')] for name, path in self._ui.configitems("ui"): if name == 'ignore' or name.startswith('ignore.'): files.append(os.path.expanduser(path)) @@ -65,7 +65,7 @@ class dirstate(object): else: raise AttributeError, name - def wjoin(self, f): + def _join(self, f): return os.path.join(self._root, f) def getcwd(self): @@ -89,11 +89,14 @@ class dirstate(object): return path.replace(os.sep, '/') return path - def __del__(self): - self.write() - def __getitem__(self, key): - return self._map[key] + ''' current states: + n normal + m needs merging + r marked for removal + a marked for addition + ? not tracked''' + return self._map.get(key, ("?",))[0] def __contains__(self, key): return key in self._map @@ -110,21 +113,14 @@ class dirstate(object): def branch(self): return self._branch - def markdirty(self): - self._dirty = 1 - def setparents(self, p1, p2=nullid): - self.markdirty() - self._dirtypl = 1 + self._dirty = self._dirtypl = True self._pl = p1, p2 def setbranch(self, branch): self._branch = branch self._opener("branch", "w").write(branch + '\n') - def state(self, key): - return self._map.get(key, ("?",))[0] - def _read(self): self._map = {} self._copymap = {} @@ -145,31 +141,29 @@ class dirstate(object): dmap = self._map copymap = self._copymap unpack = struct.unpack - - pos = 40 e_size = struct.calcsize(_format) + pos1 = 40 + l = len(st) - while pos < len(st): - newpos = pos + e_size - e = unpack(_format, st[pos:newpos]) - l = e[4] - pos = newpos - newpos = pos + l - f = st[pos:newpos] + # the inner loop + while pos1 < l: + pos2 = pos1 + e_size + e = unpack(">cllll", st[pos1:pos2]) # a literal here is faster + pos1 = pos2 + e[4] + f = st[pos2:pos1] if '\0' in f: f, c = f.split('\0') copymap[f] = c - dmap[f] = e[:4] - pos = newpos + dmap[f] = e # we hold onto e[4] because making a subtuple is slow def invalidate(self): for a in "_map _copymap _branch _pl _dirs _ignore".split(): if a in self.__dict__: delattr(self, a) - self._dirty = 0 + self._dirty = False def copy(self, source, dest): - self.markdirty() + self._dirty = True self._copymap[dest] = source def copied(self, file): @@ -179,102 +173,134 @@ class dirstate(object): return self._copymap def _incpath(self, path): - for c in strutil.findall(path, '/'): - pc = path[:c] - self._dirs.setdefault(pc, 0) - self._dirs[pc] += 1 + c = path.rfind('/') + if c >= 0: + dirs = self._dirs + base = path[:c] + if base not in dirs: + self._incpath(base) + dirs[base] = 1 + else: + dirs[base] += 1 def _decpath(self, path): - for c in strutil.findall(path, '/'): - pc = path[:c] - self._dirs.setdefault(pc, 0) - self._dirs[pc] -= 1 + if "_dirs" in self.__dict__: + c = path.rfind('/') + if c >= 0: + base = path[:c] + dirs = self._dirs + if dirs[base] == 1: + del dirs[base] + self._decpath(base) + else: + dirs[base] -= 1 def _incpathcheck(self, f): if '\r' in f or '\n' in f: raise util.Abort(_("'\\n' and '\\r' disallowed in filenames")) # shadows if f in self._dirs: - raise util.Abort(_('directory named %r already in dirstate') % f) + raise util.Abort(_('directory %r already in dirstate') % f) for c in strutil.rfindall(f, '/'): d = f[:c] if d in self._dirs: break if d in self._map: - raise util.Abort(_('file named %r already in dirstate') % d) + raise util.Abort(_('file %r in dirstate clashes with %r') % + (d, f)) self._incpath(f) - def update(self, files, state, **kw): - ''' current states: - n normal - m needs merging - r marked for removal - a marked for addition''' + def normal(self, f): + 'mark a file normal and clean' + self._dirty = True + s = os.lstat(self._join(f)) + self._map[f] = ('n', s.st_mode, s.st_size, s.st_mtime, 0) + if self._copymap.has_key(f): + del self._copymap[f] - if not files: return - self.markdirty() - for f in files: - if self._copymap.has_key(f): - del self._copymap[f] + def normallookup(self, f): + 'mark a file normal, but possibly dirty' + self._dirty = True + self._map[f] = ('n', 0, -1, -1, 0) + if f in self._copymap: + del self._copymap[f] + + def normaldirty(self, f): + 'mark a file normal, but dirty' + self._dirty = True + self._map[f] = ('n', 0, -2, -1, 0) + if f in self._copymap: + del self._copymap[f] - if state == "r": - self._map[f] = ('r', 0, 0, 0) - self._decpath(f) - continue - else: - if state == "a": - self._incpathcheck(f) - s = os.lstat(self.wjoin(f)) - st_size = kw.get('st_size', s.st_size) - st_mtime = kw.get('st_mtime', s.st_mtime) - self._map[f] = (state, s.st_mode, st_size, st_mtime) + def add(self, f): + 'mark a file added' + self._dirty = True + self._incpathcheck(f) + self._map[f] = ('a', 0, -1, -1, 0) + if f in self._copymap: + del self._copymap[f] + + def remove(self, f): + 'mark a file removed' + self._dirty = True + self._map[f] = ('r', 0, 0, 0, 0) + self._decpath(f) + if f in self._copymap: + del self._copymap[f] - def forget(self, files): - if not files: return - self.markdirty() - for f in files: - try: - del self._map[f] - self._decpath(f) - except KeyError: - self._ui.warn(_("not in dirstate: %s!\n") % f) - pass + def merge(self, f): + 'mark a file merged' + self._dirty = True + s = os.lstat(self._join(f)) + self._map[f] = ('m', s.st_mode, s.st_size, s.st_mtime, 0) + if f in self._copymap: + del self._copymap[f] + + def forget(self, f): + 'forget a file' + self._dirty = True + try: + del self._map[f] + self._decpath(f) + except KeyError: + self._ui.warn(_("not in dirstate: %s!\n") % f) def clear(self): self._map = {} self._copymap = {} self._pl = [nullid, nullid] - self.markdirty() + self._dirty = True def rebuild(self, parent, files): self.clear() for f in files: if files.execf(f): - self._map[f] = ('n', 0777, -1, 0) + self._map[f] = ('n', 0777, -1, 0, 0) else: - self._map[f] = ('n', 0666, -1, 0) + self._map[f] = ('n', 0666, -1, 0, 0) self._pl = (parent, nullid) - self.markdirty() + self._dirty = True def write(self): if not self._dirty: return cs = cStringIO.StringIO() - cs.write("".join(self._pl)) + copymap = self._copymap + pack = struct.pack + write = cs.write + write("".join(self._pl)) for f, e in self._map.iteritems(): - c = self.copied(f) - if c: - f = f + "\0" + c - e = struct.pack(_format, e[0], e[1], e[2], e[3], len(f)) - cs.write(e) - cs.write(f) + if f in copymap: + f = "%s\0%s" % (f, copymap[f]) + e = pack(_format, e[0], e[1], e[2], e[3], len(f)) + write(e) + write(f) st = self._opener("dirstate", "w", atomictemp=True) st.write(cs.getvalue()) st.rename() - self._dirty = 0 - self._dirtypl = 0 + self._dirty = self._dirtypl = False - def filterfiles(self, files): + def _filter(self, files): ret = {} unknown = [] @@ -304,16 +330,16 @@ class dirstate(object): bs += 1 return ret - def _supported(self, f, st, verbose=False): - if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode): + def _supported(self, f, mode, verbose=False): + if stat.S_ISREG(mode) or stat.S_ISLNK(mode): return True if verbose: kind = 'unknown' - if stat.S_ISCHR(st.st_mode): kind = _('character device') - elif stat.S_ISBLK(st.st_mode): kind = _('block device') - elif stat.S_ISFIFO(st.st_mode): kind = _('fifo') - elif stat.S_ISSOCK(st.st_mode): kind = _('socket') - elif stat.S_ISDIR(st.st_mode): kind = _('directory') + if stat.S_ISCHR(mode): kind = _('character device') + elif stat.S_ISBLK(mode): kind = _('block device') + elif stat.S_ISFIFO(mode): kind = _('fifo') + elif stat.S_ISSOCK(mode): kind = _('socket') + elif stat.S_ISDIR(mode): kind = _('directory') self._ui.warn(_('%s: unsupported file type (type is %s)\n') % (self.pathto(f), kind)) return False @@ -345,7 +371,7 @@ class dirstate(object): dc = self._map.copy() else: files = util.unique(files) - dc = self.filterfiles(files) + dc = self._filter(files) def imatch(file_): if file_ not in dc and self._ignore(file_): @@ -361,59 +387,73 @@ class dirstate(object): common_prefix_len = len(self._root) if not self._root.endswith(os.sep): common_prefix_len += 1 + + normpath = util.normpath + listdir = os.listdir + lstat = os.lstat + bisect_left = bisect.bisect_left + isdir = os.path.isdir + pconvert = util.pconvert + join = os.path.join + s_isdir = stat.S_ISDIR + supported = self._supported + _join = self._join + known = {'.hg': 1} + # recursion free walker, faster than os.walk. def findfiles(s): work = [s] + wadd = work.append + found = [] + add = found.append if directories: - yield 'd', util.normpath(s[common_prefix_len:]), os.lstat(s) + add((normpath(s[common_prefix_len:]), 'd', lstat(s))) while work: top = work.pop() - names = os.listdir(top) + names = listdir(top) names.sort() # nd is the top of the repository dir tree - nd = util.normpath(top[common_prefix_len:]) + nd = normpath(top[common_prefix_len:]) if nd == '.': nd = '' else: # do not recurse into a repo contained in this # one. use bisect to find .hg directory so speed # is good on big directory. - hg = bisect.bisect_left(names, '.hg') + hg = bisect_left(names, '.hg') if hg < len(names) and names[hg] == '.hg': - if os.path.isdir(os.path.join(top, '.hg')): + if isdir(join(top, '.hg')): continue for f in names: - np = util.pconvert(os.path.join(nd, f)) - if seen(np): + np = pconvert(join(nd, f)) + if np in known: continue - p = os.path.join(top, f) + known[np] = 1 + p = join(top, f) # don't trip over symlinks - st = os.lstat(p) - if stat.S_ISDIR(st.st_mode): + st = lstat(p) + if s_isdir(st.st_mode): if not ignore(np): - work.append(p) + wadd(p) if directories: - yield 'd', np, st - if imatch(np) and np in dc: - yield 'm', np, st + add((np, 'd', st)) + if np in dc and match(np): + add((np, 'm', st)) elif imatch(np): - if self._supported(np, st): - yield 'f', np, st + if supported(np, st.st_mode): + add((np, 'f', st)) elif np in dc: - yield 'm', np, st - - known = {'.hg': 1} - def seen(fn): - if fn in known: return True - known[fn] = 1 + add((np, 'm', st)) + found.sort() + return found # step one, find all files that match our criteria files.sort() for ff in files: - nf = util.normpath(ff) - f = self.wjoin(ff) + nf = normpath(ff) + f = _join(ff) try: - st = os.lstat(f) + st = lstat(f) except OSError, inst: found = False for fn in dc: @@ -427,15 +467,15 @@ class dirstate(object): elif badmatch and badmatch(ff) and imatch(nf): yield 'b', ff, None continue - if stat.S_ISDIR(st.st_mode): - cmp1 = (lambda x, y: cmp(x[1], y[1])) - sorted_ = [ x for x in findfiles(f) ] - sorted_.sort(cmp1) - for e in sorted_: - yield e + if s_isdir(st.st_mode): + for f, src, st in findfiles(f): + yield src, f, st else: - if not seen(nf) and match(nf): - if self._supported(ff, st, verbose=True): + if nf in known: + continue + known[nf] = 1 + if match(nf): + if supported(ff, st.st_mode, verbose=True): yield 'f', nf, st elif ff in dc: yield 'm', nf, st @@ -445,58 +485,74 @@ class dirstate(object): ks = dc.keys() ks.sort() for k in ks: - if not seen(k) and imatch(k): + if k in known: + continue + known[k] = 1 + if imatch(k): yield 'm', k, None - def status(self, files=None, match=util.always, list_ignored=False, - list_clean=False): + def status(self, files, match, list_ignored, list_clean): lookup, modified, added, unknown, ignored = [], [], [], [], [] removed, deleted, clean = [], [], [] + _join = self._join + lstat = os.lstat + cmap = self._copymap + dmap = self._map + ladd = lookup.append + madd = modified.append + aadd = added.append + uadd = unknown.append + iadd = ignored.append + radd = removed.append + dadd = deleted.append + cadd = clean.append + for src, fn, st in self.statwalk(files, match, ignored=list_ignored): - try: - type_, mode, size, time = self[fn] - except KeyError: + if fn in dmap: + type_, mode, size, time, foo = dmap[fn] + else: if list_ignored and self._ignore(fn): - ignored.append(fn) + iadd(fn) else: - unknown.append(fn) + uadd(fn) continue if src == 'm': nonexistent = True if not st: try: - st = os.lstat(self.wjoin(fn)) + st = lstat(_join(fn)) except OSError, inst: if inst.errno != errno.ENOENT: raise st = None # We need to re-check that it is a valid file - if st and self._supported(fn, st): + if st and self._supported(fn, st.st_mode): nonexistent = False # XXX: what to do with file no longer present in the fs # who are not removed in the dirstate ? if nonexistent and type_ in "nm": - deleted.append(fn) + dadd(fn) continue # check the common case first if type_ == 'n': if not st: - st = os.lstat(self.wjoin(fn)) + st = lstat(_join(fn)) if (size >= 0 and (size != st.st_size or (mode ^ st.st_mode) & 0100) + or size == -2 or fn in self._copymap): - modified.append(fn) + madd(fn) elif time != int(st.st_mtime): - lookup.append(fn) + ladd(fn) elif list_clean: - clean.append(fn) + cadd(fn) elif type_ == 'm': - modified.append(fn) + madd(fn) elif type_ == 'a': - added.append(fn) + aadd(fn) elif type_ == 'r': - removed.append(fn) + radd(fn) return (lookup, modified, added, removed, deleted, unknown, ignored, clean) diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py new file mode 100644 --- /dev/null +++ b/mercurial/dispatch.py @@ -0,0 +1,401 @@ +# dispatch.py - command dispatching for mercurial +# +# Copyright 2005-2007 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 _ +import os, sys, atexit, signal, pdb, traceback, socket, errno, shlex, time +import util, commands, hg, lock, fancyopts, revlog, version, extensions, hook +import cmdutil +import ui as _ui + +class ParseError(Exception): + """Exception raised on errors in parsing the command line.""" + +def run(): + "run the command in sys.argv" + sys.exit(dispatch(sys.argv[1:])) + +def dispatch(args): + "run the command specified in args" + try: + u = _ui.ui(traceback='--traceback' in args) + except util.Abort, inst: + sys.stderr.write(_("abort: %s\n") % inst) + return -1 + return _runcatch(u, args) + +def _runcatch(ui, args): + def catchterm(*args): + raise util.SignalInterrupt + + for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM': + num = getattr(signal, name, None) + if num: signal.signal(num, catchterm) + + try: + try: + # enter the debugger before command execution + if '--debugger' in args: + pdb.set_trace() + try: + return _dispatch(ui, args) + finally: + ui.flush() + except: + # enter the debugger when we hit an exception + if '--debugger' in args: + pdb.post_mortem(sys.exc_info()[2]) + ui.print_exc() + raise + + except ParseError, inst: + if inst.args[0]: + ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1])) + commands.help_(ui, inst.args[0]) + else: + ui.warn(_("hg: %s\n") % inst.args[1]) + commands.help_(ui, 'shortlist') + except cmdutil.AmbiguousCommand, inst: + ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") % + (inst.args[0], " ".join(inst.args[1]))) + except cmdutil.UnknownCommand, inst: + ui.warn(_("hg: unknown command '%s'\n") % inst.args[0]) + commands.help_(ui, 'shortlist') + except hg.RepoError, inst: + ui.warn(_("abort: %s!\n") % inst) + except lock.LockHeld, inst: + if inst.errno == errno.ETIMEDOUT: + reason = _('timed out waiting for lock held by %s') % inst.locker + else: + reason = _('lock held by %s') % inst.locker + ui.warn(_("abort: %s: %s\n") % (inst.desc or inst.filename, reason)) + except lock.LockUnavailable, inst: + ui.warn(_("abort: could not lock %s: %s\n") % + (inst.desc or inst.filename, inst.strerror)) + except revlog.RevlogError, inst: + ui.warn(_("abort: %s!\n") % inst) + except util.SignalInterrupt: + ui.warn(_("killed!\n")) + except KeyboardInterrupt: + try: + ui.warn(_("interrupted!\n")) + except IOError, inst: + if inst.errno == errno.EPIPE: + if ui.debugflag: + ui.warn(_("\nbroken pipe\n")) + else: + raise + except socket.error, inst: + ui.warn(_("abort: %s\n") % inst[1]) + except IOError, inst: + if hasattr(inst, "code"): + ui.warn(_("abort: %s\n") % inst) + elif hasattr(inst, "reason"): + try: # usually it is in the form (errno, strerror) + reason = inst.reason.args[1] + except: # it might be anything, for example a string + reason = inst.reason + ui.warn(_("abort: error: %s\n") % reason) + elif hasattr(inst, "args") and inst[0] == errno.EPIPE: + if ui.debugflag: + ui.warn(_("broken pipe\n")) + elif getattr(inst, "strerror", None): + if getattr(inst, "filename", None): + ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename)) + else: + ui.warn(_("abort: %s\n") % inst.strerror) + else: + raise + except OSError, inst: + if getattr(inst, "filename", None): + ui.warn(_("abort: %s: %s\n") % (inst.strerror, inst.filename)) + else: + ui.warn(_("abort: %s\n") % inst.strerror) + except util.UnexpectedOutput, inst: + ui.warn(_("abort: %s") % inst[0]) + if not isinstance(inst[1], basestring): + ui.warn(" %r\n" % (inst[1],)) + elif not inst[1]: + ui.warn(_(" empty string\n")) + else: + ui.warn("\n%r\n" % util.ellipsis(inst[1])) + except ImportError, inst: + m = str(inst).split()[-1] + ui.warn(_("abort: could not import module %s!\n" % m)) + if m in "mpatch bdiff".split(): + ui.warn(_("(did you forget to compile extensions?)\n")) + elif m in "zlib".split(): + ui.warn(_("(is your Python install correct?)\n")) + + except util.Abort, inst: + ui.warn(_("abort: %s\n") % inst) + except SystemExit, inst: + # Commands shouldn't sys.exit directly, but give a return code. + # Just in case catch this and and pass exit code to caller. + return inst.code + except: + ui.warn(_("** unknown exception encountered, details follow\n")) + ui.warn(_("** report bug details to " + "http://www.selenic.com/mercurial/bts\n")) + ui.warn(_("** or mercurial@selenic.com\n")) + ui.warn(_("** Mercurial Distributed SCM (version %s)\n") + % version.get_version()) + raise + + return -1 + +def _findrepo(): + p = os.getcwd() + while not os.path.isdir(os.path.join(p, ".hg")): + oldp, p = p, os.path.dirname(p) + if p == oldp: + return None + + return p + +def _parse(ui, args): + options = {} + cmdoptions = {} + + try: + args = fancyopts.fancyopts(args, commands.globalopts, options) + except fancyopts.getopt.GetoptError, inst: + raise ParseError(None, inst) + + if args: + cmd, args = args[0], args[1:] + aliases, i = cmdutil.findcmd(ui, cmd, commands.table) + cmd = aliases[0] + defaults = ui.config("defaults", cmd) + if defaults: + args = shlex.split(defaults) + args + c = list(i[1]) + else: + cmd = None + c = [] + + # combine global options into local + for o in commands.globalopts: + c.append((o[0], o[1], options[o[1]], o[3])) + + try: + args = fancyopts.fancyopts(args, c, cmdoptions) + except fancyopts.getopt.GetoptError, inst: + raise ParseError(cmd, inst) + + # separate global options back out + for o in commands.globalopts: + n = o[1] + options[n] = cmdoptions[n] + del cmdoptions[n] + + return (cmd, cmd and i[0] or None, args, options, cmdoptions) + +def _parseconfig(config): + """parse the --config options from the command line""" + parsed = [] + for cfg in config: + try: + name, value = cfg.split('=', 1) + section, name = name.split('.', 1) + if not section or not name: + raise IndexError + parsed.append((section, name, value)) + except (IndexError, ValueError): + raise util.Abort(_('malformed --config option: %s') % cfg) + return parsed + +def _earlygetopt(aliases, args): + """Return list of values for an option (or aliases). + + The values are listed in the order they appear in args. + The options and values are removed from args. + """ + try: + argcount = args.index("--") + except ValueError: + argcount = len(args) + shortopts = [opt for opt in aliases if len(opt) == 2] + values = [] + pos = 0 + while pos < argcount: + if args[pos] in aliases: + if pos + 1 >= argcount: + # ignore and let getopt report an error if there is no value + break + del args[pos] + values.append(args.pop(pos)) + argcount -= 2 + elif args[pos][:2] in shortopts: + # short option can have no following space, e.g. hg log -Rfoo + values.append(args.pop(pos)[2:]) + argcount -= 1 + else: + pos += 1 + return values + +_loaded = {} +def _dispatch(ui, args): + # read --config before doing anything else + # (e.g. to change trust settings for reading .hg/hgrc) + config = _earlygetopt(['--config'], args) + if config: + ui.updateopts(config=_parseconfig(config)) + + # check for cwd + cwd = _earlygetopt(['--cwd'], args) + if cwd: + os.chdir(cwd[-1]) + + # read the local repository .hgrc into a local ui object + path = _findrepo() or "" + if not path: + lui = ui + if path: + try: + lui = _ui.ui(parentui=ui) + lui.readconfig(os.path.join(path, ".hg", "hgrc")) + except IOError: + pass + + # now we can expand paths, even ones in .hg/hgrc + rpath = _earlygetopt(["-R", "--repository", "--repo"], args) + if rpath: + path = lui.expandpath(rpath[-1]) + lui = _ui.ui(parentui=ui) + lui.readconfig(os.path.join(path, ".hg", "hgrc")) + + extensions.loadall(lui) + for name, module in extensions.extensions(): + if name in _loaded: + continue + cmdtable = getattr(module, 'cmdtable', {}) + overrides = [cmd for cmd in cmdtable if cmd in commands.table] + if overrides: + ui.warn(_("extension '%s' overrides commands: %s\n") + % (name, " ".join(overrides))) + commands.table.update(cmdtable) + _loaded[name] = 1 + # check for fallback encoding + fallback = lui.config('ui', 'fallbackencoding') + if fallback: + util._fallbackencoding = fallback + + fullargs = args + cmd, func, args, options, cmdoptions = _parse(lui, args) + + if options["config"]: + raise util.Abort(_("Option --config may not be abbreviated!")) + if options["cwd"]: + raise util.Abort(_("Option --cwd may not be abbreviated!")) + if options["repository"]: + raise util.Abort(_( + "Option -R has to be separated from other options (i.e. not -qR) " + "and --repository may only be abbreviated as --repo!")) + + if options["encoding"]: + util._encoding = options["encoding"] + if options["encodingmode"]: + util._encodingmode = options["encodingmode"] + if options["time"]: + def get_times(): + t = os.times() + if t[4] == 0.0: # Windows leaves this as zero, so use time.clock() + t = (t[0], t[1], t[2], t[3], time.clock()) + return t + s = get_times() + def print_time(): + t = get_times() + ui.warn(_("Time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") % + (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3])) + atexit.register(print_time) + + ui.updateopts(options["verbose"], options["debug"], options["quiet"], + not options["noninteractive"], options["traceback"]) + + if options['help']: + return commands.help_(ui, cmd, options['version']) + elif options['version']: + return commands.version_(ui) + elif not cmd: + return commands.help_(ui, 'shortlist') + + repo = None + if cmd not in commands.norepo.split(): + try: + repo = hg.repository(ui, path=path) + ui = repo.ui + if not repo.local(): + raise util.Abort(_("repository '%s' is not local") % path) + except hg.RepoError: + if cmd not in commands.optionalrepo.split(): + if not path: + raise hg.RepoError(_("There is no Mercurial repository here" + " (.hg not found)")) + raise + d = lambda: func(ui, repo, *args, **cmdoptions) + else: + d = lambda: func(ui, *args, **cmdoptions) + + # run pre-hook, and abort if it fails + ret = hook.hook(ui, repo, "pre-%s" % cmd, False, args=" ".join(fullargs)) + if ret: + return ret + ret = _runcommand(ui, options, cmd, d) + # run post-hook, passing command result + hook.hook(ui, repo, "post-%s" % cmd, False, args=" ".join(fullargs), + result = ret) + return ret + +def _runcommand(ui, options, cmd, cmdfunc): + def checkargs(): + try: + return cmdfunc() + except TypeError, inst: + # was this an argument error? + tb = traceback.extract_tb(sys.exc_info()[2]) + if len(tb) != 2: # no + raise + raise ParseError(cmd, _("invalid arguments")) + + if options['profile']: + import hotshot, hotshot.stats + prof = hotshot.Profile("hg.prof") + try: + try: + return prof.runcall(checkargs) + except: + try: + ui.warn(_('exception raised - generating ' + 'profile anyway\n')) + except: + pass + raise + finally: + prof.close() + stats = hotshot.stats.load("hg.prof") + stats.strip_dirs() + stats.sort_stats('time', 'calls') + stats.print_stats(40) + elif options['lsprof']: + try: + from mercurial import lsprof + except ImportError: + raise util.Abort(_( + 'lsprof not available - install from ' + 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/')) + p = lsprof.Profiler() + p.enable(subcalls=True) + try: + return checkargs() + finally: + p.disable() + stats = lsprof.Stats(p.getstats()) + stats.sort() + stats.pprint(top=10, file=sys.stderr, climit=5) + else: + return checkargs() diff --git a/mercurial/extensions.py b/mercurial/extensions.py --- a/mercurial/extensions.py +++ b/mercurial/extensions.py @@ -6,10 +6,17 @@ # of the GNU General Public License, incorporated herein by reference. import imp, os -import commands, hg, util, sys +import util, sys from i18n import _ _extensions = {} +_order = [] + +def extensions(): + for name in _order: + module = _extensions[name] + if module: + yield name, module def find(name): '''return module with given extension name''' @@ -22,9 +29,13 @@ def find(name): raise KeyError(name) def load(ui, name, path): - if name in _extensions: + if name.startswith('hgext.'): + shortname = name[6:] + else: + shortname = name + if shortname in _extensions: return - _extensions[name] = None + _extensions[shortname] = None if path: # the module will be loaded in sys.modules # choose an unique name so that it doesn't @@ -48,20 +59,12 @@ def load(ui, name, path): mod = importh("hgext.%s" % name) except ImportError: mod = importh(name) - _extensions[name] = mod + _extensions[shortname] = mod + _order.append(shortname) uisetup = getattr(mod, 'uisetup', None) if uisetup: uisetup(ui) - reposetup = getattr(mod, 'reposetup', None) - if reposetup: - hg.repo_setup_hooks.append(reposetup) - cmdtable = getattr(mod, 'cmdtable', {}) - overrides = [cmd for cmd in cmdtable if cmd in commands.table] - if overrides: - ui.warn(_("extension '%s' overrides commands: %s\n") - % (name, " ".join(overrides))) - commands.table.update(cmdtable) def loadall(ui): result = ui.configitems("extensions") diff --git a/mercurial/hg.py b/mercurial/hg.py --- a/mercurial/hg.py +++ b/mercurial/hg.py @@ -10,7 +10,7 @@ from node import * from repo import * from i18n import _ import localrepo, bundlerepo, httprepo, sshrepo, statichttprepo -import errno, lock, os, shutil, util, cmdutil +import errno, lock, os, shutil, util, extensions import merge as _merge import verify as _verify @@ -18,16 +18,23 @@ def _local(path): return (os.path.isfile(util.drop_scheme('file', path)) and bundlerepo or localrepo) +def parseurl(url, revs): + '''parse url#branch, returning url, branch + revs''' + + if '#' not in url: + return url, (revs or None), None + + url, rev = url.split('#', 1) + return url, revs + [rev], rev + schemes = { 'bundle': bundlerepo, 'file': _local, - 'hg': httprepo, 'http': httprepo, 'https': httprepo, - 'old-http': statichttprepo, 'ssh': sshrepo, 'static-http': statichttprepo, - } +} def _lookup(path): scheme = 'file' @@ -50,14 +57,14 @@ def islocal(repo): return False return repo.local() -repo_setup_hooks = [] - def repository(ui, path='', create=False): """return a repository object for the specified path""" repo = _lookup(path).instance(ui, path, create) ui = getattr(repo, "ui", ui) - for hook in repo_setup_hooks: - hook(ui, repo) + for name, module in extensions.extensions(): + hook = getattr(module, 'reposetup', None) + if hook: + hook(ui, repo) return repo def defaultdest(source): @@ -99,7 +106,7 @@ def clone(ui, source, dest=None, pull=Fa """ origsource = source - source, rev, checkout = cmdutil.parseurl(ui.expandpath(source), rev) + source, rev, checkout = parseurl(ui.expandpath(source), rev) if isinstance(source, str): src_repo = repository(ui, source) @@ -134,109 +141,105 @@ def clone(ui, source, dest=None, pull=Fa if self.dir_: self.rmtree(self.dir_, True) - dir_cleanup = None - if islocal(dest): - dir_cleanup = DirCleanup(dest) + src_lock = dest_lock = dir_cleanup = None + try: + if islocal(dest): + dir_cleanup = DirCleanup(dest) - abspath = origsource - copy = False - if src_repo.local() and islocal(dest): - abspath = os.path.abspath(util.drop_scheme('file', origsource)) - copy = not pull and not rev + abspath = origsource + copy = False + if src_repo.local() and islocal(dest): + abspath = os.path.abspath(util.drop_scheme('file', origsource)) + copy = not pull and not rev - src_lock, dest_lock = None, None - if copy: - try: - # we use a lock here because if we race with commit, we - # can end up with extra data in the cloned revlogs that's - # not pointed to by changesets, thus causing verify to - # fail - src_lock = src_repo.lock() - except lock.LockException: - copy = False + if copy: + try: + # we use a lock here because if we race with commit, we + # can end up with extra data in the cloned revlogs that's + # not pointed to by changesets, thus causing verify to + # fail + src_lock = src_repo.lock() + except lock.LockException: + copy = False - if copy: - def force_copy(src, dst): - try: - util.copyfiles(src, dst) - except OSError, inst: - if inst.errno != errno.ENOENT: - raise + if copy: + def force_copy(src, dst): + try: + util.copyfiles(src, dst) + except OSError, inst: + if inst.errno != errno.ENOENT: + raise - src_store = os.path.realpath(src_repo.spath) - if not os.path.exists(dest): - os.mkdir(dest) - dest_path = os.path.realpath(os.path.join(dest, ".hg")) - os.mkdir(dest_path) - if src_repo.spath != src_repo.path: - # XXX racy - dummy_changelog = os.path.join(dest_path, "00changelog.i") - # copy the dummy changelog - force_copy(src_repo.join("00changelog.i"), dummy_changelog) - dest_store = os.path.join(dest_path, "store") - os.mkdir(dest_store) - else: - dest_store = dest_path - # copy the requires file - force_copy(src_repo.join("requires"), - os.path.join(dest_path, "requires")) - # we lock here to avoid premature writing to the target - dest_lock = lock.lock(os.path.join(dest_store, "lock")) + src_store = os.path.realpath(src_repo.spath) + if not os.path.exists(dest): + os.mkdir(dest) + dest_path = os.path.realpath(os.path.join(dest, ".hg")) + os.mkdir(dest_path) + if src_repo.spath != src_repo.path: + # XXX racy + dummy_changelog = os.path.join(dest_path, "00changelog.i") + # copy the dummy changelog + force_copy(src_repo.join("00changelog.i"), dummy_changelog) + dest_store = os.path.join(dest_path, "store") + os.mkdir(dest_store) + else: + dest_store = dest_path + # copy the requires file + force_copy(src_repo.join("requires"), + os.path.join(dest_path, "requires")) + # we lock here to avoid premature writing to the target + dest_lock = lock.lock(os.path.join(dest_store, "lock")) - files = ("data", - "00manifest.d", "00manifest.i", - "00changelog.d", "00changelog.i") - for f in files: - src = os.path.join(src_store, f) - dst = os.path.join(dest_store, f) - force_copy(src, dst) + files = ("data", + "00manifest.d", "00manifest.i", + "00changelog.d", "00changelog.i") + for f in files: + src = os.path.join(src_store, f) + dst = os.path.join(dest_store, f) + force_copy(src, dst) + + # we need to re-init the repo after manually copying the data + # into it + dest_repo = repository(ui, dest) + + else: + dest_repo = repository(ui, dest, create=True) - # we need to re-init the repo after manually copying the data - # into it - dest_repo = repository(ui, dest) - - else: - dest_repo = repository(ui, dest, create=True) + revs = None + if rev: + if 'lookup' not in src_repo.capabilities: + raise util.Abort(_("src repository does not support revision " + "lookup and so doesn't support clone by " + "revision")) + revs = [src_repo.lookup(r) for r in rev] - revs = None - if rev: - if 'lookup' not in src_repo.capabilities: - raise util.Abort(_("src repository does not support revision " - "lookup and so doesn't support clone by " - "revision")) - revs = [src_repo.lookup(r) for r in rev] + if dest_repo.local(): + dest_repo.clone(src_repo, heads=revs, stream=stream) + elif src_repo.local(): + src_repo.push(dest_repo, revs=revs) + else: + raise util.Abort(_("clone from remote to remote not supported")) + + if dir_cleanup: + dir_cleanup.close() if dest_repo.local(): - dest_repo.clone(src_repo, heads=revs, stream=stream) - elif src_repo.local(): - src_repo.push(dest_repo, revs=revs) - else: - raise util.Abort(_("clone from remote to remote not supported")) - - if src_lock: - src_lock.release() - - if dir_cleanup: - dir_cleanup.close() + fp = dest_repo.opener("hgrc", "w", text=True) + fp.write("[paths]\n") + fp.write("default = %s\n" % abspath) + fp.close() - if dest_repo.local(): - fp = dest_repo.opener("hgrc", "w", text=True) - fp.write("[paths]\n") - fp.write("default = %s\n" % abspath) - fp.close() - - if dest_lock: - dest_lock.release() + if update: + if not checkout: + try: + checkout = dest_repo.lookup("default") + except: + checkout = dest_repo.changelog.tip() + _update(dest_repo, checkout) - if update: - if not checkout: - try: - checkout = dest_repo.lookup("default") - except: - checkout = dest_repo.changelog.tip() - _update(dest_repo, checkout) - - return src_repo, dest_repo + return src_repo, dest_repo + finally: + del src_lock, dest_lock, dir_cleanup def _showstats(repo, stats): stats = ((stats[0], _("updated")), @@ -251,7 +254,7 @@ def _update(repo, node): return update(r def update(repo, node): """update the working directory to node, merging linear changes""" pl = repo.parents() - stats = _merge.update(repo, node, False, False, None, None) + stats = _merge.update(repo, node, False, False, None) _showstats(repo, stats) if stats[3]: repo.ui.status(_("There are unresolved merges with" @@ -265,15 +268,15 @@ def update(repo, node): % (pl[0].rev(), repo.changectx(node).rev())) return stats[3] -def clean(repo, node, wlock=None, show_stats=True): +def clean(repo, node, show_stats=True): """forcibly switch the working directory to node, clobbering changes""" - stats = _merge.update(repo, node, False, True, None, wlock) + stats = _merge.update(repo, node, False, True, None) if show_stats: _showstats(repo, stats) return stats[3] -def merge(repo, node, force=None, remind=True, wlock=None): +def merge(repo, node, force=None, remind=True): """branch merge with node, resolving changes""" - stats = _merge.update(repo, node, True, force, False, wlock) + stats = _merge.update(repo, node, True, force, False) _showstats(repo, stats) if stats[3]: pl = repo.parents() @@ -286,9 +289,9 @@ def merge(repo, node, force=None, remind repo.ui.status(_("(branch merge, don't forget to commit)\n")) return stats[3] -def revert(repo, node, choose, wlock): +def revert(repo, node, choose): """revert changes to revision in node without updating dirstate""" - return _merge.update(repo, node, False, True, choose, wlock)[3] + return _merge.update(repo, node, False, True, choose)[3] def verify(repo): """verify the consistency of a repository""" 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 @@ -64,8 +64,9 @@ 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(report_untrusted=False), repo) + if isinstance(repo, str): + parentui = ui.ui(report_untrusted=False, interactive=False) + self.repo = hg.repository(parentui, repo) else: self.repo = repo @@ -140,7 +141,10 @@ class hgweb(object): def nodebranchdict(self, ctx): branches = [] branch = ctx.branch() - if self.repo.branchtags()[branch] == ctx.node(): + # If this is an empty repo, ctx.node() == nullid, + # ctx.branch() == 'default', but branchtags() is + # an empty dict. Using dict.get avoids a traceback. + if self.repo.branchtags().get(branch) == ctx.node(): branches.append({"name": branch}) return branches @@ -206,7 +210,7 @@ class hgweb(object): opts=diffopts), f, tn) def changelog(self, ctx, shortlog=False): - def changelist(**map): + def changelist(limit=0,**map): cl = self.repo.changelog l = [] # build a list in forward order for efficiency for i in xrange(start, end): @@ -226,6 +230,9 @@ class hgweb(object): "tags": self.nodetagsdict(n), "branches": self.nodebranchdict(ctx)}) + if limit > 0: + l = l[:limit] + for e in l: yield e @@ -243,7 +250,9 @@ class hgweb(object): yield self.t(shortlog and 'shortlog' or 'changelog', changenav=changenav, node=hex(cl.tip()), - rev=pos, changesets=count, entries=changelist, + rev=pos, changesets=count, + entries=lambda **x: changelist(limit=0,**x), + latestentry=lambda **x: changelist(limit=1,**x), archives=self.archivelist("tip")) def search(self, query): @@ -344,7 +353,7 @@ class hgweb(object): pos = end - 1 parity = paritygen(self.stripecount, offset=start-end) - def entries(**map): + def entries(limit=0, **map): l = [] for i in xrange(start, end): @@ -362,13 +371,17 @@ class hgweb(object): "child": self.siblings(fctx.children()), "desc": ctx.description()}) + if limit > 0: + l = l[:limit] + for e in l: yield e nodefunc = lambda x: fctx.filectx(fileid=x) nav = revnavgen(pos, pagelen, count, nodefunc) yield self.t("filelog", file=f, node=hex(fctx.node()), nav=nav, - entries=entries) + entries=lambda **x: entries(limit=0, **x), + latestentry=lambda **x: entries(limit=1, **x)) def filerevision(self, fctx): f = fctx.path() @@ -473,10 +486,12 @@ class hgweb(object): if not fnode: continue + fctx = ctx.filectx(full) yield {"file": full, "parity": parity.next(), "basename": f, - "size": ctx.filectx(full).size(), + "date": fctx.changectx().date(), + "size": fctx.size(), "permissions": mf.flags(full)} def dirlist(**map): @@ -508,10 +523,14 @@ class hgweb(object): i.reverse() parity = paritygen(self.stripecount) - def entries(notip=False, **map): + def entries(notip=False,limit=0, **map): + count = 0 for k, n in i: if notip and k == "tip": continue + if limit > 0 and count >= limit: + continue + count = count + 1 yield {"parity": parity.next(), "tag": k, "date": self.repo.changectx(n).date(), @@ -519,8 +538,9 @@ class hgweb(object): yield self.t("tags", node=hex(self.repo.changelog.tip()), - entries=lambda **x: entries(False, **x), - entriesnotip=lambda **x: entries(True, **x)) + entries=lambda **x: entries(False,0, **x), + entriesnotip=lambda **x: entries(True,0, **x), + latestentry=lambda **x: entries(True,1, **x)) def summary(self): i = self.repo.tagslist() @@ -787,9 +807,17 @@ class hgweb(object): style = req.form['style'][0] mapfile = style_map(self.templatepath, style) + proto = req.env.get('wsgi.url_scheme') + if proto == 'https': + proto = 'https' + default_port = "443" + else: + proto = 'http' + default_port = "80" + port = req.env["SERVER_PORT"] - port = port != "80" and (":" + port) or "" - urlbase = 'http://%s%s' % (req.env['SERVER_NAME'], port) + port = port != default_port and (":" + port) or "" + urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port) staticurl = self.config("web", "staticurl") or req.url + 'static/' if not staticurl.endswith('/'): staticurl += '/' @@ -1063,7 +1091,7 @@ class hgweb(object): # replayed ssl_req = self.configbool('web', 'push_ssl', True) if ssl_req: - if not req.env.get('HTTPS'): + if req.env.get('wsgi.url_scheme') != 'https': bail(_('ssl required\n')) return proto = 'https' @@ -1160,7 +1188,7 @@ class hgweb(object): req.write('%d\n' % ret) req.write(val) finally: - lock.release() + del lock except (OSError, IOError), inst: req.write('0\n') filename = getattr(inst, 'filename', '') 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 @@ -6,7 +6,6 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -from mercurial import demandimport; demandimport.enable() import os, mimetools, cStringIO from mercurial.i18n import gettext as _ from mercurial import ui, hg, util, templater @@ -83,7 +82,8 @@ class hgwebdir(object): else: yield config('web', 'motd', '') - parentui = self.parentui or ui.ui(report_untrusted=False) + parentui = self.parentui or ui.ui(report_untrusted=False, + interactive=False) def config(section, name, default=None, untrusted=True): return parentui.config(section, name, default, untrusted) @@ -91,8 +91,12 @@ class hgwebdir(object): url = req.env['REQUEST_URI'].split('?')[0] if not url.endswith('/'): url += '/' + pathinfo = req.env.get('PATH_INFO', '').strip('/') + '/' + base = url[:len(url) - len(pathinfo)] + if not base.endswith('/'): + base += '/' - staticurl = config('web', 'staticurl') or url + 'static/' + staticurl = config('web', 'staticurl') or base + 'static/' if not staticurl.endswith('/'): staticurl += '/' @@ -119,7 +123,7 @@ class hgwebdir(object): yield {"type" : i[0], "extension": i[1], "node": nodeid, "url": url} - def entries(sortcolumn="", descending=False, **map): + def entries(sortcolumn="", descending=False, subdir="", **map): def sessionvars(**map): fields = [] if req.form.has_key('style'): @@ -135,11 +139,16 @@ class hgwebdir(object): rows = [] parity = paritygen(self.stripecount) for name, path in self.repos: + if not name.startswith(subdir): + continue + name = name[len(subdir):] + u = ui.ui(parentui=parentui) try: u.readconfig(os.path.join(path, '.hg', 'hgrc')) - except IOError: - pass + except Exception, e: + u.warn(_('error reading %s/.hg/hgrc: %s\n' % (path, e))) + continue def get(section, name, default=None): return u.config(section, name, default, untrusted=True) @@ -186,6 +195,25 @@ class hgwebdir(object): row['parity'] = parity.next() yield row + def makeindex(req, subdir=""): + sortable = ["name", "description", "contact", "lastchange"] + sortcolumn, descending = self.repos_sorted + if req.form.has_key('sort'): + sortcolumn = req.form['sort'][0] + descending = sortcolumn.startswith('-') + if descending: + sortcolumn = sortcolumn[1:] + if sortcolumn not in sortable: + sortcolumn = "" + + sort = [("sort_%s" % column, + "%s%s" % ((not descending and column == sortcolumn) + and "-" or "", column)) + for column in sortable] + req.write(tmpl("index", entries=entries, subdir=subdir, + sortcolumn=sortcolumn, descending=descending, + **dict(sort))) + try: virtual = req.env.get("PATH_INFO", "").strip('/') if virtual.startswith('static/'): @@ -194,25 +222,32 @@ class hgwebdir(object): req.write(staticfile(static, fname, req) or tmpl('error', error='%r not found' % fname)) elif virtual: + repos = dict(self.repos) while virtual: - real = dict(self.repos).get(virtual) + real = repos.get(virtual) if real: - break + req.env['REPO_NAME'] = virtual + try: + repo = hg.repository(parentui, real) + hgweb(repo).run_wsgi(req) + except IOError, inst: + req.write(tmpl("error", error=inst.strerror)) + except hg.RepoError, inst: + req.write(tmpl("error", error=str(inst))) + return + + # browse subdirectories + subdir = virtual + '/' + if [r for r in repos if r.startswith(subdir)]: + makeindex(req, subdir) + return + up = virtual.rfind('/') if up < 0: break virtual = virtual[:up] - if real: - req.env['REPO_NAME'] = virtual - try: - repo = hg.repository(parentui, real) - hgweb(repo).run_wsgi(req) - except IOError, inst: - req.write(tmpl("error", error=inst.strerror)) - except hg.RepoError, inst: - req.write(tmpl("error", error=str(inst))) - else: - req.write(tmpl("notfound", repo=virtual)) + + req.write(tmpl("notfound", repo=virtual)) else: if req.form.has_key('static'): static = os.path.join(templater.templatepath(), "static") @@ -220,22 +255,6 @@ class hgwebdir(object): req.write(staticfile(static, fname, req) or tmpl("error", error="%r not found" % fname)) else: - sortable = ["name", "description", "contact", "lastchange"] - sortcolumn, descending = self.repos_sorted - if req.form.has_key('sort'): - sortcolumn = req.form['sort'][0] - descending = sortcolumn.startswith('-') - if descending: - sortcolumn = sortcolumn[1:] - if sortcolumn not in sortable: - sortcolumn = "" - - sort = [("sort_%s" % column, - "%s%s" % ((not descending and column == sortcolumn) - and "-" or "", column)) - for column in sortable] - req.write(tmpl("index", entries=entries, - sortcolumn=sortcolumn, descending=descending, - **dict(sort))) + makeindex(req) finally: tmpl = None diff --git a/mercurial/hgweb/server.py b/mercurial/hgweb/server.py --- a/mercurial/hgweb/server.py +++ b/mercurial/hgweb/server.py @@ -37,6 +37,9 @@ class _error_logger(object): self.handler.log_error("HG error: %s", msg) class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler): + + url_scheme = 'http' + def __init__(self, *args, **kargs): self.protocol_version = 'HTTP/1.1' BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs) @@ -53,13 +56,16 @@ class _hgwebhandler(object, BaseHTTPServ self.log_date_time_string(), format % args)) + def do_write(self): + try: + self.do_hgweb() + except socket.error, inst: + if inst[0] != errno.EPIPE: + raise + def do_POST(self): try: - try: - self.do_hgweb() - except socket.error, inst: - if inst[0] != errno.EPIPE: - raise + self.do_write() except StandardError, inst: self._start_response("500 Internal Server Error", []) self._write("Internal Server Error") @@ -101,7 +107,7 @@ class _hgwebhandler(object, BaseHTTPServ env[hkey] = hval env['SERVER_PROTOCOL'] = self.request_version env['wsgi.version'] = (1, 0) - env['wsgi.url_scheme'] = 'http' + env['wsgi.url_scheme'] = self.url_scheme env['wsgi.input'] = self.rfile env['wsgi.errors'] = _error_logger(self) env['wsgi.multithread'] = isinstance(self.server, @@ -164,6 +170,31 @@ class _hgwebhandler(object, BaseHTTPServ self.wfile.write(data) self.wfile.flush() +class _shgwebhandler(_hgwebhandler): + + url_scheme = 'https' + + def setup(self): + self.connection = self.request + self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) + self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) + + def do_write(self): + from OpenSSL.SSL import SysCallError + try: + super(_shgwebhandler, self).do_write() + except SysCallError, inst: + if inst.args[0] != errno.EPIPE: + raise + + def handle_one_request(self): + from OpenSSL.SSL import SysCallError, ZeroReturnError + try: + super(_shgwebhandler, self).handle_one_request() + except (SysCallError, ZeroReturnError): + self.close_connection = True + pass + def create_server(ui, repo): use_threads = True @@ -180,6 +211,7 @@ def create_server(ui, repo): port = int(myui.config("web", "port", 8000)) use_ipv6 = myui.configbool("web", "ipv6") webdir_conf = myui.config("web", "webdir_conf") + ssl_cert = myui.config("web", "certificate") accesslog = openlog(myui.config("web", "accesslog", "-"), sys.stdout) errorlog = openlog(myui.config("web", "errorlog", "-"), sys.stderr) @@ -226,6 +258,19 @@ def create_server(ui, repo): self.addr, self.port = addr, port + if ssl_cert: + try: + from OpenSSL import SSL + ctx = SSL.Context(SSL.SSLv23_METHOD) + except ImportError: + raise util.Abort("SSL support is unavailable") + ctx.use_privatekey_file(ssl_cert) + ctx.use_certificate_file(ssl_cert) + sock = socket.socket(self.address_family, self.socket_type) + self.socket = SSL.Connection(ctx, sock) + self.server_bind() + self.server_activate() + class IPv6HTTPServer(MercurialHTTPServer): address_family = getattr(socket, 'AF_INET6', None) @@ -234,10 +279,15 @@ def create_server(ui, repo): raise hg.RepoError(_('IPv6 not available on this system')) super(IPv6HTTPServer, self).__init__(*args, **kwargs) + if ssl_cert: + handler = _shgwebhandler + else: + handler = _hgwebhandler + try: if use_ipv6: - return IPv6HTTPServer((address, port), _hgwebhandler) + return IPv6HTTPServer((address, port), handler) else: - return MercurialHTTPServer((address, port), _hgwebhandler) + return MercurialHTTPServer((address, port), handler) except socket.error, inst: raise util.Abort(_('cannot start server: %s') % inst.args[1]) diff --git a/mercurial/hgweb/wsgicgi.py b/mercurial/hgweb/wsgicgi.py --- a/mercurial/hgweb/wsgicgi.py +++ b/mercurial/hgweb/wsgicgi.py @@ -23,7 +23,7 @@ def launch(application): environ['wsgi.multiprocess'] = True environ['wsgi.run_once'] = True - if environ.get('HTTPS','off') in ('on','1'): + if environ.get('HTTPS','off').lower() in ('on','1','yes'): environ['wsgi.url_scheme'] = 'https' else: environ['wsgi.url_scheme'] = 'http' diff --git a/mercurial/httprepo.py b/mercurial/httprepo.py --- a/mercurial/httprepo.py +++ b/mercurial/httprepo.py @@ -9,7 +9,7 @@ from node import * from remoterepo import * from i18n import _ -import hg, os, urllib, urllib2, urlparse, zlib, util, httplib +import repo, os, urllib, urllib2, urlparse, zlib, util, httplib import errno, keepalive, tempfile, socket, changegroup class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm): @@ -276,9 +276,9 @@ class httprepository(remoterepository): def get_caps(self): if self.caps is None: try: - self.caps = self.do_read('capabilities').split() - except hg.RepoError: - self.caps = () + self.caps = util.set(self.do_read('capabilities').split()) + except repo.RepoError: + self.caps = util.set() self.ui.debug(_('capabilities: %s\n') % (' '.join(self.caps or ['none']))) return self.caps @@ -298,8 +298,7 @@ class httprepository(remoterepository): cu = "%s%s" % (self._url, qs) try: if data: - self.ui.debug(_("sending %s bytes\n") % - headers.get('content-length', 'X')) + self.ui.debug(_("sending %s bytes\n") % len(data)) resp = urllib2.urlopen(request(cu, data, headers)) except urllib2.HTTPError, inst: if inst.code == 401: @@ -329,7 +328,7 @@ class httprepository(remoterepository): proto.startswith('text/plain') or proto.startswith('application/hg-changegroup')): self.ui.debug(_("Requested URL: '%s'\n") % cu) - raise hg.RepoError(_("'%s' does not appear to be an hg repository") + raise repo.RepoError(_("'%s' does not appear to be an hg repository") % self._url) if proto.startswith('application/mercurial-'): @@ -337,10 +336,10 @@ class httprepository(remoterepository): version = proto.split('-', 1)[1] version_info = tuple([int(n) for n in version.split('.')]) except ValueError: - raise hg.RepoError(_("'%s' sent a broken Content-type " + raise repo.RepoError(_("'%s' sent a broken Content-type " "header (%s)") % (self._url, proto)) if version_info > (0, 1): - raise hg.RepoError(_("'%s' uses newer protocol %s") % + raise repo.RepoError(_("'%s' uses newer protocol %s") % (self._url, version)) return resp @@ -354,11 +353,12 @@ class httprepository(remoterepository): fp.close() def lookup(self, key): + self.requirecap('lookup', _('look up remote revision')) d = self.do_cmd("lookup", key = key).read() success, data = d[:-1].split(' ', 1) if int(success): return bin(data) - raise hg.RepoError(data) + raise repo.RepoError(data) def heads(self): d = self.do_read("heads") @@ -391,6 +391,7 @@ class httprepository(remoterepository): return util.chunkbuffer(zgenerator(f)) def changegroupsubset(self, bases, heads, source): + self.requirecap('changegroupsubset', _('look up remote changes')) baselst = " ".join([hex(n) for n in bases]) headlst = " ".join([hex(n) for n in heads]) f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst) @@ -449,9 +450,6 @@ class httpsrepository(httprepository): 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/ignore.py b/mercurial/ignore.py --- a/mercurial/ignore.py +++ b/mercurial/ignore.py @@ -85,9 +85,3 @@ def ignore(root, files, warn): util.matcher(root, inc=patlist, src=f)) return ignorefunc - - - '''default match function used by dirstate and - localrepository. this honours the repository .hgignore file - and any other files specified in the [ui] section of .hgrc.''' - diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -8,19 +8,16 @@ from node import * from i18n import _ import repo, changegroup -import changelog, dirstate, filelog, manifest, context -import re, lock, transaction, tempfile, stat, mdiff, errno, ui +import changelog, dirstate, filelog, manifest, context, weakref +import re, lock, transaction, tempfile, stat, errno, ui import os, revlog, time, util, extensions, hook class localrepository(repo.repository): - capabilities = ('lookup', 'changegroupsubset') + capabilities = util.set(('lookup', 'changegroupsubset')) supported = ('revlogv1', 'store') - def __del__(self): - self.transhandle = None def __init__(self, parentui, path=None, create=0): repo.repository.__init__(self) - self.path = path self.root = os.path.realpath(path) self.path = os.path.join(self.root, ".hg") self.origroot = path @@ -71,7 +68,8 @@ class localrepository(repo.repository): self.encodefn = lambda x: x self.decodefn = lambda x: x self.spath = self.path - self.sopener = util.encodedopener(util.opener(self.spath), self.encodefn) + self.sopener = util.encodedopener(util.opener(self.spath), + self.encodefn) self.ui = ui.ui(parentui=parentui) try: @@ -84,7 +82,7 @@ class localrepository(repo.repository): self.branchcache = None self.nodetagscache = None self.filterpats = {} - self.transhandle = None + self._transref = self._lockref = self._wlockref = None def __getattr__(self, name): if name == 'changelog': @@ -109,7 +107,8 @@ class localrepository(repo.repository): tag_disallowed = ':\r\n' - def _tag(self, name, node, message, local, user, date, parent=None): + def _tag(self, name, node, message, local, user, date, parent=None, + extra={}): use_dirstate = parent is None for c in self.tag_disallowed: @@ -157,10 +156,11 @@ class localrepository(repo.repository): # committed tags are stored in UTF-8 writetag(fp, name, util.fromlocal, prevtags) - if use_dirstate and self.dirstate.state('.hgtags') == '?': + if use_dirstate and '.hgtags' not in self.dirstate: self.add(['.hgtags']) - tagnode = self.commit(['.hgtags'], message, user, date, p1=parent) + tagnode = self.commit(['.hgtags'], message, user, date, p1=parent, + extra=extra) self.hook('tag', node=hex(node), tag=name, local=local) @@ -396,6 +396,11 @@ class localrepository(repo.repository): n = self.changelog._partialmatch(key) if n: return n + try: + if len(key) == 20: + key = hex(key) + except: + pass raise repo.RepoError(_("unknown revision '%s'") % key) def dev(self): @@ -495,9 +500,8 @@ class localrepository(repo.repository): return self._filter("decode", filename, data) def transaction(self): - tr = self.transhandle - if tr != None and tr.running(): - return tr.nest() + if self._transref and self._transref(): + return self._transref().nest() # save dirstate for rollback try: @@ -511,33 +515,38 @@ class localrepository(repo.repository): tr = transaction.transaction(self.ui.warn, self.sopener, self.sjoin("journal"), aftertrans(renames)) - self.transhandle = tr + self._transref = weakref.ref(tr) return tr def recover(self): l = self.lock() - if os.path.exists(self.sjoin("journal")): - self.ui.status(_("rolling back interrupted transaction\n")) - transaction.rollback(self.sopener, self.sjoin("journal")) - self.invalidate() - return True - else: - self.ui.warn(_("no interrupted transaction available\n")) - return False + try: + if os.path.exists(self.sjoin("journal")): + self.ui.status(_("rolling back interrupted transaction\n")) + transaction.rollback(self.sopener, self.sjoin("journal")) + self.invalidate() + return True + else: + self.ui.warn(_("no interrupted transaction available\n")) + return False + finally: + del l - def rollback(self, wlock=None, lock=None): - if not wlock: + def rollback(self): + wlock = lock = None + try: wlock = self.wlock() - if not lock: lock = self.lock() - if os.path.exists(self.sjoin("undo")): - self.ui.status(_("rolling back last transaction\n")) - transaction.rollback(self.sopener, self.sjoin("undo")) - util.rename(self.join("undo.dirstate"), self.join("dirstate")) - self.invalidate() - self.dirstate.invalidate() - else: - self.ui.warn(_("no rollback information available\n")) + if os.path.exists(self.sjoin("undo")): + self.ui.status(_("rolling back last transaction\n")) + transaction.rollback(self.sopener, self.sjoin("undo")) + util.rename(self.join("undo.dirstate"), self.join("dirstate")) + self.invalidate() + self.dirstate.invalidate() + else: + self.ui.warn(_("no rollback information available\n")) + finally: + del lock, wlock def invalidate(self): for a in "changelog manifest".split(): @@ -546,8 +555,7 @@ class localrepository(repo.repository): self.tagscache = None self.nodetagscache = None - def do_lock(self, lockname, wait, releasefn=None, acquirefn=None, - desc=None): + def _lock(self, lockname, wait, releasefn, acquirefn, desc): try: l = lock.lock(lockname, 0, releasefn, desc=desc) except lock.LockHeld, inst: @@ -562,17 +570,26 @@ class localrepository(repo.repository): acquirefn() return l - def lock(self, wait=1): - return self.do_lock(self.sjoin("lock"), wait, - acquirefn=self.invalidate, - desc=_('repository %s') % self.origroot) + def lock(self, wait=True): + if self._lockref and self._lockref(): + return self._lockref() + + l = self._lock(self.sjoin("lock"), wait, None, self.invalidate, + _('repository %s') % self.origroot) + self._lockref = weakref.ref(l) + return l - def wlock(self, wait=1): - return self.do_lock(self.join("wlock"), wait, self.dirstate.write, - self.dirstate.invalidate, - desc=_('working directory of %s') % self.origroot) + def wlock(self, wait=True): + if self._wlockref and self._wlockref(): + return self._wlockref() - def filecommit(self, fn, manifest1, manifest2, linkrev, transaction, changelist): + l = self._lock(self.join("wlock"), wait, self.dirstate.write, + self.dirstate.invalidate, _('working directory of %s') % + self.origroot) + self._wlockref = weakref.ref(l) + return l + + def filecommit(self, fn, manifest1, manifest2, linkrev, tr, changelist): """ commit an individual file as part of a larger transaction """ @@ -632,173 +649,183 @@ class localrepository(repo.repository): return fp1 changelist.append(fn) - return fl.add(t, meta, transaction, linkrev, fp1, fp2) + return fl.add(t, meta, tr, linkrev, fp1, fp2) - def rawcommit(self, files, text, user, date, p1=None, p2=None, wlock=None, extra={}): + def rawcommit(self, files, text, user, date, p1=None, p2=None, extra={}): if p1 is None: p1, p2 = self.dirstate.parents() return self.commit(files=files, text=text, user=user, date=date, - p1=p1, p2=p2, wlock=wlock, extra=extra) + p1=p1, p2=p2, extra=extra, empty_ok=True) def commit(self, files=None, text="", user=None, date=None, - match=util.always, force=False, lock=None, wlock=None, - force_editor=False, p1=None, p2=None, extra={}): - - commit = [] - remove = [] - changed = [] - use_dirstate = (p1 is None) # not rawcommit - extra = extra.copy() + match=util.always, force=False, force_editor=False, + p1=None, p2=None, extra={}, empty_ok=False): + wlock = lock = tr = None + try: + commit = [] + remove = [] + changed = [] + use_dirstate = (p1 is None) # not rawcommit + extra = extra.copy() - if use_dirstate: - if files: - for f in files: - s = self.dirstate.state(f) - if s in 'nmai': - commit.append(f) - elif s == 'r': - remove.append(f) - else: - self.ui.warn(_("%s not tracked!\n") % f) + if use_dirstate: + if files: + for f in files: + s = self.dirstate[f] + if s in 'nma': + commit.append(f) + elif s == 'r': + remove.append(f) + else: + self.ui.warn(_("%s not tracked!\n") % f) + else: + changes = self.status(match=match)[:5] + modified, added, removed, deleted, unknown = changes + commit = modified + added + remove = removed else: - changes = self.status(match=match)[:5] - modified, added, removed, deleted, unknown = changes - commit = modified + added - remove = removed - else: - commit = files + commit = files - if use_dirstate: - p1, p2 = self.dirstate.parents() - update_dirstate = True - else: - p1, p2 = p1, p2 or nullid - update_dirstate = (self.dirstate.parents()[0] == p1) + if use_dirstate: + p1, p2 = self.dirstate.parents() + update_dirstate = True + else: + p1, p2 = p1, p2 or nullid + update_dirstate = (self.dirstate.parents()[0] == p1) - c1 = self.changelog.read(p1) - c2 = self.changelog.read(p2) - m1 = self.manifest.read(c1[0]).copy() - m2 = self.manifest.read(c2[0]) + c1 = self.changelog.read(p1) + c2 = self.changelog.read(p2) + m1 = self.manifest.read(c1[0]).copy() + m2 = self.manifest.read(c2[0]) - if use_dirstate: - branchname = self.workingctx().branch() - try: - branchname = branchname.decode('UTF-8').encode('UTF-8') - except UnicodeDecodeError: - raise util.Abort(_('branch name not in UTF-8!')) - else: - branchname = "" + if use_dirstate: + branchname = self.workingctx().branch() + try: + branchname = branchname.decode('UTF-8').encode('UTF-8') + except UnicodeDecodeError: + raise util.Abort(_('branch name not in UTF-8!')) + else: + branchname = "" - if use_dirstate: - oldname = c1[5].get("branch") # stored in UTF-8 - if (not commit and not remove and not force and p2 == nullid - and branchname == oldname): - self.ui.status(_("nothing changed\n")) - return None + if use_dirstate: + oldname = c1[5].get("branch") # stored in UTF-8 + if (not commit and not remove and not force and p2 == nullid + and branchname == oldname): + self.ui.status(_("nothing changed\n")) + return None - xp1 = hex(p1) - if p2 == nullid: xp2 = '' - else: xp2 = hex(p2) - - self.hook("precommit", throw=True, parent1=xp1, parent2=xp2) + xp1 = hex(p1) + if p2 == nullid: xp2 = '' + else: xp2 = hex(p2) - if not wlock: + self.hook("precommit", throw=True, parent1=xp1, parent2=xp2) + wlock = self.wlock() - if not lock: lock = self.lock() - tr = self.transaction() + tr = self.transaction() + trp = weakref.proxy(tr) - # check in files - new = {} - linkrev = self.changelog.count() - commit.sort() - is_exec = util.execfunc(self.root, m1.execf) - is_link = util.linkfunc(self.root, m1.linkf) - for f in commit: - self.ui.note(f + "\n") - try: - new[f] = self.filecommit(f, m1, m2, linkrev, tr, changed) - new_exec = is_exec(f) - new_link = is_link(f) - if (not changed or changed[-1] != f) and m2.get(f) != new[f]: - # mention the file in the changelog if some flag changed, - # even if there was no content change. - old_exec = m1.execf(f) - old_link = m1.linkf(f) - if old_exec != new_exec or old_link != new_link: - changed.append(f) - m1.set(f, new_exec, new_link) - except (OSError, IOError): - if use_dirstate: - self.ui.warn(_("trouble committing %s!\n") % f) - raise - else: - remove.append(f) + # check in files + new = {} + linkrev = self.changelog.count() + commit.sort() + is_exec = util.execfunc(self.root, m1.execf) + is_link = util.linkfunc(self.root, m1.linkf) + for f in commit: + self.ui.note(f + "\n") + try: + new[f] = self.filecommit(f, m1, m2, linkrev, trp, changed) + new_exec = is_exec(f) + new_link = is_link(f) + if ((not changed or changed[-1] != f) and + m2.get(f) != new[f]): + # mention the file in the changelog if some + # flag changed, even if there was no content + # change. + old_exec = m1.execf(f) + old_link = m1.linkf(f) + if old_exec != new_exec or old_link != new_link: + changed.append(f) + m1.set(f, new_exec, new_link) + except (OSError, IOError): + if use_dirstate: + self.ui.warn(_("trouble committing %s!\n") % f) + raise + else: + remove.append(f) - # update manifest - m1.update(new) - remove.sort() - removed = [] + # update manifest + m1.update(new) + remove.sort() + removed = [] - for f in remove: - if f in m1: - del m1[f] - removed.append(f) - elif f in m2: - removed.append(f) - mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0], (new, removed)) + for f in remove: + if f in m1: + del m1[f] + removed.append(f) + elif f in m2: + removed.append(f) + mn = self.manifest.add(m1, trp, linkrev, c1[0], c2[0], + (new, removed)) - # add changeset - new = new.keys() - new.sort() + # add changeset + new = new.keys() + new.sort() - user = user or self.ui.username() - if not text or force_editor: - edittext = [] - if text: - edittext.append(text) - edittext.append("") - edittext.append("HG: user: %s" % user) - if p2 != nullid: - edittext.append("HG: branch merge") + user = user or self.ui.username() + if (not empty_ok and not text) or force_editor: + edittext = [] + if text: + edittext.append(text) + edittext.append("") + edittext.append("HG: user: %s" % user) + if p2 != nullid: + edittext.append("HG: branch merge") + if branchname: + edittext.append("HG: branch %s" % util.tolocal(branchname)) + edittext.extend(["HG: changed %s" % f for f in changed]) + edittext.extend(["HG: removed %s" % f for f in removed]) + if not changed and not remove: + edittext.append("HG: no files changed") + edittext.append("") + # run editor in the repository root + olddir = os.getcwd() + os.chdir(self.root) + text = self.ui.edit("\n".join(edittext), user) + os.chdir(olddir) + if branchname: - edittext.append("HG: branch %s" % util.tolocal(branchname)) - edittext.extend(["HG: changed %s" % f for f in changed]) - edittext.extend(["HG: removed %s" % f for f in removed]) - if not changed and not remove: - edittext.append("HG: no files changed") - edittext.append("") - # run editor in the repository root - olddir = os.getcwd() - os.chdir(self.root) - text = self.ui.edit("\n".join(edittext), user) - os.chdir(olddir) + extra["branch"] = branchname - lines = [line.rstrip() for line in text.rstrip().splitlines()] - while lines and not lines[0]: - del lines[0] - if not lines: - return None - text = '\n'.join(lines) - if branchname: - extra["branch"] = branchname - n = self.changelog.add(mn, changed + removed, text, tr, p1, p2, - user, date, extra) - self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1, - parent2=xp2) - tr.close() + if use_dirstate: + lines = [line.rstrip() for line in text.rstrip().splitlines()] + while lines and not lines[0]: + del lines[0] + if not lines: + return None + text = '\n'.join(lines) + + n = self.changelog.add(mn, changed + removed, text, trp, p1, p2, + user, date, extra) + self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1, + parent2=xp2) + tr.close() - if self.branchcache and "branch" in extra: - self.branchcache[util.tolocal(extra["branch"])] = n + if self.branchcache and "branch" in extra: + self.branchcache[util.tolocal(extra["branch"])] = n - if use_dirstate or update_dirstate: - self.dirstate.setparents(n) - if use_dirstate: - self.dirstate.update(new, "n") - self.dirstate.forget(removed) + if use_dirstate or update_dirstate: + self.dirstate.setparents(n) + if use_dirstate: + for f in new: + self.dirstate.normal(f) + for f in removed: + self.dirstate.forget(f) - self.hook("commit", node=hex(n), parent1=xp1, parent2=xp2) - return n + self.hook("commit", node=hex(n), parent1=xp1, parent2=xp2) + return n + finally: + del tr, lock, wlock def walk(self, node=None, files=[], match=util.always, badmatch=None): ''' @@ -843,7 +870,7 @@ class localrepository(repo.repository): yield src, fn def status(self, node1=None, node2=None, files=[], match=util.always, - wlock=None, list_ignored=False, list_clean=False): + list_ignored=False, list_clean=False): """return status of files between two nodes or node and working directory If node1 is None, use the first dirstate parent instead. @@ -875,8 +902,6 @@ class localrepository(repo.repository): # all the revisions in parent->child order. mf1 = mfmatches(node1) - mywlock = False - # are we comparing the working directory? if not node2: (lookup, modified, added, removed, deleted, unknown, @@ -886,24 +911,30 @@ class localrepository(repo.repository): # are we comparing working dir against its parent? if compareworking: if lookup: + fixup = [] # do a full compare of any files that might have changed - mnode = self.changelog.read(self.dirstate.parents()[0])[0] - getnode = lambda fn: (self.manifest.find(mnode, fn)[0] or - nullid) + ctx = self.changectx() for f in lookup: - if fcmp(f, getnode): + if f not in ctx or ctx[f].cmp(self.wread(f)): modified.append(f) else: + fixup.append(f) if list_clean: clean.append(f) - if not wlock and not mywlock: - mywlock = True - try: - wlock = self.wlock(wait=0) - except lock.LockException: - pass + + # update dirstate for files that are actually clean + if fixup: + wlock = None + try: + try: + wlock = self.wlock(False) + except lock.LockException: + pass if wlock: - self.dirstate.update([f], "n") + for f in fixup: + self.dirstate.normal(f) + finally: + del wlock else: # we are comparing working dir against non-parent # generate a pseudo-manifest for the working dir @@ -918,8 +949,6 @@ class localrepository(repo.repository): if f in mf2: del mf2[f] - if mywlock and wlock: - wlock.release() else: # we are comparing two revisions mf2 = mfmatches(node2) @@ -952,85 +981,100 @@ class localrepository(repo.repository): l.sort() return (modified, added, removed, deleted, unknown, ignored, clean) - def add(self, list, wlock=None): - if not wlock: - wlock = self.wlock() - for f in list: - p = self.wjoin(f) - try: - st = os.lstat(p) - except: - self.ui.warn(_("%s does not exist!\n") % f) - continue - if st.st_size > 10000000: - self.ui.warn(_("%s: files over 10MB may cause memory and" - " performance problems\n" - "(use 'hg revert %s' to unadd the file)\n") - % (f, f)) - if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)): - self.ui.warn(_("%s not added: only files and symlinks " - "supported currently\n") % f) - elif self.dirstate.state(f) in 'an': - self.ui.warn(_("%s already tracked!\n") % f) - else: - self.dirstate.update([f], "a") - - def forget(self, list, wlock=None): - if not wlock: - wlock = self.wlock() - for f in list: - if self.dirstate.state(f) not in 'ai': - self.ui.warn(_("%s not added!\n") % f) - else: - self.dirstate.forget([f]) - - def remove(self, list, unlink=False, wlock=None): - if unlink: + def add(self, list): + wlock = self.wlock() + try: for f in list: + p = self.wjoin(f) try: - util.unlink(self.wjoin(f)) - except OSError, inst: - if inst.errno != errno.ENOENT: - raise - if not wlock: - wlock = self.wlock() - for f in list: - if unlink and os.path.exists(self.wjoin(f)): - self.ui.warn(_("%s still exists!\n") % f) - elif self.dirstate.state(f) == 'a': - self.dirstate.forget([f]) - elif f not in self.dirstate: - self.ui.warn(_("%s not tracked!\n") % f) - else: - self.dirstate.update([f], "r") + st = os.lstat(p) + except: + self.ui.warn(_("%s does not exist!\n") % f) + continue + if st.st_size > 10000000: + self.ui.warn(_("%s: files over 10MB may cause memory and" + " performance problems\n" + "(use 'hg revert %s' to unadd the file)\n") + % (f, f)) + if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)): + self.ui.warn(_("%s not added: only files and symlinks " + "supported currently\n") % f) + elif self.dirstate[f] in 'amn': + self.ui.warn(_("%s already tracked!\n") % f) + elif self.dirstate[f] == 'r': + self.dirstate.normallookup(f) + else: + self.dirstate.add(f) + finally: + del wlock - def undelete(self, list, wlock=None): - p = self.dirstate.parents()[0] - mn = self.changelog.read(p)[0] - m = self.manifest.read(mn) - if not wlock: + def forget(self, list): + wlock = self.wlock() + try: + for f in list: + if self.dirstate[f] != 'a': + self.ui.warn(_("%s not added!\n") % f) + else: + self.dirstate.forget(f) + finally: + del wlock + + def remove(self, list, unlink=False): + wlock = None + try: + if unlink: + for f in list: + try: + util.unlink(self.wjoin(f)) + except OSError, inst: + if inst.errno != errno.ENOENT: + raise wlock = self.wlock() - for f in list: - if self.dirstate.state(f) not in "r": - self.ui.warn("%s not removed!\n" % f) - else: - t = self.file(f).read(m[f]) - self.wwrite(f, t, m.flags(f)) - self.dirstate.update([f], "n") + for f in list: + if unlink and os.path.exists(self.wjoin(f)): + self.ui.warn(_("%s still exists!\n") % f) + elif self.dirstate[f] == 'a': + self.dirstate.forget(f) + elif f not in self.dirstate: + self.ui.warn(_("%s not tracked!\n") % f) + else: + self.dirstate.remove(f) + finally: + del wlock - def copy(self, source, dest, wlock=None): - p = self.wjoin(dest) - if not (os.path.exists(p) or os.path.islink(p)): - self.ui.warn(_("%s does not exist!\n") % dest) - elif not (os.path.isfile(p) or os.path.islink(p)): - self.ui.warn(_("copy failed: %s is not a file or a " - "symbolic link\n") % dest) - else: - if not wlock: + def undelete(self, list): + wlock = None + try: + manifests = [self.manifest.read(self.changelog.read(p)[0]) + for p in self.dirstate.parents() if p != nullid] + wlock = self.wlock() + for f in list: + if self.dirstate[f] != 'r': + self.ui.warn("%s not removed!\n" % f) + else: + m = f in manifests[0] and manifests[0] or manifests[1] + t = self.file(f).read(m[f]) + self.wwrite(f, t, m.flags(f)) + self.dirstate.normal(f) + finally: + del wlock + + def copy(self, source, dest): + wlock = None + try: + p = self.wjoin(dest) + if not (os.path.exists(p) or os.path.islink(p)): + self.ui.warn(_("%s does not exist!\n") % dest) + elif not (os.path.isfile(p) or os.path.islink(p)): + self.ui.warn(_("copy failed: %s is not a file or a " + "symbolic link\n") % dest) + else: wlock = self.wlock() - if self.dirstate.state(dest) == '?': - self.dirstate.update([dest], "a") - self.dirstate.copy(source, dest) + if dest not in self.dirstate: + self.dirstate.add(dest) + self.dirstate.copy(source, dest) + finally: + del wlock def heads(self, start=None): heads = self.changelog.heads(start) @@ -1307,12 +1351,8 @@ class localrepository(repo.repository): else: return subset - def pull(self, remote, heads=None, force=False, lock=None): - mylock = False - if not lock: - lock = self.lock() - mylock = True - + def pull(self, remote, heads=None, force=False): + lock = self.lock() try: fetch = self.findincoming(remote, heads=heads, force=force) if fetch == [nullid]: @@ -1330,8 +1370,7 @@ class localrepository(repo.repository): cg = remote.changegroupsubset(fetch, heads, 'pull') return self.addchangegroup(cg, 'pull', remote.url()) finally: - if mylock: - lock.release() + del lock def push(self, remote, force=False, revs=None): # there are two ways to push to remote repo: @@ -1404,12 +1443,14 @@ class localrepository(repo.repository): def push_addchangegroup(self, remote, force, revs): lock = remote.lock() - - ret = self.prepush(remote, force, revs) - if ret[0] is not None: - cg, remote_heads = ret - return remote.addchangegroup(cg, 'push', self.url()) - return ret[1] + try: + ret = self.prepush(remote, force, revs) + if ret[0] is not None: + cg, remote_heads = ret + return remote.addchangegroup(cg, 'push', self.url()) + return ret[1] + finally: + del lock def push_unbundle(self, remote, force, revs): # local repo finds heads on server, finds out what revs it @@ -1580,12 +1621,9 @@ class localrepository(repo.repository): if r == next_rev[0]: # If the last rev we looked at was the one just previous, # we only need to see a diff. - delta = mdiff.patchtext(mnfst.delta(mnfstnode)) + deltamf = mnfst.readdelta(mnfstnode) # For each line in the delta - for dline in delta.splitlines(): - # get the filename and filenode for that line - f, fnode = dline.split('\0') - fnode = bin(fnode[:40]) + for f, fnode in deltamf.items(): f = changedfiles.get(f, None) # And if the file is in the list of files we care # about. @@ -1793,65 +1831,68 @@ class localrepository(repo.repository): changesets = files = revisions = 0 - tr = self.transaction() - # write changelog data to temp files so concurrent readers will not see # inconsistent view cl = self.changelog cl.delayupdate() oldheads = len(cl.heads()) - # pull off the changeset group - self.ui.status(_("adding changesets\n")) - cor = cl.count() - 1 - chunkiter = changegroup.chunkiter(source) - if cl.addgroup(chunkiter, csmap, tr, 1) is None: - raise util.Abort(_("received changelog group is empty")) - cnr = cl.count() - 1 - changesets = cnr - cor + tr = self.transaction() + try: + trp = weakref.proxy(tr) + # pull off the changeset group + self.ui.status(_("adding changesets\n")) + cor = cl.count() - 1 + chunkiter = changegroup.chunkiter(source) + if cl.addgroup(chunkiter, csmap, trp, 1) is None: + raise util.Abort(_("received changelog group is empty")) + cnr = cl.count() - 1 + changesets = cnr - cor - # pull off the manifest group - self.ui.status(_("adding manifests\n")) - chunkiter = changegroup.chunkiter(source) - # no need to check for empty manifest group here: - # if the result of the merge of 1 and 2 is the same in 3 and 4, - # no new manifest will be created and the manifest group will - # be empty during the pull - self.manifest.addgroup(chunkiter, revmap, tr) + # pull off the manifest group + self.ui.status(_("adding manifests\n")) + chunkiter = changegroup.chunkiter(source) + # no need to check for empty manifest group here: + # if the result of the merge of 1 and 2 is the same in 3 and 4, + # no new manifest will be created and the manifest group will + # be empty during the pull + self.manifest.addgroup(chunkiter, revmap, trp) - # process the files - self.ui.status(_("adding file changes\n")) - while 1: - f = changegroup.getchunk(source) - if not f: - break - self.ui.debug(_("adding %s revisions\n") % f) - fl = self.file(f) - o = fl.count() - chunkiter = changegroup.chunkiter(source) - if fl.addgroup(chunkiter, revmap, tr) is None: - raise util.Abort(_("received file revlog group is empty")) - revisions += fl.count() - o - files += 1 + # process the files + self.ui.status(_("adding file changes\n")) + while 1: + f = changegroup.getchunk(source) + if not f: + break + self.ui.debug(_("adding %s revisions\n") % f) + fl = self.file(f) + o = fl.count() + chunkiter = changegroup.chunkiter(source) + if fl.addgroup(chunkiter, revmap, trp) is None: + raise util.Abort(_("received file revlog group is empty")) + revisions += fl.count() - o + files += 1 + + # make changelog see real files again + cl.finalize(trp) - # make changelog see real files again - cl.finalize(tr) + newheads = len(self.changelog.heads()) + heads = "" + if oldheads and newheads != oldheads: + heads = _(" (%+d heads)") % (newheads - oldheads) - newheads = len(self.changelog.heads()) - heads = "" - if oldheads and newheads != oldheads: - heads = _(" (%+d heads)") % (newheads - oldheads) + self.ui.status(_("added %d changesets" + " with %d changes to %d files%s\n") + % (changesets, revisions, files, heads)) - self.ui.status(_("added %d changesets" - " with %d changes to %d files%s\n") - % (changesets, revisions, files, heads)) + if changesets > 0: + self.hook('pretxnchangegroup', throw=True, + node=hex(self.changelog.node(cor+1)), source=srctype, + url=url) - if changesets > 0: - self.hook('pretxnchangegroup', throw=True, - node=hex(self.changelog.node(cor+1)), source=srctype, - url=url) - - tr.close() + tr.close() + finally: + del tr if changesets > 0: self.hook("changegroup", node=hex(self.changelog.node(cor+1)), diff --git a/mercurial/lock.py b/mercurial/lock.py --- a/mercurial/lock.py +++ b/mercurial/lock.py @@ -29,14 +29,13 @@ class lock(object): # old-style lock: symlink to pid # new-style lock: symlink to hostname:pid + _host = None + def __init__(self, file, timeout=-1, releasefn=None, desc=None): self.f = file self.held = 0 self.timeout = timeout self.releasefn = releasefn - self.id = None - self.host = None - self.pid = None self.desc = desc self.lock() @@ -59,13 +58,12 @@ class lock(object): inst.locker) def trylock(self): - if self.id is None: - self.host = socket.gethostname() - self.pid = os.getpid() - self.id = '%s:%s' % (self.host, self.pid) + if lock._host is None: + lock._host = socket.gethostname() + lockname = '%s:%s' % (lock._host, os.getpid()) while not self.held: try: - util.makelock(self.id, self.f) + util.makelock(lockname, self.f) self.held = 1 except (OSError, IOError), why: if why.errno == errno.EEXIST: @@ -93,7 +91,7 @@ class lock(object): host, pid = locker.split(":", 1) except ValueError: return locker - if host != self.host: + if host != lock._host: return locker try: pid = int(pid) diff --git a/mercurial/manifest.py b/mercurial/manifest.py --- a/mercurial/manifest.py +++ b/mercurial/manifest.py @@ -23,10 +23,6 @@ class manifestdict(dict): 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" @@ -40,16 +36,20 @@ class manifest(revlog): self.listcache = None revlog.__init__(self, opener, "00manifest.i") - def parselines(self, lines): - for l in lines.splitlines(1): - yield l.split('\0') + def parse(self, lines): + mfdict = manifestdict() + fdict = mfdict._flags + for l in lines.splitlines(): + f, n = l.split('\0') + if len(n) > 40: + fdict[f] = n[40:] + mfdict[f] = bin(n[:40]) + else: + mfdict[f] = bin(n) + return mfdict def readdelta(self, node): - delta = mdiff.patchtext(self.delta(node)) - deltamap = manifestdict() - for f, n in self.parselines(delta): - deltamap.rawset(f, n) - return deltamap + return self.parse(mdiff.patchtext(self.delta(node))) def read(self, node): if node == nullid: return manifestdict() # don't upset local cache @@ -57,9 +57,7 @@ class manifest(revlog): return self.mapcache[1] text = self.revision(node) self.listcache = array.array('c', text) - mapping = manifestdict() - for f, n in self.parselines(text): - mapping.rawset(f, n) + mapping = self.parse(text) self.mapcache = (node, mapping) return mapping diff --git a/mercurial/merge.py b/mercurial/merge.py --- a/mercurial/merge.py +++ b/mercurial/merge.py @@ -448,13 +448,15 @@ def applyupdates(repo, action, wctx, mct repo.ui.debug(_("copying %s to %s\n") % (f, fd)) repo.wwrite(fd, repo.wread(f), flags) + audit_path = util.path_auditor(repo.root) + for a in action: f, m = a[:2] if f and f[0] == "/": continue if m == "r": # remove repo.ui.note(_("removing %s\n") % f) - util.audit_path(f) + audit_path(f) try: util.unlink(repo.wjoin(f)) except OSError, inst: @@ -512,25 +514,25 @@ def recordupdates(repo, action, branchme f, m = a[:2] if m == "r": # remove if branchmerge: - repo.dirstate.update([f], 'r') + repo.dirstate.remove(f) else: - repo.dirstate.forget([f]) + repo.dirstate.forget(f) elif m == "f": # forget - repo.dirstate.forget([f]) + repo.dirstate.forget(f) elif m in "ge": # get or exec change if branchmerge: - repo.dirstate.update([f], 'n', st_mtime=-1) + repo.dirstate.normaldirty(f) else: - repo.dirstate.update([f], 'n') + repo.dirstate.normal(f) elif m == "m": # merge f2, fd, flag, move = a[2:] if branchmerge: # We've done a branch merge, mark this file as merged # so that we properly record the merger later - repo.dirstate.update([fd], 'm') + repo.dirstate.merge(fd) if f != f2: # copy/rename if move: - repo.dirstate.update([f], 'r') + repo.dirstate.remove(f) if f != fd: repo.dirstate.copy(f, fd) else: @@ -541,95 +543,94 @@ def recordupdates(repo, action, branchme # of that file some time in the past. Thus our # merge will appear as a normal local file # modification. - repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1) + repo.dirstate.normallookup(fd) if move: - repo.dirstate.forget([f]) + repo.dirstate.forget(f) elif m == "d": # directory rename f2, fd, flag = a[2:] if not f2 and f not in repo.dirstate: # untracked file moved continue if branchmerge: - repo.dirstate.update([fd], 'a') + repo.dirstate.add(fd) if f: - repo.dirstate.update([f], 'r') + repo.dirstate.remove(f) repo.dirstate.copy(f, fd) if f2: repo.dirstate.copy(f2, fd) else: - repo.dirstate.update([fd], 'n') + repo.dirstate.normal(fd) if f: - repo.dirstate.forget([f]) + repo.dirstate.forget(f) -def update(repo, node, branchmerge, force, partial, wlock): +def update(repo, node, branchmerge, force, partial): """ Perform a merge between the working directory and the given node branchmerge = whether to merge between branches force = whether to force branch merging or file overwriting partial = a function to filter file lists (dirstate not updated) - wlock = working dir lock, if already held """ - if not wlock: - wlock = repo.wlock() - - wc = repo.workingctx() - if node is None: - # tip of current branch - try: - node = repo.branchtags()[wc.branch()] - except KeyError: - raise util.Abort(_("branch %s not found") % wc.branch()) - overwrite = force and not branchmerge - forcemerge = force and branchmerge - pl = wc.parents() - p1, p2 = pl[0], repo.changectx(node) - pa = p1.ancestor(p2) - fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2) - fastforward = False + wlock = repo.wlock() + try: + wc = repo.workingctx() + if node is None: + # tip of current branch + try: + node = repo.branchtags()[wc.branch()] + except KeyError: + raise util.Abort(_("branch %s not found") % wc.branch()) + overwrite = force and not branchmerge + forcemerge = force and branchmerge + pl = wc.parents() + p1, p2 = pl[0], repo.changectx(node) + pa = p1.ancestor(p2) + fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2) + fastforward = False - ### check phase - if not overwrite and len(pl) > 1: - raise util.Abort(_("outstanding uncommitted merges")) - if pa == p1 or pa == p2: # is there a linear path from p1 to p2? - if branchmerge: - if p1.branch() != p2.branch() and pa != p2: - fastforward = True - else: - raise util.Abort(_("there is nothing to merge, just use " - "'hg update' or look at 'hg heads'")) - elif not (overwrite or branchmerge): - raise util.Abort(_("update spans branches, use 'hg merge' " - "or 'hg update -C' to lose changes")) - if branchmerge and not forcemerge: - if wc.files(): - raise util.Abort(_("outstanding uncommitted changes")) + ### check phase + if not overwrite and len(pl) > 1: + raise util.Abort(_("outstanding uncommitted merges")) + if pa == p1 or pa == p2: # is there a linear path from p1 to p2? + if branchmerge: + if p1.branch() != p2.branch() and pa != p2: + fastforward = True + else: + raise util.Abort(_("there is nothing to merge, just use " + "'hg update' or look at 'hg heads'")) + elif not (overwrite or branchmerge): + raise util.Abort(_("update spans branches, use 'hg merge' " + "or 'hg update -C' to lose changes")) + if branchmerge and not forcemerge: + if wc.files(): + raise util.Abort(_("outstanding uncommitted changes")) - ### calculate phase - action = [] - if not force: - checkunknown(wc, p2) - if not util.checkfolding(repo.path): - checkcollision(p2) - if not branchmerge: - action += forgetremoved(wc, p2) - action += manifestmerge(repo, wc, p2, pa, overwrite, partial) + ### calculate phase + action = [] + if not force: + checkunknown(wc, p2) + if not util.checkfolding(repo.path): + checkcollision(p2) + if not branchmerge: + action += forgetremoved(wc, p2) + action += manifestmerge(repo, wc, p2, pa, overwrite, partial) - ### apply phase - if not branchmerge: # just jump to the new rev - fp1, fp2, xp1, xp2 = fp2, nullid, xp2, '' - if not partial: - repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2) + ### apply phase + if not branchmerge: # just jump to the new rev + fp1, fp2, xp1, xp2 = fp2, nullid, xp2, '' + if not partial: + repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2) - stats = applyupdates(repo, action, wc, p2) + stats = applyupdates(repo, action, wc, p2) - if not partial: - recordupdates(repo, action, branchmerge) - repo.dirstate.setparents(fp1, fp2) - if not branchmerge and not fastforward: - repo.dirstate.setbranch(p2.branch()) - repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3]) + if not partial: + recordupdates(repo, action, branchmerge) + repo.dirstate.setparents(fp1, fp2) + if not branchmerge and not fastforward: + repo.dirstate.setbranch(p2.branch()) + repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3]) - return stats - + return stats + finally: + del wlock diff --git a/mercurial/node.py b/mercurial/node.py --- a/mercurial/node.py +++ b/mercurial/node.py @@ -12,11 +12,9 @@ import binascii nullrev = -1 nullid = "\0" * 20 -def hex(node): - return binascii.hexlify(node) - -def bin(node): - return binascii.unhexlify(node) +# This ugly style has a noticeable effect in manifest parsing +hex = binascii.hexlify +bin = binascii.unhexlify def short(node): return hex(node[:6]) diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -1,16 +1,23 @@ # patch.py - patch file parsing routines # # Copyright 2006 Brendan Cully +# Copyright 2007 Chris Mason # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from i18n import _ from node import * -import base85, cmdutil, mdiff, util, context, revlog +import base85, cmdutil, mdiff, util, context, revlog, diffhelpers import cStringIO, email.Parser, os, popen2, re, sha import sys, tempfile, zlib +class PatchError(Exception): + pass + +class NoHunks(PatchError): + pass + # helper functions def copyfile(src, dst, basedir=None): @@ -50,7 +57,7 @@ def extract(ui, fileobj): try: msg = email.Parser.Parser().parse(fileobj) - message = msg['Subject'] + subject = msg['Subject'] user = msg['From'] # should try to parse msg['Date'] date = None @@ -58,18 +65,18 @@ def extract(ui, fileobj): branch = None parents = [] - if message: - if message.startswith('[PATCH'): - pend = message.find(']') + if subject: + if subject.startswith('[PATCH'): + pend = subject.find(']') if pend >= 0: - message = message[pend+1:].lstrip() - message = message.replace('\n\t', ' ') - ui.debug('Subject: %s\n' % message) + subject = subject[pend+1:].lstrip() + subject = subject.replace('\n\t', ' ') + ui.debug('Subject: %s\n' % subject) if user: ui.debug('From: %s\n' % user) diffs_seen = 0 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch') - + message = '' for part in msg.walk(): content_type = part.get_content_type() ui.debug('Content-Type: %s\n' % content_type) @@ -84,9 +91,6 @@ def extract(ui, fileobj): 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')) @@ -94,6 +98,7 @@ def extract(ui, fileobj): # drop earlier commit message content cfp.seek(0) cfp.truncate() + subject = None elif hgpatch: if line.startswith('# User '): user = line[7:] @@ -123,6 +128,8 @@ def extract(ui, fileobj): os.unlink(tmpname) raise + if subject and not message.startswith(subject): + message = '%s\n%s' % (subject, message) tmpfp.close() if not diffs_seen: os.unlink(tmpname) @@ -135,7 +142,7 @@ GP_PATCH = 1 << 0 # we have to run pat GP_FILTER = 1 << 1 # there's some copy/rename operation GP_BINARY = 1 << 2 # there's a binary patch -def readgitpatch(patchname): +def readgitpatch(fp, firstline=None): """extract git-style metadata about patches from """ class gitpatch: "op is one of ADD, DELETE, RENAME, MODIFY or COPY" @@ -148,16 +155,21 @@ def readgitpatch(patchname): self.lineno = 0 self.binary = False + def reader(fp, firstline): + if firstline is not None: + yield firstline + for line in fp: + yield line + # 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 = 0 lineno = 0 - for line in pf: + for line in reader(fp, firstline): lineno += 1 if line.startswith('diff --git'): m = gitre.match(line) @@ -190,9 +202,9 @@ def readgitpatch(patchname): gp.op = 'DELETE' elif line.startswith('new file mode '): gp.op = 'ADD' - gp.mode = int(line.rstrip()[-3:], 8) + gp.mode = int(line.rstrip()[-6:], 8) elif line.startswith('new mode '): - gp.mode = int(line.rstrip()[-3:], 8) + gp.mode = int(line.rstrip()[-6:], 8) elif line.startswith('GIT binary patch'): dopatch |= GP_BINARY gp.binary = True @@ -204,157 +216,793 @@ def readgitpatch(patchname): return (dopatch, gitpatches) -def dogitpatch(patchname, gitpatches, cwd=None): - """Preprocess git patch so that vanilla patch can handle it""" - def extractbin(fp): - i = [0] # yuck - def readline(): - i[0] += 1 - return fp.readline().rstrip() - line = readline() +def patch(patchname, ui, strip=1, cwd=None, files={}): + """apply to the working directory. + returns whether patch was applied with fuzz factor.""" + patcher = ui.config('ui', 'patch') + args = [] + try: + if patcher: + return externalpatch(patcher, args, patchname, ui, strip, cwd, + files) + else: + try: + return internalpatch(patchname, ui, strip, cwd, files) + except NoHunks: + patcher = util.find_exe('gpatch') or util.find_exe('patch') + ui.debug('no valid hunks found; trying with %r instead\n' % + patcher) + if util.needbinarypatch(): + args.append('--binary') + return externalpatch(patcher, args, patchname, ui, strip, cwd, + files) + except PatchError, err: + s = str(err) + if s: + raise util.Abort(s) + else: + raise util.Abort(_('patch failed to apply')) + +def externalpatch(patcher, args, patchname, ui, strip, cwd, files): + """use to apply to the working directory. + returns whether patch was applied with fuzz factor.""" + + fuzz = False + if cwd: + args.append('-d %s' % util.shellquote(cwd)) + fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, + util.shellquote(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 PatchError(_("patch command failed: %s") % + util.explain_exit(code)[0]) + return fuzz + +def internalpatch(patchobj, ui, strip, cwd, files={}): + """use builtin patch to apply to the working directory. + returns whether patch was applied with fuzz factor.""" + try: + fp = file(patchobj, 'rb') + except TypeError: + fp = patchobj + if cwd: + curdir = os.getcwd() + os.chdir(cwd) + try: + ret = applydiff(ui, fp, files, strip=strip) + finally: + if cwd: + os.chdir(curdir) + if ret < 0: + raise PatchError + return ret > 0 + +# @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1 +unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@') +contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)') + +class patchfile: + def __init__(self, ui, fname): + self.fname = fname + self.ui = ui + try: + fp = file(fname, 'rb') + self.lines = fp.readlines() + self.exists = True + except IOError: + dirname = os.path.dirname(fname) + if dirname and not os.path.isdir(dirname): + dirs = dirname.split(os.path.sep) + d = "" + for x in dirs: + d = os.path.join(d, x) + if not os.path.isdir(d): + os.mkdir(d) + self.lines = [] + self.exists = False + + self.hash = {} + self.dirty = 0 + self.offset = 0 + self.rej = [] + self.fileprinted = False + self.printfile(False) + self.hunks = 0 + + def printfile(self, warn): + if self.fileprinted: + return + if warn or self.ui.verbose: + self.fileprinted = True + s = _("patching file %s\n") % self.fname + if warn: + self.ui.warn(s) + else: + self.ui.note(s) + + + def findlines(self, l, linenum): + # looks through the hash and finds candidate lines. The + # result is a list of line numbers sorted based on distance + # from linenum + def sorter(a, b): + vala = abs(a - linenum) + valb = abs(b - linenum) + return cmp(vala, valb) + + try: + cand = self.hash[l] + except: + return [] + + if len(cand) > 1: + # resort our list of potentials forward then back. + cand.sort(cmp=sorter) + return cand + + def hashlines(self): + self.hash = {} + for x in xrange(len(self.lines)): + s = self.lines[x] + self.hash.setdefault(s, []).append(x) + + def write_rej(self): + # our rejects are a little different from patch(1). This always + # creates rejects in the same form as the original patch. A file + # header is inserted so that you can run the reject through patch again + # without having to type the filename. + + if not self.rej: + return + if self.hunks != 1: + hunkstr = "s" + else: + hunkstr = "" + + fname = self.fname + ".rej" + self.ui.warn( + _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") % + (len(self.rej), self.hunks, hunkstr, fname)) + try: os.unlink(fname) + except: + pass + fp = file(fname, 'wb') + base = os.path.basename(self.fname) + fp.write("--- %s\n+++ %s\n" % (base, base)) + for x in self.rej: + for l in x.hunk: + fp.write(l) + if l[-1] != '\n': + fp.write("\n\ No newline at end of file\n") + + def write(self, dest=None): + if self.dirty: + if not dest: + dest = self.fname + st = None + try: + st = os.lstat(dest) + if st.st_nlink > 1: + os.unlink(dest) + except: pass + fp = file(dest, 'wb') + if st: + os.chmod(dest, st.st_mode) + fp.writelines(self.lines) + fp.close() + + def close(self): + self.write() + self.write_rej() + + def apply(self, h, reverse): + if not h.complete(): + raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") % + (h.number, h.desc, len(h.a), h.lena, len(h.b), + h.lenb)) + + self.hunks += 1 + if reverse: + h.reverse() + + if self.exists and h.createfile(): + self.ui.warn(_("file %s already exists\n") % self.fname) + self.rej.append(h) + return -1 + + if isinstance(h, binhunk): + if h.rmfile(): + os.unlink(self.fname) + else: + self.lines[:] = h.new() + self.offset += len(h.new()) + self.dirty = 1 + return 0 + + # fast case first, no offsets, no fuzz + old = h.old() + # patch starts counting at 1 unless we are adding the file + if h.starta == 0: + start = 0 + else: + start = h.starta + self.offset - 1 + orig_start = start + if diffhelpers.testhunk(old, self.lines, start) == 0: + if h.rmfile(): + os.unlink(self.fname) + else: + self.lines[start : start + h.lena] = h.new() + self.offset += h.lenb - h.lena + self.dirty = 1 + return 0 + + # ok, we couldn't match the hunk. Lets look for offsets and fuzz it + self.hashlines() + if h.hunk[-1][0] != ' ': + # if the hunk tried to put something at the bottom of the file + # override the start line and use eof here + search_start = len(self.lines) + else: + search_start = orig_start + + for fuzzlen in xrange(3): + for toponly in [ True, False ]: + old = h.old(fuzzlen, toponly) + + cand = self.findlines(old[0][1:], search_start) + for l in cand: + if diffhelpers.testhunk(old, self.lines, l) == 0: + newlines = h.new(fuzzlen, toponly) + self.lines[l : l + len(old)] = newlines + self.offset += len(newlines) - len(old) + self.dirty = 1 + if fuzzlen: + fuzzstr = "with fuzz %d " % fuzzlen + f = self.ui.warn + self.printfile(True) + else: + fuzzstr = "" + f = self.ui.note + offset = l - orig_start - fuzzlen + if offset == 1: + linestr = "line" + else: + linestr = "lines" + f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") % + (h.number, l+1, fuzzstr, offset, linestr)) + return fuzzlen + self.printfile(True) + self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start)) + self.rej.append(h) + return -1 + +class hunk: + def __init__(self, desc, num, lr, context): + self.number = num + self.desc = desc + self.hunk = [ desc ] + self.a = [] + self.b = [] + if context: + self.read_context_hunk(lr) + else: + self.read_unified_hunk(lr) + + def read_unified_hunk(self, lr): + m = unidesc.match(self.desc) + if not m: + raise PatchError(_("bad hunk #%d") % self.number) + self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups() + if self.lena == None: + self.lena = 1 + else: + self.lena = int(self.lena) + if self.lenb == None: + self.lenb = 1 + else: + self.lenb = int(self.lenb) + self.starta = int(self.starta) + self.startb = int(self.startb) + diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b) + # if we hit eof before finishing out the hunk, the last line will + # be zero length. Lets try to fix it up. + while len(self.hunk[-1]) == 0: + del self.hunk[-1] + del self.a[-1] + del self.b[-1] + self.lena -= 1 + self.lenb -= 1 + + def read_context_hunk(self, lr): + self.desc = lr.readline() + m = contextdesc.match(self.desc) + if not m: + raise PatchError(_("bad hunk #%d") % self.number) + foo, self.starta, foo2, aend, foo3 = m.groups() + self.starta = int(self.starta) + if aend == None: + aend = self.starta + self.lena = int(aend) - self.starta + if self.starta: + self.lena += 1 + for x in xrange(self.lena): + l = lr.readline() + if l.startswith('---'): + lr.push(l) + break + s = l[2:] + if l.startswith('- ') or l.startswith('! '): + u = '-' + s + elif l.startswith(' '): + u = ' ' + s + else: + raise PatchError(_("bad hunk #%d old text line %d") % + (self.number, x)) + self.a.append(u) + self.hunk.append(u) + + l = lr.readline() + if l.startswith('\ '): + s = self.a[-1][:-1] + self.a[-1] = s + self.hunk[-1] = s + l = lr.readline() + m = contextdesc.match(l) + if not m: + raise PatchError(_("bad hunk #%d") % self.number) + foo, self.startb, foo2, bend, foo3 = m.groups() + self.startb = int(self.startb) + if bend == None: + bend = self.startb + self.lenb = int(bend) - self.startb + if self.startb: + self.lenb += 1 + hunki = 1 + for x in xrange(self.lenb): + l = lr.readline() + if l.startswith('\ '): + s = self.b[-1][:-1] + self.b[-1] = s + self.hunk[hunki-1] = s + continue + if not l: + lr.push(l) + break + s = l[2:] + if l.startswith('+ ') or l.startswith('! '): + u = '+' + s + elif l.startswith(' '): + u = ' ' + s + elif len(self.b) == 0: + # this can happen when the hunk does not add any lines + lr.push(l) + break + else: + raise PatchError(_("bad hunk #%d old text line %d") % + (self.number, x)) + self.b.append(s) + while True: + if hunki >= len(self.hunk): + h = "" + else: + h = self.hunk[hunki] + hunki += 1 + if h == u: + break + elif h.startswith('-'): + continue + else: + self.hunk.insert(hunki-1, u) + break + + if not self.a: + # this happens when lines were only added to the hunk + for x in self.hunk: + if x.startswith('-') or x.startswith(' '): + self.a.append(x) + if not self.b: + # this happens when lines were only deleted from the hunk + for x in self.hunk: + if x.startswith('+') or x.startswith(' '): + self.b.append(x[1:]) + # @@ -start,len +start,len @@ + self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena, + self.startb, self.lenb) + self.hunk[0] = self.desc + + def reverse(self): + origlena = self.lena + origstarta = self.starta + self.lena = self.lenb + self.starta = self.startb + self.lenb = origlena + self.startb = origstarta + self.a = [] + self.b = [] + # self.hunk[0] is the @@ description + for x in xrange(1, len(self.hunk)): + o = self.hunk[x] + if o.startswith('-'): + n = '+' + o[1:] + self.b.append(o[1:]) + elif o.startswith('+'): + n = '-' + o[1:] + self.a.append(n) + else: + n = o + self.b.append(o[1:]) + self.a.append(o) + self.hunk[x] = o + + def fix_newline(self): + diffhelpers.fix_newline(self.hunk, self.a, self.b) + + def complete(self): + return len(self.a) == self.lena and len(self.b) == self.lenb + + def createfile(self): + return self.starta == 0 and self.lena == 0 + + def rmfile(self): + return self.startb == 0 and self.lenb == 0 + + def fuzzit(self, l, fuzz, toponly): + # this removes context lines from the top and bottom of list 'l'. It + # checks the hunk to make sure only context lines are removed, and then + # returns a new shortened list of lines. + fuzz = min(fuzz, len(l)-1) + if fuzz: + top = 0 + bot = 0 + hlen = len(self.hunk) + for x in xrange(hlen-1): + # the hunk starts with the @@ line, so use x+1 + if self.hunk[x+1][0] == ' ': + top += 1 + else: + break + if not toponly: + for x in xrange(hlen-1): + if self.hunk[hlen-bot-1][0] == ' ': + bot += 1 + else: + break + + # top and bot now count context in the hunk + # adjust them if either one is short + context = max(top, bot, 3) + if bot < context: + bot = max(0, fuzz - (context - bot)) + else: + bot = min(fuzz, bot) + if top < context: + top = max(0, fuzz - (context - top)) + else: + top = min(fuzz, top) + + return l[top:len(l)-bot] + return l + + def old(self, fuzz=0, toponly=False): + return self.fuzzit(self.a, fuzz, toponly) + + def newctrl(self): + res = [] + for x in self.hunk: + c = x[0] + if c == ' ' or c == '+': + res.append(x) + return res + + def new(self, fuzz=0, toponly=False): + return self.fuzzit(self.b, fuzz, toponly) + +class binhunk: + 'A binary patch file. Only understands literals so far.' + def __init__(self, gitpatch): + self.gitpatch = gitpatch + self.text = None + self.hunk = ['GIT binary patch\n'] + + def createfile(self): + return self.gitpatch.op in ('ADD', 'RENAME', 'COPY') + + def rmfile(self): + return self.gitpatch.op == 'DELETE' + + def complete(self): + return self.text is not None + + def new(self): + return [self.text] + + def extract(self, fp): + line = fp.readline() + self.hunk.append(line) while line and not line.startswith('literal '): - line = readline() + line = fp.readline() + self.hunk.append(line) if not line: - return None, i[0] - size = int(line[8:]) + raise PatchError(_('could not extract binary patch')) + size = int(line[8:].rstrip()) dec = [] - line = readline() - while line: + line = fp.readline() + self.hunk.append(line) + while len(line) > 1: l = line[0] if l <= 'Z' and l >= 'A': l = ord(l) - ord('A') + 1 else: l = ord(l) - ord('a') + 27 - dec.append(base85.b85decode(line[1:])[:l]) - line = readline() + dec.append(base85.b85decode(line[1:-1])[:l]) + line = fp.readline() + self.hunk.append(line) text = zlib.decompress(''.join(dec)) if len(text) != size: - raise util.Abort(_('binary patch is %d bytes, not %d') % - (len(text), size)) - return text, i[0] + raise PatchError(_('binary patch is %d bytes, not %d') % + len(text), size) + self.text = text - pf = file(patchname) - pfline = 1 - - fd, patchname = tempfile.mkstemp(prefix='hg-patch-') - tmpfp = os.fdopen(fd, 'w') +def parsefilename(str): + # --- filename \t|space stuff + s = str[4:] + i = s.find('\t') + if i < 0: + i = s.find(' ') + if i < 0: + return s + return s[:i] - try: - for i in xrange(len(gitpatches)): - p = gitpatches[i] - if not p.copymod and not p.binary: - continue - - # rewrite patch hunk - while pfline < p.lineno: - tmpfp.write(pf.readline()) - pfline += 1 +def selectfile(afile_orig, bfile_orig, hunk, strip, reverse): + def pathstrip(path, count=1): + pathlen = len(path) + i = 0 + if count == 0: + return path.rstrip() + while count > 0: + i = path.find('/', i) + if i == -1: + raise PatchError(_("unable to strip away %d dirs from %s") % + (count, path)) + i += 1 + # consume '//' in the path + while i < pathlen - 1 and path[i] == '/': + i += 1 + count -= 1 + return path[i:].rstrip() - if p.binary: - text, delta = extractbin(pf) - if not text: - raise util.Abort(_('binary patch extraction failed')) - pfline += delta - if not cwd: - cwd = os.getcwd() - absdst = os.path.join(cwd, p.path) - basedir = os.path.dirname(absdst) - if not os.path.isdir(basedir): - os.makedirs(basedir) - out = file(absdst, 'wb') - out.write(text) - out.close() - elif p.copymod: - copyfile(p.oldpath, p.path, basedir=cwd) - 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) + nulla = afile_orig == "/dev/null" + nullb = bfile_orig == "/dev/null" + afile = pathstrip(afile_orig, strip) + gooda = os.path.exists(afile) and not nulla + bfile = pathstrip(bfile_orig, strip) + if afile == bfile: + goodb = gooda + else: + goodb = os.path.exists(bfile) and not nullb + createfunc = hunk.createfile + if reverse: + createfunc = hunk.rmfile + if not goodb and not gooda and not createfunc(): + raise PatchError(_("unable to find %s or %s for patching") % + (afile, bfile)) + if gooda and goodb: + fname = bfile + if afile in bfile: + fname = afile + elif gooda: + fname = afile + elif not nullb: + fname = bfile + if afile in bfile: + fname = afile + elif not nulla: + fname = afile + return fname + +class linereader: + # simple class to allow pushing lines back into the input stream + def __init__(self, fp): + self.fp = fp + self.buf = [] + + def push(self, line): + self.buf.append(line) - line = pf.readline() - while line: - tmpfp.write(line) - line = pf.readline() - except: - tmpfp.close() - os.unlink(patchname) - raise + def readline(self): + if self.buf: + l = self.buf[0] + del self.buf[0] + return l + return self.fp.readline() + +def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False, + rejmerge=None, updatedir=None): + """reads a patch from fp and tries to apply it. The dict 'changed' is + filled in with all of the filenames changed by the patch. Returns 0 + for a clean patch, -1 if any rejects were found and 1 if there was + any fuzz.""" + + def scangitpatch(fp, firstline, cwd=None): + '''git patches can modify a file, then copy that file to + a new file, but expect the source to be the unmodified form. + So we scan the patch looking for that case so we can do + the copies ahead of time.''' - tmpfp.close() - return patchname + pos = 0 + try: + pos = fp.tell() + except IOError: + fp = cStringIO.StringIO(fp.read()) + + (dopatch, gitpatches) = readgitpatch(fp, firstline) + for gp in gitpatches: + if gp.copymod: + copyfile(gp.oldpath, gp.path, basedir=cwd) + + fp.seek(pos) -def patch(patchname, ui, strip=1, cwd=None, files={}): - """apply the patch to the working directory. - a list of patched files is returned""" + return fp, dopatch, gitpatches + + current_hunk = None + current_file = None + afile = "" + bfile = "" + state = None + hunknum = 0 + rejects = 0 + + git = False + gitre = re.compile('diff --git (a/.*) (b/.*)') - # helper function - def __patch(patchname): - """patch and updates the files and fuzz variables""" - fuzz = False - - args = [] - patcher = ui.config('ui', 'patch') - if not patcher: - patcher = util.find_exe('gpatch') or util.find_exe('patch') - # Try to be smart only if patch call was not supplied - if util.needbinarypatch(): - args.append('--binary') - - if not patcher: - raise util.Abort(_('no patch command found in hgrc or PATH')) - - if cwd: - args.append('-d %s' % util.shellquote(cwd)) - fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, - util.shellquote(patchname))) + # our states + BFILE = 1 + err = 0 + context = None + lr = linereader(fp) + dopatch = True + gitworkdone = False - 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]) - return fuzz + while True: + newfile = False + x = lr.readline() + if not x: + break + if current_hunk: + if x.startswith('\ '): + current_hunk.fix_newline() + ret = current_file.apply(current_hunk, reverse) + if ret >= 0: + changed.setdefault(current_file.fname, (None, None)) + if ret > 0: + err = 1 + current_hunk = None + gitworkdone = False + if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or + ((context or context == None) and x.startswith('***************')))): + try: + if context == None and x.startswith('***************'): + context = True + current_hunk = hunk(x, hunknum + 1, lr, context) + except PatchError, err: + ui.debug(err) + current_hunk = None + continue + hunknum += 1 + if not current_file: + if sourcefile: + current_file = patchfile(ui, sourcefile) + else: + current_file = selectfile(afile, bfile, current_hunk, + strip, reverse) + current_file = patchfile(ui, current_file) + elif state == BFILE and x.startswith('GIT binary patch'): + current_hunk = binhunk(changed[bfile[2:]][1]) + if not current_file: + if sourcefile: + current_file = patchfile(ui, sourcefile) + else: + current_file = selectfile(afile, bfile, current_hunk, + strip, reverse) + current_file = patchfile(ui, current_file) + hunknum += 1 + current_hunk.extract(fp) + elif x.startswith('diff --git'): + # check for git diff, scanning the whole patch file if needed + m = gitre.match(x) + if m: + afile, bfile = m.group(1, 2) + if not git: + git = True + fp, dopatch, gitpatches = scangitpatch(fp, x) + for gp in gitpatches: + changed[gp.path] = (gp.op, gp) + # else error? + # copy/rename + modify should modify target, not source + if changed.get(bfile[2:], (None, None))[0] in ('COPY', + 'RENAME'): + afile = bfile + gitworkdone = True + newfile = True + elif x.startswith('---'): + # check for a unified diff + l2 = lr.readline() + if not l2.startswith('+++'): + lr.push(l2) + continue + newfile = True + context = False + afile = parsefilename(x) + bfile = parsefilename(l2) + elif x.startswith('***'): + # check for a context diff + l2 = lr.readline() + if not l2.startswith('---'): + lr.push(l2) + continue + l3 = lr.readline() + lr.push(l3) + if not l3.startswith("***************"): + lr.push(l2) + continue + newfile = True + context = True + afile = parsefilename(x) + bfile = parsefilename(l2) - (dopatch, gitpatches) = readgitpatch(patchname) - for gp in gitpatches: - files[gp.path] = (gp.op, gp) - - fuzz = False - if dopatch: - filterpatch = dopatch & (GP_FILTER | GP_BINARY) - if filterpatch: - patchname = dogitpatch(patchname, gitpatches, cwd=cwd) - try: - if dopatch & GP_PATCH: - fuzz = __patch(patchname) - finally: - if filterpatch: - os.unlink(patchname) - - return fuzz + if newfile: + if current_file: + current_file.close() + if rejmerge: + rejmerge(current_file) + rejects += len(current_file.rej) + state = BFILE + current_file = None + hunknum = 0 + if current_hunk: + if current_hunk.complete(): + ret = current_file.apply(current_hunk, reverse) + if ret >= 0: + changed.setdefault(current_file.fname, (None, None)) + if ret > 0: + err = 1 + else: + fname = current_file and current_file.fname or None + raise PatchError(_("malformed patch %s %s") % (fname, + current_hunk.desc)) + if current_file: + current_file.close() + if rejmerge: + rejmerge(current_file) + rejects += len(current_file.rej) + if updatedir and git: + updatedir(gitpatches) + if rejects: + return -1 + if hunknum == 0 and dopatch and not gitworkdone: + raise NoHunks + return err def diffopts(ui, opts={}, untrusted=False): def get(key, name=None): @@ -369,7 +1017,7 @@ def diffopts(ui, opts={}, untrusted=Fals ignorewsamount=get('ignore_space_change', 'ignorewsamount'), ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines')) -def updatedir(ui, repo, patches, wlock=None): +def updatedir(ui, repo, patches): '''Update dirstate after patch application according to metadata''' if not patches: return @@ -391,29 +1039,32 @@ def updatedir(ui, repo, patches, wlock=N for src, dst, after in copies: if not after: copyfile(src, dst, repo.root) - repo.copy(src, dst, wlock=wlock) + repo.copy(src, dst) removes = removes.keys() if removes: removes.sort() - repo.remove(removes, True, wlock=wlock) + repo.remove(removes, True) for f in patches: ctype, gp = patches[f] if gp and gp.mode: x = gp.mode & 0100 != 0 + l = gp.mode & 020000 != 0 dst = os.path.join(repo.root, gp.path) # patch won't create empty files if ctype == 'ADD' and not os.path.exists(dst): repo.wwrite(gp.path, '', x and 'x' or '') else: - util.set_exec(dst, x) - cmdutil.addremove(repo, cfiles, wlock=wlock) + util.set_link(dst, l) + if not l: + util.set_exec(dst, x) + cmdutil.addremove(repo, cfiles) files = patches.keys() files.extend([r for r in removes if r not in files]) files.sort() return files -def b85diff(fp, to, tn): +def b85diff(to, tn): '''print base85-encoded binary diff''' def gitindex(text): if not text: @@ -497,24 +1148,30 @@ def diff(repo, node1=None, node2=None, f if node2: ctx2 = context.changectx(repo, node2) execf2 = ctx2.manifest().execf + linkf2 = ctx2.manifest().linkf else: ctx2 = context.workingctx(repo) execf2 = util.execfunc(repo.root, None) + linkf2 = util.linkfunc(repo.root, None) if execf2 is None: - execf2 = ctx2.parents()[0].manifest().copy().execf + mc = ctx2.parents()[0].manifest().copy() + execf2 = mc.execf + linkf2 = mc.linkf # returns False if there was no rename between ctx1 and ctx2 # returns None if the file was created between ctx1 and ctx2 # returns the (file, node) present in ctx1 that was renamed to f in ctx2 - def renamed(f): - startrev = ctx1.rev() - c = ctx2 + # This will only really work if c1 is the Nth 1st parent of c2. + def renamed(c1, c2, man, f): + startrev = c1.rev() + c = c2 crev = c.rev() if crev is None: crev = repo.changelog.count() orig = f + files = (f,) while crev > startrev: - if f in c.files(): + if f in files: try: src = getfilectx(f, c).renamed() except revlog.LookupError: @@ -524,7 +1181,8 @@ def diff(repo, node1=None, node2=None, f crev = c.parents()[0].rev() # try to reuse c = getctx(crev) - if f not in man1: + files = c.files() + if f not in man: return None if f == orig: return False @@ -538,11 +1196,27 @@ def diff(repo, node1=None, node2=None, f if opts.git: copied = {} - for f in added: - src = renamed(f) + c1, c2 = ctx1, ctx2 + files = added + man = man1 + if node2 and ctx1.rev() >= ctx2.rev(): + # renamed() starts at c2 and walks back in history until c1. + # Since ctx1.rev() >= ctx2.rev(), invert ctx2 and ctx1 to + # detect (inverted) copies. + c1, c2 = ctx2, ctx1 + files = removed + man = ctx2.manifest() + for f in files: + src = renamed(c1, c2, man, f) if src: copied[f] = src - srcs = [x[1] for x in copied.items()] + if ctx1 == c2: + # invert the copied dict + copied = dict([(v, k) for (k, v) in copied.iteritems()]) + # If we've renamed file foo to bar (copied['bar'] = 'foo'), + # avoid showing a diff for foo if we're going to show + # the rename to bar. + srcs = [x[1] for x in copied.iteritems() if x[0] in added] all = modified + added + removed all.sort() @@ -558,8 +1232,8 @@ def diff(repo, node1=None, node2=None, f if f not in removed: tn = getfilectx(f, ctx2).data() if opts.git: - def gitmode(x): - return x and '100755' or '100644' + def gitmode(x, l): + return l and '120000' or (x and '100755' or '100644') def addmodehdr(header, omode, nmode): if omode != nmode: header.append('old mode %s\n' % omode) @@ -567,10 +1241,10 @@ def diff(repo, node1=None, node2=None, f a, b = f, f if f in added: - mode = gitmode(execf2(f)) + mode = gitmode(execf2(f), linkf2(f)) if f in copied: a = copied[f] - omode = gitmode(man1.execf(a)) + omode = gitmode(man1.execf(a), man1.linkf(a)) addmodehdr(header, omode, mode) if a in removed and a not in gone: op = 'rename' @@ -588,11 +1262,11 @@ def diff(repo, node1=None, node2=None, f if f in srcs: dodiff = False else: - mode = gitmode(man1.execf(f)) + mode = gitmode(man1.execf(f), man1.linkf(f)) header.append('deleted file mode %s\n' % mode) else: - omode = gitmode(man1.execf(f)) - nmode = gitmode(execf2(f)) + omode = gitmode(man1.execf(f), man1.linkf(f)) + nmode = gitmode(execf2(f), linkf2(f)) addmodehdr(header, omode, nmode) if util.binary(to) or util.binary(tn): dodiff = 'binary' @@ -600,7 +1274,7 @@ def diff(repo, node1=None, node2=None, f header.insert(0, 'diff --git a/%s b/%s\n' % (a, b)) if dodiff: if dodiff == 'binary': - text = b85diff(fp, to, tn) + text = b85diff(to, tn) else: text = mdiff.unidiff(to, date1, # ctx2 date may be dynamic diff --git a/mercurial/repo.py b/mercurial/repo.py --- a/mercurial/repo.py +++ b/mercurial/repo.py @@ -9,16 +9,26 @@ class RepoError(Exception): pass +class NoCapability(RepoError): + pass + class repository(object): def capable(self, name): '''tell whether repo supports named capability. return False if not supported. if boolean capability, return True. if string capability, return string.''' + if name in self.capabilities: + return True name_eq = name + '=' for cap in self.capabilities: - if name == cap: - return True if cap.startswith(name_eq): return cap[len(name_eq):] return False + + def requirecap(self, name, purpose): + '''raise an exception if the given capability is not present''' + if not self.capable(name): + raise NoCapability(_('cannot %s; remote repository does not ' + 'support the %r capability') % + (purpose, name)) diff --git a/mercurial/revlog.py b/mercurial/revlog.py --- a/mercurial/revlog.py +++ b/mercurial/revlog.py @@ -15,17 +15,34 @@ from i18n import _ import binascii, changegroup, errno, ancestor, mdiff, os import sha, struct, util, zlib -# revlog version strings -REVLOGV0 = 0 -REVLOGNG = 1 +_pack = struct.pack +_unpack = struct.unpack +_compress = zlib.compress +_decompress = zlib.decompress +_sha = sha.new # revlog flags +REVLOGV0 = 0 +REVLOGNG = 1 REVLOGNGINLINEDATA = (1 << 16) REVLOG_DEFAULT_FLAGS = REVLOGNGINLINEDATA - REVLOG_DEFAULT_FORMAT = REVLOGNG REVLOG_DEFAULT_VERSION = REVLOG_DEFAULT_FORMAT | REVLOG_DEFAULT_FLAGS +class RevlogError(Exception): + pass +class LookupError(RevlogError): + pass + +def getoffset(q): + return int(q >> 16) + +def gettype(q): + return int(q & 0xFFFF) + +def offset_type(offset, type): + return long(long(offset) << 16 | type) + def hash(text, p1, p2): """generate a hash from the given text and its parent hashes @@ -35,48 +52,39 @@ def hash(text, p1, p2): """ l = [p1, p2] l.sort() - s = sha.new(l[0]) + s = _sha(l[0]) s.update(l[1]) s.update(text) return s.digest() def compress(text): """ generate a possibly-compressed representation of text """ - if not text: return ("", text) + if not text: + return ("", text) if len(text) < 44: - if text[0] == '\0': return ("", text) + if text[0] == '\0': + return ("", text) return ('u', text) - bin = zlib.compress(text) + bin = _compress(text) if len(bin) > len(text): - if text[0] == '\0': return ("", text) + if text[0] == '\0': + return ("", text) return ('u', text) return ("", bin) def decompress(bin): """ decompress the given input """ - if not bin: return bin + if not bin: + return bin t = bin[0] - if t == '\0': return bin - if t == 'x': return zlib.decompress(bin) - if t == 'u': return bin[1:] + if t == '\0': + return bin + if t == 'x': + return _decompress(bin) + if t == 'u': + return bin[1:] raise RevlogError(_("unknown compression type %r") % t) -indexformatv0 = ">4l20s20s20s" -v0shaoffset = 56 -# index ng: -# 6 bytes offset -# 2 bytes flags -# 4 bytes compressed length -# 4 bytes uncompressed length -# 4 bytes: base rev -# 4 bytes link rev -# 4 bytes parent 1 rev -# 4 bytes parent 2 rev -# 32 bytes: nodeid -indexformatng = ">Qiiiiii20s12x" -ngshaoffset = 32 -versionformat = ">I" - class lazyparser(object): """ this class avoids the need to parse the entirety of large indices @@ -88,11 +96,9 @@ class lazyparser(object): safe_to_use = os.name != 'nt' or (not util.is_win_9x() and hasattr(util, 'win32api')) - def __init__(self, dataf, size, indexformat, shaoffset): + def __init__(self, dataf, size): self.dataf = dataf - self.format = indexformat - self.s = struct.calcsize(indexformat) - self.indexformat = indexformat + self.s = struct.calcsize(indexformatng) self.datasize = size self.l = size/self.s self.index = [None] * self.l @@ -100,7 +106,6 @@ class lazyparser(object): self.allmap = 0 self.all = 0 self.mapfind_count = 0 - self.shaoffset = shaoffset def loadmap(self): """ @@ -109,7 +114,8 @@ class lazyparser(object): which is fairly slow. loadmap can load up just the node map, which takes much less time. """ - if self.allmap: return + if self.allmap: + return end = self.datasize self.allmap = 1 cur = 0 @@ -120,7 +126,7 @@ class lazyparser(object): data = self.dataf.read(blocksize) off = 0 for x in xrange(256): - n = data[off + self.shaoffset:off + self.shaoffset + 20] + n = data[off + ngshaoffset:off + ngshaoffset + 20] self.map[n] = count count += 1 if count >= self.l: @@ -129,7 +135,8 @@ class lazyparser(object): cur += blocksize def loadblock(self, blockstart, blocksize, data=None): - if self.all: return + if self.all: + return if data is None: self.dataf.seek(blockstart) if blockstart + blocksize > self.datasize: @@ -148,13 +155,14 @@ class lazyparser(object): if self.index[i + x] == None: b = data[off : off + self.s] self.index[i + x] = b - n = b[self.shaoffset:self.shaoffset + 20] + n = b[ngshaoffset:ngshaoffset + 20] self.map[n] = i + x off += self.s def findnode(self, node): """search backwards through the index file for a specific node""" - if self.allmap: return None + if self.allmap: + return None # hg log will cause many many searches for the manifest # nodes. After we get called a few times, just load the whole @@ -180,14 +188,14 @@ class lazyparser(object): data = self.dataf.read(end - start) findend = end - start while True: - # we're searching backwards, so weh have to make sure + # we're searching backwards, so we have to make sure # we don't find a changeset where this node is a parent - off = data.rfind(node, 0, findend) + off = data.find(node, 0, findend) findend = off if off >= 0: i = off / self.s off = i * self.s - n = data[off + self.shaoffset:off + self.shaoffset + 20] + n = data[off + ngshaoffset:off + ngshaoffset + 20] if n == node: self.map[n] = i + start / self.s return node @@ -197,11 +205,12 @@ class lazyparser(object): return None def loadindex(self, i=None, end=None): - if self.all: return + if self.all: + return all = False if i == None: blockstart = 0 - blocksize = (512 / self.s) * self.s + blocksize = (65536 / self.s) * self.s end = self.datasize all = True else: @@ -210,13 +219,14 @@ class lazyparser(object): end = end * self.s blocksize = end - blockstart else: - blockstart = (i & ~63) * self.s - blocksize = self.s * 64 + blockstart = (i & ~1023) * self.s + blocksize = self.s * 1024 end = blockstart + blocksize while blockstart < end: self.loadblock(blockstart, blocksize) blockstart += blocksize - if all: self.all = True + if all: + self.all = True class lazyindex(object): """a lazy version of the index array""" @@ -230,16 +240,15 @@ class lazyindex(object): self.p.loadindex(pos) return self.p.index[pos] def __getitem__(self, pos): - ret = self.p.index[pos] or self.load(pos) - if isinstance(ret, str): - ret = struct.unpack(self.p.indexformat, ret) - return ret + return _unpack(indexformatng, self.p.index[pos] or self.load(pos)) def __setitem__(self, pos, item): - self.p.index[pos] = item + self.p.index[pos] = _pack(indexformatng, *item) def __delitem__(self, pos): del self.p.index[pos] + def insert(self, pos, e): + self.p.index.insert(pos, _pack(indexformatng, *e)) def append(self, e): - self.p.index.append(e) + self.p.index.append(_pack(indexformatng, *e)) class lazymap(object): """a lazy version of the node map""" @@ -262,8 +271,8 @@ class lazymap(object): self.p.loadindex(i) ret = self.p.index[i] if isinstance(ret, str): - ret = struct.unpack(self.p.indexformat, ret) - yield ret[-1] + ret = _unpack(indexformatng, ret) + yield ret[7] def __getitem__(self, key): try: return self.p.map[key] @@ -278,8 +287,112 @@ class lazymap(object): def __delitem__(self, key): del self.p.map[key] -class RevlogError(Exception): pass -class LookupError(RevlogError): pass +indexformatv0 = ">4l20s20s20s" +v0shaoffset = 56 + +class revlogoldio(object): + def __init__(self): + self.size = struct.calcsize(indexformatv0) + + def parseindex(self, fp, inline): + s = self.size + index = [] + nodemap = {nullid: nullrev} + n = off = 0 + data = fp.read() + l = len(data) + while off + s <= l: + cur = data[off:off + s] + off += s + e = _unpack(indexformatv0, cur) + # transform to revlogv1 format + e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3], + nodemap[e[4]], nodemap[e[5]], e[6]) + index.append(e2) + nodemap[e[6]] = n + n += 1 + + return index, nodemap, None + + def packentry(self, entry, node, version, rev): + e2 = (getoffset(entry[0]), entry[1], entry[3], entry[4], + node(entry[5]), node(entry[6]), entry[7]) + return _pack(indexformatv0, *e2) + +# index ng: +# 6 bytes offset +# 2 bytes flags +# 4 bytes compressed length +# 4 bytes uncompressed length +# 4 bytes: base rev +# 4 bytes link rev +# 4 bytes parent 1 rev +# 4 bytes parent 2 rev +# 32 bytes: nodeid +indexformatng = ">Qiiiiii20s12x" +ngshaoffset = 32 +versionformat = ">I" + +class revlogio(object): + def __init__(self): + self.size = struct.calcsize(indexformatng) + + def parseindex(self, fp, inline): + try: + size = util.fstat(fp).st_size + except AttributeError: + size = 0 + + if lazyparser.safe_to_use and not inline and size > 1000000: + # big index, let's parse it on demand + parser = lazyparser(fp, size) + index = lazyindex(parser) + nodemap = lazymap(parser) + e = list(index[0]) + type = gettype(e[0]) + e[0] = offset_type(0, type) + index[0] = e + return index, nodemap, None + + s = self.size + cache = None + index = [] + nodemap = {nullid: nullrev} + n = off = 0 + # if we're not using lazymap, always read the whole index + data = fp.read() + l = len(data) - s + append = index.append + if inline: + cache = (0, data) + while off <= l: + e = _unpack(indexformatng, data[off:off + s]) + nodemap[e[7]] = n + append(e) + n += 1 + if e[1] < 0: + break + off += e[1] + s + else: + while off <= l: + e = _unpack(indexformatng, data[off:off + s]) + nodemap[e[7]] = n + append(e) + n += 1 + off += s + + e = list(index[0]) + type = gettype(e[0]) + e[0] = offset_type(0, type) + index[0] = e + + return index, nodemap, cache + + def packentry(self, entry, node, version, rev): + p = _pack(indexformatng, *entry) + if rev == 0: + p = _pack(versionformat, version) + p[4:] + return p class revlog(object): """ @@ -316,200 +429,101 @@ class revlog(object): self.indexfile = indexfile self.datafile = indexfile[:-2] + ".d" self.opener = opener + self._cache = None + self._chunkcache = None + self.nodemap = {nullid: nullrev} + self.index = [] - self.indexstat = None - self.cache = None - self.chunkcache = None - self.defversion = REVLOG_DEFAULT_VERSION + v = REVLOG_DEFAULT_VERSION if hasattr(opener, "defversion"): - self.defversion = opener.defversion - if self.defversion & REVLOGNG: - self.defversion |= REVLOGNGINLINEDATA - self.load() + v = opener.defversion + if v & REVLOGNG: + v |= REVLOGNGINLINEDATA - def load(self): - v = self.defversion + i = "" try: f = self.opener(self.indexfile) i = f.read(4) f.seek(0) + if len(i) > 0: + v = struct.unpack(versionformat, i)[0] except IOError, inst: if inst.errno != errno.ENOENT: raise - i = "" - else: - try: - st = util.fstat(f) - except AttributeError, inst: - st = None - else: - oldst = self.indexstat - if (oldst and st.st_dev == oldst.st_dev - and st.st_ino == oldst.st_ino - and st.st_mtime == oldst.st_mtime - and st.st_ctime == oldst.st_ctime - and st.st_size == oldst.st_size): - return - self.indexstat = st - if len(i) > 0: - v = struct.unpack(versionformat, i)[0] + + self.version = v + self._inline = v & REVLOGNGINLINEDATA flags = v & ~0xFFFF fmt = v & 0xFFFF - if fmt == REVLOGV0: - if flags: - raise RevlogError(_("index %s unknown flags %#04x for format v0") - % (self.indexfile, flags >> 16)) - elif fmt == REVLOGNG: - if flags & ~REVLOGNGINLINEDATA: - raise RevlogError(_("index %s unknown flags %#04x for revlogng") - % (self.indexfile, flags >> 16)) - else: + if fmt == REVLOGV0 and flags: + raise RevlogError(_("index %s unknown flags %#04x for format v0") + % (self.indexfile, flags >> 16)) + elif fmt == REVLOGNG and flags & ~REVLOGNGINLINEDATA: + raise RevlogError(_("index %s unknown flags %#04x for revlogng") + % (self.indexfile, flags >> 16)) + elif fmt > REVLOGNG: raise RevlogError(_("index %s unknown format %d") % (self.indexfile, fmt)) - self.version = v - if v == REVLOGV0: - self.indexformat = indexformatv0 - shaoffset = v0shaoffset - else: - self.indexformat = indexformatng - shaoffset = ngshaoffset - - if i: - if (lazyparser.safe_to_use and not self.inlinedata() and - st and st.st_size > 10000): - # big index, let's parse it on demand - parser = lazyparser(f, st.st_size, self.indexformat, shaoffset) - self.index = lazyindex(parser) - self.nodemap = lazymap(parser) - else: - self.parseindex(f, st) - if self.version != REVLOGV0: - e = list(self.index[0]) - type = self.ngtype(e[0]) - e[0] = self.offset_type(0, type) - self.index[0] = e - else: - self.nodemap = {nullid: nullrev} - self.index = [] - - def parseindex(self, fp, st): - s = struct.calcsize(self.indexformat) - self.index = [] - self.nodemap = {nullid: nullrev} - inline = self.inlinedata() - n = 0 - leftover = None - while True: - if st: - data = fp.read(65536) - else: - # hack for httprangereader, it doesn't do partial reads well - data = fp.read() - if not data: - break - if n == 0 and self.inlinedata(): - # cache the first chunk - self.chunkcache = (0, data) - if leftover: - data = leftover + data - leftover = None - off = 0 - l = len(data) - while off < l: - if l - off < s: - leftover = data[off:] - break - cur = data[off:off + s] - off += s - e = struct.unpack(self.indexformat, cur) - self.index.append(e) - self.nodemap[e[-1]] = n - n += 1 - if inline: - if e[1] < 0: - break - off += e[1] - if off > l: - # some things don't seek well, just read it - fp.read(off - l) - break - if not st: - break + self._io = revlogio() + if self.version == REVLOGV0: + self._io = revlogoldio() + if i: + d = self._io.parseindex(f, self._inline) + self.index, self.nodemap, self._chunkcache = d - - def ngoffset(self, q): - if q & 0xFFFF: - raise RevlogError(_('%s: incompatible revision flag %x') % - (self.indexfile, q)) - return long(q >> 16) + # add the magic null revision at -1 + self.index.append((0, 0, 0, -1, -1, -1, -1, nullid)) - def ngtype(self, q): - return int(q & 0xFFFF) - - def offset_type(self, offset, type): - return long(long(offset) << 16 | type) - - def loadindex(self, start, end): + def _loadindex(self, start, end): """load a block of indexes all at once from the lazy parser""" if isinstance(self.index, lazyindex): self.index.p.loadindex(start, end) - def loadindexmap(self): + def _loadindexmap(self): """loads both the map and the index from the lazy parser""" if isinstance(self.index, lazyindex): p = self.index.p p.loadindex() self.nodemap = p.map - def loadmap(self): + def _loadmap(self): """loads the map from the lazy parser""" if isinstance(self.nodemap, lazymap): self.nodemap.p.loadmap() self.nodemap = self.nodemap.p.map - def inlinedata(self): return self.version & REVLOGNGINLINEDATA - def tip(self): return self.node(len(self.index) - 1) - def count(self): return len(self.index) - def node(self, rev): - return rev == nullrev and nullid or self.index[rev][-1] + def tip(self): + return self.node(len(self.index) - 2) + def count(self): + return len(self.index) - 1 + def rev(self, node): try: return self.nodemap[node] except KeyError: raise LookupError(_('%s: no node %s') % (self.indexfile, hex(node))) + def node(self, rev): + return self.index[rev][7] def linkrev(self, node): - return (node == nullid) and nullrev or self.index[self.rev(node)][-4] + return self.index[self.rev(node)][4] def parents(self, node): - if node == nullid: return (nullid, nullid) - r = self.rev(node) - d = self.index[r][-3:-1] - if self.version == REVLOGV0: - return d + d = self.index[self.rev(node)][5:7] return (self.node(d[0]), self.node(d[1])) def parentrevs(self, rev): - if rev == nullrev: - return (nullrev, nullrev) - d = self.index[rev][-3:-1] - if self.version == REVLOGV0: - return (self.rev(d[0]), self.rev(d[1])) - return d + return self.index[rev][5:7] def start(self, rev): - if rev == nullrev: - return 0 - if self.version != REVLOGV0: - return self.ngoffset(self.index[rev][0]) - return self.index[rev][0] - - def end(self, rev): return self.start(rev) + self.length(rev) + return int(self.index[rev][0] >> 16) + def end(self, rev): + return self.start(rev) + self.length(rev) + def length(self, rev): + return self.index[rev][1] + def base(self, rev): + return self.index[rev][3] def size(self, rev): """return the length of the uncompressed text for a given revision""" - if rev == nullrev: - return 0 - l = -1 - if self.version != REVLOGV0: - l = self.index[rev][2] + l = self.index[rev][2] if l >= 0: return l @@ -536,17 +550,6 @@ class revlog(object): return l """ - def length(self, rev): - if rev == nullrev: - return 0 - else: - return self.index[rev][1] - def base(self, rev): - if (rev == nullrev): - return nullrev - else: - return self.index[rev][-5] - def reachable(self, node, stop=None): """return a hash of all nodes ancestral to a given node, including the node itself, stopping when stop is matched""" @@ -730,6 +733,17 @@ class revlog(object): if stop is specified, it will consider all the revs from stop as if they had no children """ + if start is None and stop is None: + count = self.count() + if not count: + return [nullid] + ishead = [1] * (count + 1) + index = self.index + for r in xrange(count): + e = index[r] + ishead[e[5]] = ishead[e[6]] = 0 + return [self.node(r) for r in xrange(count) if ishead[r]] + if start is None: start = nullid if stop is None: @@ -781,9 +795,12 @@ class revlog(object): try: # str(rev) rev = int(id) - if str(rev) != id: raise ValueError - if rev < 0: rev = self.count() + rev - if rev < 0 or rev >= self.count(): raise ValueError + if str(rev) != id: + raise ValueError + if rev < 0: + rev = self.count() + rev + if rev < 0 or rev >= self.count(): + raise ValueError return self.node(rev) except (ValueError, OverflowError): pass @@ -817,7 +834,6 @@ class revlog(object): - revision number or str(revision number) - nodeid or subset of hex nodeid """ - n = self._match(id) if n is not None: return n @@ -832,56 +848,42 @@ class revlog(object): 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""" - return mdiff.textdiff(a, b) - - def patches(self, t, pl): - """apply a list of patches to a string""" - return mdiff.patches(t, pl) - - def chunk(self, rev, df=None, cachelen=4096): - start, length = self.start(rev), self.length(rev) - inline = self.inlinedata() - if inline: - start += (rev + 1) * struct.calcsize(self.indexformat) - end = start + length + def chunk(self, rev, df=None): def loadcache(df): - cache_length = max(cachelen, length) # 4k if not df: - if inline: + if self._inline: df = self.opener(self.indexfile) else: df = self.opener(self.datafile) df.seek(start) - self.chunkcache = (start, df.read(cache_length)) + self._chunkcache = (start, df.read(cache_length)) - if not self.chunkcache: - loadcache(df) + start, length = self.start(rev), self.length(rev) + if self._inline: + start += (rev + 1) * self._io.size + end = start + length - cache_start = self.chunkcache[0] - cache_end = cache_start + len(self.chunkcache[1]) - if start >= cache_start and end <= cache_end: - # it is cached - offset = start - cache_start - else: + offset = 0 + if not self._chunkcache: + cache_length = max(65536, length) loadcache(df) - offset = 0 + else: + cache_start = self._chunkcache[0] + cache_length = len(self._chunkcache[1]) + cache_end = cache_start + cache_length + if start >= cache_start and end <= cache_end: + # it is cached + offset = start - cache_start + else: + cache_length = max(65536, length) + loadcache(df) - #def checkchunk(): - # df = self.opener(self.datafile) - # df.seek(start) - # return df.read(length) - #assert s == checkchunk() - return decompress(self.chunkcache[1][offset:offset + length]) + # avoid copying large chunks + c = self._chunkcache[1] + if cache_length != length: + c = c[offset:offset + length] + + return decompress(c) def delta(self, node): """return or calculate a delta between a node and its predecessor""" @@ -890,55 +892,56 @@ class revlog(object): def revdiff(self, rev1, rev2): """return or calculate a delta between two revisions""" - b1 = self.base(rev1) - b2 = self.base(rev2) - if b1 == b2 and rev1 + 1 == rev2: + if rev1 + 1 == rev2 and self.base(rev1) == self.base(rev2): return self.chunk(rev2) - else: - return self.diff(self.revision(self.node(rev1)), - self.revision(self.node(rev2))) + + return mdiff.textdiff(self.revision(self.node(rev1)), + self.revision(self.node(rev2))) def revision(self, node): """return an uncompressed revision of a given""" - if node == nullid: return "" - if self.cache and self.cache[0] == node: return self.cache[2] + if node == nullid: + return "" + if self._cache and self._cache[0] == node: + return self._cache[2] # look up what we need to read text = None rev = self.rev(node) base = self.base(rev) - if self.inlinedata(): + # check rev flags + if self.index[rev][0] & 0xFFFF: + raise RevlogError(_('incompatible revision flag %x') % + (self.index[rev][0] & 0xFFFF)) + + if self._inline: # we probably have the whole chunk cached df = None else: df = self.opener(self.datafile) # do we have useful data cached? - if self.cache and self.cache[1] >= base and self.cache[1] < rev: - base = self.cache[1] - text = self.cache[2] - self.loadindex(base, rev + 1) + if self._cache and self._cache[1] >= base and self._cache[1] < rev: + base = self._cache[1] + text = self._cache[2] + self._loadindex(base, rev + 1) else: - self.loadindex(base, rev + 1) + self._loadindex(base, rev + 1) text = self.chunk(base, df=df) - bins = [] - for r in xrange(base + 1, rev + 1): - bins.append(self.chunk(r, df=df)) - - text = self.patches(text, bins) - + bins = [self.chunk(r, df) for r in xrange(base + 1, rev + 1)] + text = mdiff.patches(text, bins) p1, p2 = self.parents(node) if node != hash(text, p1, p2): raise RevlogError(_("integrity check failed on %s:%d") % (self.datafile, rev)) - self.cache = (node, rev, text) + self._cache = (node, rev, text) return text def checkinlinesize(self, tr, fp=None): - if not self.inlinedata(): + if not self._inline: return if not fp: fp = self.opener(self.indexfile, 'r') @@ -956,7 +959,7 @@ class revlog(object): tr.add(self.datafile, dataoff) df = self.opener(self.datafile, 'w') - calc = struct.calcsize(self.indexformat) + calc = self._io.size for r in xrange(self.count()): start = self.start(r) + (r + 1) * calc length = self.length(r) @@ -967,16 +970,9 @@ class revlog(object): df.close() fp = self.opener(self.indexfile, 'w', atomictemp=True) self.version &= ~(REVLOGNGINLINEDATA) - if self.count(): - x = self.index[0] - e = struct.pack(self.indexformat, *x)[4:] - l = struct.pack(versionformat, self.version) - fp.write(l) - fp.write(e) - - for i in xrange(1, self.count()): - x = self.index[i] - e = struct.pack(self.indexformat, *x) + self._inline = False + for i in xrange(self.count()): + e = self._io.packentry(self.index[i], self.node, self.version, i) fp.write(e) # if we don't call rename, the temp file will never replace the @@ -984,9 +980,9 @@ class revlog(object): fp.rename() tr.replace(self.indexfile, trindex * calc) - self.chunkcache = None + self._chunkcache = None - def addrevision(self, text, transaction, link, p1=None, p2=None, d=None): + def addrevision(self, text, transaction, link, p1, p2, d=None): """add a revision to the log text - the revision data to add @@ -995,84 +991,60 @@ class revlog(object): p1, p2 - the parent nodeids of the revision d - an optional precomputed delta """ - if not self.inlinedata(): + dfh = None + if not self._inline: dfh = self.opener(self.datafile, "a") - else: - dfh = None ifh = self.opener(self.indexfile, "a+") return self._addrevision(text, transaction, link, p1, p2, d, ifh, dfh) def _addrevision(self, text, transaction, link, p1, p2, d, ifh, dfh): - if text is None: text = "" - if p1 is None: p1 = self.tip() - if p2 is None: p2 = nullid - node = hash(text, p1, p2) - if node in self.nodemap: return node - n = self.count() - t = n - 1 + curr = self.count() + prev = curr - 1 + base = self.base(prev) + offset = self.end(prev) - if n: - base = self.base(t) - start = self.start(base) - end = self.end(t) + if curr: if not d: - prev = self.revision(self.tip()) - d = self.diff(prev, text) + ptext = self.revision(self.node(prev)) + d = mdiff.textdiff(ptext, text) data = compress(d) l = len(data[1]) + len(data[0]) - dist = end - start + l + dist = l + offset - self.start(base) # full versions are inserted when the needed deltas # become comparable to the uncompressed text - if not n or dist > len(text) * 2: + if not curr or dist > len(text) * 2: data = compress(text) l = len(data[1]) + len(data[0]) - base = n - else: - base = self.base(t) - - offset = 0 - if t >= 0: - offset = self.end(t) + base = curr - if self.version == REVLOGV0: - e = (offset, l, base, link, p1, p2, node) - else: - e = (self.offset_type(offset, 0), l, len(text), - base, link, self.rev(p1), self.rev(p2), node) + e = (offset_type(offset, 0), l, len(text), + base, link, self.rev(p1), self.rev(p2), node) + self.index.insert(-1, e) + self.nodemap[node] = curr - self.index.append(e) - self.nodemap[node] = n - entry = struct.pack(self.indexformat, *e) - - if not self.inlinedata(): + entry = self._io.packentry(e, self.node, self.version, curr) + if not self._inline: transaction.add(self.datafile, offset) - transaction.add(self.indexfile, n * len(entry)) + transaction.add(self.indexfile, curr * len(entry)) if data[0]: dfh.write(data[0]) dfh.write(data[1]) dfh.flush() + ifh.write(entry) else: - ifh.seek(0, 2) - transaction.add(self.indexfile, ifh.tell(), self.count() - 1) - - if len(self.index) == 1 and self.version != REVLOGV0: - l = struct.pack(versionformat, self.version) - ifh.write(l) - entry = entry[4:] - - ifh.write(entry) - - if self.inlinedata(): + offset += curr * self._io.size + transaction.add(self.indexfile, offset, curr) + ifh.write(entry) ifh.write(data[0]) ifh.write(data[1]) self.checkinlinesize(transaction, ifh) - self.cache = (node, n, text) + self._cache = (node, curr, text) return node def ancestor(self, a, b): @@ -1142,11 +1114,12 @@ class revlog(object): end = self.end(t) ifh = self.opener(self.indexfile, "a+") - ifh.seek(0, 2) - transaction.add(self.indexfile, ifh.tell(), self.count()) - if self.inlinedata(): + isize = r * self._io.size + if self._inline: + transaction.add(self.indexfile, end + isize, r) dfh = None else: + transaction.add(self.indexfile, isize, r) transaction.add(self.datafile, end) dfh = self.opener(self.datafile, "a") @@ -1190,10 +1163,10 @@ class revlog(object): dfh.flush() ifh.flush() text = self.revision(chain) - text = self.patches(text, [delta]) + text = mdiff.patches(text, [delta]) chk = self._addrevision(text, transaction, link, p1, p2, None, ifh, dfh) - if not dfh and not self.inlinedata(): + if not dfh and not self._inline: # addrevision switched from inline to conventional # reopen the index dfh = self.opener(self.datafile, "a") @@ -1202,23 +1175,21 @@ class revlog(object): raise RevlogError(_("consistency error adding group")) textlen = len(text) else: - if self.version == REVLOGV0: - e = (end, len(cdelta), base, link, p1, p2, node) - else: - e = (self.offset_type(end, 0), len(cdelta), textlen, base, - link, self.rev(p1), self.rev(p2), node) - self.index.append(e) + e = (offset_type(end, 0), len(cdelta), textlen, base, + link, self.rev(p1), self.rev(p2), node) + self.index.insert(-1, e) self.nodemap[node] = r - if self.inlinedata(): - ifh.write(struct.pack(self.indexformat, *e)) + entry = self._io.packentry(e, self.node, self.version, r) + if self._inline: + ifh.write(entry) ifh.write(cdelta) self.checkinlinesize(transaction, ifh) - if not self.inlinedata(): + if not self._inline: dfh = self.opener(self.datafile, "a") ifh = self.opener(self.indexfile, "a") else: dfh.write(cdelta) - ifh.write(struct.pack(self.indexformat, *e)) + ifh.write(entry) t, r, chain, prev = r, r + 1, node, node base = self.base(t) @@ -1232,41 +1203,41 @@ class revlog(object): return if isinstance(self.index, lazyindex): - self.loadindexmap() + self._loadindexmap() # When stripping away a revision, we need to make sure it # does not actually belong to an older changeset. # The minlink parameter defines the oldest revision # we're allowed to strip away. - while minlink > self.index[rev][-4]: + while minlink > self.index[rev][4]: rev += 1 if rev >= self.count(): return # first truncate the files on disk end = self.start(rev) - if not self.inlinedata(): + if not self._inline: df = self.opener(self.datafile, "a") df.truncate(end) - end = rev * struct.calcsize(self.indexformat) + end = rev * self._io.size else: - end += rev * struct.calcsize(self.indexformat) + end += rev * self._io.size indexf = self.opener(self.indexfile, "a") indexf.truncate(end) # then reset internal state in memory to forget those revisions - self.cache = None - self.chunkcache = None + self._cache = None + self._chunkcache = None for x in xrange(rev, self.count()): del self.nodemap[self.node(x)] - del self.index[rev:] + del self.index[rev:-1] def checksize(self): expected = 0 if self.count(): - expected = self.end(self.count() - 1) + expected = max(0, self.end(self.count() - 1)) try: f = self.opener(self.datafile) @@ -1282,13 +1253,13 @@ class revlog(object): f = self.opener(self.indexfile) f.seek(0, 2) actual = f.tell() - s = struct.calcsize(self.indexformat) - i = actual / s + s = self._io.size + i = max(0, actual / s) di = actual - (i * s) - if self.inlinedata(): + if self._inline: databytes = 0 for r in xrange(self.count()): - databytes += self.length(r) + databytes += max(0, self.length(r)) dd = 0 di = actual - self.count() * s - databytes except IOError, inst: @@ -1297,5 +1268,3 @@ class revlog(object): di = 0 return (dd, di) - - diff --git a/mercurial/sshrepo.py b/mercurial/sshrepo.py --- a/mercurial/sshrepo.py +++ b/mercurial/sshrepo.py @@ -8,7 +8,7 @@ from node import * from remoterepo import * from i18n import _ -import hg, os, re, stat, util +import repo, os, re, stat, util class sshrepository(remoterepository): def __init__(self, ui, path, create=0): @@ -17,7 +17,7 @@ class sshrepository(remoterepository): m = re.match(r'^ssh://(([^@]+)@)?([^:/]+)(:(\d+))?(/(.*))?$', path) if not m: - self.raise_(hg.RepoError(_("couldn't parse location %s") % path)) + self.raise_(repo.RepoError(_("couldn't parse location %s") % path)) self.user = m.group(2) self.host = m.group(3) @@ -35,9 +35,9 @@ class sshrepository(remoterepository): cmd = cmd % (sshcmd, args, remotecmd, self.path) ui.note('running %s\n' % cmd) - res = os.system(cmd) + res = util.system(cmd) if res != 0: - self.raise_(hg.RepoError(_("could not create remote repo"))) + self.raise_(repo.RepoError(_("could not create remote repo"))) self.validate_repo(ui, sshcmd, args, remotecmd) @@ -51,6 +51,7 @@ class sshrepository(remoterepository): cmd = '%s %s "%s -R %s serve --stdio"' cmd = cmd % (sshcmd, args, remotecmd, self.path) + cmd = util.quotecommand(cmd) ui.note('running %s\n' % cmd) self.pipeo, self.pipei, self.pipee = os.popen3(cmd, 'b') @@ -69,13 +70,13 @@ class sshrepository(remoterepository): lines.append(l) max_noise -= 1 else: - self.raise_(hg.RepoError(_("no suitable response from remote hg"))) + self.raise_(repo.RepoError(_("no suitable response from remote hg"))) - self.capabilities = () + self.capabilities = util.set() lines.reverse() for l in lines: if l.startswith("capabilities:"): - self.capabilities = l[:-1].split(":")[1].split() + self.capabilities.update(l[:-1].split(":")[1].split()) break def readerr(self): @@ -131,12 +132,13 @@ class sshrepository(remoterepository): self.call("unlock") def lookup(self, key): + self.requirecap('lookup', _('look up remote revision')) d = self.call("lookup", key=key) success, data = d[:-1].split(" ", 1) if int(success): return bin(data) else: - self.raise_(hg.RepoError(data)) + self.raise_(repo.RepoError(data)) def heads(self): d = self.call("heads") @@ -168,6 +170,7 @@ class sshrepository(remoterepository): return self.do_cmd("changegroup", roots=n) def changegroupsubset(self, bases, heads, kind): + self.requirecap('changegroupsubset', _('look up remote changes')) bases = " ".join(map(hex, bases)) heads = " ".join(map(hex, heads)) return self.do_cmd("changegroupsubset", bases=bases, heads=heads) @@ -176,7 +179,7 @@ class sshrepository(remoterepository): d = self.call("unbundle", heads=' '.join(map(hex, heads))) if d: # remote may send "unsynced changes" - self.raise_(hg.RepoError(_("push refused: %s") % d)) + self.raise_(repo.RepoError(_("push refused: %s") % d)) while 1: d = cg.read(4096) @@ -203,7 +206,7 @@ class sshrepository(remoterepository): def addchangegroup(self, cg, source, url): d = self.call("addchangegroup") if d: - self.raise_(hg.RepoError(_("push refused: %s") % d)) + self.raise_(repo.RepoError(_("push refused: %s") % d)) while 1: d = cg.read(4096) if not d: break diff --git a/mercurial/statichttprepo.py b/mercurial/statichttprepo.py --- a/mercurial/statichttprepo.py +++ b/mercurial/statichttprepo.py @@ -33,7 +33,7 @@ class statichttprepository(localrepo.loc self._url = path self.ui = ui - self.path = (path + "/.hg") + self.path = path.rstrip('/') + "/.hg" self.opener = opener(self.path) # find requirements try: @@ -75,10 +75,4 @@ class statichttprepository(localrepo.loc 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) + return statichttprepository(ui, path[7:]) diff --git a/mercurial/streamclone.py b/mercurial/streamclone.py --- a/mercurial/streamclone.py +++ b/mercurial/streamclone.py @@ -66,22 +66,25 @@ def stream_out(repo, fileobj, untrusted= # get consistent snapshot of repo. lock during scan so lock not # needed while we stream, and commits can happen. + lock = None try: - repolock = repo.lock() - except (lock.LockHeld, lock.LockUnavailable), inst: - repo.ui.warn('locking the repository failed: %s\n' % (inst,)) - fileobj.write('2\n') - return + try: + repolock = repo.lock() + except (lock.LockHeld, lock.LockUnavailable), inst: + repo.ui.warn('locking the repository failed: %s\n' % (inst,)) + fileobj.write('2\n') + return - fileobj.write('0\n') - repo.ui.debug('scanning\n') - entries = [] - total_bytes = 0 - for name, size in walkrepo(repo.spath): - name = repo.decodefn(util.pconvert(name)) - entries.append((name, size)) - total_bytes += size - repolock.release() + fileobj.write('0\n') + repo.ui.debug('scanning\n') + entries = [] + total_bytes = 0 + for name, size in walkrepo(repo.spath): + name = repo.decodefn(util.pconvert(name)) + entries.append((name, size)) + total_bytes += size + finally: + del repolock repo.ui.debug('%d files, %d bytes to transfer\n' % (len(entries), total_bytes)) diff --git a/mercurial/templater.py b/mercurial/templater.py --- a/mercurial/templater.py +++ b/mercurial/templater.py @@ -270,6 +270,7 @@ common_filters = { "permissions": permissions, "person": person, "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"), + "rfc3339date": lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S", True, "%+03d:%02d"), "short": lambda x: x[:12], "shortdate": shortdate, "stringify": stringify, diff --git a/mercurial/ui.py b/mercurial/ui.py --- a/mercurial/ui.py +++ b/mercurial/ui.py @@ -24,6 +24,8 @@ def updateconfig(source, dest, sections= dest.set(section, name, value) class ui(object): + _isatty = None + def __init__(self, verbose=False, debug=False, quiet=False, interactive=True, traceback=False, report_untrusted=True, parentui=None): @@ -62,6 +64,11 @@ class ui(object): def __getattr__(self, key): return getattr(self.parentui, key) + def isatty(self): + if ui._isatty is None: + ui._isatty = sys.stdin.isatty() + return ui._isatty + def updateopts(self, verbose=False, debug=False, quiet=False, interactive=True, traceback=False, config=[]): for section, name, value in config: @@ -204,7 +211,11 @@ class ui(object): if name is None or name in ('quiet', 'verbose', 'debug'): self.verbosity_constraints() if name is None or name == 'interactive': - self.interactive = self.configbool("ui", "interactive", True) + interactive = self.configbool("ui", "interactive", None) + if interactive is None and self.interactive: + self.interactive = self.isatty() + else: + self.interactive = interactive if name is None or name == 'report_untrusted': self.report_untrusted = ( self.configbool("ui", "report_untrusted", True)) @@ -382,17 +393,29 @@ class ui(object): try: sys.stderr.flush() except: pass - def readline(self): - return sys.stdin.readline()[:-1] - def prompt(self, msg, pat=None, default="y"): + def _readline(self, prompt=''): + if self.isatty(): + try: + # magically add command line editing support, where + # available + import readline + # force demandimport to really load the module + readline.read_history_file + except ImportError: + pass + return raw_input(prompt) + + def prompt(self, msg, pat=None, default="y", matchflags=0): if not self.interactive: return default - while 1: - self.write(msg, " ") - r = self.readline() - if not pat or re.match(pat, r): + try: + r = self._readline(msg + ' ') + if not pat or re.match(pat, r, matchflags): return r else: self.write(_("unrecognized response\n")) + except EOFError: + raise util.Abort(_('response expected')) + def getpass(self, prompt=None, default=None): if not self.interactive: return default return getpass.getpass(prompt or _('password: ')) diff --git a/mercurial/util.py b/mercurial/util.py --- a/mercurial/util.py +++ b/mercurial/util.py @@ -13,8 +13,8 @@ platform-specific details from the core. """ from i18n import _ -import cStringIO, errno, getpass, popen2, re, shutil, sys, tempfile -import os, threading, time, calendar, ConfigParser, locale, glob +import cStringIO, errno, getpass, popen2, re, shutil, sys, tempfile, strutil +import os, stat, threading, time, calendar, ConfigParser, locale, glob try: set = set @@ -63,7 +63,7 @@ def fromlocal(s): Convert a string from the local character encoding to UTF-8 We attempt to decode strings using the encoding mode set by - HG_ENCODINGMODE, which defaults to 'strict'. In this mode, unknown + HGENCODINGMODE, which defaults to 'strict'. In this mode, unknown characters will cause an error message. Other modes include 'replace', which replaces unknown characters with a special Unicode character, and 'ignore', which drops the character. @@ -366,6 +366,7 @@ def canonpath(root, cwd, myname): if not os.path.isabs(name): name = os.path.join(root, cwd, name) name = os.path.normpath(name) + audit_path = path_auditor(root) if name != rootsep and name.startswith(rootsep): name = name[len(rootsep):] audit_path(name) @@ -628,7 +629,7 @@ def rename(src, dst): """forcibly rename a file""" try: os.rename(src, dst) - except OSError, err: + except OSError, err: # FIXME: check err (EEXIST ?) # on windows, rename to existing file is not allowed, so we # must delete destination first. but if file is open, unlink # schedules it for delete but does not delete it. rename @@ -689,12 +690,59 @@ def copyfiles(src, dst, hardlink=None): else: shutil.copy(src, dst) -def audit_path(path): - """Abort if path contains dangerous components""" - parts = os.path.normcase(path).split(os.sep) - if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '') - or os.pardir in parts): - raise Abort(_("path contains illegal component: %s") % path) +class path_auditor(object): + '''ensure that a filesystem path contains no banned components. + the following properties of a path are checked: + + - under top-level .hg + - starts at the root of a windows drive + - contains ".." + - traverses a symlink (e.g. a/symlink_here/b) + - inside a nested repository''' + + def __init__(self, root): + self.audited = set() + self.auditeddir = set() + self.root = root + + def __call__(self, path): + if path in self.audited: + return + normpath = os.path.normcase(path) + parts = normpath.split(os.sep) + if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '') + or os.pardir in parts): + raise Abort(_("path contains illegal component: %s") % path) + def check(prefix): + curpath = os.path.join(self.root, prefix) + try: + st = os.lstat(curpath) + except OSError, err: + # EINVAL can be raised as invalid path syntax under win32. + # They must be ignored for patterns can be checked too. + if err.errno not in (errno.ENOENT, errno.EINVAL): + raise + else: + if stat.S_ISLNK(st.st_mode): + raise Abort(_('path %r traverses symbolic link %r') % + (path, prefix)) + elif (stat.S_ISDIR(st.st_mode) and + os.path.isdir(os.path.join(curpath, '.hg'))): + raise Abort(_('path %r is inside repo %r') % + (path, prefix)) + + prefixes = [] + for c in strutil.rfindall(normpath, os.sep): + prefix = normpath[:c] + if prefix in self.auditeddir: + break + check(prefix) + prefixes.append(prefix) + + self.audited.add(path) + # only add prefixes to the cache after checking everything: we don't + # want to add "foo/bar/baz" before checking if there's a "foo/.hg" + self.auditeddir.update(prefixes) def _makelock_file(info, pathname): ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL) @@ -953,6 +1001,12 @@ if os.name == 'nt': _quotere = re.compile(r'(\\*)("|\\$)') return '"%s"' % _quotere.sub(r'\1\1\\\2', s) + def quotecommand(cmd): + """Build a command string suitable for os.popen* calls.""" + # The extra quotes are needed because popen* runs the command + # through the current COMSPEC. cmd.exe suppress enclosing quotes. + return '"' + cmd + '"' + def explain_exit(code): return _("exited with status %d") % code, code @@ -1106,6 +1160,9 @@ else: else: return "'%s'" % s.replace("'", "'\\''") + def quotecommand(cmd): + return cmd + def testpid(pid): '''return False if pid dead, True if running or not sure''' if os.sys.platform == 'OpenVMS': @@ -1275,7 +1332,10 @@ class opener(object): """ def __init__(self, base, audit=True): self.base = base - self.audit = audit + if audit: + self.audit_path = path_auditor(base) + else: + self.audit_path = always def __getattr__(self, name): if name == '_can_symlink': @@ -1284,8 +1344,7 @@ class opener(object): raise AttributeError(name) def __call__(self, path, mode="r", text=False, atomictemp=False): - if self.audit: - audit_path(path) + self.audit_path(path) f = os.path.join(self.base, path) if not text and "b" not in mode: @@ -1306,8 +1365,7 @@ class opener(object): return posixfile(f, mode) def symlink(self, src, dst): - if self.audit: - audit_path(dst) + self.audit_path(dst) linkname = os.path.join(self.base, dst) try: os.unlink(linkname) @@ -1319,7 +1377,11 @@ class opener(object): os.makedirs(dirname) if self._can_symlink: - os.symlink(src, linkname) + try: + os.symlink(src, linkname) + except OSError, err: + raise OSError(err.errno, _('could not symlink to %r: %s') % + (src, err.strerror), linkname) else: f = self(dst, "w") f.write(src) @@ -1395,7 +1457,7 @@ def makedate(): tz = time.timezone return time.mktime(lt), tz -def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True): +def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True, timezone_format=" %+03d%02d"): """represent a (unixtime, offset) tuple as a localized time. unixtime is seconds since the epoch, and offset is the time zone's number of seconds away from UTC. if timezone is false, do not @@ -1403,7 +1465,7 @@ def datestr(date=None, format='%a %b %d t, tz = date or makedate() s = time.strftime(format, time.gmtime(float(t) - tz)) if timezone: - s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60)) + s += timezone_format % (-tz / 3600, ((-tz % 3600) / 60)) return s def strdate(string, format, defaults): @@ -1628,3 +1690,7 @@ def drop_scheme(scheme, path): if path.startswith('//'): path = path[2:] return path + +def uirepr(s): + # Avoid double backslash in Windows path repr() + return repr(s).replace('\\\\', '\\') diff --git a/mercurial/verify.py b/mercurial/verify.py --- a/mercurial/verify.py +++ b/mercurial/verify.py @@ -7,20 +7,36 @@ from node import * from i18n import _ -import revlog, mdiff +import revlog def verify(repo): + lock = repo.lock() + try: + return _verify(repo) + finally: + del lock + +def _verify(repo): filelinkrevs = {} filenodes = {} changesets = revisions = files = 0 + firstbad = [None] errors = [0] warnings = [0] neededmanifests = {} - lock = repo.lock() - - def err(msg): - repo.ui.warn(msg + "\n") + def err(linkrev, msg, filename=None): + if linkrev != None: + if firstbad[0] != None: + firstbad[0] = min(firstbad[0], linkrev) + else: + firstbad[0] = linkrev + else: + linkrev = "?" + msg = "%s: %s" % (linkrev, msg) + if filename: + msg = "%s@%s" % (filename, msg) + repo.ui.warn(" " + msg + "\n") errors[0] += 1 def warn(msg): @@ -30,9 +46,9 @@ def verify(repo): def checksize(obj, name): d = obj.checksize() if d[0]: - err(_("%s data length off by %d bytes") % (name, d[0])) + err(None, _("data length off by %d bytes") % d[0], name) if d[1]: - err(_("%s index contains %d extra bytes") % (name, d[1])) + err(None, _("index contains %d extra bytes") % d[1], name) def checkversion(obj, name): if obj.version != revlog.REVLOGV0: @@ -55,25 +71,25 @@ def verify(repo): n = repo.changelog.node(i) l = repo.changelog.linkrev(n) if l != i: - err(_("incorrect link (%d) for changeset revision %d") %(l, i)) + err(i, _("incorrect link (%d) for changeset") %(l)) if n in seen: - err(_("duplicate changeset at revision %d") % i) - seen[n] = 1 + err(i, _("duplicates changeset at revision %d") % seen[n]) + seen[n] = i 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))) + err(i, _("changeset has unknown parent %s") % 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)) + err(i, _("unpacking changeset: %s") % inst) continue - neededmanifests[changes[0]] = n + if changes[0] not in neededmanifests: + neededmanifests[changes[0]] = i for f in changes[3]: filelinkrevs.setdefault(f, []).append(i) @@ -88,45 +104,50 @@ def verify(repo): l = repo.manifest.linkrev(n) if l < 0 or l >= repo.changelog.count(): - err(_("bad manifest link (%d) at revision %d") % (l, i)) + err(None, _("bad link (%d) at manifest revision %d") % (l, i)) if n in neededmanifests: del neededmanifests[n] if n in seen: - err(_("duplicate manifest at revision %d") % i) + err(l, _("duplicates manifest from %d") % seen[n]) - seen[n] = 1 + seen[n] = l 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))) + err(l, _("manifest has unknown parent %s") % short(p)) try: for f, fn in repo.manifest.readdelta(n).iteritems(): - filenodes.setdefault(f, {})[fn] = 1 + fns = filenodes.setdefault(f, {}) + if fn not in fns: + fns[fn] = n except KeyboardInterrupt: repo.ui.warn(_("interrupted")) raise except Exception, inst: - err(_("reading delta for manifest %s: %s") % (short(n), inst)) + err(l, _("reading manifest delta: %s") % inst) continue 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 + nm = neededmanifests.items() + nm.sort() + for m, c in nm: + err(m, _("changeset refers to unknown manifest %s") % short(c)) + del neededmanifests, nm for f in filenodes: if f not in filelinkrevs: - err(_("file %s in manifest but not in changesets") % f) + lrs = [repo.manifest.linkrev(n) for n in filenodes[f]] + lrs.sort() + err(lrs[0], _("in manifest but not in changeset"), f) for f in filelinkrevs: if f not in filenodes: - err(_("file %s in changeset but not in manifest") % f) + lr = filelinkrevs[f][0] + err(lr, _("in changeset but not in manifest"), f) repo.ui.status(_("checking files\n")) ff = filenodes.keys() @@ -136,32 +157,40 @@ def verify(repo): continue files += 1 if not f: - err(_("file without name in manifest %s") % short(n)) + lr = repo.manifest.linkrev(filenodes[f][0]) + err(lr, _("file without name in manifest %s") % short(ff[n])) continue fl = repo.file(f) checkversion(fl, f) checksize(fl, f) + seen = {} nodes = {nullid: 1} - seen = {} for i in xrange(fl.count()): revisions += 1 n = fl.node(i) + flr = fl.linkrev(n) + + if flr not in filelinkrevs.get(f, []): + if flr < 0 or flr >= repo.changelog.count(): + err(None, _("rev %d point to nonexistent changeset %d") + % (i, flr), f) + else: + err(None, _("rev %d points to unexpected changeset %d") + % (i, flr), f) + if f in filelinkrevs: + warn(_(" (expected %s)") % filelinkrevs[f][0]) + flr = None # can't be trusted + else: + filelinkrevs[f].remove(flr) if n in seen: - err(_("%s: duplicate revision %d") % (f, i)) + err(flr, _("duplicate revision %d") % i, f) if n not in filenodes[f]: - err(_("%s: %d:%s not in manifests") % (f, i, short(n))) + err(flr, _("%s not in manifests") % (short(n)), f) 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) @@ -169,16 +198,22 @@ def verify(repo): repo.ui.warn(_("interrupted")) raise except Exception, inst: - err(_("unpacking file %s %s: %s") % (f, short(n), inst)) + err(flr, _("unpacking %s: %s") % (short(n), inst), f) # 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))) + try: + (p1, p2) = fl.parents(n) + if p1 not in nodes: + err(flr, _("unknown parent 1 %s of %s") % + (short(p1), short(n)), f) + if p2 not in nodes: + err(flr, _("unknown parent 2 %s of %s") % + (short(p2), short(p1)), f) + except KeyboardInterrupt: + repo.ui.warn(_("interrupted")) + raise + except Exception, inst: + err(flr, _("checking parents of %s: %s") % (short(n), inst), f) nodes[n] = 1 # check renames @@ -191,11 +226,15 @@ def verify(repo): repo.ui.warn(_("interrupted")) raise except Exception, inst: - err(_("checking rename on file %s %s: %s") % (f, short(n), inst)) + err(flr, _("checking rename of %s: %s") % + (short(n), inst), f) # cross-check - for node in filenodes[f]: - err(_("node %s in manifests not in %s") % (hex(node), f)) + fns = [(repo.manifest.linkrev(filenodes[f][n]), n) + for n in filenodes[f]] + fns.sort() + for lr, node in fns: + err(lr, _("%s in manifests not found") % short(node), f) repo.ui.status(_("%d files, %d changesets, %d total revisions\n") % (files, changesets, revisions)) @@ -204,5 +243,8 @@ def verify(repo): repo.ui.warn(_("%d warnings encountered!\n") % warnings[0]) if errors[0]: repo.ui.warn(_("%d integrity errors encountered!\n") % errors[0]) + if firstbad[0]: + repo.ui.warn(_("(first damaged changeset appears to be %d)\n") + % firstbad[0]) return 1 diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -2,8 +2,8 @@ # # This is the mercurial setup script. # -# './setup.py install', or -# './setup.py --help' for more options +# 'python setup.py install', or +# 'python setup.py --help' for more options import sys if not hasattr(sys, 'version_info') or sys.version_info < (2, 3, 0, 'final'): @@ -14,8 +14,6 @@ from distutils.core import setup, Extens from distutils.command.install_data import install_data import mercurial.version -import mercurial.demandimport -mercurial.demandimport.enable = lambda: None extra = {} @@ -64,7 +62,8 @@ setup(name='mercurial', packages=['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert'], ext_modules=[Extension('mercurial.mpatch', ['mercurial/mpatch.c']), Extension('mercurial.bdiff', ['mercurial/bdiff.c']), - Extension('mercurial.base85', ['mercurial/base85.c'])], + Extension('mercurial.base85', ['mercurial/base85.c']), + Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c'])], data_files=[(os.path.join('mercurial', root), [os.path.join(root, file_) for file_ in files]) for root, dirs, files in os.walk('templates')], diff --git a/templates/atom/changelog.tmpl b/templates/atom/changelog.tmpl new file mode 100644 --- /dev/null +++ b/templates/atom/changelog.tmpl @@ -0,0 +1,10 @@ +#header# + + {urlbase}{url} + + + #repo|escape# Changelog + #latestentry%feedupdated# + +#entries%changelogentry# + diff --git a/templates/atom/changelogentry.tmpl b/templates/atom/changelogentry.tmpl new file mode 100644 --- /dev/null +++ b/templates/atom/changelogentry.tmpl @@ -0,0 +1,16 @@ + + #desc|strip|firstline|strip|escape# + http://www.selenic.com/mercurial/#changeset-{node} + + + #author|person|escape# + #author|email|obfuscate# + + #date|rfc3339date# + #date|rfc3339date# + +
+
#desc|escape#
+
+
+
diff --git a/templates/atom/filelog.tmpl b/templates/atom/filelog.tmpl new file mode 100644 --- /dev/null +++ b/templates/atom/filelog.tmpl @@ -0,0 +1,8 @@ +#header# + {urlbase}{url}atom-log/tip/{file|escape} + + #repo|escape#: #file|escape# history + #latestentry%feedupdated# + +#entries%changelogentry# + diff --git a/templates/atom/header.tmpl b/templates/atom/header.tmpl new file mode 100644 --- /dev/null +++ b/templates/atom/header.tmpl @@ -0,0 +1,4 @@ +Content-type: application/atom+xml; charset={encoding} + + + \ No newline at end of file diff --git a/templates/atom/map b/templates/atom/map new file mode 100644 --- /dev/null +++ b/templates/atom/map @@ -0,0 +1,9 @@ +default = 'changelog' +feedupdated = '#date|rfc3339date#' +header = header.tmpl +changelog = changelog.tmpl +changelogentry = changelogentry.tmpl +filelog = filelog.tmpl +filelogentry = filelogentry.tmpl +tags = tags.tmpl +tagentry = tagentry.tmpl diff --git a/templates/atom/tagentry.tmpl b/templates/atom/tagentry.tmpl new file mode 100644 --- /dev/null +++ b/templates/atom/tagentry.tmpl @@ -0,0 +1,8 @@ + + #tag|escape# + + http://www.selenic.com/mercurial/#tag-{node} + #date|rfc3339date# + #date|rfc3339date# + #tag|strip|escape# + diff --git a/templates/atom/tags.tmpl b/templates/atom/tags.tmpl new file mode 100644 --- /dev/null +++ b/templates/atom/tags.tmpl @@ -0,0 +1,11 @@ +#header# + {urlbase}{url} + + + #repo|escape#: tags + #repo|escape# tag history + Mercurial SCM + #latestentry%feedupdated# + +#entriesnotip%tagentry# + diff --git a/templates/changelog.tmpl b/templates/changelog.tmpl --- a/templates/changelog.tmpl +++ b/templates/changelog.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: changelog + @@ -11,6 +13,7 @@ manifest #archives%archiveentry# rss +atom

changelog for #repo|escape#

diff --git a/templates/filelog.tmpl b/templates/filelog.tmpl --- a/templates/filelog.tmpl +++ b/templates/filelog.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: #file|escape# history + @@ -13,6 +15,7 @@ file annotate rss +atom

#file|escape# revision history

diff --git a/templates/gitweb/changelog.tmpl b/templates/gitweb/changelog.tmpl --- a/templates/gitweb/changelog.tmpl +++ b/templates/gitweb/changelog.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: Changelog + diff --git a/templates/gitweb/changeset.tmpl b/templates/gitweb/changeset.tmpl --- a/templates/gitweb/changeset.tmpl +++ b/templates/gitweb/changeset.tmpl @@ -1,5 +1,7 @@ #header# {repo|escape}: changeset {rev}:{node|short} + diff --git a/templates/gitweb/error.tmpl b/templates/gitweb/error.tmpl --- a/templates/gitweb/error.tmpl +++ b/templates/gitweb/error.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: Error + diff --git a/templates/gitweb/fileannotate.tmpl b/templates/gitweb/fileannotate.tmpl --- a/templates/gitweb/fileannotate.tmpl +++ b/templates/gitweb/fileannotate.tmpl @@ -1,5 +1,7 @@ #header# {repo|escape}: {file|escape}@{node|short} (annotated) + diff --git a/templates/gitweb/filediff.tmpl b/templates/gitweb/filediff.tmpl --- a/templates/gitweb/filediff.tmpl +++ b/templates/gitweb/filediff.tmpl @@ -1,5 +1,7 @@ {header} {repo|escape}: diff {file|escape} + diff --git a/templates/gitweb/filelog.tmpl b/templates/gitweb/filelog.tmpl --- a/templates/gitweb/filelog.tmpl +++ b/templates/gitweb/filelog.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: File revisions + diff --git a/templates/gitweb/filerevision.tmpl b/templates/gitweb/filerevision.tmpl --- a/templates/gitweb/filerevision.tmpl +++ b/templates/gitweb/filerevision.tmpl @@ -1,5 +1,7 @@ #header# {repo|escape}: {file|escape}@{node|short} + diff --git a/templates/gitweb/footer.tmpl b/templates/gitweb/footer.tmpl --- a/templates/gitweb/footer.tmpl +++ b/templates/gitweb/footer.tmpl @@ -1,6 +1,7 @@ diff --git a/templates/gitweb/index.tmpl b/templates/gitweb/index.tmpl --- a/templates/gitweb/index.tmpl +++ b/templates/gitweb/index.tmpl @@ -14,6 +14,7 @@ Contact Last change   +   #entries%indexentry# diff --git a/templates/gitweb/manifest.tmpl b/templates/gitweb/manifest.tmpl --- a/templates/gitweb/manifest.tmpl +++ b/templates/gitweb/manifest.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: Manifest + @@ -23,6 +25,7 @@ manifest | drwxr-xr-x + [up]   diff --git a/templates/gitweb/map b/templates/gitweb/map --- a/templates/gitweb/map +++ b/templates/gitweb/map @@ -16,8 +16,8 @@ changelogentry = changelogentry.tmpl searchentry = changelogentry.tmpl changeset = changeset.tmpl manifest = manifest.tmpl -manifestdirentry = 'drwxr-xr-x#basename|escape#manifest' -manifestfileentry = '#permissions|permissions##size##basename|escape#file | revisions | annotate' +manifestdirentry = 'drwxr-xr-x#basename|escape#manifest' +manifestfileentry = '#permissions|permissions##date|isodate##size##basename|escape#file | revisions | annotate' filerevision = filerevision.tmpl fileannotate = fileannotate.tmpl filediff = filediff.tmpl @@ -52,7 +52,7 @@ branchtag = '#repo|escape#: Search + diff --git a/templates/gitweb/shortlog.tmpl b/templates/gitweb/shortlog.tmpl --- a/templates/gitweb/shortlog.tmpl +++ b/templates/gitweb/shortlog.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: Shortlog + diff --git a/templates/gitweb/summary.tmpl b/templates/gitweb/summary.tmpl --- a/templates/gitweb/summary.tmpl +++ b/templates/gitweb/summary.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: Summary + diff --git a/templates/gitweb/tags.tmpl b/templates/gitweb/tags.tmpl --- a/templates/gitweb/tags.tmpl +++ b/templates/gitweb/tags.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: Tags + diff --git a/templates/manifest.tmpl b/templates/manifest.tmpl --- a/templates/manifest.tmpl +++ b/templates/manifest.tmpl @@ -17,6 +17,7 @@ drwxr-xr-x    +   [up] #dentries%manifestdirentry# diff --git a/templates/map b/templates/map --- a/templates/map +++ b/templates/map @@ -15,8 +15,8 @@ changelogentry = changelogentry.tmpl searchentry = changelogentry.tmpl changeset = changeset.tmpl manifest = manifest.tmpl -manifestdirentry = 'drwxr-xr-x  #basename|escape#/' -manifestfileentry = '#permissions|permissions# #size# #basename|escape#' +manifestdirentry = 'drwxr-xr-x   #basename|escape#/' +manifestfileentry = '#permissions|permissions# #date|isodate# #size# #basename|escape#' filerevision = filerevision.tmpl fileannotate = fileannotate.tmpl filediff = filediff.tmpl @@ -47,7 +47,7 @@ filediffparent = '#repo|escape#: changelog + @@ -11,6 +13,7 @@ manifest #archives%archiveentry# rss +atom

changelog for #repo|escape#

diff --git a/templates/old/filelog.tmpl b/templates/old/filelog.tmpl --- a/templates/old/filelog.tmpl +++ b/templates/old/filelog.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: #file|escape# history + @@ -13,6 +15,7 @@ file annotate rss +atom

#file|escape# revision history

diff --git a/templates/old/map b/templates/old/map --- a/templates/old/map +++ b/templates/old/map @@ -46,7 +46,7 @@ filediffparent = '#repo|escape#: shortlog + @@ -11,6 +13,7 @@ manifest #archives%archiveentry# rss +atom

shortlog for #repo|escape#

diff --git a/templates/old/tags.tmpl b/templates/old/tags.tmpl --- a/templates/old/tags.tmpl +++ b/templates/old/tags.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: tags + @@ -10,6 +12,7 @@ shortlog manifest rss +atom

tags:

diff --git a/templates/shortlog.tmpl b/templates/shortlog.tmpl --- a/templates/shortlog.tmpl +++ b/templates/shortlog.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: shortlog + @@ -11,6 +13,7 @@ manifest #archives%archiveentry# rss +atom

shortlog for #repo|escape#

diff --git a/templates/static/style.css b/templates/static/style.css --- a/templates/static/style.css +++ b/templates/static/style.css @@ -1,5 +1,6 @@ a { text-decoration:none; } .age { white-space:nowrap; } +.date { white-space:nowrap; } .indexlinks { white-space:nowrap; } .parity0 { background-color: #dddddd; } .parity1 { background-color: #eeeeee; } diff --git a/templates/tags.tmpl b/templates/tags.tmpl --- a/templates/tags.tmpl +++ b/templates/tags.tmpl @@ -1,5 +1,7 @@ #header# #repo|escape#: tags + @@ -10,6 +12,7 @@ shortlog manifest rss +atom

tags:

diff --git a/tests/coverage.py b/tests/coverage.py --- a/tests/coverage.py +++ b/tests/coverage.py @@ -504,7 +504,7 @@ class coverage: def get_suite_spots(self, tree, spots): import symbol, token for i in range(1, len(tree)): - if type(tree[i]) == type(()): + if isinstance(tree[i], tuple): if tree[i][0] == symbol.suite: # Found a suite, look back for the colon and keyword. lineno_colon = lineno_word = None diff --git a/tests/hghave b/tests/hghave --- a/tests/hghave +++ b/tests/hghave @@ -5,17 +5,34 @@ prefixed with "no-", the absence of feat """ import optparse import os +import re import sys import tempfile tempprefix = 'hg-hghave-' +def matchoutput(cmd, regexp, ignorestatus=False): + """Return True if cmd executes successfully and its output + is matched by the supplied regular expression. + """ + r = re.compile(regexp) + fh = os.popen(cmd) + s = fh.read() + ret = fh.close() + return (ignorestatus or ret is None) and r.search(s) + def has_symlink(): return hasattr(os, "symlink") def has_fifo(): return hasattr(os, "mkfifo") +def has_cvs(): + return matchoutput('cvs --version 2>&1', r'Concurrent Versions System') + +def has_cvsps(): + return matchoutput('cvsps -h -q 2>&1', r'cvsps version', True) + def has_executablebit(): fd, path = tempfile.mkstemp(prefix=tempprefix) os.close(fd) @@ -52,18 +69,30 @@ def has_lsprof(): return False def has_git(): - fh = os.popen('git --version 2>&1') - s = fh.read() - ret = fh.close() - return ret is None and s.startswith('git version') + return matchoutput('git --version 2>&1', r'^git version') + +def has_svn(): + return matchoutput('svn --version 2>&1', r'^svn, version') and \ + matchoutput('svnadmin --version 2>&1', r'^svnadmin, version') + +def has_svn_bindings(): + try: + import svn.core + return True + except ImportError: + return False checks = { + "cvs": (has_cvs, "cvs client"), + "cvsps": (has_cvsps, "cvsps utility"), "eol-in-paths": (has_eol_in_paths, "end-of-lines in paths"), "execbit": (has_executablebit, "executable bit"), "git": (has_git, "git command line client"), "fifo": (has_fifo, "named pipes"), "hotshot": (has_hotshot, "python hotshot module"), "lsprof": (has_lsprof, "python lsprof module"), + "svn": (has_svn, "subversion client and admin tools"), + "svn-bindings": (has_svn_bindings, "subversion python bindings"), "symlink": (has_symlink, "symbolic links"), } diff --git a/tests/run-tests.py b/tests/run-tests.py --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -134,7 +134,8 @@ def install_hg(): vlog("# Performing temporary installation of HG") installerrs = os.path.join("tests", "install.err") - os.chdir("..") # Get back to hg root + # Run installer in hg root + os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '..')) cmd = ('%s setup.py clean --all' ' install --force --home="%s" --install-lib="%s"' ' --install-scripts="%s" >%s 2>&1' @@ -152,7 +153,14 @@ def install_hg(): os.chdir(TESTDIR) os.environ["PATH"] = "%s%s%s" % (BINDIR, os.pathsep, os.environ["PATH"]) - os.environ["PYTHONPATH"] = PYTHONDIR + + pydir = os.pathsep.join([PYTHONDIR, TESTDIR]) + pythonpath = os.environ.get("PYTHONPATH") + if pythonpath: + pythonpath = pydir + os.pathsep + pythonpath + else: + pythonpath = pydir + os.environ["PYTHONPATH"] = pythonpath use_correct_python() diff --git a/tests/test-abort-checkin.out b/tests/test-abort-checkin.out --- a/tests/test-abort-checkin.out +++ b/tests/test-abort-checkin.out @@ -1,8 +1,8 @@ error: pretxncommit.nocommits hook failed: no commits allowed -abort: no commits allowed transaction abort! rollback completed +abort: no commits allowed error: pretxncommit.nocommits hook failed: no commits allowed -abort: no commits allowed transaction abort! rollback completed +abort: no commits allowed diff --git a/tests/test-acl.out b/tests/test-acl.out --- a/tests/test-acl.out +++ b/tests/test-acl.out @@ -129,9 +129,9 @@ acl: acl.allow enabled, 0 entries for us acl: acl.deny not enabled acl: user fred not allowed on foo/file.txt error: pretxnchangegroup.acl hook failed: acl: access denied for changeset ef1ea85a6374 -abort: acl: access denied for changeset ef1ea85a6374 transaction abort! rollback completed +abort: acl: access denied for changeset ef1ea85a6374 no rollback information available 0:6675d58eff77 @@ -170,9 +170,9 @@ acl: allowing changeset ef1ea85a6374 acl: allowing changeset f9cafe1212c8 acl: user fred not allowed on quux/file.py error: pretxnchangegroup.acl hook failed: acl: access denied for changeset 911600dab2ae -abort: acl: access denied for changeset 911600dab2ae transaction abort! rollback completed +abort: acl: access denied for changeset 911600dab2ae no rollback information available 0:6675d58eff77 @@ -210,9 +210,9 @@ acl: acl.allow enabled, 0 entries for us acl: acl.deny enabled, 0 entries for user barney acl: user barney not allowed on foo/file.txt error: pretxnchangegroup.acl hook failed: acl: access denied for changeset ef1ea85a6374 -abort: acl: access denied for changeset ef1ea85a6374 transaction abort! rollback completed +abort: acl: access denied for changeset ef1ea85a6374 no rollback information available 0:6675d58eff77 @@ -253,9 +253,9 @@ acl: allowing changeset ef1ea85a6374 acl: allowing changeset f9cafe1212c8 acl: user fred not allowed on quux/file.py error: pretxnchangegroup.acl hook failed: acl: access denied for changeset 911600dab2ae -abort: acl: access denied for changeset 911600dab2ae transaction abort! rollback completed +abort: acl: access denied for changeset 911600dab2ae no rollback information available 0:6675d58eff77 @@ -296,9 +296,9 @@ acl: acl.deny enabled, 2 entries for use acl: allowing changeset ef1ea85a6374 acl: user fred denied on foo/Bar/file.txt error: pretxnchangegroup.acl hook failed: acl: access denied for changeset f9cafe1212c8 -abort: acl: access denied for changeset f9cafe1212c8 transaction abort! rollback completed +abort: acl: access denied for changeset f9cafe1212c8 no rollback information available 0:6675d58eff77 @@ -338,9 +338,9 @@ acl: acl.allow enabled, 0 entries for us acl: acl.deny enabled, 0 entries for user barney acl: user barney not allowed on foo/file.txt error: pretxnchangegroup.acl hook failed: acl: access denied for changeset ef1ea85a6374 -abort: acl: access denied for changeset ef1ea85a6374 transaction abort! rollback completed +abort: acl: access denied for changeset ef1ea85a6374 no rollback information available 0:6675d58eff77 @@ -427,9 +427,9 @@ acl: allowing changeset ef1ea85a6374 acl: allowing changeset f9cafe1212c8 acl: user wilma not allowed on quux/file.py error: pretxnchangegroup.acl hook failed: acl: access denied for changeset 911600dab2ae -abort: acl: access denied for changeset 911600dab2ae transaction abort! rollback completed +abort: acl: access denied for changeset 911600dab2ae no rollback information available 0:6675d58eff77 @@ -471,9 +471,9 @@ adding quux/file.py revisions added 3 changesets with 3 changes to 3 files calling hook pretxnchangegroup.acl: hgext.acl.hook error: pretxnchangegroup.acl hook failed: unable to open ../acl.config: No such file or directory -abort: unable to open ../acl.config: No such file or directory transaction abort! rollback completed +abort: unable to open ../acl.config: No such file or directory no rollback information available 0:6675d58eff77 @@ -524,9 +524,9 @@ acl: allowing changeset ef1ea85a6374 acl: allowing changeset f9cafe1212c8 acl: user betty not allowed on quux/file.py error: pretxnchangegroup.acl hook failed: acl: access denied for changeset 911600dab2ae -abort: acl: access denied for changeset 911600dab2ae transaction abort! rollback completed +abort: acl: access denied for changeset 911600dab2ae no rollback information available 0:6675d58eff77 diff --git a/tests/test-add b/tests/test-add new file mode 100755 --- /dev/null +++ b/tests/test-add @@ -0,0 +1,42 @@ +#!/bin/sh + +hg init a +cd a +echo a > a +hg add -n +hg st +hg add +hg st + +echo b > b +hg add -n b +hg st +hg add b +hg st +echo % should fail +hg add b +hg st + +hg ci -m 0 +echo % should fail +hg add a + +echo aa > a +hg ci -m 1 +hg up 0 +echo aaa > a +hg ci -m 2 + +hg merge +hg st +echo % should fail +hg add a +hg st +hg ci -m merge + +echo % issue683 +hg rm a +hg st +echo a > a +hg add a +hg st diff --git a/tests/test-add.out b/tests/test-add.out new file mode 100644 --- /dev/null +++ b/tests/test-add.out @@ -0,0 +1,29 @@ +adding a +? a +adding a +A a +A a +? b +A a +A b +% should fail +b already tracked! +A a +A b +% should fail +a already tracked! +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +warning: conflicts during merge. +merging a +merging a failed! +0 files updated, 0 files merged, 0 files removed, 1 files unresolved +There are unresolved merges, you can redo the full merge using: + hg update -C 2 + hg merge 1 +M a +% should fail +a already tracked! +M a +% issue683 +R a +M a diff --git a/tests/test-alias b/tests/test-alias new file mode 100755 --- /dev/null +++ b/tests/test-alias @@ -0,0 +1,32 @@ +#!/bin/sh + +cat > $HGRCPATH < foo +hg ci -Amfoo + +echo '% with opts' +hg cleanst diff --git a/tests/test-alias.out b/tests/test-alias.out new file mode 100644 --- /dev/null +++ b/tests/test-alias.out @@ -0,0 +1,10 @@ +% basic +% unknown +*** [alias] unknown: command bargle is unknown +% ambiguous +*** [alias] ambiguous: command s is ambiguous +% recursive +*** [alias] recursive: circular dependency on recursive +adding foo +% with opts +C foo diff --git a/tests/test-annotate b/tests/test-annotate --- a/tests/test-annotate +++ b/tests/test-annotate @@ -12,18 +12,27 @@ hg ci -A -m test -u nobody -d '1 0' echo % annotate -c hg annotate -c a +echo % annotate -cl +hg annotate -cl a + echo % annotate -d hg annotate -d a echo % annotate -n hg annotate -n a +echo % annotate -nl +hg annotate -nl a + echo % annotate -u hg annotate -u a echo % annotate -cdnu hg annotate -cdnu a +echo % annotate -cdnul +hg annotate -cdnul a + cat <>a a a @@ -32,28 +41,34 @@ hg ci -ma1 -d '1 0' hg cp a b hg ci -mb -d '1 0' cat <> b -b -b -b +b4 +b5 +b6 EOF hg ci -mb2 -d '2 0' -echo % annotate b -hg annotate b +echo % annotate -n b +hg annotate -n b +echo % annotate -nl b +hg annotate -nl b echo % annotate -nf b hg annotate -nf b +echo % annotate -nlf b +hg annotate -nlf b hg up -C 2 cat <> b -b +b4 c -b +b5 EOF hg ci -mb2.1 -d '2 0' hg merge hg ci -mmergeb -d '3 0' echo % annotate after merge hg annotate -nf b +echo % annotate after merge with -l +hg annotate -nlf b hg up -C 1 hg cp a b @@ -65,17 +80,21 @@ EOF hg ci -mc -d '3 0' hg merge cat <> b -b +b4 c -b +b5 EOF echo d >> b hg ci -mmerge2 -d '4 0' echo % annotate after rename merge hg annotate -nf b +echo % annotate after rename merge with -l +hg annotate -nlf b echo % linkrev vs rev -hg annotate -r tip a +hg annotate -r tip -n a +echo % linkrev vs rev with -l +hg annotate -r tip -nl a # test issue 589 # annotate was crashing when trying to --follow something diff --git a/tests/test-annotate.out b/tests/test-annotate.out --- a/tests/test-annotate.out +++ b/tests/test-annotate.out @@ -3,28 +3,48 @@ adding a % annotate -c 8435f90966e4: a +% annotate -cl +8435f90966e4:1: a % annotate -d Thu Jan 01 00:00:01 1970 +0000: a % annotate -n 0: a +% annotate -nl +0:1: a % annotate -u nobody: a % annotate -cdnu nobody 0 8435f90966e4 Thu Jan 01 00:00:01 1970 +0000: a -% annotate b +% annotate -cdnul +nobody 0 8435f90966e4 Thu Jan 01 00:00:01 1970 +0000:1: a +% annotate -n b 2: a 2: a 2: a -3: b -3: b -3: b +3: b4 +3: b5 +3: b6 +% annotate -nl b +2:1: a +2:2: a +2:3: a +3:4: b4 +3:5: b5 +3:6: b6 % annotate -nf b 0 a: a 1 a: a 1 a: a -3 b: b -3 b: b -3 b: b +3 b: b4 +3 b: b5 +3 b: b6 +% annotate -nlf b +0 a:1: a +1 a:2: a +1 a:3: a +3 b:4: b4 +3 b:5: b5 +3 b:6: b6 1 files updated, 0 files merged, 0 files removed, 0 files unresolved merging b 0 files updated, 1 files merged, 0 files removed, 0 files unresolved @@ -33,9 +53,16 @@ 0 files updated, 1 files merged, 0 files 0 a: a 1 a: a 1 a: a -3 b: b +3 b: b4 4 b: c -3 b: b +3 b: b5 +% annotate after merge with -l +0 a:1: a +1 a:2: a +1 a:3: a +3 b:4: b4 +4 b:5: c +3 b:5: b5 0 files updated, 0 files merged, 1 files removed, 0 files unresolved merging b 0 files updated, 1 files merged, 0 files removed, 0 files unresolved @@ -44,14 +71,26 @@ 0 files updated, 1 files merged, 0 files 0 a: a 6 b: z 1 a: a -3 b: b +3 b: b4 4 b: c -3 b: b +3 b: b5 7 b: d +% annotate after rename merge with -l +0 a:1: a +6 b:2: z +1 a:3: a +3 b:4: b4 +4 b:5: c +3 b:5: b5 +7 b:7: d % linkrev vs rev 0: a 1: a 1: a +% linkrev vs rev with -l +0:1: a +1:2: a +1:3: a % generate ABA rename configuration % annotate after ABA with follow foo: foo diff --git a/tests/test-archive b/tests/test-archive --- a/tests/test-archive +++ b/tests/test-archive @@ -63,6 +63,7 @@ hg archive -t zip -r 2 test.zip unzip -t test.zip hg archive -t tar - | tar tf - | sed "s/$QTIP/TIP/" + hg archive -r 0 -t tar rev-%r.tar if [ -f rev-0.tar ]; then echo 'rev-0.tar created' diff --git a/tests/test-audit-path b/tests/test-audit-path new file mode 100755 --- /dev/null +++ b/tests/test-audit-path @@ -0,0 +1,23 @@ +#!/bin/sh + +hg init + +echo % should fail +hg add .hg/00changelog.i + +mkdir a +echo a > a/a +hg ci -Ama +ln -s a b +echo b > a/b + +echo % should fail +hg add b/b + +echo % should succeed +hg add b + +echo % should still fail - maybe +hg add b/b + +exit 0 diff --git a/tests/test-audit-path.out b/tests/test-audit-path.out new file mode 100644 --- /dev/null +++ b/tests/test-audit-path.out @@ -0,0 +1,8 @@ +% should fail +abort: path contains illegal component: .hg/00changelog.i +adding a/a +% should fail +abort: path 'b/b' traverses symbolic link 'b' +% should succeed +% should still fail - maybe +abort: path 'b/b' traverses symbolic link 'b' diff --git a/tests/test-bad-extension.out b/tests/test-bad-extension.out --- a/tests/test-bad-extension.out +++ b/tests/test-bad-extension.out @@ -1,5 +1,4 @@ *** failed to import extension badext: bit bucket overflow -extension 'hgext.gpg' overrides commands: sigs sigcheck sign hg help [COMMAND] show help for a command, extension, or list of commands diff --git a/tests/test-bundle-r.out b/tests/test-bundle-r.out --- a/tests/test-bundle-r.out +++ b/tests/test-bundle-r.out @@ -152,9 +152,9 @@ 1 files updated, 0 files merged, 0 files % 2 2:d62976ca1e50 adding changesets -abort: unknown parent ac69c658229d! transaction abort! rollback completed +abort: unknown parent ac69c658229d! % 2 2:d62976ca1e50 adding changesets diff --git a/tests/test-children b/tests/test-children new file mode 100755 --- /dev/null +++ b/tests/test-children @@ -0,0 +1,59 @@ +#!/bin/sh +# test children command + +cat <> $HGRCPATH +[extensions] +hgext.children= +EOF + +echo "% init" +hg init t +cd t + +echo "% no working directory" +hg children + +echo % setup +echo 0 > file0 +hg ci -qAm 0 -d '0 0' + +echo 1 > file1 +hg ci -qAm 1 -d '1 0' + +echo 2 >> file0 +hg ci -qAm 2 -d '2 0' + +hg co null +echo 3 > file3 +hg ci -qAm 3 -d '3 0' + +echo "% hg children at revision 3 (tip)" +hg children + +hg co null +echo "% hg children at nullrev (should be 0 and 3)" +hg children + +hg co 1 +echo "% hg children at revision 1 (should be 2)" +hg children + +hg co 2 +echo "% hg children at revision 2 (other head)" +hg children + +for i in null 0 1 2 3; do + echo "% hg children -r $i" + hg children -r $i +done + +echo "% hg children -r 0 file0 (should be 2)" +hg children -r 0 file0 + +echo "% hg children -r 1 file0 (should be 2)" +hg children -r 1 file0 + +hg co 0 +echo "% hg children file0 at revision 0 (should be 2)" +hg children file0 + diff --git a/tests/test-children.out b/tests/test-children.out new file mode 100644 --- /dev/null +++ b/tests/test-children.out @@ -0,0 +1,62 @@ +% init +% no working directory +% setup +0 files updated, 0 files merged, 2 files removed, 0 files unresolved +% hg children at revision 3 (tip) +0 files updated, 0 files merged, 1 files removed, 0 files unresolved +% hg children at nullrev (should be 0 and 3) +2 files updated, 0 files merged, 0 files removed, 0 files unresolved +% hg children at revision 1 (should be 2) +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +% hg children at revision 2 (other head) +% hg children -r null +changeset: 0:4df8521a7374 +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +summary: 0 + +changeset: 3:e2962852269d +tag: tip +parent: -1:000000000000 +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +summary: 3 + +% hg children -r 0 +changeset: 1:708c093edef0 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: 1 + +% hg children -r 1 +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + +% hg children -r 2 +% hg children -r 3 +% hg children -r 0 file0 (should be 2) +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + +% hg children -r 1 file0 (should be 2) +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + +1 files updated, 0 files merged, 1 files removed, 0 files unresolved +% hg children file0 at revision 0 (should be 2) +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + diff --git a/tests/test-clone-pull-corruption.out b/tests/test-clone-pull-corruption.out --- a/tests/test-clone-pull-corruption.out +++ b/tests/test-clone-pull-corruption.out @@ -1,8 +1,8 @@ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved pulling from ../source -abort: pretxncommit hook exited with status 1 transaction abort! rollback completed +abort: pretxncommit hook exited with status 1 searching for changes adding changesets adding manifests diff --git a/tests/test-commit.out b/tests/test-commit.out --- a/tests/test-commit.out +++ b/tests/test-commit.out @@ -1,4 +1,6 @@ % commit date test +transaction abort! +rollback completed abort: impossible time zone offset: 4444444 transaction abort! rollback completed @@ -6,8 +8,6 @@ abort: invalid date: '1\t15.1' transaction abort! rollback completed abort: invalid date: 'foo bar' -transaction abort! -rollback completed nothing changed % partial commit test trouble committing bar! diff --git a/tests/test-committer.out b/tests/test-committer.out --- a/tests/test-committer.out +++ b/tests/test-committer.out @@ -22,7 +22,7 @@ user: foo@bar.com date: Mon Jan 12 13:46:40 1970 +0000 summary: commit-1 -abort: Please specify a username. transaction abort! rollback completed +abort: Please specify a username. No username found, using user@host instead diff --git a/tests/test-convert b/tests/test-convert new file mode 100755 --- /dev/null +++ b/tests/test-convert @@ -0,0 +1,21 @@ +#!/bin/sh + +echo "[extensions]" >> $HGRCPATH +echo "convert=" >> $HGRCPATH + +hg init a +cd a +echo a > a +hg ci -d'0 0' -Ama +hg cp a b +hg ci -d'1 0' -mb +hg rm a +hg ci -d'2 0' -mc +hg mv b a +hg ci -d'3 0' -md +echo a >> a +hg ci -d'4 0' -me + +cd .. +hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded' +hg --cwd a-hg pull ../a diff --git a/tests/test-convert-cvs b/tests/test-convert-cvs new file mode 100755 --- /dev/null +++ b/tests/test-convert-cvs @@ -0,0 +1,50 @@ +#!/bin/sh + +"$TESTDIR/hghave" cvs cvsps || exit 80 + +echo "[extensions]" >> $HGRCPATH +echo "convert = " >> $HGRCPATH + +echo % create cvs repository +mkdir cvsrepo +cd cvsrepo +export CVSROOT=`pwd` +cd .. + +cvs -q -d "$CVSROOT" init + +echo % create source directory +mkdir src-temp +cd src-temp +echo a > a +mkdir b +cd b +echo c > c +cd .. + +echo % import source directory +cvs -q import -m import src INITIAL start +cd .. + +echo % checkout source directory +cvs -q checkout src + +echo % convert fresh repo +hg convert src src-hg | sed -e 's/connecting to.*cvsrepo/connecting to cvsrepo/g' +cat src-hg/a +cat src-hg/b/c + +echo % commit new file revisions +cd src +echo a >> a +echo c >> b/c +cvs -q commit -mci1 . | sed -e 's:.*src/\(.*\),v:src/\1,v:g' +cd .. + +echo % convert again +hg convert src src-hg | sed -e 's/connecting to.*cvsrepo/connecting to cvsrepo/g' +cat src-hg/a +cat src-hg/b/c + + + diff --git a/tests/test-convert-cvs.out b/tests/test-convert-cvs.out new file mode 100644 --- /dev/null +++ b/tests/test-convert-cvs.out @@ -0,0 +1,39 @@ +% create cvs repository +% create source directory +% import source directory +N src/a +N src/b/c + +No conflicts created by this import + +% checkout source directory +U src/a +U src/b/c +% convert fresh repo +initializing destination src-hg repository +connecting to cvsrepo +scanning source... +sorting... +converting... +1 Initial revision +0 import +updating tags +a +c +% commit new file revisions +src/a,v <-- a +new revision: 1.2; previous revision: 1.1 +src/b/c,v <-- b/c +new revision: 1.2; previous revision: 1.1 +% convert again +destination src-hg is a Mercurial repository +connecting to cvsrepo +scanning source... +sorting... +converting... +0 ci1 +updating tags +a +a +c +c diff --git a/tests/test-convert-git b/tests/test-convert-git --- a/tests/test-convert-git +++ b/tests/test-convert-git @@ -5,15 +5,42 @@ echo "[extensions]" >> $HGRCPATH echo "convert=" >> $HGRCPATH +GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME +GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL +GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE +GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME +GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL +GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE + +count=10 +commit() +{ + GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000" + GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" + git commit "$@" >/dev/null 2>/dev/null || echo "git commit error" + count=`expr $count + 1` +} + mkdir git-repo cd git-repo git init-db >/dev/null 2>/dev/null echo a > a git add a -git commit -m t1 >/dev/null 2>/dev/null || echo "git commit error" +commit -m t1 + echo b >> a -git commit -a -m t2 >/dev/null || echo "git commit error" +commit -a -m t2.1 + +git checkout -b other HEAD^ >/dev/null 2>/dev/null +echo c > a +echo a >> a +commit -a -m t2.2 + +git checkout master >/dev/null 2>/dev/null +git pull --no-commit . other > /dev/null 2>/dev/null +commit -m 'Merge branch other' cd .. -hg convert git-repo +hg convert --datesort git-repo +hg -R git-repo-hg tip -v diff --git a/tests/test-convert-git.out b/tests/test-convert-git.out --- a/tests/test-convert-git.out +++ b/tests/test-convert-git.out @@ -3,5 +3,20 @@ initializing destination git-repo-hg rep scanning source... sorting... converting... -1 t1 -0 t2 +3 t1 +2 t2.1 +1 t2.2 +0 Merge branch other +changeset: 3:69b3a302b4a1 +tag: tip +parent: 1:0de2a40e261b +parent: 2:8815d3b33506 +user: test +date: Mon Jan 01 00:00:13 2007 +0000 +files: a +description: +Merge branch other + +committer: test + + diff --git a/tests/test-convert-hg-sink b/tests/test-convert-hg-sink new file mode 100755 --- /dev/null +++ b/tests/test-convert-hg-sink @@ -0,0 +1,53 @@ +#!/bin/sh + +echo "[extensions]" >> $HGRCPATH +echo "hgext.convert=" >> $HGRCPATH + +hg init orig +cd orig +echo foo > foo +echo bar > bar +hg ci -qAm 'add foo and bar' -d '0 0' + +hg rm foo +hg ci -m 'remove foo' -d '0 0' + +mkdir foo +echo file > foo/file +hg ci -qAm 'add foo/file' -d '0 0' + +hg tag -d '0 0' some-tag + +hg log +cd .. + +hg convert orig new 2>&1 | grep -v 'subversion python bindings could not be loaded' +cd new +hg out ../orig + +echo '% dirstate should be empty:' +hg debugstate +hg parents -q + +hg up -C +hg copy bar baz +echo '% put something in the dirstate:' +hg debugstate > debugstate +grep baz debugstate + +echo '% add a new revision in the original repo' +cd ../orig +echo baz > baz +hg ci -qAm 'add baz' + +cd .. +hg convert orig new 2>&1 | grep -v 'subversion python bindings could not be loaded' +cd new +hg out ../orig +echo '% dirstate should be the same (no output below):' +hg debugstate > new-debugstate +diff debugstate new-debugstate + +echo '% no copies' +hg up -C +hg debugrename baz diff --git a/tests/test-convert-hg-sink.out b/tests/test-convert-hg-sink.out new file mode 100644 --- /dev/null +++ b/tests/test-convert-hg-sink.out @@ -0,0 +1,51 @@ +changeset: 3:593cbf6fb2b4 +tag: tip +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +summary: Added tag some-tag for changeset ad681a868e44 + +changeset: 2:ad681a868e44 +tag: some-tag +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +summary: add foo/file + +changeset: 1:cbba8ecc03b7 +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +summary: remove foo + +changeset: 0:327daa9251fa +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +summary: add foo and bar + +initializing destination new repository +scanning source... +sorting... +converting... +3 add foo and bar +2 remove foo +1 add foo/file +0 Added tag some-tag for changeset ad681a868e44 +comparing with ../orig +searching for changes +no changes found +% dirstate should be empty: +3 files updated, 0 files merged, 0 files removed, 0 files unresolved +% put something in the dirstate: +a 0 -1 unset baz +copy: bar -> baz +% add a new revision in the original repo +destination new is a Mercurial repository +scanning source... +sorting... +converting... +0 add baz +comparing with ../orig +searching for changes +no changes found +% dirstate should be the same (no output below): +% no copies +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +baz not renamed diff --git a/tests/test-convert-hg-source b/tests/test-convert-hg-source new file mode 100755 --- /dev/null +++ b/tests/test-convert-hg-source @@ -0,0 +1,33 @@ +#!/bin/sh + +echo "[extensions]" >> $HGRCPATH +echo "hgext.convert=" >> $HGRCPATH + +hg init orig +cd orig + +echo foo > foo +echo bar > bar +hg ci -qAm 'add foo bar' -d '0 0' + +echo >> foo +hg ci -m 'change foo' -d '1 0' + +hg up -qC 0 +hg copy --after --force foo bar +hg copy foo baz +hg ci -m 'make bar and baz copies of foo' -d '2 0' + +hg merge +hg ci -m 'merge local copy' -d '3 0' + +hg up -C 1 +hg merge 2 +hg ci -m 'merge remote copy' -d '4 0' + +cd .. +hg convert --datesort orig new 2>&1 | grep -v 'subversion python bindings could not be loaded' +cd new +hg out ../orig + +true diff --git a/tests/test-convert-hg-source.out b/tests/test-convert-hg-source.out new file mode 100644 --- /dev/null +++ b/tests/test-convert-hg-source.out @@ -0,0 +1,19 @@ +merging baz and foo +1 files updated, 1 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +1 files updated, 0 files merged, 1 files removed, 0 files unresolved +merging foo and baz +1 files updated, 1 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +initializing destination new repository +scanning source... +sorting... +converting... +4 add foo bar +3 change foo +2 make bar and baz copies of foo +1 merge local copy +0 merge remote copy +comparing with ../orig +searching for changes +no changes found diff --git a/tests/test-convert-svn b/tests/test-convert-svn new file mode 100755 --- /dev/null +++ b/tests/test-convert-svn @@ -0,0 +1,53 @@ +#!/bin/sh + +"$TESTDIR/hghave" svn svn-bindings || exit 80 + +fix_path() +{ + tr '\\' / +} + +echo "[extensions]" >> $HGRCPATH +echo "convert = " >> $HGRCPATH + +svnadmin create svn-repo + +echo % initial svn import +mkdir t +cd t +echo a > a +cd .. + +svnpath=`pwd | fix_path` +# SVN wants all paths to start with a slash. Unfortunately, +# Windows ones don't. Handle that. +expr $svnpath : "\/" > /dev/null +if [ $? -ne 0 ]; then + svnpath='/'$svnpath +fi + +svnurl=file://$svnpath/svn-repo/trunk +svn import -m init t $svnurl | fix_path + +echo % update svn repository +svn co $svnurl t2 | fix_path +cd t2 +echo b >> a +echo b > b +svn add b +svn ci -m changea +cd .. + +echo % convert to hg once +hg convert $svnurl + +echo % update svn repository again +cd t2 +echo c >> a +echo c >> b +svn ci -m changeb +cd .. + +echo % test incremental conversion +hg convert $svnurl + diff --git a/tests/test-convert-svn.out b/tests/test-convert-svn.out new file mode 100644 --- /dev/null +++ b/tests/test-convert-svn.out @@ -0,0 +1,32 @@ +% initial svn import +Adding t/a + +Committed revision 1. +% update svn repository +A t2/a +Checked out revision 1. +A b +Sending a +Adding b +Transmitting file data .. +Committed revision 2. +% convert to hg once +assuming destination trunk-hg +initializing destination trunk-hg repository +scanning source... +sorting... +converting... +1 init +0 changea +% update svn repository again +Sending a +Sending b +Transmitting file data .. +Committed revision 3. +% test incremental conversion +assuming destination trunk-hg +destination trunk-hg is a Mercurial repository +scanning source... +sorting... +converting... +0 changeb diff --git a/tests/test-convert.out b/tests/test-convert.out new file mode 100644 --- /dev/null +++ b/tests/test-convert.out @@ -0,0 +1,14 @@ +adding a +assuming destination a-hg +initializing destination a-hg repository +scanning source... +sorting... +converting... +4 a +3 b +2 c +1 d +0 e +pulling from ../a +searching for changes +no changes found diff --git a/tests/test-debugcomplete.out b/tests/test-debugcomplete.out --- a/tests/test-debugcomplete.out +++ b/tests/test-debugcomplete.out @@ -110,6 +110,7 @@ rawcommit % Show the options for the "serve" command --accesslog --address +--certificate --config --cwd --daemon diff --git a/tests/test-dispatch.py b/tests/test-dispatch.py --- a/tests/test-dispatch.py +++ b/tests/test-dispatch.py @@ -1,32 +1,32 @@ import os -from mercurial import commands +from mercurial import dispatch -def dispatch(cmd): - """Simple wrapper around commands.dispatch() +def testdispatch(cmd): + """Simple wrapper around dispatch.dispatch() Prints command and result value, but does not handle quoting. """ print "running: %s" % (cmd,) - result = commands.dispatch(cmd.split()) + result = dispatch.dispatch(cmd.split()) print "result: %r" % (result,) -dispatch("init test1") +testdispatch("init test1") os.chdir('test1') # create file 'foo', add and commit f = file('foo', 'wb') f.write('foo\n') f.close() -dispatch("add foo") -dispatch("commit -m commit1 -d 2000-01-01 foo") +testdispatch("add foo") +testdispatch("commit -m commit1 -d 2000-01-01 foo") # append to file 'foo' and commit f = file('foo', 'ab') f.write('bar\n') f.close() -dispatch("commit -m commit2 -d 2000-01-02 foo") +testdispatch("commit -m commit2 -d 2000-01-02 foo") # check 88803a69b24 (fancyopts modified command table) -dispatch("log -r 0") -dispatch("log -r tip") +testdispatch("log -r 0") +testdispatch("log -r tip") diff --git a/tests/test-encoding.out b/tests/test-encoding.out --- a/tests/test-encoding.out +++ b/tests/test-encoding.out @@ -9,9 +9,9 @@ M a ? latin-1 ? latin-1-tag ? utf-8 -abort: decoding near ' encoded: é': 'ascii' codec can't decode byte 0xe9 in position 20: ordinal not in range(128)! transaction abort! rollback completed +abort: decoding near ' encoded: é': 'ascii' codec can't decode byte 0xe9 in position 20: ordinal not in range(128)! % these should work marked working directory as branch é % ascii diff --git a/tests/test-extdiff b/tests/test-extdiff --- a/tests/test-extdiff +++ b/tests/test-extdiff @@ -6,7 +6,9 @@ echo "extdiff=" >> $HGRCPATH hg init a cd a echo a > a +echo b > b hg add +# should diff cloned directories hg extdiff -o -r $opt echo "[extdiff]" >> $HGRCPATH @@ -22,13 +24,17 @@ hg ci -d '0 0' -mtest1 echo b >> a hg ci -d '1 0' -mtest2 +# should diff cloned files directly hg falabala -r 0:1 # test diff during merge hg update 0 -echo b >> b -hg add b +echo c >> c +hg add c hg ci -m "new branch" -d '1 0' hg update -C 1 hg merge tip -hg falabala || echo "diff-like tools yield a non-zero exit code" +# should diff cloned file against wc file +hg falabala > out || echo "diff-like tools yield a non-zero exit code" +# cleanup the output since the wc is a tmp directory +sed 's:\(.* \).*\(\/test-extdiff\):\1[tmp]\2:' out diff --git a/tests/test-extdiff.out b/tests/test-extdiff.out --- a/tests/test-extdiff.out +++ b/tests/test-extdiff.out @@ -1,9 +1,7 @@ adding a -making snapshot of 0 files from rev 000000000000 -making snapshot of 1 files from working dir +adding b Only in a: a -making snapshot of 0 files from rev 000000000000 -making snapshot of 1 files from working dir +Only in a: b diffing a.000000000000 a hg falabala [OPTION]... [FILE]... @@ -26,14 +24,10 @@ options: -X --exclude exclude names matching the given patterns use "hg -v help falabala" to show global options -making snapshot of 1 files from rev e27a2475d60a -making snapshot of 1 files from rev 5e49ec8d3f05 -diffing a.e27a2475d60a a.5e49ec8d3f05 +diffing a.8a5febb7f867/a a.34eed99112ab/a 1 files updated, 0 files merged, 0 files removed, 0 files unresolved 1 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) -making snapshot of 1 files from rev 5e49ec8d3f05 -making snapshot of 1 files from working dir -diffing a.5e49ec8d3f05 a diff-like tools yield a non-zero exit code +diffing a.34eed99112ab/c [tmp]/test-extdiff/a/c diff --git a/tests/test-git-export b/tests/test-git-export --- a/tests/test-git-export +++ b/tests/test-git-export @@ -78,6 +78,8 @@ hg mv dst2 dst3 hg ci -m 'mv dst2 dst3; revert start' -d '0 0' hg diff --git -r 9:11 +echo '% reversed' +hg diff --git -r 11:9 echo a >> foo hg add foo @@ -92,12 +94,18 @@ hg ci -m 'change bar' echo echo '% file created before r1 and renamed before r2' hg diff --git -r -3:-1 +echo '% reversed' +hg diff --git -r -1:-3 echo echo '% file created in r1 and renamed before r2' hg diff --git -r -4:-1 +echo '% reversed' +hg diff --git -r -1:-4 echo echo '% file created after r1 and renamed before r2' hg diff --git -r -5:-1 +echo '% reversed' +hg diff --git -r -1:-5 echo echo '% comparing with the working dir' @@ -139,6 +147,8 @@ hg cp brand-new2 brand-new3 hg mv brand-new2 brand-new3-2 hg ci -m 'multiple renames/copies' hg diff --git -r -2 -r -1 +echo '% reversed' +hg diff --git -r -1 -r -2 echo '% there should be a trailing TAB if there are spaces in the file name' echo foo > 'with spaces' diff --git a/tests/test-git-export.out b/tests/test-git-export.out --- a/tests/test-git-export.out +++ b/tests/test-git-export.out @@ -75,6 +75,10 @@ rename to renamed.bin diff --git a/dst2 b/dst3 rename from dst2 rename to dst3 +% reversed +diff --git a/dst3 b/dst2 +rename from dst3 +rename to dst2 % file created before r1 and renamed before r2 diff --git a/foo b/bar @@ -86,6 +90,16 @@ rename to bar a b +c +% reversed +diff --git a/bar b/foo +rename from bar +rename to foo +--- a/foo ++++ b/foo +@@ -1,3 +1,2 @@ a + a + b +-c % file created in r1 and renamed before r2 diff --git a/foo b/bar @@ -97,6 +111,16 @@ rename to bar a +b +c +% reversed +diff --git a/bar b/foo +rename from bar +rename to foo +--- a/foo ++++ b/foo +@@ -1,3 +1,1 @@ a + a +-b +-c % file created after r1 and renamed before r2 diff --git a/bar b/bar @@ -107,6 +131,15 @@ new file mode 100644 +a +b +c +% reversed +diff --git a/bar b/bar +deleted file mode 100644 +--- a/bar ++++ /dev/null +@@ -1,3 +0,0 @@ +-a +-b +-c % comparing with the working dir % there's a copy in the working dir... @@ -145,6 +178,16 @@ rename to brand-new3 diff --git a/brand-new2 b/brand-new3-2 copy from brand-new2 copy to brand-new3-2 +% reversed +diff --git a/brand-new3 b/brand-new2 +rename from brand-new3 +rename to brand-new2 +diff --git a/brand-new3-2 b/brand-new3-2 +deleted file mode 100644 +--- a/brand-new3-2 ++++ /dev/null +@@ -1,1 +0,0 @@ +- % there should be a trailing TAB if there are spaces in the file name diff --git a/with spaces b/with spaces new file mode 100644 diff --git a/tests/test-hook.out b/tests/test-hook.out --- a/tests/test-hook.out +++ b/tests/test-hook.out @@ -60,9 +60,9 @@ precommit hook: HG_PARENT1=8ea2ef7ad3e8c pretxncommit hook: HG_NODE=fad284daf8c032148abaffcd745dafeceefceb61 HG_PARENT1=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 5:fad284daf8c0 pretxncommit.forbid hook: HG_NODE=fad284daf8c032148abaffcd745dafeceefceb61 HG_PARENT1=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 -abort: pretxncommit.forbid1 hook exited with status 1 transaction abort! rollback completed +abort: pretxncommit.forbid1 hook exited with status 1 4:8ea2ef7ad3e8 precommit hook: HG_PARENT1=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 precommit.forbid hook: HG_PARENT1=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 @@ -86,9 +86,9 @@ adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files -abort: pretxnchangegroup.forbid1 hook exited with status 1 transaction abort! rollback completed +abort: pretxnchangegroup.forbid1 hook exited with status 1 3:4c52fb2e4022 preoutgoing hook: HG_SOURCE=pull outgoing hook: HG_NODE=8ea2ef7ad3e8cac946c72f1e0c79d6aebc301198 HG_SOURCE=pull diff --git a/tests/test-hup.out b/tests/test-hup.out --- a/tests/test-hup.out +++ b/tests/test-hup.out @@ -1,7 +1,7 @@ 0 0 adding changesets -killed! transaction abort! rollback completed +killed! .hg/00changelog.i .hg/journal.dirstate .hg/requires .hg/store .hg/store/00changelog.i .hg/store/00changelog.i.a diff --git a/tests/test-imerge b/tests/test-imerge new file mode 100755 --- /dev/null +++ b/tests/test-imerge @@ -0,0 +1,64 @@ +#!/bin/sh + +echo "[extensions]" >> $HGRCPATH +echo "imerge=" >> $HGRCPATH +HGMERGE=true +export HGMERGE + +hg init base +cd base + +echo foo > foo +echo bar > bar +hg ci -Am0 -d '0 0' + +hg mv foo foo2 +echo foo >> foo2 +hg ci -m1 -d '1 0' + +hg up -C 0 +echo bar >> foo +echo bar >> bar +hg ci -m2 -d '2 0' + +echo % start imerge +hg imerge + +cat foo2 +cat bar + +echo % status -v +hg -v imerge st + +echo % next +hg imerge next + +echo % merge next +hg --traceback imerge + +echo % unresolve +hg imerge unres foo + +echo % merge foo +hg imerge merge foo + +echo % save +echo foo > foo2 +hg imerge save ../savedmerge + +echo % load +hg up -C 0 +hg imerge --traceback load ../savedmerge +cat foo2 + +hg ci -m'merged' -d '3 0' +hg tip -v + +echo % nothing to merge -- tip +hg imerge + +hg up 0 +echo % nothing to merge +hg imerge + +exit 0 diff --git a/tests/test-imerge.out b/tests/test-imerge.out new file mode 100644 --- /dev/null +++ b/tests/test-imerge.out @@ -0,0 +1,48 @@ +adding bar +adding foo +1 files updated, 0 files merged, 1 files removed, 0 files unresolved +% start imerge +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +U foo +foo +bar +bar +bar +% status -v +merging e6da46716401 and 30d266f502e7 +U foo (foo2) +% next +foo +% merge next +merging foo and foo2 +all conflicts resolved +% unresolve +% merge foo +merging foo and foo2 +all conflicts resolved +% save +% load +2 files updated, 0 files merged, 1 files removed, 0 files unresolved +2 files updated, 0 files merged, 0 files removed, 0 files unresolved +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +R foo +all conflicts resolved +foo +changeset: 3:fa9a6defdcaf +tag: tip +parent: 2:e6da46716401 +parent: 1:30d266f502e7 +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +files: foo foo2 +description: +merged + + +% nothing to merge -- tip +abort: there is nothing to merge +2 files updated, 0 files merged, 1 files removed, 0 files unresolved +% nothing to merge +abort: there is nothing to merge - use "hg update" instead diff --git a/tests/test-import b/tests/test-import --- a/tests/test-import +++ b/tests/test-import @@ -93,6 +93,24 @@ python mkmsg.py | hg --cwd b import - hg --cwd b tip | grep second rm -r b +# subject: duplicate detection, removal of [PATCH] +cat > mkmsg2.py < tip.patch +python mkmsg2.py | hg --cwd b import - +hg --cwd b tip --template '{desc}\n' +rm -r b + + # bug non regression test # importing a patch in a subdirectory failed at the commit stage echo line 2 >> a/d1/d2/a diff --git a/tests/test-import.out b/tests/test-import.out --- a/tests/test-import.out +++ b/tests/test-import.out @@ -100,6 +100,17 @@ added 1 changesets with 2 changes to 2 f 2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying patch from stdin summary: second change +% plain diff in email, [PATCH] subject, message body with subject +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 patch from stdin +email patch + +next line % hg import in a subdirectory requesting all changes adding changesets diff --git a/tests/test-issue322.out b/tests/test-issue322.out --- a/tests/test-issue322.out +++ b/tests/test-issue322.out @@ -1,12 +1,12 @@ % file replaced with directory adding a % should fail - would corrupt dirstate -abort: file named 'a' already in dirstate +abort: file 'a' in dirstate clashes with 'a/a' % directory replaced with file adding a/a % should fail - would corrupt dirstate -abort: directory named 'a' already in dirstate +abort: directory 'a' already in dirstate % directory replaced with file adding b/c/d % should fail - would corrupt dirstate -abort: directory named 'b' already in dirstate +abort: directory 'b' already in dirstate diff --git a/tests/test-issue522 b/tests/test-issue522 new file mode 100755 --- /dev/null +++ b/tests/test-issue522 @@ -0,0 +1,31 @@ +#!/bin/sh + +# In the merge below, the file "foo" has the same contents in both +# parents, but if we look at the file-level history, we'll notice that +# the version in p1 is an ancestor of the version in p2. This test +# makes sure that we'll use the version from p2 in the manifest of the +# merge revision. + +hg init repo +cd repo + +echo foo > foo +hg ci -d '0 0' -qAm 'add foo' + +echo bar >> foo +hg ci -d '0 0' -m 'change foo' + +hg backout -d '0 0' -r tip -m 'backout changed foo' + +hg up -C 0 +touch bar +hg ci -d '0 0' -qAm 'add bar' + +hg merge --debug +hg debugstate | grep foo +hg st -A foo +hg ci -d '0 0' -m 'merge' + +hg manifest --debug | grep foo +hg debugindex .hg/store/data/foo.i + diff --git a/tests/test-issue522.out b/tests/test-issue522.out new file mode 100644 --- /dev/null +++ b/tests/test-issue522.out @@ -0,0 +1,17 @@ +reverting foo +changeset 2:4d9e78aaceee backs out changeset 1:b515023e500e +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +resolving manifests + overwrite None partial False + ancestor bbd179dfa0a7 local 71766447bdbb+ remote 4d9e78aaceee + foo: remote is newer -> g +getting foo +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +n 0 -2 unset foo +M foo +c6fc755d7e68f49f880599da29f15add41f42f5a 644 foo + rev offset length base linkrev nodeid p1 p2 + 0 0 5 0 0 2ed2a3912a0b 000000000000 000000000000 + 1 5 9 1 1 6f4310b00b9a 2ed2a3912a0b 000000000000 + 2 14 5 2 2 c6fc755d7e68 6f4310b00b9a 000000000000 diff --git a/tests/test-manifest b/tests/test-manifest new file mode 100755 --- /dev/null +++ b/tests/test-manifest @@ -0,0 +1,20 @@ +#!/bin/sh + +hg init +echo a > a +hg ci -Ama -d'0 0' +mkdir b +echo a > b/a +hg ci -Amb -d'1 0' +hg manifest +hg manifest -v +hg manifest --debug +hg manifest -r 0 +hg manifest -r 1 +hg manifest -r tip + +echo % should fail +hg manifest -r 2 +hg manifest -r tip tip + +hg manifest tip diff --git a/tests/test-manifest.out b/tests/test-manifest.out new file mode 100644 --- /dev/null +++ b/tests/test-manifest.out @@ -0,0 +1,18 @@ +adding a +adding b/a +a +b/a +644 a +644 b/a +b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3 644 a +b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3 644 b/a +a +a +b/a +a +b/a +% should fail +abort: unknown revision '2'! +abort: please specify just one revision +a +b/a diff --git a/tests/test-merge-default b/tests/test-merge-default --- a/tests/test-merge-default +++ b/tests/test-merge-default @@ -34,6 +34,10 @@ echo % should succeed - 2 heads hg merge hg commit -mm2 +echo % should fail because at tip +hg merge + +hg up 0 echo % should fail because 1 head hg merge diff --git a/tests/test-merge-default.out b/tests/test-merge-default.out --- a/tests/test-merge-default.out +++ b/tests/test-merge-default.out @@ -13,5 +13,8 @@ 0 files updated, 0 files merged, 0 files % 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 at tip +abort: there is nothing to merge +1 files updated, 0 files merged, 0 files removed, 0 files unresolved % should fail because 1 head abort: there is nothing to merge - use "hg update" instead diff --git a/tests/test-mq b/tests/test-mq --- a/tests/test-mq +++ b/tests/test-mq @@ -338,6 +338,46 @@ hg qrefresh --git cat .hg/patches/bar hg log -vC --template '{rev} {file_copies%filecopy}\n' -r . +echo % refresh omitting an added file +hg qnew baz +echo newfile > newfile +hg add newfile +hg qrefresh +hg st -A newfile +hg qrefresh -X newfile +hg st -A newfile +hg revert newfile +rm newfile +hg qpop +hg qdel baz + +echo % create a git patch +echo a > alexander +hg add alexander +hg qnew -f --git addalexander +grep diff .hg/patches/addalexander + +echo % create a git binary patch +cat > writebin.py < foo hg add foo hg ci -m 'add foo' -hg qinit -c +hg qinit hg qnew patch1 echo bar >> foo hg qrefresh -m 'change foo' +cd .. + +# repo with unversioned patch dir +hg qclone qclonesource failure + +cd qclonesource +hg qinit -c hg qci -m checkpoint qlog cd .. diff --git a/tests/test-mq-qrefresh-replace-log-message b/tests/test-mq-qrefresh-replace-log-message --- a/tests/test-mq-qrefresh-replace-log-message +++ b/tests/test-mq-qrefresh-replace-log-message @@ -8,6 +8,11 @@ echo "mq=" >> $HGRCPATH hg init hg qinit +echo ======================= +echo "Should fail if no patches applied" +hg qrefresh +hg qrefresh -e + hg qnew -m "First commit message" first-patch echo aaaa > file hg add file diff --git a/tests/test-mq-qrefresh-replace-log-message.out b/tests/test-mq-qrefresh-replace-log-message.out --- a/tests/test-mq-qrefresh-replace-log-message.out +++ b/tests/test-mq-qrefresh-replace-log-message.out @@ -1,3 +1,7 @@ +======================= +Should fail if no patches applied +No patches applied +No patches applied ======================= Should display 'First commit message' description: diff --git a/tests/test-mq-symlinks b/tests/test-mq-symlinks new file mode 100755 --- /dev/null +++ b/tests/test-mq-symlinks @@ -0,0 +1,34 @@ +#!/bin/sh + +echo "[extensions]" >> $HGRCPATH +echo "mq=" >> $HGRCPATH + +cat >> readlink.py <', os.readlink(f) + except OSError, err: + if err.errno != errno.EINVAL: raise + print f, 'not a symlink' +EOF + +hg init +hg qinit +hg qnew base.patch +echo a > a +echo b > b +hg add a b +hg qrefresh +python readlink.py a + +hg qnew symlink.patch +rm a +ln -s b a +hg qrefresh --git +python readlink.py a + +hg qpop +hg qpush +python readlink.py a diff --git a/tests/test-mq-symlinks.out b/tests/test-mq-symlinks.out new file mode 100644 --- /dev/null +++ b/tests/test-mq-symlinks.out @@ -0,0 +1,6 @@ +a -> a not a symlink +a -> b +Now at: base.patch +applying symlink.patch +Now at: symlink.patch +a -> b diff --git a/tests/test-mq.out b/tests/test-mq.out --- a/tests/test-mq.out +++ b/tests/test-mq.out @@ -262,7 +262,8 @@ M a Patch queue now empty applying foo applying bar -1 out of 1 hunk ignored -- saving rejects to file foo.rej +file foo already exists +1 out of 1 hunk FAILED -- saving rejects to file foo.rej patch failed, unable to continue (try -v) patch failed, rejects left in working dir Errors during apply, please fix and refresh bar @@ -359,6 +360,20 @@ new file mode 100644 @@ -0,0 +1,1 @@ +bar 3 barney (foo) +% refresh omitting an added file +C newfile +A newfile +Now at: bar +% create a git patch +diff --git a/alexander b/alexander +% create a git binary patch +8ba2a2f3e77b55d03051ff9c24ad65e7 bucephalus +diff --git a/bucephalus b/bucephalus +% check binary patches can be popped and pushed +Now at: addalexander +applying addbucephalus +Now at: addbucephalus +8ba2a2f3e77b55d03051ff9c24ad65e7 bucephalus % strip again 1 files updated, 0 files merged, 0 files removed, 0 files unresolved merging foo @@ -409,6 +424,8 @@ date: Thu Jan 01 00:00:00 1970 +0 summary: add foo % qclone +abort: versioned patch repository not found (see qinit -c) +adding .hg/patches/patch1 main repo: rev 1: change foo rev 0: add foo diff --git a/tests/test-nested-repo b/tests/test-nested-repo --- a/tests/test-nested-repo +++ b/tests/test-nested-repo @@ -4,16 +4,25 @@ hg init a cd a hg init b echo x > b/x + echo '# should print nothing' +hg add b hg st -echo '# should print ? b/x' + +echo '# should fail' hg st b/x - hg add b/x -echo '# should print A b/x' +echo '# should fail' +hg add b b/x hg st -echo '# should forget b/x' -hg revert --all -echo '# should print nothing' + +echo '# should arguably print nothing' hg st b + +echo a > a +hg ci -Ama a + +echo '# should fail' +hg mv a b +hg st diff --git a/tests/test-nested-repo.out b/tests/test-nested-repo.out --- a/tests/test-nested-repo.out +++ b/tests/test-nested-repo.out @@ -1,8 +1,9 @@ # should print nothing -# should print ? b/x -? b/x -# should print A b/x -A b/x -# should forget b/x -forgetting b/x -# should print nothing +# should fail +abort: path 'b/x' is inside repo 'b' +abort: path 'b/x' is inside repo 'b' +# should fail +abort: path 'b/x' is inside repo 'b' +# should arguably print nothing +# should fail +abort: path 'b/a' is inside repo 'b' diff --git a/tests/test-non-interactive-wsgi b/tests/test-non-interactive-wsgi new file mode 100755 --- /dev/null +++ b/tests/test-non-interactive-wsgi @@ -0,0 +1,70 @@ +#!/bin/sh + +mkdir repo +cd repo +hg init +echo foo > bar +hg add bar +hg commit -m "test" -d "0 0" +hg tip + +cat > request.py <> sys.__stdout__, 'FILENO' + return self.real.fileno() + def read(self): + print >> sys.__stdout__, 'READ' + return self.real.read() + def readline(self): + print >> sys.__stdout__, 'READLINE' + return self.real.readline() + def isatty(self): + print >> sys.__stdout__, 'ISATTY' + return False + +sys.stdin = FileLike(sys.stdin) +errors = StringIO() +input = StringIO() +output = StringIO() + +def startrsp(headers, data): + print '---- HEADERS' + print headers + print '---- DATA' + print data + return output.write + +env = { + 'wsgi.version': (1, 0), + 'wsgi.url_scheme': 'http', + 'wsgi.errors': errors, + 'wsgi.input': input, + 'wsgi.multithread': False, + 'wsgi.multiprocess': False, + 'wsgi.run_once': False, + 'REQUEST_METHOD': 'GET', + 'SCRIPT_NAME': '', + 'PATH_INFO': '', + 'QUERY_STRING': '', + 'SERVER_NAME': '127.0.0.1', + 'SERVER_PORT': '20059', + 'SERVER_PROTOCOL': 'HTTP/1.0' +} + +_wsgirequest(hgweb('.'), env, startrsp) +print '---- ERRORS' +print errors.getvalue() +EOF + +python request.py diff --git a/tests/test-non-interactive-wsgi.out b/tests/test-non-interactive-wsgi.out new file mode 100644 --- /dev/null +++ b/tests/test-non-interactive-wsgi.out @@ -0,0 +1,12 @@ +changeset: 0:61c9426e69fe +tag: tip +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +summary: test + +---- HEADERS +200 Script output follows +---- DATA +[('content-type', 'text/html; charset=ascii')] +---- ERRORS + diff --git a/tests/test-parentrevspec b/tests/test-parentrevspec new file mode 100755 --- /dev/null +++ b/tests/test-parentrevspec @@ -0,0 +1,69 @@ +#!/bin/sh + +commit() +{ + msg=$1 + p1=$2 + p2=$3 + + if [ "$p1" ]; then + hg up -qC $p1 + fi + + if [ "$p2" ]; then + HGMERGE=true hg merge -q $p2 + fi + + echo >> foo + + hg commit -d '0 0' -qAm "$msg" foo +} + +hg init repo +cd repo + +echo '[extensions]' > .hg/hgrc +echo 'hgext.parentrevspec =' >> .hg/hgrc + +commit '0: add foo' +commit '1: change foo 1' +commit '2: change foo 2a' +commit '3: change foo 3a' +commit '4: change foo 2b' 1 +commit '5: merge' 3 4 +commit '6: change foo again' + +hg log --template '#rev#:#node|short# #parents#\n' +echo + +lookup() +{ + for rev in "$@"; do + printf "$rev: " + hg id -nr $rev + done + true +} + +tipnode=`hg id -ir tip` + +echo 'should work with tag/branch/node/rev' +for r in tip default $tipnode 6; do + lookup "$r^" +done +echo + +echo 'some random lookups' +lookup "6^^" "6^^^" "6^^^^" "6^^^^^" "6^^^^^^" "6^1" "6^2" "6^^2" "6^1^2" "6^^3" +lookup "6~" "6~1" "6~2" "6~3" "6~4" "6~5" "6~42" "6~1^2" "6~1^2~2" +echo + +echo 'with a tag "6^" pointing to rev 1' +hg tag -l -r 1 "6^" +lookup "6^" "6^1" "6~1" "6^^" +echo + +echo 'with a tag "foo^bar" pointing to rev 2' +hg tag -l -r 2 "foo^bar" +lookup "foo^bar" "foo^bar^" + diff --git a/tests/test-parentrevspec.out b/tests/test-parentrevspec.out new file mode 100644 --- /dev/null +++ b/tests/test-parentrevspec.out @@ -0,0 +1,44 @@ +6:755d1e0d79e9 +5:9ce2ce29723a 3:a3e00c7dbf11 4:bb4475edb621 +4:bb4475edb621 1:5d953a1917d1 +3:a3e00c7dbf11 +2:befc7d89d081 +1:5d953a1917d1 +0:837088b6e1d9 + +should work with tag/branch/node/rev +tip^: 5 +default^: 5 +755d1e0d79e9^: 5 +6^: 5 + +some random lookups +6^^: 3 +6^^^: 2 +6^^^^: 1 +6^^^^^: 0 +6^^^^^^: -1 +6^1: 5 +6^2: abort: unknown revision '6^2'! +6^^2: 4 +6^1^2: 4 +6^^3: abort: unknown revision '6^^3'! +6~: abort: unknown revision '6~'! +6~1: 5 +6~2: 3 +6~3: 2 +6~4: 1 +6~5: 0 +6~42: -1 +6~1^2: 4 +6~1^2~2: 0 + +with a tag "6^" pointing to rev 1 +6^: 1 +6^1: 5 +6~1: 5 +6^^: 3 + +with a tag "foo^bar" pointing to rev 2 +foo^bar: 2 +foo^bar^: abort: unknown revision 'foo^bar^'! diff --git a/tests/test-parents b/tests/test-parents --- a/tests/test-parents +++ b/tests/test-parents @@ -13,6 +13,12 @@ echo a >> a hg ci -Ama -d '1 0' echo b >> b hg ci -Amb -d '2 0' +echo c > c +hg ci -Amc -d '3 0' +hg up -C 1 +echo d > c +hg ci -Amc2 -d '4 0' +hg up -C 3 echo % hg parents hg parents @@ -20,6 +26,12 @@ hg parents echo % hg parents a hg parents a +echo % hg parents c, single revision +hg parents c + +echo % hg parents -r 3 c +hg parents -r 3 c + echo % hg parents -r 2 hg parents -r 2 @@ -41,4 +53,15 @@ echo '% hg parents -r 2 glob:a' cd .. hg parents -r 2 glob:a +echo % merge working dir with 2 parents, hg parents c +HGMERGE=true hg merge +hg parents c + +echo % merge working dir with 1 parent, hg parents +hg up -C 2 +HGMERGE=true hg merge -r 4 +hg parents +echo % merge working dir with 1 parent, hg parents c +hg parents c + true diff --git a/tests/test-parents.out b/tests/test-parents.out --- a/tests/test-parents.out +++ b/tests/test-parents.out @@ -1,19 +1,30 @@ % no working directory adding a adding b +adding c +1 files updated, 0 files merged, 1 files removed, 0 files unresolved +adding c +2 files updated, 0 files merged, 0 files removed, 0 files unresolved % hg parents -changeset: 2:6cfac479f009 -tag: tip +changeset: 3:02d851b7e549 user: test -date: Thu Jan 01 00:00:02 1970 +0000 -summary: b +date: Thu Jan 01 00:00:03 1970 +0000 +summary: c % hg parents a -changeset: 0:b6a1406d8886 +changeset: 1:d786049f033a user: test -date: Thu Jan 01 00:00:00 1970 +0000 -summary: ab +date: Thu Jan 01 00:00:01 1970 +0000 +summary: a +% hg parents c, single revision +changeset: 3:02d851b7e549 +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +summary: c + +% hg parents -r 3 c +abort: 'c' not found in manifest! % hg parents -r 2 changeset: 1:d786049f033a user: test @@ -21,24 +32,64 @@ date: Thu Jan 01 00:00:01 1970 +0 summary: a % hg parents -r 2 a -changeset: 0:b6a1406d8886 +changeset: 1:d786049f033a user: test -date: Thu Jan 01 00:00:00 1970 +0000 -summary: ab +date: Thu Jan 01 00:00:01 1970 +0000 +summary: a % hg parents -r 2 ../a abort: ../a not under root % cd dir; hg parents -r 2 ../a -changeset: 0:b6a1406d8886 +changeset: 1:d786049f033a user: test -date: Thu Jan 01 00:00:00 1970 +0000 -summary: ab +date: Thu Jan 01 00:00:01 1970 +0000 +summary: a % hg parents -r 2 path:a -changeset: 0:b6a1406d8886 +changeset: 1:d786049f033a user: test -date: Thu Jan 01 00:00:00 1970 +0000 -summary: ab +date: Thu Jan 01 00:00:01 1970 +0000 +summary: a % hg parents -r 2 glob:a abort: can only specify an explicit file name +% merge working dir with 2 parents, hg parents c +merging c +0 files updated, 1 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +changeset: 3:02d851b7e549 +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +summary: c + +changeset: 4:48cee28d4b4e +tag: tip +parent: 1:d786049f033a +user: test +date: Thu Jan 01 00:00:04 1970 +0000 +summary: c2 + +% merge working dir with 1 parent, hg parents +0 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) +changeset: 2:6cfac479f009 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: b + +changeset: 4:48cee28d4b4e +tag: tip +parent: 1:d786049f033a +user: test +date: Thu Jan 01 00:00:04 1970 +0000 +summary: c2 + +% merge working dir with 1 parent, hg parents c +changeset: 4:48cee28d4b4e +tag: tip +parent: 1:d786049f033a +user: test +date: Thu Jan 01 00:00:04 1970 +0000 +summary: c2 + 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 @@ -3,6 +3,8 @@ changeset 3:107ce1ee2b43 backs out chang merging with changeset 2:e6c3abc120e7 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) +transaction abort! +rollback completed abort: invalid date: 'should fail' transaction abort! rollback completed @@ -10,8 +12,6 @@ abort: date exceeds 32 bits: 10000000000 transaction abort! rollback completed abort: impossible time zone offset: 1400000 -transaction abort! -rollback completed Sun Jan 15 13:30:00 2006 +0500 Sun Jan 15 13:30:00 2006 -0800 Sat Jul 15 13:30:00 2006 +0500 diff --git a/tests/test-rebuildstate.out b/tests/test-rebuildstate.out --- a/tests/test-rebuildstate.out +++ b/tests/test-rebuildstate.out @@ -1,7 +1,7 @@ adding bar adding foo % state dump -a 644 0 baz +a 0 -1 baz n 644 0 foo r 0 0 bar % status diff --git a/tests/test-record b/tests/test-record new file mode 100755 --- /dev/null +++ b/tests/test-record @@ -0,0 +1,266 @@ +#!/bin/sh + +echo "[ui]" >> $HGRCPATH +echo "interactive=true" >> $HGRCPATH +echo "[extensions]" >> $HGRCPATH +echo "record=" >> $HGRCPATH + +echo % help + +hg help record + +hg init a +cd a + +echo % select no files + +touch empty-rw +hg add empty-rw +hg record empty-rw<> plain +done + +hg add plain +hg record -d '7 0' -m plain plain<> plain +hg record -d '8 0' -m end plain <> plain +hg record -d '9 0' -m noeol plain <> plain +hg record -d '10 0' -m eol plain <> plain +done + +hg record -d '10 0' -m begin-and-end plain <> plain +done + +echo % record end + +hg record -d '11 0' -m end-only plain <> plain +done + +echo % record end + +hg record --traceback -d '13 0' -m end-again plain<> plain +done + +echo % record beginning, middle + +hg record -d '14 0' -m middle-only plain < a +hg ci -d '16 0' -Amsubdir + +echo a >> a +hg record -d '16 0' -m subdir-change a < f1 +echo b > f2 +hg add f1 f2 + +hg ci -mz -d '17 0' + +echo a >> f1 +echo b >> f2 + +echo % help, quit + +hg record < as commit message + -l --logfile read commit message from + -d --date record datecode as commit date + -u --user record user as committer + +use "hg -v help record" to show global options +% select no files +diff --git a/empty-rw b/empty-rw +new file mode 100644 +examine changes to 'empty-rw'? [Ynsfdaq?] no changes to record + +changeset: -1:000000000000 +tag: tip +user: +date: Thu Jan 01 00:00:00 1970 +0000 + + +% select files but no hunks +diff --git a/empty-rw b/empty-rw +new file mode 100644 +examine changes to 'empty-rw'? [Ynsfdaq?] transaction abort! +rollback completed + +changeset: -1:000000000000 +tag: tip +user: +date: Thu Jan 01 00:00:00 1970 +0000 + + +% record empty file +diff --git a/empty-rw b/empty-rw +new file mode 100644 +examine changes to 'empty-rw'? [Ynsfdaq?] +changeset: 0:c0708cf4e46e +tag: tip +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +summary: empty + + +% rename empty file +diff --git a/empty-rw b/empty-rename +rename from empty-rw +rename to empty-rename +examine changes to 'empty-rw' and 'empty-rename'? [Ynsfdaq?] +changeset: 1:df251d174da3 +tag: tip +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: rename + + +% copy empty file +diff --git a/empty-rename b/empty-copy +copy from empty-rename +copy to empty-copy +examine changes to 'empty-rename' and 'empty-copy'? [Ynsfdaq?] +changeset: 2:b63ea3939f8d +tag: tip +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: copy + + +% delete empty file +diff --git a/empty-copy b/empty-copy +deleted file mode 100644 +examine changes to 'empty-copy'? [Ynsfdaq?] +changeset: 3:a2546574bce9 +tag: tip +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +summary: delete + + +% add binary file +diff --git a/tip.bundle b/tip.bundle +new file mode 100644 +this is a binary file +examine changes to 'tip.bundle'? [Ynsfdaq?] +changeset: 4:9e998a545a8b +tag: tip +user: test +date: Thu Jan 01 00:00:04 1970 +0000 +summary: binary + +diff -r a2546574bce9 -r 9e998a545a8b tip.bundle +Binary file tip.bundle has changed + +% change binary file +diff --git a/tip.bundle b/tip.bundle +this modifies a binary file (all or nothing) +examine changes to 'tip.bundle'? [Ynsfdaq?] +changeset: 5:93d05561507d +tag: tip +user: test +date: Thu Jan 01 00:00:05 1970 +0000 +summary: binary-change + +diff -r 9e998a545a8b -r 93d05561507d tip.bundle +Binary file tip.bundle has changed + +% rename and change binary file +diff --git a/tip.bundle b/top.bundle +rename from tip.bundle +rename to top.bundle +this modifies a binary file (all or nothing) +examine changes to 'tip.bundle' and 'top.bundle'? [Ynsfdaq?] +changeset: 6:699cc1bea9aa +tag: tip +user: test +date: Thu Jan 01 00:00:06 1970 +0000 +summary: binary-change-rename + +diff -r 93d05561507d -r 699cc1bea9aa tip.bundle +Binary file tip.bundle has changed +diff -r 93d05561507d -r 699cc1bea9aa top.bundle +Binary file top.bundle has changed + +% add plain file +diff --git a/plain b/plain +new file mode 100644 +examine changes to 'plain'? [Ynsfdaq?] +changeset: 7:118ed744216b +tag: tip +user: test +date: Thu Jan 01 00:00:07 1970 +0000 +summary: plain + +diff -r 699cc1bea9aa -r 118ed744216b plain +--- /dev/null Thu Jan 01 00:00:00 1970 +0000 ++++ b/plain Thu Jan 01 00:00:07 1970 +0000 +@@ -0,0 +1,10 @@ ++1 ++2 ++3 ++4 ++5 ++6 ++7 ++8 ++9 ++10 + +% modify end of plain file +diff --git a/plain b/plain +1 hunks, 1 lines changed +examine changes to 'plain'? [Ynsfdaq?] @@ -8,3 +8,4 @@ 8 + 8 + 9 + 10 ++11 +record this change to 'plain'? [Ynsfdaq?] % modify end of plain file, no EOL +diff --git a/plain b/plain +1 hunks, 1 lines changed +examine changes to 'plain'? [Ynsfdaq?] @@ -9,3 +9,4 @@ 9 + 9 + 10 + 11 ++cf81a2760718a74d44c0c2eecb72f659e63a69c5 +\ No newline at end of file +record this change to 'plain'? [Ynsfdaq?] % modify end of plain file, add EOL +diff --git a/plain b/plain +1 hunks, 2 lines changed +examine changes to 'plain'? [Ynsfdaq?] @@ -9,4 +9,4 @@ 9 + 9 + 10 + 11 +-cf81a2760718a74d44c0c2eecb72f659e63a69c5 +\ No newline at end of file ++cf81a2760718a74d44c0c2eecb72f659e63a69c5 +record this change to 'plain'? [Ynsfdaq?] % modify beginning, trim end, record both +diff --git a/plain b/plain +2 hunks, 4 lines changed +examine changes to 'plain'? [Ynsfdaq?] @@ -1,4 +1,4 @@ 1 +-1 ++2 + 2 + 3 + 4 +record this change to 'plain'? [Ynsfdaq?] @@ -8,5 +8,3 @@ 8 + 8 + 9 + 10 +-11 +-cf81a2760718a74d44c0c2eecb72f659e63a69c5 +record this change to 'plain'? [Ynsfdaq?] +changeset: 11:d09ab1967dab +tag: tip +user: test +date: Thu Jan 01 00:00:10 1970 +0000 +summary: begin-and-end + +diff -r e2ecd9b0b78d -r d09ab1967dab plain +--- a/plain Thu Jan 01 00:00:10 1970 +0000 ++++ b/plain Thu Jan 01 00:00:10 1970 +0000 +@@ -1,4 +1,4 @@ 1 +-1 ++2 + 2 + 3 + 4 +@@ -8,5 +8,3 @@ 8 + 8 + 9 + 10 +-11 +-cf81a2760718a74d44c0c2eecb72f659e63a69c5 + +% trim beginning, modify end +% record end +diff --git a/plain b/plain +2 hunks, 5 lines changed +examine changes to 'plain'? [Ynsfdaq?] @@ -1,9 +1,6 @@ 2 +-2 +-2 +-3 + 4 + 5 + 6 + 7 + 8 + 9 +record this change to 'plain'? [Ynsfdaq?] @@ -4,7 +1,7 @@ + 4 + 5 + 6 + 7 + 8 + 9 +-10 ++10.new +record this change to 'plain'? [Ynsfdaq?] +changeset: 12:44516c9708ae +tag: tip +user: test +date: Thu Jan 01 00:00:11 1970 +0000 +summary: end-only + +diff -r d09ab1967dab -r 44516c9708ae plain +--- a/plain Thu Jan 01 00:00:10 1970 +0000 ++++ b/plain Thu Jan 01 00:00:11 1970 +0000 +@@ -7,4 +7,4 @@ 7 + 7 + 8 + 9 +-10 ++10.new + +% record beginning +diff --git a/plain b/plain +1 hunks, 3 lines changed +examine changes to 'plain'? [Ynsfdaq?] @@ -1,6 +1,3 @@ 2 +-2 +-2 +-3 + 4 + 5 + 6 +record this change to 'plain'? [Ynsfdaq?] +changeset: 13:3ebbace64a8d +tag: tip +user: test +date: Thu Jan 01 00:00:12 1970 +0000 +summary: begin-only + +diff -r 44516c9708ae -r 3ebbace64a8d plain +--- a/plain Thu Jan 01 00:00:11 1970 +0000 ++++ b/plain Thu Jan 01 00:00:12 1970 +0000 +@@ -1,6 +1,3 @@ 2 +-2 +-2 +-3 + 4 + 5 + 6 + +% add to beginning, trim from end +% record end +diff --git a/plain b/plain +2 hunks, 4 lines changed +examine changes to 'plain'? [Ynsfdaq?] @@ -1,6 +1,9 @@ 4 ++1 ++2 ++3 + 4 + 5 + 6 + 7 + 8 + 9 +record this change to 'plain'? [Ynsfdaq?] @@ -1,7 +4,6 @@ + 4 + 5 + 6 + 7 + 8 + 9 +-10.new +record this change to 'plain'? [Ynsfdaq?] % add to beginning, middle, end +% record beginning, middle +diff --git a/plain b/plain +3 hunks, 7 lines changed +examine changes to 'plain'? [Ynsfdaq?] @@ -1,2 +1,5 @@ 4 ++1 ++2 ++3 + 4 + 5 +record this change to 'plain'? [Ynsfdaq?] @@ -1,6 +4,8 @@ + 4 + 5 ++5.new ++5.reallynew + 6 + 7 + 8 + 9 +record this change to 'plain'? [Ynsfdaq?] @@ -3,4 +8,6 @@ + 6 + 7 + 8 + 9 ++10 ++11 +record this change to 'plain'? [Ynsfdaq?] +changeset: 15:c1c639d8b268 +tag: tip +user: test +date: Thu Jan 01 00:00:14 1970 +0000 +summary: middle-only + +diff -r efc0dad7bd9f -r c1c639d8b268 plain +--- a/plain Thu Jan 01 00:00:13 1970 +0000 ++++ b/plain Thu Jan 01 00:00:14 1970 +0000 +@@ -1,5 +1,10 @@ 4 ++1 ++2 ++3 + 4 + 5 ++5.new ++5.reallynew + 6 + 7 + 8 + +% record end +diff --git a/plain b/plain +1 hunks, 2 lines changed +examine changes to 'plain'? [Ynsfdaq?] @@ -9,3 +9,5 @@ 7 + 7 + 8 + 9 ++10 ++11 +record this change to 'plain'? [Ynsfdaq?] +changeset: 16:80b74bbc7808 +tag: tip +user: test +date: Thu Jan 01 00:00:15 1970 +0000 +summary: end-only + +diff -r c1c639d8b268 -r 80b74bbc7808 plain +--- a/plain Thu Jan 01 00:00:14 1970 +0000 ++++ b/plain Thu Jan 01 00:00:15 1970 +0000 +@@ -9,3 +9,5 @@ 7 + 7 + 8 + 9 ++10 ++11 + +adding subdir/a +diff --git a/subdir/a b/subdir/a +1 hunks, 1 lines changed +examine changes to 'subdir/a'? [Ynsfdaq?] @@ -1,1 +1,2 @@ a + a ++a +record this change to 'subdir/a'? [Ynsfdaq?] +changeset: 18:33ff5c4fb017 +tag: tip +user: test +date: Thu Jan 01 00:00:16 1970 +0000 +summary: subdir-change + +diff -r aecf2b2ea83c -r 33ff5c4fb017 subdir/a +--- a/subdir/a Thu Jan 01 00:00:16 1970 +0000 ++++ b/subdir/a Thu Jan 01 00:00:16 1970 +0000 +@@ -1,1 +1,2 @@ a + a ++a + +% help, quit +diff --git a/subdir/f1 b/subdir/f1 +1 hunks, 1 lines changed +examine changes to 'subdir/f1'? [Ynsfdaq?] y - record this change +n - skip this change +s - skip remaining changes to this file +f - record remaining changes to this file +d - done, skip remaining changes and files +a - record all changes to all remaining files +q - quit, recording no changes +? - display help +examine changes to 'subdir/f1'? [Ynsfdaq?] abort: user quit +% skip +diff --git a/subdir/f1 b/subdir/f1 +1 hunks, 1 lines changed +examine changes to 'subdir/f1'? [Ynsfdaq?] diff --git a/subdir/f2 b/subdir/f2 +1 hunks, 1 lines changed +examine changes to 'subdir/f2'? [Ynsfdaq?] abort: response expected +% no +diff --git a/subdir/f1 b/subdir/f1 +1 hunks, 1 lines changed +examine changes to 'subdir/f1'? [Ynsfdaq?] diff --git a/subdir/f2 b/subdir/f2 +1 hunks, 1 lines changed +examine changes to 'subdir/f2'? [Ynsfdaq?] abort: response expected +% f, quit +diff --git a/subdir/f1 b/subdir/f1 +1 hunks, 1 lines changed +examine changes to 'subdir/f1'? [Ynsfdaq?] diff --git a/subdir/f2 b/subdir/f2 +1 hunks, 1 lines changed +examine changes to 'subdir/f2'? [Ynsfdaq?] abort: user quit +% s, all +diff --git a/subdir/f1 b/subdir/f1 +1 hunks, 1 lines changed +examine changes to 'subdir/f1'? [Ynsfdaq?] diff --git a/subdir/f2 b/subdir/f2 +1 hunks, 1 lines changed +examine changes to 'subdir/f2'? [Ynsfdaq?] +changeset: 20:094183e04b7c +tag: tip +user: test +date: Thu Jan 01 00:00:18 1970 +0000 +summary: x + +diff -r f9e855cd9374 -r 094183e04b7c subdir/f2 +--- a/subdir/f2 Thu Jan 01 00:00:17 1970 +0000 ++++ b/subdir/f2 Thu Jan 01 00:00:18 1970 +0000 +@@ -1,1 +1,2 @@ b + b ++b + +% f +diff --git a/subdir/f1 b/subdir/f1 +1 hunks, 1 lines changed +examine changes to 'subdir/f1'? [Ynsfdaq?] +changeset: 21:38164785b0ef +tag: tip +user: test +date: Thu Jan 01 00:00:19 1970 +0000 +summary: y + +diff -r 094183e04b7c -r 38164785b0ef subdir/f1 +--- a/subdir/f1 Thu Jan 01 00:00:18 1970 +0000 ++++ b/subdir/f1 Thu Jan 01 00:00:19 1970 +0000 +@@ -1,1 +1,2 @@ a + a ++a + diff --git a/tests/test-rename b/tests/test-rename --- a/tests/test-rename +++ b/tests/test-rename @@ -88,6 +88,11 @@ hg status -C diff d1/b d2/b hg update -C +echo "# attempt to move one file into a non-existent directory" +hg rename d1/a dx/ +hg status -C +hg update -C + echo "# attempt to move potentially more than one file into a non-existent" echo "# directory" hg rename 'glob:d1/**' dx diff --git a/tests/test-rename-after-merge b/tests/test-rename-after-merge new file mode 100755 --- /dev/null +++ b/tests/test-rename-after-merge @@ -0,0 +1,33 @@ +#!/bin/sh + +# Test issue 746: renaming files brought by the +# second parent of a merge was broken. + +echo % create source repository +hg init t +cd t +echo a > a +hg ci -Am a +cd .. + +echo % fork source repository +hg clone t t2 +cd t2 +echo b > b +hg ci -Am b + +echo % update source repository +cd ../t +echo a >> a +hg ci -m a2 + +echo % merge repositories +hg pull ../t2 +hg merge + +echo % rename b as c +hg mv b c +hg st +echo % rename back c as b +hg mv c b +hg st diff --git a/tests/test-rename-after-merge.out b/tests/test-rename-after-merge.out new file mode 100644 --- /dev/null +++ b/tests/test-rename-after-merge.out @@ -0,0 +1,20 @@ +% create source repository +adding a +% fork source repository +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +adding b +% update source repository +% merge repositories +pulling from ../t2 +searching for changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files (+1 heads) +(run 'hg heads' to see heads, 'hg merge' to merge) +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +(branch merge, don't forget to commit) +% rename b as c +A c +R b +% rename back c as b diff --git a/tests/test-rename.out b/tests/test-rename.out --- a/tests/test-rename.out +++ b/tests/test-rename.out @@ -166,6 +166,9 @@ 1c1 --- > d2/b 3 files updated, 0 files merged, 3 files removed, 0 files unresolved +# attempt to move one file into a non-existent directory +abort: destination dx/ is not a directory +0 files updated, 0 files merged, 0 files removed, 0 files unresolved # attempt to move potentially more than one file into a non-existent # directory abort: with multiple sources, destination must be an existing directory diff --git a/tests/test-revlog-packentry b/tests/test-revlog-packentry new file mode 100755 --- /dev/null +++ b/tests/test-revlog-packentry @@ -0,0 +1,14 @@ +#!/bin/sh + +hg init repo +cd repo + +touch foo +hg ci -Am 'add foo' + +hg up -C null +# this should be stored as a delta against rev 0 +echo foo bar baz > foo +hg ci -Am 'add foo again' + +hg debugindex .hg/store/data/foo.i diff --git a/tests/test-revlog-packentry.out b/tests/test-revlog-packentry.out new file mode 100644 --- /dev/null +++ b/tests/test-revlog-packentry.out @@ -0,0 +1,6 @@ +adding foo +0 files updated, 0 files merged, 1 files removed, 0 files unresolved +adding foo + rev offset length base linkrev nodeid p1 p2 + 0 0 0 0 0 b80de5d13875 000000000000 000000000000 + 1 0 24 0 1 0376abec49b8 000000000000 000000000000 diff --git a/tests/test-static-http b/tests/test-static-http --- a/tests/test-static-http +++ b/tests/test-static-http @@ -49,4 +49,18 @@ echo '[hooks]' >> .hg/hgrc echo 'changegroup = python ../printenv.py changegroup' >> .hg/hgrc http_proxy= hg pull +echo '% test with "/" URI (issue 747)' +cd .. +hg init +echo a > a +hg add a +hg ci -ma + +http_proxy= hg clone static-http://localhost:20059/ local2 + +cd local2 +hg verify +cat a +hg paths + kill $! diff --git a/tests/test-static-http.out b/tests/test-static-http.out --- a/tests/test-static-http.out +++ b/tests/test-static-http.out @@ -28,3 +28,17 @@ adding manifests adding file changes added 1 changesets with 1 changes to 1 files (run 'hg update' to get a working copy) +% test with "/" URI (issue 747) +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 +checking changesets +checking manifests +crosschecking files in changesets and manifests +checking files +1 files, 1 changesets, 1 total revisions +a +default = static-http://localhost:20059/ diff --git a/tests/test-symlinks b/tests/test-symlinks --- a/tests/test-symlinks +++ b/tests/test-symlinks @@ -72,3 +72,13 @@ hg commit -A -m 'add symlink in a/b/c su echo '2. clone it' cd .. hg clone test testclone + +echo '# git symlink diff' +cd testclone +hg diff --git -r null:tip +hg export --git tip > ../sl.diff +echo '# import git symlink diff' +hg rm a/b/c/demo +hg commit -m'remove link' +hg import ../sl.diff +hg diff --git -r 1:tip diff --git a/tests/test-symlinks.out b/tests/test-symlinks.out --- a/tests/test-symlinks.out +++ b/tests/test-symlinks.out @@ -20,3 +20,20 @@ 1. commit a symlink adding a/b/c/demo 2. clone it 1 files updated, 0 files merged, 0 files removed, 0 files unresolved +# git symlink diff +diff --git a/a/b/c/demo b/a/b/c/demo +new file mode 120000 +--- /dev/null ++++ b/a/b/c/demo +@@ -0,0 +1,1 @@ ++/path/to/symlink/source +\ No newline at end of file +# import git symlink diff +applying ../sl.diff +diff --git a/a/b/c/demo b/a/b/c/demo +new file mode 120000 +--- /dev/null ++++ b/a/b/c/demo +@@ -0,0 +1,1 @@ ++/path/to/symlink/source +\ No newline at end of file diff --git a/tests/test-transplant.out b/tests/test-transplant.out --- a/tests/test-transplant.out +++ b/tests/test-transplant.out @@ -101,17 +101,17 @@ removing toremove adding bar 2 files updated, 0 files merged, 2 files removed, 0 files unresolved applying a1e30dd1b8e7 -foo -Hunk #1 FAILED at 1. +patching file foo +Hunk #1 FAILED at 0 1 out of 1 hunk FAILED -- saving rejects to file foo.rej -patch command failed: exited with status 1 +patch failed to apply abort: Fix up the merge and run hg transplant --continue 1 files updated, 0 files merged, 1 files removed, 0 files unresolved applying a1e30dd1b8e7 -foo -Hunk #1 FAILED at 1. +patching file foo +Hunk #1 FAILED at 0 1 out of 1 hunk FAILED -- saving rejects to file foo.rej -patch command failed: exited with status 1 +patch failed to apply abort: Fix up the merge and run hg transplant --continue a1e30dd1b8e7 transplanted as f1563cf27039 skipping already applied revision 1:a1e30dd1b8e7 diff --git a/tests/test-ui-config b/tests/test-ui-config --- a/tests/test-ui-config +++ b/tests/test-ui-config @@ -1,10 +1,10 @@ #!/usr/bin/env python import ConfigParser -from mercurial import ui, util, cmdutil +from mercurial import ui, util, dispatch testui = ui.ui() -parsed = cmdutil.parseconfig([ +parsed = dispatch._parseconfig([ 'values.string=string value', 'values.bool1=true', 'values.bool2=false',