diff options
| author | Josip Sokcevic <sokcevic@chromium.org> | 2024-03-07 22:18:58 +0000 |
|---|---|---|
| committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2024-03-08 17:58:24 +0000 |
| commit | 46790229fcdb041c414a27035b72cbc0d2e78af6 (patch) | |
| tree | 67dd6b331edd153680e856b8859d99c634a4995d | |
| parent | edadb25c0270398e9afa3eb0093d6b94aa51c3f4 (diff) | |
| download | git-repo-46790229fcdb041c414a27035b72cbc0d2e78af6.tar.gz | |
sync: Fix sorting for nested projects
The current logic to create checkout layers doesn't work in all cases.
For example, let's assume there are three projects: "foo", "foo/bar" and
"foo-bar". Sorting lexicographical order is incorrect as foo-bar would
be placed between foo and foo/bar, breaking layering logic.
Instead, we split filepaths based using path delimiter (always /) and
then use lexicographical sort.
BUG=b:325119758
TEST=./run_tests, manual sync on chromiumos repository
Change-Id: I76924c3cc6ba2bb860d7a3e48406a6bba8f58c10
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/412338
Tested-by: Josip Sokcevic <sokcevic@google.com>
Commit-Queue: Josip Sokcevic <sokcevic@google.com>
Reviewed-by: George Engelbrecht <engeg@google.com>
| -rw-r--r-- | subcmds/sync.py | 10 | ||||
| -rw-r--r-- | tests/test_subcmds_sync.py | 45 |
2 files changed, 42 insertions, 13 deletions
diff --git a/subcmds/sync.py b/subcmds/sync.py index 7acb6e5b..113e7a67 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py | |||
| @@ -102,9 +102,13 @@ def _SafeCheckoutOrder(checkouts: List[Project]) -> List[List[Project]]: | |||
| 102 | 102 | ||
| 103 | # depth_stack contains a current stack of parent paths. | 103 | # depth_stack contains a current stack of parent paths. |
| 104 | depth_stack = [] | 104 | depth_stack = [] |
| 105 | # checkouts are iterated in asc order by relpath. That way, it can easily be | 105 | # Checkouts are iterated in the hierarchical order. That way, it can easily |
| 106 | # determined if the previous checkout is parent of the current checkout. | 106 | # be determined if the previous checkout is parent of the current checkout. |
| 107 | for checkout in sorted(checkouts, key=lambda x: x.relpath): | 107 | # We are splitting by the path separator so the final result is |
| 108 | # hierarchical, and not just lexicographical. For example, if the projects | ||
| 109 | # are: foo, foo/bar, foo-bar, lexicographical order produces foo, foo-bar | ||
| 110 | # and foo/bar, which doesn't work. | ||
| 111 | for checkout in sorted(checkouts, key=lambda x: x.relpath.split("/")): | ||
| 108 | checkout_path = Path(checkout.relpath) | 112 | checkout_path = Path(checkout.relpath) |
| 109 | while depth_stack: | 113 | while depth_stack: |
| 110 | try: | 114 | try: |
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py index 13e23e34..8dde687c 100644 --- a/tests/test_subcmds_sync.py +++ b/tests/test_subcmds_sync.py | |||
| @@ -304,29 +304,54 @@ class LocalSyncState(unittest.TestCase): | |||
| 304 | self.assertEqual(self.state.GetFetchTime(projA), 5) | 304 | self.assertEqual(self.state.GetFetchTime(projA), 5) |
| 305 | 305 | ||
| 306 | 306 | ||
| 307 | class FakeProject: | ||
| 308 | def __init__(self, relpath): | ||
| 309 | self.relpath = relpath | ||
| 310 | |||
| 311 | def __str__(self): | ||
| 312 | return f"project: {self.relpath}" | ||
| 313 | |||
| 314 | def __repr__(self): | ||
| 315 | return str(self) | ||
| 316 | |||
| 317 | |||
| 307 | class SafeCheckoutOrder(unittest.TestCase): | 318 | class SafeCheckoutOrder(unittest.TestCase): |
| 308 | def test_no_nested(self): | 319 | def test_no_nested(self): |
| 309 | p_f = mock.MagicMock(relpath="f") | 320 | p_f = FakeProject("f") |
| 310 | p_foo = mock.MagicMock(relpath="foo") | 321 | p_foo = FakeProject("foo") |
| 311 | out = sync._SafeCheckoutOrder([p_f, p_foo]) | 322 | out = sync._SafeCheckoutOrder([p_f, p_foo]) |
| 312 | self.assertEqual(out, [[p_f, p_foo]]) | 323 | self.assertEqual(out, [[p_f, p_foo]]) |
| 313 | 324 | ||
| 314 | def test_basic_nested(self): | 325 | def test_basic_nested(self): |
| 315 | p_foo = p_foo = mock.MagicMock(relpath="foo") | 326 | p_foo = p_foo = FakeProject("foo") |
| 316 | p_foo_bar = mock.MagicMock(relpath="foo/bar") | 327 | p_foo_bar = FakeProject("foo/bar") |
| 317 | out = sync._SafeCheckoutOrder([p_foo, p_foo_bar]) | 328 | out = sync._SafeCheckoutOrder([p_foo, p_foo_bar]) |
| 318 | self.assertEqual(out, [[p_foo], [p_foo_bar]]) | 329 | self.assertEqual(out, [[p_foo], [p_foo_bar]]) |
| 319 | 330 | ||
| 320 | def test_complex_nested(self): | 331 | def test_complex_nested(self): |
| 321 | p_foo = mock.MagicMock(relpath="foo") | 332 | p_foo = FakeProject("foo") |
| 322 | p_foo_bar = mock.MagicMock(relpath="foo/bar") | 333 | p_foobar = FakeProject("foobar") |
| 323 | p_foo_bar_baz_baq = mock.MagicMock(relpath="foo/bar/baz/baq") | 334 | p_foo_dash_bar = FakeProject("foo-bar") |
| 324 | p_bar = mock.MagicMock(relpath="bar") | 335 | p_foo_bar = FakeProject("foo/bar") |
| 336 | p_foo_bar_baz_baq = FakeProject("foo/bar/baz/baq") | ||
| 337 | p_bar = FakeProject("bar") | ||
| 325 | out = sync._SafeCheckoutOrder( | 338 | out = sync._SafeCheckoutOrder( |
| 326 | [p_foo_bar_baz_baq, p_foo, p_foo_bar, p_bar] | 339 | [ |
| 340 | p_foo_bar_baz_baq, | ||
| 341 | p_foo, | ||
| 342 | p_foobar, | ||
| 343 | p_foo_dash_bar, | ||
| 344 | p_foo_bar, | ||
| 345 | p_bar, | ||
| 346 | ] | ||
| 327 | ) | 347 | ) |
| 328 | self.assertEqual( | 348 | self.assertEqual( |
| 329 | out, [[p_bar, p_foo], [p_foo_bar], [p_foo_bar_baz_baq]] | 349 | out, |
| 350 | [ | ||
| 351 | [p_bar, p_foo, p_foo_dash_bar, p_foobar], | ||
| 352 | [p_foo_bar], | ||
| 353 | [p_foo_bar_baz_baq], | ||
| 354 | ], | ||
| 330 | ) | 355 | ) |
| 331 | 356 | ||
| 332 | 357 | ||
