Skip to content

Commit ac67dfd

Browse files
thorsten-kleincarlescufi
authored andcommitted
added support for 'update --auto-cache'
This option automatically handles the specified auto-cache by cloning the repository to the specified location into a subdirectory named according to remote url (hashed name). After a repository is cloned from the local cache into the workspace, the remote URL is updated to point to the actual remote. By this subsequent updates will fetch from remote. Only the local auto-cache repositories are automatically synced. Signed-off-by: Thorsten Klein <thorsten.klein@bshg.com>
1 parent e0869c9 commit ac67dfd

3 files changed

Lines changed: 360 additions & 8 deletions

File tree

src/west/app/project.py

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'''West project commands'''
77

88
import argparse
9+
import hashlib
910
import logging
1011
import os
1112
import shlex
@@ -15,7 +16,7 @@
1516
import textwrap
1617
import time
1718
from functools import partial
18-
from os.path import abspath, relpath
19+
from os.path import abspath, basename, relpath
1920
from pathlib import Path, PurePath
2021
from time import perf_counter
2122
from urllib.parse import urlparse
@@ -1071,6 +1072,73 @@ def do_add_parser(self, parser_adder):
10711072
help='''cached repositories are in the same relative
10721073
paths as the workspace being updated. This cache has
10731074
lower priority (Prio 1).''')
1075+
group.add_argument('--auto-cache',
1076+
help='''automatically setup local cache repositories
1077+
in a flat folder hierarchy, but with an additional
1078+
subfolder (hashed name) for different remote URLs.
1079+
Each local cache repository is automatically cloned
1080+
on first usage and synced on subsequent clones.
1081+
This cache has the lowest priority (Prio 2).''')
1082+
1083+
parser.epilog = textwrap.dedent('''\
1084+
CACHING
1085+
-------
1086+
Projects are typically initialized by fetching from remote URLs.
1087+
However, they can also be cloned from local caches on the file system.
1088+
Three types of caches are supported, and they may be used together.
1089+
They are searched in the following order of priority:
1090+
- Priority 0: --name-cache
1091+
- Priority 1: --path-cache
1092+
- Priority 2: --auto-cache
1093+
1094+
When using local caches, after the initial setup of a workspace the
1095+
remote URL of each repo is switched back to its original remote.
1096+
As a result, subsequent manual updates (git pull) fetch changes
1097+
directly from the original remote.
1098+
Also in case of name-cache and path-cache, the local caches are not used
1099+
during subsequent updates. They are only used during the initial
1100+
workspace setup.
1101+
Only in case of auto-cache the 'west update' process updates the local
1102+
caches first, which then serve as the source for pulling changes into
1103+
the workspace.
1104+
1105+
Example: Assume your manifest describes this workspace structure:
1106+
(workspace)
1107+
├── bar
1108+
└── subdir
1109+
└── foo
1110+
1111+
The local caches must be structured as follows:
1112+
1113+
> Name Cache
1114+
Repositories should be present in a flat folder hierarchy. The cache is
1115+
searched by the repository name (according to the workspace manifest).
1116+
(name cache directory)
1117+
├── bar
1118+
└── foo
1119+
1120+
> Path Cache
1121+
Repositories should be present in a folder hierarchy that matches the
1122+
workspace's path layout.
1123+
(path cache directory)
1124+
├── bar
1125+
└── subdir
1126+
└── foo
1127+
1128+
> Auto Cache
1129+
Use this when you prefer not to manage local caches manually. The complete
1130+
folder hierarchy is setup automatically. Each repository is stored under a
1131+
directory named after the basename of its remote URL. To prevent conflicts
1132+
between repos with same name, a hash of the remote URL is used as subfolder.
1133+
Note: Each local cache repo is automatically synced on subsequent updates.
1134+
(auto cache directory)
1135+
├── bar.git
1136+
│ ├── <hash>
1137+
│ └── <hash>.info # contains metadata about the hash
1138+
└── foo.git
1139+
├── <hash>
1140+
└── <hash>.info # contains metadata about the hash
1141+
''')
10741142

