66'''West project commands'''
77
88import argparse
9+ import hashlib
910import logging
1011import os
1112import shlex
1516import textwrap
1617import time
1718from functools import partial
18- from os .path import abspath , relpath
19+ from os .path import abspath , basename , relpath
1920from pathlib import Path , PurePath
2021from time import perf_counter
2122from 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
0 commit comments