13 ''' |
13 ''' |
14 |
14 |
15 from mercurial.cmdutil import show_changeset |
15 from mercurial.cmdutil import show_changeset |
16 from mercurial.i18n import _ |
16 from mercurial.i18n import _ |
17 from mercurial import cmdutil, commands, extensions, scmutil |
17 from mercurial import cmdutil, commands, extensions, scmutil |
18 from mercurial import hg, util, graphmod, templatekw, revset |
18 from mercurial import hg, util, graphmod, templatekw |
19 |
19 |
20 cmdtable = {} |
20 cmdtable = {} |
21 command = cmdutil.command(cmdtable) |
21 command = cmdutil.command(cmdtable) |
22 testedwith = 'internal' |
22 testedwith = 'internal' |
23 |
23 |
24 def _checkunsupportedflags(pats, opts): |
24 def _checkunsupportedflags(pats, opts): |
25 for op in ["newest_first"]: |
25 for op in ["newest_first"]: |
26 if op in opts and opts[op]: |
26 if op in opts and opts[op]: |
27 raise util.Abort(_("-G/--graph option is incompatible with --%s") |
27 raise util.Abort(_("-G/--graph option is incompatible with --%s") |
28 % op.replace("_", "-")) |
28 % op.replace("_", "-")) |
29 |
|
30 def _makefilematcher(repo, pats, followfirst): |
|
31 # When displaying a revision with --patch --follow FILE, we have |
|
32 # to know which file of the revision must be diffed. With |
|
33 # --follow, we want the names of the ancestors of FILE in the |
|
34 # revision, stored in "fcache". "fcache" is populated by |
|
35 # reproducing the graph traversal already done by --follow revset |
|
36 # and relating linkrevs to file names (which is not "correct" but |
|
37 # good enough). |
|
38 fcache = {} |
|
39 fcacheready = [False] |
|
40 pctx = repo['.'] |
|
41 wctx = repo[None] |
|
42 |
|
43 def populate(): |
|
44 for fn in pats: |
|
45 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)): |
|
46 for c in i: |
|
47 fcache.setdefault(c.linkrev(), set()).add(c.path()) |
|
48 |
|
49 def filematcher(rev): |
|
50 if not fcacheready[0]: |
|
51 # Lazy initialization |
|
52 fcacheready[0] = True |
|
53 populate() |
|
54 return scmutil.match(wctx, fcache.get(rev, []), default='path') |
|
55 |
|
56 return filematcher |
|
57 |
|
58 def _makelogrevset(repo, pats, opts, revs): |
|
59 """Return (expr, filematcher) where expr is a revset string built |
|
60 from log options and file patterns or None. If --stat or --patch |
|
61 are not passed filematcher is None. Otherwise it is a callable |
|
62 taking a revision number and returning a match objects filtering |
|
63 the files to be detailed when displaying the revision. |
|
64 """ |
|
65 opt2revset = { |
|
66 'no_merges': ('not merge()', None), |
|
67 'only_merges': ('merge()', None), |
|
68 '_ancestors': ('ancestors(%(val)s)', None), |
|
69 '_fancestors': ('_firstancestors(%(val)s)', None), |
|
70 '_descendants': ('descendants(%(val)s)', None), |
|
71 '_fdescendants': ('_firstdescendants(%(val)s)', None), |
|
72 '_matchfiles': ('_matchfiles(%(val)s)', None), |
|
73 'date': ('date(%(val)r)', None), |
|
74 'branch': ('branch(%(val)r)', ' or '), |
|
75 '_patslog': ('filelog(%(val)r)', ' or '), |
|
76 '_patsfollow': ('follow(%(val)r)', ' or '), |
|
77 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '), |
|
78 'keyword': ('keyword(%(val)r)', ' or '), |
|
79 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '), |
|
80 'user': ('user(%(val)r)', ' or '), |
|
81 } |
|
82 |
|
83 opts = dict(opts) |
|
84 # follow or not follow? |
|
85 follow = opts.get('follow') or opts.get('follow_first') |
|
86 followfirst = opts.get('follow_first') and 1 or 0 |
|
87 # --follow with FILE behaviour depends on revs... |
|
88 startrev = revs[0] |
|
89 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0 |
|
90 |
|
91 # branch and only_branch are really aliases and must be handled at |
|
92 # the same time |
|
93 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', []) |
|
94 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']] |
|
95 # pats/include/exclude are passed to match.match() directly in |
|
96 # _matchfile() revset but walkchangerevs() builds its matcher with |
|
97 # scmutil.match(). The difference is input pats are globbed on |
|
98 # platforms without shell expansion (windows). |
|
99 pctx = repo[None] |
|
100 match, pats = scmutil.matchandpats(pctx, pats, opts) |
|
101 slowpath = match.anypats() or (match.files() and opts.get('removed')) |
|
102 if not slowpath: |
|
103 for f in match.files(): |
|
104 if follow and f not in pctx: |
|
105 raise util.Abort(_('cannot follow file not in parent ' |
|
106 'revision: "%s"') % f) |
|
107 filelog = repo.file(f) |
|
108 if not len(filelog): |
|
109 # A zero count may be a directory or deleted file, so |
|
110 # try to find matching entries on the slow path. |
|
111 if follow: |
|
112 raise util.Abort( |
|
113 _('cannot follow nonexistent file: "%s"') % f) |
|
114 slowpath = True |
|
115 if slowpath: |
|
116 # See cmdutil.walkchangerevs() slow path. |
|
117 # |
|
118 if follow: |
|
119 raise util.Abort(_('can only follow copies/renames for explicit ' |
|
120 'filenames')) |
|
121 # pats/include/exclude cannot be represented as separate |
|
122 # revset expressions as their filtering logic applies at file |
|
123 # level. For instance "-I a -X a" matches a revision touching |
|
124 # "a" and "b" while "file(a) and not file(b)" does |
|
125 # not. Besides, filesets are evaluated against the working |
|
126 # directory. |
|
127 matchargs = ['r:', 'd:relpath'] |
|
128 for p in pats: |
|
129 matchargs.append('p:' + p) |
|
130 for p in opts.get('include', []): |
|
131 matchargs.append('i:' + p) |
|
132 for p in opts.get('exclude', []): |
|
133 matchargs.append('x:' + p) |
|
134 matchargs = ','.join(('%r' % p) for p in matchargs) |
|
135 opts['_matchfiles'] = matchargs |
|
136 else: |
|
137 if follow: |
|
138 fpats = ('_patsfollow', '_patsfollowfirst') |
|
139 fnopats = (('_ancestors', '_fancestors'), |
|
140 ('_descendants', '_fdescendants')) |
|
141 if pats: |
|
142 # follow() revset inteprets its file argument as a |
|
143 # manifest entry, so use match.files(), not pats. |
|
144 opts[fpats[followfirst]] = list(match.files()) |
|
145 else: |
|
146 opts[fnopats[followdescendants][followfirst]] = str(startrev) |
|
147 else: |
|
148 opts['_patslog'] = list(pats) |
|
149 |
|
150 filematcher = None |
|
151 if opts.get('patch') or opts.get('stat'): |
|
152 if follow: |
|
153 filematcher = _makefilematcher(repo, pats, followfirst) |
|
154 else: |
|
155 filematcher = lambda rev: match |
|
156 |
|
157 expr = [] |
|
158 for op, val in opts.iteritems(): |
|
159 if not val: |
|
160 continue |
|
161 if op not in opt2revset: |
|
162 continue |
|
163 revop, andor = opt2revset[op] |
|
164 if '%(val)' not in revop: |
|
165 expr.append(revop) |
|
166 else: |
|
167 if not isinstance(val, list): |
|
168 e = revop % {'val': val} |
|
169 else: |
|
170 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')' |
|
171 expr.append(e) |
|
172 |
|
173 if expr: |
|
174 expr = '(' + ' and '.join(expr) + ')' |
|
175 else: |
|
176 expr = None |
|
177 return expr, filematcher |
|
178 |
|
179 def getlogrevs(repo, pats, opts): |
|
180 """Return (revs, expr, filematcher) where revs is an iterable of |
|
181 revision numbers, expr is a revset string built from log options |
|
182 and file patterns or None, and used to filter 'revs'. If --stat or |
|
183 --patch are not passed filematcher is None. Otherwise it is a |
|
184 callable taking a revision number and returning a match objects |
|
185 filtering the files to be detailed when displaying the revision. |
|
186 """ |
|
187 def increasingrevs(repo, revs, matcher): |
|
188 # The sorted input rev sequence is chopped in sub-sequences |
|
189 # which are sorted in ascending order and passed to the |
|
190 # matcher. The filtered revs are sorted again as they were in |
|
191 # the original sub-sequence. This achieve several things: |
|
192 # |
|
193 # - getlogrevs() now returns a generator which behaviour is |
|
194 # adapted to log need. First results come fast, last ones |
|
195 # are batched for performances. |
|
196 # |
|
197 # - revset matchers often operate faster on revision in |
|
198 # changelog order, because most filters deal with the |
|
199 # changelog. |
|
200 # |
|
201 # - revset matchers can reorder revisions. "A or B" typically |
|
202 # returns returns the revision matching A then the revision |
|
203 # matching B. We want to hide this internal implementation |
|
204 # detail from the caller, and sorting the filtered revision |
|
205 # again achieves this. |
|
206 for i, window in cmdutil.increasingwindows(0, len(revs), windowsize=1): |
|
207 orevs = revs[i:i + window] |
|
208 nrevs = set(matcher(repo, sorted(orevs))) |
|
209 for rev in orevs: |
|
210 if rev in nrevs: |
|
211 yield rev |
|
212 |
|
213 if not len(repo): |
|
214 return iter([]), None, None |
|
215 # Default --rev value depends on --follow but --follow behaviour |
|
216 # depends on revisions resolved from --rev... |
|
217 follow = opts.get('follow') or opts.get('follow_first') |
|
218 if opts.get('rev'): |
|
219 revs = scmutil.revrange(repo, opts['rev']) |
|
220 else: |
|
221 if follow and len(repo) > 0: |
|
222 revs = scmutil.revrange(repo, ['.:0']) |
|
223 else: |
|
224 revs = range(len(repo) - 1, -1, -1) |
|
225 if not revs: |
|
226 return iter([]), None, None |
|
227 expr, filematcher = _makelogrevset(repo, pats, opts, revs) |
|
228 if expr: |
|
229 matcher = revset.match(repo.ui, expr) |
|
230 revs = increasingrevs(repo, revs, matcher) |
|
231 if not opts.get('hidden'): |
|
232 # --hidden is still experimental and not worth a dedicated revset |
|
233 # yet. Fortunately, filtering revision number is fast. |
|
234 revs = (r for r in revs if r not in repo.changelog.hiddenrevs) |
|
235 else: |
|
236 revs = iter(revs) |
|
237 return revs, expr, filematcher |
|
238 |
|
239 def generate(ui, dag, displayer, showparents, edgefn, getrenamed=None, |
|
240 filematcher=None): |
|
241 seen, state = [], graphmod.asciistate() |
|
242 for rev, type, ctx, parents in dag: |
|
243 char = 'o' |
|
244 if ctx.node() in showparents: |
|
245 char = '@' |
|
246 elif ctx.obsolete(): |
|
247 char = 'x' |
|
248 copies = None |
|
249 if getrenamed and ctx.rev(): |
|
250 copies = [] |
|
251 for fn in ctx.files(): |
|
252 rename = getrenamed(fn, ctx.rev()) |
|
253 if rename: |
|
254 copies.append((fn, rename[0])) |
|
255 revmatchfn = None |
|
256 if filematcher is not None: |
|
257 revmatchfn = filematcher(ctx.rev()) |
|
258 displayer.show(ctx, copies=copies, matchfn=revmatchfn) |
|
259 lines = displayer.hunk.pop(rev).split('\n') |
|
260 if not lines[-1]: |
|
261 del lines[-1] |
|
262 displayer.flush(rev) |
|
263 edges = edgefn(type, char, lines, seen, rev, parents) |
|
264 for type, char, lines, coldata in edges: |
|
265 graphmod.ascii(ui, state, type, char, lines, coldata) |
|
266 displayer.close() |
|
267 |
29 |
268 @command('glog', |
30 @command('glog', |
269 [('f', 'follow', None, |
31 [('f', 'follow', None, |
270 _('follow changeset history, or file history across copies and renames')), |
32 _('follow changeset history, or file history across copies and renames')), |
271 ('', 'follow-first', None, |
33 ('', 'follow-first', None, |