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]'),