10751143
group = parser.add_argument_group(
10761144
title='fetching behavior',
@@ -1153,6 +1221,7 @@ def init_state(self, args):
11531221
self.narrow = args.narrow or config.getboolean('update.narrow')
11541222
self.path_cache = args.path_cache or config.get('update.path-cache')
11551223
self.name_cache = args.name_cache or config.get('update.name-cache')
1224+
self.auto_cache = args.auto_cache or config.get('update.auto-cache')
11561225
self.sync_submodules = config.getboolean('update.sync-submodules',
11571226
default=True)
11581227

@@ -1363,13 +1432,19 @@ def update_submodules(self, project):
13631432
'--', submodule.path])
13641433
ref = []
13651434
if (cache_dir):
1435+
# check if the submodule cache is present within the project cache
13661436
submodule_ref = Path(cache_dir, submodule.path)
1367-
if any(os.scandir(submodule_ref)):
1437+
if submodule_ref.is_dir() and any(os.scandir(submodule_ref)):
13681438
ref = ['--reference', os.fspath(submodule_ref)]
13691439
self.small_banner(f'using reference from: {submodule_ref}')
13701440
self.dbg(
13711441
f'found {submodule.path} in --path-cache {submodule_ref}',
13721442
level=Verbosity.DBG_MORE)
1443+
else:
1444+
# The submodule is not cached yet, so the original remote
1445+
# url is used for updating.
1446+
pass
1447+
13731448
project.git(config_opts +
13741449
['submodule', 'update',
13751450
'--init', submodules_update_strategy,
@@ -1503,12 +1578,53 @@ def ensure_cloned(self, project, stats, take_stats):
15031578
if take_stats:
15041579
stats['init'] = perf_counter() - start
15051580

1581+
def create_auto_cache_info(self, project, cache_dir):
1582+
# auto-cache helper: Create some short informal file with basic info
1583+
# about the local cache folder.
1584+
content = textwrap.dedent(f"""
1585+
The following local cache directory was automatically created by west:
1586+
- Local Cache: {basename(cache_dir)}
1587+
- Project Url: {project.url}
1588+
""")
1589+
with open(cache_dir + '.info', 'w') as f:
1590+
f.write(content)
1591+
1592+
def handle_auto_cache(self, project):
1593+
# update() helper. Initialize the specified cache directory if it has
1594+
# not been cloned yet. If the cache directory is already existing, it
1595+
# will be synced with remote.
1596+
auto_cache_dir = self.project_auto_cache(project)
1597+
1598+
if not auto_cache_dir:
1599+
# nothing to do: auto-cache directory was not specified
1600+
return
1601+
1602+
cache_dir = self.project_cache(project)
1603+
if cache_dir != auto_cache_dir:
1604+
# another cache was specified with higher priority.
1605+
return
1606+
1607+
if not Path(cache_dir).exists():
1608+
# The cache has not been created yet at the expected location.
1609+
# Ensure that its parent directory exist before cloning anything.
1610+
# Then clone the repository into the local cache.
1611+
cache_dir_parent = Path(cache_dir).parent
1612+
cache_dir_parent.mkdir(parents=True, exist_ok=True)
1613+
project.git(['clone', '--mirror', '--', project.url, os.fspath(cache_dir)],
1614+
cwd=cache_dir_parent)
1615+
self.create_auto_cache_info(project, cache_dir)
1616+
else:
1617+
# The local cache already exists. Sync it with remote.
1618+
project.git(['remote', 'update', '--prune'], cwd=cache_dir)
1619+
15061620
def init_project(self, project):
15071621
# update() helper. Initialize an uncloned project repository.
15081622
# If there's a local clone available, it uses that. Otherwise,
15091623
# it just creates the local repository and sets up the
15101624
# convenience remote without fetching anything from the network.
15111625

1626+
self.handle_auto_cache(project)
1627+
15121628
cache_dir = self.project_cache(project)
15131629

15141630
if cache_dir is None:
@@ -1558,6 +1674,15 @@ def init_project(self, project):
15581674
# f'refs/remotes/{project.remote_name}/{branch}'.
15591675
project.git(['update-ref', '-d', branch])
15601676

1677+
def project_auto_cache(self, project):
1678+
if self.auto_cache is None:
1679+
return None
1680+
# get the location of the project within the auto-cache.
1681+
# Note: The url is hashed and used as a subfolder to accomodate
1682+
# changes in the manifest.
1683+
subdir_hash = hashlib.md5(project.url.encode('utf-8')).hexdigest()
1684+
return os.fspath(Path(self.auto_cache) / basename(project.url) / subdir_hash)
1685+
15611686
def project_cache(self, project):
15621687
# Find the absolute path to a pre-existing local clone of a project
15631688
# and return it. If the search fails, return None.
@@ -1584,6 +1709,12 @@ def project_cache(self, project):
15841709
self.dbg(
15851710
f'{project.path} not in --path-cache {self.path_cache}',
15861711
level=Verbosity.DBG_MORE)
1712+
if self.auto_cache is not None:
1713+
project_auto_cache = self.project_auto_cache(project)
1714+
self.dbg(
1715+
f'auto-caching {project.path} in {project_auto_cache}',
1716+
level=Verbosity.DBG_MORE)
1717+
return os.fspath(project_auto_cache)
15871718

15881719
return None
15891720

@@ -1647,12 +1778,20 @@ def fetch(self, project, stats, take_stats):
16471778
tags = (['--tags'] if not self.narrow else [])
16481779
clone_depth = (['--depth', str(project.clone_depth)] if
16491780
project.clone_depth else [])
1781+
1782+
# automatically fetch the auto-cache so that it is up-to-date
1783+
self.handle_auto_cache(project)
1784+
1785+
# fetch workspace repository from freshly synced auto-cache if
1786+
# auto-cache is used. Otherwise fetch directly from remote url.
1787+
fetch_url = self.project_auto_cache(project) or project.url
1788+
16501789
# -f is needed to avoid errors in case multiple remotes are
16511790
# present, at least one of which contains refs that can't be
16521791
# fast-forwarded to our local ref space.
16531792
project.git(['fetch', '-f'] + tags + clone_depth +
16541793
self.args.fetch_opt +
1655-
['--', project.url, refspec])
1794+
['--', fetch_url, refspec])
16561795

16571796
if take_stats:
16581797
stats['fetch'] = perf_counter() - start

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,11 @@ def rev_parse(repo, revision):
481481
out = subprocess.check_output([GIT, 'rev-parse', revision], cwd=repo)
482482
return out.decode(sys.getdefaultencoding()).strip()
483483

484+
def rev_list(repo):
485+
repo = os.fspath(repo)
486+
out = subprocess.check_output([GIT, 'rev-list', '--all'], cwd=repo)
487+
return out.decode(sys.getdefaultencoding()).strip()
488+
484489
def check_proj_consistency(actual, expected):
485490
# Check equality of all project fields (projects themselves are
486491
# not comparable), with extra semantic consistency checking

0 commit comments

Comments
 (0)