1189 fns = fns_generator() |
1189 fns = fns_generator() |
1190 prepare(ctx, fns) |
1190 prepare(ctx, fns) |
1191 for rev in nrevs: |
1191 for rev in nrevs: |
1192 yield change(rev) |
1192 yield change(rev) |
1193 return iterate() |
1193 return iterate() |
|
1194 |
|
1195 def _makegraphfilematcher(repo, pats, followfirst): |
|
1196 # When displaying a revision with --patch --follow FILE, we have |
|
1197 # to know which file of the revision must be diffed. With |
|
1198 # --follow, we want the names of the ancestors of FILE in the |
|
1199 # revision, stored in "fcache". "fcache" is populated by |
|
1200 # reproducing the graph traversal already done by --follow revset |
|
1201 # and relating linkrevs to file names (which is not "correct" but |
|
1202 # good enough). |
|
1203 fcache = {} |
|
1204 fcacheready = [False] |
|
1205 pctx = repo['.'] |
|
1206 wctx = repo[None] |
|
1207 |
|
1208 def populate(): |
|
1209 for fn in pats: |
|
1210 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)): |
|
1211 for c in i: |
|
1212 fcache.setdefault(c.linkrev(), set()).add(c.path()) |
|
1213 |
|
1214 def filematcher(rev): |
|
1215 if not fcacheready[0]: |
|
1216 # Lazy initialization |
|
1217 fcacheready[0] = True |
|
1218 populate() |
|
1219 return scmutil.match(wctx, fcache.get(rev, []), default='path') |
|
1220 |
|
1221 return filematcher |
|
1222 |
|
1223 def _makegraphlogrevset(repo, pats, opts, revs): |
|
1224 """Return (expr, filematcher) where expr is a revset string built |
|
1225 from log options and file patterns or None. If --stat or --patch |
|
1226 are not passed filematcher is None. Otherwise it is a callable |
|
1227 taking a revision number and returning a match objects filtering |
|
1228 the files to be detailed when displaying the revision. |
|
1229 """ |
|
1230 opt2revset = { |
|
1231 'no_merges': ('not merge()', None), |
|
1232 'only_merges': ('merge()', None), |
|
1233 '_ancestors': ('ancestors(%(val)s)', None), |
|
1234 '_fancestors': ('_firstancestors(%(val)s)', None), |
|
1235 '_descendants': ('descendants(%(val)s)', None), |
|
1236 '_fdescendants': ('_firstdescendants(%(val)s)', None), |
|
1237 '_matchfiles': ('_matchfiles(%(val)s)', None), |
|
1238 'date': ('date(%(val)r)', None), |
|
1239 'branch': ('branch(%(val)r)', ' or '), |
|
1240 '_patslog': ('filelog(%(val)r)', ' or '), |
|
1241 '_patsfollow': ('follow(%(val)r)', ' or '), |
|
1242 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '), |
|
1243 'keyword': ('keyword(%(val)r)', ' or '), |
|
1244 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '), |
|
1245 'user': ('user(%(val)r)', ' or '), |
|
1246 } |
|
1247 |
|
1248 opts = dict(opts) |
|
1249 # follow or not follow? |
|
1250 follow = opts.get('follow') or opts.get('follow_first') |
|
1251 followfirst = opts.get('follow_first') and 1 or 0 |
|
1252 # --follow with FILE behaviour depends on revs... |
|
1253 startrev = revs[0] |
|
1254 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0 |
|
1255 |
|
1256 # branch and only_branch are really aliases and must be handled at |
|
1257 # the same time |
|
1258 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', []) |
|
1259 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']] |
|
1260 # pats/include/exclude are passed to match.match() directly in |
|
1261 # _matchfile() revset but walkchangerevs() builds its matcher with |
|
1262 # scmutil.match(). The difference is input pats are globbed on |
|
1263 # platforms without shell expansion (windows). |
|
1264 pctx = repo[None] |
|
1265 match, pats = scmutil.matchandpats(pctx, pats, opts) |
|
1266 slowpath = match.anypats() or (match.files() and opts.get('removed')) |
|
1267 if not slowpath: |
|
1268 for f in match.files(): |
|
1269 if follow and f not in pctx: |
|
1270 raise util.Abort(_('cannot follow file not in parent ' |
|
1271 'revision: "%s"') % f) |
|
1272 filelog = repo.file(f) |
|
1273 if not len(filelog): |
|
1274 # A zero count may be a directory or deleted file, so |
|
1275 # try to find matching entries on the slow path. |
|
1276 if follow: |
|
1277 raise util.Abort( |
|
1278 _('cannot follow nonexistent file: "%s"') % f) |
|
1279 slowpath = True |
|
1280 if slowpath: |
|
1281 # See walkchangerevs() slow path. |
|
1282 # |
|
1283 if follow: |
|
1284 raise util.Abort(_('can only follow copies/renames for explicit ' |
|
1285 'filenames')) |
|
1286 # pats/include/exclude cannot be represented as separate |
|
1287 # revset expressions as their filtering logic applies at file |
|
1288 # level. For instance "-I a -X a" matches a revision touching |
|
1289 # "a" and "b" while "file(a) and not file(b)" does |
|
1290 # not. Besides, filesets are evaluated against the working |
|
1291 # directory. |
|
1292 matchargs = ['r:', 'd:relpath'] |
|
1293 for p in pats: |
|
1294 matchargs.append('p:' + p) |
|
1295 for p in opts.get('include', []): |
|
1296 matchargs.append('i:' + p) |
|
1297 for p in opts.get('exclude', []): |
|
1298 matchargs.append('x:' + p) |
|
1299 matchargs = ','.join(('%r' % p) for p in matchargs) |
|
1300 opts['_matchfiles'] = matchargs |
|
1301 else: |
|
1302 if follow: |
|
1303 fpats = ('_patsfollow', '_patsfollowfirst') |
|
1304 fnopats = (('_ancestors', '_fancestors'), |
|
1305 ('_descendants', '_fdescendants')) |
|
1306 if pats: |
|
1307 # follow() revset inteprets its file argument as a |
|
1308 # manifest entry, so use match.files(), not pats. |
|
1309 opts[fpats[followfirst]] = list(match.files()) |
|
1310 else: |
|
1311 opts[fnopats[followdescendants][followfirst]] = str(startrev) |
|
1312 else: |
|
1313 opts['_patslog'] = list(pats) |
|
1314 |
|
1315 filematcher = None |
|
1316 if opts.get('patch') or opts.get('stat'): |
|
1317 if follow: |
|
1318 filematcher = _makegraphfilematcher(repo, pats, followfirst) |
|
1319 else: |
|
1320 filematcher = lambda rev: match |
|
1321 |
|
1322 expr = [] |
|
1323 for op, val in opts.iteritems(): |
|
1324 if not val: |
|
1325 continue |
|
1326 if op not in opt2revset: |
|
1327 continue |
|
1328 revop, andor = opt2revset[op] |
|
1329 if '%(val)' not in revop: |
|
1330 expr.append(revop) |
|
1331 else: |
|
1332 if not isinstance(val, list): |
|
1333 e = revop % {'val': val} |
|
1334 else: |
|
1335 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')' |
|
1336 expr.append(e) |
|
1337 |
|
1338 if expr: |
|
1339 expr = '(' + ' and '.join(expr) + ')' |
|
1340 else: |
|
1341 expr = None |
|
1342 return expr, filematcher |
|
1343 |
|
1344 def getgraphlogrevs(repo, pats, opts): |
|
1345 """Return (revs, expr, filematcher) where revs is an iterable of |
|
1346 revision numbers, expr is a revset string built from log options |
|
1347 and file patterns or None, and used to filter 'revs'. If --stat or |
|
1348 --patch are not passed filematcher is None. Otherwise it is a |
|
1349 callable taking a revision number and returning a match objects |
|
1350 filtering the files to be detailed when displaying the revision. |
|
1351 """ |
|
1352 def increasingrevs(repo, revs, matcher): |
|
1353 # The sorted input rev sequence is chopped in sub-sequences |
|
1354 # which are sorted in ascending order and passed to the |
|
1355 # matcher. The filtered revs are sorted again as they were in |
|
1356 # the original sub-sequence. This achieve several things: |
|
1357 # |
|
1358 # - getlogrevs() now returns a generator which behaviour is |
|
1359 # adapted to log need. First results come fast, last ones |
|
1360 # are batched for performances. |
|
1361 # |
|
1362 # - revset matchers often operate faster on revision in |
|
1363 # changelog order, because most filters deal with the |
|
1364 # changelog. |
|
1365 # |
|
1366 # - revset matchers can reorder revisions. "A or B" typically |
|
1367 # returns returns the revision matching A then the revision |
|
1368 # matching B. We want to hide this internal implementation |
|
1369 # detail from the caller, and sorting the filtered revision |
|
1370 # again achieves this. |
|
1371 for i, window in increasingwindows(0, len(revs), windowsize=1): |
|
1372 orevs = revs[i:i + window] |
|
1373 nrevs = set(matcher(repo, sorted(orevs))) |
|
1374 for rev in orevs: |
|
1375 if rev in nrevs: |
|
1376 yield rev |
|
1377 |
|
1378 if not len(repo): |
|
1379 return iter([]), None, None |
|
1380 # Default --rev value depends on --follow but --follow behaviour |
|
1381 # depends on revisions resolved from --rev... |
|
1382 follow = opts.get('follow') or opts.get('follow_first') |
|
1383 if opts.get('rev'): |
|
1384 revs = scmutil.revrange(repo, opts['rev']) |
|
1385 else: |
|
1386 if follow and len(repo) > 0: |
|
1387 revs = scmutil.revrange(repo, ['.:0']) |
|
1388 else: |
|
1389 revs = range(len(repo) - 1, -1, -1) |
|
1390 if not revs: |
|
1391 return iter([]), None, None |
|
1392 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs) |
|
1393 if expr: |
|
1394 matcher = revset.match(repo.ui, expr) |
|
1395 revs = increasingrevs(repo, revs, matcher) |
|
1396 if not opts.get('hidden'): |
|
1397 # --hidden is still experimental and not worth a dedicated revset |
|
1398 # yet. Fortunately, filtering revision number is fast. |
|
1399 revs = (r for r in revs if r not in repo.changelog.hiddenrevs) |
|
1400 else: |
|
1401 revs = iter(revs) |
|
1402 return revs, expr, filematcher |
|
1403 |
|
1404 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None, |
|
1405 filematcher=None): |
|
1406 seen, state = [], graphmod.asciistate() |
|
1407 for rev, type, ctx, parents in dag: |
|
1408 char = 'o' |
|
1409 if ctx.node() in showparents: |
|
1410 char = '@' |
|
1411 elif ctx.obsolete(): |
|
1412 char = 'x' |
|
1413 copies = None |
|
1414 if getrenamed and ctx.rev(): |
|
1415 copies = [] |
|
1416 for fn in ctx.files(): |
|
1417 rename = getrenamed(fn, ctx.rev()) |
|
1418 if rename: |
|
1419 copies.append((fn, rename[0])) |
|
1420 revmatchfn = None |
|
1421 if filematcher is not None: |
|
1422 revmatchfn = filematcher(ctx.rev()) |
|
1423 displayer.show(ctx, copies=copies, matchfn=revmatchfn) |
|
1424 lines = displayer.hunk.pop(rev).split('\n') |
|
1425 if not lines[-1]: |
|
1426 del lines[-1] |
|
1427 displayer.flush(rev) |
|
1428 edges = edgefn(type, char, lines, seen, rev, parents) |
|
1429 for type, char, lines, coldata in edges: |
|
1430 graphmod.ascii(ui, state, type, char, lines, coldata) |
|
1431 displayer.close() |
1194 |
1432 |
1195 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly): |
1433 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly): |
1196 join = lambda f: os.path.join(prefix, f) |
1434 join = lambda f: os.path.join(prefix, f) |
1197 bad = [] |
1435 bad = [] |
1198 oldbad = match.bad |
1436 oldbad = match.bad |