# HG changeset patch # User Alexis S. L. Carvalho # Date 1202082427 7200 # Node ID 30d2fecaab7634c0185fd951bf72d7ad7eb6401d # Parent 8e7d64989bb8ecc9609e1272a411397f523e66a9# Parent ee317dbfb9d0b1247ab6f882e530a8d8dc320413 merge with crew-stable diff -r ee317dbfb9d0 -r 30d2fecaab76 .hgignore --- a/.hgignore Sun Feb 03 21:03:46 2008 -0200 +++ b/.hgignore Sun Feb 03 21:47:07 2008 -0200 @@ -22,8 +22,10 @@ MANIFEST patches mercurial/__version__.py +Output/Mercurial-*.exe .DS_Store +tags +cscope.* syntax: regexp ^\.pc/ -Output/Mercurial-[0-9.]*.exe diff -r ee317dbfb9d0 -r 30d2fecaab76 CONTRIBUTORS --- a/CONTRIBUTORS Sun Feb 03 21:03:46 2008 -0200 +++ b/CONTRIBUTORS Sun Feb 03 21:47:07 2008 -0200 @@ -1,4 +1,7 @@ -Andrea Arcangeli +[This file is here for historical purposes, all recent contributors +should appear in the changelog directly] + +Andrea Arcangeli Thomas Arendsen Hein Goffredo Baroncelli Muli Ben-Yehuda @@ -36,5 +39,3 @@ Rafael Villar Burke Tristan Wibberley Mark Williamson - -If you are a contributor and don't see your name here, please let me know. diff -r ee317dbfb9d0 -r 30d2fecaab76 contrib/bash_completion --- a/contrib/bash_completion Sun Feb 03 21:03:46 2008 -0200 +++ b/contrib/bash_completion Sun Feb 03 21:47:07 2008 -0200 @@ -305,6 +305,15 @@ _hg_ext_mq_patchlist qunapplied } +_hg_cmd_qgoto() +{ + if [[ "$prev" = @(-n|--name) ]]; then + _hg_ext_mq_queues + return + fi + _hg_ext_mq_patchlist qseries +} + _hg_cmd_qdelete() { local qcmd=qunapplied diff -r ee317dbfb9d0 -r 30d2fecaab76 contrib/churn.py --- a/contrib/churn.py Sun Feb 03 21:03:46 2008 -0200 +++ b/contrib/churn.py Sun Feb 03 21:47:07 2008 -0200 @@ -12,7 +12,7 @@ # from mercurial.i18n import gettext as _ -from mercurial import hg, mdiff, cmdutil, ui, util, templater, node +from mercurial import hg, mdiff, cmdutil, ui, util, templatefilters, node import os, sys def get_tty_width(): @@ -69,7 +69,7 @@ modified, added, removed, deleted, unknown = changes who = repo.changelog.read(node2)[1] - who = templater.email(who) # get the email of the person + who = util.email(who) # get the email of the person mmap1 = repo.manifest.read(repo.changelog.read(node1)[0]) mmap2 = repo.manifest.read(repo.changelog.read(node2)[0]) @@ -114,23 +114,24 @@ who, lines = __gather(ui, repo, node1, node2) # remap the owner if possible - if amap.has_key(who): + if who in amap: ui.note("using '%s' alias for '%s'\n" % (amap[who], who)) who = amap[who] - if not stats.has_key(who): + if not who in stats: stats[who] = 0 stats[who] += lines ui.note("rev %d: %d lines by %s\n" % (rev, lines, who)) if progress: + nr_revs = max(nr_revs, 1) if int(100.0*(cur_rev - 1)/nr_revs) < int(100.0*cur_rev/nr_revs): - ui.write("%d%%.." % (int(100.0*cur_rev/nr_revs),)) + ui.write("\rGenerating stats: %d%%" % (int(100.0*cur_rev/nr_revs),)) sys.stdout.flush() if progress: - ui.write("done\n") + ui.write("\r") sys.stdout.flush() return stats @@ -144,6 +145,7 @@ return s[0:l] def graph(n, maximum, width, char): + maximum = max(1, maximum) n = int(n * width / float(maximum)) return char * (n) @@ -178,6 +180,8 @@ ordered = stats.items() ordered.sort(lambda x, y: cmp(y[1], x[1])) + if not ordered: + return maximum = ordered[0][1] width = get_tty_width() diff -r ee317dbfb9d0 -r 30d2fecaab76 contrib/hgk --- a/contrib/hgk Sun Feb 03 21:03:46 2008 -0200 +++ b/contrib/hgk Sun Feb 03 21:47:07 2008 -0200 @@ -274,6 +274,7 @@ set comname {} set comdate {} set rev {} + set branch {} if {![info exists nchildren($id)]} { set children($id) {} set nchildren($id) 0 @@ -310,6 +311,8 @@ set comname [join [lrange $line 1 [expr {$x - 1}]]] } elseif {$tag == "revision"} { set rev [lindex $line 1] + } elseif {$tag == "branch"} { + set branch [join [lrange $line 1 end]] } } } else { @@ -334,7 +337,7 @@ set comdate [clock format $comdate -format "%Y-%m-%d %H:%M:%S"] } set commitinfo($id) [list $headline $auname $audate \ - $comname $comdate $comment $rev] + $comname $comdate $comment $rev $branch] } proc readrefs {} { @@ -649,7 +652,7 @@ if {$stuffsaved} return if {![winfo viewable .]} return catch { - set f [open "~/.gitk-new" w] + set f [open "~/.hgk-new" w] puts $f [list set mainfont $mainfont] puts $f [list set curidfont $curidfont] puts $f [list set textfont $textfont] @@ -687,7 +690,7 @@ puts $f "#" puts $f "set authorcolors {$authorcolors}" close $f - file rename -force "~/.gitk-new" "~/.gitk" + file rename -force "~/.hgk-new" "~/.hgk" } set stuffsaved 1 } @@ -2286,6 +2289,9 @@ $ctext mark gravity fmark.0 left set info $commitinfo($id) $ctext insert end "Revision: [lindex $info 6]\n" + if {[llength [lindex $info 7]] > 0} { + $ctext insert end "Branch: [lindex $info 7]\n" + } $ctext insert end "Author: [lindex $info 1] [lindex $info 2]\n" $ctext insert end "Committer: [lindex $info 3] [lindex $info 4]\n" if {[info exists idtags($id)]} { @@ -3844,10 +3850,10 @@ set colors {green red blue magenta darkgrey brown orange} set authorcolors { - deeppink mediumorchid blue burlywood4 goldenrod slateblue red2 navy dimgrey + black blue deeppink mediumorchid blue burlywood4 goldenrod slateblue red2 navy dimgrey } -catch {source ~/.gitk} +catch {source ~/.hgk} if {$curidfont == ""} { # initialize late based on current mainfont set curidfont "$mainfont bold italic underline" diff -r ee317dbfb9d0 -r 30d2fecaab76 contrib/hgwebdir.fcgi --- a/contrib/hgwebdir.fcgi Sun Feb 03 21:03:46 2008 -0200 +++ b/contrib/hgwebdir.fcgi Sun Feb 03 21:47:07 2008 -0200 @@ -23,6 +23,7 @@ from mercurial.hgweb.hgwebdir_mod import hgwebdir from mercurial.hgweb.request import wsgiapplication +from mercurial import dispatch, ui from flup.server.fcgi import WSGIServer # The config file looks like this. You can have paths to individual @@ -44,7 +45,8 @@ # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples # or use a dictionary with entries like 'virtual/path': '/real/path' -def make_web_app(): - return hgwebdir("hgweb.config") +def web_app(ui): + return lambda: hgwebdir("hgweb.config", ui) -WSGIServer(wsgiapplication(make_web_app)).run() +u = ui.ui(report_untrusted=False, interactive=False) +dispatch.profiled(u, lambda: WSGIServer(wsgiapplication(web_app(u))).run()) diff -r ee317dbfb9d0 -r 30d2fecaab76 contrib/win32/ReadMe.html --- a/contrib/win32/ReadMe.html Sun Feb 03 21:03:46 2008 -0200 +++ b/contrib/win32/ReadMe.html Sun Feb 03 21:47:07 2008 -0200 @@ -33,7 +33,7 @@ href="http://hgbook.red-bean.com/">Distributed revision control with Mercurial.

-

By default, Mercurial installs to C:\Mercurial. The +

By default, Mercurial installs to C:\Program Files\Mercurial. The Mercurial command is called hg.exe.

Testing Mercurial after you've installed it

