# HG changeset patch # User Patrick Mezard # Date 1224769463 -7200 # Node ID 5d14b06b1cc1a6ce2e06ba63de24e157d880cd25 # Parent 60826e071ce21d9f578723c02c734a44baaba496# Parent c1dc903dc7b65eafb56eaf3808942dde29cf12ff Merge with crew-stable diff -r c1dc903dc7b6 -r 5d14b06b1cc1 .hgignore --- a/.hgignore Thu Oct 23 14:05:11 2008 +0200 +++ b/.hgignore Thu Oct 23 15:44:23 2008 +0200 @@ -7,6 +7,7 @@ *.mergebackup *.o *.so +*.pyd *.pyc *.swp *.prof diff -r c1dc903dc7b6 -r 5d14b06b1cc1 contrib/darcs2hg.py --- a/contrib/darcs2hg.py Thu Oct 23 14:05:11 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,255 +0,0 @@ -#!/usr/bin/env python -# Encoding: iso-8859-1 -# vim: tw=80 ts=4 sw=4 noet -# ----------------------------------------------------------------------------- -# Project : Basic Darcs to Mercurial conversion script -# -# *** DEPRECATED. Use the convert extension instead. This script will -# *** be removed soon. -# -# ----------------------------------------------------------------------------- -# Authors : Sebastien Pierre -# TK Soh -# ----------------------------------------------------------------------------- -# Creation : 24-May-2006 -# ----------------------------------------------------------------------------- - -import os, sys -import tempfile -import xml.dom.minidom as xml_dom -from time import strptime, mktime -import re - -DARCS_REPO = None -HG_REPO = None - -USAGE = """\ -%s DARCSREPO HGREPO [SKIP] - - Converts the given Darcs repository to a new Mercurial repository. The given - HGREPO must not exist, as it will be created and filled up (this will avoid - overwriting valuable data. - - In case an error occurs within the process, you can resume the process by - giving the last successfuly applied change number. -""" % (os.path.basename(sys.argv[0])) - -# ------------------------------------------------------------------------------ -# -# Utilities -# -# ------------------------------------------------------------------------------ - -def cmd(text, path=None, silent=False): - """Executes a command, in the given directory (if any), and returns the - command result as a string.""" - cwd = None - if path: - path = os.path.abspath(path) - cwd = os.getcwd() - os.chdir(path) - if not silent: print "> ", text - res = os.popen(text).read() - if path: - os.chdir(cwd) - return res - -def writefile(path, data): - """Writes the given data into the given file.""" - f = file(path, "w") ; f.write(data) ; f.close() - -def error( *args ): - sys.stderr.write("ERROR: ") - for a in args: sys.stderr.write(str(a)) - sys.stderr.write("\n") - sys.stderr.write("You can make manual fixes if necessary and then resume by" - " giving the last changeset number") - sys.exit(-1) - -# ------------------------------------------------------------------------------ -# -# Darcs interface -# -# ------------------------------------------------------------------------------ - -def darcs_changes(darcsRepo): - """Gets the changes list from the given darcs repository. This returns the - chronological list of changes as (change name, change summary).""" - changes = cmd("darcs changes --reverse --xml-output", darcsRepo) - doc = xml_dom.parseString(changes) - for patch_node in doc.childNodes[0].childNodes: - name = filter(lambda n: n.nodeName == "name", patch_node.childNodes) - comm = filter(lambda n: n.nodeName == "comment", patch_node.childNodes) - if not name:continue - else: name = name[0].childNodes[0].data - if not comm: comm = "" - else: comm = comm[0].childNodes[0].data - author = patch_node.getAttribute("author") - date = patch_node.getAttribute("date") - chash = os.path.splitext(patch_node.getAttribute("hash"))[0] - yield author, date, name, chash, comm - -def darcs_tip(darcs_repo): - changes = cmd("darcs changes",darcs_repo,silent=True) - changes = filter(lambda l: l.strip().startswith("* "), changes.split("\n")) - return len(changes) - -def darcs_pull(hg_repo, darcs_repo, chash): - old_tip = darcs_tip(darcs_repo) - res = cmd("darcs pull \"%s\" --all --match=\"hash %s\"" % (darcs_repo, chash), hg_repo) - if re.search('^We have conflicts in the following files:$', res, re.MULTILINE): - print "Trying to revert files to work around conflict..." - rev_res = cmd ("darcs revert --all", hg_repo) - print rev_res - print res - new_tip = darcs_tip(darcs_repo) - if not new_tip != old_tip + 1: - error("Darcs pull did not work as expected: " + res) - -def darcs_changes_summary(darcs_repo, chash): - """Gets the changes from the darcs summary. This returns the chronological - list of changes as (change_type, args). Eg. ('add_file', 'foo.txt') or - ('move', ['foo.txt','bar.txt']).""" - change = cmd("darcs changes --summary --xml-output --match=\"hash %s\"" % (chash), darcs_repo) - doc = xml_dom.parseString(change) - for patch_node in doc.childNodes[0].childNodes: - summary_nodes = filter(lambda n: n.nodeName == "summary" and n.nodeType == n.ELEMENT_NODE, patch_node.childNodes) - for summary_node in summary_nodes: - change_nodes = filter(lambda n: n.nodeType == n.ELEMENT_NODE, summary_node.childNodes) - if len(change_nodes) == 0: - name = filter(lambda n: n.nodeName == "name", patch_node.childNodes) - if not name: - error("Darcs patch has an empty summary node and no name: " + patch_node.toxml()) - name = name[0].childNodes[0].data.strip() - (tag, sub_count) = re.subn('^TAG ', '', name, 1) - if sub_count != 1: - error("Darcs patch has an empty summary node but doesn't look like a tag: " + patch_node.toxml()); - for change_node in change_nodes: - change = change_node.nodeName - if change == 'modify_file': - yield change, change_node.childNodes[0].data.strip() - elif change == 'add_file': - yield change, change_node.childNodes[0].data.strip() - elif change == 'remove_file': - yield change, change_node.childNodes[0].data.strip() - elif change == 'add_directory': - yield change, change_node.childNodes[0].data.strip() - elif change == 'remove_directory': - yield change, change_node.childNodes[0].data.strip() - elif change == 'move': - yield change, (change_node.getAttribute('from'), change_node.getAttribute('to')) - else: - error('Problem parsing summary xml: Unexpected element: ' + change_node.toxml()) - -# ------------------------------------------------------------------------------ -# -# Mercurial interface -# -# ------------------------------------------------------------------------------ - -def hg_commit( hg_repo, text, author, date ): - fd, tmpfile = tempfile.mkstemp(prefix="darcs2hg_") - writefile(tmpfile, text) - old_tip = hg_tip(hg_repo) - cmd("hg add -X _darcs", hg_repo) - cmd("hg remove -X _darcs --after", hg_repo) - res = cmd("hg commit -l %s -u \"%s\" -d \"%s 0\"" % (tmpfile, author, date), hg_repo) - os.close(fd) - os.unlink(tmpfile) - new_tip = hg_tip(hg_repo) - if not new_tip == old_tip + 1: - # Sometimes we may have empty commits, we simply skip them - if res.strip().lower().find("nothing changed") != -1: - pass - else: - error("Mercurial commit did not work as expected: " + res) - -def hg_tip( hg_repo ): - """Returns the latest local revision number in the given repository.""" - tip = cmd("hg tip", hg_repo, silent=True) - tip = tip.split("\n")[0].split(":")[1].strip() - return int(tip) - -def hg_rename( hg_repo, from_file, to_file ): - cmd("hg rename --after \"%s\" \"%s\"" % (from_file, to_file), hg_repo); - -def hg_tag ( hg_repo, text, author, date ): - old_tip = hg_tip(hg_repo) - res = cmd("hg tag -u \"%s\" -d \"%s 0\" \"%s\"" % (author, date, text), hg_repo) - new_tip = hg_tip(hg_repo) - if not new_tip == old_tip + 1: - error("Mercurial tag did not work as expected: " + res) - -def hg_handle_change( hg_repo, author, date, change, arg ): - """Processes a change event as output by darcs_changes_summary. These - consist of file move/rename/add/delete commands.""" - if change == 'modify_file': - pass - elif change == 'add_file': - pass - elif change =='remove_file': - pass - elif change == 'add_directory': - pass - elif change == 'remove_directory': - pass - elif change == 'move': - hg_rename(hg_repo, arg[0], arg[1]) - elif change == 'tag': - hg_tag(hg_repo, arg, author, date) - else: - error('Unknown change type ' + change + ': ' + arg) - -# ------------------------------------------------------------------------------ -# -# Main -# -# ------------------------------------------------------------------------------ - -if __name__ == "__main__": - args = sys.argv[1:] - # We parse the arguments - if len(args) == 2: - darcs_repo = os.path.abspath(args[0]) - hg_repo = os.path.abspath(args[1]) - skip = None - elif len(args) == 3: - darcs_repo = os.path.abspath(args[0]) - hg_repo = os.path.abspath(args[1]) - skip = int(args[2]) - else: - print USAGE - sys.exit(-1) - print 'This command is deprecated. Use the convert extension instead.' - # Initializes the target repo - if not os.path.isdir(darcs_repo + "/_darcs"): - print "No darcs directory found at: " + darcs_repo - sys.exit(-1) - if not os.path.isdir(hg_repo): - os.mkdir(hg_repo) - elif skip == None: - print "Given HG repository must not exist when no SKIP is specified." - sys.exit(-1) - if skip == None: - cmd("hg init \"%s\"" % (hg_repo)) - cmd("darcs initialize", hg_repo) - # Get the changes from the Darcs repository - change_number = 0 - for author, date, summary, chash, description in darcs_changes(darcs_repo): - print "== changeset", change_number, - if skip != None and change_number <= skip: - print "(skipping)" - else: - text = summary + "\n" + description - # The commit hash has a date like 20021020201112 - # --------------------------------YYYYMMDDHHMMSS - date = chash.split("-")[0] - epoch = int(mktime(strptime(date, '%Y%m%d%H%M%S'))) - darcs_pull(hg_repo, darcs_repo, chash) - for change, arg in darcs_changes_summary(darcs_repo, chash): - hg_handle_change(hg_repo, author, epoch, change, arg) - hg_commit(hg_repo, text, author, epoch) - change_number += 1 - print "Darcs repository (_darcs) was not deleted. You can keep or remove it." - -# EOF diff -r c1dc903dc7b6 -r 5d14b06b1cc1 contrib/dumprevlog --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/dumprevlog Thu Oct 23 15:44:23 2008 +0200 @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# Dump revlogs as raw data stream +# $ find .hg/store/ -name "*.i" | xargs dumprevlog > repo.dump + +import sys +from mercurial import revlog, node, util + +for fp in (sys.stdin, sys.stdout, sys.stderr): + util.set_binary(fp) + +for f in sys.argv[1:]: + binopen = lambda fn: open(fn, 'rb') + r = revlog.revlog(binopen, f) + print "file:", f + for i in r: + n = r.node(i) + p = r.parents(n) + d = r.revision(n) + print "node:", node.hex(n) + print "linkrev:", r.linkrev(n) + print "parents:", node.hex(p[0]), node.hex(p[1]) + print "length:", len(d) + print "-start-" + print d + print "-end-" diff -r c1dc903dc7b6 -r 5d14b06b1cc1 contrib/hgk --- a/contrib/hgk Thu Oct 23 14:05:11 2008 +0200 +++ b/contrib/hgk Thu Oct 23 15:44:23 2008 +0200 @@ -370,6 +370,25 @@ lappend tagids($direct) $tag lappend idtags($tag) $direct } + + set status [catch {exec $env(HG) --config ui.report_untrusted=false heads} heads] + if { $status != 0 } { + puts $::errorInfo + if { ![string equal $::errorCode NONE] } { + exit 2 + } + } + regsub -all "\r\n" $heads "\n" heads + + set lines [split $heads "\n"] + foreach f $lines { + set match "" + regexp {changeset:\s+(\S+):(\S+)$} $f match id sha + if {$match != ""} { + lappend idheads($sha) $id + } + } + } proc readotherrefs {base dname excl} { @@ -1045,7 +1064,7 @@ } proc drawtags {id x xt y1} { - global idtags idheads idotherrefs + global idtags idheads idotherrefs commitinfo global linespc lthickness global canv mainfont idline rowtextx @@ -1057,8 +1076,11 @@ set ntags [llength $marks] } if {[info exists idheads($id)]} { - set marks [concat $marks $idheads($id)] - set nheads [llength $idheads($id)] + set headmark [lindex $commitinfo($id) 7] + if {$headmark ne "default"} { + lappend marks $headmark + set nheads 1 + } } if {[info exists idotherrefs($id)]} { set marks [concat $marks $idotherrefs($id)] diff -r c1dc903dc7b6 -r 5d14b06b1cc1 contrib/mercurial.el --- a/contrib/mercurial.el Thu Oct 23 14:05:11 2008 +0200 +++ b/contrib/mercurial.el Thu Oct 23 15:44:23 2008 +0200 @@ -35,8 +35,10 @@ ;; This code has been developed under XEmacs 21.5, and may not work as ;; well under GNU Emacs (albeit tested under 21.4). Patches to ;; enhance the portability of this code, fix bugs, and add features -;; are most welcome. You can clone a Mercurial repository for this -;; package from http://www.serpentine.com/hg/hg-emacs +;; are most welcome. + +;; As of version 22.3, GNU Emacs's VC mode has direct support for +;; Mercurial, so this package may not prove as useful there. ;; Please send problem reports and suggestions to bos@serpentine.com. diff -r c1dc903dc7b6 -r 5d14b06b1cc1 contrib/simplemerge --- a/contrib/simplemerge Thu Oct 23 14:05:11 2008 +0200 +++ b/contrib/simplemerge Thu Oct 23 15:44:23 2008 +0200 @@ -42,6 +42,9 @@ sys.stdout.write(' %-*s %s\n' % (opts_len, first, second)) try: + for fp in (sys.stdin, sys.stdout, sys.stderr): + util.set_binary(fp) + opts = {} try: args = fancyopts.fancyopts(sys.argv[1:], options, opts) diff -r c1dc903dc7b6 -r 5d14b06b1cc1 contrib/undumprevlog --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/undumprevlog Thu Oct 23 15:44:23 2008 +0200 @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# Undump a dump from dumprevlog +# $ hg init +# $ undumprevlog < repo.dump + +import sys +from mercurial import revlog, node, util, transaction + +for fp in (sys.stdin, sys.stdout, sys.stderr): + util.set_binary(fp) + +opener = util.opener('.', False) +tr = transaction.transaction(sys.stderr.write, opener, "undump.journal") +while 1: + l = sys.stdin.readline() + if not l: + break + if l.startswith("file:"): + f = l[6:-1] + r = revlog.revlog(opener, f) + print f + elif l.startswith("node:"): + n = node.bin(l[6:-1]) + elif l.startswith("linkrev:"): + lr = int(l[9:-1]) + elif l.startswith("parents:"): + p = l[9:-1].split() + p1 = node.bin(p[0]) + p2 = node.bin(p[1]) + elif l.startswith("length:"): + length = int(l[8:-1]) + sys.stdin.readline() # start marker + d = sys.stdin.read(length) + sys.stdin.readline() # end marker + r.addrevision(d, tr, lr, p1, p2) + +tr.close() diff -r c1dc903dc7b6 -r 5d14b06b1cc1 contrib/vim/hgcommand.vim --- a/contrib/vim/hgcommand.vim Thu Oct 23 14:05:11 2008 +0200 +++ b/contrib/vim/hgcommand.vim Thu Oct 23 15:44:23 2008 +0200 @@ -10,6 +10,12 @@ " Bob Hiestand for the fabulous " cvscommand.vim from which this script was directly created by " means of sed commands and minor tweaks. +" Note: +" For Vim7 the use of Bob Hiestand's vcscommand.vim +" +" in conjunction with Vladmir Marek's Hg backend +" +" is recommended. """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" " diff -r c1dc903dc7b6 -r 5d14b06b1cc1 contrib/win32/hg.bat --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/win32/hg.bat Thu Oct 23 15:44:23 2008 +0200 @@ -0,0 +1,12 @@ +@echo off +rem Windows Driver script for Mercurial + +setlocal +set HG=%~f0 + +rem Use a full path to Python (relative to this script) as the standard Python +rem install does not put python.exe on the PATH... +rem %~dp0 is the directory of this script + +%~dp0..\python "%~dp0hg" %* +endlocal diff -r c1dc903dc7b6 -r 5d14b06b1cc1 contrib/zsh_completion --- a/contrib/zsh_completion Thu Oct 23 14:05:11 2008 +0200 +++ b/contrib/zsh_completion Thu Oct 23 15:44:23 2008 +0200 @@ -205,8 +205,7 @@ _hg_config() { typeset -a items - local line - items=(${${(%f)"$(_hg_cmd showconfig)"}%%\=*}) + items=(${${(%f)"$(_call_program hg hg showconfig)"}%%\=*}) (( $#items )) && _describe -t config 'config item' items } @@ -291,10 +290,14 @@ '--cwd[change working directory]:new working directory:_files -/' '(--noninteractive -y)'{-y,--noninteractive}'[do not prompt, assume yes for any required answers]' '(--verbose -v)'{-v,--verbose}'[enable additional output]' + '*--config[set/override config option]:defined config items:_hg_config' '(--quiet -q)'{-q,--quiet}'[suppress output]' '(--help -h)'{-h,--help}'[display help and exit]' '--debug[debug mode]' '--debugger[start debugger]' + '--encoding[set the charset encoding (default: UTF8)]' + '--encodingmode[set the charset encoding mode (default: strict)]' + '--lsprof[print improved command execution profile]' '--traceback[print traceback on exception]' '--time[time how long the command takes]' '--profile[profile]' diff -r c1dc903dc7b6 -r 5d14b06b1cc1 doc/Makefile --- a/doc/Makefile Thu Oct 23 14:05:11 2008 +0200 +++ b/doc/Makefile Thu Oct 23 15:44:23 2008 +0200 @@ -30,7 +30,7 @@ asciidoc -b html4 $*.txt || asciidoc -b html $*.txt MANIFEST: man html - # versionned files are already in the main MANIFEST + # tracked files are already in the main MANIFEST $(RM) $@ for i in $(MAN) $(HTML) hg.1.gendoc.txt; do \ echo "doc/$$i" >> $@ ; \ diff -r c1dc903dc7b6 -r 5d14b06b1cc1 doc/gendoc.py --- a/doc/gendoc.py Thu Oct 23 14:05:11 2008 +0200 +++ b/doc/gendoc.py Thu Oct 23 15:44:23 2008 +0200 @@ -3,7 +3,7 @@ sys.path.insert(0, "..") from mercurial import demandimport; demandimport.enable() from mercurial.commands import table, globalopts -from mercurial.i18n import gettext as _ +from mercurial.i18n import gettext, _ from mercurial.help import helptable def get_desc(docstr): @@ -69,6 +69,7 @@ if f.startswith("debug"): continue d = get_cmd(h[f]) # synopsis + ui.write("[[%s]]\n" % d['cmd']) ui.write("%s::\n" % d['synopsis'].replace("hg ","", 1)) # description ui.write("%s\n\n" % d['desc'][1]) @@ -91,14 +92,11 @@ ui.write(_(" aliases: %s\n\n") % " ".join(d['aliases'])) # print topics - for t in helptable: - l = t.split("|") - section = l[-1] - underlined(_(section).upper()) - doc = helptable[t] + for names, section, doc in helptable: + underlined(gettext(section).upper()) if callable(doc): doc = doc() - ui.write(_(doc)) + ui.write(gettext(doc)) ui.write("\n") if __name__ == "__main__": diff -r c1dc903dc7b6 -r 5d14b06b1cc1 doc/hg.1.txt --- a/doc/hg.1.txt Thu Oct 23 14:05:11 2008 +0200 +++ b/doc/hg.1.txt Thu Oct 23 15:44:23 2008 +0200 @@ -30,65 +30,10 @@ repository path:: either the pathname of a local repository or the URI of a remote - repository. There are two available URI protocols, http:// which is - fast and the static-http:// protocol which is much slower but does not - require a special server on the web host. - + repository. include::hg.1.gendoc.txt[] -SPECIFYING SINGLE REVISIONS ---------------------------- - - Mercurial accepts several notations for identifying individual - revisions. - - A plain integer is treated as a revision number. Negative - integers are treated as offsets from the tip, with -1 denoting the - tip. - - A 40-digit hexadecimal string is treated as a unique revision - identifier. - - A hexadecimal string less than 40 characters long is treated as a - unique revision identifier, and referred to as a short-form - identifier. A short-form identifier is only valid if it is the - prefix of one full-length identifier. - - Any other string is treated as a tag name, which is a symbolic - name associated with a revision identifier. Tag names may not - contain the ":" character. - - The reserved name "tip" is a special tag that always identifies - the most recent revision. - - The reserved name "null" indicates the null revision. This is the - revision of an empty repository, and the parent of revision 0. - - The reserved name "." indicates the working directory parent. If - no working directory is checked out, it is equivalent to null. - If an uncommitted merge is in progress, "." is the revision of - the first parent. - -SPECIFYING MULTIPLE REVISIONS ------------------------------ - - When Mercurial accepts more than one revision, they may be - specified individually, or provided as a continuous range, - separated by the ":" character. - - The syntax of range notation is [BEGIN]:[END], where BEGIN and END - are revision identifiers. Both BEGIN and END are optional. If - BEGIN is not specified, it defaults to revision number 0. If END - is not specified, it defaults to the tip. The range ":" thus - means "all revisions". - - If BEGIN is greater than END, revisions are treated in reverse - order. - - A range acts as a closed interval. This means that a range of 3:5 - gives 3, 4 and 5. Similarly, a range of 4:2 gives 4, 3, and 2. - FILES ----- .hgignore:: @@ -103,7 +48,7 @@ /etc/mercurial/hgrc, $HOME/.hgrc, .hg/hgrc:: This file contains defaults and configuration. Values in .hg/hgrc override those in $HOME/.hgrc, and these override settings made in the - global /etc/mercurial/hgrc configuration. See hgrc(5) for details of + global /etc/mercurial/hgrc configuration. See hgrc(5) for details of the contents and format of these files. Some commands (e.g. revert) produce backup files ending in .orig, if diff -r c1dc903dc7b6 -r 5d14b06b1cc1 doc/hgignore.5.txt --- a/doc/hgignore.5.txt Thu Oct 23 14:05:11 2008 +0200 +++ b/doc/hgignore.5.txt Thu Oct 23 15:44:23 2008 +0200 @@ -17,25 +17,25 @@ ----------- Mercurial ignores every unmanaged file that matches any pattern in an -ignore file. The patterns in an ignore file do not apply to files -managed by Mercurial. To control Mercurial's handling of files that -it manages, see the hg(1) man page. Look for the "-I" and "-X" +ignore file. The patterns in an ignore file do not apply to files +managed by Mercurial. To control Mercurial's handling of files that +it manages, see the hg(1) man page. Look for the "-I" and "-X" options. In addition, a Mercurial configuration file can point to a set of -per-user or global ignore files. See the hgrc(5) man page for details -of how to configure these files. Look for the "ignore" entry in the +per-user or global ignore files. See the hgrc(5) man page for details +of how to configure these files. Look for the "ignore" entry in the "ui" section. SYNTAX ------ An ignore file is a plain text file consisting of a list of patterns, -with one pattern per line. Empty lines are skipped. The "#" +with one pattern per line. Empty lines are skipped. The "#" character is treated as a comment character, and the "\" character is treated as an escape character. -Mercurial supports several pattern syntaxes. The default syntax used +Mercurial supports several pattern syntaxes. The default syntax used is Python/Perl-style regular expressions. To change the syntax used, use a line of the following form: @@ -52,9 +52,9 @@ The chosen syntax stays in effect when parsing all patterns that follow, until another syntax is selected. -Neither glob nor regexp patterns are rooted. A glob-syntax pattern of +Neither glob nor regexp patterns are rooted. A glob-syntax pattern of the form "*.c" will match a file ending in ".c" in any directory, and -a regexp pattern of the form "\.c$" will do the same. To root a +a regexp pattern of the form "\.c$" will do the same. To root a regexp pattern, start it with "^". EXAMPLE diff -r c1dc903dc7b6 -r 5d14b06b1cc1 doc/hgrc.5.txt --- a/doc/hgrc.5.txt Thu Oct 23 14:05:11 2008 +0200 +++ b/doc/hgrc.5.txt Thu Oct 23 15:44:23 2008 +0200 @@ -17,26 +17,26 @@ Mercurial reads configuration data from several files, if they exist. The names of these files depend on the system on which Mercurial is -installed. *.rc files from a single directory are read in -alphabetical order, later ones overriding earlier ones. Where +installed. *.rc files from a single directory are read in +alphabetical order, later ones overriding earlier ones. Where multiple paths are given below, settings from later paths override earlier ones. (Unix) /etc/mercurial/hgrc.d/*.rc:: (Unix) /etc/mercurial/hgrc:: Per-installation configuration files, searched for in the - directory where Mercurial is installed. is the + directory where Mercurial is installed. is the parent directory of the hg executable (or symlink) being run. For example, if installed in /shared/tools/bin/hg, Mercurial will - look in /shared/tools/etc/mercurial/hgrc. Options in these files + look in /shared/tools/etc/mercurial/hgrc. Options in these files apply to all Mercurial commands executed by any user in any directory. (Unix) /etc/mercurial/hgrc.d/*.rc:: (Unix) /etc/mercurial/hgrc:: Per-system configuration files, for the system on which Mercurial - is running. Options in these files apply to all Mercurial - commands executed by any user in any directory. Options in these + is running. Options in these files apply to all Mercurial + commands executed by any user in any directory. Options in these files override per-installation options. (Windows) \Mercurial.ini:: @@ -45,7 +45,7 @@ or else:: (Windows) C:\Mercurial\Mercurial.ini:: Per-installation/system configuration files, for the system on - which Mercurial is running. Options in these files apply to all + which Mercurial is running. Options in these files apply to all Mercurial commands executed by any user in any directory. Registry keys contain PATH-like strings, every part of which must reference a Mercurial.ini file or be a directory where *.rc files @@ -59,16 +59,16 @@ Per-user configuration file(s), for the user running Mercurial. On Windows 9x, %HOME% is replaced by %APPDATA%. Options in these files apply to all Mercurial commands executed - by this user in any directory. Options in thes files override + by this user in any directory. Options in thes files override per-installation and per-system options. (Unix, Windows) /.hg/hgrc:: Per-repository configuration options that only apply in a - particular repository. This file is not version-controlled, and - will not get transferred during a "clone" operation. Options in + particular repository. This file is not version-controlled, and + will not get transferred during a "clone" operation. Options in this file override options in all other configuration files. On Unix, most of this file will be ignored if it doesn't belong - to a trusted user or to a trusted group. See the documentation + to a trusted user or to a trusted group. See the documentation for the trusted section below for more details. SYNTAX @@ -82,10 +82,10 @@ green= eggs -Each line contains one entry. If the lines that follow are indented, +Each line contains one entry. If the lines that follow are indented, they are treated as continuations of that entry. -Leading whitespace is removed from values. Empty lines are skipped. +Leading whitespace is removed from values. Empty lines are skipped. The optional values can contain format strings which refer to other values in the same section, or values in a special DEFAULT section. @@ -100,6 +100,7 @@ Mercurial "hgrc" file, the purpose of each section, its possible keys, and their possible values. +[[decode]] decode/encode:: Filters for transforming files on checkout/checkin. This would typically be used for newline processing or other @@ -107,12 +108,12 @@ Filters consist of a filter pattern followed by a filter command. Filter patterns are globs by default, rooted at the repository - root. For example, to match any file ending in ".txt" in the root - directory only, use the pattern "*.txt". To match any file ending + root. For example, to match any file ending in ".txt" in the root + directory only, use the pattern "*.txt". To match any file ending in ".c" anywhere in the repository, use the pattern "**.c". The filter command can start with a specifier, either "pipe:" or - "tempfile:". If no specifier is given, "pipe:" is used by default. + "tempfile:". If no specifier is given, "pipe:" is used by default. A "pipe:" command must accept data on stdin and return the transformed data on stdout. @@ -129,9 +130,9 @@ # can safely omit "pipe:", because it's the default) *.gz = gzip - A "tempfile:" command is a template. The string INFILE is replaced + A "tempfile:" command is a template. The string INFILE is replaced with the name of a temporary file that contains the data to be - filtered by the command. The string OUTFILE is replaced with the + filtered by the command. The string OUTFILE is replaced with the name of an empty temporary file, where the filtered data must be written by the command. @@ -158,6 +159,7 @@ [decode] **.txt = dumbdecode: +[[defaults]] defaults:: Use the [defaults] section to define command defaults, i.e. the default options/arguments to pass to the specified commands. @@ -173,6 +175,7 @@ defining command defaults. The command defaults will also be applied to the aliases of the commands defined. +[[diff]] diff:: Settings used when displaying diffs. They are all boolean and defaults to False. @@ -189,34 +192,47 @@ ignoreblanklines;; Ignore changes whose lines are all blank. +[[email]] email:: Settings for extensions that send email messages. from;; - Optional. Email address to use in "From" header and SMTP envelope + Optional. Email address to use in "From" header and SMTP envelope of outgoing messages. to;; - Optional. Comma-separated list of recipients' email addresses. + Optional. Comma-separated list of recipients' email addresses. cc;; - Optional. Comma-separated list of carbon copy recipients' + Optional. Comma-separated list of carbon copy recipients' email addresses. bcc;; - Optional. Comma-separated list of blind carbon copy - recipients' email addresses. Cannot be set interactively. + Optional. Comma-separated list of blind carbon copy + recipients' email addresses. Cannot be set interactively. method;; - Optional. Method to use to send email messages. If value is + Optional. Method to use to send email messages. If value is "smtp" (default), use SMTP (see section "[smtp]" for - configuration). Otherwise, use as name of program to run that + configuration). Otherwise, use as name of program to run that acts like sendmail (takes "-f" option for sender, list of - recipients on command line, message on stdin). Normally, setting + recipients on command line, message on stdin). Normally, setting this to "sendmail" or "/usr/sbin/sendmail" is enough to use sendmail to send messages. + charsets;; + Optional. Comma-separated list of charsets considered + convenient for recipients. Addresses, headers, and parts not + containing patches of outgoing messages will be encoded in + the first charset to which conversion from local encoding + (ui.encoding, ui.fallbackencoding) succeeds. If correct + conversion, including to ui.encoding, fails, the text in + question is sent as is in fake ascii. Defaults to empty list. Email example: [email] from = Joseph User method = /usr/sbin/sendmail + # charsets for western europeans + # us-ascii, utf-8 omitted, as they are tried first and last + charsets = iso-8859-1, iso-8859-15, windows-1252 +[[extensions]] extensions:: Mercurial has an extension mechanism for adding new features. To enable an extension, create an entry for it in this section. @@ -241,6 +257,7 @@ # (this extension will get loaded from the file specified) myfeature = ~/.hgext/myfeature.py +[[format]] format:: usestore;; @@ -250,6 +267,7 @@ you to store longer filenames in some situations at the expense of compatibility. +[[merge-patterns]] merge-patterns:: This section specifies merge tools to associate with particular file patterns. Tools matched here will take precedence over the default @@ -261,6 +279,7 @@ **.c = kdiff3 **.jpg = myimgmerge +[[merge-tools]] merge-tools:: This section configures external merge tools to use for file-level merges. @@ -281,6 +300,7 @@ myHtmlTool.priority = 1 Supported arguments: + priority;; The priority in which to evaluate this tool. Default: 0. @@ -297,10 +317,10 @@ launching external tool. Default: True binary;; - This tool can merge binary files. Defaults to False, unless tool + This tool can merge binary files. Defaults to False, unless tool was selected by file pattern match. symlink;; - This tool can merge symlinks. Defaults to False, even if tool was + This tool can merge symlinks. Defaults to False, even if tool was selected by file pattern match. checkconflicts;; Check whether there are conflicts even though the tool reported @@ -313,19 +333,20 @@ fixeol;; Attempt to fix up EOL changes caused by the merge tool. Default: False - gui:; + gui;; This tool requires a graphical interface to run. Default: False regkey;; Windows registry key which describes install location of this tool. Mercurial will search for this key first under HKEY_CURRENT_USER and - then under HKEY_LOCAL_MACHINE. Default: None + then under HKEY_LOCAL_MACHINE. Default: None regname;; - Name of value to read from specified registry key. Defaults to the + Name of value to read from specified registry key. Defaults to the unnamed (default) value. regappend;; String to append to the value read from the registry, typically the - executable name of the tool. Default: None + executable name of the tool. Default: None +[[hooks]] hooks:: Commands or Python functions that get automatically executed by various actions such as starting or finishing a commit. Multiple @@ -342,24 +363,24 @@ incoming.autobuild = /my/build/hook Most hooks are run with environment variables set that give added - useful information. For each hook below, the environment variables + useful information. For each hook below, the environment variables it is passed are listed with names of the form "$HG_foo". changegroup;; Run after a changegroup has been added via push, pull or - unbundle. ID of the first new changeset is in $HG_NODE. URL from + unbundle. ID of the first new changeset is in $HG_NODE. URL from which changes came is in $HG_URL. commit;; Run after a changeset has been created in the local repository. - ID of the newly created changeset is in $HG_NODE. Parent + ID of the newly created changeset is in $HG_NODE. Parent changeset IDs are in $HG_PARENT1 and $HG_PARENT2. incoming;; Run after a changeset has been pulled, pushed, or unbundled into - the local repository. The ID of the newly arrived changeset is in - $HG_NODE. URL that was source of changes came is in $HG_URL. + the local repository. The ID of the newly arrived changeset is in + $HG_NODE. URL that was source of changes came is in $HG_URL. outgoing;; - Run after sending changes from local repository to another. ID of - first changeset sent is in $HG_NODE. Source of operation is in + Run after sending changes from local repository to another. ID of + first changeset sent is in $HG_NODE. Source of operation is in $HG_SOURCE; see "preoutgoing" hook for description. post-;; Run after successful invocations of the associated command. The @@ -371,56 +392,56 @@ the command doesn't execute and Mercurial returns the failure code. prechangegroup;; Run before a changegroup is added via push, pull or unbundle. - Exit status 0 allows the changegroup to proceed. Non-zero status - will cause the push, pull or unbundle to fail. URL from which + Exit status 0 allows the changegroup to proceed. Non-zero status + will cause the push, pull or unbundle to fail. URL from which changes will come is in $HG_URL. precommit;; - Run before starting a local commit. Exit status 0 allows the - commit to proceed. Non-zero status will cause the commit to fail. + Run before starting a local commit. Exit status 0 allows the + 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 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, + 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 - can just copy files instead then. Source of operation is in - $HG_SOURCE. If "serve", operation is happening on behalf of - remote ssh or http repository. If "push", "pull" or "bundle", + can just copy files instead then. Source of operation is in + $HG_SOURCE. If "serve", operation is happening on behalf of + remote ssh or http repository. If "push", "pull" or "bundle", operation is happening on behalf of repository on same system. pretag;; - Run before creating a tag. Exit status 0 allows the tag to be - created. Non-zero status will cause the tag to fail. ID of - changeset to tag is in $HG_NODE. Name of tag is in $HG_TAG. Tag + Run before creating a tag. Exit status 0 allows the tag to be + created. Non-zero status will cause the tag to fail. ID of + changeset to tag is in $HG_NODE. Name of tag is in $HG_TAG. Tag is local if $HG_LOCAL=1, in repo if $HG_LOCAL=0. pretxnchangegroup;; Run after a changegroup has been added via push, pull or unbundle, - but before the transaction has been committed. Changegroup is - visible to hook program. This lets you validate incoming changes - before accepting them. Passed the ID of the first new changeset - in $HG_NODE. Exit status 0 allows the transaction to commit. + but before the transaction has been committed. Changegroup is + visible to hook program. This lets you validate incoming changes + before accepting them. Passed the ID of the first new changeset + in $HG_NODE. Exit status 0 allows the transaction to commit. Non-zero status will cause the transaction to be rolled back and - the push, pull or unbundle will fail. URL that was source of + the push, pull or unbundle will fail. URL that was source of changes is in $HG_URL. pretxncommit;; Run after a changeset has been created but the transaction not yet - committed. Changeset is visible to hook program. This lets you - validate commit message and changes. Exit status 0 allows the - commit to proceed. Non-zero status will cause the transaction to - be rolled back. ID of changeset is in $HG_NODE. Parent changeset + committed. Changeset is visible to hook program. This lets you + validate commit message and changes. Exit status 0 allows the + commit to proceed. Non-zero status will cause the transaction to + be rolled back. ID of changeset is in $HG_NODE. Parent changeset IDs are in $HG_PARENT1 and $HG_PARENT2. preupdate;; - Run before updating the working directory. Exit status 0 allows - the update to proceed. Non-zero status will prevent the update. - Changeset ID of first new parent is in $HG_PARENT1. If merge, ID + Run before updating the working directory. Exit status 0 allows + the update to proceed. Non-zero status will prevent the update. + Changeset ID of first new parent is in $HG_PARENT1. If merge, ID of second new parent is in $HG_PARENT2. tag;; - Run after a tag is created. ID of tagged changeset is in - $HG_NODE. Name of tag is in $HG_TAG. Tag is local if + Run after a tag is created. ID of tagged changeset is in + $HG_NODE. Name of tag is in $HG_TAG. Tag is local if $HG_LOCAL=1, in repo if $HG_LOCAL=0. update;; - Run after updating the working directory. Changeset ID of first - new parent is in $HG_PARENT1. If merge, ID of second new parent - is in $HG_PARENT2. If update succeeded, $HG_ERROR=0. If update + Run after updating the working directory. Changeset ID of first + new parent is in $HG_PARENT1. If merge, ID of second new parent + is in $HG_PARENT2. If update succeeded, $HG_ERROR=0. If update failed (e.g. because conflicts not resolved), $HG_ERROR=1. Note: it is generally better to use standard hooks rather than the @@ -438,16 +459,17 @@ hookname = python:modulename.submodule.callable - Python hooks are run within the Mercurial process. Each hook is + Python hooks are run within the Mercurial process. Each hook is called with at least three keyword arguments: a ui object (keyword "ui"), a repository object (keyword "repo"), and a "hooktype" - keyword that tells what kind of hook is used. Arguments listed as + keyword that tells what kind of hook is used. Arguments listed as environment variables above are passed as keyword arguments, with no "HG_" prefix, and names in lower case. If a Python hook returns a "true" value or raises an exception, this is treated as failure of the hook. +[[http_proxy]] http_proxy:: Used to access web-based Mercurial repositories through a HTTP proxy. @@ -455,68 +477,72 @@ Host name and (optional) port of the proxy server, for example "myproxy:8000". no;; - Optional. Comma-separated list of host names that should bypass + Optional. Comma-separated list of host names that should bypass the proxy. passwd;; - Optional. Password to authenticate with at the proxy server. + Optional. Password to authenticate with at the proxy server. user;; - Optional. User name to authenticate with at the proxy server. + Optional. User name to authenticate with at the proxy server. +[[smtp]] smtp:: Configuration for extensions that need to send email messages. host;; Host name of mail server, e.g. "mail.example.com". port;; - Optional. Port to connect to on mail server. Default: 25. + Optional. Port to connect to on mail server. Default: 25. tls;; - Optional. Whether to connect to mail server using TLS. True or - False. Default: False. + Optional. Whether to connect to mail server using TLS. True or + False. Default: False. username;; - Optional. User name to authenticate to SMTP server with. + Optional. User name to authenticate to SMTP server with. If username is specified, password must also be specified. Default: none. password;; - Optional. Password to authenticate to SMTP server with. + Optional. Password to authenticate to SMTP server with. If username is specified, password must also be specified. Default: none. local_hostname;; - Optional. It's the hostname that the sender can use to identify itself + Optional. It's the hostname that the sender can use to identify itself to the MTA. +[[paths]] paths:: - Assigns symbolic names to repositories. The left side is the + Assigns symbolic names to repositories. The left side is the symbolic name, and the right gives the directory or URL that is the - location of the repository. Default paths can be declared by + location of the repository. Default paths can be declared by setting the following entries. default;; Directory or URL to use when pulling if no source is specified. Default is set to repository from which the current repository was cloned. default-push;; - Optional. Directory or URL to use when pushing if no destination + Optional. Directory or URL to use when pushing if no destination is specified. +[[server]] server:: Controls generic server settings. uncompressed;; Whether to allow clients to clone a repo using the uncompressed - streaming protocol. This transfers about 40% more data than a + streaming protocol. This transfers about 40% more data than a regular clone, but uses less memory and CPU on both server and - client. Over a LAN (100Mbps or better) or a very fast WAN, an + client. Over a LAN (100Mbps or better) or a very fast WAN, an uncompressed streaming clone is a lot faster (~10x) than a regular - clone. Over most WAN connections (anything slower than about + clone. Over most WAN connections (anything slower than about 6Mbps), uncompressed streaming is slower, because of the extra - data transfer overhead. Default is False. + data transfer overhead. Default is False. +[[trusted]] trusted:: For security reasons, Mercurial will not use the settings in the .hg/hgrc file from a repository if it doesn't belong to a - trusted user or to a trusted group. The main exception is the + trusted user or to a trusted group. The main exception is the web interface, which automatically uses some safe settings, since it's common to serve repositories from different users. - This section specifies what users and groups are trusted. The - current user is always trusted. To trust everybody, list a user + This section specifies what users and groups are trusted. The + current user is always trusted. To trust everybody, list a user or a group with name "*". users;; @@ -524,6 +550,7 @@ groups;; Comma-separated list of trusted groups. +[[ui]] ui:: User interface controls. archivemeta;; @@ -531,13 +558,19 @@ (hashes for the repository base and for tip) in archives created by the hg archive command or downloaded via hgweb. Default is true. + askusername;; + Whether to prompt for a username when committing. If True, and + neither $HGUSER nor $EMAIL has been specified, then the user will + be prompted to enter a username. If no username is entered, the + default USER@HOST is used instead. + Default is False. debug;; - Print debugging information. True or False. Default is False. + Print debugging information. True or False. Default is False. editor;; - The editor to use during a commit. Default is $EDITOR or "vi". + The editor to use during a commit. Default is $EDITOR or "vi". fallbackencoding;; Encoding to try if it's not possible to decode the changelog using - UTF-8. Default is ISO-8859-1. + UTF-8. Default is ISO-8859-1. ignore;; A file to read per-user ignore patterns from. This file should be in the same format as a repository-wide .hgignore file. This option @@ -546,7 +579,7 @@ "ignore.other = ~/.hgignore2". For details of the ignore file format, see the hgignore(5) man page. interactive;; - Allow to prompt the user. True or False. Default is True. + Allow to prompt the user. True or False. Default is True. logtemplate;; Template string for commands that print changesets. merge;; @@ -563,18 +596,19 @@ fail to merge See the merge-tools section for more information on configuring tools. + patch;; command to use to apply patches. Look for 'gpatch' or 'patch' in PATH if unset. quiet;; - Reduce the amount of output printed. True or False. Default is False. + Reduce the amount of output printed. True or False. Default is False. remotecmd;; remote command to use for clone/push/pull operations. Default is 'hg'. report_untrusted;; Warn if a .hg/hgrc file is ignored due to not being owned by a - trusted user or group. True or False. Default is True. + trusted user or group. True or False. Default is True. slash;; - Display paths using a slash ("/") as the path separator. This only + Display paths using a slash ("/") as the path separator. This only makes a difference on systems where the default path separator is not the slash character (e.g. Windows uses the backslash character ("\")). Default is False. @@ -582,7 +616,7 @@ command to use for SSH connections. Default is 'ssh'. strict;; Require exact command names, instead of allowing unambiguous - abbreviations. True or False. Default is False. + abbreviations. True or False. Default is False. style;; Name of style to use for command output. timeout;; @@ -591,14 +625,15 @@ username;; The committer of a changeset created when running "commit". Typically a person's name and email address, e.g. "Fred Widget - ". Default is $EMAIL or username@hostname. + ". Default is $EMAIL or username@hostname. If the username in hgrc is empty, it has to be specified manually or in a different hgrc file (e.g. $HOME/.hgrc, if the admin set "username =" in the system hgrc). verbose;; - Increase the amount of output printed. True or False. Default is False. + Increase the amount of output printed. True or False. Default is False. +[[web]] web:: Web interface configuration. accesslog;; @@ -617,9 +652,9 @@ allowpull;; Whether to allow pulling from the repository. Default is true. allow_push;; - Whether to allow pushing to the repository. If empty or not set, - push is not allowed. If the special value "*", any remote user - can push, including unauthenticated users. Otherwise, the remote + Whether to allow pushing to the repository. If empty or not set, + push is not allowed. If the special value "*", any remote user + can push, including unauthenticated users. Otherwise, the remote user must have been authenticated, and the authenticated user name must be present in this list (separated by whitespace or ","). The contents of the allow_push list are examined after the @@ -635,11 +670,11 @@ Name or email address of the person in charge of the repository. 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 - are denied push. Otherwise, unauthenticated users are all denied, + Whether to deny pushing to the repository. If empty or not set, + push is not denied. If the special value "*", all remote users + are denied push. Otherwise, unauthenticated users are all denied, and any authenticated user name present in this list (separated by - whitespace or ",") is also denied. The contents of the deny_push + whitespace or ",") is also denied. The contents of the deny_push list are examined before the allow_push list. description;; Textual description of the repository's purpose or contents. @@ -666,7 +701,7 @@ 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. + prevent password sniffing. Default is true. staticurl;; Base URL to use for static files. If unset, static files (e.g. the hgicon.png favicon) will be served by the CGI script itself. diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/acl.py --- a/hgext/acl.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/acl.py Thu Oct 23 15:44:23 2008 +0200 @@ -46,79 +46,45 @@ # ** = user6 from mercurial.i18n import _ -from mercurial.node import bin, short from mercurial import util import getpass -class checker(object): - '''acl checker.''' - - def buildmatch(self, key): - '''return tuple of (match function, list enabled).''' - if not self.ui.has_section(key): - self.ui.debug(_('acl: %s not enabled\n') % key) - return None, False - - thisuser = self.getuser() - pats = [pat for pat, users in self.ui.configitems(key) - if thisuser in users.replace(',', ' ').split()] - self.ui.debug(_('acl: %s enabled, %d entries for user %s\n') % - (key, len(pats), thisuser)) - if pats: - match = util.matcher(self.repo.root, names=pats)[1] - else: - match = util.never - return match, True - - def getuser(self): - '''return name of authenticated user.''' - return self.user +def buildmatch(ui, repo, user, key): + '''return tuple of (match function, list enabled).''' + if not ui.has_section(key): + ui.debug(_('acl: %s not enabled\n') % key) + return None - def __init__(self, ui, repo): - self.ui = ui - self.repo = repo - self.user = getpass.getuser() - cfg = self.ui.config('acl', 'config') - if cfg: - self.ui.readsections(cfg, 'acl.allow', 'acl.deny') - self.allow, self.allowable = self.buildmatch('acl.allow') - self.deny, self.deniable = self.buildmatch('acl.deny') - - def skipsource(self, source): - '''true if incoming changes from this source should be skipped.''' - ok_sources = self.ui.config('acl', 'sources', 'serve').split() - return source not in ok_sources - - def check(self, node): - '''return if access allowed, raise exception if not.''' - files = self.repo.changectx(node).files() - if self.deniable: - for f in files: - if self.deny(f): - self.ui.debug(_('acl: user %s denied on %s\n') % - (self.getuser(), f)) - raise util.Abort(_('acl: access denied for changeset %s') % - short(node)) - if self.allowable: - for f in files: - if not self.allow(f): - self.ui.debug(_('acl: user %s not allowed on %s\n') % - (self.getuser(), f)) - raise util.Abort(_('acl: access denied for changeset %s') % - short(node)) - self.ui.debug(_('acl: allowing changeset %s\n') % short(node)) + pats = [pat for pat, users in ui.configitems(key) + if user in users.replace(',', ' ').split()] + ui.debug(_('acl: %s enabled, %d entries for user %s\n') % + (key, len(pats), user)) + if pats: + return util.matcher(repo.root, names=pats)[1] + return util.never def hook(ui, repo, hooktype, node=None, source=None, **kwargs): if hooktype != 'pretxnchangegroup': raise util.Abort(_('config error - hook type "%s" cannot stop ' 'incoming changesets') % hooktype) - - c = checker(ui, repo) - if c.skipsource(source): + if source not in ui.config('acl', 'sources', 'serve').split(): ui.debug(_('acl: changes have source "%s" - skipping\n') % source) return - start = repo.changelog.rev(bin(node)) - end = repo.changelog.count() - for rev in xrange(start, end): - c.check(repo.changelog.node(rev)) + user = getpass.getuser() + cfg = ui.config('acl', 'config') + if cfg: + ui.readsections(cfg, 'acl.allow', 'acl.deny') + allow = buildmatch(ui, repo, user, 'acl.allow') + deny = buildmatch(ui, repo, user, 'acl.deny') + + for rev in xrange(repo[node], len(repo)): + ctx = repo[rev] + for f in ctx.files(): + if deny and deny(f): + ui.debug(_('acl: user %s denied on %s\n') % (user, f)) + raise util.Abort(_('acl: access denied for changeset %s') % ctx) + if allow and not allow(f): + ui.debug(_('acl: user %s not allowed on %s\n') % (user, f)) + raise util.Abort(_('acl: access denied for changeset %s') % ctx) + ui.debug(_('acl: allowing changeset %s\n') % ctx) diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/alias.py --- a/hgext/alias.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/alias.py Thu Oct 23 15:44:23 2008 +0200 @@ -11,6 +11,7 @@ from mercurial.cmdutil import findcmd, UnknownCommand, AmbiguousCommand from mercurial import commands +from mercurial.i18n import _ cmdtable = {} @@ -42,20 +43,20 @@ return try: - self._cmd = findcmd(self._ui, self._target, commands.table)[1] + self._cmd = findcmd(self._target, commands.table, False)[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' % \ + msg = _('*** [alias] %s: command %s is unknown') % \ (self._name, self._target) except AmbiguousCommand: - msg = '*** [alias] %s: command %s is ambiguous' % \ + msg = _('*** [alias] %s: command %s is ambiguous') % \ (self._name, self._target) except RecursiveCommand: - msg = '*** [alias] %s: circular dependency on %s' % \ + msg = _('*** [alias] %s: circular dependency on %s') % \ (self._name, self._target) def nocmd(*args, **opts): self._ui.warn(msg + '\n') @@ -67,7 +68,7 @@ def uisetup(ui): for cmd, target in ui.configitems('alias'): if not target: - ui.warn('*** [alias] %s: no definition\n' % cmd) + ui.warn(_('*** [alias] %s: no definition\n') % cmd) continue args = target.split(' ') tcmd = args.pop(0) diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/bugzilla.py --- a/hgext/bugzilla.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/bugzilla.py Thu Oct 23 15:44:23 2008 +0200 @@ -55,7 +55,7 @@ from mercurial.i18n import _ from mercurial.node import short from mercurial import cmdutil, templater, util -import os, re, time +import re, time MySQLdb = None @@ -80,11 +80,7 @@ self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout) self.cursor = self.conn.cursor() - self.run('select fieldid from fielddefs where name = "longdesc"') - ids = self.cursor.fetchall() - if len(ids) != 1: - raise util.Abort(_('unknown database schema')) - self.longdesc_id = ids[0][0] + self.longdesc_id = self.get_longdesc_id() self.user_ids = {} def run(self, *args, **kwargs): @@ -96,12 +92,18 @@ self.ui.note(_('failed query: %s %s\n') % (args, kwargs)) raise + def get_longdesc_id(self): + '''get identity of longdesc field''' + self.run('select fieldid from fielddefs where name = "longdesc"') + ids = self.cursor.fetchall() + if len(ids) != 1: + raise util.Abort(_('unknown database schema')) + return ids[0][0] + def filter_real_bug_ids(self, ids): '''filter not-existing bug ids from list.''' self.run('select bug_id from bugs where bug_id in %s' % buglist(ids)) - ids = [c[0] for c in self.cursor.fetchall()] - ids.sort() - return ids + return util.sort([c[0] for c in self.cursor.fetchall()]) def filter_unknown_bug_ids(self, node, ids): '''filter bug ids from list that already refer to this changeset.''' @@ -114,9 +116,7 @@ self.ui.status(_('bug %d already knows about changeset %s\n') % (id, short(node))) unknown.pop(id, None) - ids = unknown.keys() - ids.sort() - return ids + return util.sort(unknown.keys()) def notify(self, ids): '''tell bugzilla to send mail.''' @@ -127,7 +127,7 @@ cmd = self.ui.config('bugzilla', 'notify', 'cd /var/www/html/bugzilla && ' './processmail %s nobody@nowhere.com') % id - fp = os.popen('(%s) 2>&1' % cmd) + fp = util.popen('(%s) 2>&1' % cmd) out = fp.read() ret = fp.close() if ret: @@ -186,11 +186,26 @@ values (%s, %s, %s, %s)''', (bugid, userid, now, self.longdesc_id)) +class bugzilla_3_0(bugzilla_2_16): + '''support for bugzilla 3.0 series.''' + + def __init__(self, ui): + bugzilla_2_16.__init__(self, ui) + + def get_longdesc_id(self): + '''get identity of longdesc field''' + self.run('select id from fielddefs where name = "longdesc"') + ids = self.cursor.fetchall() + if len(ids) != 1: + raise util.Abort(_('unknown database schema')) + return ids[0][0] + class bugzilla(object): # supported versions of bugzilla. different versions have # different schemas. _versions = { '2.16': bugzilla_2_16, + '3.0': bugzilla_3_0 } _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*' @@ -300,7 +315,7 @@ hooktype) try: bz = bugzilla(ui, repo) - ctx = repo.changectx(node) + ctx = repo[node] ids = bz.find_bug_ids(ctx) if ids: for id in ids: diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/children.py --- a/hgext/children.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/children.py Thu Oct 23 15:44:23 2008 +0200 @@ -25,7 +25,7 @@ if file_: ctx = repo.filectx(file_, changeid=rev) else: - ctx = repo.changectx(rev) + ctx = repo[rev] displayer = cmdutil.show_changeset(ui, repo, opts) for node in [cp.node() for cp in ctx.children()]: diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/churn.py --- a/hgext/churn.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/churn.py Thu Oct 23 15:44:23 2008 +0200 @@ -1,19 +1,16 @@ -# churn.py - create a graph showing who changed the most lines +# churn.py - create a graph of revisions count grouped by template # # Copyright 2006 Josef "Jeff" Sipek +# Copyright 2008 Alexander Solovyov # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -# -# -# Aliases map file format is simple one alias per line in the following -# format: -# -# +'''command to show certain statistics about revision history''' -from mercurial.i18n import gettext as _ -from mercurial import mdiff, cmdutil, util, node +from mercurial.i18n import _ +from mercurial import patch, cmdutil, util, templater import os, sys +import time, datetime def get_tty_width(): if 'COLUMNS' in os.environ: @@ -36,171 +33,152 @@ pass return 80 -def __gather(ui, repo, node1, node2): - def dirtywork(f, mmap1, mmap2): - lines = 0 - - to = mmap1 and repo.file(f).read(mmap1[f]) or None - tn = mmap2 and repo.file(f).read(mmap2[f]) or None - - diff = mdiff.unidiff(to, "", tn, "", f, f).split("\n") +def maketemplater(ui, repo, tmpl): + tmpl = templater.parsestring(tmpl, quoted=False) + try: + t = cmdutil.changeset_templater(ui, repo, False, None, False) + except SyntaxError, inst: + raise util.Abort(inst.args[0]) + t.use_template(tmpl) + return t - for line in diff: - if not line: - continue # skip EOF - if line.startswith(" "): - continue # context line - if line.startswith("--- ") or line.startswith("+++ "): - continue # begining of diff - if line.startswith("@@ "): - continue # info line - - # changed lines +def changedlines(ui, repo, ctx1, ctx2): + lines = 0 + ui.pushbuffer() + patch.diff(repo, ctx1.node(), ctx2.node()) + diff = ui.popbuffer() + for l in diff.split('\n'): + if (l.startswith("+") and not l.startswith("+++ ") or + l.startswith("-") and not l.startswith("--- ")): lines += 1 - - return lines - - ## - - lines = 0 - - changes = repo.status(node1, node2, None, util.always)[:5] - - modified, added, removed, deleted, unknown = changes - - who = repo.changelog.read(node2)[1] - who = util.email(who) # get the email of the person + return lines - mmap1 = repo.manifest.read(repo.changelog.read(node1)[0]) - mmap2 = repo.manifest.read(repo.changelog.read(node2)[0]) - for f in modified: - lines += dirtywork(f, mmap1, mmap2) - - for f in added: - lines += dirtywork(f, None, mmap2) - - for f in removed: - lines += dirtywork(f, mmap1, None) - - for f in deleted: - lines += dirtywork(f, mmap1, mmap2) - - for f in unknown: - lines += dirtywork(f, mmap1, mmap2) - - return (who, lines) +def countrate(ui, repo, amap, *pats, **opts): + """Calculate stats""" + if opts.get('dateformat'): + def getkey(ctx): + t, tz = ctx.date() + date = datetime.datetime(*time.gmtime(float(t) - tz)[:6]) + return date.strftime(opts['dateformat']) + else: + tmpl = opts.get('template', '{author|email}') + tmpl = maketemplater(ui, repo, tmpl) + def getkey(ctx): + ui.pushbuffer() + tmpl.show(changenode=ctx.node()) + return ui.popbuffer() -def gather_stats(ui, repo, amap, revs=None, progress=False): - stats = {} - - cl = repo.changelog - - if not revs: - revs = range(0, cl.count()) + count = pct = 0 + rate = {} + df = False + if opts.get('date'): + df = util.matchdate(opts['date']) - nr_revs = len(revs) - cur_rev = 0 - - for rev in revs: - cur_rev += 1 # next revision - - node2 = cl.node(rev) - node1 = cl.parents(node2)[0] - - if cl.parents(node2)[1] != node.nullid: - ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) + get = util.cachefunc(lambda r: repo[r].changeset()) + changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts) + for st, rev, fns in changeiter: + if not st == 'add': + continue + if df and not df(get(rev)[2][0]): # doesn't match date format continue - who, lines = __gather(ui, repo, node1, node2) - - # remap the owner if possible - if who in amap: - ui.note("using '%s' alias for '%s'\n" % (amap[who], who)) - who = amap[who] + ctx = repo[rev] + key = getkey(ctx) + key = amap.get(key, key) # alias remap + if opts.get('changesets'): + rate[key] = rate.get(key, 0) + 1 + else: + parents = ctx.parents() + if len(parents) > 1: + ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,)) + continue - if not who in stats: - stats[who] = 0 - stats[who] += lines + ctx1 = parents[0] + lines = changedlines(ui, repo, ctx1, ctx) + rate[key] = rate.get(key, 0) + 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("\rGenerating stats: %d%%" % (int(100.0*cur_rev/nr_revs),)) + if opts.get('progress'): + count += 1 + newpct = int(100.0 * count / max(len(repo), 1)) + if pct < newpct: + pct = newpct + ui.write(_("\rGenerating stats: %d%%") % pct) sys.stdout.flush() - if progress: + if opts.get('progress'): ui.write("\r") sys.stdout.flush() - return stats + return rate + -def churn(ui, repo, **opts): - "Graphs the number of lines changed" +def churn(ui, repo, *pats, **opts): + '''Graph count of revisions grouped by template - def pad(s, l): - if len(s) < l: - return s + " " * (l-len(s)) - return s[0:l] + Will graph count of changed lines or revisions grouped by template or + alternatively by date, if dateformat is used. In this case it will override + template. + + By default statistics are counted for number of changed lines. + + Examples: - def graph(n, maximum, width, char): - maximum = max(1, maximum) - n = int(n * width / float(maximum)) + # display count of changed lines for every committer + hg churn -t '{author|email}' - return char * (n) + # display daily activity graph + hg churn -f '%H' -s -c + + # display activity of developers by month + hg churn -f '%Y-%m' -s -c - def get_aliases(f): - aliases = {} + # display count of lines changed in every year + hg churn -f '%Y' -s - for l in f.readlines(): - l = l.strip() - alias, actual = l.split() - aliases[alias] = actual + The map file format used to specify aliases is fairly simple: - return aliases + ''' + def pad(s, l): + return (s + " " * l)[:l] amap = {} aliases = opts.get('aliases') if aliases: - try: - f = open(aliases,"r") - except OSError, e: - print "Error: " + e - return + for l in open(aliases, "r"): + l = l.strip() + alias, actual = l.split() + amap[alias] = actual - amap = get_aliases(f) - f.close() + rate = countrate(ui, repo, amap, *pats, **opts).items() + if not rate: + return - revs = [int(r) for r in cmdutil.revrange(repo, opts['rev'])] - revs.sort() - stats = gather_stats(ui, repo, amap, revs, opts.get('progress')) + sortfn = ((not opts.get('sort')) and (lambda a, b: cmp(b[1], a[1])) or None) + rate.sort(sortfn) - # make a list of tuples (name, lines) and sort it in descending order - ordered = stats.items() - if not ordered: - return - ordered.sort(lambda x, y: cmp(y[1], x[1])) - max_churn = ordered[0][1] + maxcount = float(max([v for k, v in rate])) + maxname = max([len(k) for k, v in rate]) - tty_width = get_tty_width() - ui.note(_("assuming %i character terminal\n") % tty_width) - tty_width -= 1 - - max_user_width = max([len(user) for user, churn in ordered]) + ttywidth = get_tty_width() + ui.debug(_("assuming %i character terminal\n") % ttywidth) + width = ttywidth - maxname - 2 - 6 - 2 - 2 - graph_width = tty_width - max_user_width - 1 - 6 - 2 - 2 + for date, count in rate: + print "%s %6d %s" % (pad(date, maxname), count, + "*" * int(count * width / maxcount)) - for user, churn in ordered: - print "%s %6d %s" % (pad(user, max_user_width), - churn, - graph(churn, max_churn, graph_width, '*')) cmdtable = { "churn": - (churn, - [('r', 'rev', [], _('limit statistics to the specified revisions')), - ('', 'aliases', '', _('file with email aliases')), - ('', 'progress', None, _('show progress'))], - 'hg churn [-r REVISIONS] [--aliases FILE] [--progress]'), + (churn, + [('r', 'rev', [], _('count rate for the specified revision or range')), + ('d', 'date', '', _('count rate for revs matching date spec')), + ('t', 'template', '{author|email}', _('template to group changesets')), + ('f', 'dateformat', '', + _('strftime-compatible format for grouping by date')), + ('c', 'changesets', False, _('count rate by number of changesets')), + ('s', 'sort', False, _('sort by key (default: sort by count)')), + ('', 'aliases', '', _('file with email aliases')), + ('', 'progress', None, _('show progress'))], + _("hg churn [-d DATE] [-r REV] [--aliases FILE] [--progress] [FILE]")), } diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/color.py --- a/hgext/color.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/color.py Thu Oct 23 15:44:23 2008 +0200 @@ -51,7 +51,7 @@ import re, sys -from mercurial import commands, cmdutil +from mercurial import commands, cmdutil, extensions from mercurial.i18n import _ # start and stop parameters for effects @@ -89,14 +89,14 @@ stop = '\033[' + ';'.join(stop) + 'm' return start + text + stop -def colorstatus(statusfunc, ui, repo, *pats, **opts): +def colorstatus(orig, 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) + retval = orig(ui, repo, *pats, **opts) # filter out empty strings lines = [ line for line in ui.popbuffer().split(delimiter) if line ] @@ -139,10 +139,10 @@ 'clean': ('none', ), 'copied': ('none', ), } -def colorqseries(qseriesfunc, ui, repo, *dummy, **opts): +def colorqseries(orig, ui, repo, *dummy, **opts): '''run the qseries command with colored output''' ui.pushbuffer() - retval = qseriesfunc(ui, repo, **opts) + retval = orig(ui, repo, **opts) patches = ui.popbuffer().splitlines() for patch in patches: patchname = patch @@ -168,58 +168,23 @@ 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); + _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects) if ui.config('extensions', 'hgext.mq') is not None or \ ui.config('extensions', 'mq') 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 + _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects) -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. +def _setupcmd(ui, cmd, table, func, effectsmap): + '''patch in command to command table and load effect map''' + def nocolor(orig, *args, **kwargs): + if kwargs['no_color']: + return orig(*args, **kwargs) + return func(orig, *args, **kwargs) - 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) + entry = extensions.wrapcommand(table, cmd, nocolor) + entry[1].append(('', 'no-color', None, _("don't colorize output"))) -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) + effects = ui.config('color', cmd + '.' + status) if effects: effectsmap[status] = re.split('\W+', effects) diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/convert/__init__.py --- a/hgext/convert/__init__.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/convert/__init__.py Thu Oct 23 15:44:23 2008 +0200 @@ -4,27 +4,30 @@ # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. +'''converting foreign VCS repositories to Mercurial''' import convcmd from mercurial import commands +from mercurial.i18n import _ # Commands definition was moved elsewhere to ease demandload job. def convert(ui, src, dest=None, revmapfile=None, **opts): """Convert a foreign SCM repository to a Mercurial one. - Accepted source formats: - - Mercurial - - CVS - - Darcs - - git - - Subversion - - Monotone - - GNU Arch + Accepted source formats [identifiers]: + - Mercurial [hg] + - CVS [cvs] + - Darcs [darcs] + - git [git] + - Subversion [svn] + - Monotone [mtn] + - GNU Arch [gnuarch] + - Bazaar [bzr] - Accepted destination formats: - - Mercurial - - Subversion (history on branches is not preserved) + Accepted destination formats [identifiers]: + - Mercurial [hg] + - Subversion [svn] (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 @@ -84,6 +87,53 @@ --config convert.hg.saverev=True (boolean) allow target to preserve source revision ID + --config convert.hg.startrev=0 (hg revision identifier) + convert start revision and its descendants + + CVS Source + ---------- + + CVS source will use a sandbox (i.e. a checked-out copy) from CVS + to indicate the starting point of what will be converted. Direct + access to the repository files is not needed, unless of course + the repository is :local:. The conversion uses the top level + directory in the sandbox to find the CVS repository, and then uses + CVS rlog commands to find files to convert. This means that unless + a filemap is given, all files under the starting directory will be + converted, and that any directory reorganisation in the CVS + sandbox is ignored. + + Because CVS does not have changesets, it is necessary to collect + individual commits to CVS and merge them into changesets. CVS + source uses its internal changeset merging code by default but can + be configured to call the external 'cvsps' program by setting: + --config convert.cvsps='cvsps -A -u --cvs-direct -q' + This is a legacy option and may be removed in future. + + The options shown are the defaults. + + Internal cvsps is selected by setting + --config convert.cvsps=builtin + and has a few more configurable options: + --config convert.cvsps.fuzz=60 (integer) + Specify the maximum time (in seconds) that is allowed between + commits with identical user and log message in a single + changeset. When very large files were checked in as part + of a changeset then the default may not be long enough. + --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}' + Specify a regular expression to which commit log messages are + matched. If a match occurs, then the conversion process will + insert a dummy revision merging the branch on which this log + message occurs to the branch indicated in the regex. + --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}' + Specify a regular expression to which commit log messages are + matched. If a match occurs, then the conversion process will + add the most recent revision on the branch indicated in the + regex as the second parent of the changeset. + + The hgext/convert/cvsps wrapper script allows the builtin changeset + merging code to be run without doing a conversion. Its parameters and + output are similar to that of cvsps 2.1. Subversion Source ----------------- @@ -134,14 +184,14 @@ cmdtable = { "convert": (convert, - [('A', 'authors', '', 'username mapping filename'), - ('d', 'dest-type', '', 'destination repository type'), - ('', 'filemap', '', 'remap file names using contents of file'), - ('r', 'rev', '', 'import up to target revision REV'), - ('s', 'source-type', '', 'source repository type'), - ('', 'splicemap', '', 'splice synthesized history into place'), - ('', 'datesort', None, 'try to sort changesets by date')], - 'hg convert [OPTION]... SOURCE [DEST [REVMAP]]'), + [('A', 'authors', '', _('username mapping filename')), + ('d', 'dest-type', '', _('destination repository type')), + ('', 'filemap', '', _('remap file names using contents of file')), + ('r', 'rev', '', _('import up to target revision REV')), + ('s', 'source-type', '', _('source repository type')), + ('', 'splicemap', '', _('splice synthesized history into place')), + ('', 'datesort', None, _('try to sort changesets by date'))], + _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]')), "debugsvnlog": (debugsvnlog, [], diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/convert/bzr.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/convert/bzr.py Thu Oct 23 15:44:23 2008 +0200 @@ -0,0 +1,216 @@ +# bzr support for the convert extension +# This module is for handling 'bzr', that was formerly known as Bazaar-NG; +# it cannot access 'bar' repositories, but they were never used very much + +import os +from mercurial import demandimport +# these do not work with demandimport, blacklist +demandimport.ignore.extend([ + 'bzrlib.transactions', + 'bzrlib.urlutils', + ]) + +from mercurial.i18n import _ +from mercurial import util +from common import NoRepo, commit, converter_source + +try: + # bazaar imports + from bzrlib import branch, revision, errors + from bzrlib.revisionspec import RevisionSpec +except ImportError: + pass + +class bzr_source(converter_source): + """Reads Bazaar repositories by using the Bazaar Python libraries""" + + def __init__(self, ui, path, rev=None): + super(bzr_source, self).__init__(ui, path, rev=rev) + + try: + # access bzrlib stuff + branch + except NameError: + raise NoRepo('Bazaar modules could not be loaded') + + if not os.path.exists(os.path.join(path, '.bzr')): + raise NoRepo('%s does not look like a Bazaar repo' % path) + + path = os.path.abspath(path) + self.branch = branch.Branch.open(path) + self.sourcerepo = self.branch.repository + self._parentids = {} + + def before(self): + """Before the conversion begins, acquire a read lock + for all the operations that might need it. Fortunately + read locks don't block other reads or writes to the + repository, so this shouldn't have any impact on the usage of + the source repository. + + The alternative would be locking on every operation that + needs locks (there are currently two: getting the file and + getting the parent map) and releasing immediately after, + but this approach can take even 40% longer.""" + self.sourcerepo.lock_read() + + def after(self): + self.sourcerepo.unlock() + + def getheads(self): + if not self.rev: + return [self.branch.last_revision()] + try: + r = RevisionSpec.from_string(self.rev) + info = r.in_history(self.branch) + except errors.BzrError: + raise util.Abort(_('%s is not a valid revision in current branch') + % self.rev) + return [info.rev_id] + + def getfile(self, name, rev): + revtree = self.sourcerepo.revision_tree(rev) + fileid = revtree.path2id(name) + if fileid is None: + # the file is not available anymore - was deleted + raise IOError(_('%s is not available in %s anymore') % + (name, rev)) + sio = revtree.get_file(fileid) + return sio.read() + + def getmode(self, name, rev): + return self._modecache[(name, rev)] + + def getchanges(self, version): + # set up caches: modecache and revtree + self._modecache = {} + self._revtree = self.sourcerepo.revision_tree(version) + # get the parentids from the cache + parentids = self._parentids.pop(version) + # only diff against first parent id + prevtree = self.sourcerepo.revision_tree(parentids[0]) + return self._gettreechanges(self._revtree, prevtree) + + def getcommit(self, version): + rev = self.sourcerepo.get_revision(version) + # populate parent id cache + if not rev.parent_ids: + parents = [] + self._parentids[version] = (revision.NULL_REVISION,) + else: + parents = self._filterghosts(rev.parent_ids) + self._parentids[version] = parents + + return commit(parents=parents, + # bzr uses 1 second timezone precision + date='%d %d' % (rev.timestamp, rev.timezone / 3600), + author=self.recode(rev.committer), + # bzr returns bytestrings or unicode, depending on the content + desc=self.recode(rev.message), + rev=version) + + def gettags(self): + if not self.branch.supports_tags(): + return {} + tagdict = self.branch.tags.get_tag_dict() + bytetags = {} + for name, rev in tagdict.iteritems(): + bytetags[self.recode(name)] = rev + return bytetags + + def getchangedfiles(self, rev, i): + self._modecache = {} + curtree = self.sourcerepo.revision_tree(rev) + parentids = self._parentids.pop(rev) + if i is not None: + parentid = parentids[i] + else: + # no parent id, get the empty revision + parentid = revision.NULL_REVISION + + prevtree = self.sourcerepo.revision_tree(parentid) + changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]] + return changes + + def _gettreechanges(self, current, origin): + revid = current._revision_id; + changes = [] + renames = {} + for (fileid, paths, changed_content, versioned, parent, name, + kind, executable) in current.iter_changes(origin): + + if paths[0] == u'' or paths[1] == u'': + # ignore changes to tree root + continue + + # bazaar tracks directories, mercurial does not, so + # we have to rename the directory contents + if kind[1] == 'directory': + if None not in paths and paths[0] != paths[1]: + # neither an add nor an delete - a move + # rename all directory contents manually + subdir = origin.inventory.path2id(paths[0]) + # get all child-entries of the directory + for name, entry in origin.inventory.iter_entries(subdir): + # hg does not track directory renames + if entry.kind == 'directory': + continue + frompath = self.recode(paths[0] + '/' + name) + topath = self.recode(paths[1] + '/' + name) + # register the files as changed + changes.append((frompath, revid)) + changes.append((topath, revid)) + # add to mode cache + mode = ((entry.executable and 'x') or (entry.kind == 'symlink' and 's') + or '') + self._modecache[(topath, revid)] = mode + # register the change as move + renames[topath] = frompath + + # no futher changes, go to the next change + continue + + # we got unicode paths, need to convert them + path, topath = [self.recode(part) for part in paths] + + if topath is None: + # file deleted + changes.append((path, revid)) + continue + + # renamed + if path and path != topath: + renames[topath] = path + + # populate the mode cache + kind, executable = [e[1] for e in (kind, executable)] + mode = ((executable and 'x') or (kind == 'symlink' and 's') + or '') + self._modecache[(topath, revid)] = mode + changes.append((topath, revid)) + + return changes, renames + + def _filterghosts(self, ids): + """Filters out ghost revisions which hg does not support, see + + """ + parentmap = self.sourcerepo.get_parent_map(ids) + parents = tuple([parent for parent in ids if parent in parentmap]) + return parents + + def recode(self, s, encoding=None): + """This version of recode tries to encode unicode to bytecode, + and preferably using the UTF-8 codec. + Other types than Unicode are silently returned, this is by + intention, e.g. the None-type is not going to be encoded but instead + just passed through + """ + if not encoding: + encoding = self.encoding or 'utf-8' + + if isinstance(s, unicode): + return s.encode(encoding) + else: + # leave it alone + return s diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/convert/common.py --- a/hgext/convert/common.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/convert/common.py Thu Oct 23 15:44:23 2008 +0200 @@ -68,17 +68,23 @@ raise NotImplementedError() def getfile(self, name, rev): - """Return file contents as a string""" + """Return file contents as a string. rev is the identifier returned + by a previous call to getchanges(). + """ raise NotImplementedError() def getmode(self, name, rev): - """Return file mode, eg. '', 'x', or 'l'""" + """Return file mode, eg. '', 'x', or 'l'. rev is the identifier + returned by a previous call to getchanges(). + """ raise NotImplementedError() def getchanges(self, version): - """Returns a tuple of (files, copies) - Files is a sorted list of (filename, id) tuples for all files changed - in version, where id is the source revision id of the file. + """Returns a tuple of (files, copies). + + files is a sorted list of (filename, id) tuples for all files + changed between version and it's first parent returned by + getcommit(). id is the source revision id of the file. copies is a dictionary of dest: source """ @@ -153,26 +159,18 @@ mapping equivalent authors identifiers for each system.""" return None - def putfile(self, f, e, data): - """Put file for next putcommit(). - f: path to file - e: '', 'x', or 'l' (regular file, executable, or symlink) - data: file contents""" - raise NotImplementedError() - - def delfile(self, f): - """Delete file for next putcommit(). - f: path to file""" - raise NotImplementedError() - - def putcommit(self, files, parents, commit): + def putcommit(self, files, copies, parents, commit, source): """Create a revision with all changed files listed in 'files' and having listed parents. 'commit' is a commit object containing at a minimum the author, date, and message for this changeset. - Called after putfile() and delfile() calls. Note that the sink - repository is not told to update itself to a particular revision - (or even what that revision would be) before it receives the - file data.""" + 'files' is a list of (path, version) tuples, 'copies'is a dictionary + mapping destinations to sources, and 'source' is the source repository. + Only getfile() and getmode() should be called on 'source'. + + Note that the sink repository is not told to update itself to + a particular revision (or even what that revision would be) + before it receives the file data. + """ raise NotImplementedError() def puttags(self, tags): @@ -181,7 +179,7 @@ raise NotImplementedError() def setbranch(self, branch, pbranches): - """Set the current branch name. Called before the first putfile + """Set the current branch name. Called before the first putcommit on the branch. branch: branch name for subsequent commits pbranches: (converted parent revision, parent branch) tuples""" @@ -236,7 +234,7 @@ def _run(self, cmd, *args, **kwargs): cmdline = self._cmdline(cmd, *args, **kwargs) - self.ui.debug('running: %s\n' % (cmdline,)) + self.ui.debug(_('running: %s\n') % (cmdline,)) self.prerun() try: return util.popen(cmdline) diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/convert/convcmd.py --- a/hgext/convert/convcmd.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/convert/convcmd.py Thu Oct 23 15:44:23 2008 +0200 @@ -13,6 +13,7 @@ from subversion import debugsvnlog, svn_source, svn_sink from monotone import monotone_source from gnuarch import gnuarch_source +from bzr import bzr_source import filemap import os, shutil @@ -35,6 +36,7 @@ ('darcs', darcs_source), ('mtn', monotone_source), ('gnuarch', gnuarch_source), + ('bzr', bzr_source), ] sink_converters = [ @@ -52,8 +54,8 @@ exceptions.append(inst) if not ui.quiet: for inst in exceptions: - ui.write(_("%s\n") % inst) - raise util.Abort('%s: unknown repository type' % path) + ui.write("%s\n" % inst) + raise util.Abort(_('%s: missing or unsupported repository') % path) def convertsink(ui, path, type): for name, sink in sink_converters: @@ -62,7 +64,7 @@ return sink(ui, path) except NoRepo, inst: ui.note(_("convert: %s\n") % inst) - raise util.Abort('%s: unknown repository type' % path) + raise util.Abort(_('%s: unknown repository type') % path) class converter(object): def __init__(self, ui, source, dest, revmapfile, opts): @@ -184,7 +186,7 @@ def writeauthormap(self): authorfile = self.authorfile if authorfile: - self.ui.status('Writing author map file %s\n' % authorfile) + self.ui.status(_('Writing author map file %s\n') % authorfile) ofile = open(authorfile, 'w+') for author in self.authors: ofile.write("%s=%s\n" % (author, self.authors[author])) @@ -201,15 +203,15 @@ dstauthor = dstauthor.strip() if srcauthor in self.authors and dstauthor != self.authors[srcauthor]: self.ui.status( - 'Overriding mapping for author %s, was %s, will be %s\n' + _('Overriding mapping for author %s, was %s, will be %s\n') % (srcauthor, self.authors[srcauthor], dstauthor)) else: - self.ui.debug('Mapping author %s to %s\n' + self.ui.debug(_('Mapping author %s to %s\n') % (srcauthor, dstauthor)) self.authors[srcauthor] = dstauthor except IndexError: self.ui.warn( - 'Ignoring bad line in author map file %s: %s\n' + _('Ignoring bad line in author map file %s: %s\n') % (authorfile, line.rstrip())) afile.close() @@ -221,8 +223,6 @@ def copy(self, rev): commit = self.commitcache[rev] - do_copies = hasattr(self.dest, 'copyfile') - filenames = [] changes = self.source.getchanges(rev) if isinstance(changes, basestring): @@ -241,29 +241,14 @@ pbranches.append((self.map[prev], self.commitcache[prev].branch)) self.dest.setbranch(commit.branch, pbranches) - for f, v in files: - filenames.append(f) - try: - data = self.source.getfile(f, v) - except IOError, inst: - self.dest.delfile(f) - else: - e = self.source.getmode(f, v) - self.dest.putfile(f, e, data) - if do_copies: - if f in copies: - copyf = copies[f] - # Merely marks that a copy happened. - self.dest.copyfile(copyf, f) - try: parents = self.splicemap[rev].replace(',', ' ').split() - self.ui.status('spliced in %s as parents of %s\n' % + self.ui.status(_('spliced in %s as parents of %s\n') % (parents, rev)) parents = [self.map.get(p, p) for p in parents] except KeyError: parents = [b[0] for b in pbranches] - newnode = self.dest.putcommit(filenames, parents, commit) + newnode = self.dest.putcommit(files, copies, parents, commit, self.source) self.source.converted(rev, newnode) self.map[rev] = newnode @@ -273,15 +258,15 @@ self.source.before() self.dest.before() self.source.setrevmap(self.map) - self.ui.status("scanning source...\n") + self.ui.status(_("scanning source...\n")) heads = self.source.getheads() parents = self.walktree(heads) - self.ui.status("sorting...\n") + self.ui.status(_("sorting...\n")) t = self.toposort(parents) num = len(t) c = None - self.ui.status("converting...\n") + self.ui.status(_("converting...\n")) for c in t: num -= 1 desc = self.commitcache[c].desc @@ -291,7 +276,7 @@ # 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.ui.note(_("source: %s\n") % recode(c)) self.copy(c) tags = self.source.gettags() @@ -326,7 +311,7 @@ if not dest: dest = hg.defaultdest(src) + "-hg" - ui.status("assuming destination %s\n" % dest) + ui.status(_("assuming destination %s\n") % dest) destc = convertsink(ui, dest, opts.get('dest_type')) diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/convert/cvs.py --- a/hgext/convert/cvs.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/convert/cvs.py Thu Oct 23 15:44:23 2008 +0200 @@ -3,8 +3,10 @@ import os, locale, re, socket from cStringIO import StringIO from mercurial import util +from mercurial.i18n import _ from common import NoRepo, commit, converter_source, checktool +import cvsps class convert_cvs(converter_source): def __init__(self, ui, path, rev=None): @@ -14,10 +16,13 @@ if not os.path.exists(cvs): raise NoRepo("%s does not look like a CVS checkout" % path) - self.cmd = ui.config('convert', 'cvsps', 'cvsps -A -u --cvs-direct -q') + checktool('cvs') + self.cmd = ui.config('convert', 'cvsps', 'builtin') cvspsexe = self.cmd.split(None, 1)[0] - for tool in (cvspsexe, 'cvs'): - checktool(tool) + self.builtin = cvspsexe == 'builtin' + + if not self.builtin: + checktool(cvspsexe) self.changeset = {} self.files = {} @@ -28,10 +33,11 @@ self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1] self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1] self.encoding = locale.getpreferredencoding() - self._parse() + + self._parse(ui) self._connect() - def _parse(self): + def _parse(self, ui): if self.changeset: return @@ -48,7 +54,7 @@ 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) + raise util.Abort(_('revision %s is not a patchset number or date') % self.rev) d = os.getcwd() try: @@ -56,80 +62,114 @@ id = None state = 0 filerevids = {} - for l in util.popen(cmd): - if state == 0: # header - if l.startswith("PatchSet"): - id = l[9:-2] - if maxrev and int(id) > maxrev: - # ignore everything - state = 3 - elif l.startswith("Date"): - date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"]) - date = util.datestr(date) - elif l.startswith("Branch"): - branch = l[8:-1] - self.parent[id] = self.lastbranch.get(branch, 'bad') - self.lastbranch[branch] = id - elif l.startswith("Ancestor branch"): - ancestor = l[17:-1] - # figure out the parent later - self.parent[id] = self.lastbranch[ancestor] - elif l.startswith("Author"): - author = self.recode(l[8:-1]) - elif l.startswith("Tag:") or l.startswith("Tags:"): - t = l[l.index(':')+1:] - t = [ut.strip() for ut in t.split(',')] - if (len(t) > 1) or (t[0] and (t[0] != "(none)")): - self.tags.update(dict.fromkeys(t, id)) - elif l.startswith("Log:"): - # switch to gathering log - state = 1 - log = "" - elif state == 1: # log - if l == "Members: \n": - # switch to gathering members - files = {} - oldrevs = [] - log = self.recode(log[:-1]) - state = 2 - else: - # gather log - log += l - elif state == 2: # members - if l == "\n": # start of next entry - state = 0 - p = [self.parent[id]] - if id == "1": - p = [] - if branch == "HEAD": - branch = "" - if branch: - latest = None - # the last changeset that contains a base - # file is our parent - for r in oldrevs: - latest = max(filerevids.get(r, None), latest) - if latest: - p = [latest] + + if self.builtin: + # builtin cvsps code + ui.status(_('using builtin cvsps\n')) + + db = cvsps.createlog(ui, cache='update') + db = cvsps.createchangeset(ui, db, + fuzz=int(ui.config('convert', 'cvsps.fuzz', 60)), + mergeto=ui.config('convert', 'cvsps.mergeto', None), + mergefrom=ui.config('convert', 'cvsps.mergefrom', None)) + + for cs in db: + if maxrev and cs.id>maxrev: + break + id = str(cs.id) + cs.author = self.recode(cs.author) + self.lastbranch[cs.branch] = id + cs.comment = self.recode(cs.comment) + date = util.datestr(cs.date) + self.tags.update(dict.fromkeys(cs.tags, id)) + + files = {} + for f in cs.entries: + files[f.file] = "%s%s" % ('.'.join([str(x) for x in f.revision]), + ['', '(DEAD)'][f.dead]) - # add current commit to set - c = commit(author=author, date=date, parents=p, - desc=log, branch=branch) - self.changeset[id] = c - self.files[id] = files - else: - colon = l.rfind(':') - file = l[1:colon] - rev = l[colon+1:-2] - oldrev, rev = rev.split("->") - files[file] = rev + # add current commit to set + c = commit(author=cs.author, date=date, + parents=[str(p.id) for p in cs.parents], + desc=cs.comment, branch=cs.branch or '') + self.changeset[id] = c + self.files[id] = files + else: + # external cvsps + for l in util.popen(cmd): + if state == 0: # header + if l.startswith("PatchSet"): + id = l[9:-2] + if maxrev and int(id) > maxrev: + # ignore everything + state = 3 + elif l.startswith("Date"): + date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"]) + date = util.datestr(date) + elif l.startswith("Branch"): + branch = l[8:-1] + self.parent[id] = self.lastbranch.get(branch, 'bad') + self.lastbranch[branch] = id + elif l.startswith("Ancestor branch"): + ancestor = l[17:-1] + # figure out the parent later + self.parent[id] = self.lastbranch[ancestor] + elif l.startswith("Author"): + author = self.recode(l[8:-1]) + elif l.startswith("Tag:") or l.startswith("Tags:"): + t = l[l.index(':')+1:] + t = [ut.strip() for ut in t.split(',')] + if (len(t) > 1) or (t[0] and (t[0] != "(none)")): + self.tags.update(dict.fromkeys(t, id)) + elif l.startswith("Log:"): + # switch to gathering log + state = 1 + log = "" + elif state == 1: # log + if l == "Members: \n": + # switch to gathering members + files = {} + oldrevs = [] + log = self.recode(log[:-1]) + state = 2 + else: + # gather log + log += l + elif state == 2: # members + if l == "\n": # start of next entry + state = 0 + p = [self.parent[id]] + if id == "1": + p = [] + if branch == "HEAD": + branch = "" + if branch: + latest = None + # the last changeset that contains a base + # file is our parent + for r in oldrevs: + latest = max(filerevids.get(r, None), latest) + if latest: + p = [latest] - # save some information for identifying branch points - oldrevs.append("%s:%s" % (oldrev, file)) - filerevids["%s:%s" % (rev, file)] = id - elif state == 3: - # swallow all input - continue + # add current commit to set + c = commit(author=author, date=date, parents=p, + desc=log, branch=branch) + self.changeset[id] = c + self.files[id] = files + else: + colon = l.rfind(':') + file = l[1:colon] + rev = l[colon+1:-2] + oldrev, rev = rev.split("->") + files[file] = rev + + # save some information for identifying branch points + oldrevs.append("%s:%s" % (oldrev, file)) + filerevids["%s:%s" % (rev, file)] = id + elif state == 3: + # swallow all input + continue self.heads = self.lastbranch.values() finally: @@ -141,7 +181,7 @@ user, host = None, None cmd = ['cvs', 'server'] - self.ui.status("connecting to %s\n" % root) + self.ui.status(_("connecting to %s\n") % root) if root.startswith(":pserver:"): root = root[9:] @@ -181,7 +221,7 @@ sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw, "END AUTH REQUEST", ""])) if sck.recv(128) != "I LOVE YOU\n": - raise util.Abort("CVS pserver authentication failed") + raise util.Abort(_("CVS pserver authentication failed")) self.writep = self.readp = sck.makefile('r+') @@ -212,7 +252,7 @@ # popen2 does not support argument lists under Windows cmd = [util.shellquote(arg) for arg in cmd] cmd = util.quotecommand(' '.join(cmd)) - self.writep, self.readp = os.popen2(cmd, 'b') + self.writep, self.readp = util.popen2(cmd, 'b') self.realroot = root @@ -224,7 +264,7 @@ self.writep.flush() r = self.readp.readline() if not r.startswith("Valid-requests"): - raise util.Abort("server sucks") + raise util.Abort(_("server sucks")) if "UseUnchanged" in r: self.writep.write("UseUnchanged\n") self.writep.flush() @@ -243,7 +283,7 @@ while count > 0: data = fp.read(min(count, chunksize)) if not data: - raise util.Abort("%d bytes missing from remote file" % count) + raise util.Abort(_("%d bytes missing from remote file") % count) count -= len(data) output.write(data) return output.getvalue() @@ -278,14 +318,14 @@ if line == "ok\n": return (data, "x" in mode and "x" or "") elif line.startswith("E "): - self.ui.warn("cvs server: %s\n" % line[2:]) + self.ui.warn(_("cvs server: %s\n") % line[2:]) elif line.startswith("Remove"): l = self.readp.readline() l = self.readp.readline() if l != "ok\n": - raise util.Abort("unknown CVS response: %s" % l) + raise util.Abort(_("unknown CVS response: %s") % l) else: - raise util.Abort("unknown CVS response: %s" % line) + raise util.Abort(_("unknown CVS response: %s") % line) def getfile(self, file, rev): data, mode = self._getfile(file, rev) @@ -297,10 +337,7 @@ def getchanges(self, rev): self.modecache = {} - files = self.files[rev] - cl = files.items() - cl.sort() - return (cl, {}) + return util.sort(self.files[rev].items()), {} def getcommit(self, rev): return self.changeset[rev] @@ -309,7 +346,4 @@ return self.tags def getchangedfiles(self, rev, i): - files = self.files[rev].keys() - files.sort() - return files - + return util.sort(self.files[rev].keys()) diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/convert/cvsps --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/convert/cvsps Thu Oct 23 15:44:23 2008 +0200 @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# +# Commandline front-end for cvsps.py +# +# Copyright 2008, Frank Kingswood +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +import sys +from mercurial import util +from mercurial.i18n import _ +from optparse import OptionParser, SUPPRESS_HELP +from hgext.convert.cvsps import createlog, createchangeset, logerror + +def main(): + '''Main program to mimic cvsps.''' + + op = OptionParser(usage='%prog [-bpruvxz] path', + description='Read CVS rlog for current directory or named ' + 'path in repository, and convert the log to changesets ' + 'based on matching commit log entries and dates.') + + # Options that are ignored for compatibility with cvsps-2.1 + op.add_option('-A', dest='Ignore', action='store_true', help=SUPPRESS_HELP) + op.add_option('--cvs-direct', dest='Ignore', action='store_true', help=SUPPRESS_HELP) + op.add_option('-q', dest='Ignore', action='store_true', help=SUPPRESS_HELP) + + # Main options shared with cvsps-2.1 + op.add_option('-b', dest='Branches', action='append', default=[], + help='Only return changes on specified branches') + op.add_option('-p', dest='Prefix', action='store', default='', + help='Prefix to remove from file names') + op.add_option('-r', dest='Revisions', action='append', default=[], + help='Only return changes after or between specified tags') + op.add_option('-u', dest='Cache', action='store_const', const='update', + help="Update cvs log cache") + op.add_option('-v', dest='Verbose', action='count', default=0, + help='Be verbose') + op.add_option('-x', dest='Cache', action='store_const', const='write', + help="Create new cvs log cache") + op.add_option('-z', dest='Fuzz', action='store', type='int', default=60, + help='Set commit time fuzz', metavar='seconds') + op.add_option('--root', dest='Root', action='store', default='', + help='Specify cvsroot', metavar='cvsroot') + + # Options specific to this version + op.add_option('--parents', dest='Parents', action='store_true', + help='Show parent changesets') + op.add_option('--ancestors', dest='Ancestors', action='store_true', + help='Show current changeset in ancestor branches') + + options, args = op.parse_args() + + # Create a ui object for printing progress messages + class UI: + def __init__(self, verbose): + if verbose: + self.status = self.message + if verbose>1: + self.note = self.message + if verbose>2: + self.debug = self.message + def message(self, msg): + sys.stderr.write(msg) + def nomessage(self, msg): + pass + status = nomessage + note = nomessage + debug = nomessage + ui = UI(options.Verbose) + + try: + if args: + log = [] + for d in args: + log += createlog(ui, d, root=options.Root, cache=options.Cache) + else: + log = createlog(ui, root=options.Root, cache=options.Cache) + except logerror, e: + print e + return + + changesets = createchangeset(ui, log, options.Fuzz) + del log + + # Print changesets (optionally filtered) + + off = len(options.Revisions) + branches = {} # latest version number in each branch + ancestors = {} # parent branch + for cs in changesets: + + if options.Ancestors: + if cs.branch not in branches and cs.parents and cs.parents[0].id: + ancestors[cs.branch] = changesets[cs.parents[0].id-1].branch, cs.parents[0].id + branches[cs.branch] = cs.id + + # limit by branches + if options.Branches and (cs.branch or 'HEAD') not in options.Branches: + continue + + if not off: + # Note: trailing spaces on several lines here are needed to have + # bug-for-bug compatibility with cvsps. + print '---------------------' + print 'PatchSet %d ' % cs.id + print 'Date: %s' % util.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2') + print 'Author: %s' % cs.author + print 'Branch: %s' % (cs.branch or 'HEAD') + print 'Tag%s: %s ' % (['', 's'][len(cs.tags)>1], + ','.join(cs.tags) or '(none)') + if options.Parents and cs.parents: + if len(cs.parents)>1: + print 'Parents: %s' % (','.join([str(p.id) for p in cs.parents])) + else: + print 'Parent: %d' % cs.parents[0].id + + if options.Ancestors: + b = cs.branch + r = [] + while b: + b, c = ancestors[b] + r.append('%s:%d:%d' % (b or "HEAD", c, branches[b])) + if r: + print 'Ancestors: %s' % (','.join(r)) + + print 'Log:' + print cs.comment + print + print 'Members: ' + for f in cs.entries: + fn = f.file + if fn.startswith(options.Prefix): + fn = fn[len(options.Prefix):] + print '\t%s:%s->%s%s ' % (fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL', + '.'.join([str(x) for x in f.revision]), ['', '(DEAD)'][f.dead]) + print + + # have we seen the start tag? + if options.Revisions and off: + if options.Revisions[0] == str(cs.id) or \ + options.Revisions[0] in cs.tags: + off = False + + # see if we reached the end tag + if len(options.Revisions)>1 and not off: + if options.Revisions[1] == str(cs.id) or \ + options.Revisions[1] in cs.tags: + break + + +if __name__ == '__main__': + main() diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/convert/cvsps.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/convert/cvsps.py Thu Oct 23 15:44:23 2008 +0200 @@ -0,0 +1,587 @@ +# +# Mercurial built-in replacement for cvsps. +# +# Copyright 2008, Frank Kingswood +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +import os +import re +import sys +import cPickle as pickle +from mercurial import util +from mercurial.i18n import _ + +def listsort(list, key): + "helper to sort by key in Python 2.3" + try: + list.sort(key=key) + except TypeError: + list.sort(lambda l, r: cmp(key(l), key(r))) + +class logentry(object): + '''Class logentry has the following attributes: + .author - author name as CVS knows it + .branch - name of branch this revision is on + .branches - revision tuple of branches starting at this revision + .comment - commit message + .date - the commit date as a (time, tz) tuple + .dead - true if file revision is dead + .file - Name of file + .lines - a tuple (+lines, -lines) or None + .parent - Previous revision of this entry + .rcs - name of file as returned from CVS + .revision - revision number as tuple + .tags - list of tags on the file + ''' + def __init__(self, **entries): + self.__dict__.update(entries) + +class logerror(Exception): + pass + +def getrepopath(cvspath): + """Return the repository path from a CVS path. + + >>> getrepopath('/foo/bar') + '/foo/bar' + >>> getrepopath('c:/foo/bar') + 'c:/foo/bar' + >>> getrepopath(':pserver:10/foo/bar') + '/foo/bar' + >>> getrepopath(':pserver:10c:/foo/bar') + '/foo/bar' + >>> getrepopath(':pserver:/foo/bar') + '/foo/bar' + >>> getrepopath(':pserver:c:/foo/bar') + 'c:/foo/bar' + >>> getrepopath(':pserver:truc@foo.bar:/foo/bar') + '/foo/bar' + >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar') + 'c:/foo/bar' + """ + # According to CVS manual, CVS paths are expressed like: + # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository + # + # Unfortunately, Windows absolute paths start with a drive letter + # like 'c:' making it harder to parse. Here we assume that drive + # letters are only one character long and any CVS component before + # the repository path is at least 2 characters long, and use this + # to disambiguate. + parts = cvspath.split(':') + if len(parts) == 1: + return parts[0] + # Here there is an ambiguous case if we have a port number + # immediately followed by a Windows driver letter. We assume this + # never happens and decide it must be CVS path component, + # therefore ignoring it. + if len(parts[-2]) > 1: + return parts[-1].lstrip('0123456789') + return parts[-2] + ':' + parts[-1] + +def createlog(ui, directory=None, root="", rlog=True, cache=None): + '''Collect the CVS rlog''' + + # Because we store many duplicate commit log messages, reusing strings + # saves a lot of memory and pickle storage space. + _scache = {} + def scache(s): + "return a shared version of a string" + return _scache.setdefault(s, s) + + ui.status(_('collecting CVS rlog\n')) + + log = [] # list of logentry objects containing the CVS state + + # patterns to match in CVS (r)log output, by state of use + re_00 = re.compile('RCS file: (.+)$') + re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$') + re_02 = re.compile('cvs (r?log|server): (.+)\n$') + re_03 = re.compile("(Cannot access.+CVSROOT)|(can't create temporary directory.+)$") + re_10 = re.compile('Working file: (.+)$') + re_20 = re.compile('symbolic names:') + re_30 = re.compile('\t(.+): ([\\d.]+)$') + re_31 = re.compile('----------------------------$') + re_32 = re.compile('=============================================================================$') + re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$') + re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?') + re_70 = re.compile('branches: (.+);$') + + prefix = '' # leading path to strip of what we get from CVS + + if directory is None: + # Current working directory + + # Get the real directory in the repository + try: + prefix = file(os.path.join('CVS','Repository')).read().strip() + if prefix == ".": + prefix = "" + directory = prefix + except IOError: + raise logerror('Not a CVS sandbox') + + if prefix and not prefix.endswith(os.sep): + prefix += os.sep + + # Use the Root file in the sandbox, if it exists + try: + root = file(os.path.join('CVS','Root')).read().strip() + except IOError: + pass + + if not root: + root = os.environ.get('CVSROOT', '') + + # read log cache if one exists + oldlog = [] + date = None + + if cache: + cachedir = os.path.expanduser('~/.hg.cvsps') + if not os.path.exists(cachedir): + os.mkdir(cachedir) + + # The cvsps cache pickle needs a uniquified name, based on the + # repository location. The address may have all sort of nasties + # in it, slashes, colons and such. So here we take just the + # alphanumerics, concatenated in a way that does not mix up the + # various components, so that + # :pserver:user@server:/path + # and + # /pserver/user/server/path + # are mapped to different cache file names. + cachefile = root.split(":") + [directory, "cache"] + cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s] + cachefile = os.path.join(cachedir, + '.'.join([s for s in cachefile if s])) + + if cache == 'update': + try: + ui.note(_('reading cvs log cache %s\n') % cachefile) + oldlog = pickle.load(file(cachefile)) + ui.note(_('cache has %d log entries\n') % len(oldlog)) + except Exception, e: + ui.note(_('error reading cache: %r\n') % e) + + if oldlog: + date = oldlog[-1].date # last commit date as a (time,tz) tuple + date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2') + + # build the CVS commandline + cmd = ['cvs', '-q'] + if root: + cmd.append('-d%s' % root) + p = util.normpath(getrepopath(root)) + if not p.endswith('/'): + p += '/' + prefix = p + util.normpath(prefix) + cmd.append(['log', 'rlog'][rlog]) + if date: + # no space between option and date string + cmd.append('-d>%s' % date) + cmd.append(directory) + + # state machine begins here + tags = {} # dictionary of revisions on current file with their tags + state = 0 + store = False # set when a new record can be appended + + cmd = [util.shellquote(arg) for arg in cmd] + ui.note(_("running %s\n") % (' '.join(cmd))) + ui.debug(_("prefix=%r directory=%r root=%r\n") % (prefix, directory, root)) + + for line in util.popen(' '.join(cmd)): + if line.endswith('\n'): + line = line[:-1] + #ui.debug('state=%d line=%r\n' % (state, line)) + + if state == 0: + # initial state, consume input until we see 'RCS file' + match = re_00.match(line) + if match: + rcs = match.group(1) + tags = {} + if rlog: + filename = util.normpath(rcs[:-2]) + if filename.startswith(prefix): + filename = filename[len(prefix):] + if filename.startswith('/'): + filename = filename[1:] + if filename.startswith('Attic/'): + filename = filename[6:] + else: + filename = filename.replace('/Attic/', '/') + state = 2 + continue + state = 1 + continue + match = re_01.match(line) + if match: + raise Exception(match.group(1)) + match = re_02.match(line) + if match: + raise Exception(match.group(2)) + if re_03.match(line): + raise Exception(line) + + elif state == 1: + # expect 'Working file' (only when using log instead of rlog) + match = re_10.match(line) + assert match, _('RCS file must be followed by working file') + filename = util.normpath(match.group(1)) + state = 2 + + elif state == 2: + # expect 'symbolic names' + if re_20.match(line): + state = 3 + + elif state == 3: + # read the symbolic names and store as tags + match = re_30.match(line) + if match: + rev = [int(x) for x in match.group(2).split('.')] + + # Convert magic branch number to an odd-numbered one + revn = len(rev) + if revn > 3 and (revn % 2) == 0 and rev[-2] == 0: + rev = rev[:-2] + rev[-1:] + rev = tuple(rev) + + if rev not in tags: + tags[rev] = [] + tags[rev].append(match.group(1)) + + elif re_31.match(line): + state = 5 + elif re_32.match(line): + state = 0 + + elif state == 4: + # expecting '------' separator before first revision + if re_31.match(line): + state = 5 + else: + assert not re_32.match(line), _('Must have at least some revisions') + + elif state == 5: + # expecting revision number and possibly (ignored) lock indication + # we create the logentry here from values stored in states 0 to 4, + # as this state is re-entered for subsequent revisions of a file. + match = re_50.match(line) + assert match, _('expected revision number') + e = logentry(rcs=scache(rcs), file=scache(filename), + revision=tuple([int(x) for x in match.group(1).split('.')]), + branches=[], parent=None) + state = 6 + + elif state == 6: + # expecting date, author, state, lines changed + match = re_60.match(line) + assert match, _('revision must be followed by date line') + d = match.group(1) + if d[2] == '/': + # Y2K + d = '19' + d + + if len(d.split()) != 3: + # cvs log dates always in GMT + d = d + ' UTC' + e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S']) + e.author = scache(match.group(2)) + e.dead = match.group(3).lower() == 'dead' + + if match.group(5): + if match.group(6): + e.lines = (int(match.group(5)), int(match.group(6))) + else: + e.lines = (int(match.group(5)), 0) + elif match.group(6): + e.lines = (0, int(match.group(6))) + else: + e.lines = None + e.comment = [] + state = 7 + + elif state == 7: + # read the revision numbers of branches that start at this revision + # or store the commit log message otherwise + m = re_70.match(line) + if m: + e.branches = [tuple([int(y) for y in x.strip().split('.')]) + for x in m.group(1).split(';')] + state = 8 + elif re_31.match(line): + state = 5 + store = True + elif re_32.match(line): + state = 0 + store = True + else: + e.comment.append(line) + + elif state == 8: + # store commit log message + if re_31.match(line): + state = 5 + store = True + elif re_32.match(line): + state = 0 + store = True + else: + e.comment.append(line) + + if store: + # clean up the results and save in the log. + store = False + e.tags = util.sort([scache(x) for x in tags.get(e.revision, [])]) + e.comment = scache('\n'.join(e.comment)) + + revn = len(e.revision) + if revn > 3 and (revn % 2) == 0: + e.branch = tags.get(e.revision[:-1], [None])[0] + else: + e.branch = None + + log.append(e) + + if len(log) % 100 == 0: + ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n') + + listsort(log, key=lambda x:(x.rcs, x.revision)) + + # find parent revisions of individual files + versions = {} + for e in log: + branch = e.revision[:-1] + p = versions.get((e.rcs, branch), None) + if p is None: + p = e.revision[:-2] + e.parent = p + versions[(e.rcs, branch)] = e.revision + + # update the log cache + if cache: + if log: + # join up the old and new logs + listsort(log, key=lambda x:x.date) + + if oldlog and oldlog[-1].date >= log[0].date: + raise logerror('Log cache overlaps with new log entries,' + ' re-run without cache.') + + log = oldlog + log + + # write the new cachefile + ui.note(_('writing cvs log cache %s\n') % cachefile) + pickle.dump(log, file(cachefile, 'w')) + else: + log = oldlog + + ui.status(_('%d log entries\n') % len(log)) + + return log + + +class changeset(object): + '''Class changeset has the following attributes: + .author - author name as CVS knows it + .branch - name of branch this changeset is on, or None + .comment - commit message + .date - the commit date as a (time,tz) tuple + .entries - list of logentry objects in this changeset + .parents - list of one or two parent changesets + .tags - list of tags on this changeset + ''' + def __init__(self, **entries): + self.__dict__.update(entries) + +def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None): + '''Convert log into changesets.''' + + ui.status(_('creating changesets\n')) + + # Merge changesets + + listsort(log, key=lambda x:(x.comment, x.author, x.branch, x.date)) + + changesets = [] + files = {} + c = None + for i, e in enumerate(log): + + # Check if log entry belongs to the current changeset or not. + if not (c and + e.comment == c.comment and + e.author == c.author and + e.branch == c.branch and + ((c.date[0] + c.date[1]) <= + (e.date[0] + e.date[1]) <= + (c.date[0] + c.date[1]) + fuzz) and + e.file not in files): + c = changeset(comment=e.comment, author=e.author, + branch=e.branch, date=e.date, entries=[]) + changesets.append(c) + files = {} + if len(changesets) % 100 == 0: + t = '%d %s' % (len(changesets), repr(e.comment)[1:-1]) + ui.status(util.ellipsis(t, 80) + '\n') + + c.entries.append(e) + files[e.file] = True + c.date = e.date # changeset date is date of latest commit in it + + # Sort files in each changeset + + for c in changesets: + def pathcompare(l, r): + 'Mimic cvsps sorting order' + l = l.split('/') + r = r.split('/') + nl = len(l) + nr = len(r) + n = min(nl, nr) + for i in range(n): + if i + 1 == nl and nl < nr: + return -1 + elif i + 1 == nr and nl > nr: + return +1 + elif l[i] < r[i]: + return -1 + elif l[i] > r[i]: + return +1 + return 0 + def entitycompare(l, r): + return pathcompare(l.file, r.file) + + c.entries.sort(entitycompare) + + # Sort changesets by date + + def cscmp(l, r): + d = sum(l.date) - sum(r.date) + if d: + return d + + # detect vendor branches and initial commits on a branch + le = {} + for e in l.entries: + le[e.rcs] = e.revision + re = {} + for e in r.entries: + re[e.rcs] = e.revision + + d = 0 + for e in l.entries: + if re.get(e.rcs, None) == e.parent: + assert not d + d = 1 + break + + for e in r.entries: + if le.get(e.rcs, None) == e.parent: + assert not d + d = -1 + break + + return d + + changesets.sort(cscmp) + + # Collect tags + + globaltags = {} + for c in changesets: + tags = {} + for e in c.entries: + for tag in e.tags: + # remember which is the latest changeset to have this tag + globaltags[tag] = c + + for c in changesets: + tags = {} + for e in c.entries: + for tag in e.tags: + tags[tag] = True + # remember tags only if this is the latest changeset to have it + c.tags = util.sort([tag for tag in tags if globaltags[tag] is c]) + + # Find parent changesets, handle {{mergetobranch BRANCHNAME}} + # by inserting dummy changesets with two parents, and handle + # {{mergefrombranch BRANCHNAME}} by setting two parents. + + if mergeto is None: + mergeto = r'{{mergetobranch ([-\w]+)}}' + if mergeto: + mergeto = re.compile(mergeto) + + if mergefrom is None: + mergefrom = r'{{mergefrombranch ([-\w]+)}}' + if mergefrom: + mergefrom = re.compile(mergefrom) + + versions = {} # changeset index where we saw any particular file version + branches = {} # changeset index where we saw a branch + n = len(changesets) + i = 0 + while i= self.startrev: parents = [previd] - self.ui.note('found parent of branch %s at %d: %s\n' % + 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.ui.debug(_("No copyfrom path, don't know what to do.\n")) paths = [] # filter out unrelated paths @@ -797,7 +795,7 @@ self.child_cset = cset return cset, branched - self.ui.note('fetching revision log for "%s" from %d to %d\n' % + self.ui.note(_('fetching revision log for "%s" from %d to %d\n') % (self.module, from_revnum, to_revnum)) try: @@ -811,11 +809,11 @@ lastonbranch = True break if self.is_blacklisted(revnum): - self.ui.note('skipping blacklisted revision %d\n' + self.ui.note(_('skipping blacklisted revision %d\n') % revnum) continue if paths is None: - self.ui.debug('revision %d has no entries\n' % revnum) + self.ui.debug(_('revision %d has no entries\n') % revnum) continue cset, lastonbranch = parselogentry(paths, revnum, author, date, message) @@ -840,7 +838,7 @@ pass except SubversionException, (inst, num): if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION: - raise util.Abort('svn: branch has no revision %s' % to_revnum) + raise util.Abort(_('svn: branch has no revision %s') % to_revnum) raise def _getfile(self, file, rev): @@ -894,7 +892,7 @@ return relative # The path is outside our tracked tree... - self.ui.debug('%r is not under %r, ignoring\n' % (path, module)) + self.ui.debug(_('%r is not under %r, ignoring\n') % (path, module)) return None def _checkpath(self, path, revnum): @@ -916,7 +914,7 @@ arg = encodeargs(args) hgexe = util.hgexecutable() cmd = '%s debugsvnlog' % util.shellquote(hgexe) - stdin, stdout = os.popen2(cmd, 'b') + stdin, stdout = util.popen2(cmd, 'b') stdin.write(arg) stdin.close() return logstream(stdout) @@ -1036,12 +1034,6 @@ 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 @@ -1074,10 +1066,9 @@ return dirs def add_dirs(self, files): - add_dirs = [d for d in self.dirs_of(files) + add_dirs = [d for d in util.sort(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 @@ -1087,8 +1078,7 @@ return files def tidy_dirs(self, names): - dirs = list(self.dirs_of(names)) - dirs.sort() + dirs = util.sort(self.dirs_of(names)) dirs.reverse() deleted = [] for d in dirs: @@ -1104,7 +1094,20 @@ def revid(self, rev): return u"svn:%s@%s" % (self.uuid, rev) - def putcommit(self, files, parents, commit): + def putcommit(self, files, copies, parents, commit, source): + # Apply changes to working copy + for f, v in files: + try: + data = source.getfile(f, v) + except IOError, inst: + self.delete.append(f) + else: + e = source.getmode(f, v) + self.putfile(f, e, data) + if f in copies: + self.copies.append([copies[f], f]) + files = [f[0] for f in files] + for parent in parents: try: return self.revid(self.childmap[parent]) diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/extdiff.py --- a/hgext/extdiff.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/extdiff.py Thu Oct 23 15:44:23 2008 +0200 @@ -52,7 +52,6 @@ def snapshot_node(ui, repo, files, node, tmproot): '''snapshot files as of some revision''' - mf = repo.changectx(node).manifest() dirname = os.path.basename(repo.root) if dirname == "": dirname = "root" @@ -61,17 +60,18 @@ os.mkdir(base) ui.note(_('making snapshot of %d files from rev %s\n') % (len(files), short(node))) + ctx = repo[node] for fn in files: - if not fn in mf: + wfn = util.pconvert(fn) + if not wfn in ctx: # skipping new file after a merge ? continue - wfn = util.pconvert(fn) ui.note(' %s\n' % wfn) dest = os.path.join(base, wfn) destdir = os.path.dirname(dest) if not os.path.isdir(destdir): os.makedirs(destdir) - data = repo.wwritedata(wfn, repo.file(wfn).read(mf[wfn])) + data = repo.wwritedata(wfn, ctx[wfn].data()) open(dest, 'wb').write(data) return dirname @@ -121,9 +121,8 @@ - just invoke the diff for a single file in the working dir ''' node1, node2 = cmdutil.revpair(repo, opts['rev']) - files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts) - modified, added, removed, deleted, unknown = repo.status( - node1, node2, files, match=matchfn)[:5] + matcher = cmdutil.match(repo, pats, opts) + modified, added, removed = repo.status(node1, node2, matcher)[:3] if not (modified or added or removed): return 0 @@ -165,13 +164,13 @@ cmdline = ('%s %s %s %s' % (util.shellquote(diffcmd), ' '.join(diffopts), util.shellquote(dir1), util.shellquote(dir2))) - ui.debug('running %r in %s\n' % (cmdline, tmproot)) + ui.debug(_('running %r in %s\n') % (cmdline, tmproot)) util.system(cmdline, cwd=tmproot) for copy_fn, working_fn, mtime in fns_and_mtime: if os.path.getmtime(copy_fn) != mtime: - ui.debug('File changed while diffing. ' - 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn)) + ui.debug(_('File changed while diffing. ' + 'Overwriting: %s (src: %s)\n') % (working_fn, copy_fn)) util.copyfile(copy_fn, working_fn) return 1 diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/fetch.py --- a/hgext/fetch.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/fetch.py Thu Oct 23 15:44:23 2008 +0200 @@ -4,6 +4,7 @@ # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. +'''pulling, updating and merging in one command''' from mercurial.i18n import _ from mercurial.node import nullid, short @@ -15,7 +16,7 @@ This finds all changes from the repository at the specified path or URL and adds them to the local repository. - If the pulled changes add a new head, the head is automatically + If the pulled changes add a new branch head, the head is automatically merged, and the result of the merge is committed. Otherwise, the working directory is updated to include the new changes. @@ -27,23 +28,75 @@ See 'hg help dates' for a list of formats valid for -d/--date. ''' - def postincoming(other, modheads): + date = opts.get('date') + if date: + opts['date'] = util.parsedate(date) + + parent, p2 = repo.dirstate.parents() + branch = repo.dirstate.branch() + branchnode = repo.branchtags().get(branch) + if parent != branchnode: + raise util.Abort(_('working dir not at branch tip ' + '(use "hg update" to check out branch tip)')) + + if p2 != nullid: + raise util.Abort(_('outstanding uncommitted merge')) + + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + mod, add, rem, del_ = repo.status()[:4] + + if mod or add or rem: + raise util.Abort(_('outstanding uncommitted changes')) + if del_: + raise util.Abort(_('working directory is missing some files')) + if len(repo.branchheads(branch)) > 1: + raise util.Abort(_('multiple heads in this branch ' + '(use "hg heads ." and "hg merge" to merge)')) + + cmdutil.setremoteconfig(ui, opts) + + other = hg.repository(ui, ui.expandpath(source)) + ui.status(_('pulling from %s\n') % + util.hidepassword(ui.expandpath(source))) + revs = None + if opts['rev']: + if not other.local(): + raise util.Abort(_("fetch -r doesn't work for remote " + "repositories yet")) + else: + revs = [other.lookup(rev) for rev in opts['rev']] + + # Are there any changes at all? + modheads = repo.pull(other, heads=revs) if modheads == 0: return 0 - if modheads == 1: - return hg.clean(repo, repo.changelog.tip()) - newheads = repo.heads(parent) - newchildren = [n for n in repo.heads(parent) if n != parent] + + # Is this a simple fast-forward along the current branch? + newheads = repo.branchheads(branch) + newchildren = repo.changelog.nodesbetween([parent], newheads)[2] + if len(newheads) == 1: + if newchildren[0] != parent: + return hg.clean(repo, newchildren[0]) + else: + return + + # Are there more than one additional branch heads? + newchildren = [n for n in newchildren if n != parent] newparent = parent if newchildren: newparent = newchildren[0] hg.clean(repo, newparent) - newheads = [n for n in repo.heads() if n != newparent] + newheads = [n for n in newheads if n != newparent] if len(newheads) > 1: - ui.status(_('not merging with %d other new heads ' - '(use "hg heads" and "hg merge" to merge them)') % + ui.status(_('not merging with %d other new branch heads ' + '(use "hg heads ." and "hg merge" to merge them)\n') % (len(newheads) - 1)) return + + # Otherwise, let's merge. err = False if newheads: # By default, we consider the repository we're pulling @@ -60,6 +113,7 @@ ui.status(_('merging with %d:%s\n') % (repo.changelog.rev(secondparent), short(secondparent))) err = hg.merge(repo, secondparent, remind=False) + if not err: mod, add, rem = repo.status()[:3] message = (cmdutil.logmessage(opts) or @@ -73,45 +127,6 @@ 'with local\n') % (repo.changelog.rev(n), short(n))) - def pull(): - cmdutil.setremoteconfig(ui, opts) - - other = hg.repository(ui, ui.expandpath(source)) - ui.status(_('pulling from %s\n') % - util.hidepassword(ui.expandpath(source))) - revs = None - if opts['rev']: - if not other.local(): - raise util.Abort(_("fetch -r doesn't work for remote " - "repositories yet")) - else: - revs = [other.lookup(rev) for rev in opts['rev']] - modheads = repo.pull(other, heads=revs) - return postincoming(other, modheads) - - date = opts.get('date') - if date: - opts['date'] = util.parsedate(date) - - parent, p2 = repo.dirstate.parents() - if parent != repo.changelog.tip(): - raise util.Abort(_('working dir not at tip ' - '(use "hg update" to check out tip)')) - if p2 != nullid: - raise util.Abort(_('outstanding uncommitted merge')) - wlock = lock = None - try: - wlock = repo.wlock() - lock = repo.lock() - mod, add, rem, del_ = repo.status()[:4] - if mod or add or rem: - raise util.Abort(_('outstanding uncommitted changes')) - if del_: - raise util.Abort(_('working directory is missing some files')) - if len(repo.heads()) > 1: - raise util.Abort(_('multiple heads in this repository ' - '(use "hg heads" and "hg merge" to merge)')) - return pull() finally: del lock, wlock diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/gpg.py --- a/hgext/gpg.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/gpg.py Thu Oct 23 15:44:23 2008 +0200 @@ -239,7 +239,7 @@ repo.opener("localsigs", "ab").write(sigmessage) return - for x in repo.status()[:5]: + for x in repo.status(unknown=True)[:5]: if ".hgsigs" in x and not opts["force"]: raise util.Abort(_("working copy of .hgsigs is changed " "(please commit .hgsigs manually " diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/graphlog.py --- a/hgext/graphlog.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/graphlog.py Thu Oct 23 15:44:23 2008 +0200 @@ -4,6 +4,7 @@ # # This software may be used and distributed according to the terms of # the GNU General Public License, incorporated herein by reference. +'''show revision graphs in terminal windows''' import os import sys @@ -12,6 +13,7 @@ from mercurial.i18n import _ from mercurial.node import nullrev from mercurial.util import Abort, canonpath +from mercurial import util def revision_grapher(repo, start_rev, stop_rev): """incremental revision grapher @@ -52,8 +54,7 @@ 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 + next_revs[rev_index:rev_index + 1] = util.sort(parents_to_add) edges = [] for parent in parents: @@ -88,7 +89,7 @@ assert start_rev >= stop_rev curr_rev = start_rev revs = [] - filerev = repo.file(path).count() - 1 + filerev = len(repo.file(path)) - 1 while filerev >= 0: fctx = repo.filectx(path, fileid=filerev) @@ -104,8 +105,7 @@ 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 + next_revs[rev_index:rev_index + 1] = util.sort(parents_to_add) edges = [] for parent in parents: @@ -197,7 +197,7 @@ revs = revrange(repo, rev_opt) return (max(revs), min(revs)) else: - return (repo.changelog.count() - 1, 0) + return (len(repo) - 1, 0) def graphlog(ui, repo, path=None, **opts): """show revision history alongside an ASCII revision graph diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/hgk.py --- a/hgext/hgk.py Thu Oct 23 14:05:11 2008 +0200 +++ b/hgext/hgk.py Thu Oct 23 15:44:23 2008 +0200 @@ -4,60 +4,59 @@ # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -# -# The hgk extension allows browsing the history of a repository in a -# graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is -# not distributed with Mercurial.) -# -# hgk consists of two parts: a Tcl script that does the displaying and -# 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. -# -# 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 -# can specify an absolute path: -# -# [extensions] -# hgk=/usr/local/lib/hgk.py -# -# Mercurial can also scan the default python library path for a file -# named 'hgk.py' if you set hgk empty: -# -# [extensions] -# hgk= -# -# The hg view command will launch the hgk Tcl script. For this command -# to work, hgk must be in your search path. Alternately, you can -# specify the path to hgk in your .hgrc file: -# -# [hgk] -# path=/location/of/hgk -# -# hgk can make use of the extdiff extension to visualize -# revisions. Assuming you had already configured extdiff vdiff -# command, just add: -# -# [hgk] -# vdiff=vdiff -# -# Revisions context menu will now display additional entries to fire -# vdiff on hovered and selected revisions. +'''browsing the repository in a graphical way + +The hgk extension allows browsing the history of a repository in a +graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is +not distributed with Mercurial.) + +hgk consists of two parts: a Tcl script that does the displaying and +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. + +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 +can specify an absolute path: + + [extensions] + hgk=/usr/local/lib/hgk.py + +Mercurial can also scan the default python library path for a file +named 'hgk.py' if you set hgk empty: + + [extensions] + hgk= + +The hg view command will launch the hgk Tcl script. For this command +to work, hgk must be in your search path. Alternately, you can +specify the path to hgk in your .hgrc file: + + [hgk] + path=/location/of/hgk + +hgk can make use of the extdiff extension to visualize revisions. +Assuming you had already configured extdiff vdiff command, just add: + + [hgk] + vdiff=vdiff + +Revisions context menu will now display additional entries to fire +vdiff on hovered and selected revisions.''' import os -from mercurial import commands, util, patch, revlog +from mercurial import commands, util, patch, revlog, cmdutil from mercurial.node import nullid, nullrev, short +from mercurial.i18n import _ def difftree(ui, repo, node1=None, node2=None, *files, **opts): """diff trees from two commits""" def __difftree(repo, node1, node2, files=[]): assert node2 is not None - mmap = repo.changectx(node1).manifest() - mmap2 = repo.changectx(node2).manifest() - status = repo.status(node1, node2, files=files)[:5] - modified, added, removed, deleted, unknown = status - + mmap = repo[node1].manifest() + mmap2 = repo[node2].manifest() + m = cmdutil.match(repo, files) + modified, added, removed = repo.status(node1, node2, m)[:3] empty = short(nullid) for f in modified: @@ -92,8 +91,8 @@ if opts['patch']: if opts['pretty']: catcommit(ui, repo, node2, "") - patch.diff(repo, node1, node2, - files=files, + m = cmdutil.match(repo, files) + patch.diff(repo, node1, node2, match=m, opts=patch.diffopts(ui, {'git': True})) else: __difftree(repo, node1, node2, files=files) @@ -103,11 +102,11 @@ def catcommit(ui, repo, n, prefix, ctx=None): nlprefix = '\n' + prefix; if ctx is None: - ctx = repo.changectx(n) - (p1, p2) = ctx.parents() + ctx = repo[n] ui.write("tree %s\n" % short(ctx.changeset()[0])) # use ctx.node() instead ?? - if p1: ui.write("parent %s\n" % short(p1.node())) - if p2: ui.write("parent %s\n" % short(p2.node())) + for p in ctx.parents(): + ui.write("parent %s\n" % p) + date = ctx.date() description = ctx.description().replace("\0", "") lines = description.splitlines() @@ -151,12 +150,12 @@ else: if not type or not r: - ui.warn("cat-file: type or revision not supplied\n") + ui.warn(_("cat-file: type or revision not supplied\n")) commands.help_(ui, 'cat-file') while r: if type != "commit": - ui.warn("aborting hg cat-file only understands commits\n") + ui.warn(_("aborting hg cat-file only understands commits\n")) return 1; n = repo.lookup(r) catcommit(ui, repo, n, prefix) @@ -175,7 +174,7 @@ # you can specify a commit to stop at by starting the sha1 with ^ def revtree(ui, args, repo, full="tree", maxnr=0, parents=False): def chlogwalk(): - count = repo.changelog.count() + count = len(repo) i = count l = [0] * 100 chunk = 100 @@ -191,7 +190,7 @@ l[chunk - x:] = [0] * (chunk - x) break if full != None: - l[x] = repo.changectx(i + x) + l[x] = repo[i + x] l[x].changeset() # force reading else: l[x] = 1 @@ -318,40 +317,40 @@ os.chdir(repo.root) optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v]) cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc)) - ui.debug("running %s\n" % cmd) + ui.debug(_("running %s\n") % cmd) util.system(cmd) cmdtable = { "^view": (view, - [('l', 'limit', '', 'limit number of changes displayed')], - 'hg view [-l LIMIT] [REVRANGE]'), + [('l', 'limit', '', _('limit number of changes displayed'))], + _('hg view [-l LIMIT] [REVRANGE]')), "debug-diff-tree": (difftree, - [('p', 'patch', None, 'generate patch'), - ('r', 'recursive', None, 'recursive'), - ('P', 'pretty', None, 'pretty'), - ('s', 'stdin', None, 'stdin'), - ('C', 'copy', None, 'detect copies'), - ('S', 'search', "", 'search')], - 'hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...'), + [('p', 'patch', None, _('generate patch')), + ('r', 'recursive', None, _('recursive')), + ('P', 'pretty', None, _('pretty')), + ('s', 'stdin', None, _('stdin')), + ('C', 'copy', None, _('detect copies')), + ('S', 'search', "", _('search'))], + _('hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...')), "debug-cat-file": (catfile, - [('s', 'stdin', None, 'stdin')], - 'hg debug-cat-file [OPTION]... TYPE FILE'), + [('s', 'stdin', None, _('stdin'))], + _('hg debug-cat-file [OPTION]... TYPE FILE')), "debug-config": - (config, [], 'hg debug-config'), + (config, [], _('hg debug-config')), "debug-merge-base": - (base, [], 'hg debug-merge-base node node'), + (base, [], _('hg debug-merge-base node node')), "debug-rev-parse": (revparse, - [('', 'default', '', 'ignored')], - 'hg debug-rev-parse REV'), + [('', 'default', '', _('ignored'))], + _('hg debug-rev-parse REV')), "debug-rev-list": (revlist, - [('H', 'header', None, 'header'), - ('t', 'topo-order', None, 'topo-order'), - ('p', 'parents', None, 'parents'), - ('n', 'max-count', 0, 'max-count')], - 'hg debug-rev-list [options] revs'), + [('H', 'header', None, _('header')), + ('t', 'topo-order', None, _('topo-order')), + ('p', 'parents', None, _('parents')), + ('n', 'max-count', 0, _('max-count'))], + _('hg debug-rev-list [options] revs')), } diff -r c1dc903dc7b6 -r 5d14b06b1cc1 hgext/highlight.py --- a/hgext/highlight.py Thu Oct 23 14:05:11 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -""" -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 =