# HG changeset patch # User Georges Racinet <georges.racinet@octobus.net> # Date 1695394029 -7200 # Fri Sep 22 16:47:09 2023 +0200 # Branch stable # Node ID 798ac9c4d639537f8750fd3bfbf6e1f4dc7c91cd # Parent c77043842f651f3d1be13cb55de522028c63bf37 LastCommitForPath: simpler and more perfomant implementation The `files` revset predicate is way too slow and answers a much more complicated question, as it has to follow the entire history of the given path. Using `linkrev` on filelogs is not appropriate, because it can point to any changeset that introduced this version of the file, even if not an ancestor of `seen_from` (deduplication). Besides, it raises thorny questions in case path is to be interpreted as a directory, notably being by definition unable to take children removals into account. The `files` field of changelog entries, on the other hand, is supposed to list all files touched by the corresponding changeset. This forces us to iterate on ancestors, and could be quite arbitrary in cases of merges whose both parents changed a given path, but in that case the problem itself is not so well-defined. diff --git a/hgitaly/service/commit.py b/hgitaly/service/commit.py --- a/hgitaly/service/commit.py +++ b/hgitaly/service/commit.py @@ -1018,6 +1018,11 @@ return dt.isoformat() +def ancestors_inclusive(changeset): + yield changeset + yield from changeset.ancestors() + + def latest_changeset_for_path(path, seen_from): """Return latest ancestor of ``seen_from`` that touched the given path. @@ -1029,17 +1034,19 @@ # all changesets change. return seen_from - # gracinet: just hoping that performance wise, this does the right - # thing, i.e do any scanning from the end - # While we can be reasonably confident that the file exists - # in the given revision, there are cases where deduplication implies - # that the filelog() predicate would not see any new file revision - # in some subgraph, because it's identical to another one that's not - # in that subgraph. Hence using the slower `file` is the only way - # to go. - repo = seen_from.repo() - rev = repo.revs('file(%s) and ::%s', b'path:' + path, seen_from).last() - return None if rev is None else repo[rev] + if path.endswith(b'/'): + dir_prefix = path + else: + dir_prefix = path + b'/' + + for changeset in ancestors_inclusive(seen_from): + files = changeset.files() + # TODO some performance improvement possible with a binary + # search if changeset files are guaranteed to be sorted + # but it'd probably make more of a difference in a Rust impl + for fpath in files: + if fpath == path or fpath.startswith(dir_prefix): + return changeset def blamelines(repo, ctx, file):