diff --git a/hgitaly/service/commit.py b/hgitaly/service/commit.py index 798ac9c4d639537f8750fd3bfbf6e1f4dc7c91cd_aGdpdGFseS9zZXJ2aWNlL2NvbW1pdC5weQ==..830f307161b0530b86b26bf81831a6dfc31f690a_aGdpdGFseS9zZXJ2aWNlL2NvbW1pdC5weQ== 100644 --- a/hgitaly/service/commit.py +++ b/hgitaly/service/commit.py @@ -669,5 +669,7 @@ # subtrees first, then regular files, each one in lexicographical order subtrees, file_paths = manifest.miner(from_ctx).ls_dir(req_path) + changesets = latest_changesets_for_paths(subtrees, file_paths, + from_ctx) all_paths = subtrees all_paths.extend(file_paths) @@ -672,7 +674,6 @@ all_paths = subtrees all_paths.extend(file_paths) - for chunk in chunked(all_paths[offset:offset + limit]): yield ListLastCommitsForTreeResponse( commits=[ message.commit_for_tree( @@ -675,8 +676,8 @@ for chunk in chunked(all_paths[offset:offset + limit]): yield ListLastCommitsForTreeResponse( commits=[ message.commit_for_tree( - latest_changeset_for_path(path, from_ctx), + changesets[path], path ) for path in chunk @@ -1049,6 +1050,45 @@ return changeset +def latest_changesets_for_paths(subdirs, file_paths, seen_from): + """Return latest ancestor of ``seen_from`` that touched the given path. + + :param bytes path: subdir or file + :param seen_from: changectx + """ + dir_prefixes = set(d.rstrip(b'/') + b'/' for d in subdirs) + file_paths = set(file_paths) + + latest_changesets = {} + + for changeset in ancestors_inclusive(seen_from): + if not dir_prefixes and not file_paths: + break + + for cfp in 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 dp in dir_prefixes: + if cfp.startswith(dp): + latest_changesets[dp.rstrip(b'/')] = changeset + dir_prefixes.discard(dp) + break + else: + for fp in file_paths: + if fp == cfp: + latest_changesets[fp] = changeset + break + else: + continue + file_paths.discard(fp) + continue + + dir_prefixes.discard(dp) + + return latest_changesets + + def blamelines(repo, ctx, file): """Yield blame lines of a file. """ diff --git a/hgitaly/service/tests/test_commit.py b/hgitaly/service/tests/test_commit.py index 798ac9c4d639537f8750fd3bfbf6e1f4dc7c91cd_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfY29tbWl0LnB5..830f307161b0530b86b26bf81831a6dfc31f690a_aGdpdGFseS9zZXJ2aWNlL3Rlc3RzL3Rlc3RfY29tbWl0LnB5 100644 --- a/hgitaly/service/tests/test_commit.py +++ b/hgitaly/service/tests/test_commit.py @@ -1140,6 +1140,13 @@ (b'foo', sha3), ] + # case when we don't have to walk the entire changelog + (wrapper.path / 'more').mkdir() + sha5 = wrapper.commit_file('more/foo').hex().decode() + assert do_rpc(sha5, b'more/') == [ + (b'more/foo', sha5), + ] + def test_raw_blame(grpc_channel, server_repos_root): grpc_stub = CommitServiceStub(grpc_channel)