diff -r ee317dbfb9d0 -r 30d2fecaab76 contrib/win32/mercurial.iss --- a/contrib/win32/mercurial.iss Sun Feb 03 21:03:46 2008 -0200 +++ b/contrib/win32/mercurial.iss Sun Feb 03 21:47:07 2008 -0200 @@ -15,8 +15,8 @@ AppID={{4B95A5F1-EF59-4B08-BED8-C891C46121B3} AppContact=mercurial@selenic.com OutputBaseFilename=Mercurial-snapshot -DefaultDirName={sd}\Mercurial -SourceDir=C:\hg\hg-release +DefaultDirName={pf}\Mercurial +SourceDir=..\.. VersionInfoDescription=Mercurial distributed SCM VersionInfoCopyright=Copyright 2005-2007 Matt Mackall and others VersionInfoCompany=Matt Mackall and others @@ -29,17 +29,17 @@ [Files] Source: contrib\mercurial.el; DestDir: {app}/Contrib +Source: contrib\vim\*.*; DestDir: {app}/Contrib/Vim +Source: contrib\zsh_completion; DestDir: {app}/Contrib Source: contrib\win32\ReadMe.html; DestDir: {app}; Flags: isreadme Source: contrib\win32\mercurial.ini; DestDir: {app}; DestName: Mercurial.ini; Flags: confirmoverwrite Source: contrib\win32\postinstall.txt; DestDir: {app}; DestName: ReleaseNotes.txt Source: dist\hg.exe; DestDir: {app}; AfterInstall: Touch('{app}\hg.exe.local') Source: dist\library.zip; DestDir: {app} -Source: dist\patch.exe; DestDir: {app} Source: dist\mfc71.dll; DestDir: {app} Source: dist\msvcr71.dll; DestDir: {app} Source: dist\w9xpopen.exe; DestDir: {app} Source: dist\add_path.exe; DestDir: {app} -Source: doc\*.txt; DestDir: {app}\Docs Source: doc\*.html; DestDir: {app}\Docs Source: templates\*.*; DestDir: {app}\Templates; Flags: recursesubdirs createallsubdirs Source: CONTRIBUTORS; DestDir: {app}; DestName: Contributors.txt diff -r ee317dbfb9d0 -r 30d2fecaab76 contrib/zsh_completion --- a/contrib/zsh_completion Sun Feb 03 21:03:46 2008 -0200 +++ b/contrib/zsh_completion Sun Feb 03 21:47:07 2008 -0200 @@ -13,6 +13,9 @@ # option) any later version. # +emulate -LR zsh +setopt extendedglob + local curcontext="$curcontext" state line typeset -A _hg_cmd_globals @@ -153,9 +156,9 @@ typeset -a tags local tag rev - _hg_cmd tags 2> /dev/null | while read tag rev + _hg_cmd tags 2> /dev/null | while read tag do - tags+=($tag) + tags+=(${tag/ # [0-9]#:*}) done (( $#tags )) && _describe -t tags 'tags' tags } @@ -674,13 +677,13 @@ # MQ _hg_qseries() { typeset -a patches - patches=($(_hg_cmd qseries 2>/dev/null)) + patches=(${(f)"$(_hg_cmd qseries 2>/dev/null)"}) (( $#patches )) && _describe -t hg-patches 'patches' patches } _hg_qapplied() { typeset -a patches - patches=($(_hg_cmd qapplied 2>/dev/null)) + patches=(${(f)"$(_hg_cmd qapplied 2>/dev/null)"}) if (( $#patches )) then patches+=(qbase qtip) @@ -690,7 +693,7 @@ _hg_qunapplied() { typeset -a patches - patches=($(_hg_cmd qunapplied 2>/dev/null)) + patches=(${(f)"$(_hg_cmd qunapplied 2>/dev/null)"}) (( $#patches )) && _describe -t hg-unapplied-patches 'unapplied patches' patches } @@ -730,6 +733,12 @@ '*:unapplied patch:_hg_qunapplied' } +_hg_cmd_qgoto() { + _arguments -s -w : $_hg_global_opts \ + '(--force -f)'{-f,--force}'[overwrite any local changes]' \ + ':patch:_hg_qseries' +} + _hg_cmd_qguard() { _arguments -s -w : $_hg_global_opts \ '(--list -l)'{-l,--list}'[list all patches and guards]' \ diff -r ee317dbfb9d0 -r 30d2fecaab76 doc/hg.1.txt --- a/doc/hg.1.txt Sun Feb 03 21:03:46 2008 -0200 +++ b/doc/hg.1.txt Sun Feb 03 21:47:07 2008 -0200 @@ -91,11 +91,11 @@ FILES ----- - .hgignore:: + repo/.hgignore:: This file contains regular expressions (one per line) that describe file names that should be ignored by hg. For details, see hgignore(5). - .hgtags:: + repo/.hgtags:: This file contains changeset hash values and text tag names (one of each separated by spaces) that correspond to tagged versions of the repository contents. diff -r ee317dbfb9d0 -r 30d2fecaab76 doc/hgrc.5.txt --- a/doc/hgrc.5.txt Sun Feb 03 21:03:46 2008 -0200 +++ b/doc/hgrc.5.txt Sun Feb 03 21:47:07 2008 -0200 @@ -17,7 +17,9 @@ Mercurial reads configuration data from several files, if they exist. The names of these files depend on the system on which Mercurial is -installed. +installed. Windows registry keys contain PATH-like strings, every +part must reference a Mercurial.ini file or be a directory where *.rc +files will be read. (Unix) /etc/mercurial/hgrc.d/*.rc:: (Unix) /etc/mercurial/hgrc:: @@ -29,6 +31,8 @@ (Unix) /etc/mercurial/hgrc.d/*.rc:: (Unix) /etc/mercurial/hgrc:: +(Windows) HKEY_LOCAL_MACHINE\SOFTWARE\Mercurial:: + or:: (Windows) C:\Mercurial\Mercurial.ini:: Per-system configuration files, for the system on which Mercurial is running. Options in these files apply to all Mercurial @@ -120,21 +124,26 @@ NOTE: the tempfile mechanism is recommended for Windows systems, where the standard shell I/O redirection operators often have - strange effects. In particular, if you are doing line ending - conversion on Windows using the popular dos2unix and unix2dos - programs, you *must* use the tempfile mechanism, as using pipes will - corrupt the contents of your files. + strange effects and may corrupt the contents of your files. - Tempfile example: + The most common usage is for LF <-> CRLF translation on Windows. + For this, use the "smart" convertors which check for binary files: + [extensions] + hgext.win32text = [encode] - # convert files to unix line ending conventions on checkin - **.txt = tempfile: dos2unix -n INFILE OUTFILE - + ** = cleverencode: [decode] - # convert files to windows line ending conventions when writing - # them to the working dir - **.txt = tempfile: unix2dos -n INFILE OUTFILE + ** = cleverdecode: + + or if you only want to translate certain files: + + [extensions] + hgext.win32text = + [encode] + **.txt = dumbencode: + [decode] + **.txt = dumbdecode: defaults:: Use the [defaults] section to define command defaults, i.e. the @@ -277,7 +286,7 @@ commit to proceed. Non-zero status will cause the commit to fail. Parent changeset IDs are in $HG_PARENT1 and $HG_PARENT2. preoutgoing;; - Run before computing changes to send from the local repository to + Run before collecting changes to send from the local repository to another. Non-zero status will cause failure. This lets you prevent pull over http or ssh. Also prevents against local pull, push (outbound) or bundle commands, but not effective, since you @@ -394,6 +403,20 @@ Optional. Directory or URL to use when pushing if no destination is specified. +profile:: + Configuration of profiling options, for in-depth performance + analysis. Mostly useful to developers. + enable;; + Enable a particular profiling mode. Useful for profiling + server-side processes. "lsprof" enables modern profiling. + "hotshot" is deprecated, and produces less reliable results. + Default is no profiling. + output;; + The name of a file to write profiling data to. Each occurrence of + "%%p" will be replaced with the current process ID (the repeated + "%" protects against the config parser's string interpolator). + Default output is to stderr. + server:: Controls generic server settings. uncompressed;; @@ -515,7 +538,7 @@ Example: "http://hgserver/repos/" contact;; Name or email address of the person in charge of the repository. - Default is "unknown". + Defaults to ui.username or $EMAIL or "unknown" if unset or empty. deny_push;; Whether to deny pushing to the repository. If empty or not set, push is not denied. If the special value "*", all remote users @@ -544,6 +567,8 @@ Maximum number of files to list per changeset. Default is 10. port;; Port to listen on. Default is 8000. + prefix;; + Prefix path to serve from. Default is '' (server root). push_ssl;; Whether to require that inbound pushes be transported over SSL to prevent password sniffing. Default is true. diff -r ee317dbfb9d0 -r 30d2fecaab76 hg --- a/hg Sun Feb 03 21:03:46 2008 -0200 +++ b/hg Sun Feb 03 21:47:07 2008 -0200 @@ -10,5 +10,11 @@ # enable importing on demand to reduce startup time from mercurial import demandimport; demandimport.enable() +import sys +import mercurial.util import mercurial.dispatch + +for fp in (sys.stdin, sys.stdout, sys.stderr): + mercurial.util.set_binary(fp) + mercurial.dispatch.run() diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/bugzilla.py --- a/hgext/bugzilla.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/bugzilla.py Sun Feb 03 21:47:07 2008 -0200 @@ -282,7 +282,7 @@ root=self.repo.root, webroot=webroot(self.repo.root)) data = self.ui.popbuffer() - self.add_comment(bugid, data, templater.email(ctx.user())) + self.add_comment(bugid, data, util.email(ctx.user())) def hook(ui, repo, hooktype, node=None, **kwargs): '''add comment to bugzilla for each changeset that refers to a diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/color.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/color.py Sun Feb 03 21:47:07 2008 -0200 @@ -0,0 +1,219 @@ +# color.py color output for the status and qseries commands +# +# Copyright (C) 2007 Kevin Christen +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +'''add color output to the status and qseries commands + +This extension modifies the status command to add color to its output to +reflect file status, and the qseries command to add color to reflect patch +status (applied, unapplied, missing). Other effects in addition to color, +like bold and underlined text, are also available. Effects are rendered +with the ECMA-48 SGR control function (aka ANSI escape codes). This module +also provides the render_text function, which can be used to add effects to +any text. + +To enable this extension, add this to your .hgrc file: +[extensions] +color = + +Default effects my be overriden from the .hgrc file: + +[color] +status.modified = blue bold underline red_background +status.added = green bold +status.removed = red bold blue_background +status.deleted = cyan bold underline +status.unknown = magenta bold underline +status.ignored = black bold + + 'none' turns off all effects +status.clean = none +status.copied = none + +qseries.applied = blue bold underline +qseries.unapplied = black bold +qseries.missing = red bold +''' + +import re, sys + +from mercurial import commands, cmdutil, ui +from mercurial.i18n import _ + +# start and stop parameters for effects +_effect_params = { 'none': (0, 0), + 'black': (30, 39), + 'red': (31, 39), + 'green': (32, 39), + 'yellow': (33, 39), + 'blue': (34, 39), + 'magenta': (35, 39), + 'cyan': (36, 39), + 'white': (37, 39), + 'bold': (1, 22), + 'italic': (3, 23), + 'underline': (4, 24), + 'inverse': (7, 27), + 'black_background': (40, 49), + 'red_background': (41, 49), + 'green_background': (42, 49), + 'yellow_background': (43, 49), + 'blue_background': (44, 49), + 'purple_background': (45, 49), + 'cyan_background': (46, 49), + 'white_background': (47, 49), } + +def render_effects(text, *effects): + 'Wrap text in commands to turn on each effect.' + start = [] + stop = [] + for effect in effects: + start.append(str(_effect_params[effect][0])) + stop.append(str(_effect_params[effect][1])) + start = '\033[' + ';'.join(start) + 'm' + stop = '\033[' + ';'.join(stop) + 'm' + return start + text + stop + +def colorstatus(statusfunc, ui, repo, *pats, **opts): + '''run the status command with colored output''' + + delimiter = opts['print0'] and '\0' or '\n' + + # run status and capture it's output + ui.pushbuffer() + retval = statusfunc(ui, repo, *pats, **opts) + # filter out empty strings + lines = [ line for line in ui.popbuffer().split(delimiter) if line ] + + if opts['no_status']: + # if --no-status, run the command again without that option to get + # output with status abbreviations + opts['no_status'] = False + ui.pushbuffer() + statusfunc(ui, repo, *pats, **opts) + # filter out empty strings + lines_with_status = [ line for + line in ui.popbuffer().split(delimiter) if line ] + else: + lines_with_status = lines + + # apply color to output and display it + for i in xrange(0, len(lines)): + status = _status_abbreviations[lines_with_status[i][0]] + effects = _status_effects[status] + if effects: + lines[i] = render_effects(lines[i], *effects) + sys.stdout.write(lines[i] + delimiter) + return retval + +_status_abbreviations = { 'M': 'modified', + 'A': 'added', + 'R': 'removed', + '!': 'deleted', + '?': 'unknown', + 'I': 'ignored', + 'C': 'clean', + ' ': 'copied', } + +_status_effects = { 'modified': ('blue', 'bold'), + 'added': ('green', 'bold'), + 'removed': ('red', 'bold'), + 'deleted': ('cyan', 'bold', 'underline'), + 'unknown': ('magenta', 'bold', 'underline'), + 'ignored': ('black', 'bold'), + 'clean': ('none', ), + 'copied': ('none', ), } + +def colorqseries(qseriesfunc, ui, repo, *dummy, **opts): + '''run the qseries command with colored output''' + ui.pushbuffer() + retval = qseriesfunc(ui, repo, **opts) + patches = ui.popbuffer().splitlines() + for patch in patches: + if opts['missing']: + effects = _patch_effects['missing'] + # Determine if patch is applied. Search for beginning of output + # line in the applied patch list, in case --summary has been used + # and output line isn't just the patch name. + elif [ applied for applied in repo.mq.applied + if patch.startswith(applied.name) ]: + effects = _patch_effects['applied'] + else: + effects = _patch_effects['unapplied'] + sys.stdout.write(render_effects(patch, *effects) + '\n') + return retval + +_patch_effects = { 'applied': ('blue', 'bold', 'underline'), + 'missing': ('red', 'bold'), + 'unapplied': ('black', 'bold'), } + +def uisetup(ui): + '''Initialize the extension.''' + nocoloropt = ('', 'no-color', None, _("don't colorize output")) + _decoratecmd(ui, 'status', commands.table, colorstatus, nocoloropt) + _configcmdeffects(ui, 'status', _status_effects); + if ui.config('extensions', 'hgext.mq', default=None) is not None: + from hgext import mq + _decoratecmd(ui, 'qseries', mq.cmdtable, colorqseries, nocoloropt) + _configcmdeffects(ui, 'qseries', _patch_effects); + +def _decoratecmd(ui, cmd, table, delegate, *delegateoptions): + '''Replace the function that implements cmd in table with a decorator. + + The decorator that becomes the new implementation of cmd calls + delegate. The delegate's first argument is the replaced function, + followed by the normal Mercurial command arguments (ui, repo, ...). If + the delegate adds command options, supply them as delegateoptions. + ''' + cmdkey, cmdentry = _cmdtableitem(ui, cmd, table) + decorator = lambda ui, repo, *args, **opts: \ + _colordecorator(delegate, cmdentry[0], + ui, repo, *args, **opts) + # make sure 'hg help cmd' still works + decorator.__doc__ = cmdentry[0].__doc__ + decoratorentry = (decorator,) + cmdentry[1:] + for option in delegateoptions: + decoratorentry[1].append(option) + table[cmdkey] = decoratorentry + +def _cmdtableitem(ui, cmd, table): + '''Return key, value from table for cmd, or None if not found.''' + aliases, entry = cmdutil.findcmd(ui, cmd, table) + for candidatekey, candidateentry in table.iteritems(): + if candidateentry is entry: + return candidatekey, entry + +def _colordecorator(colorfunc, nocolorfunc, ui, repo, *args, **opts): + '''Delegate to colorfunc or nocolorfunc, depending on conditions. + + Delegate to colorfunc unless --no-color option is set or output is not + to a tty. + ''' + if opts['no_color'] or not sys.stdout.isatty(): + return nocolorfunc(ui, repo, *args, **opts) + return colorfunc(nocolorfunc, ui, repo, *args, **opts) + +def _configcmdeffects(ui, cmdname, effectsmap): + '''Override default effects for cmdname with those from .hgrc file. + + Entries in the .hgrc file are in the [color] section, and look like + 'cmdname'.'status' (for instance, 'status.modified = blue bold inverse'). + ''' + for status in effectsmap: + effects = ui.config('color', cmdname + '.' + status) + if effects: + effectsmap[status] = re.split('\W+', effects) diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/convert/__init__.py --- a/hgext/convert/__init__.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/convert/__init__.py Sun Feb 03 21:47:07 2008 -0200 @@ -22,6 +22,7 @@ Accepted destination formats: - Mercurial + - Subversion (history on branches is not preserved) If no revision is given, all revisions will be converted. Otherwise, convert will only import up to the named revision (given in a format @@ -56,7 +57,7 @@ exclude path/to/file rename from/file to/file - + The 'include' directive causes a file, or all files under a directory, to be included in the destination repository, and the exclusion of all other files and dirs not explicitely included. @@ -64,6 +65,24 @@ The 'rename' directive renames a file or directory. To rename from a subdirectory into the root of the repository, use '.' as the path to rename to. + + Back end options: + + --config convert.hg.clonebranches=False (boolean) + hg target: XXX not documented + --config convert.hg.saverev=True (boolean) + hg source: allow target to preserve source revision ID + --config convert.hg.tagsbranch=default (branch name) + hg target: XXX not documented + --config convert.hg.usebranchnames=True (boolean) + hg target: preserve branch names + + --config convert.svn.branches=branches (directory name) + svn source: specify the directory containing branches + --config convert.svn.tags=tags (directory name) + svn source: specify the directory containing tags + --config convert.svn.trunk=trunk (directory name) + svn source: specify the name of the trunk branch """ return convcmd.convert(ui, src, dest, revmapfile, **opts) diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/convert/common.py --- a/hgext/convert/common.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/convert/common.py Sun Feb 03 21:47:07 2008 -0200 @@ -1,6 +1,9 @@ # common code for the convert extension -import base64 +import base64, errno +import os import cPickle as pickle +from mercurial import util +from mercurial.i18n import _ def encodeargs(args): def encodearg(s): @@ -15,6 +18,11 @@ s = base64.decodestring(s) return pickle.loads(s) +def checktool(exe, name=None): + name = name or exe + if not util.find_exe(exe): + raise util.Abort('cannot find required "%s" tool' % name) + class NoRepo(Exception): pass SKIPREV = 'SKIP' @@ -33,7 +41,7 @@ class converter_source(object): """Conversion source interface""" - def __init__(self, ui, path, rev=None): + def __init__(self, ui, path=None, rev=None): """Initialize conversion source (or raise NoRepo("message") exception if path is not a valid repository)""" self.ui = ui @@ -48,11 +56,8 @@ def after(self): pass - def setrevmap(self, revmap, order): - """set the map of already-converted revisions - - order is a list with the keys from revmap in the order they - appear in the revision map file.""" + def setrevmap(self, revmap): + """set the map of already-converted revisions""" pass def getheads(self): @@ -100,17 +105,22 @@ def getchangedfiles(self, rev, i): """Return the files changed by rev compared to parent[i]. - + i is an index selecting one of the parents of rev. The return value should be the list of files that are different in rev and this parent. If rev has no parents, i is None. - + This function is only needed to support --filemap """ raise NotImplementedError() + def converted(self, rev, sinkrev): + '''Notify the source that a revision has been converted.''' + pass + + class converter_sink(object): """Conversion sink (target) interface""" @@ -183,3 +193,151 @@ filter empty revisions. """ pass + + def before(self): + pass + + def after(self): + pass + + +class commandline(object): + def __init__(self, ui, command): + self.ui = ui + self.command = command + + def prerun(self): + pass + + def postrun(self): + pass + + def _cmdline(self, cmd, *args, **kwargs): + cmdline = [self.command, cmd] + list(args) + for k, v in kwargs.iteritems(): + if len(k) == 1: + cmdline.append('-' + k) + else: + cmdline.append('--' + k.replace('_', '-')) + try: + if len(k) == 1: + cmdline.append('' + v) + else: + cmdline[-1] += '=' + v + except TypeError: + pass + cmdline = [util.shellquote(arg) for arg in cmdline] + cmdline += ['<', util.nulldev] + cmdline = ' '.join(cmdline) + self.ui.debug(cmdline, '\n') + return cmdline + + def _run(self, cmd, *args, **kwargs): + cmdline = self._cmdline(cmd, *args, **kwargs) + self.prerun() + try: + return util.popen(cmdline) + finally: + self.postrun() + + def run(self, cmd, *args, **kwargs): + fp = self._run(cmd, *args, **kwargs) + output = fp.read() + self.ui.debug(output) + return output, fp.close() + + def checkexit(self, status, output=''): + if status: + if output: + self.ui.warn(_('%s error:\n') % self.command) + self.ui.warn(output) + msg = util.explain_exit(status)[0] + raise util.Abort(_('%s %s') % (self.command, msg)) + + def run0(self, cmd, *args, **kwargs): + output, status = self.run(cmd, *args, **kwargs) + self.checkexit(status, output) + return output + + def getargmax(self): + if '_argmax' in self.__dict__: + return self._argmax + + # POSIX requires at least 4096 bytes for ARG_MAX + self._argmax = 4096 + try: + self._argmax = os.sysconf("SC_ARG_MAX") + except: + pass + + # Windows shells impose their own limits on command line length, + # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes + # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for + # details about cmd.exe limitations. + + # Since ARG_MAX is for command line _and_ environment, lower our limit + # (and make happy Windows shells while doing this). + + self._argmax = self._argmax/2 - 1 + return self._argmax + + def limit_arglist(self, arglist, cmd, *args, **kwargs): + limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs)) + bytes = 0 + fl = [] + for fn in arglist: + b = len(fn) + 3 + if bytes + b < limit or len(fl) == 0: + fl.append(fn) + bytes += b + else: + yield fl + fl = [fn] + bytes = b + if fl: + yield fl + + def xargs(self, arglist, cmd, *args, **kwargs): + for l in self.limit_arglist(arglist, cmd, *args, **kwargs): + self.run0(cmd, *(list(args) + l), **kwargs) + +class mapfile(dict): + def __init__(self, ui, path): + super(mapfile, self).__init__() + self.ui = ui + self.path = path + self.fp = None + self.order = [] + self._read() + + def _read(self): + if self.path is None: + return + try: + fp = open(self.path, 'r') + except IOError, err: + if err.errno != errno.ENOENT: + raise + return + for line in fp: + key, value = line[:-1].split(' ', 1) + if key not in self: + self.order.append(key) + super(mapfile, self).__setitem__(key, value) + fp.close() + + def __setitem__(self, key, value): + if self.fp is None: + try: + self.fp = open(self.path, 'a') + except IOError, err: + raise util.Abort(_('could not open map file %r: %s') % + (self.path, err.strerror)) + self.fp.write('%s %s\n' % (key, value)) + self.fp.flush() + super(mapfile, self).__setitem__(key, value) + + def close(self): + if self.fp: + self.fp.close() + self.fp = None diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/convert/convcmd.py --- a/hgext/convert/convcmd.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/convert/convcmd.py Sun Feb 03 21:47:07 2008 -0200 @@ -5,12 +5,12 @@ # 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, SKIPREV, converter_source, converter_sink +from common import NoRepo, SKIPREV, converter_source, converter_sink, mapfile from cvs import convert_cvs from darcs import darcs_source from git import convert_git from hg import mercurial_source, mercurial_sink -from subversion import svn_source, debugsvnlog +from subversion import debugsvnlog, svn_source, svn_sink import filemap import os, shutil @@ -27,6 +27,7 @@ sink_converters = [ ('hg', mercurial_sink), + ('svn', svn_sink), ] def convertsource(ui, path, type, rev): @@ -59,23 +60,10 @@ self.ui = ui self.opts = opts self.commitcache = {} - self.revmapfile = revmapfile - self.revmapfilefd = None self.authors = {} self.authorfile = None - self.maporder = [] - self.map = {} - try: - origrevmapfile = open(self.revmapfile, 'r') - for l in origrevmapfile: - sv, dv = l[:-1].split() - if sv not in self.map: - self.maporder.append(sv) - self.map[sv] = dv - origrevmapfile.close() - except IOError: - pass + self.map = mapfile(ui, revmapfile) # Read first the dst author map if any authorfile = self.dest.authorfile() @@ -86,6 +74,8 @@ self.readauthormap(opts.get('authors')) self.authorfile = self.dest.authorfile() + self.splicemap = mapfile(ui, ui.config('convert', 'splicemap')) + def walktree(self, heads): '''Return a mapping that identifies the uncommitted parents of every uncommitted changeset.''' @@ -157,22 +147,13 @@ if pl: depth[n] = max([depth[p] for p in pl]) + 1 - s = [(depth[n], self.commitcache[n].date, n) for n in s] + s = [(depth[n], util.parsedate(self.commitcache[n].date), n) + for n in s] s.sort() s = [e[2] for e in s] return s - def mapentry(self, src, dst): - if self.revmapfilefd is None: - try: - self.revmapfilefd = open(self.revmapfile, "a") - except IOError, (errno, strerror): - raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror)) - self.map[src] = dst - self.revmapfilefd.write("%s %s\n" % (src, dst)) - self.revmapfilefd.flush() - def writeauthormap(self): authorfile = self.authorfile if authorfile: @@ -219,7 +200,7 @@ dest = SKIPREV else: dest = self.map[changes] - self.mapentry(rev, dest) + self.map[rev] = dest return files, copies = changes pbranches = [] @@ -245,15 +226,25 @@ # Merely marks that a copy happened. self.dest.copyfile(copyf, f) - parents = [b[0] for b in pbranches] + try: + parents = [self.splicemap[rev]] + self.ui.debug('spliced in %s as parents of %s\n' % + (parents, rev)) + except KeyError: + parents = [b[0] for b in pbranches] newnode = self.dest.putcommit(filenames, parents, commit) - self.mapentry(rev, newnode) + self.source.converted(rev, newnode) + self.map[rev] = newnode def convert(self): + + def recode(s): + return s.decode('utf-8').encode(orig_encoding, 'replace') + try: self.source.before() self.dest.before() - self.source.setrevmap(self.map, self.maporder) + self.source.setrevmap(self.map) self.ui.status("scanning source...\n") heads = self.source.getheads() parents = self.walktree(heads) @@ -268,7 +259,11 @@ desc = self.commitcache[c].desc if "\n" in desc: desc = desc.splitlines()[0] - self.ui.status("%d %s\n" % (num, desc)) + # convert log message to local encoding without using + # tolocal() because util._encoding conver() use it as + # 'utf-8' + self.ui.status("%d %s\n" % (num, recode(desc))) + self.ui.note(_("source: %s\n" % recode(c))) self.copy(c) tags = self.source.gettags() @@ -283,7 +278,7 @@ # write another hash correspondence to override the previous # one so we don't end up with extra tag heads if nrev: - self.mapentry(c, nrev) + self.map[c] = nrev self.writeauthormap() finally: @@ -294,10 +289,13 @@ self.dest.after() finally: self.source.after() - if self.revmapfilefd: - self.revmapfilefd.close() + self.map.close() + +orig_encoding = 'ascii' def convert(ui, src, dest=None, revmapfile=None, **opts): + global orig_encoding + orig_encoding = util._encoding util._encoding = 'UTF-8' if not dest: diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/convert/cvs.py --- a/hgext/convert/cvs.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/convert/cvs.py Sun Feb 03 21:47:07 2008 -0200 @@ -1,9 +1,10 @@ # CVS conversion code inspired by hg-cvs-import and git-cvsimport import os, locale, re, socket +from cStringIO import StringIO from mercurial import util -from common import NoRepo, commit, converter_source +from common import NoRepo, commit, converter_source, checktool class convert_cvs(converter_source): def __init__(self, ui, path, rev=None): @@ -13,6 +14,9 @@ if not os.path.exists(cvs): raise NoRepo("%s does not look like a CVS checkout" % path) + for tool in ('cvsps', 'cvs'): + checktool(tool) + self.changeset = {} self.files = {} self.tags = {} @@ -196,7 +200,7 @@ if conntype != "pserver": if conntype == "rsh": - rsh = os.environ.get("CVS_RSH" or "rsh") + rsh = os.environ.get("CVS_RSH") or "ssh" if user: cmd = [rsh, '-l', user, host] + cmd else: @@ -227,6 +231,20 @@ return self.heads def _getfile(self, name, rev): + + def chunkedread(fp, count): + # file-objects returned by socked.makefile() do not handle + # large read() requests very well. + chunksize = 65536 + output = StringIO() + while count > 0: + data = fp.read(min(count, chunksize)) + if not data: + raise util.Abort("%d bytes missing from remote file" % count) + count -= len(data) + output.write(data) + return output.getvalue() + if rev.endswith("(DEAD)"): raise IOError @@ -245,14 +263,14 @@ self.readp.readline() # entries mode = self.readp.readline()[:-1] count = int(self.readp.readline()[:-1]) - data = self.readp.read(count) + data = chunkedread(self.readp, count) elif line.startswith(" "): data += line[1:] elif line.startswith("M "): pass elif line.startswith("Mbinary "): count = int(self.readp.readline()[:-1]) - data = self.readp.read(count) + data = chunkedread(self.readp, count) else: if line == "ok\n": return (data, "x" in mode and "x" or "") diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/convert/darcs.py --- a/hgext/convert/darcs.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/convert/darcs.py Sun Feb 03 21:47:07 2008 -0200 @@ -1,6 +1,6 @@ # darcs support for the convert extension -from common import NoRepo, commit, converter_source +from common import NoRepo, checktool, commandline, commit, converter_source from mercurial.i18n import _ from mercurial import util import os, shutil, tempfile @@ -17,15 +17,18 @@ except ImportError: ElementTree = None -class darcs_source(converter_source): +class darcs_source(converter_source, commandline): def __init__(self, ui, path, rev=None): - super(darcs_source, self).__init__(ui, path, rev=rev) + converter_source.__init__(self, ui, path, rev=rev) + commandline.__init__(self, ui, 'darcs') # check for _darcs, ElementTree, _darcs/inventory so that we can # easily skip test-convert-darcs if ElementTree is not around if not os.path.exists(os.path.join(path, '_darcs')): raise NoRepo("%s does not look like a darcs repo" % path) + checktool('darcs') + if ElementTree is None: raise util.Abort(_("Python ElementTree module is not available")) @@ -45,7 +48,8 @@ output, status = self.run('init', repodir=self.tmppath) self.checkexit(status) - tree = self.xml('changes', '--xml-output', '--summary') + tree = self.xml('changes', xml_output=True, summary=True, + repodir=self.path) tagname = None child = None for elt in tree.findall('patch'): @@ -65,31 +69,9 @@ self.ui.debug('cleaning up %s\n' % self.tmppath) shutil.rmtree(self.tmppath, ignore_errors=True) - def _run(self, cmd, *args, **kwargs): - cmdline = ['darcs', cmd, '--repodir', kwargs.get('repodir', self.path)] - cmdline += args - cmdline = [util.shellquote(arg) for arg in cmdline] - cmdline += ['<', util.nulldev] - cmdline = ' '.join(cmdline) - self.ui.debug(cmdline, '\n') - return util.popen(cmdline) - - def run(self, cmd, *args, **kwargs): - fp = self._run(cmd, *args, **kwargs) - output = fp.read() - return output, fp.close() - - def checkexit(self, status, output=''): - if status: - if output: - self.ui.warn(_('darcs error:\n')) - self.ui.warn(output) - msg = util.explain_exit(status)[0] - raise util.Abort(_('darcs %s') % msg) - - def xml(self, cmd, *opts): + def xml(self, cmd, **kwargs): etree = ElementTree() - fp = self._run(cmd, *opts) + fp = self._run(cmd, **kwargs) etree.parse(fp) self.checkexit(fp.close()) return etree.getroot() @@ -105,15 +87,15 @@ desc=desc.strip(), parents=self.parents[rev]) def pull(self, rev): - output, status = self.run('pull', self.path, '--all', - '--match', 'hash %s' % rev, - '--no-test', '--no-posthook', - '--external-merge', '/bin/false', + output, status = self.run('pull', self.path, all=True, + match='hash %s' % rev, + no_test=True, no_posthook=True, + external_merge='/bin/false', repodir=self.tmppath) if status: if output.find('We have conflicts in') == -1: self.checkexit(status, output) - output, status = self.run('revert', '--all', repodir=self.tmppath) + output, status = self.run('revert', all=True, repodir=self.tmppath) self.checkexit(status, output) def getchanges(self, rev): diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/convert/filemap.py --- a/hgext/convert/filemap.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/convert/filemap.py Sun Feb 03 21:47:07 2008 -0200 @@ -7,9 +7,10 @@ import shlex from mercurial.i18n import _ from mercurial import util -from common import SKIPREV +from common import SKIPREV, converter_source def rpairs(name): + yield '.', name e = len(name) while e != -1: yield name[:e], name[e+1:] @@ -110,9 +111,9 @@ # touch files we're interested in, but also merges that merge two # or more interesting revisions. -class filemap_source(object): +class filemap_source(converter_source): def __init__(self, ui, baseconverter, filemap): - self.ui = ui + super(filemap_source, self).__init__(ui) self.base = baseconverter self.filemapper = filemapper(ui, filemap) self.commits = {} @@ -134,7 +135,7 @@ def after(self): self.base.after() - def setrevmap(self, revmap, order): + def setrevmap(self, revmap): # rebuild our state to make things restartable # # To avoid calling getcommit for every revision that has already @@ -149,7 +150,7 @@ seen = {SKIPREV: SKIPREV} dummyset = util.set() converted = [] - for rev in order: + for rev in revmap.order: mapped = revmap[rev] wanted = mapped not in seen if wanted: @@ -163,7 +164,7 @@ arg = None converted.append((rev, wanted, arg)) self.convertedorder = converted - return self.base.setrevmap(revmap, order) + return self.base.setrevmap(revmap) def rebuild(self): if self._rebuilt: @@ -350,9 +351,3 @@ def gettags(self): return self.base.gettags() - - def before(self): - pass - - def after(self): - pass diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/convert/git.py --- a/hgext/convert/git.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/convert/git.py Sun Feb 03 21:47:07 2008 -0200 @@ -3,7 +3,7 @@ import os from mercurial import util -from common import NoRepo, commit, converter_source +from common import NoRepo, commit, converter_source, checktool class convert_git(converter_source): # Windows does not support GIT_DIR= construct while other systems @@ -31,6 +31,9 @@ path += "/.git" if not os.path.exists(path + "/objects"): raise NoRepo("%s does not look like a Git repo" % path) + + checktool('git-rev-parse', 'git') + self.path = path def getheads(self): diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/convert/hg.py --- a/hgext/convert/hg.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/convert/hg.py Sun Feb 03 21:47:07 2008 -0200 @@ -1,10 +1,16 @@ # hg backend for convert extension -# Note for hg->hg conversion: Old versions of Mercurial didn't trim -# the whitespace from the ends of commit messages, but new versions -# do. Changesets created by those older versions, then converted, may -# thus have different hashes for changesets that are otherwise -# identical. +# Notes for hg->hg conversion: +# +# * Old versions of Mercurial didn't trim the whitespace from the ends +# of commit messages, but new versions do. Changesets created by +# those older versions, then converted, may thus have different +# hashes for changesets that are otherwise identical. +# +# * By default, the source revision is stored in the converted +# revision. This will cause the converted revision to have a +# different identity than the source. To avoid this, use the +# following option: "--config convert.hg.saverev=false" import os, time @@ -26,8 +32,6 @@ self.repo = hg.repository(self.ui, path) if not self.repo.local(): raise NoRepo(_('%s is not a local Mercurial repo') % path) - ui.status(_('destination %s is a Mercurial repository\n') % - path) except hg.RepoError, err: ui.print_exc() raise NoRepo(err.args[0]) @@ -46,11 +50,13 @@ self.filemapmode = False def before(self): + self.ui.debug(_('run hg sink pre-conversion action\n')) self.wlock = self.repo.wlock() self.lock = self.repo.lock() self.repo.dirstate.clear() def after(self): + self.ui.debug(_('run hg sink post-conversion action\n')) self.repo.dirstate.invalidate() self.lock = None self.wlock = None @@ -191,7 +197,7 @@ except hg.RepoError, inst: tagparent = nullid self.repo.rawcommit([".hgtags"], "update tags", "convert-repo", - date, tagparent, nullid) + date, tagparent, nullid, extra=extra) return hex(self.repo.changelog.tip()) def setfilemapmode(self, active): @@ -200,6 +206,7 @@ class mercurial_source(converter_source): def __init__(self, ui, path, rev=None): converter_source.__init__(self, ui, path, rev) + self.saverev = ui.configbool('convert', 'hg.saverev', True) try: self.repo = hg.repository(self.ui, path) # try to provoke an exception if this isn't really a hg @@ -212,6 +219,7 @@ self.lastrev = None self.lastctx = None self._changescache = None + self.convertfp = None def changectx(self, rev): if self.lastrev != rev: @@ -257,8 +265,12 @@ def getcommit(self, rev): ctx = self.changectx(rev) parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid] + if self.saverev: + crev = rev + else: + crev = None return commit(author=ctx.user(), date=util.datestr(ctx.date()), - desc=ctx.description(), parents=parents, + desc=ctx.description(), rev=crev, parents=parents, branch=ctx.branch(), extra=ctx.extra()) def gettags(self): @@ -275,3 +287,15 @@ return changes[0] + changes[1] + changes[2] + def converted(self, rev, destrev): + if self.convertfp is None: + self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'), + 'a') + self.convertfp.write('%s %s\n' % (destrev, rev)) + self.convertfp.flush() + + def before(self): + self.ui.debug(_('run hg source pre-conversion action\n')) + + def after(self): + self.ui.debug(_('run hg source post-conversion action\n')) diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/convert/subversion.py --- a/hgext/convert/subversion.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/convert/subversion.py Sun Feb 03 21:47:07 2008 -0200 @@ -17,9 +17,13 @@ import locale import os +import re import sys import cPickle as pickle -from mercurial import util +import tempfile + +from mercurial import strutil, util +from mercurial.i18n import _ # Subversion stuff. Works best with very recent Python SVN bindings # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing @@ -28,6 +32,7 @@ from cStringIO import StringIO from common import NoRepo, commit, converter_source, encodeargs, decodeargs +from common import commandline, converter_sink, mapfile try: from svn.core import SubversionException, Pool @@ -46,7 +51,10 @@ except SubversionException: pass if os.path.isdir(path): - return 'file://%s' % os.path.normpath(os.path.abspath(path)) + path = os.path.normpath(os.path.abspath(path)) + if os.name == 'nt': + path = '/' + util.normpath(path) + return 'file://%s' % path return path def optrev(number): @@ -81,6 +89,9 @@ receiver) except SubversionException, (inst, num): pickle.dump(num, fp, protocol) + except IOError: + # Caller may interrupt the iteration + pickle.dump(None, fp, protocol) else: pickle.dump(None, fp, protocol) fp.close() @@ -94,7 +105,53 @@ args = decodeargs(sys.stdin.read()) get_log_child(sys.stdout, *args) +class logstream: + """Interruptible revision log iterator.""" + def __init__(self, stdout): + self._stdout = stdout + + def __iter__(self): + while True: + entry = pickle.load(self._stdout) + try: + orig_paths, revnum, author, date, message = entry + except: + if entry is None: + break + raise SubversionException("child raised exception", entry) + yield entry + + def close(self): + if self._stdout: + self._stdout.close() + self._stdout = None + +def get_log(url, paths, start, end, limit=0, discover_changed_paths=True, + strict_node_history=False): + args = [url, paths, start, end, limit, discover_changed_paths, + strict_node_history] + arg = encodeargs(args) + hgexe = util.hgexecutable() + cmd = '%s debugsvnlog' % util.shellquote(hgexe) + stdin, stdout = os.popen2(cmd, 'b') + stdin.write(arg) + stdin.close() + return logstream(stdout) + # SVN conversion code stolen from bzr-svn and tailor +# +# Subversion looks like a versioned filesystem, branches structures +# are defined by conventions and not enforced by the tool. First, +# we define the potential branches (modules) as "trunk" and "branches" +# children directories. Revisions are then identified by their +# module and revision number (and a repository identifier). +# +# The revision graph is really a tree (or a forest). By default, a +# revision parent is the previous revision in the same module. If the +# module directory is copied/moved from another module then the +# revision is the module root and its parent the source revision in +# the parent module. A revision has at most one parent. +# class svn_source(converter_source): def __init__(self, ui, url, rev=None): super(svn_source, self).__init__(ui, url, rev=rev) @@ -125,7 +182,7 @@ self.ctx = self.transport.client self.base = svn.ra.get_repos_root(self.ra) self.module = self.url[len(self.base):] - self.modulemap = {} # revision, module + self.rootmodule = self.module self.commits = {} self.paths = {} self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding) @@ -144,14 +201,23 @@ except IOError, e: pass - self.last_changed = self.latest(self.module, latest) - - self.head = self.revid(self.last_changed) + self.head = self.latest(self.module, latest) + if not self.head: + raise util.Abort(_('no revision found in module %s') % + self.module.encode(self.encoding)) + self.last_changed = self.revnum(self.head) + self._changescache = None - def setrevmap(self, revmap, order): + if os.path.exists(os.path.join(url, '.svn/entries')): + self.wc = url + else: + self.wc = None + self.convertfp = None + + def setrevmap(self, revmap): lastrevs = {} - for revid in revmap.keys(): + for revid in revmap.iterkeys(): uuid, module, revnum = self.revsplit(revid) lastrevnum = lastrevs.setdefault(module, revnum) if revnum > lastrevnum: @@ -167,46 +233,54 @@ return False def getheads(self): - # detect standard /branches, /tags, /trunk layout + + def getcfgpath(name, rev): + cfgpath = self.ui.config('convert', 'svn.' + name) + path = (cfgpath or name).strip('/') + if not self.exists(path, rev): + if cfgpath: + raise util.Abort(_('expected %s to be at %r, but not found') + % (name, path)) + return None + self.ui.note(_('found %s at %r\n') % (name, path)) + return path + rev = optrev(self.last_changed) - rpath = self.url.strip('/') - cfgtrunk = self.ui.config('convert', 'svn.trunk') - cfgbranches = self.ui.config('convert', 'svn.branches') - cfgtags = self.ui.config('convert', 'svn.tags') - trunk = (cfgtrunk or 'trunk').strip('/') - branches = (cfgbranches or 'branches').strip('/') - tags = (cfgtags or 'tags').strip('/') - if self.exists(trunk, rev) and self.exists(branches, rev) and self.exists(tags, rev): - self.ui.note('found trunk at %r, branches at %r and tags at %r\n' % - (trunk, branches, tags)) - oldmodule = self.module + oldmodule = '' + trunk = getcfgpath('trunk', rev) + tags = getcfgpath('tags', rev) + branches = getcfgpath('branches', rev) + + # If the project has a trunk or branches, we will extract heads + # from them. We keep the project root otherwise. + if trunk: + oldmodule = self.module or '' self.module += '/' + trunk - lt = self.latest(self.module, self.last_changed) - self.head = self.revid(lt) - self.heads = [self.head] + self.head = self.latest(self.module, self.last_changed) + if not self.head: + raise util.Abort(_('no revision found in module %s') % + self.module.encode(self.encoding)) + + # First head in the list is the module's head + self.heads = [self.head] + self.tags = '%s/%s' % (oldmodule , (tags or 'tags')) + + # Check if branches bring a few more heads to the list + if branches: + rpath = self.url.strip('/') branchnames = svn.client.ls(rpath + '/' + branches, rev, False, self.ctx) for branch in branchnames.keys(): - if oldmodule: - module = oldmodule + '/' + branches + '/' + branch - else: - module = '/' + branches + '/' + branch - brevnum = self.latest(module, self.last_changed) - brev = self.revid(brevnum, module) - self.ui.note('found branch %s at %d\n' % (branch, brevnum)) - self.heads.append(brev) + module = '%s/%s/%s' % (oldmodule, branches, branch) + brevid = self.latest(module, self.last_changed) + if not brevid: + self.ui.note(_('ignoring empty branch %s\n') % + branch.encode(self.encoding)) + continue + self.ui.note('found branch %s at %d\n' % + (branch, self.revnum(brevid))) + self.heads.append(brevid) - if oldmodule: - self.tags = '%s/%s' % (oldmodule, tags) - else: - self.tags = '/%s' % tags - - elif cfgtrunk or cfgbranches or cfgtags: - raise util.Abort('trunk/branch/tags layout expected, but not found') - else: - self.ui.note('working with one branch\n') - self.heads = [self.head] - self.tags = tags return self.heads def getfile(self, file, rev): @@ -223,7 +297,17 @@ self._changescache = None self.modecache = {} (paths, parents) = self.paths[rev] - files, copies = self.expandpaths(rev, paths, parents) + if parents: + files, copies = self.expandpaths(rev, paths, parents) + else: + # Perform a full checkout on roots + uuid, module, revnum = self.revsplit(rev) + entries = svn.client.ls(self.base + module, optrev(revnum), + True, self.ctx) + files = [n for n,e in entries.iteritems() + if e.kind == svn.core.svn_node_file] + copies = {} + files.sort() files = zip(files, [rev] * len(files)) @@ -241,45 +325,26 @@ uuid, module, revnum = self.revsplit(rev) self.module = module self.reparent(module) + # We assume that: + # - requests for revisions after "stop" come from the + # revision graph backward traversal. Cache all of them + # down to stop, they will be used eventually. + # - requests for revisions before "stop" come to get + # isolated branches parents. Just fetch what is needed. stop = self.lastrevs.get(module, 0) - self._fetch_revisions(from_revnum=revnum, to_revnum=stop) + if revnum < stop: + stop = revnum + 1 + self._fetch_revisions(revnum, stop) commit = self.commits[rev] # caller caches the result, so free it here to release memory del self.commits[rev] return commit - def get_log(self, paths, start, end, limit=0, discover_changed_paths=True, - strict_node_history=False): - - def parent(fp): - while True: - entry = pickle.load(fp) - try: - orig_paths, revnum, author, date, message = entry - except: - if entry is None: - break - raise SubversionException("child raised exception", entry) - yield entry - - args = [self.url, paths, start, end, limit, discover_changed_paths, - strict_node_history] - arg = encodeargs(args) - hgexe = util.hgexecutable() - cmd = '%s debugsvnlog' % util.shellquote(hgexe) - stdin, stdout = os.popen2(cmd, 'b') - - stdin.write(arg) - stdin.close() - - for p in parent(stdout): - yield p - def gettags(self): tags = {} start = self.revnum(self.head) try: - for entry in self.get_log([self.tags], 0, start): + for entry in get_log(self.url, [self.tags], 0, start): orig_paths, revnum, author, date, message = entry for path in orig_paths: if not path.startswith(self.tags+'/'): @@ -293,6 +358,15 @@ self.ui.note('no tags found at revision %d\n' % start) return tags + def converted(self, rev, destrev): + if not self.wc: + return + if self.convertfp is None: + self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'), + 'a') + self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev))) + self.convertfp.flush() + # -- helper functions -- def revid(self, revnum, module=None): @@ -315,7 +389,11 @@ return uuid, mod, revnum def latest(self, path, stop=0): - 'find the latest revision affecting path, up to stop' + """Find the latest revid affecting path, up to stop. It may return + a revision in a different module, since a branch may be moved without + a change being reported. Return None if computed module does not + belong to rootmodule subtree. + """ if not stop: stop = svn.ra.get_latest_revnum(self.ra) try: @@ -327,7 +405,31 @@ if not dirent: raise util.Abort('%s not found up to revision %d' % (path, stop)) - return dirent.created_rev + # stat() gives us the previous revision on this line of development, but + # it might be in *another module*. Fetch the log and detect renames down + # to the latest revision. + stream = get_log(self.url, [path], stop, dirent.created_rev) + try: + for entry in stream: + paths, revnum, author, date, message = entry + if revnum <= dirent.created_rev: + break + + for p in paths: + if not path.startswith(p) or not paths[p].copyfrom_path: + continue + newpath = paths[p].copyfrom_path + path[len(p):] + self.ui.debug("branch renamed from %s to %s at %d\n" % + (path, newpath, revnum)) + path = newpath + break + finally: + stream.close() + + if not path.startswith(self.rootmodule): + self.ui.debug(_('ignoring foreign branch %r\n') % path) + return None + return self.revid(dirent.created_rev, path) def get_blacklist(self): """Avoid certain revision numbers. @@ -375,13 +477,11 @@ entries = [] copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions. copies = {} - revnum = self.revnum(rev) - if revnum in self.modulemap: - new_module = self.modulemap[revnum] - if new_module != self.module: - self.module = new_module - self.reparent(self.module) + new_module, revnum = self.revsplit(rev)[1:] + if new_module != self.module: + self.module = new_module + self.reparent(self.module) for path, ent in paths: entrypath = get_entry_from_path(path, module=self.module) @@ -392,7 +492,9 @@ 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)) + self.ui.debug("Copied to %s from %s@%s\n" % + (entrypath, 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 @@ -405,12 +507,9 @@ # 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) + # parents cannot be empty here, you cannot remove things from + # a root revision. + uuid, old_module, fromrev = self.revsplit(parents[0]) basepath = old_module + "/" + get_entry_from_path(path, module=self.module) entrypath = old_module + "/" + get_entry_from_path(path, module=self.module) @@ -486,6 +585,9 @@ # If the directory just had a prop change, # then we shouldn't need to look for its children. + if ent.action == 'M': + continue + # 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 @@ -546,42 +648,46 @@ return (util.unique(entries), copies) - def _fetch_revisions(self, from_revnum = 0, to_revnum = 347): + def _fetch_revisions(self, from_revnum, to_revnum): + if from_revnum < to_revnum: + from_revnum, to_revnum = to_revnum, from_revnum + self.child_cset = None def parselogentry(orig_paths, revnum, author, date, message): + """Return the parsed commit object or None, and True if + the revision is a branch root. + """ self.ui.debug("parsing revision %d (%d changes)\n" % (revnum, len(orig_paths))) - if revnum in self.modulemap: - new_module = self.modulemap[revnum] - if new_module != self.module: - self.module = new_module - self.reparent(self.module) - + branched = False rev = self.revid(revnum) # branch log might return entries for a parent we already have - if (rev in self.commits or - (revnum < self.lastrevs.get(self.module, 0))): - return + + if (rev in self.commits or revnum < to_revnum): + return None, branched parents = [] - # check whether this revision is the start of a branch - if self.module in orig_paths: - ent = orig_paths[self.module] + # check whether this revision is the start of a branch or part + # of a branch renaming + orig_paths = orig_paths.items() + orig_paths.sort() + root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)] + if root_paths: + path, ent = root_paths[-1] if ent.copyfrom_path: + branched = True + newpath = ent.copyfrom_path + self.module[len(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)) + previd = self.latest(newpath, ent.copyfrom_rev) + if previd is not None: + parents = [previd] + prevmodule, prevnum = self.revsplit(previd)[1:] + self.ui.note('found parent of branch %s at %d: %s\n' % + (self.module, prevnum, prevmodule)) else: self.ui.debug("No copyfrom path, don't know what to do.\n") - self.modulemap[revnum] = self.module # track backwards in time - - orig_paths = orig_paths.items() - orig_paths.sort() paths = [] # filter out unrelated paths for path, ent in orig_paths: @@ -590,8 +696,6 @@ continue paths.append((path, ent)) - self.paths[rev] = (paths, parents) - # Example SVN datetime. Includes microseconds. # ISO-8601 conformant # '2007-01-04T17:35:00.902377Z' @@ -614,23 +718,52 @@ rev=rev.encode('utf-8')) self.commits[rev] = cset + # The parents list is *shared* among self.paths and the + # commit object. Both will be updated below. + self.paths[rev] = (paths, cset.parents) if self.child_cset and not self.child_cset.parents: - self.child_cset.parents = [rev] + self.child_cset.parents[:] = [rev] self.child_cset = cset + return cset, branched self.ui.note('fetching revision log for "%s" from %d to %d\n' % (self.module, from_revnum, to_revnum)) try: - for entry in self.get_log([self.module], from_revnum, to_revnum): - orig_paths, revnum, author, date, message = entry - if self.is_blacklisted(revnum): - self.ui.note('skipping blacklisted revision %d\n' % revnum) - continue - if orig_paths is None: - self.ui.debug('revision %d has no entries\n' % revnum) - continue - parselogentry(orig_paths, revnum, author, date, message) + firstcset = None + branched = False + stream = get_log(self.url, [self.module], from_revnum, to_revnum) + try: + for entry in stream: + paths, revnum, author, date, message = entry + if self.is_blacklisted(revnum): + self.ui.note('skipping blacklisted revision %d\n' + % revnum) + continue + if paths is None: + self.ui.debug('revision %d has no entries\n' % revnum) + continue + cset, branched = parselogentry(paths, revnum, author, + date, message) + if cset: + firstcset = cset + if branched: + break + finally: + stream.close() + + if not branched and firstcset and not firstcset.parents: + # The first revision of the sequence (the last fetched one) + # has invalid parents if not a branch root. Find the parent + # revision now, if any. + try: + firstrevnum = self.revnum(firstcset.rev) + if firstrevnum > 1: + latest = self.latest(self.module, firstrevnum - 1) + if latest: + firstcset.parents.append(latest) + except util.Abort: + pass except SubversionException, (inst, num): if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION: raise NoSuchRevision(branch=self, @@ -642,9 +775,9 @@ # 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] + new_module, revnum = self.revsplit(rev)[1:] + if self.module != new_module: + self.module = new_module self.reparent(self.module) info = svn.ra.get_file(self.ra, file, revnum, io) if isinstance(info, list): @@ -669,3 +802,240 @@ pool = Pool() rpath = '/'.join([self.base, path]).strip('/') return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()] + +pre_revprop_change = '''#!/bin/sh + +REPOS="$1" +REV="$2" +USER="$3" +PROPNAME="$4" +ACTION="$5" + +if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi +if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi +if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi + +echo "Changing prohibited revision property" >&2 +exit 1 +''' + +class svn_sink(converter_sink, commandline): + commit_re = re.compile(r'Committed revision (\d+).', re.M) + + def prerun(self): + if self.wc: + os.chdir(self.wc) + + def postrun(self): + if self.wc: + os.chdir(self.cwd) + + def join(self, name): + return os.path.join(self.wc, '.svn', name) + + def revmapfile(self): + return self.join('hg-shamap') + + def authorfile(self): + return self.join('hg-authormap') + + def __init__(self, ui, path): + converter_sink.__init__(self, ui, path) + commandline.__init__(self, ui, 'svn') + self.delete = [] + self.setexec = [] + self.delexec = [] + self.copies = [] + self.wc = None + self.cwd = os.getcwd() + + path = os.path.realpath(path) + + created = False + if os.path.isfile(os.path.join(path, '.svn', 'entries')): + self.wc = path + self.run0('update') + else: + wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc') + + if os.path.isdir(os.path.dirname(path)): + if not os.path.exists(os.path.join(path, 'db', 'fs-type')): + ui.status(_('initializing svn repo %r\n') % + os.path.basename(path)) + commandline(ui, 'svnadmin').run0('create', path) + created = path + path = util.normpath(path) + if not path.startswith('/'): + path = '/' + path + path = 'file://' + path + + ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath)) + self.run0('checkout', path, wcpath) + + self.wc = wcpath + self.opener = util.opener(self.wc) + self.wopener = util.opener(self.wc) + self.childmap = mapfile(ui, self.join('hg-childmap')) + self.is_exec = util.checkexec(self.wc) and util.is_exec or None + + if created: + hook = os.path.join(created, 'hooks', 'pre-revprop-change') + fp = open(hook, 'w') + fp.write(pre_revprop_change) + fp.close() + util.set_flags(hook, "x") + + xport = transport.SvnRaTransport(url=geturl(path)) + self.uuid = svn.ra.get_uuid(xport.ra) + + def wjoin(self, *names): + return os.path.join(self.wc, *names) + + def putfile(self, filename, flags, data): + if 'l' in flags: + self.wopener.symlink(data, filename) + else: + try: + if os.path.islink(self.wjoin(filename)): + os.unlink(filename) + except OSError: + pass + self.wopener(filename, 'w').write(data) + + if self.is_exec: + was_exec = self.is_exec(self.wjoin(filename)) + else: + # On filesystems not supporting execute-bit, there is no way + # to know if it is set but asking subversion. Setting it + # systematically is just as expensive and much simpler. + was_exec = 'x' not in flags + + util.set_flags(self.wjoin(filename), flags) + if was_exec: + if 'x' not in flags: + self.delexec.append(filename) + else: + if 'x' in flags: + self.setexec.append(filename) + + def delfile(self, name): + self.delete.append(name) + + def copyfile(self, source, dest): + self.copies.append([source, dest]) + + def _copyfile(self, source, dest): + # SVN's copy command pukes if the destination file exists, but + # our copyfile method expects to record a copy that has + # already occurred. Cross the semantic gap. + wdest = self.wjoin(dest) + exists = os.path.exists(wdest) + if exists: + fd, tempname = tempfile.mkstemp( + prefix='hg-copy-', dir=os.path.dirname(wdest)) + os.close(fd) + os.unlink(tempname) + os.rename(wdest, tempname) + try: + self.run0('copy', source, dest) + finally: + if exists: + try: + os.unlink(wdest) + except OSError: + pass + os.rename(tempname, wdest) + + def dirs_of(self, files): + dirs = set() + for f in files: + if os.path.isdir(self.wjoin(f)): + dirs.add(f) + for i in strutil.rfindall(f, '/'): + dirs.add(f[:i]) + return dirs + + def add_dirs(self, files): + add_dirs = [d for d in self.dirs_of(files) + if not os.path.exists(self.wjoin(d, '.svn', 'entries'))] + if add_dirs: + add_dirs.sort() + self.xargs(add_dirs, 'add', non_recursive=True, quiet=True) + return add_dirs + + def add_files(self, files): + if files: + self.xargs(files, 'add', quiet=True) + return files + + def tidy_dirs(self, names): + dirs = list(self.dirs_of(names)) + dirs.sort(reverse=True) + deleted = [] + for d in dirs: + wd = self.wjoin(d) + if os.listdir(wd) == '.svn': + self.run0('delete', d) + deleted.append(d) + return deleted + + def addchild(self, parent, child): + self.childmap[parent] = child + + def revid(self, rev): + return u"svn:%s@%s" % (self.uuid, rev) + + def putcommit(self, files, parents, commit): + for parent in parents: + try: + return self.revid(self.childmap[parent]) + except KeyError: + pass + entries = set(self.delete) + files = util.frozenset(files) + entries.update(self.add_dirs(files.difference(entries))) + if self.copies: + for s, d in self.copies: + self._copyfile(s, d) + self.copies = [] + if self.delete: + self.xargs(self.delete, 'delete') + self.delete = [] + entries.update(self.add_files(files.difference(entries))) + entries.update(self.tidy_dirs(entries)) + if self.delexec: + self.xargs(self.delexec, 'propdel', 'svn:executable') + self.delexec = [] + if self.setexec: + self.xargs(self.setexec, 'propset', 'svn:executable', '*') + self.setexec = [] + + fd, messagefile = tempfile.mkstemp(prefix='hg-convert-') + fp = os.fdopen(fd, 'w') + fp.write(commit.desc) + fp.close() + try: + output = self.run0('commit', + username=util.shortuser(commit.author), + file=messagefile, + encoding='utf-8') + try: + rev = self.commit_re.search(output).group(1) + except AttributeError: + self.ui.warn(_('unexpected svn output:\n')) + self.ui.warn(output) + raise util.Abort(_('unable to cope with svn output')) + if commit.rev: + self.run('propset', 'hg:convert-rev', commit.rev, + revprop=True, revision=rev) + if commit.branch and commit.branch != 'default': + self.run('propset', 'hg:convert-branch', commit.branch, + revprop=True, revision=rev) + for parent in parents: + self.addchild(parent, rev) + return self.revid(rev) + finally: + os.unlink(messagefile) + + def puttags(self, tags): + self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n')) diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/fetch.py --- a/hgext/fetch.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/fetch.py Sun Feb 03 21:47:07 2008 -0200 @@ -43,7 +43,8 @@ if not err: mod, add, rem = repo.status()[:3] message = (cmdutil.logmessage(opts) or - (_('Automated merge with %s') % other.url())) + (_('Automated merge with %s') % + util.removeauth(other.url()))) n = repo.commit(mod + add + rem, message, opts['user'], opts['date'], force_editor=opts.get('force_editor')) @@ -54,7 +55,8 @@ cmdutil.setremoteconfig(ui, opts) other = hg.repository(ui, ui.expandpath(source)) - ui.status(_('pulling from %s\n') % ui.expandpath(source)) + ui.status(_('pulling from %s\n') % + util.hidepassword(ui.expandpath(source))) revs = None if opts['rev'] and not other.local(): raise util.Abort(_("fetch -r doesn't work for remote repositories yet")) diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/gpg.py --- a/hgext/gpg.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/gpg.py Sun Feb 03 21:47:07 2008 -0200 @@ -249,7 +249,7 @@ message = opts['message'] if not message: message = "\n".join([_("Added signature for changeset %s") - % hgnode.hex(n) + % hgnode.short(n) for n in nodes]) try: repo.commit([".hgsigs"], message, opts['user'], opts['date']) diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/graphlog.py --- a/hgext/graphlog.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/graphlog.py Sun Feb 03 21:47:07 2008 -0200 @@ -5,11 +5,12 @@ # This software may be used and distributed according to the terms of # the GNU General Public License, incorporated herein by reference. +import os import sys from mercurial.cmdutil import revrange, show_changeset from mercurial.i18n import _ from mercurial.node import nullid, nullrev -from mercurial.util import Abort +from mercurial.util import Abort, canonpath def revision_grapher(repo, start_rev, stop_rev): """incremental revision grapher @@ -63,6 +64,62 @@ revs = next_revs curr_rev -= 1 +def filelog_grapher(repo, path, start_rev, stop_rev): + """incremental file log grapher + + This generator function walks through the revision history of a + single file from revision start_rev to revision stop_rev (which must + be less than or equal to start_rev) and for each revision emits + tuples with the following elements: + + - Current revision. + - Current node. + - Column of the current node in the set of ongoing edges. + - Edges; a list of (col, next_col) indicating the edges between + the current node and its parents. + - Number of columns (ongoing edges) in the current revision. + - The difference between the number of columns (ongoing edges) + in the next revision and the number of columns (ongoing edges) + in the current revision. That is: -1 means one column removed; + 0 means no columns added or removed; 1 means one column added. + """ + + assert start_rev >= stop_rev + curr_rev = start_rev + revs = [] + filerev = repo.file(path).count() - 1 + while filerev >= 0: + fctx = repo.filectx(path, fileid=filerev) + + # Compute revs and next_revs. + if filerev not in revs: + revs.append(filerev) + rev_index = revs.index(filerev) + next_revs = revs[:] + + # Add parents to next_revs. + parents = [f.filerev() for f in fctx.parents() if f.path() == path] + parents_to_add = [] + for parent in parents: + if parent not in next_revs: + parents_to_add.append(parent) + parents_to_add.sort() + next_revs[rev_index:rev_index + 1] = parents_to_add + + edges = [] + for parent in parents: + edges.append((rev_index, next_revs.index(parent))) + + changerev = fctx.linkrev() + if changerev <= start_rev: + node = repo.changelog.node(changerev) + n_columns_diff = len(next_revs) - len(revs) + yield (changerev, node, rev_index, edges, len(revs), n_columns_diff) + if changerev <= stop_rev: + break + revs = next_revs + filerev -= 1 + def get_rev_parents(repo, rev): return [x for x in repo.changelog.parentrevs(rev) if x != nullrev] @@ -141,7 +198,7 @@ else: return (repo.changelog.count() - 1, 0) -def graphlog(ui, repo, **opts): +def graphlog(ui, repo, path=None, **opts): """show revision history alongside an ASCII revision graph Print a revision history alongside a revision graph drawn with @@ -157,7 +214,11 @@ if start_rev == nullrev: return cs_printer = show_changeset(ui, repo, opts) - grapher = revision_grapher(repo, start_rev, stop_rev) + if path: + cpath = canonpath(repo.root, os.getcwd(), path) + grapher = filelog_grapher(repo, cpath, start_rev, stop_rev) + else: + grapher = revision_grapher(repo, start_rev, stop_rev) repo_parents = repo.dirstate.parents() prev_n_columns_diff = 0 prev_node_index = 0 @@ -261,5 +322,5 @@ ('r', 'rev', [], _('show the specified revision or range')), ('', 'style', '', _('display using template map file')), ('', 'template', '', _('display with template'))], - _('hg glog [OPTION]...')), + _('hg glog [OPTION]... [FILE]')), } diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/hbisect.py --- a/hgext/hbisect.py Sun Feb 03 21:03:46 2008 -0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,313 +0,0 @@ -# bisect extension for mercurial -# -# Copyright 2005, 2006 Benoit Boissinot -# Inspired by git bisect, extension skeleton taken from mq.py. -# -# This software may be used and distributed according to the terms -# of the GNU General Public License, incorporated herein by reference. - -from mercurial.i18n import _ -from mercurial import hg, util, commands, cmdutil -import os, sys, sets - -versionstr = "0.0.3" - -def lookup_rev(ui, repo, rev=None): - """returns rev or the checked-out revision if rev is None""" - if not rev is None: - return repo.lookup(rev) - parents = [p for p in repo.dirstate.parents() if p != hg.nullid] - if len(parents) != 1: - raise util.Abort(_("unexpected number of parents, " - "please commit or revert")) - return parents.pop() - -def check_clean(ui, repo): - modified, added, removed, deleted, unknown = repo.status()[:5] - if modified or added or removed: - ui.warn("Repository is not clean, please commit or revert\n") - sys.exit(1) - -class bisect(object): - """dichotomic search in the DAG of changesets""" - def __init__(self, ui, repo): - self.repo = repo - self.path = repo.join("bisect") - self.opener = util.opener(self.path) - self.ui = ui - self.goodrevs = [] - self.badrev = None - self.good_path = "good" - self.bad_path = "bad" - self.is_reset = False - - if os.path.exists(os.path.join(self.path, self.good_path)): - self.goodrevs = self.opener(self.good_path).read().splitlines() - self.goodrevs = [hg.bin(x) for x in self.goodrevs] - if os.path.exists(os.path.join(self.path, self.bad_path)): - r = self.opener(self.bad_path).read().splitlines() - if r: - self.badrev = hg.bin(r.pop(0)) - - def write(self): - if self.is_reset: - return - if not os.path.isdir(self.path): - os.mkdir(self.path) - f = self.opener(self.good_path, "w") - f.write("\n".join([hg.hex(r) for r in self.goodrevs])) - if len(self.goodrevs) > 0: - f.write("\n") - f = self.opener(self.bad_path, "w") - if self.badrev: - f.write(hg.hex(self.badrev) + "\n") - - def init(self): - """start a new bisection""" - if os.path.isdir(self.path): - raise util.Abort(_("bisect directory already exists\n")) - os.mkdir(self.path) - check_clean(self.ui, self.repo) - return 0 - - def reset(self): - """finish a bisection""" - if os.path.isdir(self.path): - sl = [os.path.join(self.path, p) - for p in [self.bad_path, self.good_path]] - for s in sl: - if os.path.exists(s): - os.unlink(s) - os.rmdir(self.path) - # Not sure about this - #self.ui.write("Going back to tip\n") - #self.repo.update(self.repo.changelog.tip()) - self.is_reset = True - return 0 - - def num_ancestors(self, head=None, stop=None): - """ - returns a dict with the mapping: - node -> number of ancestors (self included) - for all nodes who are ancestor of head and - not in stop. - """ - if head is None: - head = self.badrev - return self.__ancestors_and_nb_ancestors(head, stop)[1] - - def ancestors(self, head=None, stop=None): - """ - returns the set of the ancestors of head (self included) - who are not in stop. - """ - if head is None: - head = self.badrev - return self.__ancestors_and_nb_ancestors(head, stop)[0] - - def __ancestors_and_nb_ancestors(self, head, stop=None): - """ - if stop is None then ancestors of goodrevs are used as - lower limit. - - returns (anc, n_child) where anc is the set of the ancestors of head - and n_child is a dictionary with the following mapping: - node -> number of ancestors (self included) - """ - cl = self.repo.changelog - if not stop: - stop = sets.Set([]) - for i in xrange(len(self.goodrevs)-1, -1, -1): - g = self.goodrevs[i] - if g in stop: - continue - stop.update(cl.reachable(g)) - def num_children(a): - """ - returns a dictionnary with the following mapping - node -> [number of children, empty set] - """ - d = {a: [0, sets.Set([])]} - for i in xrange(cl.rev(a)+1): - n = cl.node(i) - if not d.has_key(n): - d[n] = [0, sets.Set([])] - parents = [p for p in cl.parents(n) if p != hg.nullid] - for p in parents: - d[p][0] += 1 - return d - - if head in stop: - raise util.Abort(_("Inconsistent state, %s:%s is good and bad") - % (cl.rev(head), hg.short(head))) - n_child = num_children(head) - for i in xrange(cl.rev(head)+1): - n = cl.node(i) - parents = [p for p in cl.parents(n) if p != hg.nullid] - for p in parents: - n_child[p][0] -= 1 - if not n in stop: - n_child[n][1].union_update(n_child[p][1]) - if n_child[p][0] == 0: - n_child[p] = len(n_child[p][1]) - if not n in stop: - n_child[n][1].add(n) - if n_child[n][0] == 0: - if n == head: - anc = n_child[n][1] - n_child[n] = len(n_child[n][1]) - return anc, n_child - - def next(self): - if not self.badrev: - raise util.Abort(_("You should give at least one bad revision")) - if not self.goodrevs: - self.ui.warn(_("No good revision given\n")) - self.ui.warn(_("Marking the first revision as good\n")) - ancestors, num_ancestors = self.__ancestors_and_nb_ancestors( - self.badrev) - tot = len(ancestors) - if tot == 1: - if ancestors.pop() != self.badrev: - raise util.Abort(_("Could not find the first bad revision")) - self.ui.write(_("The first bad revision is:\n")) - displayer = cmdutil.show_changeset(self.ui, self.repo, {}) - displayer.show(changenode=self.badrev) - return None - best_rev = None - best_len = -1 - for n in ancestors: - l = num_ancestors[n] - l = min(l, tot - l) - if l > best_len: - best_len = l - best_rev = n - assert best_rev is not None - nb_tests = 0 - q, r = divmod(tot, 2) - while q: - nb_tests += 1 - q, r = divmod(q, 2) - msg = _("Testing changeset %s:%s (%s changesets remaining, " - "~%s tests)\n") % (self.repo.changelog.rev(best_rev), - hg.short(best_rev), tot, nb_tests) - self.ui.write(msg) - return best_rev - - def autonext(self): - """find and update to the next revision to test""" - check_clean(self.ui, self.repo) - rev = self.next() - if rev is not None: - return hg.clean(self.repo, rev) - - def good(self, rev): - self.goodrevs.append(rev) - - def autogood(self, rev=None): - """mark revision as good and update to the next revision to test""" - check_clean(self.ui, self.repo) - rev = lookup_rev(self.ui, self.repo, rev) - self.good(rev) - if self.badrev: - return self.autonext() - - def bad(self, rev): - self.badrev = rev - - def autobad(self, rev=None): - """mark revision as bad and update to the next revision to test""" - check_clean(self.ui, self.repo) - rev = lookup_rev(self.ui, self.repo, rev) - self.bad(rev) - if self.goodrevs: - self.autonext() - -# should we put it in the class ? -def test(ui, repo, rev): - """test the bisection code""" - b = bisect(ui, repo) - rev = repo.lookup(rev) - ui.write("testing with rev %s\n" % hg.hex(rev)) - anc = b.ancestors() - while len(anc) > 1: - if not rev in anc: - ui.warn("failure while bisecting\n") - sys.exit(1) - ui.write("it worked :)\n") - new_rev = b.next() - ui.write("choosing if good or bad\n") - if rev in b.ancestors(head=new_rev): - b.bad(new_rev) - ui.write("it is bad\n") - else: - b.good(new_rev) - ui.write("it is good\n") - anc = b.ancestors() - #repo.update(new_rev, force=True) - for v in anc: - if v != rev: - ui.warn("fail to found cset! :(\n") - return 1 - ui.write("Found bad cset: %s\n" % hg.hex(b.badrev)) - ui.write("Everything is ok :)\n") - return 0 - -def bisect_run(ui, repo, cmd=None, *args): - """Dichotomic search in the DAG of changesets - -This extension helps to find changesets which cause problems. -To use, mark the earliest changeset you know introduces the problem -as bad, then mark the latest changeset which is free from the problem -as good. Bisect will update your working directory to a revision for -testing. Once you have performed tests, mark the working directory -as bad or good and bisect will either update to another candidate -changeset or announce that it has found the bad revision. - -Note: bisect expects bad revisions to be descendants of good revisions. -If you are looking for the point at which a problem was fixed, then make -the problem-free state "bad" and the problematic state "good." - -For subcommands see "hg bisect help\" - """ - def help_(cmd=None, *args): - """show help for a given bisect subcommand or all subcommands""" - cmdtable = bisectcmdtable - if cmd: - doc = cmdtable[cmd][0].__doc__ - synopsis = cmdtable[cmd][2] - ui.write(synopsis + "\n") - ui.write("\n" + doc + "\n") - return - ui.write(_("list of subcommands for the bisect extension\n\n")) - cmds = cmdtable.keys() - cmds.sort() - m = max([len(c) for c in cmds]) - for cmd in cmds: - doc = cmdtable[cmd][0].__doc__.splitlines(0)[0].rstrip() - ui.write(" %-*s %s\n" % (m, cmd, doc)) - - b = bisect(ui, repo) - bisectcmdtable = { - "init": (b.init, 0, _("hg bisect init")), - "bad": (b.autobad, 1, _("hg bisect bad []")), - "good": (b.autogood, 1, _("hg bisect good []")), - "next": (b.autonext, 0, _("hg bisect next")), - "reset": (b.reset, 0, _("hg bisect reset")), - "help": (help_, 1, _("hg bisect help []")), - } - - if not bisectcmdtable.has_key(cmd): - ui.warn(_("bisect: Unknown sub-command\n")) - return help_() - if len(args) > bisectcmdtable[cmd][1]: - ui.warn(_("bisect: Too many arguments\n")) - return help_() - ret = bisectcmdtable[cmd][0](*args) - b.write() - return ret - -cmdtable = { - "bisect": (bisect_run, [], _("hg bisect [help|init|reset|next|good|bad]")), - #"bisect-test": (test, [], "hg bisect-test rev"), -} diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/hgk.py --- a/hgext/hgk.py Sun Feb 03 21:03:46 2008 -0200 +++ b/hgext/hgk.py Sun Feb 03 21:47:07 2008 -0200 @@ -13,7 +13,7 @@ # querying of information, and an extension to mercurial named hgk.py, # which provides hooks for hgk to get information. hgk can be found in # the contrib directory, and hgk.py can be found in the hgext -# directory. +# directory. # # To load the hgext.py extension, add it to your .hgrc file (you have # to use your global $HOME/.hgrc file, not one in a repository). You @@ -45,7 +45,7 @@ # Revisions context menu will now display additional entries to fire # vdiff on hovered and selected revisions. -import sys, os +import os from mercurial import hg, fancyopts, commands, ui, util, patch, revlog def difftree(ui, repo, node1=None, node2=None, *files, **opts): @@ -61,17 +61,14 @@ for f in modified: # TODO get file permissions - print ":100664 100664 %s %s M\t%s\t%s" % (hg.short(mmap[f]), - hg.short(mmap2[f]), - f, f) + ui.write(":100664 100664 %s %s M\t%s\t%s\n" % + (hg.short(mmap[f]), hg.short(mmap2[f]), f, f)) for f in added: - print ":000000 100664 %s %s N\t%s\t%s" % (empty, - hg.short(mmap2[f]), - f, f) + ui.write(":000000 100664 %s %s N\t%s\t%s\n" % + (empty, hg.short(mmap2[f]), f, f)) for f in removed: - print ":100664 000000 %s %s D\t%s\t%s" % (hg.short(mmap[f]), - empty, - f, f) + ui.write(":100664 000000 %s %s D\t%s\t%s\n" % + (hg.short(mmap[f]), empty, f, f)) ## while True: @@ -93,7 +90,7 @@ node1 = repo.changelog.parents(node1)[0] if opts['patch']: if opts['pretty']: - catcommit(repo, node2, "") + catcommit(ui, repo, node2, "") patch.diff(repo, node1, node2, files=files, opts=patch.diffopts(ui, {'git': True})) @@ -102,14 +99,14 @@ if not opts['stdin']: break -def catcommit(repo, n, prefix, ctx=None): +def catcommit(ui, repo, n, prefix, ctx=None): nlprefix = '\n' + prefix; if ctx is None: ctx = repo.changectx(n) (p1, p2) = ctx.parents() - print "tree %s" % (hg.short(ctx.changeset()[0])) # use ctx.node() instead ?? - if p1: print "parent %s" % (hg.short(p1.node())) - if p2: print "parent %s" % (hg.short(p2.node())) + ui.write("tree %s\n" % hg.short(ctx.changeset()[0])) # use ctx.node() instead ?? + if p1: ui.write("parent %s\n" % hg.short(p1.node())) + if p2: ui.write("parent %s\n" % hg.short(p2.node())) date = ctx.date() description = ctx.description().replace("\0", "") lines = description.splitlines() @@ -118,23 +115,24 @@ else: committer = ctx.user() - print "author %s %s %s" % (ctx.user(), int(date[0]), date[1]) - print "committer %s %s %s" % (committer, int(date[0]), date[1]) - print "revision %d" % ctx.rev() - print "" + ui.write("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1])) + ui.write("committer %s %s %s\n" % (committer, int(date[0]), date[1])) + ui.write("revision %d\n" % ctx.rev()) + ui.write("branch %s\n\n" % ctx.branch()) + if prefix != "": - print "%s%s" % (prefix, description.replace('\n', nlprefix).strip()) + ui.write("%s%s\n" % (prefix, description.replace('\n', nlprefix).strip())) else: - print description + ui.write(description + "\n") if prefix: - sys.stdout.write('\0') + ui.write('\0') def base(ui, repo, node1, node2): """Output common ancestor information""" node1 = repo.lookup(node1) node2 = repo.lookup(node2) n = repo.changelog.ancestor(node1, node2) - print hg.short(n) + ui.write(hg.short(n) + "\n") def catfile(ui, repo, type=None, r=None, **opts): """cat a specific revision""" @@ -157,10 +155,10 @@ while r: if type != "commit": - sys.stderr.write("aborting hg cat-file only understands commits\n") - sys.exit(1); + ui.warn("aborting hg cat-file only understands commits\n") + return 1; n = repo.lookup(r) - catcommit(repo, n, prefix) + catcommit(ui, repo, n, prefix) if opts['stdin']: try: (type, r) = raw_input().split(' '); @@ -174,7 +172,7 @@ # telling you which commits are reachable from the supplied ones via # a bitmask based on arg position. # you can specify a commit to stop at by starting the sha1 with ^ -def revtree(args, repo, full="tree", maxnr=0, parents=False): +def revtree(ui, args, repo, full="tree", maxnr=0, parents=False): def chlogwalk(): count = repo.changelog.count() i = count @@ -259,24 +257,24 @@ if pp[1] != hg.nullid: parentstr += " " + hg.short(pp[1]) if not full: - print hg.short(n) + parentstr + ui.write("%s%s\n" % (hg.short(n), parentstr)) elif full == "commit": - print hg.short(n) + parentstr - catcommit(repo, n, ' ', ctx) + ui.write("%s%s\n" % (hg.short(n), parentstr)) + catcommit(ui, repo, n, ' ', ctx) else: (p1, p2) = repo.changelog.parents(n) (h, h1, h2) = map(hg.short, (n, p1, p2)) (i1, i2) = map(repo.changelog.rev, (p1, p2)) date = ctx.date()[0] - print "%s %s:%s" % (date, h, mask), + ui.write("%s %s:%s" % (date, h, mask)) mask = is_reachable(want_sha1, reachable, p1) if i1 != hg.nullrev and mask > 0: - print "%s:%s " % (h1, mask), + ui.write("%s:%s " % (h1, mask)), mask = is_reachable(want_sha1, reachable, p2) if i2 != hg.nullrev and mask > 0: - print "%s:%s " % (h2, mask), - print "" + ui.write("%s:%s " % (h2, mask)) + ui.write("\n") if maxnr and count >= maxnr: break count += 1 @@ -304,15 +302,15 @@ else: full = None copy = [x for x in revs] - revtree(copy, repo, full, opts['max_count'], opts['parents']) + revtree(ui, copy, repo, full, opts['max_count'], opts['parents']) def config(ui, repo, **opts): """print extension options""" def writeopt(name, value): - ui.write('k=%s\nv=%s\n' % (name, value)) + ui.write('k=%s\nv=%s\n' % (name, value)) writeopt('vdiff', ui.config('hgk', 'vdiff', '')) - + def view(ui, repo, *etc, **opts): "start interactive history viewer" diff -r ee317dbfb9d0 -r 30d2fecaab76 hgext/highlight.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/highlight.py Sun Feb 03 21:47:07 2008 -0200 @@ -0,0 +1,139 @@ +""" +This is Mercurial extension for syntax highlighting in the file +revision view of hgweb. + +It depends on the pygments syntax highlighting library: +http://pygments.org/ + +To enable the extension add this to hgrc: + +[extensions] +hgext.highlight = + +There is a single configuration option: + +[web] +pygments_style =