30 command = registrar.command(cmdtable) |
30 command = registrar.command(cmdtable) |
31 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
31 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
32 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
32 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
33 # be specifying the version(s) of Mercurial they are tested with, or |
33 # be specifying the version(s) of Mercurial they are tested with, or |
34 # leave the attribute unspecified. |
34 # leave the attribute unspecified. |
35 testedwith = 'ships-with-hg-core' |
35 testedwith = b'ships-with-hg-core' |
36 |
36 |
37 |
37 |
38 def changedlines(ui, repo, ctx1, ctx2, fns): |
38 def changedlines(ui, repo, ctx1, ctx2, fns): |
39 added, removed = 0, 0 |
39 added, removed = 0, 0 |
40 fmatch = scmutil.matchfiles(repo, fns) |
40 fmatch = scmutil.matchfiles(repo, fns) |
41 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch)) |
41 diff = b''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch)) |
42 for l in diff.split('\n'): |
42 for l in diff.split(b'\n'): |
43 if l.startswith("+") and not l.startswith("+++ "): |
43 if l.startswith(b"+") and not l.startswith(b"+++ "): |
44 added += 1 |
44 added += 1 |
45 elif l.startswith("-") and not l.startswith("--- "): |
45 elif l.startswith(b"-") and not l.startswith(b"--- "): |
46 removed += 1 |
46 removed += 1 |
47 return (added, removed) |
47 return (added, removed) |
48 |
48 |
49 |
49 |
50 def countrate(ui, repo, amap, *pats, **opts): |
50 def countrate(ui, repo, amap, *pats, **opts): |
51 """Calculate stats""" |
51 """Calculate stats""" |
52 opts = pycompat.byteskwargs(opts) |
52 opts = pycompat.byteskwargs(opts) |
53 if opts.get('dateformat'): |
53 if opts.get(b'dateformat'): |
54 |
54 |
55 def getkey(ctx): |
55 def getkey(ctx): |
56 t, tz = ctx.date() |
56 t, tz = ctx.date() |
57 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6]) |
57 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6]) |
58 return encoding.strtolocal( |
58 return encoding.strtolocal( |
59 date.strftime(encoding.strfromlocal(opts['dateformat'])) |
59 date.strftime(encoding.strfromlocal(opts[b'dateformat'])) |
60 ) |
60 ) |
61 |
61 |
62 else: |
62 else: |
63 tmpl = opts.get('oldtemplate') or opts.get('template') |
63 tmpl = opts.get(b'oldtemplate') or opts.get(b'template') |
64 tmpl = logcmdutil.maketemplater(ui, repo, tmpl) |
64 tmpl = logcmdutil.maketemplater(ui, repo, tmpl) |
65 |
65 |
66 def getkey(ctx): |
66 def getkey(ctx): |
67 ui.pushbuffer() |
67 ui.pushbuffer() |
68 tmpl.show(ctx) |
68 tmpl.show(ctx) |
69 return ui.popbuffer() |
69 return ui.popbuffer() |
70 |
70 |
71 progress = ui.makeprogress( |
71 progress = ui.makeprogress( |
72 _('analyzing'), unit=_('revisions'), total=len(repo) |
72 _(b'analyzing'), unit=_(b'revisions'), total=len(repo) |
73 ) |
73 ) |
74 rate = {} |
74 rate = {} |
75 df = False |
75 df = False |
76 if opts.get('date'): |
76 if opts.get(b'date'): |
77 df = dateutil.matchdate(opts['date']) |
77 df = dateutil.matchdate(opts[b'date']) |
78 |
78 |
79 m = scmutil.match(repo[None], pats, opts) |
79 m = scmutil.match(repo[None], pats, opts) |
80 |
80 |
81 def prep(ctx, fns): |
81 def prep(ctx, fns): |
82 rev = ctx.rev() |
82 rev = ctx.rev() |
83 if df and not df(ctx.date()[0]): # doesn't match date format |
83 if df and not df(ctx.date()[0]): # doesn't match date format |
84 return |
84 return |
85 |
85 |
86 key = getkey(ctx).strip() |
86 key = getkey(ctx).strip() |
87 key = amap.get(key, key) # alias remap |
87 key = amap.get(key, key) # alias remap |
88 if opts.get('changesets'): |
88 if opts.get(b'changesets'): |
89 rate[key] = (rate.get(key, (0,))[0] + 1, 0) |
89 rate[key] = (rate.get(key, (0,))[0] + 1, 0) |
90 else: |
90 else: |
91 parents = ctx.parents() |
91 parents = ctx.parents() |
92 if len(parents) > 1: |
92 if len(parents) > 1: |
93 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,)) |
93 ui.note(_(b'revision %d is a merge, ignoring...\n') % (rev,)) |
94 return |
94 return |
95 |
95 |
96 ctx1 = parents[0] |
96 ctx1 = parents[0] |
97 lines = changedlines(ui, repo, ctx1, ctx, fns) |
97 lines = changedlines(ui, repo, ctx1, ctx, fns) |
98 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)] |
98 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)] |
106 |
106 |
107 return rate |
107 return rate |
108 |
108 |
109 |
109 |
110 @command( |
110 @command( |
111 'churn', |
111 b'churn', |
112 [ |
112 [ |
113 ( |
113 ( |
114 'r', |
114 b'r', |
115 'rev', |
115 b'rev', |
116 [], |
116 [], |
117 _('count rate for the specified revision or revset'), |
117 _(b'count rate for the specified revision or revset'), |
118 _('REV'), |
118 _(b'REV'), |
119 ), |
119 ), |
120 ( |
120 ( |
121 'd', |
121 b'd', |
122 'date', |
122 b'date', |
123 '', |
123 b'', |
124 _('count rate for revisions matching date spec'), |
124 _(b'count rate for revisions matching date spec'), |
125 _('DATE'), |
125 _(b'DATE'), |
126 ), |
126 ), |
127 ( |
127 ( |
128 't', |
128 b't', |
129 'oldtemplate', |
129 b'oldtemplate', |
130 '', |
130 b'', |
131 _('template to group changesets (DEPRECATED)'), |
131 _(b'template to group changesets (DEPRECATED)'), |
132 _('TEMPLATE'), |
132 _(b'TEMPLATE'), |
133 ), |
133 ), |
134 ( |
134 ( |
135 'T', |
135 b'T', |
136 'template', |
136 b'template', |
137 '{author|email}', |
137 b'{author|email}', |
138 _('template to group changesets'), |
138 _(b'template to group changesets'), |
139 _('TEMPLATE'), |
139 _(b'TEMPLATE'), |
140 ), |
140 ), |
141 ( |
141 ( |
142 'f', |
142 b'f', |
143 'dateformat', |
143 b'dateformat', |
144 '', |
144 b'', |
145 _('strftime-compatible format for grouping by date'), |
145 _(b'strftime-compatible format for grouping by date'), |
146 _('FORMAT'), |
146 _(b'FORMAT'), |
147 ), |
147 ), |
148 ('c', 'changesets', False, _('count rate by number of changesets')), |
148 (b'c', b'changesets', False, _(b'count rate by number of changesets')), |
149 ('s', 'sort', False, _('sort by key (default: sort by count)')), |
149 (b's', b'sort', False, _(b'sort by key (default: sort by count)')), |
150 ('', 'diffstat', False, _('display added/removed lines separately')), |
150 (b'', b'diffstat', False, _(b'display added/removed lines separately')), |
151 ('', 'aliases', '', _('file with email aliases'), _('FILE')), |
151 (b'', b'aliases', b'', _(b'file with email aliases'), _(b'FILE')), |
152 ] |
152 ] |
153 + cmdutil.walkopts, |
153 + cmdutil.walkopts, |
154 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"), |
154 _(b"hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]"), |
155 helpcategory=command.CATEGORY_MAINTENANCE, |
155 helpcategory=command.CATEGORY_MAINTENANCE, |
156 inferrepo=True, |
156 inferrepo=True, |
157 ) |
157 ) |
158 def churn(ui, repo, *pats, **opts): |
158 def churn(ui, repo, *pats, **opts): |
159 '''histogram of changes to the repository |
159 '''histogram of changes to the repository |
191 a .hgchurn file will be looked for in the working directory root. |
191 a .hgchurn file will be looked for in the working directory root. |
192 Aliases will be split from the rightmost "=". |
192 Aliases will be split from the rightmost "=". |
193 ''' |
193 ''' |
194 |
194 |
195 def pad(s, l): |
195 def pad(s, l): |
196 return s + " " * (l - encoding.colwidth(s)) |
196 return s + b" " * (l - encoding.colwidth(s)) |
197 |
197 |
198 amap = {} |
198 amap = {} |
199 aliases = opts.get(r'aliases') |
199 aliases = opts.get(r'aliases') |
200 if not aliases and os.path.exists(repo.wjoin('.hgchurn')): |
200 if not aliases and os.path.exists(repo.wjoin(b'.hgchurn')): |
201 aliases = repo.wjoin('.hgchurn') |
201 aliases = repo.wjoin(b'.hgchurn') |
202 if aliases: |
202 if aliases: |
203 for l in open(aliases, "rb"): |
203 for l in open(aliases, b"rb"): |
204 try: |
204 try: |
205 alias, actual = l.rsplit('=' in l and '=' or None, 1) |
205 alias, actual = l.rsplit(b'=' in l and b'=' or None, 1) |
206 amap[alias.strip()] = actual.strip() |
206 amap[alias.strip()] = actual.strip() |
207 except ValueError: |
207 except ValueError: |
208 l = l.strip() |
208 l = l.strip() |
209 if l: |
209 if l: |
210 ui.warn(_("skipping malformed alias: %s\n") % l) |
210 ui.warn(_(b"skipping malformed alias: %s\n") % l) |
211 continue |
211 continue |
212 |
212 |
213 rate = list(countrate(ui, repo, amap, *pats, **opts).items()) |
213 rate = list(countrate(ui, repo, amap, *pats, **opts).items()) |
214 if not rate: |
214 if not rate: |
215 return |
215 return |
222 # Be careful not to have a zero maxcount (issue833) |
222 # Be careful not to have a zero maxcount (issue833) |
223 maxcount = float(max(sum(v) for k, v in rate)) or 1.0 |
223 maxcount = float(max(sum(v) for k, v in rate)) or 1.0 |
224 maxname = max(len(k) for k, v in rate) |
224 maxname = max(len(k) for k, v in rate) |
225 |
225 |
226 ttywidth = ui.termwidth() |
226 ttywidth = ui.termwidth() |
227 ui.debug("assuming %i character terminal\n" % ttywidth) |
227 ui.debug(b"assuming %i character terminal\n" % ttywidth) |
228 width = ttywidth - maxname - 2 - 2 - 2 |
228 width = ttywidth - maxname - 2 - 2 - 2 |
229 |
229 |
230 if opts.get(r'diffstat'): |
230 if opts.get(r'diffstat'): |
231 width -= 15 |
231 width -= 15 |
232 |
232 |
233 def format(name, diffstat): |
233 def format(name, diffstat): |
234 added, removed = diffstat |
234 added, removed = diffstat |
235 return "%s %15s %s%s\n" % ( |
235 return b"%s %15s %s%s\n" % ( |
236 pad(name, maxname), |
236 pad(name, maxname), |
237 '+%d/-%d' % (added, removed), |
237 b'+%d/-%d' % (added, removed), |
238 ui.label('+' * charnum(added), 'diffstat.inserted'), |
238 ui.label(b'+' * charnum(added), b'diffstat.inserted'), |
239 ui.label('-' * charnum(removed), 'diffstat.deleted'), |
239 ui.label(b'-' * charnum(removed), b'diffstat.deleted'), |
240 ) |
240 ) |
241 |
241 |
242 else: |
242 else: |
243 width -= 6 |
243 width -= 6 |
244 |
244 |
245 def format(name, count): |
245 def format(name, count): |
246 return "%s %6d %s\n" % ( |
246 return b"%s %6d %s\n" % ( |
247 pad(name, maxname), |
247 pad(name, maxname), |
248 sum(count), |
248 sum(count), |
249 '*' * charnum(sum(count)), |
249 b'*' * charnum(sum(count)), |
250 ) |
250 ) |
251 |
251 |
252 def charnum(count): |
252 def charnum(count): |
253 return int(count * width // maxcount) |
253 return int(count * width // maxcount) |
254 |
254 |