Mercurial > hg
changeset 4934:8535c1770dd3
merge with crew-stable
author | Thomas Arendsen Hein <thomas@intevation.de> |
---|---|
date | Wed, 18 Jul 2007 16:46:01 +0200 |
parents | 5e89b0dafce5 (diff) 5fb8f5992a3d (current diff) |
children | 7d745e9d2cf0 |
files | hgext/mq.py mercurial/commands.py mercurial/localrepo.py tests/test-mq tests/test-mq.out |
diffstat | 53 files changed, 2706 insertions(+), 390 deletions(-) [+] |
line wrap: on
line diff
--- a/contrib/buildrpm Wed Jul 18 16:41:45 2007 +0200 +++ b/contrib/buildrpm Wed Jul 18 16:46:01 2007 +0200 @@ -29,7 +29,7 @@ version=`hg tags | perl -e 'while(<STDIN>){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(<STDIN>){/^(\S+)\s+(\d+)/;if($1eq"tip"){$t=$2}else{print$t-$2+1;exit}}'` +release=`hg tags | perl -e 'while(<STDIN>){($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 @@ -e 's,^%setup.*,,' \ $specfile > $tmpspec +cat <<EOF >> $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
--- a/contrib/macosx/Readme.html Wed Jul 18 16:41:45 2007 +0200 +++ b/contrib/macosx/Readme.html Wed Jul 18 16:46:01 2007 +0200 @@ -19,10 +19,14 @@ <p class="p2"><br></p> <p class="p3">This is <i>not</i> a stand-alone version of Mercurial.</p> <p class="p2"><br></p> -<p class="p3">To use it, you must have the Universal MacPython 2.4.3 from <a href="http://www.python.org">www.python.org</a> installed.</p> +<p class="p3">To use it, you must have the appropriate version of Universal MacPython from <a href="http://www.python.org">www.python.org</a> installed.</p> <p class="p2"><br></p> -<p class="p3">You can download MacPython 2.4.3 from here:</p> -<p class="p4"><span class="s1"><a href="http://www.python.org/ftp/python/2.4.3/Universal-MacPython-2.4.3-2006-04-07.dmg">http://www.python.org/ftp/python/2.4.3/Universal-MacPython-2.4.3-2006-04-07.dmg</a></span></p> +<p class="p3">You can find more information and download MacPython from here:</p> +<p class="p4"><span class="s1"><a href="http://www.python.org/download">http://www.python.org/download</a></span></p> +<p class="p2"><br></p> +<p class="p3">Or direct links to the latest version are:</p> +<p class="p4"><span class="s1"><a href="http://www.python.org/ftp/python/2.5.1/python-2.5.1-macosx.dmg">Python 2.5.1 for Macintosh OS X</a></span></p> +<p class="p4"><span class="s1"><a href="http://www.python.org/ftp/python/2.4.4/python-2.4.4-macosx2006-10-18.dmg">Python 2.4.4 for Macintosh OS X</a></span></p> <p class="p2"><br></p> <p class="p1"><b>After you install</b></p> <p class="p2"><br></p>
--- a/contrib/mercurial.el Wed Jul 18 16:41:45 2007 +0200 +++ b/contrib/mercurial.el Wed Jul 18 16:46:01 2007 +0200 @@ -1261,9 +1261,22 @@ (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)
--- a/contrib/mercurial.spec Wed Jul 18 16:41:45 2007 +0200 +++ b/contrib/mercurial.spec Wed Jul 18 16:46:01 2007 +0200 @@ -8,6 +8,17 @@ 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 @@ %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*
--- a/contrib/win32/mercurial.ini Wed Jul 18 16:41:45 2007 +0200 +++ b/contrib/win32/mercurial.ini Wed Jul 18 16:46:01 2007 +0200 @@ -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:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/alias.py Wed Jul 18 16:46:01 2007 +0200 @@ -0,0 +1,77 @@ +# Copyright (C) 2007 Brendan Cully <brendan@kublai.com> +# 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)[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: + pui = ui.parentui or ui + pui.setconfig('defaults', cmd, ' '.join(args)) + cmdtable[cmd] = lazycommand(ui, cmd, tcmd)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/children.py Wed Jul 18 16:46:01 2007 +0200 @@ -0,0 +1,41 @@ +# Mercurial extension to provide the 'hg children' command +# +# Copyright 2007 by Intevation GmbH <intevation@intevation.de> +# Author(s): +# Thomas Arendsen Hein <thomas@intevation.de> +# +# 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]')), +}
--- a/hgext/convert/__init__.py Wed Jul 18 16:41:45 2007 +0200 +++ b/hgext/convert/__init__.py Wed Jul 18 16:46:01 2007 +0200 @@ -5,27 +5,40 @@ # 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 subversion import convert_svn -import os +import os, shutil from mercurial import hg, ui, util, commands commands.norepo += " convert" -converters = [convert_cvs, convert_git, convert_mercurial] +converters = [convert_cvs, convert_git, convert_svn, convert_mercurial] -def converter(ui, path): +def convertsource(ui, path, **opts): + for c in converters: + if not hasattr(c, 'getcommit'): + continue + try: + return c(ui, path, **opts) + except NoRepo: + pass + raise util.Abort('%s: unknown repository type' % path) + +def convertsink(ui, path): if not os.path.isdir(path): raise util.Abort("%s: not a directory" % path) for c in converters: + if not hasattr(c, 'putcommit'): + continue try: return c(ui, path) except 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): @@ -179,6 +192,8 @@ def copy(self, rev): c = self.commitcache[rev] files = self.source.getchanges(rev) + + do_copies = (hasattr(c, 'copies') and hasattr(self.dest, 'copyfile')) for f, v in files: try: @@ -188,6 +203,11 @@ else: e = self.source.getmode(f, v) self.dest.putfile(f, e, data) + if do_copies: + if f in c.copies: + # Merely marks that a copy happened. + self.dest.copyfile(c.copies[f], f) + r = [self.map[v] for v in c.parents] f = [f for f, v in files] @@ -196,6 +216,7 @@ def convert(self): try: + self.source.setrevmap(self.map) self.ui.status("scanning source...\n") heads = self.source.getheads() parents = self.walktree(heads) @@ -244,21 +265,26 @@ 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. + basename of the source with \'-hg\' appended. If the destination + repository doesn\'t exist, it will be created. - If <mapfile> isn't given, it will be put in a default location + If <mapfile> isn\'t given, it will be put in a default location (<dest>/.hg/shamap by default). The <mapfile> is a simple text file that maps each source commit ID to the destination ID for that revision, like so: <source ID> <destination ID> - If the file doesn't exist, it's automatically created. It's updated + If the file doesn\'t exist, it\'s automatically created. It\'s updated on each commit copied, so convert-repo can be interrupted and can be run repeatedly to copy new commits. @@ -271,15 +297,12 @@ 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,15 +317,22 @@ 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) + + try: + srcc = convertsource(ui, src, rev=opts.get('rev')) + except Exception: + if created: + shutil.rmtree(dest, True) + raise if not mapfile: try: @@ -317,6 +347,7 @@ "convert": (_convert, [('A', 'authors', '', 'username mapping filename'), + ('r', 'rev', '', 'import up to target revision REV'), ('', 'datesort', None, 'try to sort changesets by date')], 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'), }
--- a/hgext/convert/common.py Wed Jul 18 16:41:45 2007 +0200 +++ b/hgext/convert/common.py Wed Jul 18 16:46:01 2007 +0200 @@ -4,18 +4,31 @@ class commit(object): def __init__(self, **parts): + self.rev = None + self.branch = None + 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) + if not self.desc or self.desc.isspace(): + self.desc = '*** empty log message ***' 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""" @@ -44,6 +57,18 @@ """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' + + 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"""
--- a/hgext/convert/cvs.py Wed Jul 18 16:41:45 2007 +0200 +++ b/hgext/convert/cvs.py Wed Jul 18 16:46:01 2007 +0200 @@ -6,9 +6,9 @@ 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,32 @@ 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) + 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 +79,6 @@ 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 +100,8 @@ rev = l[colon+1:-2] rev = rev.split("->")[1] files[file] = rev + elif state == 3: + continue self.heads = self.lastbranch.values() finally: @@ -235,9 +252,6 @@ cl.sort() return cl - def recode(self, text): - return text.decode(self.encoding, "replace").encode("utf-8") - def getcommit(self, rev): return self.changeset[rev]
--- a/hgext/convert/git.py Wed Jul 18 16:41:45 2007 +0200 +++ b/hgext/convert/git.py Wed Jul 18 16:46:01 2007 +0200 @@ -4,32 +4,29 @@ 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): + def gitcmd(self, s): + return os.popen('GIT_DIR=%s %s' % (self.path, s)) -class convert_git(converter_source): - 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 = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path) - 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() - fh = os.popen("GIT_DIR=%s git-cat-file %s %s 2>/dev/null" - % (self.path, type, rev)) + fh = self.gitcmd("git-cat-file %s %s 2>/dev/null" % (type, rev)) return fh.read() def getfile(self, name, rev): @@ -40,8 +37,7 @@ def getchanges(self, version): self.modecache = {} - fh = os.popen("GIT_DIR=%s git-diff-tree --root -m -r %s" - % (self.path, version)) + fh = self.gitcmd("git-diff-tree --root -m -r %s" % version) changes = [] for l in fh: if "\t" not in l: continue @@ -58,7 +54,7 @@ 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 = [] @@ -69,13 +65,13 @@ 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) @@ -84,12 +80,13 @@ 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): tags = {} - fh = os.popen('git-ls-remote --tags "%s" 2>/dev/null' % self.path) + fh = self.gitcmd('git-ls-remote --tags "%s" 2>/dev/null' % self.path) prefix = 'refs/tags/' for line in fh: line = line.strip()
--- a/hgext/convert/hg.py Wed Jul 18 16:41:45 2007 +0200 +++ b/hgext/convert/hg.py Wed Jul 18 16:46:01 2007 +0200 @@ -29,6 +29,9 @@ if self.repo.dirstate.state(f) == '?': self.repo.dirstate.update([f], "a") + def copyfile(self, source, dest): + self.repo.copy(source, dest) + def delfile(self, f): try: os.unlink(self.repo.wjoin(f)) @@ -51,11 +54,11 @@ text = commit.desc extra = {} - try: - extra["branch"] = commit.branch - except AttributeError: - pass - + if commit.branch: + extra['branch'] = commit.branch + if commit.rev: + extra['convert_revision'] = commit.rev + while parents: p1 = p2 p2 = parents.pop(0)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/convert/subversion.py Wed Jul 18 16:46:01 2007 +0200 @@ -0,0 +1,623 @@ +# 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 pprint +import locale + +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 + +try: + from svn.core import SubversionException, Pool + import svn.core + import svn.ra + import svn.delta + import svn + import transport +except ImportError: + pass + +class CompatibilityException(Exception): pass + +# 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 + if rev: + try: + latest = int(rev) + except ValueError: + raise util.Abort('svn: revision %s is not an integer' % rev) + 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 = url + self.encoding = 'UTF-8' # Subversion is always nominal UTF-8 + try: + self.transport = transport.SvnRaTransport(url = url) + self.ra = self.transport.ra + self.ctx = svn.client.create_context() + self.base = svn.ra.get_repos_root(self.ra) + self.module = self.url[len(self.base):] + self.modulemap = {} # revision, module + self.commits = {} + self.files = {} + self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding) + except SubversionException, e: + raise NoRepo("couldn't open SVN repo %s" % url) + + 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 + optrev = svn.core.svn_opt_revision_t() + optrev.kind = svn.core.svn_opt_revision_number + optrev.value.number = 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, optrev) and self.exists(branches, optrev): + 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, optrev, 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 = {} + files = self.files[rev] + cl = files + cl.sort() + # caller caches the result, so free it here to release memory + del self.files[rev] + return cl + + 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 gettags(self): + tags = {} + def parselogentry(*arg, **args): + orig_paths, revnum, author, date, message, pool = arg + 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) + + start = self.revnum(self.head) + try: + svn.ra.get_log(self.ra, ['/tags'], 0, start, 0, True, False, + parselogentry) + return tags + except SubversionException: + self.ui.note('no tags found at revision %d\n' % start) + return {} + + # -- helper functions -- + + def revid(self, revnum, module=None): + if not module: + module = self.module + return (u"svn:%s%s@%s" % (self.uuid, module, revnum)).decode(self.encoding) + + 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: + print self.base, path + 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 = 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 _fetch_revisions(self, from_revnum = 0, to_revnum = 347): + 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('Ignoring %r since it is not under %r\n' % (path, module)) + return None + + received = [] + # svn.ra.get_log requires no other calls to the ra until it completes, + # so we just collect the log entries and parse them afterwards + def receivelog(*arg, **args): + received.append(arg) + + self.child_cset = None + def parselogentry(*arg, **args): + orig_paths, revnum, author, date, message, pool = arg + + if self.is_blacklisted(revnum): + self.ui.note('skipping blacklisted revision %d\n' % revnum) + return + + self.ui.debug("parsing revision %d\n" % revnum) + + if orig_paths is None: + self.ui.debug('revision %d has no entries\n' % revnum) + return + + if revnum in self.modulemap: + new_module = self.modulemap[revnum] + if new_module != self.module: + self.module = new_module + self.reparent(self.module) + + copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions. + copies = {} + entries = [] + rev = self.revid(revnum) + parents = [] + + # 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 + + try: + branch = self.module.split("/")[-1] + if branch == 'trunk': + branch = '' + except IndexError: + branch = None + + paths = orig_paths.keys() + paths.sort() + for path in paths: + # self.ui.write("path %s\n" % path) + if path == self.module: # Follow branching back in history + ent = orig_paths[path] + if ent: + 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") + # Maybe it was added and there is no more history. + entrypath = get_entry_from_path(path, module=self.module) + # self.ui.write("entrypath %s\n" % entrypath) + if entrypath is None: + # Outside our area of interest + self.ui.debug("boring@%s: %s\n" % (revnum, path)) + continue + entry = entrypath.decode(self.encoding) + ent = orig_paths[path] + + 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 + + self.modulemap[revnum] = self.module # track backwards in time + # a list of (filename, id) where id lets us retrieve the file. + # eg in git, id is the object hash. for svn it'll be the + self.files[rev] = zip(entries, [rev] * len(entries)) + if not entries: + return + + # 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 '' + + cset = commit(author=author, + date=util.datestr(date), + desc=log, + parents=parents, + copies=copies, + 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: + discover_changed_paths = True + strict_node_history = False + svn.ra.get_log(self.ra, [self.module], from_revnum, to_revnum, 0, + discover_changed_paths, strict_node_history, + receivelog) + for entry in received: + parselogentry(*entry) + except SubversionException, (_, 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("/") + + def _find_children_fallback(path, revnum): + # SWIG python bindings for getdir are broken up to at least 1.4.3 + pool = Pool() + optrev = svn.core.svn_opt_revision_t() + optrev.kind = svn.core.svn_opt_revision_number + optrev.value.number = revnum + rpath = '/'.join([self.base, path]).strip('/') + return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev, True, self.ctx, pool).keys()] + + if hasattr(self, '_find_children_fallback'): + return _find_children_fallback(path, revnum) + + self.reparent("/" + path) + pool = Pool() + + children = [] + def find_children_inner(children, path, revnum = revnum): + if hasattr(svn.ra, 'get_dir2'): # Since SVN 1.4 + fields = 0xffffffff # Binding does not provide SVN_DIRENT_ALL + getdir = svn.ra.get_dir2(self.ra, path, revnum, fields, pool) + else: + getdir = svn.ra.get_dir(self.ra, path, revnum, pool) + if type(getdir) == dict: + # python binding for getdir is broken up to at least 1.4.3 + raise CompatibilityException() + dirents = getdir[0] + if type(dirents) == int: + # got here once due to infinite recursion bug + # pprint.pprint(getdir) + return + c = dirents.keys() + c.sort() + for child in c: + dirent = dirents[child] + if dirent.kind == svn.core.svn_node_dir: + find_children_inner(children, (path + "/" + child).strip("/")) + else: + children.append((path + "/" + child).strip("/")) + + try: + find_children_inner(children, "") + except CompatibilityException: + self._find_children_fallback = True + self.reparent(self.module) + return _find_children_fallback(path, revnum) + + self.reparent(self.module) + return [path + "/" + c for c in children]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/convert/transport.py Wed Jul 18 16:46:01 2007 +0200 @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2007 Daniel Holth <dholth@fastmail.fm> +# This is a stripped-down version of the original bzr-svn transport.py, +# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org> + +# 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.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), + ] + return svn.core.svn_auth_open(providers, pool) + + +# # The SVN libraries don't like trailing slashes... +# return url.rstrip('/') + + +class SvnRaCallbacks(svn.ra.callbacks2_t): + """Remote access callbacks implementation for bzr-svn.""" + def __init__(self, pool): + svn.ra.callbacks2_t.__init__(self) + self.auth_baton = _create_auth_baton(pool) + self.pool = pool + + def open_tmp_file(self, pool): + return mktemp(prefix='tailor-svn') + +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 + + # Only Subversion 1.4 has reparent() + if ra is None or not hasattr(svn.ra, 'reparent'): + self.callbacks = SvnRaCallbacks(self.pool) + try: + ver = svn.ra.version() + try: # Older SVN bindings + self.ra = svn.ra.open2(self.svn_url.encode('utf8'), self.callbacks, None, svn_config, None) + except TypeError, e: + self.ra = svn.ra.open2(self.svn_url.encode('utf8'), self.callbacks, svn_config, None) + except SubversionException, (_, num): + if num == svn.core.SVN_ERR_RA_ILLEGAL_URL: + raise NotBranchError(url) + if num == svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED: + raise NotBranchError(url) + if num == 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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/interhg.py Wed Jul 18 16:46:01 2007 +0200 @@ -0,0 +1,64 @@ +# interhg.py - interhg +# +# Copyright 2007 OHASHI Hideya <ohachige@gmail.com> +# +# 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 = +# +# This is an example to link to a bug tracking system. +# +# [interhg] +# pat1 = s/issue(\d+)/ <a href="http:\/\/bts\/issue\1">issue\1<\/a> / +# +# You can add patterns to use pat2, pat3, ... +# For exapmle. +# +# pat2 = s/(^|\s)#(\d+)\b/ <b>#\2<\/b> / + +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 pat in interhg_table: + regexp = pat[0] + format = pat[1] + 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[:] = [] + num = 1 + while True: + key = 'pat%d' % num + pat = self.config('interhg', key) + if pat == None: + break + pat = pat[2:-1] + span = re.search(r'[^\\]/', pat).span() + regexp = pat[:span[0] + 1] + format = pat[span[1]:] + format = re.sub(r'\\/', '/', format) + regexp = re.compile(regexp) + interhg_table.append((regexp, format)) + num += 1 + return orig_refresh(self) + +hgweb_mod.hgweb.refresh = interhg_refresh
--- a/hgext/mq.py Wed Jul 18 16:41:45 2007 +0200 +++ b/hgext/mq.py Wed Jul 18 16:46:01 2007 +0200 @@ -1487,11 +1487,20 @@ Source patch repository is looked for in <src>/.hg/patches by default. Use -p <url> to change. + + The patch directory must be a nested mercurial repository, as + would be created by qinit -c. ''' cmdutil.setremoteconfig(ui, opts) if dest is None: dest = hg.defaultdest(source) sr = hg.repository(ui, ui.expandpath(source)) + patchdir = opts['patches'] or (sr.url() + '/.hg/patches') + try: + pr = hg.repository(ui, patchdir) + 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:
--- a/hgext/patchbomb.py Wed Jul 18 16:41:45 2007 +0200 +++ b/hgext/patchbomb.py Wed Jul 18 16:46:01 2007 +0200 @@ -306,8 +306,12 @@ 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 @@ ('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')),
--- a/hgext/win32text.py Wed Jul 18 16:41:45 2007 +0200 +++ b/hgext/win32text.py Wed Jul 18 16:46:01 2007 +0200 @@ -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 @@ return dumbencode(s, cmd) return s -mercurial.util.filtertable.update({ +util.filtertable.update({ 'dumbdecode:': dumbdecode, 'dumbencode:': dumbencode, 'cleverdecode:': cleverdecode,
--- a/hgmerge Wed Jul 18 16:41:45 2007 +0200 +++ b/hgmerge Wed Jul 18 16:46:01 2007 +0200 @@ -96,6 +96,20 @@ 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 @@ # 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 @@ # 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 @@ -158,7 +172,7 @@ $EDITOR "$LOCAL" || failure # 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
--- a/mercurial/changelog.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/changelog.py Wed Jul 18 16:46:01 2007 +0200 @@ -131,7 +131,10 @@ 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):
--- a/mercurial/commands.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/commands.py Wed Jul 18 16:46:01 2007 +0200 @@ -70,19 +70,31 @@ 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, @@ -92,15 +104,15 @@ 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): @@ -130,7 +142,10 @@ The default is the basename of the archive, with suffixes removed. ''' - node = repo.changectx(opts['rev']).node() + ctx = repo.changectx(opts['rev']) + if not ctx: + raise util.Abort(_('repository has no revisions')) + node = ctx.node() dest = cmdutil.make_filename(repo, dest, node) if os.path.realpath(dest) == repo.root: raise util.Abort(_('repository root cannot be destination')) @@ -835,7 +850,7 @@ '''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() @@ -2463,7 +2478,7 @@ 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])) @@ -2771,8 +2786,10 @@ ('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')), @@ -3072,7 +3089,8 @@ ('', '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, @@ -3123,6 +3141,8 @@ "version": (version_, [], _('hg version')), } +extensions.commandtable = table + norepo = ("clone init version help debugancestor debugcomplete debugdata" " debugindex debugindexdot debugdate debuginstall") optionalrepo = ("paths serve showconfig")
--- a/mercurial/context.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/context.py Wed Jul 18 16:46:01 2007 +0200 @@ -184,7 +184,7 @@ 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 +240,32 @@ 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]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/diffhelpers.c Wed Jul 18 16:46:01 2007 +0200 @@ -0,0 +1,150 @@ +/* + * diffhelpers.c - helper routines for mpatch + * + * Copyright 2007 Chris Mason <chris.mason@oracle.com> + * + * This software may be used and distributed according to the terms + * of the GNU General Public License v2, incorporated herein by reference. + */ + +#include <Python.h> +#include <stdlib.h> +#include <string.h> + +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); +} +
--- a/mercurial/extensions.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/extensions.py Wed Jul 18 16:46:01 2007 +0200 @@ -6,10 +6,12 @@ # 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 = {} +commandtable = {} +setuphooks = [] def find(name): '''return module with given extension name''' @@ -54,13 +56,13 @@ uisetup(ui) reposetup = getattr(mod, 'reposetup', None) if reposetup: - hg.repo_setup_hooks.append(reposetup) + setuphooks.append(reposetup) cmdtable = getattr(mod, 'cmdtable', {}) - overrides = [cmd for cmd in cmdtable if cmd in commands.table] + overrides = [cmd for cmd in cmdtable if cmd in commandtable] if overrides: ui.warn(_("extension '%s' overrides commands: %s\n") % (name, " ".join(overrides))) - commands.table.update(cmdtable) + commandtable.update(cmdtable) def loadall(ui): result = ui.configitems("extensions")
--- a/mercurial/hg.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/hg.py Wed Jul 18 16:46:01 2007 +0200 @@ -10,7 +10,7 @@ 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, cmdutil, extensions import merge as _merge import verify as _verify @@ -21,13 +21,11 @@ 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,13 +48,11 @@ 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: + for hook in extensions.setuphooks: hook(ui, repo) return repo
--- a/mercurial/hgweb/hgweb_mod.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/hgweb/hgweb_mod.py Wed Jul 18 16:46:01 2007 +0200 @@ -64,7 +64,7 @@ class hgweb(object): def __init__(self, repo, name=None): - if type(repo) == type(""): + if isinstance(repo, str): self.repo = hg.repository(ui.ui(report_untrusted=False), repo) else: self.repo = repo @@ -787,9 +787,17 @@ 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 +1071,7 @@ # 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'
--- a/mercurial/hgweb/hgwebdir_mod.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/hgweb/hgwebdir_mod.py Wed Jul 18 16:46:01 2007 +0200 @@ -90,8 +90,12 @@ 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 += '/' @@ -118,7 +122,7 @@ 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'): @@ -134,6 +138,10 @@ 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')) @@ -185,6 +193,25 @@ 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/'): @@ -193,25 +220,32 @@ 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") @@ -219,22 +253,6 @@ 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
--- a/mercurial/hgweb/server.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/hgweb/server.py Wed Jul 18 16:46:01 2007 +0200 @@ -37,6 +37,9 @@ 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 @@ 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 @@ 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 @@ 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 @@ -176,6 +207,7 @@ port = int(repo.ui.config("web", "port", 8000)) use_ipv6 = repo.ui.configbool("web", "ipv6") webdir_conf = repo.ui.config("web", "webdir_conf") + ssl_cert = repo.ui.config("web", "certificate") accesslog = openlog(repo.ui.config("web", "accesslog", "-"), sys.stdout) errorlog = openlog(repo.ui.config("web", "errorlog", "-"), sys.stderr) @@ -222,6 +254,19 @@ 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) @@ -230,10 +275,15 @@ 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])
--- a/mercurial/hgweb/wsgicgi.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/hgweb/wsgicgi.py Wed Jul 18 16:46:01 2007 +0200 @@ -23,7 +23,7 @@ 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'
--- a/mercurial/httprepo.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/httprepo.py Wed Jul 18 16:46:01 2007 +0200 @@ -409,9 +409,6 @@ 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)
--- a/mercurial/localrepo.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/localrepo.py Wed Jul 18 16:46:01 2007 +0200 @@ -109,7 +109,8 @@ 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: @@ -158,7 +159,8 @@ if use_dirstate and self.dirstate.state('.hgtags') == '?': 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)
--- a/mercurial/patch.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/patch.py Wed Jul 18 16:46:01 2007 +0200 @@ -1,16 +1,23 @@ # patch.py - patch file parsing routines # # Copyright 2006 Brendan Cully <brendan@kublai.com> +# Copyright 2007 Chris Mason <chris.mason@oracle.com> # # 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 @@ 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 @@ 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 @@ 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 @@ # 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 @@ 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_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): """extract git-style metadata about patches from <patchname>""" class gitpatch: "op is one of ADD, DELETE, RENAME, MODIFY or COPY" @@ -148,16 +155,20 @@ self.lineno = 0 self.binary = False + def reader(fp, firstline): + 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) @@ -204,157 +215,790 @@ 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 <patchname> 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 <patcher> to apply <patchname> 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(patchname, ui, strip, cwd, files): + """use builtin patch to apply <patchname> to the working directory. + returns whether patch was applied with fuzz factor.""" + fp = file(patchname, 'rb') + 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 <patchname> 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):
--- a/mercurial/statichttprepo.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/statichttprepo.py Wed Jul 18 16:46:01 2007 +0200 @@ -75,10 +75,4 @@ 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:])
--- a/mercurial/util.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/util.py Wed Jul 18 16:46:01 2007 +0200 @@ -63,7 +63,7 @@ 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.
--- a/mercurial/util_win32.py Wed Jul 18 16:41:45 2007 +0200 +++ b/mercurial/util_win32.py Wed Jul 18 16:46:01 2007 +0200 @@ -209,9 +209,9 @@ def __init__(self, name, mode='rb'): access = 0 - if 'r' in mode or '+' in mode: + if 'r' in mode: access |= win32file.GENERIC_READ - if 'w' in mode or 'a' in mode: + if 'w' in mode or 'a' in mode or '+' in mode: access |= win32file.GENERIC_WRITE if 'r' in mode: creation = win32file.OPEN_EXISTING
--- a/setup.py Wed Jul 18 16:41:45 2007 +0200 +++ b/setup.py Wed Jul 18 16:46:01 2007 +0200 @@ -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'): @@ -64,7 +64,8 @@ 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')],
--- a/templates/gitweb/map Wed Jul 18 16:41:45 2007 +0200 +++ b/templates/gitweb/map Wed Jul 18 16:46:01 2007 +0200 @@ -5,6 +5,7 @@ changelog = changelog.tmpl summary = summary.tmpl error = error.tmpl +notfound = notfound.tmpl naventry = '<a href="{url}log/{node|short}{sessionvars%urlparameter}">{label|escape}</a> ' navshortentry = '<a href="{url}shortlog/{node|short}{sessionvars%urlparameter}">{label|escape}</a> ' filenaventry = '<a href="{url}log/{node|short}/{file|urlescape}{sessionvars%urlparameter}">{label|escape}</a> '
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/gitweb/notfound.tmpl Wed Jul 18 16:46:01 2007 +0200 @@ -0,0 +1,19 @@ +{header} +<title>Mercurial repositories index</title> +</head> + +<body> + +<div class="page_header"> +<a href="http://www.selenic.com/mercurial/" title="Mercurial"><div + style="float:right;">Mercurial</div></a> Not found: {repo|escape} +</div> + +<div class="page_body"> +The specified repository "{repo|escape}" is unknown, sorry. +<br/> +<br/> +Please go back to the <a href="/">main repository list page</a>. +</div> + +{footer}
--- a/tests/coverage.py Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/coverage.py Wed Jul 18 16:46:01 2007 +0200 @@ -504,7 +504,7 @@ 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-alias Wed Jul 18 16:46:01 2007 +0200 @@ -0,0 +1,32 @@ +#!/bin/sh + +cat > $HGRCPATH <<EOF +[extensions] +alias= + +[alias] +myinit = init +cleanstatus = status -c +unknown = bargle +ambiguous = s +recursive = recursive +EOF + +echo '% basic' +hg myinit alias + +echo '% unknown' +hg unknown + +echo '% ambiguous' +hg ambiguous + +echo '% recursive' +hg recursive + +cd alias +echo foo > foo +hg ci -Amfoo + +echo '% with opts' +hg cleanst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-alias.out Wed Jul 18 16:46:01 2007 +0200 @@ -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
--- a/tests/test-annotate Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-annotate Wed Jul 18 16:46:01 2007 +0200 @@ -12,18 +12,27 @@ 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 <<EOF >>a a a @@ -32,28 +41,34 @@ hg cp a b hg ci -mb -d '1 0' cat <<EOF >> 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 <<EOF >> 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 @@ hg ci -mc -d '3 0' hg merge cat <<EOF >> 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
--- a/tests/test-annotate.out Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-annotate.out Wed Jul 18 16:46:01 2007 +0200 @@ -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 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 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
--- a/tests/test-archive Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-archive Wed Jul 18 16:46:01 2007 +0200 @@ -63,7 +63,14 @@ 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' fi + +echo '% empty repo' +hg init ../empty +cd ../empty +hg archive ../test-empty +exit 0
--- a/tests/test-archive.out Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-archive.out Wed Jul 18 16:46:01 2007 +0200 @@ -39,3 +39,5 @@ test-TIP/baz/bletch test-TIP/foo rev-0.tar created +% empty repo +abort: repository has no revisions
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-children Wed Jul 18 16:46:01 2007 +0200 @@ -0,0 +1,59 @@ +#!/bin/sh +# test children command + +cat <<EOF >> $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 +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-children.out Wed Jul 18 16:46:01 2007 +0200 @@ -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 +
--- a/tests/test-debugcomplete.out Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-debugcomplete.out Wed Jul 18 16:46:01 2007 +0200 @@ -110,6 +110,7 @@ % Show the options for the "serve" command --accesslog --address +--certificate --config --cwd --daemon
--- a/tests/test-import Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-import Wed Jul 18 16:46:01 2007 +0200 @@ -93,6 +93,24 @@ hg --cwd b tip | grep second rm -r b +# subject: duplicate detection, removal of [PATCH] +cat > mkmsg2.py <<EOF +import email.Message, sys +msg = email.Message.Message() +msg.set_payload('email patch\n\nnext line\n' + open('tip.patch').read()) +msg['Subject'] = '[PATCH] email patch' +msg['From'] = 'email patcher' +sys.stdout.write(msg.as_string()) +EOF + +echo '% plain diff in email, [PATCH] subject, message body with subject' +hg clone -r0 a b +hg --cwd a diff -r0:1 > 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
--- a/tests/test-import.out Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-import.out Wed Jul 18 16:46:01 2007 +0200 @@ -100,6 +100,17 @@ 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
--- a/tests/test-mq Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-mq Wed Jul 18 16:46:01 2007 +0200 @@ -370,10 +370,17 @@ echo foo > 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 ..
--- a/tests/test-mq.out Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-mq.out Wed Jul 18 16:46:01 2007 +0200 @@ -262,7 +262,8 @@ 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 @@ -409,6 +410,8 @@ 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
--- a/tests/test-transplant.out Wed Jul 18 16:41:45 2007 +0200 +++ b/tests/test-transplant.out Wed Jul 18 16:46:01 2007 +0200 @@ -101,17 +101,17 @@ 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