# HG changeset patch # User Patrick Mezard # Date 1183661424 -7200 # Node ID 79210a63f4521bc4c478268cac989c3e8d59b96e # Parent 6aa1fae4c28a29cac3bd38b262c43ec603beae85# Parent beb774707c527363d9dbb728460029dae192a66b Merge with crew-stable diff --git a/contrib/buildrpm b/contrib/buildrpm --- a/contrib/buildrpm +++ b/contrib/buildrpm @@ -29,7 +29,7 @@ tmpspec=/tmp/`basename "$specfile"`.$$ version=`hg tags | perl -e 'while(){if(/^(\d\S+)/){print$1;exit}}'` # Compute the release number as the difference in revision numbers # between the tip and the most recent tag. -release=`hg tags | perl -e 'while(){/^(\S+)\s+(\d+)/;if($1eq"tip"){$t=$2}else{print$t-$2+1;exit}}'` +release=`hg tags | perl -e 'while(){($tag,$id)=/^(\S+)\s+(\d+)/;if($tag eq "tip"){$tip = $id}elsif($tag=~/^\d/){print $tip-$id+1;exit}}'` tip=`hg -q tip` # Beat up the spec file @@ -40,6 +40,19 @@ sed -e 's,^Source:.*,Source: /dev/null,' -e 's,^%setup.*,,' \ $specfile > $tmpspec +cat <> $tmpspec +%changelog +* `date +'%a %b %d %Y'` `hg showconfig ui.username` $version-$release +- Automatically built via $0 + +EOF +hg log \ + --template '* {date|rfc822date} {author}\n- {desc|firstline}\n\n' \ + .hgtags \ + | sed -e 's/^\(\* [MTWFS][a-z][a-z]\), \([0-3][0-9]\) \([A-Z][a-z][a-z]\) /\1 \3 \2 /' \ + -e '/^\* [MTWFS][a-z][a-z] /{s/ [012][0-9]:[0-9][0-9]:[0-9][0-9] [+-][0-9]\{4\}//}' \ + >> $tmpspec + rpmbuild --define "_topdir $rpmdir" -bb $tmpspec if [ $? = 0 ]; then rm -rf $tmpspec $rpmdir/BUILD diff --git a/contrib/macosx/Readme.html b/contrib/macosx/Readme.html --- a/contrib/macosx/Readme.html +++ b/contrib/macosx/Readme.html @@ -19,10 +19,14 @@


This is not a stand-alone version of Mercurial.


-

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

+

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


-

You can download MacPython 2.4.3 from here:

-

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

+

You can find more information and download MacPython from here:

+

http://www.python.org/download

+


+

Or direct links to the latest version are:

+

Python 2.5.1 for Macintosh OS X

+

Python 2.4.4 for Macintosh OS X


After you install


