##// END OF EJS Templates
progress: use a verb in present participle
Martin Geisler -
r10700:b2b71c30 default
parent child Browse files
Show More
@@ -1,186 +1,186 b''
1 # churn.py - create a graph of revisions count grouped by template
1 # churn.py - create a graph of revisions count grouped by template
2 #
2 #
3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''command to display statistics about repository history'''
9 '''command to display statistics about repository history'''
10
10
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial import patch, cmdutil, util, templater
12 from mercurial import patch, cmdutil, util, templater
13 import sys, os
13 import sys, os
14 import time, datetime
14 import time, datetime
15
15
16 def maketemplater(ui, repo, tmpl):
16 def maketemplater(ui, repo, tmpl):
17 tmpl = templater.parsestring(tmpl, quoted=False)
17 tmpl = templater.parsestring(tmpl, quoted=False)
18 try:
18 try:
19 t = cmdutil.changeset_templater(ui, repo, False, None, None, False)
19 t = cmdutil.changeset_templater(ui, repo, False, None, None, False)
20 except SyntaxError, inst:
20 except SyntaxError, inst:
21 raise util.Abort(inst.args[0])
21 raise util.Abort(inst.args[0])
22 t.use_template(tmpl)
22 t.use_template(tmpl)
23 return t
23 return t
24
24
25 def changedlines(ui, repo, ctx1, ctx2, fns):
25 def changedlines(ui, repo, ctx1, ctx2, fns):
26 added, removed = 0, 0
26 added, removed = 0, 0
27 fmatch = cmdutil.matchfiles(repo, fns)
27 fmatch = cmdutil.matchfiles(repo, fns)
28 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
28 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
29 for l in diff.split('\n'):
29 for l in diff.split('\n'):
30 if l.startswith("+") and not l.startswith("+++ "):
30 if l.startswith("+") and not l.startswith("+++ "):
31 added += 1
31 added += 1
32 elif l.startswith("-") and not l.startswith("--- "):
32 elif l.startswith("-") and not l.startswith("--- "):
33 removed += 1
33 removed += 1
34 return (added, removed)
34 return (added, removed)
35
35
36 def countrate(ui, repo, amap, *pats, **opts):
36 def countrate(ui, repo, amap, *pats, **opts):
37 """Calculate stats"""
37 """Calculate stats"""
38 if opts.get('dateformat'):
38 if opts.get('dateformat'):
39 def getkey(ctx):
39 def getkey(ctx):
40 t, tz = ctx.date()
40 t, tz = ctx.date()
41 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
41 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
42 return date.strftime(opts['dateformat'])
42 return date.strftime(opts['dateformat'])
43 else:
43 else:
44 tmpl = opts.get('template', '{author|email}')
44 tmpl = opts.get('template', '{author|email}')
45 tmpl = maketemplater(ui, repo, tmpl)
45 tmpl = maketemplater(ui, repo, tmpl)
46 def getkey(ctx):
46 def getkey(ctx):
47 ui.pushbuffer()
47 ui.pushbuffer()
48 tmpl.show(ctx)
48 tmpl.show(ctx)
49 return ui.popbuffer()
49 return ui.popbuffer()
50
50
51 state = {'count': 0}
51 state = {'count': 0}
52 rate = {}
52 rate = {}
53 df = False
53 df = False
54 if opts.get('date'):
54 if opts.get('date'):
55 df = util.matchdate(opts['date'])
55 df = util.matchdate(opts['date'])
56
56
57 m = cmdutil.match(repo, pats, opts)
57 m = cmdutil.match(repo, pats, opts)
58 def prep(ctx, fns):
58 def prep(ctx, fns):
59 rev = ctx.rev()
59 rev = ctx.rev()
60 if df and not df(ctx.date()[0]): # doesn't match date format
60 if df and not df(ctx.date()[0]): # doesn't match date format
61 return
61 return
62
62
63 key = getkey(ctx)
63 key = getkey(ctx)
64 key = amap.get(key, key) # alias remap
64 key = amap.get(key, key) # alias remap
65 if opts.get('changesets'):
65 if opts.get('changesets'):
66 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
66 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
67 else:
67 else:
68 parents = ctx.parents()
68 parents = ctx.parents()
69 if len(parents) > 1:
69 if len(parents) > 1:
70 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
70 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
71 return
71 return
72
72
73 ctx1 = parents[0]
73 ctx1 = parents[0]
74 lines = changedlines(ui, repo, ctx1, ctx, fns)
74 lines = changedlines(ui, repo, ctx1, ctx, fns)
75 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
75 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
76
76
77 state['count'] += 1
77 state['count'] += 1
78 ui.progress(_('churning changes'), state['count'], total=len(repo))
78 ui.progress(_('analyzing'), state['count'], total=len(repo))
79
79
80 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
80 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
81 continue
81 continue
82
82
83 ui.progress(_('churning changes'), None)
83 ui.progress(_('analyzing'), None)
84
84
85 return rate
85 return rate
86
86
87
87
88 def churn(ui, repo, *pats, **opts):
88 def churn(ui, repo, *pats, **opts):
89 '''histogram of changes to the repository
89 '''histogram of changes to the repository
90
90
91 This command will display a histogram representing the number
91 This command will display a histogram representing the number
92 of changed lines or revisions, grouped according to the given
92 of changed lines or revisions, grouped according to the given
93 template. The default template will group changes by author.
93 template. The default template will group changes by author.
94 The --dateformat option may be used to group the results by
94 The --dateformat option may be used to group the results by
95 date instead.
95 date instead.
96
96
97 Statistics are based on the number of changed lines, or
97 Statistics are based on the number of changed lines, or
98 alternatively the number of matching revisions if the
98 alternatively the number of matching revisions if the
99 --changesets option is specified.
99 --changesets option is specified.
100
100
101 Examples::
101 Examples::
102
102
103 # display count of changed lines for every committer
103 # display count of changed lines for every committer
104 hg churn -t '{author|email}'
104 hg churn -t '{author|email}'
105
105
106 # display daily activity graph
106 # display daily activity graph
107 hg churn -f '%H' -s -c
107 hg churn -f '%H' -s -c
108
108
109 # display activity of developers by month
109 # display activity of developers by month
110 hg churn -f '%Y-%m' -s -c
110 hg churn -f '%Y-%m' -s -c
111
111
112 # display count of lines changed in every year
112 # display count of lines changed in every year
113 hg churn -f '%Y' -s
113 hg churn -f '%Y' -s
114
114
115 It is possible to map alternate email addresses to a main address
115 It is possible to map alternate email addresses to a main address
116 by providing a file using the following format::
116 by providing a file using the following format::
117
117
118 <alias email> <actual email>
118 <alias email> <actual email>
119
119
120 Such a file may be specified with the --aliases option, otherwise
120 Such a file may be specified with the --aliases option, otherwise
121 a .hgchurn file will be looked for in the working directory root.
121 a .hgchurn file will be looked for in the working directory root.
122 '''
122 '''
123 def pad(s, l):
123 def pad(s, l):
124 return (s + " " * l)[:l]
124 return (s + " " * l)[:l]
125
125
126 amap = {}
126 amap = {}
127 aliases = opts.get('aliases')
127 aliases = opts.get('aliases')
128 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
128 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
129 aliases = repo.wjoin('.hgchurn')
129 aliases = repo.wjoin('.hgchurn')
130 if aliases:
130 if aliases:
131 for l in open(aliases, "r"):
131 for l in open(aliases, "r"):
132 l = l.strip()
132 l = l.strip()
133 alias, actual = l.split()
133 alias, actual = l.split()
134 amap[alias] = actual
134 amap[alias] = actual
135
135
136 rate = countrate(ui, repo, amap, *pats, **opts).items()
136 rate = countrate(ui, repo, amap, *pats, **opts).items()
137 if not rate:
137 if not rate:
138 return
138 return
139
139
140 sortkey = ((not opts.get('sort')) and (lambda x: -sum(x[1])) or None)
140 sortkey = ((not opts.get('sort')) and (lambda x: -sum(x[1])) or None)
141 rate.sort(key=sortkey)
141 rate.sort(key=sortkey)
142
142
143 # Be careful not to have a zero maxcount (issue833)
143 # Be careful not to have a zero maxcount (issue833)
144 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
144 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
145 maxname = max(len(k) for k, v in rate)
145 maxname = max(len(k) for k, v in rate)
146
146
147 ttywidth = util.termwidth()
147 ttywidth = util.termwidth()
148 ui.debug("assuming %i character terminal\n" % ttywidth)
148 ui.debug("assuming %i character terminal\n" % ttywidth)
149 width = ttywidth - maxname - 2 - 2 - 2
149 width = ttywidth - maxname - 2 - 2 - 2
150
150
151 if opts.get('diffstat'):
151 if opts.get('diffstat'):
152 width -= 15
152 width -= 15
153 def format(name, (added, removed)):
153 def format(name, (added, removed)):
154 return "%s %15s %s%s\n" % (pad(name, maxname),
154 return "%s %15s %s%s\n" % (pad(name, maxname),
155 '+%d/-%d' % (added, removed),
155 '+%d/-%d' % (added, removed),
156 '+' * charnum(added),
156 '+' * charnum(added),
157 '-' * charnum(removed))
157 '-' * charnum(removed))
158 else:
158 else:
159 width -= 6
159 width -= 6
160 def format(name, count):
160 def format(name, count):
161 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
161 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
162 '*' * charnum(sum(count)))
162 '*' * charnum(sum(count)))
163
163
164 def charnum(count):
164 def charnum(count):
165 return int(round(count * width / maxcount))
165 return int(round(count * width / maxcount))
166
166
167 for name, count in rate:
167 for name, count in rate:
168 ui.write(format(name, count))
168 ui.write(format(name, count))
169
169
170
170
171 cmdtable = {
171 cmdtable = {
172 "churn":
172 "churn":
173 (churn,
173 (churn,
174 [('r', 'rev', [], _('count rate for the specified revision or range')),
174 [('r', 'rev', [], _('count rate for the specified revision or range')),
175 ('d', 'date', '', _('count rate for revisions matching date spec')),
175 ('d', 'date', '', _('count rate for revisions matching date spec')),
176 ('t', 'template', '{author|email}',
176 ('t', 'template', '{author|email}',
177 _('template to group changesets')),
177 _('template to group changesets')),
178 ('f', 'dateformat', '',
178 ('f', 'dateformat', '',
179 _('strftime-compatible format for grouping by date')),
179 _('strftime-compatible format for grouping by date')),
180 ('c', 'changesets', False, _('count rate by number of changesets')),
180 ('c', 'changesets', False, _('count rate by number of changesets')),
181 ('s', 'sort', False, _('sort by key (default: sort by count)')),
181 ('s', 'sort', False, _('sort by key (default: sort by count)')),
182 ('', 'diffstat', False, _('display added/removed lines separately')),
182 ('', 'diffstat', False, _('display added/removed lines separately')),
183 ('', 'aliases', '', _('file with email aliases')),
183 ('', 'aliases', '', _('file with email aliases')),
184 ],
184 ],
185 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]")),
185 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]")),
186 }
186 }
@@ -1,1245 +1,1245 b''
1 # cmdutil.py - help for command processing in mercurial
1 # cmdutil.py - help for command processing in mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from node import hex, nullid, nullrev, short
8 from node import hex, nullid, nullrev, short
9 from i18n import _
9 from i18n import _
10 import os, sys, errno, re, glob, tempfile
10 import os, sys, errno, re, glob, tempfile
11 import mdiff, bdiff, util, templater, patch, error, encoding, templatekw
11 import mdiff, bdiff, util, templater, patch, error, encoding, templatekw
12 import match as _match
12 import match as _match
13
13
14 revrangesep = ':'
14 revrangesep = ':'
15
15
16 def parsealiases(cmd):
16 def parsealiases(cmd):
17 return cmd.lstrip("^").split("|")
17 return cmd.lstrip("^").split("|")
18
18
19 def findpossible(cmd, table, strict=False):
19 def findpossible(cmd, table, strict=False):
20 """
20 """
21 Return cmd -> (aliases, command table entry)
21 Return cmd -> (aliases, command table entry)
22 for each matching command.
22 for each matching command.
23 Return debug commands (or their aliases) only if no normal command matches.
23 Return debug commands (or their aliases) only if no normal command matches.
24 """
24 """
25 choice = {}
25 choice = {}
26 debugchoice = {}
26 debugchoice = {}
27 for e in table.keys():
27 for e in table.keys():
28 aliases = parsealiases(e)
28 aliases = parsealiases(e)
29 found = None
29 found = None
30 if cmd in aliases:
30 if cmd in aliases:
31 found = cmd
31 found = cmd
32 elif not strict:
32 elif not strict:
33 for a in aliases:
33 for a in aliases:
34 if a.startswith(cmd):
34 if a.startswith(cmd):
35 found = a
35 found = a
36 break
36 break
37 if found is not None:
37 if found is not None:
38 if aliases[0].startswith("debug") or found.startswith("debug"):
38 if aliases[0].startswith("debug") or found.startswith("debug"):
39 debugchoice[found] = (aliases, table[e])
39 debugchoice[found] = (aliases, table[e])
40 else:
40 else:
41 choice[found] = (aliases, table[e])
41 choice[found] = (aliases, table[e])
42
42
43 if not choice and debugchoice:
43 if not choice and debugchoice:
44 choice = debugchoice
44 choice = debugchoice
45
45
46 return choice
46 return choice
47
47
48 def findcmd(cmd, table, strict=True):
48 def findcmd(cmd, table, strict=True):
49 """Return (aliases, command table entry) for command string."""
49 """Return (aliases, command table entry) for command string."""
50 choice = findpossible(cmd, table, strict)
50 choice = findpossible(cmd, table, strict)
51
51
52 if cmd in choice:
52 if cmd in choice:
53 return choice[cmd]
53 return choice[cmd]
54
54
55 if len(choice) > 1:
55 if len(choice) > 1:
56 clist = choice.keys()
56 clist = choice.keys()
57 clist.sort()
57 clist.sort()
58 raise error.AmbiguousCommand(cmd, clist)
58 raise error.AmbiguousCommand(cmd, clist)
59
59
60 if choice:
60 if choice:
61 return choice.values()[0]
61 return choice.values()[0]
62
62
63 raise error.UnknownCommand(cmd)
63 raise error.UnknownCommand(cmd)
64
64
65 def findrepo(p):
65 def findrepo(p):
66 while not os.path.isdir(os.path.join(p, ".hg")):
66 while not os.path.isdir(os.path.join(p, ".hg")):
67 oldp, p = p, os.path.dirname(p)
67 oldp, p = p, os.path.dirname(p)
68 if p == oldp:
68 if p == oldp:
69 return None
69 return None
70
70
71 return p
71 return p
72
72
73 def bail_if_changed(repo):
73 def bail_if_changed(repo):
74 if repo.dirstate.parents()[1] != nullid:
74 if repo.dirstate.parents()[1] != nullid:
75 raise util.Abort(_('outstanding uncommitted merge'))
75 raise util.Abort(_('outstanding uncommitted merge'))
76 modified, added, removed, deleted = repo.status()[:4]
76 modified, added, removed, deleted = repo.status()[:4]
77 if modified or added or removed or deleted:
77 if modified or added or removed or deleted:
78 raise util.Abort(_("outstanding uncommitted changes"))
78 raise util.Abort(_("outstanding uncommitted changes"))
79
79
80 def logmessage(opts):
80 def logmessage(opts):
81 """ get the log message according to -m and -l option """
81 """ get the log message according to -m and -l option """
82 message = opts.get('message')
82 message = opts.get('message')
83 logfile = opts.get('logfile')
83 logfile = opts.get('logfile')
84
84
85 if message and logfile:
85 if message and logfile:
86 raise util.Abort(_('options --message and --logfile are mutually '
86 raise util.Abort(_('options --message and --logfile are mutually '
87 'exclusive'))
87 'exclusive'))
88 if not message and logfile:
88 if not message and logfile:
89 try:
89 try:
90 if logfile == '-':
90 if logfile == '-':
91 message = sys.stdin.read()
91 message = sys.stdin.read()
92 else:
92 else:
93 message = open(logfile).read()
93 message = open(logfile).read()
94 except IOError, inst:
94 except IOError, inst:
95 raise util.Abort(_("can't read commit message '%s': %s") %
95 raise util.Abort(_("can't read commit message '%s': %s") %
96 (logfile, inst.strerror))
96 (logfile, inst.strerror))
97 return message
97 return message
98
98
99 def loglimit(opts):
99 def loglimit(opts):
100 """get the log limit according to option -l/--limit"""
100 """get the log limit according to option -l/--limit"""
101 limit = opts.get('limit')
101 limit = opts.get('limit')
102 if limit:
102 if limit:
103 try:
103 try:
104 limit = int(limit)
104 limit = int(limit)
105 except ValueError:
105 except ValueError:
106 raise util.Abort(_('limit must be a positive integer'))
106 raise util.Abort(_('limit must be a positive integer'))
107 if limit <= 0:
107 if limit <= 0:
108 raise util.Abort(_('limit must be positive'))
108 raise util.Abort(_('limit must be positive'))
109 else:
109 else:
110 limit = None
110 limit = None
111 return limit
111 return limit
112
112
113 def remoteui(src, opts):
113 def remoteui(src, opts):
114 'build a remote ui from ui or repo and opts'
114 'build a remote ui from ui or repo and opts'
115 if hasattr(src, 'baseui'): # looks like a repository
115 if hasattr(src, 'baseui'): # looks like a repository
116 dst = src.baseui.copy() # drop repo-specific config
116 dst = src.baseui.copy() # drop repo-specific config
117 src = src.ui # copy target options from repo
117 src = src.ui # copy target options from repo
118 else: # assume it's a global ui object
118 else: # assume it's a global ui object
119 dst = src.copy() # keep all global options
119 dst = src.copy() # keep all global options
120
120
121 # copy ssh-specific options
121 # copy ssh-specific options
122 for o in 'ssh', 'remotecmd':
122 for o in 'ssh', 'remotecmd':
123 v = opts.get(o) or src.config('ui', o)
123 v = opts.get(o) or src.config('ui', o)
124 if v:
124 if v:
125 dst.setconfig("ui", o, v)
125 dst.setconfig("ui", o, v)
126
126
127 # copy bundle-specific options
127 # copy bundle-specific options
128 r = src.config('bundle', 'mainreporoot')
128 r = src.config('bundle', 'mainreporoot')
129 if r:
129 if r:
130 dst.setconfig('bundle', 'mainreporoot', r)
130 dst.setconfig('bundle', 'mainreporoot', r)
131
131
132 # copy auth section settings
132 # copy auth section settings
133 for key, val in src.configitems('auth'):
133 for key, val in src.configitems('auth'):
134 dst.setconfig('auth', key, val)
134 dst.setconfig('auth', key, val)
135
135
136 return dst
136 return dst
137
137
138 def revpair(repo, revs):
138 def revpair(repo, revs):
139 '''return pair of nodes, given list of revisions. second item can
139 '''return pair of nodes, given list of revisions. second item can
140 be None, meaning use working dir.'''
140 be None, meaning use working dir.'''
141
141
142 def revfix(repo, val, defval):
142 def revfix(repo, val, defval):
143 if not val and val != 0 and defval is not None:
143 if not val and val != 0 and defval is not None:
144 val = defval
144 val = defval
145 return repo.lookup(val)
145 return repo.lookup(val)
146
146
147 if not revs:
147 if not revs:
148 return repo.dirstate.parents()[0], None
148 return repo.dirstate.parents()[0], None
149 end = None
149 end = None
150 if len(revs) == 1:
150 if len(revs) == 1:
151 if revrangesep in revs[0]:
151 if revrangesep in revs[0]:
152 start, end = revs[0].split(revrangesep, 1)
152 start, end = revs[0].split(revrangesep, 1)
153 start = revfix(repo, start, 0)
153 start = revfix(repo, start, 0)
154 end = revfix(repo, end, len(repo) - 1)
154 end = revfix(repo, end, len(repo) - 1)
155 else:
155 else:
156 start = revfix(repo, revs[0], None)
156 start = revfix(repo, revs[0], None)
157 elif len(revs) == 2:
157 elif len(revs) == 2:
158 if revrangesep in revs[0] or revrangesep in revs[1]:
158 if revrangesep in revs[0] or revrangesep in revs[1]:
159 raise util.Abort(_('too many revisions specified'))
159 raise util.Abort(_('too many revisions specified'))
160 start = revfix(repo, revs[0], None)
160 start = revfix(repo, revs[0], None)
161 end = revfix(repo, revs[1], None)
161 end = revfix(repo, revs[1], None)
162 else:
162 else:
163 raise util.Abort(_('too many revisions specified'))
163 raise util.Abort(_('too many revisions specified'))
164 return start, end
164 return start, end
165
165
166 def revrange(repo, revs):
166 def revrange(repo, revs):
167 """Yield revision as strings from a list of revision specifications."""
167 """Yield revision as strings from a list of revision specifications."""
168
168
169 def revfix(repo, val, defval):
169 def revfix(repo, val, defval):
170 if not val and val != 0 and defval is not None:
170 if not val and val != 0 and defval is not None:
171 return defval
171 return defval
172 return repo.changelog.rev(repo.lookup(val))
172 return repo.changelog.rev(repo.lookup(val))
173
173
174 seen, l = set(), []
174 seen, l = set(), []
175 for spec in revs:
175 for spec in revs:
176 if revrangesep in spec:
176 if revrangesep in spec:
177 start, end = spec.split(revrangesep, 1)
177 start, end = spec.split(revrangesep, 1)
178 start = revfix(repo, start, 0)
178 start = revfix(repo, start, 0)
179 end = revfix(repo, end, len(repo) - 1)
179 end = revfix(repo, end, len(repo) - 1)
180 step = start > end and -1 or 1
180 step = start > end and -1 or 1
181 for rev in xrange(start, end + step, step):
181 for rev in xrange(start, end + step, step):
182 if rev in seen:
182 if rev in seen:
183 continue
183 continue
184 seen.add(rev)
184 seen.add(rev)
185 l.append(rev)
185 l.append(rev)
186 else:
186 else:
187 rev = revfix(repo, spec, None)
187 rev = revfix(repo, spec, None)
188 if rev in seen:
188 if rev in seen:
189 continue
189 continue
190 seen.add(rev)
190 seen.add(rev)
191 l.append(rev)
191 l.append(rev)
192
192
193 return l
193 return l
194
194
195 def make_filename(repo, pat, node,
195 def make_filename(repo, pat, node,
196 total=None, seqno=None, revwidth=None, pathname=None):
196 total=None, seqno=None, revwidth=None, pathname=None):
197 node_expander = {
197 node_expander = {
198 'H': lambda: hex(node),
198 'H': lambda: hex(node),
199 'R': lambda: str(repo.changelog.rev(node)),
199 'R': lambda: str(repo.changelog.rev(node)),
200 'h': lambda: short(node),
200 'h': lambda: short(node),
201 }
201 }
202 expander = {
202 expander = {
203 '%': lambda: '%',
203 '%': lambda: '%',
204 'b': lambda: os.path.basename(repo.root),
204 'b': lambda: os.path.basename(repo.root),
205 }
205 }
206
206
207 try:
207 try:
208 if node:
208 if node:
209 expander.update(node_expander)
209 expander.update(node_expander)
210 if node:
210 if node:
211 expander['r'] = (lambda:
211 expander['r'] = (lambda:
212 str(repo.changelog.rev(node)).zfill(revwidth or 0))
212 str(repo.changelog.rev(node)).zfill(revwidth or 0))
213 if total is not None:
213 if total is not None:
214 expander['N'] = lambda: str(total)
214 expander['N'] = lambda: str(total)
215 if seqno is not None:
215 if seqno is not None:
216 expander['n'] = lambda: str(seqno)
216 expander['n'] = lambda: str(seqno)
217 if total is not None and seqno is not None:
217 if total is not None and seqno is not None:
218 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
218 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
219 if pathname is not None:
219 if pathname is not None:
220 expander['s'] = lambda: os.path.basename(pathname)
220 expander['s'] = lambda: os.path.basename(pathname)
221 expander['d'] = lambda: os.path.dirname(pathname) or '.'
221 expander['d'] = lambda: os.path.dirname(pathname) or '.'
222 expander['p'] = lambda: pathname
222 expander['p'] = lambda: pathname
223
223
224 newname = []
224 newname = []
225 patlen = len(pat)
225 patlen = len(pat)
226 i = 0
226 i = 0
227 while i < patlen:
227 while i < patlen:
228 c = pat[i]
228 c = pat[i]
229 if c == '%':
229 if c == '%':
230 i += 1
230 i += 1
231 c = pat[i]
231 c = pat[i]
232 c = expander[c]()
232 c = expander[c]()
233 newname.append(c)
233 newname.append(c)
234 i += 1
234 i += 1
235 return ''.join(newname)
235 return ''.join(newname)
236 except KeyError, inst:
236 except KeyError, inst:
237 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
237 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
238 inst.args[0])
238 inst.args[0])
239
239
240 def make_file(repo, pat, node=None,
240 def make_file(repo, pat, node=None,
241 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
241 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
242
242
243 writable = 'w' in mode or 'a' in mode
243 writable = 'w' in mode or 'a' in mode
244
244
245 if not pat or pat == '-':
245 if not pat or pat == '-':
246 return writable and sys.stdout or sys.stdin
246 return writable and sys.stdout or sys.stdin
247 if hasattr(pat, 'write') and writable:
247 if hasattr(pat, 'write') and writable:
248 return pat
248 return pat
249 if hasattr(pat, 'read') and 'r' in mode:
249 if hasattr(pat, 'read') and 'r' in mode:
250 return pat
250 return pat
251 return open(make_filename(repo, pat, node, total, seqno, revwidth,
251 return open(make_filename(repo, pat, node, total, seqno, revwidth,
252 pathname),
252 pathname),
253 mode)
253 mode)
254
254
255 def expandpats(pats):
255 def expandpats(pats):
256 if not util.expandglobs:
256 if not util.expandglobs:
257 return list(pats)
257 return list(pats)
258 ret = []
258 ret = []
259 for p in pats:
259 for p in pats:
260 kind, name = _match._patsplit(p, None)
260 kind, name = _match._patsplit(p, None)
261 if kind is None:
261 if kind is None:
262 try:
262 try:
263 globbed = glob.glob(name)
263 globbed = glob.glob(name)
264 except re.error:
264 except re.error:
265 globbed = [name]
265 globbed = [name]
266 if globbed:
266 if globbed:
267 ret.extend(globbed)
267 ret.extend(globbed)
268 continue
268 continue
269 ret.append(p)
269 ret.append(p)
270 return ret
270 return ret
271
271
272 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
272 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
273 if not globbed and default == 'relpath':
273 if not globbed and default == 'relpath':
274 pats = expandpats(pats or [])
274 pats = expandpats(pats or [])
275 m = _match.match(repo.root, repo.getcwd(), pats,
275 m = _match.match(repo.root, repo.getcwd(), pats,
276 opts.get('include'), opts.get('exclude'), default)
276 opts.get('include'), opts.get('exclude'), default)
277 def badfn(f, msg):
277 def badfn(f, msg):
278 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
278 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
279 m.bad = badfn
279 m.bad = badfn
280 return m
280 return m
281
281
282 def matchall(repo):
282 def matchall(repo):
283 return _match.always(repo.root, repo.getcwd())
283 return _match.always(repo.root, repo.getcwd())
284
284
285 def matchfiles(repo, files):
285 def matchfiles(repo, files):
286 return _match.exact(repo.root, repo.getcwd(), files)
286 return _match.exact(repo.root, repo.getcwd(), files)
287
287
288 def findrenames(repo, added, removed, threshold):
288 def findrenames(repo, added, removed, threshold):
289 '''find renamed files -- yields (before, after, score) tuples'''
289 '''find renamed files -- yields (before, after, score) tuples'''
290 copies = {}
290 copies = {}
291 ctx = repo['.']
291 ctx = repo['.']
292 for i, r in enumerate(removed):
292 for i, r in enumerate(removed):
293 repo.ui.progress(_('looking for similarities'), i, total=len(removed))
293 repo.ui.progress(_('searching'), i, total=len(removed))
294 if r not in ctx:
294 if r not in ctx:
295 continue
295 continue
296 fctx = ctx.filectx(r)
296 fctx = ctx.filectx(r)
297
297
298 # lazily load text
298 # lazily load text
299 @util.cachefunc
299 @util.cachefunc
300 def data():
300 def data():
301 orig = fctx.data()
301 orig = fctx.data()
302 return orig, mdiff.splitnewlines(orig)
302 return orig, mdiff.splitnewlines(orig)
303
303
304 def score(text):
304 def score(text):
305 if not len(text):
305 if not len(text):
306 return 0.0
306 return 0.0
307 if not fctx.cmp(text):
307 if not fctx.cmp(text):
308 return 1.0
308 return 1.0
309 if threshold == 1.0:
309 if threshold == 1.0:
310 return 0.0
310 return 0.0
311 orig, lines = data()
311 orig, lines = data()
312 # bdiff.blocks() returns blocks of matching lines
312 # bdiff.blocks() returns blocks of matching lines
313 # count the number of bytes in each
313 # count the number of bytes in each
314 equal = 0
314 equal = 0
315 matches = bdiff.blocks(text, orig)
315 matches = bdiff.blocks(text, orig)
316 for x1, x2, y1, y2 in matches:
316 for x1, x2, y1, y2 in matches:
317 for line in lines[y1:y2]:
317 for line in lines[y1:y2]:
318 equal += len(line)
318 equal += len(line)
319
319
320 lengths = len(text) + len(orig)
320 lengths = len(text) + len(orig)
321 return equal * 2.0 / lengths
321 return equal * 2.0 / lengths
322
322
323 for a in added:
323 for a in added:
324 bestscore = copies.get(a, (None, threshold))[1]
324 bestscore = copies.get(a, (None, threshold))[1]
325 myscore = score(repo.wread(a))
325 myscore = score(repo.wread(a))
326 if myscore >= bestscore:
326 if myscore >= bestscore:
327 copies[a] = (r, myscore)
327 copies[a] = (r, myscore)
328 repo.ui.progress(_('looking for similarities'), None, total=len(removed))
328 repo.ui.progress(_('searching'), None, total=len(removed))
329
329
330 for dest, v in copies.iteritems():
330 for dest, v in copies.iteritems():
331 source, score = v
331 source, score = v
332 yield source, dest, score
332 yield source, dest, score
333
333
334 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
334 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
335 if dry_run is None:
335 if dry_run is None:
336 dry_run = opts.get('dry_run')
336 dry_run = opts.get('dry_run')
337 if similarity is None:
337 if similarity is None:
338 similarity = float(opts.get('similarity') or 0)
338 similarity = float(opts.get('similarity') or 0)
339 # we'd use status here, except handling of symlinks and ignore is tricky
339 # we'd use status here, except handling of symlinks and ignore is tricky
340 added, unknown, deleted, removed = [], [], [], []
340 added, unknown, deleted, removed = [], [], [], []
341 audit_path = util.path_auditor(repo.root)
341 audit_path = util.path_auditor(repo.root)
342 m = match(repo, pats, opts)
342 m = match(repo, pats, opts)
343 for abs in repo.walk(m):
343 for abs in repo.walk(m):
344 target = repo.wjoin(abs)
344 target = repo.wjoin(abs)
345 good = True
345 good = True
346 try:
346 try:
347 audit_path(abs)
347 audit_path(abs)
348 except:
348 except:
349 good = False
349 good = False
350 rel = m.rel(abs)
350 rel = m.rel(abs)
351 exact = m.exact(abs)
351 exact = m.exact(abs)
352 if good and abs not in repo.dirstate:
352 if good and abs not in repo.dirstate:
353 unknown.append(abs)
353 unknown.append(abs)
354 if repo.ui.verbose or not exact:
354 if repo.ui.verbose or not exact:
355 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
355 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
356 elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
356 elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
357 or (os.path.isdir(target) and not os.path.islink(target))):
357 or (os.path.isdir(target) and not os.path.islink(target))):
358 deleted.append(abs)
358 deleted.append(abs)
359 if repo.ui.verbose or not exact:
359 if repo.ui.verbose or not exact:
360 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
360 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
361 # for finding renames
361 # for finding renames
362 elif repo.dirstate[abs] == 'r':
362 elif repo.dirstate[abs] == 'r':
363 removed.append(abs)
363 removed.append(abs)
364 elif repo.dirstate[abs] == 'a':
364 elif repo.dirstate[abs] == 'a':
365 added.append(abs)
365 added.append(abs)
366 copies = {}
366 copies = {}
367 if similarity > 0:
367 if similarity > 0:
368 for old, new, score in findrenames(repo, added + unknown,
368 for old, new, score in findrenames(repo, added + unknown,
369 removed + deleted, similarity):
369 removed + deleted, similarity):
370 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
370 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
371 repo.ui.status(_('recording removal of %s as rename to %s '
371 repo.ui.status(_('recording removal of %s as rename to %s '
372 '(%d%% similar)\n') %
372 '(%d%% similar)\n') %
373 (m.rel(old), m.rel(new), score * 100))
373 (m.rel(old), m.rel(new), score * 100))
374 copies[new] = old
374 copies[new] = old
375
375
376 if not dry_run:
376 if not dry_run:
377 wlock = repo.wlock()
377 wlock = repo.wlock()
378 try:
378 try:
379 repo.remove(deleted)
379 repo.remove(deleted)
380 repo.add(unknown)
380 repo.add(unknown)
381 for new, old in copies.iteritems():
381 for new, old in copies.iteritems():
382 repo.copy(old, new)
382 repo.copy(old, new)
383 finally:
383 finally:
384 wlock.release()
384 wlock.release()
385
385
386 def copy(ui, repo, pats, opts, rename=False):
386 def copy(ui, repo, pats, opts, rename=False):
387 # called with the repo lock held
387 # called with the repo lock held
388 #
388 #
389 # hgsep => pathname that uses "/" to separate directories
389 # hgsep => pathname that uses "/" to separate directories
390 # ossep => pathname that uses os.sep to separate directories
390 # ossep => pathname that uses os.sep to separate directories
391 cwd = repo.getcwd()
391 cwd = repo.getcwd()
392 targets = {}
392 targets = {}
393 after = opts.get("after")
393 after = opts.get("after")
394 dryrun = opts.get("dry_run")
394 dryrun = opts.get("dry_run")
395
395
396 def walkpat(pat):
396 def walkpat(pat):
397 srcs = []
397 srcs = []
398 m = match(repo, [pat], opts, globbed=True)
398 m = match(repo, [pat], opts, globbed=True)
399 for abs in repo.walk(m):
399 for abs in repo.walk(m):
400 state = repo.dirstate[abs]
400 state = repo.dirstate[abs]
401 rel = m.rel(abs)
401 rel = m.rel(abs)
402 exact = m.exact(abs)
402 exact = m.exact(abs)
403 if state in '?r':
403 if state in '?r':
404 if exact and state == '?':
404 if exact and state == '?':
405 ui.warn(_('%s: not copying - file is not managed\n') % rel)
405 ui.warn(_('%s: not copying - file is not managed\n') % rel)
406 if exact and state == 'r':
406 if exact and state == 'r':
407 ui.warn(_('%s: not copying - file has been marked for'
407 ui.warn(_('%s: not copying - file has been marked for'
408 ' remove\n') % rel)
408 ' remove\n') % rel)
409 continue
409 continue
410 # abs: hgsep
410 # abs: hgsep
411 # rel: ossep
411 # rel: ossep
412 srcs.append((abs, rel, exact))
412 srcs.append((abs, rel, exact))
413 return srcs
413 return srcs
414
414
415 # abssrc: hgsep
415 # abssrc: hgsep
416 # relsrc: ossep
416 # relsrc: ossep
417 # otarget: ossep
417 # otarget: ossep
418 def copyfile(abssrc, relsrc, otarget, exact):
418 def copyfile(abssrc, relsrc, otarget, exact):
419 abstarget = util.canonpath(repo.root, cwd, otarget)
419 abstarget = util.canonpath(repo.root, cwd, otarget)
420 reltarget = repo.pathto(abstarget, cwd)
420 reltarget = repo.pathto(abstarget, cwd)
421 target = repo.wjoin(abstarget)
421 target = repo.wjoin(abstarget)
422 src = repo.wjoin(abssrc)
422 src = repo.wjoin(abssrc)
423 state = repo.dirstate[abstarget]
423 state = repo.dirstate[abstarget]
424
424
425 # check for collisions
425 # check for collisions
426 prevsrc = targets.get(abstarget)
426 prevsrc = targets.get(abstarget)
427 if prevsrc is not None:
427 if prevsrc is not None:
428 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
428 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
429 (reltarget, repo.pathto(abssrc, cwd),
429 (reltarget, repo.pathto(abssrc, cwd),
430 repo.pathto(prevsrc, cwd)))
430 repo.pathto(prevsrc, cwd)))
431 return
431 return
432
432
433 # check for overwrites
433 # check for overwrites
434 exists = os.path.exists(target)
434 exists = os.path.exists(target)
435 if not after and exists or after and state in 'mn':
435 if not after and exists or after and state in 'mn':
436 if not opts['force']:
436 if not opts['force']:
437 ui.warn(_('%s: not overwriting - file exists\n') %
437 ui.warn(_('%s: not overwriting - file exists\n') %
438 reltarget)
438 reltarget)
439 return
439 return
440
440
441 if after:
441 if after:
442 if not exists:
442 if not exists:
443 return
443 return
444 elif not dryrun:
444 elif not dryrun:
445 try:
445 try:
446 if exists:
446 if exists:
447 os.unlink(target)
447 os.unlink(target)
448 targetdir = os.path.dirname(target) or '.'
448 targetdir = os.path.dirname(target) or '.'
449 if not os.path.isdir(targetdir):
449 if not os.path.isdir(targetdir):
450 os.makedirs(targetdir)
450 os.makedirs(targetdir)
451 util.copyfile(src, target)
451 util.copyfile(src, target)
452 except IOError, inst:
452 except IOError, inst:
453 if inst.errno == errno.ENOENT:
453 if inst.errno == errno.ENOENT:
454 ui.warn(_('%s: deleted in working copy\n') % relsrc)
454 ui.warn(_('%s: deleted in working copy\n') % relsrc)
455 else:
455 else:
456 ui.warn(_('%s: cannot copy - %s\n') %
456 ui.warn(_('%s: cannot copy - %s\n') %
457 (relsrc, inst.strerror))
457 (relsrc, inst.strerror))
458 return True # report a failure
458 return True # report a failure
459
459
460 if ui.verbose or not exact:
460 if ui.verbose or not exact:
461 if rename:
461 if rename:
462 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
462 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
463 else:
463 else:
464 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
464 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
465
465
466 targets[abstarget] = abssrc
466 targets[abstarget] = abssrc
467
467
468 # fix up dirstate
468 # fix up dirstate
469 origsrc = repo.dirstate.copied(abssrc) or abssrc
469 origsrc = repo.dirstate.copied(abssrc) or abssrc
470 if abstarget == origsrc: # copying back a copy?
470 if abstarget == origsrc: # copying back a copy?
471 if state not in 'mn' and not dryrun:
471 if state not in 'mn' and not dryrun:
472 repo.dirstate.normallookup(abstarget)
472 repo.dirstate.normallookup(abstarget)
473 else:
473 else:
474 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
474 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
475 if not ui.quiet:
475 if not ui.quiet:
476 ui.warn(_("%s has not been committed yet, so no copy "
476 ui.warn(_("%s has not been committed yet, so no copy "
477 "data will be stored for %s.\n")
477 "data will be stored for %s.\n")
478 % (repo.pathto(origsrc, cwd), reltarget))
478 % (repo.pathto(origsrc, cwd), reltarget))
479 if repo.dirstate[abstarget] in '?r' and not dryrun:
479 if repo.dirstate[abstarget] in '?r' and not dryrun:
480 repo.add([abstarget])
480 repo.add([abstarget])
481 elif not dryrun:
481 elif not dryrun:
482 repo.copy(origsrc, abstarget)
482 repo.copy(origsrc, abstarget)
483
483
484 if rename and not dryrun:
484 if rename and not dryrun:
485 repo.remove([abssrc], not after)
485 repo.remove([abssrc], not after)
486
486
487 # pat: ossep
487 # pat: ossep
488 # dest ossep
488 # dest ossep
489 # srcs: list of (hgsep, hgsep, ossep, bool)
489 # srcs: list of (hgsep, hgsep, ossep, bool)
490 # return: function that takes hgsep and returns ossep
490 # return: function that takes hgsep and returns ossep
491 def targetpathfn(pat, dest, srcs):
491 def targetpathfn(pat, dest, srcs):
492 if os.path.isdir(pat):
492 if os.path.isdir(pat):
493 abspfx = util.canonpath(repo.root, cwd, pat)
493 abspfx = util.canonpath(repo.root, cwd, pat)
494 abspfx = util.localpath(abspfx)
494 abspfx = util.localpath(abspfx)
495 if destdirexists:
495 if destdirexists:
496 striplen = len(os.path.split(abspfx)[0])
496 striplen = len(os.path.split(abspfx)[0])
497 else:
497 else:
498 striplen = len(abspfx)
498 striplen = len(abspfx)
499 if striplen:
499 if striplen:
500 striplen += len(os.sep)
500 striplen += len(os.sep)
501 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
501 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
502 elif destdirexists:
502 elif destdirexists:
503 res = lambda p: os.path.join(dest,
503 res = lambda p: os.path.join(dest,
504 os.path.basename(util.localpath(p)))
504 os.path.basename(util.localpath(p)))
505 else:
505 else:
506 res = lambda p: dest
506 res = lambda p: dest
507 return res
507 return res
508
508
509 # pat: ossep
509 # pat: ossep
510 # dest ossep
510 # dest ossep
511 # srcs: list of (hgsep, hgsep, ossep, bool)
511 # srcs: list of (hgsep, hgsep, ossep, bool)
512 # return: function that takes hgsep and returns ossep
512 # return: function that takes hgsep and returns ossep
513 def targetpathafterfn(pat, dest, srcs):
513 def targetpathafterfn(pat, dest, srcs):
514 if _match.patkind(pat):
514 if _match.patkind(pat):
515 # a mercurial pattern
515 # a mercurial pattern
516 res = lambda p: os.path.join(dest,
516 res = lambda p: os.path.join(dest,
517 os.path.basename(util.localpath(p)))
517 os.path.basename(util.localpath(p)))
518 else:
518 else:
519 abspfx = util.canonpath(repo.root, cwd, pat)
519 abspfx = util.canonpath(repo.root, cwd, pat)
520 if len(abspfx) < len(srcs[0][0]):
520 if len(abspfx) < len(srcs[0][0]):
521 # A directory. Either the target path contains the last
521 # A directory. Either the target path contains the last
522 # component of the source path or it does not.
522 # component of the source path or it does not.
523 def evalpath(striplen):
523 def evalpath(striplen):
524 score = 0
524 score = 0
525 for s in srcs:
525 for s in srcs:
526 t = os.path.join(dest, util.localpath(s[0])[striplen:])
526 t = os.path.join(dest, util.localpath(s[0])[striplen:])
527 if os.path.exists(t):
527 if os.path.exists(t):
528 score += 1
528 score += 1
529 return score
529 return score
530
530
531 abspfx = util.localpath(abspfx)
531 abspfx = util.localpath(abspfx)
532 striplen = len(abspfx)
532 striplen = len(abspfx)
533 if striplen:
533 if striplen:
534 striplen += len(os.sep)
534 striplen += len(os.sep)
535 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
535 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
536 score = evalpath(striplen)
536 score = evalpath(striplen)
537 striplen1 = len(os.path.split(abspfx)[0])
537 striplen1 = len(os.path.split(abspfx)[0])
538 if striplen1:
538 if striplen1:
539 striplen1 += len(os.sep)
539 striplen1 += len(os.sep)
540 if evalpath(striplen1) > score:
540 if evalpath(striplen1) > score:
541 striplen = striplen1
541 striplen = striplen1
542 res = lambda p: os.path.join(dest,
542 res = lambda p: os.path.join(dest,
543 util.localpath(p)[striplen:])
543 util.localpath(p)[striplen:])
544 else:
544 else:
545 # a file
545 # a file
546 if destdirexists:
546 if destdirexists:
547 res = lambda p: os.path.join(dest,
547 res = lambda p: os.path.join(dest,
548 os.path.basename(util.localpath(p)))
548 os.path.basename(util.localpath(p)))
549 else:
549 else:
550 res = lambda p: dest
550 res = lambda p: dest
551 return res
551 return res
552
552
553
553
554 pats = expandpats(pats)
554 pats = expandpats(pats)
555 if not pats:
555 if not pats:
556 raise util.Abort(_('no source or destination specified'))
556 raise util.Abort(_('no source or destination specified'))
557 if len(pats) == 1:
557 if len(pats) == 1:
558 raise util.Abort(_('no destination specified'))
558 raise util.Abort(_('no destination specified'))
559 dest = pats.pop()
559 dest = pats.pop()
560 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
560 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
561 if not destdirexists:
561 if not destdirexists:
562 if len(pats) > 1 or _match.patkind(pats[0]):
562 if len(pats) > 1 or _match.patkind(pats[0]):
563 raise util.Abort(_('with multiple sources, destination must be an '
563 raise util.Abort(_('with multiple sources, destination must be an '
564 'existing directory'))
564 'existing directory'))
565 if util.endswithsep(dest):
565 if util.endswithsep(dest):
566 raise util.Abort(_('destination %s is not a directory') % dest)
566 raise util.Abort(_('destination %s is not a directory') % dest)
567
567
568 tfn = targetpathfn
568 tfn = targetpathfn
569 if after:
569 if after:
570 tfn = targetpathafterfn
570 tfn = targetpathafterfn
571 copylist = []
571 copylist = []
572 for pat in pats:
572 for pat in pats:
573 srcs = walkpat(pat)
573 srcs = walkpat(pat)
574 if not srcs:
574 if not srcs:
575 continue
575 continue
576 copylist.append((tfn(pat, dest, srcs), srcs))
576 copylist.append((tfn(pat, dest, srcs), srcs))
577 if not copylist:
577 if not copylist:
578 raise util.Abort(_('no files to copy'))
578 raise util.Abort(_('no files to copy'))
579
579
580 errors = 0
580 errors = 0
581 for targetpath, srcs in copylist:
581 for targetpath, srcs in copylist:
582 for abssrc, relsrc, exact in srcs:
582 for abssrc, relsrc, exact in srcs:
583 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
583 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
584 errors += 1
584 errors += 1
585
585
586 if errors:
586 if errors:
587 ui.warn(_('(consider using --after)\n'))
587 ui.warn(_('(consider using --after)\n'))
588
588
589 return errors
589 return errors
590
590
591 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
591 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
592 runargs=None, appendpid=False):
592 runargs=None, appendpid=False):
593 '''Run a command as a service.'''
593 '''Run a command as a service.'''
594
594
595 if opts['daemon'] and not opts['daemon_pipefds']:
595 if opts['daemon'] and not opts['daemon_pipefds']:
596 # Signal child process startup with file removal
596 # Signal child process startup with file removal
597 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
597 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
598 os.close(lockfd)
598 os.close(lockfd)
599 try:
599 try:
600 if not runargs:
600 if not runargs:
601 runargs = util.hgcmd() + sys.argv[1:]
601 runargs = util.hgcmd() + sys.argv[1:]
602 runargs.append('--daemon-pipefds=%s' % lockpath)
602 runargs.append('--daemon-pipefds=%s' % lockpath)
603 # Don't pass --cwd to the child process, because we've already
603 # Don't pass --cwd to the child process, because we've already
604 # changed directory.
604 # changed directory.
605 for i in xrange(1, len(runargs)):
605 for i in xrange(1, len(runargs)):
606 if runargs[i].startswith('--cwd='):
606 if runargs[i].startswith('--cwd='):
607 del runargs[i]
607 del runargs[i]
608 break
608 break
609 elif runargs[i].startswith('--cwd'):
609 elif runargs[i].startswith('--cwd'):
610 del runargs[i:i + 2]
610 del runargs[i:i + 2]
611 break
611 break
612 def condfn():
612 def condfn():
613 return not os.path.exists(lockpath)
613 return not os.path.exists(lockpath)
614 pid = util.rundetached(runargs, condfn)
614 pid = util.rundetached(runargs, condfn)
615 if pid < 0:
615 if pid < 0:
616 raise util.Abort(_('child process failed to start'))
616 raise util.Abort(_('child process failed to start'))
617 finally:
617 finally:
618 try:
618 try:
619 os.unlink(lockpath)
619 os.unlink(lockpath)
620 except OSError, e:
620 except OSError, e:
621 if e.errno != errno.ENOENT:
621 if e.errno != errno.ENOENT:
622 raise
622 raise
623 if parentfn:
623 if parentfn:
624 return parentfn(pid)
624 return parentfn(pid)
625 else:
625 else:
626 return
626 return
627
627
628 if initfn:
628 if initfn:
629 initfn()
629 initfn()
630
630
631 if opts['pid_file']:
631 if opts['pid_file']:
632 mode = appendpid and 'a' or 'w'
632 mode = appendpid and 'a' or 'w'
633 fp = open(opts['pid_file'], mode)
633 fp = open(opts['pid_file'], mode)
634 fp.write(str(os.getpid()) + '\n')
634 fp.write(str(os.getpid()) + '\n')
635 fp.close()
635 fp.close()
636
636
637 if opts['daemon_pipefds']:
637 if opts['daemon_pipefds']:
638 lockpath = opts['daemon_pipefds']
638 lockpath = opts['daemon_pipefds']
639 try:
639 try:
640 os.setsid()
640 os.setsid()
641 except AttributeError:
641 except AttributeError:
642 pass
642 pass
643 os.unlink(lockpath)
643 os.unlink(lockpath)
644 util.hidewindow()
644 util.hidewindow()
645 sys.stdout.flush()
645 sys.stdout.flush()
646 sys.stderr.flush()
646 sys.stderr.flush()
647
647
648 nullfd = os.open(util.nulldev, os.O_RDWR)
648 nullfd = os.open(util.nulldev, os.O_RDWR)
649 logfilefd = nullfd
649 logfilefd = nullfd
650 if logfile:
650 if logfile:
651 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
651 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
652 os.dup2(nullfd, 0)
652 os.dup2(nullfd, 0)
653 os.dup2(logfilefd, 1)
653 os.dup2(logfilefd, 1)
654 os.dup2(logfilefd, 2)
654 os.dup2(logfilefd, 2)
655 if nullfd not in (0, 1, 2):
655 if nullfd not in (0, 1, 2):
656 os.close(nullfd)
656 os.close(nullfd)
657 if logfile and logfilefd not in (0, 1, 2):
657 if logfile and logfilefd not in (0, 1, 2):
658 os.close(logfilefd)
658 os.close(logfilefd)
659
659
660 if runfn:
660 if runfn:
661 return runfn()
661 return runfn()
662
662
663 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
663 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
664 opts=None):
664 opts=None):
665 '''export changesets as hg patches.'''
665 '''export changesets as hg patches.'''
666
666
667 total = len(revs)
667 total = len(revs)
668 revwidth = max([len(str(rev)) for rev in revs])
668 revwidth = max([len(str(rev)) for rev in revs])
669
669
670 def single(rev, seqno, fp):
670 def single(rev, seqno, fp):
671 ctx = repo[rev]
671 ctx = repo[rev]
672 node = ctx.node()
672 node = ctx.node()
673 parents = [p.node() for p in ctx.parents() if p]
673 parents = [p.node() for p in ctx.parents() if p]
674 branch = ctx.branch()
674 branch = ctx.branch()
675 if switch_parent:
675 if switch_parent:
676 parents.reverse()
676 parents.reverse()
677 prev = (parents and parents[0]) or nullid
677 prev = (parents and parents[0]) or nullid
678
678
679 if not fp:
679 if not fp:
680 fp = make_file(repo, template, node, total=total, seqno=seqno,
680 fp = make_file(repo, template, node, total=total, seqno=seqno,
681 revwidth=revwidth, mode='ab')
681 revwidth=revwidth, mode='ab')
682 if fp != sys.stdout and hasattr(fp, 'name'):
682 if fp != sys.stdout and hasattr(fp, 'name'):
683 repo.ui.note("%s\n" % fp.name)
683 repo.ui.note("%s\n" % fp.name)
684
684
685 fp.write("# HG changeset patch\n")
685 fp.write("# HG changeset patch\n")
686 fp.write("# User %s\n" % ctx.user())
686 fp.write("# User %s\n" % ctx.user())
687 fp.write("# Date %d %d\n" % ctx.date())
687 fp.write("# Date %d %d\n" % ctx.date())
688 if branch and (branch != 'default'):
688 if branch and (branch != 'default'):
689 fp.write("# Branch %s\n" % branch)
689 fp.write("# Branch %s\n" % branch)
690 fp.write("# Node ID %s\n" % hex(node))
690 fp.write("# Node ID %s\n" % hex(node))
691 fp.write("# Parent %s\n" % hex(prev))
691 fp.write("# Parent %s\n" % hex(prev))
692 if len(parents) > 1:
692 if len(parents) > 1:
693 fp.write("# Parent %s\n" % hex(parents[1]))
693 fp.write("# Parent %s\n" % hex(parents[1]))
694 fp.write(ctx.description().rstrip())
694 fp.write(ctx.description().rstrip())
695 fp.write("\n\n")
695 fp.write("\n\n")
696
696
697 for chunk in patch.diff(repo, prev, node, opts=opts):
697 for chunk in patch.diff(repo, prev, node, opts=opts):
698 fp.write(chunk)
698 fp.write(chunk)
699
699
700 for seqno, rev in enumerate(revs):
700 for seqno, rev in enumerate(revs):
701 single(rev, seqno + 1, fp)
701 single(rev, seqno + 1, fp)
702
702
703 class changeset_printer(object):
703 class changeset_printer(object):
704 '''show changeset information when templating not requested.'''
704 '''show changeset information when templating not requested.'''
705
705
706 def __init__(self, ui, repo, patch, diffopts, buffered):
706 def __init__(self, ui, repo, patch, diffopts, buffered):
707 self.ui = ui
707 self.ui = ui
708 self.repo = repo
708 self.repo = repo
709 self.buffered = buffered
709 self.buffered = buffered
710 self.patch = patch
710 self.patch = patch
711 self.diffopts = diffopts
711 self.diffopts = diffopts
712 self.header = {}
712 self.header = {}
713 self.hunk = {}
713 self.hunk = {}
714 self.lastheader = None
714 self.lastheader = None
715 self.footer = None
715 self.footer = None
716
716
717 def flush(self, rev):
717 def flush(self, rev):
718 if rev in self.header:
718 if rev in self.header:
719 h = self.header[rev]
719 h = self.header[rev]
720 if h != self.lastheader:
720 if h != self.lastheader:
721 self.lastheader = h
721 self.lastheader = h
722 self.ui.write(h)
722 self.ui.write(h)
723 del self.header[rev]
723 del self.header[rev]
724 if rev in self.hunk:
724 if rev in self.hunk:
725 self.ui.write(self.hunk[rev])
725 self.ui.write(self.hunk[rev])
726 del self.hunk[rev]
726 del self.hunk[rev]
727 return 1
727 return 1
728 return 0
728 return 0
729
729
730 def close(self):
730 def close(self):
731 if self.footer:
731 if self.footer:
732 self.ui.write(self.footer)
732 self.ui.write(self.footer)
733
733
734 def show(self, ctx, copies=None, **props):
734 def show(self, ctx, copies=None, **props):
735 if self.buffered:
735 if self.buffered:
736 self.ui.pushbuffer()
736 self.ui.pushbuffer()
737 self._show(ctx, copies, props)
737 self._show(ctx, copies, props)
738 self.hunk[ctx.rev()] = self.ui.popbuffer()
738 self.hunk[ctx.rev()] = self.ui.popbuffer()
739 else:
739 else:
740 self._show(ctx, copies, props)
740 self._show(ctx, copies, props)
741
741
742 def _show(self, ctx, copies, props):
742 def _show(self, ctx, copies, props):
743 '''show a single changeset or file revision'''
743 '''show a single changeset or file revision'''
744 changenode = ctx.node()
744 changenode = ctx.node()
745 rev = ctx.rev()
745 rev = ctx.rev()
746
746
747 if self.ui.quiet:
747 if self.ui.quiet:
748 self.ui.write("%d:%s\n" % (rev, short(changenode)))
748 self.ui.write("%d:%s\n" % (rev, short(changenode)))
749 return
749 return
750
750
751 log = self.repo.changelog
751 log = self.repo.changelog
752 date = util.datestr(ctx.date())
752 date = util.datestr(ctx.date())
753
753
754 hexfunc = self.ui.debugflag and hex or short
754 hexfunc = self.ui.debugflag and hex or short
755
755
756 parents = [(p, hexfunc(log.node(p)))
756 parents = [(p, hexfunc(log.node(p)))
757 for p in self._meaningful_parentrevs(log, rev)]
757 for p in self._meaningful_parentrevs(log, rev)]
758
758
759 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
759 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
760
760
761 branch = ctx.branch()
761 branch = ctx.branch()
762 # don't show the default branch name
762 # don't show the default branch name
763 if branch != 'default':
763 if branch != 'default':
764 branch = encoding.tolocal(branch)
764 branch = encoding.tolocal(branch)
765 self.ui.write(_("branch: %s\n") % branch)
765 self.ui.write(_("branch: %s\n") % branch)
766 for tag in self.repo.nodetags(changenode):
766 for tag in self.repo.nodetags(changenode):
767 self.ui.write(_("tag: %s\n") % tag)
767 self.ui.write(_("tag: %s\n") % tag)
768 for parent in parents:
768 for parent in parents:
769 self.ui.write(_("parent: %d:%s\n") % parent)
769 self.ui.write(_("parent: %d:%s\n") % parent)
770
770
771 if self.ui.debugflag:
771 if self.ui.debugflag:
772 mnode = ctx.manifestnode()
772 mnode = ctx.manifestnode()
773 self.ui.write(_("manifest: %d:%s\n") %
773 self.ui.write(_("manifest: %d:%s\n") %
774 (self.repo.manifest.rev(mnode), hex(mnode)))
774 (self.repo.manifest.rev(mnode), hex(mnode)))
775 self.ui.write(_("user: %s\n") % ctx.user())
775 self.ui.write(_("user: %s\n") % ctx.user())
776 self.ui.write(_("date: %s\n") % date)
776 self.ui.write(_("date: %s\n") % date)
777
777
778 if self.ui.debugflag:
778 if self.ui.debugflag:
779 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
779 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
780 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
780 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
781 files):
781 files):
782 if value:
782 if value:
783 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
783 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
784 elif ctx.files() and self.ui.verbose:
784 elif ctx.files() and self.ui.verbose:
785 self.ui.write(_("files: %s\n") % " ".join(ctx.files()))
785 self.ui.write(_("files: %s\n") % " ".join(ctx.files()))
786 if copies and self.ui.verbose:
786 if copies and self.ui.verbose:
787 copies = ['%s (%s)' % c for c in copies]
787 copies = ['%s (%s)' % c for c in copies]
788 self.ui.write(_("copies: %s\n") % ' '.join(copies))
788 self.ui.write(_("copies: %s\n") % ' '.join(copies))
789
789
790 extra = ctx.extra()
790 extra = ctx.extra()
791 if extra and self.ui.debugflag:
791 if extra and self.ui.debugflag:
792 for key, value in sorted(extra.items()):
792 for key, value in sorted(extra.items()):
793 self.ui.write(_("extra: %s=%s\n")
793 self.ui.write(_("extra: %s=%s\n")
794 % (key, value.encode('string_escape')))
794 % (key, value.encode('string_escape')))
795
795
796 description = ctx.description().strip()
796 description = ctx.description().strip()
797 if description:
797 if description:
798 if self.ui.verbose:
798 if self.ui.verbose:
799 self.ui.write(_("description:\n"))
799 self.ui.write(_("description:\n"))
800 self.ui.write(description)
800 self.ui.write(description)
801 self.ui.write("\n\n")
801 self.ui.write("\n\n")
802 else:
802 else:
803 self.ui.write(_("summary: %s\n") %
803 self.ui.write(_("summary: %s\n") %
804 description.splitlines()[0])
804 description.splitlines()[0])
805 self.ui.write("\n")
805 self.ui.write("\n")
806
806
807 self.showpatch(changenode)
807 self.showpatch(changenode)
808
808
809 def showpatch(self, node):
809 def showpatch(self, node):
810 if self.patch:
810 if self.patch:
811 prev = self.repo.changelog.parents(node)[0]
811 prev = self.repo.changelog.parents(node)[0]
812 chunks = patch.diff(self.repo, prev, node, match=self.patch,
812 chunks = patch.diff(self.repo, prev, node, match=self.patch,
813 opts=patch.diffopts(self.ui, self.diffopts))
813 opts=patch.diffopts(self.ui, self.diffopts))
814 for chunk in chunks:
814 for chunk in chunks:
815 self.ui.write(chunk)
815 self.ui.write(chunk)
816 self.ui.write("\n")
816 self.ui.write("\n")
817
817
818 def _meaningful_parentrevs(self, log, rev):
818 def _meaningful_parentrevs(self, log, rev):
819 """Return list of meaningful (or all if debug) parentrevs for rev.
819 """Return list of meaningful (or all if debug) parentrevs for rev.
820
820
821 For merges (two non-nullrev revisions) both parents are meaningful.
821 For merges (two non-nullrev revisions) both parents are meaningful.
822 Otherwise the first parent revision is considered meaningful if it
822 Otherwise the first parent revision is considered meaningful if it
823 is not the preceding revision.
823 is not the preceding revision.
824 """
824 """
825 parents = log.parentrevs(rev)
825 parents = log.parentrevs(rev)
826 if not self.ui.debugflag and parents[1] == nullrev:
826 if not self.ui.debugflag and parents[1] == nullrev:
827 if parents[0] >= rev - 1:
827 if parents[0] >= rev - 1:
828 parents = []
828 parents = []
829 else:
829 else:
830 parents = [parents[0]]
830 parents = [parents[0]]
831 return parents
831 return parents
832
832
833
833
834 class changeset_templater(changeset_printer):
834 class changeset_templater(changeset_printer):
835 '''format changeset information.'''
835 '''format changeset information.'''
836
836
837 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
837 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
838 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
838 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
839 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
839 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
840 defaulttempl = {
840 defaulttempl = {
841 'parent': '{rev}:{node|formatnode} ',
841 'parent': '{rev}:{node|formatnode} ',
842 'manifest': '{rev}:{node|formatnode}',
842 'manifest': '{rev}:{node|formatnode}',
843 'file_copy': '{name} ({source})',
843 'file_copy': '{name} ({source})',
844 'extra': '{key}={value|stringescape}'
844 'extra': '{key}={value|stringescape}'
845 }
845 }
846 # filecopy is preserved for compatibility reasons
846 # filecopy is preserved for compatibility reasons
847 defaulttempl['filecopy'] = defaulttempl['file_copy']
847 defaulttempl['filecopy'] = defaulttempl['file_copy']
848 self.t = templater.templater(mapfile, {'formatnode': formatnode},
848 self.t = templater.templater(mapfile, {'formatnode': formatnode},
849 cache=defaulttempl)
849 cache=defaulttempl)
850 self.cache = {}
850 self.cache = {}
851
851
852 def use_template(self, t):
852 def use_template(self, t):
853 '''set template string to use'''
853 '''set template string to use'''
854 self.t.cache['changeset'] = t
854 self.t.cache['changeset'] = t
855
855
856 def _meaningful_parentrevs(self, ctx):
856 def _meaningful_parentrevs(self, ctx):
857 """Return list of meaningful (or all if debug) parentrevs for rev.
857 """Return list of meaningful (or all if debug) parentrevs for rev.
858 """
858 """
859 parents = ctx.parents()
859 parents = ctx.parents()
860 if len(parents) > 1:
860 if len(parents) > 1:
861 return parents
861 return parents
862 if self.ui.debugflag:
862 if self.ui.debugflag:
863 return [parents[0], self.repo['null']]
863 return [parents[0], self.repo['null']]
864 if parents[0].rev() >= ctx.rev() - 1:
864 if parents[0].rev() >= ctx.rev() - 1:
865 return []
865 return []
866 return parents
866 return parents
867
867
868 def _show(self, ctx, copies, props):
868 def _show(self, ctx, copies, props):
869 '''show a single changeset or file revision'''
869 '''show a single changeset or file revision'''
870
870
871 showlist = templatekw.showlist
871 showlist = templatekw.showlist
872
872
873 # showparents() behaviour depends on ui trace level which
873 # showparents() behaviour depends on ui trace level which
874 # causes unexpected behaviours at templating level and makes
874 # causes unexpected behaviours at templating level and makes
875 # it harder to extract it in a standalone function. Its
875 # it harder to extract it in a standalone function. Its
876 # behaviour cannot be changed so leave it here for now.
876 # behaviour cannot be changed so leave it here for now.
877 def showparents(**args):
877 def showparents(**args):
878 ctx = args['ctx']
878 ctx = args['ctx']
879 parents = [[('rev', p.rev()), ('node', p.hex())]
879 parents = [[('rev', p.rev()), ('node', p.hex())]
880 for p in self._meaningful_parentrevs(ctx)]
880 for p in self._meaningful_parentrevs(ctx)]
881 return showlist('parent', parents, **args)
881 return showlist('parent', parents, **args)
882
882
883 props = props.copy()
883 props = props.copy()
884 props.update(templatekw.keywords)
884 props.update(templatekw.keywords)
885 props['parents'] = showparents
885 props['parents'] = showparents
886 props['templ'] = self.t
886 props['templ'] = self.t
887 props['ctx'] = ctx
887 props['ctx'] = ctx
888 props['repo'] = self.repo
888 props['repo'] = self.repo
889 props['revcache'] = {'copies': copies}
889 props['revcache'] = {'copies': copies}
890 props['cache'] = self.cache
890 props['cache'] = self.cache
891
891
892 # find correct templates for current mode
892 # find correct templates for current mode
893
893
894 tmplmodes = [
894 tmplmodes = [
895 (True, None),
895 (True, None),
896 (self.ui.verbose, 'verbose'),
896 (self.ui.verbose, 'verbose'),
897 (self.ui.quiet, 'quiet'),
897 (self.ui.quiet, 'quiet'),
898 (self.ui.debugflag, 'debug'),
898 (self.ui.debugflag, 'debug'),
899 ]
899 ]
900
900
901 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
901 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
902 for mode, postfix in tmplmodes:
902 for mode, postfix in tmplmodes:
903 for type in types:
903 for type in types:
904 cur = postfix and ('%s_%s' % (type, postfix)) or type
904 cur = postfix and ('%s_%s' % (type, postfix)) or type
905 if mode and cur in self.t:
905 if mode and cur in self.t:
906 types[type] = cur
906 types[type] = cur
907
907
908 try:
908 try:
909
909
910 # write header
910 # write header
911 if types['header']:
911 if types['header']:
912 h = templater.stringify(self.t(types['header'], **props))
912 h = templater.stringify(self.t(types['header'], **props))
913 if self.buffered:
913 if self.buffered:
914 self.header[ctx.rev()] = h
914 self.header[ctx.rev()] = h
915 else:
915 else:
916 self.ui.write(h)
916 self.ui.write(h)
917
917
918 # write changeset metadata, then patch if requested
918 # write changeset metadata, then patch if requested
919 key = types['changeset']
919 key = types['changeset']
920 self.ui.write(templater.stringify(self.t(key, **props)))
920 self.ui.write(templater.stringify(self.t(key, **props)))
921 self.showpatch(ctx.node())
921 self.showpatch(ctx.node())
922
922
923 if types['footer']:
923 if types['footer']:
924 if not self.footer:
924 if not self.footer:
925 self.footer = templater.stringify(self.t(types['footer'],
925 self.footer = templater.stringify(self.t(types['footer'],
926 **props))
926 **props))
927
927
928 except KeyError, inst:
928 except KeyError, inst:
929 msg = _("%s: no key named '%s'")
929 msg = _("%s: no key named '%s'")
930 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
930 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
931 except SyntaxError, inst:
931 except SyntaxError, inst:
932 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
932 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
933
933
934 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
934 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
935 """show one changeset using template or regular display.
935 """show one changeset using template or regular display.
936
936
937 Display format will be the first non-empty hit of:
937 Display format will be the first non-empty hit of:
938 1. option 'template'
938 1. option 'template'
939 2. option 'style'
939 2. option 'style'
940 3. [ui] setting 'logtemplate'
940 3. [ui] setting 'logtemplate'
941 4. [ui] setting 'style'
941 4. [ui] setting 'style'
942 If all of these values are either the unset or the empty string,
942 If all of these values are either the unset or the empty string,
943 regular display via changeset_printer() is done.
943 regular display via changeset_printer() is done.
944 """
944 """
945 # options
945 # options
946 patch = False
946 patch = False
947 if opts.get('patch'):
947 if opts.get('patch'):
948 patch = matchfn or matchall(repo)
948 patch = matchfn or matchall(repo)
949
949
950 tmpl = opts.get('template')
950 tmpl = opts.get('template')
951 style = None
951 style = None
952 if tmpl:
952 if tmpl:
953 tmpl = templater.parsestring(tmpl, quoted=False)
953 tmpl = templater.parsestring(tmpl, quoted=False)
954 else:
954 else:
955 style = opts.get('style')
955 style = opts.get('style')
956
956
957 # ui settings
957 # ui settings
958 if not (tmpl or style):
958 if not (tmpl or style):
959 tmpl = ui.config('ui', 'logtemplate')
959 tmpl = ui.config('ui', 'logtemplate')
960 if tmpl:
960 if tmpl:
961 tmpl = templater.parsestring(tmpl)
961 tmpl = templater.parsestring(tmpl)
962 else:
962 else:
963 style = util.expandpath(ui.config('ui', 'style', ''))
963 style = util.expandpath(ui.config('ui', 'style', ''))
964
964
965 if not (tmpl or style):
965 if not (tmpl or style):
966 return changeset_printer(ui, repo, patch, opts, buffered)
966 return changeset_printer(ui, repo, patch, opts, buffered)
967
967
968 mapfile = None
968 mapfile = None
969 if style and not tmpl:
969 if style and not tmpl:
970 mapfile = style
970 mapfile = style
971 if not os.path.split(mapfile)[0]:
971 if not os.path.split(mapfile)[0]:
972 mapname = (templater.templatepath('map-cmdline.' + mapfile)
972 mapname = (templater.templatepath('map-cmdline.' + mapfile)
973 or templater.templatepath(mapfile))
973 or templater.templatepath(mapfile))
974 if mapname:
974 if mapname:
975 mapfile = mapname
975 mapfile = mapname
976
976
977 try:
977 try:
978 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
978 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
979 except SyntaxError, inst:
979 except SyntaxError, inst:
980 raise util.Abort(inst.args[0])
980 raise util.Abort(inst.args[0])
981 if tmpl:
981 if tmpl:
982 t.use_template(tmpl)
982 t.use_template(tmpl)
983 return t
983 return t
984
984
985 def finddate(ui, repo, date):
985 def finddate(ui, repo, date):
986 """Find the tipmost changeset that matches the given date spec"""
986 """Find the tipmost changeset that matches the given date spec"""
987
987
988 df = util.matchdate(date)
988 df = util.matchdate(date)
989 m = matchall(repo)
989 m = matchall(repo)
990 results = {}
990 results = {}
991
991
992 def prep(ctx, fns):
992 def prep(ctx, fns):
993 d = ctx.date()
993 d = ctx.date()
994 if df(d[0]):
994 if df(d[0]):
995 results[ctx.rev()] = d
995 results[ctx.rev()] = d
996
996
997 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
997 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
998 rev = ctx.rev()
998 rev = ctx.rev()
999 if rev in results:
999 if rev in results:
1000 ui.status(_("Found revision %s from %s\n") %
1000 ui.status(_("Found revision %s from %s\n") %
1001 (rev, util.datestr(results[rev])))
1001 (rev, util.datestr(results[rev])))
1002 return str(rev)
1002 return str(rev)
1003
1003
1004 raise util.Abort(_("revision matching date not found"))
1004 raise util.Abort(_("revision matching date not found"))
1005
1005
1006 def walkchangerevs(repo, match, opts, prepare):
1006 def walkchangerevs(repo, match, opts, prepare):
1007 '''Iterate over files and the revs in which they changed.
1007 '''Iterate over files and the revs in which they changed.
1008
1008
1009 Callers most commonly need to iterate backwards over the history
1009 Callers most commonly need to iterate backwards over the history
1010 in which they are interested. Doing so has awful (quadratic-looking)
1010 in which they are interested. Doing so has awful (quadratic-looking)
1011 performance, so we use iterators in a "windowed" way.
1011 performance, so we use iterators in a "windowed" way.
1012
1012
1013 We walk a window of revisions in the desired order. Within the
1013 We walk a window of revisions in the desired order. Within the
1014 window, we first walk forwards to gather data, then in the desired
1014 window, we first walk forwards to gather data, then in the desired
1015 order (usually backwards) to display it.
1015 order (usually backwards) to display it.
1016
1016
1017 This function returns an iterator yielding contexts. Before
1017 This function returns an iterator yielding contexts. Before
1018 yielding each context, the iterator will first call the prepare
1018 yielding each context, the iterator will first call the prepare
1019 function on each context in the window in forward order.'''
1019 function on each context in the window in forward order.'''
1020
1020
1021 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1021 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1022 if start < end:
1022 if start < end:
1023 while start < end:
1023 while start < end:
1024 yield start, min(windowsize, end - start)
1024 yield start, min(windowsize, end - start)
1025 start += windowsize
1025 start += windowsize
1026 if windowsize < sizelimit:
1026 if windowsize < sizelimit:
1027 windowsize *= 2
1027 windowsize *= 2
1028 else:
1028 else:
1029 while start > end:
1029 while start > end:
1030 yield start, min(windowsize, start - end - 1)
1030 yield start, min(windowsize, start - end - 1)
1031 start -= windowsize
1031 start -= windowsize
1032 if windowsize < sizelimit:
1032 if windowsize < sizelimit:
1033 windowsize *= 2
1033 windowsize *= 2
1034
1034
1035 follow = opts.get('follow') or opts.get('follow_first')
1035 follow = opts.get('follow') or opts.get('follow_first')
1036
1036
1037 if not len(repo):
1037 if not len(repo):
1038 return []
1038 return []
1039
1039
1040 if follow:
1040 if follow:
1041 defrange = '%s:0' % repo['.'].rev()
1041 defrange = '%s:0' % repo['.'].rev()
1042 else:
1042 else:
1043 defrange = '-1:0'
1043 defrange = '-1:0'
1044 revs = revrange(repo, opts['rev'] or [defrange])
1044 revs = revrange(repo, opts['rev'] or [defrange])
1045 wanted = set()
1045 wanted = set()
1046 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1046 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1047 fncache = {}
1047 fncache = {}
1048 change = util.cachefunc(repo.changectx)
1048 change = util.cachefunc(repo.changectx)
1049
1049
1050 if not slowpath and not match.files():
1050 if not slowpath and not match.files():
1051 # No files, no patterns. Display all revs.
1051 # No files, no patterns. Display all revs.
1052 wanted = set(revs)
1052 wanted = set(revs)
1053 copies = []
1053 copies = []
1054
1054
1055 if not slowpath:
1055 if not slowpath:
1056 # Only files, no patterns. Check the history of each file.
1056 # Only files, no patterns. Check the history of each file.
1057 def filerevgen(filelog, node):
1057 def filerevgen(filelog, node):
1058 cl_count = len(repo)
1058 cl_count = len(repo)
1059 if node is None:
1059 if node is None:
1060 last = len(filelog) - 1
1060 last = len(filelog) - 1
1061 else:
1061 else:
1062 last = filelog.rev(node)
1062 last = filelog.rev(node)
1063 for i, window in increasing_windows(last, nullrev):
1063 for i, window in increasing_windows(last, nullrev):
1064 revs = []
1064 revs = []
1065 for j in xrange(i - window, i + 1):
1065 for j in xrange(i - window, i + 1):
1066 n = filelog.node(j)
1066 n = filelog.node(j)
1067 revs.append((filelog.linkrev(j),
1067 revs.append((filelog.linkrev(j),
1068 follow and filelog.renamed(n)))
1068 follow and filelog.renamed(n)))
1069 for rev in reversed(revs):
1069 for rev in reversed(revs):
1070 # only yield rev for which we have the changelog, it can
1070 # only yield rev for which we have the changelog, it can
1071 # happen while doing "hg log" during a pull or commit
1071 # happen while doing "hg log" during a pull or commit
1072 if rev[0] < cl_count:
1072 if rev[0] < cl_count:
1073 yield rev
1073 yield rev
1074 def iterfiles():
1074 def iterfiles():
1075 for filename in match.files():
1075 for filename in match.files():
1076 yield filename, None
1076 yield filename, None
1077 for filename_node in copies:
1077 for filename_node in copies:
1078 yield filename_node
1078 yield filename_node
1079 minrev, maxrev = min(revs), max(revs)
1079 minrev, maxrev = min(revs), max(revs)
1080 for file_, node in iterfiles():
1080 for file_, node in iterfiles():
1081 filelog = repo.file(file_)
1081 filelog = repo.file(file_)
1082 if not len(filelog):
1082 if not len(filelog):
1083 if node is None:
1083 if node is None:
1084 # A zero count may be a directory or deleted file, so
1084 # A zero count may be a directory or deleted file, so
1085 # try to find matching entries on the slow path.
1085 # try to find matching entries on the slow path.
1086 if follow:
1086 if follow:
1087 raise util.Abort(
1087 raise util.Abort(
1088 _('cannot follow nonexistent file: "%s"') % file_)
1088 _('cannot follow nonexistent file: "%s"') % file_)
1089 slowpath = True
1089 slowpath = True
1090 break
1090 break
1091 else:
1091 else:
1092 continue
1092 continue
1093 for rev, copied in filerevgen(filelog, node):
1093 for rev, copied in filerevgen(filelog, node):
1094 if rev <= maxrev:
1094 if rev <= maxrev:
1095 if rev < minrev:
1095 if rev < minrev:
1096 break
1096 break
1097 fncache.setdefault(rev, [])
1097 fncache.setdefault(rev, [])
1098 fncache[rev].append(file_)
1098 fncache[rev].append(file_)
1099 wanted.add(rev)
1099 wanted.add(rev)
1100 if follow and copied:
1100 if follow and copied:
1101 copies.append(copied)
1101 copies.append(copied)
1102 if slowpath:
1102 if slowpath:
1103 if follow:
1103 if follow:
1104 raise util.Abort(_('can only follow copies/renames for explicit '
1104 raise util.Abort(_('can only follow copies/renames for explicit '
1105 'filenames'))
1105 'filenames'))
1106
1106
1107 # The slow path checks files modified in every changeset.
1107 # The slow path checks files modified in every changeset.
1108 def changerevgen():
1108 def changerevgen():
1109 for i, window in increasing_windows(len(repo) - 1, nullrev):
1109 for i, window in increasing_windows(len(repo) - 1, nullrev):
1110 for j in xrange(i - window, i + 1):
1110 for j in xrange(i - window, i + 1):
1111 yield change(j)
1111 yield change(j)
1112
1112
1113 for ctx in changerevgen():
1113 for ctx in changerevgen():
1114 matches = filter(match, ctx.files())
1114 matches = filter(match, ctx.files())
1115 if matches:
1115 if matches:
1116 fncache[ctx.rev()] = matches
1116 fncache[ctx.rev()] = matches
1117 wanted.add(ctx.rev())
1117 wanted.add(ctx.rev())
1118
1118
1119 class followfilter(object):
1119 class followfilter(object):
1120 def __init__(self, onlyfirst=False):
1120 def __init__(self, onlyfirst=False):
1121 self.startrev = nullrev
1121 self.startrev = nullrev
1122 self.roots = set()
1122 self.roots = set()
1123 self.onlyfirst = onlyfirst
1123 self.onlyfirst = onlyfirst
1124
1124
1125 def match(self, rev):
1125 def match(self, rev):
1126 def realparents(rev):
1126 def realparents(rev):
1127 if self.onlyfirst:
1127 if self.onlyfirst:
1128 return repo.changelog.parentrevs(rev)[0:1]
1128 return repo.changelog.parentrevs(rev)[0:1]
1129 else:
1129 else:
1130 return filter(lambda x: x != nullrev,
1130 return filter(lambda x: x != nullrev,
1131 repo.changelog.parentrevs(rev))
1131 repo.changelog.parentrevs(rev))
1132
1132
1133 if self.startrev == nullrev:
1133 if self.startrev == nullrev:
1134 self.startrev = rev
1134 self.startrev = rev
1135 return True
1135 return True
1136
1136
1137 if rev > self.startrev:
1137 if rev > self.startrev:
1138 # forward: all descendants
1138 # forward: all descendants
1139 if not self.roots:
1139 if not self.roots:
1140 self.roots.add(self.startrev)
1140 self.roots.add(self.startrev)
1141 for parent in realparents(rev):
1141 for parent in realparents(rev):
1142 if parent in self.roots:
1142 if parent in self.roots:
1143 self.roots.add(rev)
1143 self.roots.add(rev)
1144 return True
1144 return True
1145 else:
1145 else:
1146 # backwards: all parents
1146 # backwards: all parents
1147 if not self.roots:
1147 if not self.roots:
1148 self.roots.update(realparents(self.startrev))
1148 self.roots.update(realparents(self.startrev))
1149 if rev in self.roots:
1149 if rev in self.roots:
1150 self.roots.remove(rev)
1150 self.roots.remove(rev)
1151 self.roots.update(realparents(rev))
1151 self.roots.update(realparents(rev))
1152 return True
1152 return True
1153
1153
1154 return False
1154 return False
1155
1155
1156 # it might be worthwhile to do this in the iterator if the rev range
1156 # it might be worthwhile to do this in the iterator if the rev range
1157 # is descending and the prune args are all within that range
1157 # is descending and the prune args are all within that range
1158 for rev in opts.get('prune', ()):
1158 for rev in opts.get('prune', ()):
1159 rev = repo.changelog.rev(repo.lookup(rev))
1159 rev = repo.changelog.rev(repo.lookup(rev))
1160 ff = followfilter()
1160 ff = followfilter()
1161 stop = min(revs[0], revs[-1])
1161 stop = min(revs[0], revs[-1])
1162 for x in xrange(rev, stop - 1, -1):
1162 for x in xrange(rev, stop - 1, -1):
1163 if ff.match(x):
1163 if ff.match(x):
1164 wanted.discard(x)
1164 wanted.discard(x)
1165
1165
1166 def iterate():
1166 def iterate():
1167 if follow and not match.files():
1167 if follow and not match.files():
1168 ff = followfilter(onlyfirst=opts.get('follow_first'))
1168 ff = followfilter(onlyfirst=opts.get('follow_first'))
1169 def want(rev):
1169 def want(rev):
1170 return ff.match(rev) and rev in wanted
1170 return ff.match(rev) and rev in wanted
1171 else:
1171 else:
1172 def want(rev):
1172 def want(rev):
1173 return rev in wanted
1173 return rev in wanted
1174
1174
1175 for i, window in increasing_windows(0, len(revs)):
1175 for i, window in increasing_windows(0, len(revs)):
1176 change = util.cachefunc(repo.changectx)
1176 change = util.cachefunc(repo.changectx)
1177 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1177 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1178 for rev in sorted(nrevs):
1178 for rev in sorted(nrevs):
1179 fns = fncache.get(rev)
1179 fns = fncache.get(rev)
1180 ctx = change(rev)
1180 ctx = change(rev)
1181 if not fns:
1181 if not fns:
1182 def fns_generator():
1182 def fns_generator():
1183 for f in ctx.files():
1183 for f in ctx.files():
1184 if match(f):
1184 if match(f):
1185 yield f
1185 yield f
1186 fns = fns_generator()
1186 fns = fns_generator()
1187 prepare(ctx, fns)
1187 prepare(ctx, fns)
1188 for rev in nrevs:
1188 for rev in nrevs:
1189 yield change(rev)
1189 yield change(rev)
1190 return iterate()
1190 return iterate()
1191
1191
1192 def commit(ui, repo, commitfunc, pats, opts):
1192 def commit(ui, repo, commitfunc, pats, opts):
1193 '''commit the specified files or all outstanding changes'''
1193 '''commit the specified files or all outstanding changes'''
1194 date = opts.get('date')
1194 date = opts.get('date')
1195 if date:
1195 if date:
1196 opts['date'] = util.parsedate(date)
1196 opts['date'] = util.parsedate(date)
1197 message = logmessage(opts)
1197 message = logmessage(opts)
1198
1198
1199 # extract addremove carefully -- this function can be called from a command
1199 # extract addremove carefully -- this function can be called from a command
1200 # that doesn't support addremove
1200 # that doesn't support addremove
1201 if opts.get('addremove'):
1201 if opts.get('addremove'):
1202 addremove(repo, pats, opts)
1202 addremove(repo, pats, opts)
1203
1203
1204 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1204 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1205
1205
1206 def commiteditor(repo, ctx, subs):
1206 def commiteditor(repo, ctx, subs):
1207 if ctx.description():
1207 if ctx.description():
1208 return ctx.description()
1208 return ctx.description()
1209 return commitforceeditor(repo, ctx, subs)
1209 return commitforceeditor(repo, ctx, subs)
1210
1210
1211 def commitforceeditor(repo, ctx, subs):
1211 def commitforceeditor(repo, ctx, subs):
1212 edittext = []
1212 edittext = []
1213 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1213 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1214 if ctx.description():
1214 if ctx.description():
1215 edittext.append(ctx.description())
1215 edittext.append(ctx.description())
1216 edittext.append("")
1216 edittext.append("")
1217 edittext.append("") # Empty line between message and comments.
1217 edittext.append("") # Empty line between message and comments.
1218 edittext.append(_("HG: Enter commit message."
1218 edittext.append(_("HG: Enter commit message."
1219 " Lines beginning with 'HG:' are removed."))
1219 " Lines beginning with 'HG:' are removed."))
1220 edittext.append(_("HG: Leave message empty to abort commit."))
1220 edittext.append(_("HG: Leave message empty to abort commit."))
1221 edittext.append("HG: --")
1221 edittext.append("HG: --")
1222 edittext.append(_("HG: user: %s") % ctx.user())
1222 edittext.append(_("HG: user: %s") % ctx.user())
1223 if ctx.p2():
1223 if ctx.p2():
1224 edittext.append(_("HG: branch merge"))
1224 edittext.append(_("HG: branch merge"))
1225 if ctx.branch():
1225 if ctx.branch():
1226 edittext.append(_("HG: branch '%s'")
1226 edittext.append(_("HG: branch '%s'")
1227 % encoding.tolocal(ctx.branch()))
1227 % encoding.tolocal(ctx.branch()))
1228 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1228 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1229 edittext.extend([_("HG: added %s") % f for f in added])
1229 edittext.extend([_("HG: added %s") % f for f in added])
1230 edittext.extend([_("HG: changed %s") % f for f in modified])
1230 edittext.extend([_("HG: changed %s") % f for f in modified])
1231 edittext.extend([_("HG: removed %s") % f for f in removed])
1231 edittext.extend([_("HG: removed %s") % f for f in removed])
1232 if not added and not modified and not removed:
1232 if not added and not modified and not removed:
1233 edittext.append(_("HG: no files changed"))
1233 edittext.append(_("HG: no files changed"))
1234 edittext.append("")
1234 edittext.append("")
1235 # run editor in the repository root
1235 # run editor in the repository root
1236 olddir = os.getcwd()
1236 olddir = os.getcwd()
1237 os.chdir(repo.root)
1237 os.chdir(repo.root)
1238 text = repo.ui.edit("\n".join(edittext), ctx.user())
1238 text = repo.ui.edit("\n".join(edittext), ctx.user())
1239 text = re.sub("(?m)^HG:.*\n", "", text)
1239 text = re.sub("(?m)^HG:.*\n", "", text)
1240 os.chdir(olddir)
1240 os.chdir(olddir)
1241
1241
1242 if not text.strip():
1242 if not text.strip():
1243 raise util.Abort(_("empty commit message"))
1243 raise util.Abort(_("empty commit message"))
1244
1244
1245 return text
1245 return text
General Comments 0
You need to be logged in to leave comments. Login now