Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/commands.py @ 1146:9061f79c6c6f
grep: extend functionality, add man page entry, add unit test.
walkchangerevs now returns a two-tuple. Its behaviour is also
extensively commented.
The annotate command's getname function has been factored out to a new
function, trimname, so it can be shared between annotate and grep.
The behaviour of grep has been beefed up, so that it now performs a
number of useful functions.
author | bos@serpentine.internal.keyresearch.com |
---|---|
date | Mon, 29 Aug 2005 10:05:49 -0700 |
parents | bd917e1a26dd |
children | d32b91ebad5d |
comparison
equal
deleted
inserted
replaced
1145:bd917e1a26dd | 1146:9061f79c6c6f |
---|---|
47 files, matchfn, results = makewalk(repo, pats, opts, head) | 47 files, matchfn, results = makewalk(repo, pats, opts, head) |
48 for r in results: | 48 for r in results: |
49 yield r | 49 yield r |
50 | 50 |
51 def walkchangerevs(ui, repo, cwd, pats, opts): | 51 def walkchangerevs(ui, repo, cwd, pats, opts): |
52 # This code most commonly needs to iterate backwards over the | 52 '''Iterate over files and the revs they changed in. |
53 # history it is interested in. Doing so has awful | 53 |
54 # (quadratic-looking) performance, so we use iterators in a | 54 Callers most commonly need to iterate backwards over the history |
55 # "windowed" way. Walk forwards through a window of revisions, | 55 it is interested in. Doing so has awful (quadratic-looking) |
56 # yielding them in the desired order, and walk the windows | 56 performance, so we use iterators in a "windowed" way. |
57 # themselves backwards. | 57 |
58 We walk a window of revisions in the desired order. Within the | |
59 window, we first walk forwards to gather data, then in the desired | |
60 order (usually backwards) to display it. | |
61 | |
62 This function returns an (iterator, getchange) pair. The | |
63 getchange function returns the changelog entry for a numeric | |
64 revision. The iterator yields 3-tuples. They will be of one of | |
65 the following forms: | |
66 | |
67 "window", incrementing, lastrev: stepping through a window, | |
68 positive if walking forwards through revs, last rev in the | |
69 sequence iterated over - use to reset state for the current window | |
70 | |
71 "add", rev, fns: out-of-order traversal of the given file names | |
72 fns, which changed during revision rev - use to gather data for | |
73 possible display | |
74 | |
75 "iter", rev, None: in-order traversal of the revs earlier iterated | |
76 over with "add" - use to display data''' | |
58 cwd = repo.getcwd() | 77 cwd = repo.getcwd() |
59 if not pats and cwd: | 78 if not pats and cwd: |
60 opts['include'] = [os.path.join(cwd, i) for i in opts['include']] | 79 opts['include'] = [os.path.join(cwd, i) for i in opts['include']] |
61 opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']] | 80 opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']] |
62 files, matchfn, anypats = matchpats(repo, (pats and cwd) or '', | 81 files, matchfn, anypats = matchpats(repo, (pats and cwd) or '', |
64 revs = map(int, revrange(ui, repo, opts['rev'] or ['tip:0'])) | 83 revs = map(int, revrange(ui, repo, opts['rev'] or ['tip:0'])) |
65 wanted = {} | 84 wanted = {} |
66 slowpath = anypats | 85 slowpath = anypats |
67 window = 300 | 86 window = 300 |
68 fncache = {} | 87 fncache = {} |
88 | |
89 chcache = {} | |
90 def getchange(rev): | |
91 ch = chcache.get(rev) | |
92 if ch is None: | |
93 chcache[rev] = ch = repo.changelog.read(repo.lookup(str(rev))) | |
94 return ch | |
95 | |
69 if not slowpath and not files: | 96 if not slowpath and not files: |
70 # No files, no patterns. Display all revs. | 97 # No files, no patterns. Display all revs. |
71 wanted = dict(zip(revs, revs)) | 98 wanted = dict(zip(revs, revs)) |
72 if not slowpath: | 99 if not slowpath: |
73 # Only files, no patterns. Check the history of each file. | 100 # Only files, no patterns. Check the history of each file. |
98 if slowpath: | 125 if slowpath: |
99 # The slow path checks files modified in every changeset. | 126 # The slow path checks files modified in every changeset. |
100 def changerevgen(): | 127 def changerevgen(): |
101 for i in xrange(repo.changelog.count() - 1, -1, -window): | 128 for i in xrange(repo.changelog.count() - 1, -1, -window): |
102 for j in xrange(max(0, i - window), i + 1): | 129 for j in xrange(max(0, i - window), i + 1): |
103 yield j, repo.changelog.read(repo.lookup(str(j)))[3] | 130 yield j, getchange(j)[3] |
104 | 131 |
105 for rev, changefiles in changerevgen(): | 132 for rev, changefiles in changerevgen(): |
106 matches = filter(matchfn, changefiles) | 133 matches = filter(matchfn, changefiles) |
107 if matches: | 134 if matches: |
108 fncache[rev] = matches | 135 fncache[rev] = matches |
109 wanted[rev] = 1 | 136 wanted[rev] = 1 |
110 | 137 |
111 for i in xrange(0, len(revs), window): | 138 def iterate(): |
112 yield 'window', revs[0] < revs[-1], revs[-1] | 139 for i in xrange(0, len(revs), window): |
113 nrevs = [rev for rev in revs[i:min(i+window, len(revs))] | 140 yield 'window', revs[0] < revs[-1], revs[-1] |
114 if rev in wanted] | 141 nrevs = [rev for rev in revs[i:min(i+window, len(revs))] |
115 srevs = list(nrevs) | 142 if rev in wanted] |
116 srevs.sort() | 143 srevs = list(nrevs) |
117 for rev in srevs: | 144 srevs.sort() |
118 fns = fncache.get(rev) | 145 for rev in srevs: |
119 if not fns: | 146 fns = fncache.get(rev) or filter(matchfn, getchange(rev)[3]) |
120 fns = repo.changelog.read(repo.lookup(str(rev)))[3] | 147 yield 'add', rev, fns |
121 fns = filter(matchfn, fns) | 148 for rev in nrevs: |
122 yield 'add', rev, fns | 149 yield 'iter', rev, None |
123 for rev in nrevs: | 150 return iterate(), getchange |
124 yield 'iter', rev, None | |
125 | 151 |
126 revrangesep = ':' | 152 revrangesep = ':' |
127 | 153 |
128 def revrange(ui, repo, revs, revlog=None): | 154 def revrange(ui, repo, revs, revlog=None): |
129 """Yield revision as strings from a list of revision specifications.""" | 155 """Yield revision as strings from a list of revision specifications.""" |
148 try: | 174 try: |
149 num = revlog.rev(revlog.lookup(val)) | 175 num = revlog.rev(revlog.lookup(val)) |
150 except KeyError: | 176 except KeyError: |
151 raise util.Abort('invalid revision identifier %s', val) | 177 raise util.Abort('invalid revision identifier %s', val) |
152 return num | 178 return num |
179 seen = {} | |
153 for spec in revs: | 180 for spec in revs: |
154 if spec.find(revrangesep) >= 0: | 181 if spec.find(revrangesep) >= 0: |
155 start, end = spec.split(revrangesep, 1) | 182 start, end = spec.split(revrangesep, 1) |
156 start = fix(start, 0) | 183 start = fix(start, 0) |
157 end = fix(end, revcount - 1) | 184 end = fix(end, revcount - 1) |
158 step = start > end and -1 or 1 | 185 step = start > end and -1 or 1 |
159 for rev in xrange(start, end+step, step): | 186 for rev in xrange(start, end+step, step): |
187 if rev in seen: continue | |
188 seen[rev] = 1 | |
160 yield str(rev) | 189 yield str(rev) |
161 else: | 190 else: |
162 yield str(fix(spec, None)) | 191 rev = fix(spec, None) |
192 if rev in seen: continue | |
193 seen[rev] = 1 | |
194 yield str(rev) | |
163 | 195 |
164 def make_filename(repo, r, pat, node=None, | 196 def make_filename(repo, r, pat, node=None, |
165 total=None, seqno=None, revwidth=None): | 197 total=None, seqno=None, revwidth=None): |
166 node_expander = { | 198 node_expander = { |
167 'H': lambda: hex(node), | 199 'H': lambda: hex(node), |
263 for f in d: | 295 for f in d: |
264 to = repo.file(f).read(mmap[f]) | 296 to = repo.file(f).read(mmap[f]) |
265 tn = None | 297 tn = None |
266 fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text)) | 298 fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text)) |
267 | 299 |
300 def trimuser(ui, rev, name, revcache): | |
301 """trim the name of the user who committed a change""" | |
302 try: | |
303 return revcache[rev] | |
304 except KeyError: | |
305 if not ui.verbose: | |
306 f = name.find('@') | |
307 if f >= 0: | |
308 name = name[:f] | |
309 f = name.find('<') | |
310 if f >= 0: | |
311 name = name[f+1:] | |
312 revcache[rev] = name | |
313 return name | |
314 | |
268 def show_changeset(ui, repo, rev=0, changenode=None, brinfo=None): | 315 def show_changeset(ui, repo, rev=0, changenode=None, brinfo=None): |
269 """show a single changeset or file revision""" | 316 """show a single changeset or file revision""" |
270 log = repo.changelog | 317 log = repo.changelog |
271 if changenode is None: | 318 if changenode is None: |
272 changenode = log.node(rev) | 319 changenode = log.node(rev) |
465 def annotate(ui, repo, *pats, **opts): | 512 def annotate(ui, repo, *pats, **opts): |
466 """show changeset information per file line""" | 513 """show changeset information per file line""" |
467 def getnode(rev): | 514 def getnode(rev): |
468 return short(repo.changelog.node(rev)) | 515 return short(repo.changelog.node(rev)) |
469 | 516 |
517 ucache = {} | |
470 def getname(rev): | 518 def getname(rev): |
471 try: | 519 cl = repo.changelog.read(repo.changelog.node(rev)) |
472 return bcache[rev] | 520 return trimuser(ui, rev, cl[1], ucache) |
473 except KeyError: | |
474 cl = repo.changelog.read(repo.changelog.node(rev)) | |
475 name = cl[1] | |
476 f = name.find('@') | |
477 if f >= 0: | |
478 name = name[:f] | |
479 f = name.find('<') | |
480 if f >= 0: | |
481 name = name[f+1:] | |
482 bcache[rev] = name | |
483 return name | |
484 | 521 |
485 if not pats: | 522 if not pats: |
486 raise util.Abort('at least one file name or pattern required') | 523 raise util.Abort('at least one file name or pattern required') |
487 | 524 |
488 bcache = {} | |
489 opmap = [['user', getname], ['number', str], ['changeset', getnode]] | 525 opmap = [['user', getname], ['number', str], ['changeset', getnode]] |
490 if not opts['user'] and not opts['changeset']: | 526 if not opts['user'] and not opts['changeset']: |
491 opts['number'] = 1 | 527 opts['number'] = 1 |
492 | 528 |
493 if opts['rev']: | 529 if opts['rev']: |
824 """search for a pattern in specified files and revisions""" | 860 """search for a pattern in specified files and revisions""" |
825 reflags = 0 | 861 reflags = 0 |
826 if opts['ignore_case']: | 862 if opts['ignore_case']: |
827 reflags |= re.I | 863 reflags |= re.I |
828 regexp = re.compile(pattern, reflags) | 864 regexp = re.compile(pattern, reflags) |
829 sep, end = ':', '\n' | 865 sep, eol = ':', '\n' |
830 if opts['print0']: | 866 if opts['print0']: |
831 sep = end = '\0' | 867 sep = eol = '\0' |
832 | 868 |
833 fcache = {} | 869 fcache = {} |
834 def getfile(fn): | 870 def getfile(fn): |
835 if fn not in fcache: | 871 if fn not in fcache: |
836 fcache[fn] = repo.file(fn) | 872 fcache[fn] = repo.file(fn) |
868 for lnum, cstart, cend, line in matchlines(body): | 904 for lnum, cstart, cend, line in matchlines(body): |
869 s = linestate(line, lnum, cstart, cend) | 905 s = linestate(line, lnum, cstart, cend) |
870 m[s] = s | 906 m[s] = s |
871 | 907 |
872 prev = {} | 908 prev = {} |
909 ucache = {} | |
873 def display(fn, rev, states, prevstates): | 910 def display(fn, rev, states, prevstates): |
874 diff = list(sets.Set(states).symmetric_difference(sets.Set(prevstates))) | 911 diff = list(sets.Set(states).symmetric_difference(sets.Set(prevstates))) |
875 diff.sort(lambda x, y: cmp(x.linenum, y.linenum)) | 912 diff.sort(lambda x, y: cmp(x.linenum, y.linenum)) |
876 counts = {'-': 0, '+': 0} | 913 counts = {'-': 0, '+': 0} |
914 filerevmatches = {} | |
877 for l in diff: | 915 for l in diff: |
878 if incrementing or not opts['every_match']: | 916 if incrementing or not opts['every_match']: |
879 change = ((l in prevstates) and '-') or '+' | 917 change = ((l in prevstates) and '-') or '+' |
880 r = rev | 918 r = rev |
881 else: | 919 else: |
882 change = ((l in states) and '-') or '+' | 920 change = ((l in states) and '-') or '+' |
883 r = prev[fn] | 921 r = prev[fn] |
884 ui.write('%s:%s:%s:%s%s\n' % (fn, r, l.linenum, change, l.line)) | 922 cols = [fn, str(rev)] |
923 if opts['line_number']: cols.append(str(l.linenum)) | |
924 if opts['every_match']: cols.append(change) | |
925 if opts['user']: cols.append(trimuser(ui, rev, getchange(rev)[1], | |
926 ucache)) | |
927 if opts['files_with_matches']: | |
928 c = (fn, rev) | |
929 if c in filerevmatches: continue | |
930 filerevmatches[c] = 1 | |
931 else: | |
932 cols.append(l.line) | |
933 ui.write(sep.join(cols), eol) | |
885 counts[change] += 1 | 934 counts[change] += 1 |
886 return counts['+'], counts['-'] | 935 return counts['+'], counts['-'] |
887 | 936 |
888 fstate = {} | 937 fstate = {} |
889 skip = {} | 938 skip = {} |
890 for st, rev, fns in walkchangerevs(ui, repo, repo.getcwd(), pats, opts): | 939 changeiter, getchange = walkchangerevs(ui, repo, repo.getcwd(), pats, opts) |
940 count = 0 | |
941 for st, rev, fns in changeiter: | |
891 if st == 'window': | 942 if st == 'window': |
892 incrementing = rev | 943 incrementing = rev |
893 matches.clear() | 944 matches.clear() |
894 elif st == 'add': | 945 elif st == 'add': |
895 change = repo.changelog.read(repo.lookup(str(rev))) | 946 change = repo.changelog.read(repo.lookup(str(rev))) |
907 states.sort() | 958 states.sort() |
908 for fn, m in states: | 959 for fn, m in states: |
909 if fn in skip: continue | 960 if fn in skip: continue |
910 if incrementing or not opts['every_match'] or fstate[fn]: | 961 if incrementing or not opts['every_match'] or fstate[fn]: |
911 pos, neg = display(fn, rev, m, fstate[fn]) | 962 pos, neg = display(fn, rev, m, fstate[fn]) |
963 count += pos + neg | |
912 if pos and not opts['every_match']: | 964 if pos and not opts['every_match']: |
913 skip[fn] = True | 965 skip[fn] = True |
914 fstate[fn] = m | 966 fstate[fn] = m |
915 prev[fn] = rev | 967 prev[fn] = rev |
916 | 968 |
918 fstate = fstate.items() | 970 fstate = fstate.items() |
919 fstate.sort() | 971 fstate.sort() |
920 for fn, state in fstate: | 972 for fn, state in fstate: |
921 if fn in skip: continue | 973 if fn in skip: continue |
922 display(fn, rev, {}, state) | 974 display(fn, rev, {}, state) |
975 return (count == 0 and 1) or 0 | |
923 | 976 |
924 def heads(ui, repo, **opts): | 977 def heads(ui, repo, **opts): |
925 """show current repository heads""" | 978 """show current repository heads""" |
926 heads = repo.changelog.heads() | 979 heads = repo.changelog.heads() |
927 br = None | 980 br = None |
1071 return getattr(self.ui, key) | 1124 return getattr(self.ui, key) |
1072 cwd = repo.getcwd() | 1125 cwd = repo.getcwd() |
1073 if not pats and cwd: | 1126 if not pats and cwd: |
1074 opts['include'] = [os.path.join(cwd, i) for i in opts['include']] | 1127 opts['include'] = [os.path.join(cwd, i) for i in opts['include']] |
1075 opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']] | 1128 opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']] |
1076 for st, rev, fns in walkchangerevs(ui, repo, (pats and cwd) or '', pats, | 1129 changeiter, getchange = walkchangerevs(ui, repo, (pats and cwd) or '', |
1077 opts): | 1130 pats, opts) |
1131 for st, rev, fns in changeiter: | |
1078 if st == 'window': | 1132 if st == 'window': |
1079 du = dui(ui) | 1133 du = dui(ui) |
1080 elif st == 'add': | 1134 elif st == 'add': |
1081 du.bump(rev) | 1135 du.bump(rev) |
1082 show_changeset(du, repo, rev) | 1136 show_changeset(du, repo, rev) |
1569 [('I', 'include', [], 'include path in search'), | 1623 [('I', 'include', [], 'include path in search'), |
1570 ('X', 'exclude', [], 'exclude path from search')], | 1624 ('X', 'exclude', [], 'exclude path from search')], |
1571 "hg forget [OPTION]... FILE..."), | 1625 "hg forget [OPTION]... FILE..."), |
1572 "grep": | 1626 "grep": |
1573 (grep, | 1627 (grep, |
1574 [('0', 'print0', None, 'end filenames with NUL'), | 1628 [('0', 'print0', None, 'end fields with NUL'), |
1575 ('I', 'include', [], 'include path in search'), | 1629 ('I', 'include', [], 'include path in search'), |
1576 ('X', 'exclude', [], 'include path in search'), | 1630 ('X', 'exclude', [], 'include path in search'), |
1577 ('e', 'every-match', None, 'print every match in file history'), | 1631 ('e', 'every-match', None, 'print every rev with matches'), |
1578 ('i', 'ignore-case', None, 'ignore case when matching'), | 1632 ('i', 'ignore-case', None, 'ignore case when matching'), |
1579 ('l', 'files-with-matches', None, 'print names of files with matches'), | 1633 ('l', 'files-with-matches', None, 'print names of files and revs with matches'), |
1580 ('n', 'line-number', '', 'print line numbers'), | 1634 ('n', 'line-number', None, 'print line numbers'), |
1581 ('r', 'rev', [], 'search in revision rev')], | 1635 ('r', 'rev', [], 'search in revision rev'), |
1636 ('u', 'user', None, 'print user who made change')], | |
1582 "hg grep [OPTION]... PATTERN [FILE]..."), | 1637 "hg grep [OPTION]... PATTERN [FILE]..."), |
1583 "heads": | 1638 "heads": |
1584 (heads, | 1639 (heads, |
1585 [('b', 'branches', None, 'find branch info')], | 1640 [('b', 'branches', None, 'find branch info')], |
1586 'hg heads [-b]'), | 1641 'hg heads [-b]'), |