diff --git a/contrib/mercurial.spec b/contrib/mercurial.spec old mode 100644 new mode 100755 --- a/contrib/mercurial.spec +++ b/contrib/mercurial.spec @@ -8,6 +8,17 @@ Source: http://www.selenic.com/mercurial URL: http://www.selenic.com/mercurial BuildRoot: /tmp/build.%{name}-%{version}-%{release} +# From the README: +# +# Note: some distributions fails to include bits of distutils by +# default, you'll need python-dev to install. You'll also need a C +# compiler and a 3-way merge tool like merge, tkdiff, or kdiff3. +# +# python-devel provides an adequate python-dev. The merge tool is a +# run-time dependency. +# +BuildRequires: python >= 2.3, python-devel, make, gcc, asciidoc, xmlto + %define pythonver %(python -c 'import sys;print ".".join(map(str, sys.version_info[:2]))') %define pythonlib %{_libdir}/python%{pythonver}/site-packages/%{name} %define hgext %{_libdir}/python%{pythonver}/site-packages/hgext @@ -21,23 +32,51 @@ rm -rf $RPM_BUILD_ROOT %setup -q %build -python setup.py build +make all %install -python setup.py install --root $RPM_BUILD_ROOT +python setup.py install --root $RPM_BUILD_ROOT --prefix %{_prefix} +make install-doc DESTDIR=$RPM_BUILD_ROOT MANDIR=%{_mandir} + +install contrib/hgk $RPM_BUILD_ROOT%{_bindir} +install contrib/convert-repo $RPM_BUILD_ROOT%{_bindir}/mercurial-convert-repo +install contrib/hg-ssh $RPM_BUILD_ROOT%{_bindir} +install contrib/git-viz/{hg-viz,git-rev-tree} $RPM_BUILD_ROOT%{_bindir} + +bash_completion_dir=$RPM_BUILD_ROOT%{_sysconfdir}/bash_completion.d +mkdir -p $bash_completion_dir +install contrib/bash_completion $bash_completion_dir/mercurial.sh + +zsh_completion_dir=$RPM_BUILD_ROOT%{_datadir}/zsh/site-functions +mkdir -p $zsh_completion_dir +install contrib/zsh_completion $zsh_completion_dir/_mercurial + +lisp_dir=$RPM_BUILD_ROOT%{_datadir}/emacs/site-lisp +mkdir -p $lisp_dir +install contrib/mercurial.el $lisp_dir %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-,root,root,-) -%doc doc/* *.cgi +%doc CONTRIBUTORS COPYING doc/README doc/hg*.txt doc/hg*.html doc/ja *.cgi +%{_mandir}/man?/hg*.gz %dir %{pythonlib} %dir %{hgext} +%{_sysconfdir}/bash_completion.d/mercurial.sh +%{_datadir}/zsh/site-functions/_mercurial +%{_datadir}/emacs/site-lisp/mercurial.el +%{_bindir}/hg +%{_bindir}/hgk %{_bindir}/hgmerge -%{_bindir}/hg +%{_bindir}/hg-ssh +%{_bindir}/hg-viz +%{_bindir}/git-rev-tree +%{_bindir}/mercurial-convert-repo %{pythonlib}/templates %{pythonlib}/*.py* %{pythonlib}/hgweb/*.py* %{pythonlib}/*.so %{hgext}/*.py* +%{hgext}/convert/*.py* diff --git a/contrib/win32/mercurial.ini b/contrib/win32/mercurial.ini --- a/contrib/win32/mercurial.ini +++ b/contrib/win32/mercurial.ini @@ -1,41 +1,41 @@ -; System-wide Mercurial config file. To override these settings on a -; per-user basis, please edit the following file instead, where -; USERNAME is your Windows user name: -; C:\Documents and Settings\USERNAME\Mercurial.ini - -[ui] -editor = notepad - -; By default, we try to encode and decode all files that do not -; contain ASCII NUL characters. What this means is that we try to set -; line endings to Windows style on update, and to Unix style on -; commit. This lets us cooperate with Linux and Unix users, so -; everybody sees files with their native line endings. - -[extensions] -; The win32text extension is available and installed by default. It -; provides built-in Python hooks to perform line ending conversions. -; This is normally much faster than running an external program. -hgext.win32text = - - -[encode] -; Encode files that don't contain NUL characters. - -; ** = cleverencode: - -; Alternatively, you can explicitly specify each file extension that -; you want encoded (any you omit will be left untouched), like this: - -; *.txt = dumbencode: - - -[decode] -; Decode files that don't contain NUL characters. - -; ** = cleverdecode: - -; Alternatively, you can explicitly specify each file extension that -; you want decoded (any you omit will be left untouched), like this: - -; **.txt = dumbdecode: +; System-wide Mercurial config file. To override these settings on a +; per-user basis, please edit the following file instead, where +; USERNAME is your Windows user name: +; C:\Documents and Settings\USERNAME\Mercurial.ini + +[ui] +editor = notepad + +; By default, we try to encode and decode all files that do not +; contain ASCII NUL characters. What this means is that we try to set +; line endings to Windows style on update, and to Unix style on +; commit. This lets us cooperate with Linux and Unix users, so +; everybody sees files with their native line endings. + +[extensions] +; The win32text extension is available and installed by default. It +; provides built-in Python hooks to perform line ending conversions. +; This is normally much faster than running an external program. +hgext.win32text = + + +[encode] +; Encode files that don't contain NUL characters. + +; ** = cleverencode: + +; Alternatively, you can explicitly specify each file extension that +; you want encoded (any you omit will be left untouched), like this: + +; *.txt = dumbencode: + + +[decode] +; Decode files that don't contain NUL characters. + +; ** = cleverdecode: + +; Alternatively, you can explicitly specify each file extension that +; you want decoded (any you omit will be left untouched), like this: + +; **.txt = dumbdecode: diff --git a/doc/Makefile b/doc/Makefile old mode 100644 new mode 100755 --- a/doc/Makefile +++ b/doc/Makefile @@ -36,8 +36,8 @@ MANIFEST: man html install: man for i in $(MAN) ; do \ subdir=`echo $$i | sed -n 's/..*\.\([0-9]\)$$/man\1/p'` ; \ - mkdir -p $(DESTDIR)/$(MANDIR)/$$subdir ; \ - $(INSTALL) $$i $(DESTDIR)/$(MANDIR)/$$subdir ; \ + mkdir -p $(DESTDIR)$(MANDIR)/$$subdir ; \ + $(INSTALL) $$i $(DESTDIR)$(MANDIR)/$$subdir ; \ done clean: diff --git a/hgext/alias.py b/hgext/alias.py new file mode 100644 --- /dev/null +++ b/hgext/alias.py @@ -0,0 +1,78 @@ +# Copyright (C) 2007 Brendan Cully +# This file is published under the GNU GPL. + +'''allow user-defined command aliases + +To use, create entries in your hgrc of the form + +[alias] +mycmd = cmd --args +''' + +from mercurial.cmdutil import findcmd, UnknownCommand, AmbiguousCommand +from mercurial import commands + +cmdtable = {} + +class RecursiveCommand(Exception): pass + +class lazycommand(object): + '''defer command lookup until needed, so that extensions loaded + after alias can be aliased''' + def __init__(self, ui, name, target): + self._ui = ui + self._name = name + self._target = target + self._cmd = None + + def __len__(self): + self._resolve() + return len(self._cmd) + + def __getitem__(self, key): + self._resolve() + return self._cmd[key] + + def __iter__(self): + self._resolve() + return self._cmd.__iter__() + + def _resolve(self): + if self._cmd is not None: + return + + try: + self._loading = True + 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) diff --git a/hgext/children.py b/hgext/children.py new file mode 100644 --- /dev/null +++ b/hgext/children.py @@ -0,0 +1,41 @@ +# Mercurial extension to provide the 'hg children' command +# +# Copyright 2007 by Intevation GmbH +# Author(s): +# Thomas Arendsen Hein +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from mercurial import cmdutil +from mercurial.i18n import _ + + +def children(ui, repo, file_=None, **opts): + """show the children of the given or working dir revision + + Print the children of the working directory's revisions. + If a revision is given via --rev, the children of that revision + will be printed. If a file argument is given, revision in + which the file was last changed (after the working directory + revision or the argument to --rev if given) is printed. + """ + rev = opts.get('rev') + if file_: + ctx = repo.filectx(file_, changeid=rev) + else: + ctx = repo.changectx(rev) + + displayer = cmdutil.show_changeset(ui, repo, opts) + for node in [cp.node() for cp in ctx.children()]: + displayer.show(changenode=node) + + +cmdtable = { + "children": + (children, + [('r', 'rev', '', _('show children of the specified rev')), + ('', 'style', '', _('display using template map file')), + ('', 'template', '', _('display with template'))], + _('hg children [-r REV] [FILE]')), +} diff --git a/hgext/convert/__init__.py b/hgext/convert/__init__.py --- a/hgext/convert/__init__.py +++ b/hgext/convert/__init__.py @@ -5,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 @@ class convert(object): 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 @@ class convert(object): 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] @@ -244,10 +264,15 @@ def _convert(ui, src, dest=None, mapfile 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 destination isn't given, a new Mercurial repo named -hg will be created. If isn't given, it will be put in a default location (/.hg/shamap by default) @@ -267,15 +292,12 @@ def _convert(ui, src, dest=None, mapfile srcauthor=whatever string you want ''' - 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 = 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: @@ -290,15 +312,22 @@ def _convert(ui, src, dest=None, mapfile else: ui.status("initializing destination %s repository\n" % dest) hg.repository(ui, dest, create=True) + created = True elif os.path.exists(dest): raise util.Abort("destination %s exists and is not a directory" % dest) else: ui.status("initializing destination %s repository\n" % dest) hg.repository(ui, dest, create=True) + created = True - destc = converter(ui, dest) - if not hasattr(destc, "putcommit"): - raise util.Abort("%s: can't write to this repo type" % src) + destc = convertsink(ui, dest) + + try: + srcc = convertsource(ui, src, rev=opts.get('rev')) + except Exception: + if created: + shutil.rmtree(dest, True) + raise if not mapfile: try: @@ -313,6 +342,7 @@ cmdtable = { "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]]'), } diff --git a/hgext/convert/common.py b/hgext/convert/common.py --- a/hgext/convert/common.py +++ b/hgext/convert/common.py @@ -8,11 +8,13 @@ class commit(object): 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() @@ -44,6 +46,18 @@ class converter_source(object): """Return the tags as a dictionary of name: revision""" raise NotImplementedError() + def recode(self, s, encoding=None): + if not encoding: + encoding = hasattr(self, 'encoding') and 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""" diff --git a/hgext/convert/cvs.py b/hgext/convert/cvs.py --- a/hgext/convert/cvs.py +++ b/hgext/convert/cvs.py @@ -6,9 +6,10 @@ from mercurial import util from common import NoRepo, commit, converter_source class convert_cvs(converter_source): - def __init__(self, ui, path): + def __init__(self, ui, path, rev=None): self.path = path self.ui = ui + self.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 +30,32 @@ class convert_cvs(converter_source): if self.changeset: return + maxrev = 0 + cmd = 'cvsps -A -u --cvs-direct -q' + if self.rev: + # TODO: handle tags + try: + # patchset number? + maxrev = int(self.rev) + except ValueError: + try: + # date + util.parsedate(self.rev, ['%Y/%m/%d %H:%M:%S']) + cmd = "%s -d '1970/01/01 00:00:01' -d '%s'" % (cmd, self.rev) + except util.Abort: + raise util.Abort('revision %s is not a patchset number or date' % self.rev) + d = os.getcwd() try: os.chdir(self.path) id = None state = 0 - for l in os.popen("cvsps -A -u --cvs-direct -q"): + for l in os.popen(cmd): if state == 0: # header if l.startswith("PatchSet"): id = l[9:-2] + if maxrev and int(id) > maxrev: + state = 3 elif l.startswith("Date"): date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"]) date = util.datestr(date) @@ -62,8 +80,6 @@ class convert_cvs(converter_source): if l == "Members: \n": files = {} log = self.recode(log[:-1]) - if log.isspace(): - log = "*** empty log message ***\n" state = 2 else: log += l @@ -85,6 +101,8 @@ class convert_cvs(converter_source): rev = l[colon+1:-2] rev = rev.split("->")[1] files[file] = rev + elif state == 3: + continue self.heads = self.lastbranch.values() finally: @@ -235,9 +253,6 @@ class convert_cvs(converter_source): 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] diff --git a/hgext/convert/git.py b/hgext/convert/git.py --- a/hgext/convert/git.py +++ b/hgext/convert/git.py @@ -4,32 +4,31 @@ import os 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): 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 + self.ui = ui + self.rev = rev + self.encoding = 'utf-8' + 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 +39,7 @@ class convert_git(converter_source): 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 +56,7 @@ class convert_git(converter_source): 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 +67,13 @@ class convert_git(converter_source): tm, tz = p[-2:] author = " ".join(p[:-2]) if author[0] == "<": author = author[1:-1] - author = recode(author) + author = self.recode(author) if n == "committer": p = v.split() tm, tz = p[-2:] committer = " ".join(p[:-2]) if committer[0] == "<": committer = committer[1:-1] - committer = recode(committer) + committer = self.recode(committer) message += "\ncommitter: %s\n" % committer if n == "parent": parents.append(v) @@ -89,7 +87,7 @@ class convert_git(converter_source): 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() diff --git a/hgext/convert/hg.py b/hgext/convert/hg.py --- a/hgext/convert/hg.py +++ b/hgext/convert/hg.py @@ -29,6 +29,9 @@ class convert_mercurial(converter_sink): 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)) diff --git a/hgext/convert/subversion.py b/hgext/convert/subversion.py new file mode 100644 --- /dev/null +++ b/hgext/convert/subversion.py @@ -0,0 +1,602 @@ +# Subversion 1.4/1.5 Python API backend +# +# Copyright(C) 2007 Daniel Holth et al + +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 + +class svn_entry(object): + """Emulate a Subversion path change.""" + __slots__ = ['path', 'copyfrom_path', 'copyfrom_rev', 'action'] + def __init__(self, entry): + self.copyfrom_path = entry.copyfrom_path + self.copyfrom_rev = entry.copyfrom_rev + self.action = entry.action + + def __str__(self): + return "%s %s %s" % (self.action, self.copyfrom_path, self.copyfrom_rev) + + def __repr__(self): + return self.__str__() + +class svn_paths(object): + """Emulate a Subversion ordered dictionary of changed paths.""" + __slots__ = ['values', 'order'] + def __init__(self, orig_paths): + self.order = [] + self.values = {} + if hasattr(orig_paths, 'keys'): + self.order = sorted(orig_paths.keys()) + self.values.update(orig_paths) + return + if not orig_paths: + return + for path in orig_paths: + self.order.append(path) + self.values[path] = svn_entry(orig_paths[path]) + self.order.sort() # maybe the order it came in isn't so great... + + def __iter__(self): + return iter(self.order) + + def __getitem__(self, key): + return self.values[key] + + def __str__(self): + s = "{\n" + for path in self.order: + s += "'%s': %s,\n" % (path, self.values[path]) + s += "}" + return s + + def __repr__(self): + return self.__str__() + +# SVN conversion code stolen from bzr-svn and tailor +class convert_svn(converter_source): + def __init__(self, ui, url, rev=None): + try: + SubversionException + except NameError: + msg = 'subversion python bindings could not be loaded\n' + ui.warn(msg) + raise NoRepo(msg) + + self.ui = ui + self.encoding = locale.getpreferredencoding() + 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. + url, latest = url.rsplit("@", 1) + latest = int(latest) + 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.rev(self.last_changed) + + def rev(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: + 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 + + self.child_cset = None + def parselogentry(*arg, **args): + orig_paths, revnum, author, date, message, pool = arg + orig_paths = svn_paths(orig_paths) + + 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.rev(revnum) + parents = [] + try: + branch = self.module.split("/")[-1] + if branch == 'trunk': + branch = '' + except IndexError: + branch = None + + for path in orig_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.rev(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) + + 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, + parselogentry) + self.last_revnum = to_revnum + 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 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('/') + paths = svn.client.ls(rpath, optrev, False, self.ctx) + if 'branches' in paths and 'trunk' in paths: + self.module += '/trunk' + lt = self.latest(self.module, self.last_changed) + self.head = self.rev(lt) + self.heads = [self.head] + branches = svn.client.ls(rpath + '/branches', optrev, False, self.ctx) + for branch in branches.keys(): + module = '/branches/' + branch + brevnum = self.latest(module, self.last_changed) + brev = self.rev(brevnum, module) + self.ui.note('found branch %s at %d\n' % (branch, brevnum)) + self.heads.append(brev) + else: + self.heads = [self.head] + return self.heads + + 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 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() + return cl + + def getcommit(self, rev): + if rev not in self.commits: + uuid, module, revnum = self.revsplit(rev) + self.module = module + self.reparent(module) + self._fetch_revisions(from_revnum=revnum, to_revnum=0) + return self.commits[rev] + + def gettags(self): + tags = {} + def parselogentry(*arg, **args): + orig_paths, revnum, author, date, message, pool = arg + orig_paths = svn_paths(orig_paths) + 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.rev(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 {} + + 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] diff --git a/hgext/convert/transport.py b/hgext/convert/transport.py new file mode 100644 --- /dev/null +++ b/hgext/convert/transport.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2007 Daniel Holth +# This is a stripped-down version of the original bzr-svn transport.py, +# Copyright (C) 2006 Jelmer Vernooij + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from cStringIO import StringIO +import os +from tempfile import mktemp + +from svn.core import SubversionException, Pool +import svn.ra +import svn.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) diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -50,7 +50,7 @@ def extract(ui, fileobj): try: msg = email.Parser.Parser().parse(fileobj) - message = msg['Subject'] + subject = msg['Subject'] user = msg['From'] # should try to parse msg['Date'] date = None @@ -58,13 +58,13 @@ def extract(ui, fileobj): branch = None parents = [] - if message: - if message.startswith('[PATCH'): - pend = message.find(']') + if subject: + if subject.startswith('[PATCH'): + pend = subject.find(']') if pend >= 0: - message = message[pend+1:].lstrip() - message = message.replace('\n\t', ' ') - ui.debug('Subject: %s\n' % message) + subject = subject[pend+1:].lstrip() + subject = subject.replace('\n\t', ' ') + ui.debug('Subject: %s\n' % subject) if user: ui.debug('From: %s\n' % user) diffs_seen = 0 @@ -84,9 +84,6 @@ def extract(ui, fileobj): ui.debug(_('found patch at byte %d\n') % m.start(0)) diffs_seen += 1 cfp = cStringIO.StringIO() - if message: - cfp.write(message) - cfp.write('\n') for line in payload[:m.start(0)].splitlines(): if line.startswith('# HG changeset patch'): ui.debug(_('patch generated by hg export\n')) @@ -94,6 +91,7 @@ def extract(ui, fileobj): # drop earlier commit message content cfp.seek(0) cfp.truncate() + subject = None elif hgpatch: if line.startswith('# User '): user = line[7:] @@ -123,6 +121,8 @@ def extract(ui, fileobj): os.unlink(tmpname) raise + if subject and not message.startswith(subject): + message = '%s\n%s' % (subject, message) tmpfp.close() if not diffs_seen: os.unlink(tmpname) diff --git a/tests/test-alias b/tests/test-alias new file mode 100755 --- /dev/null +++ b/tests/test-alias @@ -0,0 +1,32 @@ +#!/bin/sh + +cat > $HGRCPATH < foo +hg ci -Amfoo + +echo '% with opts' +hg cleanst diff --git a/tests/test-alias.out b/tests/test-alias.out new file mode 100644 --- /dev/null +++ b/tests/test-alias.out @@ -0,0 +1,10 @@ +% basic +% unknown +*** [alias] unknown: command bargle is unknown +% ambiguous +*** [alias] ambiguous: command s is ambiguous +% recursive +*** [alias] recursive: circular dependency on recursive +adding foo +% with opts +C foo diff --git a/tests/test-children b/tests/test-children new file mode 100755 --- /dev/null +++ b/tests/test-children @@ -0,0 +1,59 @@ +#!/bin/sh +# test children command + +cat <> $HGRCPATH +[extensions] +hgext.children= +EOF + +echo "% init" +hg init t +cd t + +echo "% no working directory" +hg children + +echo % setup +echo 0 > file0 +hg ci -qAm 0 -d '0 0' + +echo 1 > file1 +hg ci -qAm 1 -d '1 0' + +echo 2 >> file0 +hg ci -qAm 2 -d '2 0' + +hg co null +echo 3 > file3 +hg ci -qAm 3 -d '3 0' + +echo "% hg children at revision 3 (tip)" +hg children + +hg co null +echo "% hg children at nullrev (should be 0 and 3)" +hg children + +hg co 1 +echo "% hg children at revision 1 (should be 2)" +hg children + +hg co 2 +echo "% hg children at revision 2 (other head)" +hg children + +for i in null 0 1 2 3; do + echo "% hg children -r $i" + hg children -r $i +done + +echo "% hg children -r 0 file0 (should be 2)" +hg children -r 0 file0 + +echo "% hg children -r 1 file0 (should be 2)" +hg children -r 1 file0 + +hg co 0 +echo "% hg children file0 at revision 0 (should be 2)" +hg children file0 + diff --git a/tests/test-children.out b/tests/test-children.out new file mode 100644 --- /dev/null +++ b/tests/test-children.out @@ -0,0 +1,62 @@ +% init +% no working directory +% setup +0 files updated, 0 files merged, 2 files removed, 0 files unresolved +% hg children at revision 3 (tip) +0 files updated, 0 files merged, 1 files removed, 0 files unresolved +% hg children at nullrev (should be 0 and 3) +2 files updated, 0 files merged, 0 files removed, 0 files unresolved +% hg children at revision 1 (should be 2) +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +% hg children at revision 2 (other head) +% hg children -r null +changeset: 0:4df8521a7374 +user: test +date: Thu Jan 01 00:00:00 1970 +0000 +summary: 0 + +changeset: 3:e2962852269d +tag: tip +parent: -1:000000000000 +user: test +date: Thu Jan 01 00:00:03 1970 +0000 +summary: 3 + +% hg children -r 0 +changeset: 1:708c093edef0 +user: test +date: Thu Jan 01 00:00:01 1970 +0000 +summary: 1 + +% hg children -r 1 +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + +% hg children -r 2 +% hg children -r 3 +% hg children -r 0 file0 (should be 2) +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + +% hg children -r 1 file0 (should be 2) +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + +1 files updated, 0 files merged, 1 files removed, 0 files unresolved +% hg children file0 at revision 0 (should be 2) +changeset: 2:8f5eea5023c2 +user: test +date: Thu Jan 01 00:00:02 1970 +0000 +summary: 2 + diff --git a/tests/test-import b/tests/test-import --- a/tests/test-import +++ b/tests/test-import @@ -93,6 +93,24 @@ python mkmsg.py | hg --cwd b import - hg --cwd b tip | grep second rm -r b +# subject: duplicate detection, removal of [PATCH] +cat > mkmsg2.py < tip.patch +python mkmsg2.py | hg --cwd b import - +hg --cwd b tip --template '{desc}\n' +rm -r b + + # bug non regression test # importing a patch in a subdirectory failed at the commit stage echo line 2 >> a/d1/d2/a diff --git a/tests/test-import.out b/tests/test-import.out --- a/tests/test-import.out +++ b/tests/test-import.out @@ -100,6 +100,17 @@ added 1 changesets with 2 changes to 2 f 2 files updated, 0 files merged, 0 files removed, 0 files unresolved applying patch from stdin summary: second change +% plain diff in email, [PATCH] subject, message body with subject +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 2 changes to 2 files +2 files updated, 0 files merged, 0 files removed, 0 files unresolved +applying patch from stdin +email patch + +next line % hg import in a subdirectory requesting all changes adding changesets