##// 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 -I, --include <pat> include names matching the given patterns
207 -I, --include <pat> include names matching the given patterns
208 -X, --exclude <pat> exclude names matching the given patterns
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 heads::
235 heads::
211 Show all repository head changesets.
236 Show all repository head changesets.
212
237
@@ -49,12 +49,31 b" def walk(repo, pats, opts, head=''):"
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']]
@@ -66,6 +85,14 b' def walkchangerevs(ui, repo, cwd, pats, '
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))
@@ -100,7 +127,7 b' def walkchangerevs(ui, repo, cwd, pats, '
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)
@@ -108,20 +135,19 b' def walkchangerevs(ui, repo, cwd, pats, '
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
@@ -150,6 +176,7 b' def revrange(ui, repo, revs, revlog=None'
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)
@@ -157,9 +184,14 b' def revrange(ui, repo, revs, revlog=None'
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):
@@ -265,6 +297,21 b' def dodiff(fp, ui, repo, node1, node2, 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
@@ -467,25 +514,14 b' def annotate(ui, repo, *pats, **opts):'
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
@@ -826,9 +862,9 b' def grep(ui, repo, pattern, *pats, **opt'
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):
@@ -870,10 +906,12 b' def grep(ui, repo, pattern, *pats, **opt'
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 '+'
@@ -881,13 +919,26 b' def grep(ui, repo, pattern, *pats, **opt'
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()
@@ -909,6 +960,7 b' def grep(ui, repo, pattern, *pats, **opt'
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
@@ -920,6 +972,7 b' def grep(ui, repo, pattern, *pats, **opt'
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"""
@@ -1073,8 +1126,9 b' def log(ui, repo, *pats, **opts):'
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':
@@ -1571,14 +1625,15 b' table = {'
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,
General Comments 0
You need to be logged in to leave comments. Login now