##// END OF EJS Templates
grep: extend functionality, add man page entry, add unit test....
bos@serpentine.internal.keyresearch.com -
r1146:9061f79c default
parent child Browse files
Show More
@@ -0,0 +1,22 b''
1 #!/bin/sh
2
3 mkdir t
4 cd t
5 hg init
6 echo import > port
7 hg add port
8 hg commit -m 0 -u spam -d '0 0'
9 echo export >> port
10 hg commit -m 1 -u eggs -d '1 0'
11 echo export > port
12 echo vaportight >> port
13 echo 'import/export' >> port
14 hg commit -m 2 -u spam -d '2 0'
15 echo 'import/export' >> port
16 hg commit -m 3 -u eggs -d '3 0'
17 head -3 port > port1
18 mv port1 port
19 hg commit -m 4 -u spam -d '4 0'
20 hg grep port port
21 hg grep -enu port port
22 hg grep import port
@@ -207,6 +207,31 b' forget [options] [files]::'
207 207 -I, --include <pat> include names matching the given patterns
208 208 -X, --exclude <pat> exclude names matching the given patterns
209 209
210 grep [options] pattern [files]::
211 Search revisions of files for a regular expression.
212
213 This command behaves differently than Unix grep. It only accepts
214 Python/Perl regexps. It searches repository history, not the
215 working directory. It always prints the revision number in which
216 a match appears.
217
218 By default, grep only prints output for the first revision of a
219 file in which it finds a match. To get it to print every revision
220 that contains a change in match status ("-" for a match that
221 becomes a non-match, or "+" for a non-match that becomes a match),
222 use the --every-match flag.
223
224 options:
225 -0, --print0 end fields with NUL
226 -I, --include <pat> include names matching the given patterns
227 -X, --exclude <pat> exclude names matching the given patterns
228 -e, --every-match print every revision that matches
229 -i, --ignore-case ignore case when matching
230 -l, --files-with-matches print only file names and revs that match
231 -n, --line-number print matching line numbers
232 -r <rev>, --rev <rev> search in given revision range
233 -u, --user print user who committed change
234
210 235 heads::
211 236 Show all repository head changesets.
212 237
@@ -49,12 +49,31 b" def walk(repo, pats, opts, head=''):"
49 49 yield r
50 50
51 51 def walkchangerevs(ui, repo, cwd, pats, opts):
52 # This code most commonly needs to iterate backwards over the
53 # history it is interested in. Doing so has awful
54 # (quadratic-looking) performance, so we use iterators in a
55 # "windowed" way. Walk forwards through a window of revisions,
56 # yielding them in the desired order, and walk the windows
57 # themselves backwards.
52 '''Iterate over files and the revs they changed in.
53
54 Callers most commonly need to iterate backwards over the history
55 it is interested in. Doing so has awful (quadratic-looking)
56 performance, so we use iterators in a "windowed" way.
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 77 cwd = repo.getcwd()
59 78 if not pats and cwd:
60 79 opts['include'] = [os.path.join(cwd, i) for i in opts['include']]
@@ -66,6 +85,14 b' def walkchangerevs(ui, repo, cwd, pats, '
66 85 slowpath = anypats
67 86 window = 300
68 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 96 if not slowpath and not files:
70 97 # No files, no patterns. Display all revs.
71 98 wanted = dict(zip(revs, revs))
@@ -100,7 +127,7 b' def walkchangerevs(ui, repo, cwd, pats, '
100 127 def changerevgen():
101 128 for i in xrange(repo.changelog.count() - 1, -1, -window):
102 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 132 for rev, changefiles in changerevgen():
106 133 matches = filter(matchfn, changefiles)
@@ -108,20 +135,19 b' def walkchangerevs(ui, repo, cwd, pats, '
108 135 fncache[rev] = matches
109 136 wanted[rev] = 1
110 137
111 for i in xrange(0, len(revs), window):
112 yield 'window', revs[0] < revs[-1], revs[-1]
113 nrevs = [rev for rev in revs[i:min(i+window, len(revs))]
114 if rev in wanted]
115 srevs = list(nrevs)
116 srevs.sort()
117 for rev in srevs:
118 fns = fncache.get(rev)
119 if not fns:
120 fns = repo.changelog.read(repo.lookup(str(rev)))[3]
121 fns = filter(matchfn, fns)
122 yield 'add', rev, fns
123 for rev in nrevs:
124 yield 'iter', rev, None
138 def iterate():
139 for i in xrange(0, len(revs), window):
140 yield 'window', revs[0] < revs[-1], revs[-1]
141 nrevs = [rev for rev in revs[i:min(i+window, len(revs))]
142 if rev in wanted]
143 srevs = list(nrevs)
144 srevs.sort()
145 for rev in srevs:
146 fns = fncache.get(rev) or filter(matchfn, getchange(rev)[3])
147 yield 'add', rev, fns
148 for rev in nrevs:
149 yield 'iter', rev, None
150 return iterate(), getchange
125 151
126 152 revrangesep = ':'
127 153
@@ -150,6 +176,7 b' def revrange(ui, repo, revs, revlog=None'
150 176 except KeyError:
151 177 raise util.Abort('invalid revision identifier %s', val)
152 178 return num
179 seen = {}
153 180 for spec in revs:
154 181 if spec.find(revrangesep) >= 0:
155 182 start, end = spec.split(revrangesep, 1)
@@ -157,9 +184,14 b' def revrange(ui, repo, revs, revlog=None'
157 184 end = fix(end, revcount - 1)
158 185 step = start > end and -1 or 1
159 186 for rev in xrange(start, end+step, step):
187 if rev in seen: continue
188 seen[rev] = 1
160 189 yield str(rev)
161 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 196 def make_filename(repo, r, pat, node=None,
165 197 total=None, seqno=None, revwidth=None):
@@ -265,6 +297,21 b' def dodiff(fp, ui, repo, node1, node2, f'
265 297 tn = None
266 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 315 def show_changeset(ui, repo, rev=0, changenode=None, brinfo=None):
269 316 """show a single changeset or file revision"""
270 317 log = repo.changelog
@@ -467,25 +514,14 b' def annotate(ui, repo, *pats, **opts):'
467 514 def getnode(rev):
468 515 return short(repo.changelog.node(rev))
469 516
517 ucache = {}
470 518 def getname(rev):
471 try:
472 return bcache[rev]
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
519 cl = repo.changelog.read(repo.changelog.node(rev))
520 return trimuser(ui, rev, cl[1], ucache)
484 521
485 522 if not pats:
486 523 raise util.Abort('at least one file name or pattern required')
487 524
488 bcache = {}
489 525 opmap = [['user', getname], ['number', str], ['changeset', getnode]]
490 526 if not opts['user'] and not opts['changeset']:
491 527 opts['number'] = 1
@@ -826,9 +862,9 b' def grep(ui, repo, pattern, *pats, **opt'
826 862 if opts['ignore_case']:
827 863 reflags |= re.I
828 864 regexp = re.compile(pattern, reflags)
829 sep, end = ':', '\n'
865 sep, eol = ':', '\n'
830 866 if opts['print0']:
831 sep = end = '\0'
867 sep = eol = '\0'
832 868
833 869 fcache = {}
834 870 def getfile(fn):
@@ -870,10 +906,12 b' def grep(ui, repo, pattern, *pats, **opt'
870 906 m[s] = s
871 907
872 908 prev = {}
909 ucache = {}
873 910 def display(fn, rev, states, prevstates):
874 911 diff = list(sets.Set(states).symmetric_difference(sets.Set(prevstates)))
875 912 diff.sort(lambda x, y: cmp(x.linenum, y.linenum))
876 913 counts = {'-': 0, '+': 0}
914 filerevmatches = {}
877 915 for l in diff:
878 916 if incrementing or not opts['every_match']:
879 917 change = ((l in prevstates) and '-') or '+'
@@ -881,13 +919,26 b' def grep(ui, repo, pattern, *pats, **opt'
881 919 else:
882 920 change = ((l in states) and '-') or '+'
883 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 934 counts[change] += 1
886 935 return counts['+'], counts['-']
887 936
888 937 fstate = {}
889 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 942 if st == 'window':
892 943 incrementing = rev
893 944 matches.clear()
@@ -909,6 +960,7 b' def grep(ui, repo, pattern, *pats, **opt'
909 960 if fn in skip: continue
910 961 if incrementing or not opts['every_match'] or fstate[fn]:
911 962 pos, neg = display(fn, rev, m, fstate[fn])
963 count += pos + neg
912 964 if pos and not opts['every_match']:
913 965 skip[fn] = True
914 966 fstate[fn] = m
@@ -920,6 +972,7 b' def grep(ui, repo, pattern, *pats, **opt'
920 972 for fn, state in fstate:
921 973 if fn in skip: continue
922 974 display(fn, rev, {}, state)
975 return (count == 0 and 1) or 0
923 976
924 977 def heads(ui, repo, **opts):
925 978 """show current repository heads"""
@@ -1073,8 +1126,9 b' def log(ui, repo, *pats, **opts):'
1073 1126 if not pats and cwd:
1074 1127 opts['include'] = [os.path.join(cwd, i) for i in opts['include']]
1075 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,
1077 opts):
1129 changeiter, getchange = walkchangerevs(ui, repo, (pats and cwd) or '',
1130 pats, opts)
1131 for st, rev, fns in changeiter:
1078 1132 if st == 'window':
1079 1133 du = dui(ui)
1080 1134 elif st == 'add':
@@ -1571,14 +1625,15 b' table = {'
1571 1625 "hg forget [OPTION]... FILE..."),
1572 1626 "grep":
1573 1627 (grep,
1574 [('0', 'print0', None, 'end filenames with NUL'),
1628 [('0', 'print0', None, 'end fields with NUL'),
1575 1629 ('I', 'include', [], 'include path in search'),
1576 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 1632 ('i', 'ignore-case', None, 'ignore case when matching'),
1579 ('l', 'files-with-matches', None, 'print names of files with matches'),
1580 ('n', 'line-number', '', 'print line numbers'),
1581 ('r', 'rev', [], 'search in revision rev')],
1633 ('l', 'files-with-matches', None, 'print names of files and revs with matches'),
1634 ('n', 'line-number', None, 'print line numbers'),
1635 ('r', 'rev', [], 'search in revision rev'),
1636 ('u', 'user', None, 'print user who made change')],
1582 1637 "hg grep [OPTION]... PATTERN [FILE]..."),
1583 1638 "heads":
1584 1639 (heads,
General Comments 0
You need to be logged in to leave comments. Login now