comparison mercurial/commands.py @ 1057:2fd15d743b3b

Add grep command. It currently searches all revs of every matching file. I'll change this soon so that it can still do this, but it will not be the default behaviour. Many options are unimplemented. There's only one output mode. Binary files are not handled yet.
author Bryan O'Sullivan <bos@serpentine.com>
date Thu, 25 Aug 2005 02:00:03 -0700
parents 23f9d71ab9ae
children 402279974aea
comparison
equal deleted inserted replaced
1056:34be48b4ca85 1057:2fd15d743b3b
43 return files, matchfn, walk() 43 return files, matchfn, walk()
44 44
45 def walk(repo, pats, opts, head = ''): 45 def walk(repo, pats, opts, head = ''):
46 files, matchfn, results = makewalk(repo, pats, opts, head) 46 files, matchfn, results = makewalk(repo, pats, opts, head)
47 for r in results: yield r 47 for r in results: yield r
48
49 def walkchangerevs(ui, repo, cwd, pats, opts):
50 # This code most commonly needs to iterate backwards over the
51 # history it is interested in. Doing so has awful
52 # (quadratic-looking) performance, so we use iterators in a
53 # "windowed" way. Walk forwards through a window of revisions,
54 # yielding them in the desired order, and walk the windows
55 # themselves backwards.
56 cwd = repo.getcwd()
57 if not pats and cwd:
58 opts['include'] = [os.path.join(cwd, i) for i in opts['include']]
59 opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']]
60 files, matchfn, anypats = matchpats(repo, (pats and cwd) or '',
61 pats, opts)
62 revs = map(int, revrange(ui, repo, opts['rev'] or ['tip:0']))
63 wanted = {}
64 slowpath = anypats
65 window = 300
66 fncache = {}
67 if not slowpath and not files:
68 # No files, no patterns. Display all revs.
69 wanted = dict(zip(revs, revs))
70 if not slowpath:
71 # Only files, no patterns. Check the history of each file.
72 def filerevgen(filelog):
73 for i in xrange(filelog.count() - 1, -1, -window):
74 revs = []
75 for j in xrange(max(0, i - window), i + 1):
76 revs.append(filelog.linkrev(filelog.node(j)))
77 revs.reverse()
78 for rev in revs:
79 yield rev
80
81 minrev, maxrev = min(revs), max(revs)
82 for file in files:
83 filelog = repo.file(file)
84 # A zero count may be a directory or deleted file, so
85 # try to find matching entries on the slow path.
86 if filelog.count() == 0:
87 slowpath = True
88 break
89 for rev in filerevgen(filelog):
90 if rev <= maxrev:
91 if rev < minrev: break
92 fncache.setdefault(rev, [])
93 fncache[rev].append(file)
94 wanted[rev] = 1
95 if slowpath:
96 # The slow path checks files modified in every changeset.
97 def changerevgen():
98 for i in xrange(repo.changelog.count() - 1, -1, -window):
99 for j in xrange(max(0, i - window), i + 1):
100 yield j, repo.changelog.read(repo.lookup(str(j)))[3]
101
102 for rev, changefiles in changerevgen():
103 matches = filter(matchfn, changefiles)
104 if matches:
105 fncache[rev] = matches
106 wanted[rev] = 1
107
108 for i in xrange(0, len(revs), window):
109 yield 'window', revs[0] < revs[-1], revs[-1]
110 nrevs = [rev for rev in revs[i : min(i + window, len(revs))]
111 if rev in wanted]
112 srevs = list(nrevs)
113 srevs.sort()
114 for rev in srevs:
115 fns = fncache.get(rev)
116 if not fns:
117 fns = repo.changelog.read(repo.lookup(str(rev)))[3]
118 fns = filter(matchfn, fns)
119 yield 'add', rev, fns
120 for rev in nrevs:
121 yield 'iter', rev, None
48 122
49 revrangesep = ':' 123 revrangesep = ':'
50 124
51 def revrange(ui, repo, revs, revlog=None): 125 def revrange(ui, repo, revs, revlog=None):
52 if revlog is None: 126 if revlog is None:
714 if repo.dirstate.state(abs) == 'a': 788 if repo.dirstate.state(abs) == 'a':
715 forget.append(abs) 789 forget.append(abs)
716 if not exact: ui.status('forgetting ', rel, '\n') 790 if not exact: ui.status('forgetting ', rel, '\n')
717 repo.forget(forget) 791 repo.forget(forget)
718 792
793 def grep(ui, repo, pattern = None, *pats, **opts):
794 if pattern is None: pattern = opts['regexp']
795 if not pattern: raise util.Abort('no pattern to search for')
796 reflags = 0
797 if opts['ignore_case']: reflags |= re.I
798 regexp = re.compile(pattern, reflags)
799 sep, end = ':', '\n'
800 if opts['null'] or opts['print0']: sep = end = '\0'
801
802 fcache = {}
803 def getfile(fn):
804 if fn not in fcache:
805 fcache[fn] = repo.file(fn)
806 return fcache[fn]
807
808 def matchlines(body):
809 for match in regexp.finditer(body):
810 start, end = match.span()
811 lnum = body.count('\n', 0, start) + 1
812 lstart = body.rfind('\n', 0, start) + 1
813 lend = body.find('\n', end)
814 yield lnum, start - lstart, end - lstart, body[lstart:lend]
815
816 class linestate:
817 def __init__(self, line, linenum, colstart, colend):
818 self.line = line
819 self.linenum = linenum
820 self.colstart = colstart
821 self.colend = colend
822 def __eq__(self, other): return self.line == other.line
823 def __hash__(self): return hash(self.line)
824
825 matches = {}
826 def grepbody(fn, rev, body):
827 matches[rev].setdefault(fn, {})
828 m = matches[rev][fn]
829 for lnum, cstart, cend, line in matchlines(body):
830 s = linestate(line, lnum, cstart, cend)
831 m[s] = s
832
833 prev = {}
834 def display(fn, rev, states, prevstates):
835 diff = list(set(states).symmetric_difference(set(prevstates)))
836 diff.sort(lambda x, y: cmp(x.linenum, y.linenum))
837 for l in diff:
838 if incrementing:
839 change = ((l in prevstates) and '-') or '+'
840 r = rev
841 else:
842 change = ((l in states) and '-') or '+'
843 r = prev[fn]
844 ui.write('%s:%s:%s:%s%s\n' % (fn, r, l.linenum, change, l.line))
845
846 fstate = {}
847 for st, rev, fns in walkchangerevs(ui, repo, repo.getcwd(), pats, opts):
848 if st == 'window':
849 incrementing = rev
850 matches.clear()
851 elif st == 'add':
852 change = repo.changelog.read(repo.lookup(str(rev)))
853 mf = repo.manifest.read(change[0])
854 matches[rev] = {}
855 for fn in fns:
856 fstate.setdefault(fn, {})
857 try:
858 grepbody(fn, rev, getfile(fn).read(mf[fn]))
859 except KeyError:
860 pass
861 elif st == 'iter':
862 states = matches[rev].items()
863 states.sort()
864 for fn, m in states:
865 if incrementing or fstate[fn]:
866 display(fn, rev, m, fstate[fn])
867 fstate[fn] = m
868 prev[fn] = rev
869
870 if not incrementing:
871 fstate = fstate.items()
872 fstate.sort()
873 for fn, state in fstate:
874 display(fn, rev, {}, state)
875
719 def heads(ui, repo, **opts): 876 def heads(ui, repo, **opts):
720 """show current repository heads""" 877 """show current repository heads"""
721 heads = repo.changelog.heads() 878 heads = repo.changelog.heads()
722 br = None 879 br = None
723 if opts['branches']: 880 if opts['branches']:
843 else: 1000 else:
844 ui.write(rel, end) 1001 ui.write(rel, end)
845 1002
846 def log(ui, repo, *pats, **opts): 1003 def log(ui, repo, *pats, **opts):
847 """show revision history of entire repository or files""" 1004 """show revision history of entire repository or files"""
848 # This code most commonly needs to iterate backwards over the 1005 class dui:
849 # history it is interested in. This has awful (quadratic-looking) 1006 # Implement and delegate some ui protocol. Save hunks of
850 # performance, so we use iterators that walk forwards through 1007 # output for later display in the desired order.
851 # windows of revisions, yielding revisions in reverse order, while 1008 def __init__(self, ui):
852 # walking the windows backwards. 1009 self.ui = ui
1010 self.hunk = {}
1011 def bump(self, rev):
1012 self.rev = rev
1013 self.hunk[rev] = []
1014 def note(self, *args):
1015 if self.verbose: self.write(*args)
1016 def status(self, *args):
1017 if not self.quiet: self.write(*args)
1018 def write(self, *args):
1019 self.hunk[self.rev].append(args)
1020 def __getattr__(self, key):
1021 return getattr(self.ui, key)
853 cwd = repo.getcwd() 1022 cwd = repo.getcwd()
854 if not pats and cwd: 1023 if not pats and cwd:
855 opts['include'] = [os.path.join(cwd, i) for i in opts['include']] 1024 opts['include'] = [os.path.join(cwd, i) for i in opts['include']]
856 opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']] 1025 opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']]
857 files, matchfn, anypats = matchpats(repo, (pats and cwd) or '', 1026 for st, rev, fns in walkchangerevs(ui, repo, (pats and cwd) or '', pats,
858 pats, opts) 1027 opts):
859 revs = map(int, revrange(ui, repo, opts['rev'] or ['tip:0'])) 1028 if st == 'window':
860 wanted = {}
861 slowpath = anypats
862 window = 300
863 if not slowpath and not files:
864 # No files, no patterns. Display all revs.
865 wanted = dict(zip(revs, revs))
866 if not slowpath:
867 # Only files, no patterns. Check the history of each file.
868 def filerevgen(filelog):
869 for i in xrange(filelog.count() - 1, -1, -window):
870 print "filelog"
871 revs = []
872 for j in xrange(max(0, i - window), i + 1):
873 revs.append(filelog.linkrev(filelog.node(j)))
874 revs.reverse()
875 for rev in revs:
876 yield rev
877
878 minrev, maxrev = min(revs), max(revs)
879 for filelog in map(repo.file, files):
880 # A zero count may be a directory or deleted file, so
881 # try to find matching entries on the slow path.
882 if filelog.count() == 0:
883 slowpath = True
884 break
885 for rev in filerevgen(filelog):
886 if rev <= maxrev:
887 if rev < minrev: break
888 wanted[rev] = 1
889 if slowpath:
890 # The slow path checks files modified in every changeset.
891 def mfrevgen():
892 for i in xrange(repo.changelog.count() - 1, -1, -window):
893 for j in xrange(max(0, i - window), i + 1):
894 yield j, repo.changelog.read(repo.lookup(str(j)))[3]
895
896 for rev, mf in mfrevgen():
897 if filter(matchfn, mf):
898 wanted[rev] = 1
899
900 def changerevgen():
901 class dui:
902 # Implement and delegate some ui protocol. Save hunks of
903 # output for later display in the desired order.
904 def __init__(self, ui):
905 self.ui = ui
906 self.hunk = {}
907 def bump(self, rev):
908 self.rev = rev
909 self.hunk[rev] = []
910 def note(self, *args):
911 if self.verbose: self.write(*args)
912 def status(self, *args):
913 if not self.quiet: self.write(*args)
914 def write(self, *args):
915 self.hunk[self.rev].append(args)
916 def __getattr__(self, key):
917 return getattr(self.ui, key)
918 for i in xrange(0, len(revs), window):
919 nrevs = [rev for rev in revs[i : min(i + window, len(revs))]
920 if rev in wanted]
921 srevs = list(nrevs)
922 srevs.sort()
923 du = dui(ui) 1029 du = dui(ui)
924 for rev in srevs: 1030 elif st == 'add':
925 du.bump(rev) 1031 du.bump(rev)
926 yield rev, du 1032 show_changeset(du, repo, rev)
927 for rev in nrevs: 1033 if opts['patch']:
928 for args in du.hunk[rev]: 1034 changenode = repo.changelog.node(rev)
929 ui.write(*args) 1035 prev, other = repo.changelog.parents(changenode)
930 1036 dodiff(du, du, repo, prev, changenode, fns)
931 for rev, dui in changerevgen(): 1037 du.write("\n\n")
932 show_changeset(dui, repo, rev) 1038 elif st == 'iter':
933 if opts['patch']: 1039 for args in du.hunk[rev]:
934 changenode = repo.changelog.node(rev) 1040 ui.write(*args)
935 prev, other = repo.changelog.parents(changenode)
936 dodiff(dui, dui, repo, prev, changenode, files)
937 dui.write("\n\n")
938 1041
939 def manifest(ui, repo, rev=None): 1042 def manifest(ui, repo, rev=None):
940 """output the latest or given revision of the project manifest""" 1043 """output the latest or given revision of the project manifest"""
941 if rev: 1044 if rev:
942 try: 1045 try:
1406 "forget": 1509 "forget":
1407 (forget, 1510 (forget,
1408 [('I', 'include', [], 'include path in search'), 1511 [('I', 'include', [], 'include path in search'),
1409 ('X', 'exclude', [], 'exclude path from search')], 1512 ('X', 'exclude', [], 'exclude path from search')],
1410 "hg forget [OPTION]... FILE..."), 1513 "hg forget [OPTION]... FILE..."),
1514 "grep": (grep,
1515 [('0', 'print0', None, 'terminate file names with NUL'),
1516 ('I', 'include', [], 'include path in search'),
1517 ('X', 'exclude', [], 'include path in search'),
1518 ('Z', 'null', None, 'terminate file names with NUL'),
1519 ('a', 'all-revs', '', 'search all revs'),
1520 ('e', 'regexp', '', 'pattern to search for'),
1521 ('f', 'full-path', None, 'print complete paths'),
1522 ('i', 'ignore-case', None, 'ignore case when matching'),
1523 ('l', 'files-with-matches', None, 'print names of files with matches'),
1524 ('n', 'line-number', '', 'print line numbers'),
1525 ('r', 'rev', [], 'search in revision rev'),
1526 ('s', 'no-messages', None, 'do not print error messages'),
1527 ('v', 'invert-match', None, 'select non-matching lines')],
1528 "hg grep [options] [pat] [files]"),
1411 "heads": 1529 "heads":
1412 (heads, 1530 (heads,
1413 [('b', 'branches', None, 'find branch info')], 1531 [('b', 'branches', None, 'find branch info')],
1414 'hg [-b] heads'), 1532 'hg [-b] heads'),
1415 "help": (help_, [], 'hg help [COMMAND]'), 1533 "help": (help_, [], 'hg help [COMMAND]'),