From f6740d55ff80bc6e16da5c3df0ee1ba2235d6629 Mon Sep 17 00:00:00 2001 From: James Moger Date: Mon, 26 Sep 2011 15:33:19 -0400 Subject: [PATCH] Implemented a Federation Client. Bare clone tweaks. Documentation. --- .gitignore | 2 + NOTICE | 8 ++ build.xml | 67 ++++++++- distrib/federation.properties | 69 +++++++++ distrib/gitblit.properties | 1 + docs/01_setup.mkd | 2 +- docs/02_federation.mkd | 31 +++- docs/04_design.mkd | 1 + src/com/gitblit/FederationClient.java | 133 ++++++++++++++++++ src/com/gitblit/FederationClientLauncher.java | 54 +++++++ src/com/gitblit/FederationPullExecutor.java | 54 ++++++- src/com/gitblit/GitBlit.java | 87 ++---------- src/com/gitblit/GitBlitServer.java | 2 +- src/com/gitblit/build/Build.java | 13 ++ src/com/gitblit/models/FederationModel.java | 2 + src/com/gitblit/utils/FederationUtils.java | 93 ++++++++++++ src/com/gitblit/utils/JGitUtils.java | 21 ++- tests/com/gitblit/tests/GitBlitSuite.java | 2 +- tools/GenJar.jar | Bin 0 -> 32710 bytes 19 files changed, 541 insertions(+), 101 deletions(-) create mode 100644 distrib/federation.properties create mode 100644 src/com/gitblit/FederationClient.java create mode 100644 src/com/gitblit/FederationClientLauncher.java create mode 100644 tools/GenJar.jar diff --git a/.gitignore b/.gitignore index 5236c647..389f6635 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ /war /*.war /proposals +/*.jar +/federation.properties \ No newline at end of file diff --git a/NOTICE b/NOTICE index 8c8a2a90..41b61f12 100644 --- a/NOTICE +++ b/NOTICE @@ -142,6 +142,14 @@ ant-googlecode New BSD License http://code.google.com/p/ant-googlecode + +--------------------------------------------------------------------------- +GenJar +--------------------------------------------------------------------------- + GenJar, released under the + Apache Software License, Version 1.1. + + http://genjar.sourceforge.net --------------------------------------------------------------------------- Fancybox image viewer diff --git a/build.xml b/build.xml index 10259434..da0dee09 100644 --- a/build.xml +++ b/build.xml @@ -5,6 +5,9 @@ + + + @@ -81,6 +84,7 @@ + @@ -104,7 +108,7 @@ - + @@ -116,8 +120,9 @@ + - + @@ -144,6 +149,7 @@ + @@ -255,6 +261,9 @@ + + + @@ -362,6 +371,45 @@ + + + Building Gitblit Federation Client ${gb.version} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -539,6 +590,16 @@ targetfilename="gitblit-${gb.version}.war" summary="Gitblit WAR v${gb.version} (standard WAR webapp for servlet containers)" labels="Featured, Type-Package, OpSys-All" /> + + + diff --git a/distrib/federation.properties b/distrib/federation.properties new file mode 100644 index 00000000..e7e17f7c --- /dev/null +++ b/distrib/federation.properties @@ -0,0 +1,69 @@ +# +# Git Repository Settings +# + +# Base folder for repositories +# Use forward slashes even on Windows!! +# e.g. c:/gitrepos +# +# SINCE 0.5.0 +# RESTART REQUIRED +git.repositoriesFolder = git + +# Search the repositories folder subfolders for other repositories. +# Repositories MAY NOT be nested (i.e. one repository within another) +# but they may be grouped together in subfolders. +# e.g. c:/gitrepos/libraries/mylibrary.git +# c:/gitrepos/libraries/myotherlibrary.git +# +# SINCE 0.5.0 +git.searchRepositoriesSubfolders = true + +# Your federation name is used for federation status acknowledgments. If it is +# unset, and you elect to send a status acknowledgment, your Gitblit instance +# will be identified by its hostname, if available, else your internal ip address. +# The source Gitblit instance will also append your external IP address to your +# identification to differentiate multiple pulling systems behind a single proxy. +# +# SINCE 0.6.0 +federation.name = + +# Federation pull registrations +# Registrations are read once, at startup. +# +# RESTART REQUIRED +# +# frequency: +# The shortest frequency allowed is every 5 minutes +# Decimal frequency values are cast to integers +# Frequency values may be specified in mins, hours, or days +# Values that can not be parsed default to *federation.defaultFrequency* +# +# folder: +# if blank, the folder is *git.repositoriesFolder* +# if specified, the folder is relative to *git.repositoriesFolder* +# +# mergeAccounts: +# if true, remote accounts and their permissions are merged into your +# users.properties file +# +# notifyOnError: +# if true and the mail configuration is properly set, administrators will be +# notified by email of pull failures +# +# include and exclude: +# space-separated list of repositories to include or exclude from pull +# may be * wildcard to include or exclude all +# may use fuzzy match (e.g. org.eclipse.*) + +# +# (Nearly) Perfect Mirror example +# + +#federation.example1.url = https://go.gitblit.com +#federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4 +#federation.example1.frequency = 120 mins +#federation.example1.folder = +#federation.example1.bare = true +#federation.example1.mirror = true +#federation.example1.mergeAccounts = true diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index 734dddd0..e8acfa93 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -419,6 +419,7 @@ federation.sets = #federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4 #federation.example1.frequency = 120 mins #federation.example1.folder = +#federation.example1.bare = true #federation.example1.mirror = true #federation.example1.mergeAccounts = true diff --git a/docs/01_setup.mkd b/docs/01_setup.mkd index 1c8db122..91900e1e 100644 --- a/docs/01_setup.mkd +++ b/docs/01_setup.mkd @@ -130,7 +130,7 @@ All repository settings are stored within the repository `.git/config` file unde accessRestriction = clone isFrozen = false showReadme = false - excludeFromFederation = false + federationStrategy = FEDERATE_THIS isFederated = false federationSets = diff --git a/docs/02_federation.mkd b/docs/02_federation.mkd index e5cc797f..e004a27a 100644 --- a/docs/02_federation.mkd +++ b/docs/02_federation.mkd @@ -37,7 +37,7 @@ Changing your *federation.passphrase* will break any registrations you have esta If you want your repositories (and optionally users accounts and settings) to be pulled by another Gitblit instance, you need to register your origin Gitblit instance with a pulling Gitblit instance by providing the url of your Gitblit instance and a federation token. -Gitblit generates the following federation tokens: +Gitblit generates the following standard federation tokens: %BEGINCODE% String allToken = SHA1(passphrase + "-ALL"); String usersAndRepositoriesToken = SHA1(passphrase + "-USERS_AND_REPOSITORIES"); @@ -52,9 +52,11 @@ Individual Gitblit repository configurations such as *description* and *accessRe If *federation.passphrase* has a non-empty value, the federation tokens are displayed in the log file and are visible, to administrators, in the web ui. +The three standard tokens grant access to ALL your non-excluded repositories. However, if you only want to specify different groups of repositories to be federated then you need to define *federation sets*. + #### Federation Sets -Federation Sets (*federation.sets*) are named groups of repositories. The Federation Sets are defined in `gitblit.properties` and are available for selection in the repository settings page. You can assign a repository to one or more sets and then distribute the token for the set. This allows you to grant federation pull access to a subset of your available repositories. Tokens for federation sets only grant pull access for the member repositories. +Federation Sets (*federation.sets*) are named groups of repositories. The Federation Sets are defined in `gitblit.properties` and are available for selection in the repository settings page. You can assign a repository to one or more sets and then distribute the federation token for the set. This allows you to grant federation pull access to a subset of your available repositories. Tokens for federation sets only grant pull access for the member repositories. ### Federation Proposals (Origin Gitblit Instance) @@ -91,7 +93,7 @@ You may view the details of a proposal by scrolling down to the bottom of the re ### Excluding Repositories (Origin Gitblit Instance) -You may exclude a repository from being pulled by a federated Gitblit instance by setting its *federation strategy* to EXCLUDE in the repository's settings page. +You may exclude a repository from being pulled by any federated Gitblit instance by setting its *federation strategy* to EXCLUDE in the repository's settings page. ### Excluding Repositories (Pulling Gitblit Instance) @@ -286,4 +288,25 @@ The repositories will be put in *git.repositoriesFolder*/example4. federation.example4.bare = true federation.example4.mirror = true federation.example4.exclude = * - federation.example4.include = somerepo.git \ No newline at end of file + federation.example4.include = somerepo.git + +## Federation Client + +Instead of setting up a full-blown pulling Gitblit instance, you can also use the [federation client](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%) command-line utility. This is a packaged subset of the federation feature in a smaller, simpler command-line only tool. + +The *federation client* relies on many of the same dependencies as Gitblit and will download them on first execution. + +### federation.properties +You may use the `federation.properties` file to configure one or more Gitblit instances that you want to pull from. This file is a subset of the standard `gitblit.properties` file. + +By default this tool does not daemonize itself; it executes and then quits. This allows you to use the native scheduling feature of your OS. Of course, if you'd rather use Gitblit's scheduler you may use that by specifying the `--daemon` parameter. + +### Command-Line Parameters +Instead of using `federation.properties` you may directly specify a Gitblit instance to pull from with command-line parameters. + + java -jar fedclient.jar --url https://go.gitblit.com --mirror --bare --token 123456789 + --repositoriesFolder c:/mymirror + + java -jar fedclient.jar --url https://go.gitblit.com --mirror --bare --token 123456789 + --repositoriesFolder c:/mymirror --daemon --frequency "24 hours" + diff --git a/docs/04_design.mkd b/docs/04_design.mkd index 70cea7c7..523d31b9 100644 --- a/docs/04_design.mkd +++ b/docs/04_design.mkd @@ -39,6 +39,7 @@ The following dependencies are automatically downloaded by Gitblit GO (or alread - [JUnit](http://junit.org) (Common Public License) - [commons-net](http://commons.apache.org/net) (Apache 2.0) - [ant-googlecode](http://code.google.com/p/ant-googlecode) (New BSD) +- [GenJar](http://genjar.sourceforge.net) (Apache 1.1) ## Building from Source [Eclipse](http://eclipse.org) is recommended for development as the project settings are preconfigured. diff --git a/src/com/gitblit/FederationClient.java b/src/com/gitblit/FederationClient.java new file mode 100644 index 00000000..daa9bfbf --- /dev/null +++ b/src/com/gitblit/FederationClient.java @@ -0,0 +1,133 @@ +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterException; +import com.beust.jcommander.Parameters; +import com.gitblit.models.FederationModel; +import com.gitblit.utils.FederationUtils; +import com.gitblit.utils.StringUtils; + +/** + * Command-line client to pull federated Gitblit repositories. + * + * @author James Moger + * + */ +public class FederationClient { + + public static void main(String[] args) { + Params params = new Params(); + JCommander jc = new JCommander(params); + try { + jc.parse(args); + } catch (ParameterException t) { + usage(jc, t); + } + + IStoredSettings settings = new FileSettings(params.registrationsFile); + List registrations = new ArrayList(); + if (StringUtils.isEmpty(params.url)) { + registrations.addAll(FederationUtils.getFederationRegistrations(settings)); + } else { + if (StringUtils.isEmpty(params.token)) { + System.out.println("Must specify --token parameter!"); + System.exit(0); + } + FederationModel model = new FederationModel("Gitblit"); + model.url = params.url; + model.token = params.token; + model.mirror = params.mirror; + model.bare = params.bare; + model.frequency = params.frequency; + model.folder = ""; + registrations.add(model); + } + if (registrations.size() == 0) { + System.out.println("No Federation Registrations! Nothing to do."); + System.exit(0); + } + + System.out.println("Gitblit Federation Client v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")"); + + // command-line specified repositories folder + if (!StringUtils.isEmpty(params.repositoriesFolder)) { + settings.overrideSetting(Keys.git.repositoriesFolder, new File( + params.repositoriesFolder).getAbsolutePath()); + } + + // configure the Gitblit singleton for minimal, non-server operation + GitBlit.self().configureContext(settings, false); + FederationPullExecutor executor = new FederationPullExecutor(registrations, params.isDaemon); + executor.run(); + if (!params.isDaemon) { + System.out.println("Finished."); + System.exit(0); + } + } + + private static void usage(JCommander jc, ParameterException t) { + System.out.println(Constants.getGitBlitVersion()); + System.out.println(); + if (t != null) { + System.out.println(t.getMessage()); + System.out.println(); + } + + if (jc != null) { + jc.usage(); + } + System.exit(0); + } + + /** + * JCommander Parameters class for FederationClient. + */ + @Parameters(separators = " ") + private static class Params { + + @Parameter(names = { "--registrations" }, description = "Gitblit Federation Registrations File", required = false) + public String registrationsFile = "federation.properties"; + + @Parameter(names = { "--daemon" }, description = "Runs in daemon mode to schedule and pull repositories", required = false) + public boolean isDaemon; + + @Parameter(names = { "--url" }, description = "URL of Gitblit instance to mirror from", required = false) + public String url; + + @Parameter(names = { "--mirror" }, description = "Mirror repositories", required = false) + public boolean mirror; + + @Parameter(names = { "--bare" }, description = "Create bare repositories", required = false) + public boolean bare; + + @Parameter(names = { "--token" }, description = "Federation Token", required = false) + public String token; + + @Parameter(names = { "--frequency" }, description = "Period to wait between pull attempts (requires --daemon)", required = false) + public String frequency = "60 mins"; + + @Parameter(names = { "--repositoriesFolder" }, description = "Destination folder for cloned repositories", required = false) + public String repositoriesFolder; + + } +} diff --git a/src/com/gitblit/FederationClientLauncher.java b/src/com/gitblit/FederationClientLauncher.java new file mode 100644 index 00000000..80f5a3d9 --- /dev/null +++ b/src/com/gitblit/FederationClientLauncher.java @@ -0,0 +1,54 @@ +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import com.gitblit.build.Build; + +/** + * Downloads dependencies and launches command-line Federation client. + * + * @author James Moger + * + */ +public class FederationClientLauncher { + + public static void main(String[] args) { + // download federation client runtime dependencies + Build.federationClient(); + + File libFolder = new File("ext"); + List jars = Launcher.findJars(libFolder.getAbsoluteFile()); + + // sort the jars by name and then reverse the order so the newer version + // of the library gets loaded in the event that this is an upgrade + Collections.sort(jars); + Collections.reverse(jars); + for (File jar : jars) { + try { + Launcher.addJarFile(jar); + } catch (IOException e) { + + } + } + + FederationClient.main(args); + } +} diff --git a/src/com/gitblit/FederationPullExecutor.java b/src/com/gitblit/FederationPullExecutor.java index f27ea3c2..ef089d03 100644 --- a/src/com/gitblit/FederationPullExecutor.java +++ b/src/com/gitblit/FederationPullExecutor.java @@ -15,6 +15,8 @@ */ package com.gitblit; +import static org.eclipse.jgit.lib.Constants.DOT_GIT_EXT; + import java.io.File; import java.io.FileOutputStream; import java.net.InetAddress; @@ -23,14 +25,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.slf4j.Logger; @@ -57,6 +62,8 @@ public class FederationPullExecutor implements Runnable { private final List registrations; + private final boolean isDaemon; + /** * Constructor for specifying a single federation registration. This * constructor is used to schedule the next pull execution. @@ -64,7 +71,7 @@ public class FederationPullExecutor implements Runnable { * @param registration */ private FederationPullExecutor(FederationModel registration) { - this(Arrays.asList(registration)); + this(Arrays.asList(registration), true); } /** @@ -73,9 +80,13 @@ public class FederationPullExecutor implements Runnable { * on each registrations frequency setting. * * @param registrations + * @param isDaemon + * if true, registrations are rescheduled in perpetuity. if false, + * the federation pull operation is executed once. */ - public FederationPullExecutor(List registrations) { + public FederationPullExecutor(List registrations, boolean isDaemon) { this.registrations = registrations; + this.isDaemon = isDaemon; } /** @@ -109,7 +120,9 @@ public class FederationPullExecutor implements Runnable { "Failed to pull from federated gitblit ({0} @ {1})", registration.name, registration.url), t); } finally { - schedule(registration); + if (isDaemon) { + schedule(registration); + } } } } @@ -149,12 +162,25 @@ public class FederationPullExecutor implements Runnable { continue; } + // Determine local repository name String repositoryName; if (StringUtils.isEmpty(registrationFolder)) { repositoryName = repository.name; } else { repositoryName = registrationFolder + "/" + repository.name; } + + if (registration.bare) { + // bare repository, ensure .git suffix + if (!repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) { + repositoryName += DOT_GIT_EXT; + } + } else { + // normal repository, strip .git suffix + if (repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) { + repositoryName = repositoryName.substring(0, repositoryName.indexOf(DOT_GIT_EXT)); + } + } // confirm that the origin of any pre-existing repository matches // the clone url @@ -164,8 +190,10 @@ public class FederationPullExecutor implements Runnable { StoredConfig config = existingRepository.getConfig(); config.load(); String origin = config.getString("remote", "origin", "url"); - fetchHead = JGitUtils.getCommit(existingRepository, "refs/remotes/origin/master") - .getName(); + RevCommit commit = JGitUtils.getCommit(existingRepository, "refs/remotes/origin/master"); + if (commit != null) { + fetchHead = commit.getName(); + } existingRepository.close(); if (!origin.startsWith(registration.url)) { logger.warn(MessageFormat @@ -181,6 +209,7 @@ public class FederationPullExecutor implements Runnable { Constants.FEDERATION_USER, registration.token); logger.info(MessageFormat.format("Pulling federated repository {0} from {1} @ {2}", repository.name, registration.name, registration.url)); + CloneResult result = JGitUtils.cloneRepository(registrationFolderFile, repository.name, cloneUrl, registration.bare, credentials); Repository r = GitBlit.self().getRepository(repositoryName); @@ -196,8 +225,9 @@ public class FederationPullExecutor implements Runnable { } else { // fetch and update boolean fetched = false; - String origin = JGitUtils.getCommit(r, "refs/remotes/origin/master").getName(); - fetched = !fetchHead.equals(origin); + RevCommit commit = JGitUtils.getCommit(r, "refs/remotes/origin/master"); + String origin = commit.getName(); + fetched = fetchHead == null || !fetchHead.equals(origin); if (registration.mirror) { // mirror @@ -225,6 +255,16 @@ public class FederationPullExecutor implements Runnable { // preserve local settings repository.isFrozen = rm.isFrozen; repository.federationStrategy = rm.federationStrategy; + + // merge federation sets + Set federationSets = new HashSet(); + if (rm.federationSets != null) { + federationSets.addAll(rm.federationSets); + } + if (repository.federationSets != null) { + federationSets.addAll(repository.federationSets); + } + repository.federationSets = new ArrayList(federationSets); } // only repositories that are actually _cloned_ from the origin // Gitblit repository are marked as federated. If the origin diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index c2b214b4..62b93e7c 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -61,6 +61,7 @@ import com.gitblit.models.FederationModel; import com.gitblit.models.FederationProposal; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; +import com.gitblit.utils.FederationUtils; import com.gitblit.utils.JGitUtils; import com.gitblit.utils.StringUtils; import com.google.gson.Gson; @@ -830,8 +831,8 @@ public class GitBlit implements ServletContextListener { // Schedule the federation executor List registrations = getFederationRegistrations(); if (registrations.size() > 0) { - scheduledExecutor.schedule(new FederationPullExecutor(registrations), 1, - TimeUnit.MINUTES); + FederationPullExecutor executor = new FederationPullExecutor(registrations, true); + scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES); } } @@ -843,79 +844,7 @@ public class GitBlit implements ServletContextListener { */ public List getFederationRegistrations() { if (federationRegistrations.isEmpty()) { - List keys = settings.getAllKeys(Keys.federation._ROOT); - keys.remove(Keys.federation.name); - keys.remove(Keys.federation.passphrase); - keys.remove(Keys.federation.allowProposals); - keys.remove(Keys.federation.proposalsFolder); - keys.remove(Keys.federation.defaultFrequency); - keys.remove(Keys.federation.sets); - Collections.sort(keys); - Map federatedModels = new HashMap(); - for (String key : keys) { - String value = key.substring(Keys.federation._ROOT.length() + 1); - List values = StringUtils.getStringsFromValue(value, "\\."); - String server = values.get(0); - if (!federatedModels.containsKey(server)) { - federatedModels.put(server, new FederationModel(server)); - } - String setting = values.get(1); - if (setting.equals("url")) { - // url of the origin Gitblit instance - federatedModels.get(server).url = settings.getString(key, ""); - } else if (setting.equals("token")) { - // token for the origin Gitblit instance - federatedModels.get(server).token = settings.getString(key, ""); - } else if (setting.equals("frequency")) { - // frequency of the pull operation - federatedModels.get(server).frequency = settings.getString(key, ""); - } else if (setting.equals("folder")) { - // destination folder of the pull operation - federatedModels.get(server).folder = settings.getString(key, ""); - } else if (setting.equals("bare")) { - // whether pulled repositories should be bare - federatedModels.get(server).bare = settings.getBoolean(key, true); - } else if (setting.equals("mirror")) { - // are the repositories to be true mirrors of the origin - federatedModels.get(server).mirror = settings.getBoolean(key, true); - } else if (setting.equals("mergeAccounts")) { - // merge remote accounts into local accounts - federatedModels.get(server).mergeAccounts = settings.getBoolean(key, false); - } else if (setting.equals("sendStatus")) { - // send a status acknowledgment to source Gitblit instance - // at end of git pull - federatedModels.get(server).sendStatus = settings.getBoolean(key, false); - } else if (setting.equals("notifyOnError")) { - // notify administrators on federation pull failures - federatedModels.get(server).notifyOnError = settings.getBoolean(key, false); - } else if (setting.equals("exclude")) { - // excluded repositories - federatedModels.get(server).exclusions = settings.getStrings(key); - } else if (setting.equals("include")) { - // included repositories - federatedModels.get(server).inclusions = settings.getStrings(key); - } - } - - // verify that registrations have a url and a token - for (FederationModel model : federatedModels.values()) { - if (StringUtils.isEmpty(model.url)) { - logger.warn(MessageFormat.format( - "Dropping federation registration {0}. Missing url.", model.name)); - continue; - } - if (StringUtils.isEmpty(model.token)) { - logger.warn(MessageFormat.format( - "Dropping federation registration {0}. Missing token.", model.name)); - continue; - } - // set default frequency if unspecified - if (StringUtils.isEmpty(model.frequency)) { - model.frequency = settings.getString(Keys.federation.defaultFrequency, - "60 mins"); - } - federationRegistrations.add(model); - } + federationRegistrations.addAll(FederationUtils.getFederationRegistrations(settings)); } return federationRegistrations; } @@ -1239,7 +1168,7 @@ public class GitBlit implements ServletContextListener { * * @param settings */ - public void configureContext(IStoredSettings settings) { + public void configureContext(IStoredSettings settings, boolean startFederation) { logger.info("Reading configuration from " + settings.toString()); this.settings = settings; repositoriesFolder = new File(settings.getString(Keys.git.repositoriesFolder, "git")); @@ -1268,13 +1197,15 @@ public class GitBlit implements ServletContextListener { loginService = new FileUserService(realmFile); } setUserService(loginService); - configureFederation(); mailExecutor = new MailExecutor(settings); if (mailExecutor.isReady()) { scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES); } else { logger.warn("Mail server is not properly configured. Mail services disabled."); } + if (startFederation) { + configureFederation(); + } } /** @@ -1288,7 +1219,7 @@ public class GitBlit implements ServletContextListener { if (settings == null) { // Gitblit WAR is running in a servlet container WebXmlSettings webxmlSettings = new WebXmlSettings(contextEvent.getServletContext()); - configureContext(webxmlSettings); + configureContext(webxmlSettings, true); } } diff --git a/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java index d2164f18..039f59d3 100644 --- a/src/com/gitblit/GitBlitServer.java +++ b/src/com/gitblit/GitBlitServer.java @@ -237,7 +237,7 @@ public class GitBlitServer { // Setup the GitBlit context GitBlit gitblit = GitBlit.self(); - gitblit.configureContext(settings); + gitblit.configureContext(settings, true); rootContext.addEventListener(gitblit); try { diff --git a/src/com/gitblit/build/Build.java b/src/com/gitblit/build/Build.java index 40342269..684f2787 100644 --- a/src/com/gitblit/build/Build.java +++ b/src/com/gitblit/build/Build.java @@ -115,6 +115,19 @@ public class Build { // needed for site publishing downloadFromApache(MavenObject.COMMONSNET, BuildType.RUNTIME); } + + public static void federationClient() { + downloadFromApache(MavenObject.JCOMMANDER, BuildType.RUNTIME); + downloadFromApache(MavenObject.SERVLET, BuildType.RUNTIME); + downloadFromApache(MavenObject.MAIL, BuildType.RUNTIME); + downloadFromApache(MavenObject.SLF4JAPI, BuildType.RUNTIME); + downloadFromApache(MavenObject.SLF4LOG4J, BuildType.RUNTIME); + downloadFromApache(MavenObject.LOG4J, BuildType.RUNTIME); + downloadFromApache(MavenObject.GSON, BuildType.RUNTIME); + downloadFromApache(MavenObject.JSCH, BuildType.RUNTIME); + + downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME); + } /** * Builds the Keys class based on the gitblit.properties file and inserts diff --git a/src/com/gitblit/models/FederationModel.java b/src/com/gitblit/models/FederationModel.java index efb1b322..d67ae561 100644 --- a/src/com/gitblit/models/FederationModel.java +++ b/src/com/gitblit/models/FederationModel.java @@ -73,6 +73,8 @@ public class FederationModel implements Serializable, Comparable getFederationRegistrations(IStoredSettings settings) { + List federationRegistrations = new ArrayList(); + List keys = settings.getAllKeys(Keys.federation._ROOT); + keys.remove(Keys.federation.name); + keys.remove(Keys.federation.passphrase); + keys.remove(Keys.federation.allowProposals); + keys.remove(Keys.federation.proposalsFolder); + keys.remove(Keys.federation.defaultFrequency); + keys.remove(Keys.federation.sets); + Collections.sort(keys); + Map federatedModels = new HashMap(); + for (String key : keys) { + String value = key.substring(Keys.federation._ROOT.length() + 1); + List values = StringUtils.getStringsFromValue(value, "\\."); + String server = values.get(0); + if (!federatedModels.containsKey(server)) { + federatedModels.put(server, new FederationModel(server)); + } + String setting = values.get(1); + if (setting.equals("url")) { + // url of the origin Gitblit instance + federatedModels.get(server).url = settings.getString(key, ""); + } else if (setting.equals("token")) { + // token for the origin Gitblit instance + federatedModels.get(server).token = settings.getString(key, ""); + } else if (setting.equals("frequency")) { + // frequency of the pull operation + federatedModels.get(server).frequency = settings.getString(key, ""); + } else if (setting.equals("folder")) { + // destination folder of the pull operation + federatedModels.get(server).folder = settings.getString(key, ""); + } else if (setting.equals("bare")) { + // whether pulled repositories should be bare + federatedModels.get(server).bare = settings.getBoolean(key, true); + } else if (setting.equals("mirror")) { + // are the repositories to be true mirrors of the origin + federatedModels.get(server).mirror = settings.getBoolean(key, true); + } else if (setting.equals("mergeAccounts")) { + // merge remote accounts into local accounts + federatedModels.get(server).mergeAccounts = settings.getBoolean(key, false); + } else if (setting.equals("sendStatus")) { + // send a status acknowledgment to source Gitblit instance + // at end of git pull + federatedModels.get(server).sendStatus = settings.getBoolean(key, false); + } else if (setting.equals("notifyOnError")) { + // notify administrators on federation pull failures + federatedModels.get(server).notifyOnError = settings.getBoolean(key, false); + } else if (setting.equals("exclude")) { + // excluded repositories + federatedModels.get(server).exclusions = settings.getStrings(key); + } else if (setting.equals("include")) { + // included repositories + federatedModels.get(server).inclusions = settings.getStrings(key); + } + } + + // verify that registrations have a url and a token + for (FederationModel model : federatedModels.values()) { + if (StringUtils.isEmpty(model.url)) { + LOGGER.warn(MessageFormat.format( + "Dropping federation registration {0}. Missing url.", model.name)); + continue; + } + if (StringUtils.isEmpty(model.token)) { + LOGGER.warn(MessageFormat.format( + "Dropping federation registration {0}. Missing token.", model.name)); + continue; + } + // set default frequency if unspecified + if (StringUtils.isEmpty(model.frequency)) { + model.frequency = settings.getString(Keys.federation.defaultFrequency, "60 mins"); + } + federationRegistrations.add(model); + } + return federationRegistrations; + } + /** * Sends a federation proposal to the Gitblit instance at remoteUrl * diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java index faca9cb6..bfbc6240 100644 --- a/src/com/gitblit/utils/JGitUtils.java +++ b/src/com/gitblit/utils/JGitUtils.java @@ -37,8 +37,6 @@ import java.util.zip.ZipOutputStream; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.PullCommand; -import org.eclipse.jgit.api.PullResult; import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.diff.DiffEntry; @@ -142,6 +140,7 @@ public class JGitUtils { * Encapsulates the result of cloning or pulling from a repository. */ public static class CloneResult { + public String name; public FetchResult fetchResult; public boolean createdRepository; } @@ -175,12 +174,22 @@ public class JGitUtils { * @return CloneResult * @throws Exception */ - public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl, boolean bare, - CredentialsProvider credentialsProvider) throws Exception { + public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl, + boolean bare, CredentialsProvider credentialsProvider) throws Exception { CloneResult result = new CloneResult(); - if (bare && !name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { - name += Constants.DOT_GIT_EXT; + if (bare) { + // bare repository, ensure .git suffix + if (!name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { + name += Constants.DOT_GIT_EXT; + } + } else { + // normal repository, strip .git suffix + if (name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { + name = name.substring(0, name.indexOf(Constants.DOT_GIT_EXT)); + } } + result.name = name; + File folder = new File(repositoriesFolder, name); if (folder.exists()) { File gitDir = FileKey.resolve(new File(repositoriesFolder, name), FS.DETECTED); diff --git a/tests/com/gitblit/tests/GitBlitSuite.java b/tests/com/gitblit/tests/GitBlitSuite.java index 2908383e..52a1d0bf 100644 --- a/tests/com/gitblit/tests/GitBlitSuite.java +++ b/tests/com/gitblit/tests/GitBlitSuite.java @@ -73,7 +73,7 @@ public class GitBlitSuite extends TestSetup { @Override protected void setUp() throws Exception { FileSettings settings = new FileSettings("distrib/gitblit.properties"); - GitBlit.self().configureContext(settings); + GitBlit.self().configureContext(settings, true); FileUserService loginService = new FileUserService(new File("distrib/users.properties")); GitBlit.self().setUserService(loginService); diff --git a/tools/GenJar.jar b/tools/GenJar.jar new file mode 100644 index 0000000000000000000000000000000000000000..6f225cb47f6685fe61d5b240f355ae735f063503 GIT binary patch literal 32710 zcmbSxQ*cn7Y?`&abYiP}2W@2k;=*S>pVk>RvNN;Ry=;TzR4dsox z^7rTO?)0oId6Jcg|A}sgV?3e|m=@RxKXia2_WgNcPVmT#?e>nndtu>7mAVqDxlKh& zLkl&bwZCe=GB6eXh&E~ie#@q|6~c{PN#EY|bpj03kAU~%=}gw%r{3M?XV0G3!~(9)qGoaVQ4!10~+azBwMlAQSCU*`X#L zM83pO)H_#f*2A{$;BuNJr_e|Qcd}tgY?8eryZk`K1}9uu6{kp;boRtv?;uH2V>yS; z5H;M{dGW3;T3_V>Zz}We5pZt6`fJFBJrn(+J(B=~u}Nh|o9IZ%^rxe3?fkto9{Hi# zws(xjyNhlp8#aXdF|w`fsZNzqPUMtJdXSLs?*b7t{Td_5uy@Tm!}ORq^Y#1#Hg53@ zk!Y#Iuf=g*4M)3_&?+|FA`vi#rF-qxZ*I7{vo{>pZl$5D_Ad3IEVgd3F&&4T(6)ys zSKHp&L(gwtNDenk_q0$qr#F1`Z@AT%;oYhOj*hMJ10U?&x&sJ2b|bj0S0Jx@AzSuJ zKs-Fh(<5K?P|dNU<2^YX$J3aM`%J6-oz8TR{)pX`JqG_EwYS93-J?@g)&sMbLOoZ~+LunI$kgO1l1{c!2)CPmWZ$szpr-}Vt$d}41`Pk$f z1aBf<=#=&g>ND-o!ip0WZjkol)W>=otA$T*X_%B0;l+gd6iIP0P1n?zt!)7;2Y)qa z1_|`y9(2odR$DL{r#M9fpEuEcR17F?M-xZi6i!QBObpAE5P32oIof~*B}n_8DFCye z=i!nfX(_RwL)EYr{W)(h(Mukao^v`y(U>_v6C9tizwl1tD+nfbaj{-5r-#lknK6dP zfE*eCxNFBJE{gqnESBv5BQ;R3wW8TK9Zi4FU~IM1M9lU*Ppr-7>jrTGcr>WK8Z65= z$nc%YIJImlWs=X?uXj?}rK!wS)}wX9i`xy~e}rG?b)LKGQ~dFXfq>$pr? zxQ47)gNCr}z-~-?{kNTAKQZk4kV}dvBJ5fN$bfynuAK6<`*M2CPD|OU_CTNzL={%; z5rU^>&p4>~bhLu>*i(KY1DyFN)#7wF_9Bz5sVu&xvM2flA%n^xZ<4Nxr+9&=k@$># zjD-w2CM*|^<1DWQ8r73?NOW_{YYH7}<^Sj4%OR7XOBIcqUu1zzr1lOo3)wQFVUtOYSgBx`Csk zG4=kRyp93HVn)$*OTrVFG*{F;$p=GDuX$9Swdp%r-C63N6xYN@X&&Yd6m5-|oP^E< zpj{kU#|SKVzz~~yD?P1+j<;s+FHTVh;s;VQ?S6AUAA875=V3+)Esb=wRQAf3S=?uH zW~h7QnYMgu4Y-ZhQ;z5jta=P>$t~6^sD*MrxTi9ObjE!LKmo8oAOSJ#onyhDew2G- z(A?lMSan}qRSYAQFkN`Z#+;vP?aAh9& ziMF!OZk51?6Vv6Zx7zxnF`Nj`j$jxt3r6{=-e_qQJI2$P%r#bsBGdl z)BPv+@%|x75@$P@w20;-$K1(0)1v55&B!^ceI)OItRjeS+&C-%G0m<((DYzD2YMX) z>M%|bslfFU@<^Js6?N~Q{V70XWFO-_XK&>p1DrP;|5alb3v*&XUtk<8FE9Z%fVPD3PVLgeiUd8De|QfI;Pn;m?J_e8OS*(Q- zqpdQ>(Z-5q9Tm1poQ2iIlXrvg9ZY)cc@==yoK57%H|l4s$-(rY{*FLqR#_Ik#343p zT*8SRo>&~U_qX}W@;%bucUT7D8|ue$e|JsiB1~ui!O7lP_R`qq!n0gHZ=5P`p3saO z+vTTNa#<6Xf(=mc4)Gq+F=o6z1AkH8R{=+jsD1E=lA1BHBQzSNm78A4PI~ywTcW{q z28RFQrJ;-qw)iw|z+LdO)&H~aht%Ic_W)xnzl5II@#|O-I8@Mdtt{si%Tf`khZFk` zYCKLb;r6eIM`|aj&+rwo4l*>814l1OE%LM}UKpdYcHm^WCJE^&Jv1Pv zjicPs84c|h2(T={#Hu3Z#8p#7Gv9KhZ0ZsmqU`u2*Yl%zY{5j1o!MGY(NeL~XPX8h zXU?8REJ)-Xe=AaY?!X*aXgaBvAUkDNBx+sByaqrz&(O+3)52dCF5Nz?rB^;N_V z6|m!~r5a)dM4b`D)RduGA6SJvpS4jT!96A_iRbp4AerLybQuF;TI?7LtOjX+-m!R| zu!Y6=@+h#2R5M2~z^-OC1{A~N4wR)XWGg?pkNG*U4}unon-yNTS#G0=-FbMpIZ;(> za@VAKvWpE`#++M9U2W~%YcGR=uoN;M#A13Ta@R5}rH~hG{z$akW>7SY^P4e`zs`&L zO*g~iy_1#L)J*~u6F+JJIPD+ely_fX^R=zmTcO)rJ-Gq8Q4-e)t&n@mN)pgwm)upv zH_BFx0=K^f`5v7aQ@PJ|09wM3XRBsl$XL#{wdP6Ek=9;CC$<98n($dw7QXbLF~pA2 zjA7qODqX&jRqd9 zjgfBIO~4BBIWDGhdbJ%o@1q^GtCqwE?E>Q(qGEV-tnWI+cI%oXwyl}d+)wRl>jUoz zadAX6y6K+6jf)gQKH5}+y^?+=DeIrFNbQf)62t5&HQl0(iy90)n^MxgBwRyFV*vFw zhnyLMSNf!=G4&C`8^&(qW_k#?OBPdH9#6B}lxYf8IN1?gdl?s-Vl2{!Y6JJ<%hbt#-cNRKPFlQN#XHfcVk}yFdFbo`qm|{Pd%P?Q6)FcE zWGWi>2S4pY&VvDQ|LSco$ldS4xq1itTKD9KQqYpj-M0+s*mQ^7&sjn)WxMBd)yfn#Q{ZSofK)Mn&l-Uz#eq1*nI15k}$U zWLew7+B$}lrkRV z6KldA?K7wUT4BUVMI2Fkeq~#iXoA&|T1S99kGRLhB3uVLf>6vy6r|gQ+o2~zi>#bN zp6|i(tcTzc5`eYEzR{BV8)$l8zq#!6CM{@p`G+7h_n8r1LG_U=Pq%t%p_Pq+P<6+}Nq8AA$jM=?0g6rNNYrKu1^!Y?n`3bE; z;8Jk(&1~b+mh)NY?y4xT)kZu?q8La@A#w|o_J>LSOmzDsl?`go)HwIBs`mX z*G?X~CSC^FgvF_!$+DwI5e;FmonTNtK@mL?4dA++d0`ZY3?oJBvMc22*GhP$C_dUD z5?i22VAi3$RXke#hvZgp%A<0Iui7A`o6{5ArUvgO99L#9l{D7hwdB9*6~hE3iKF~MVBU(L6hGgQM{`C zD2YP*Y>`pvONSQZYv$5?dP^U`u0d2*F}HVUw6*p_Smwel^Hxv_09*Yk z_ei9egRt~PE~nW7e$gJ2^yA6*Rq}(#uf0YsUn@!N;Zuasc5-ezGLJzm4)F9D6HF1O zA|*E4qm(Y~E;vD@B0;tnQ7&_VMyC?HTwNlt4YR%f1wUwn8RO86Zx^^=)SgaSI%r;@ ziA^6z0?HruTAWc!6^!!%{+hfjmas$XQc3b2Yyy)cHu|ikg`EKD?Ojm4|e8+C+ zg0vtM&Dy+ENuzp4$~pYvxZXdd!aq?Fo)Yrn6X~S=5u5L>5D8nur6p?e4!lfp^W-mAj21xvlh{7&L@I5j&+A~8qrB-*cE95fTBxlt;sP+O(ImI&ceLQSRECL7 zh2O`#@W7?P8*o@u!e!_QM=3tyl;RFhDNf>);|^IWd9!cNj=`AZ;?#CM!t8*IP4PP5 z>_CW3`#b{kK$K6EafsB7T{<@8)WeTpIRV!sV;JdZjCeZka!BY#Vo1sxnK}V=5AjC) z`zWJHk~i%CMW6X7p-JNx=<-;GnJh3?rH8%RRu9vw@zs+Wp5IGU997Vd!0Q(Zk=nyhKp>wt5kbUQ#d zq41TELiB-IARzn*68+&_ARzj{i1_AQ=o5dY2>+(rF7b}_QpL+t>pI|Xfy7tj17vT8 zjfUvD>=bVZZR6TmNFK&7?E4#24H z$(VQMHl65xHGlr-iSRX22BaUH^;@7a}YBy?_7#L#^L8;49cMA<$Y0PTE{ zaxv@7REEX?JKKM)!VM=miH>3*K9>78+l6Z78~=2Rmm`50tp;A!O$ypfYH;++rh?5|Mk@DD2=Ose76ccbFpSfYp{|gTeGnt~(!SL#~2 zdKALv6+ynUwmd$FrOOFQJw`-44uNUVT2=DsqM z`i&V^c#9q$0WoVCsBy9#px+d_*}=E&)JK429vANqQ19)CQmZY?4`7QALyF9AB>o20 zzbIttSv$hWv#Po1?HddCsaoE?n3ih~B3qa5W{md?^PoSOM1x1+jOYCT3_umxp$dRj z#JIJzeg{czYgPR1N0{C>_rQNJ8T1>%&#vyFCU*2{wzxrYAisN0gZ2l z&M10=e{4CeufGrei{{xvbdu$zW@TH7nxumkn2%T-ED1z$GyvgiEdh}Xs(LIOy}a7h zJ24=^w4H8=l;5hgZB~vDlnKZ)LWoflHuPyE9u`J|Q`t*M0f5_EPsyOVl^Dece;8!H`IWTY1q z?|CjAa^!3l^E9+if~^lw%-CS3KkZkBBCf;XTh`@} z@79iVUq9mQ6@lTV=pSQAc_Tq7T$wU+N&)MjKTS%R?$t%)ponE`Ei+2-|5;Mai ze0RzC1%$i6>}D_WL{PzLP`zo$HciR%->{$~CXA}I0fvXrm}Iqq8-oZM;n?9YD60|N z>%_I8KB8Wl!3Qo#4cpv{uIWeWc>2OAugs*^c*1Z^lG+@bz?@ssjEJrI@-kFqj@Ran zp6uB0r?zDjMChUUC%5>zFRyq)QC$|U??m<~cE&^1h3;1u2#hgLzF-1~YXOLBD$YBS zuiIcdBQx_!yO0qssktWXXUmTW4X+y7zLgM=B`@X%7+vNuATz38 zhk`$MD_^%v<0vr6R+db6O;Sh4vu}xM3i1JaFpg1`d?1I%E-sGSBTdv#ma3C?HOxn> z!bD{1!XP9pJxy#&00J*;&rW!<(JUN#b<`X%qtVy3<+z@_K51Nsig3FsyoxLT&~_x6 zItlvh!BY|t3^?<5@U=ogFq!op5OZa5-oi}1&q(*-hxlZ6GI>@8%KKSp#85m5;s)Y6 z1grlAKc|Qt1agQrx-r3|_XR#0uBuO_ZW4C@QIATpZ*oUjT>|_)q+6%lZPn%6VP`uT z46DxyLA*uNqQ91)pdO7BU8#)Nv5Eu_?Jzv4|-hD zhXDj+!TRrP0*?RKCXh3A{y!c;js}#b?h4vB0_;=hK}bL8aC~|&E%~)^VPbgZFDOi6 zM7elLU4nbUq9NE>USbO?z0SL;)zZ~f+x5-WcCcurBE4qqwarbjt_r>CO*Na9%D;17 zLyYl4RsjCn9;dsGAHGknyWTas9?*{R1RJ4fqv?J+h*xk2npemUA05P)qkKIL8fwIqr_0GlsLzun8&9SFb&NCDzL zIiM_PJy-zjUSi}^1<`Mm1>zgIzX_NZW0-o|bpM_3@?PWQ+799yzh3|ZUW*IJTLl1lis!%-{g6td!b(joB7LieHzz;J8Q>uB-hkt^)Chtue2idYJqF%5uJA( zw~=hTR+I}QKwR7~@vL^Qn*M^6((0I(%?Ukn(^eQn=Z$FCO3h%1_7R_Y^OCgavzz78 z*>!mQN|E-2;JPv4H8UbPsNtA&?w-{f+0cU!1#+C$0wb9-__@liPb$q>H`QWAE6I*X zY)$s;d0S3s`jQ1jqA21tecgDx84Z>3U(zYL_1MPr6G}TqB!XiWC+1wP5FOXSh<1hJ z@>JlHal(%q2Wrkg(|CD=89(nPUb@!KJC0dD%zQ#lkUL*CkW`(b^T6 zP+ZeV-D{-e9<)${128GCZL&>+HxJPWQC>sI3aTgjh6p0vk|B8%pL31|(B+|Os1bHj z5aMbgA~a$?T8;dgFPCIjNo_6Q+eK@$@R2p#8B`MEjwpBPt93_&G{xDlWy}T}7EFPnh58>k zIdml%lK!)blm1mNAhoM-?b;B!HLGAKYgMrrVNIdO@^=N=X6c1$k|k7X$~KZQR4waL zc30M$jV&flT~vljNofgOd!eDRB~ekxR8iRoV{Fitr8`zy8_KY*yhU{L<2zlat1J!BnoO!+APUwS zd<~51$);duXLL*?F9lcF3Bky**j6^epde8!@dl>Y>AXtOdSYckc}Zgze!h*M+}Li; z+&{UNYoqE0W2+DOHWGQ^JVgP@m+Tg?WX&8E7?e0EirSLb&Z zx0_pzvQUY;ddvr5589S^J@$>{-V1WOJktz4^_*E zye6+vtPF6B5l_-mdq(9r%gBftsp%S5l zF2(Llair81rp;XfGrc>Cwt)ewweUyAk2be{O=seQo4>N}_J;lHgR96c`A^f<>#~Hb zc>0R`9md8|Kwj-CqRAHec6LH{4?v3x%sWfmR= zH+})OrXNSMR)gDV`stpD*HD*1hd0^U^(}|)_&)p~1g>n96ytl1NqtOiFzPpwzN&I- z*nu9(@h`*6hiK_Nebh@!^0~;)QN`-1EGDPq-^pqmM(XCTl+vwgA*a`4hP=5r)OaJ^ zlS%sWS zFUs^dUG7$~V|=>-ZjQUbXzm#Fh1T<~^GQmV2{1uCX(8yXw`0VQDyyy}=zBpK1!Qdo z7He{vydVN@!c*c7qFydDl&MdCVv%p2zbopvRrw>Knfq#%F)E)=FBMmY^;N|w4Th7-qP#7_zV z-?U^Wsy>j)R;(#*!X>w*H0H`%%#~F8B(q1#nE%K`IAZzrtgXyx1JoXqeH}fHkd+xh z>>=FthSpM#L>GJG%&Zin-i|Bsxv?I|!;eM-k#OIbrc`xCSv}x$=FR2we0$SwmNfG+6>mTUr17%@V>- z1NPuuTftX@ZZ#l~M{JJC@AM>Cg}xo`d}Y0jAg#lKv?+h+Zc7Nh0`J;$7wysE^8<~@ zT0XH0y%=nXGmw&luI<&{(8od>P9Gh35 z;)_1go*mi~h?+++W2@{FaM}(1du+{~WNhmmjyDpCcXm(Vh{rv&i(#J}X`7b(RT^sz z4fCjMl&*Gwqq0hd^B)(;2bF*Xvd6rC>;cNro-gMp34p56W{AWu$M5*S9qat&7{QgL zt0%AW`=GOm@ymbiU{ZBZbIR)y{di5thAgQ093AvlTZtXjp?-|ZC&^?Avx&dO2y##6dA3~e{#)UK&K2!%~| z<^hXvWncv44;a~9$S;+04(aFJ;(kR7^{2{N9O36h)%J$W`oI=otEi&PqO)an zD=q|g)L;zev2vp()>6b-Gpljfg{`JW#=qB5bY4B@R77D%(0VfOB478G_Hh@gg>p&# zVij?3q}*5X!B(0}MnrKnCPd1;g1HHpKDbkXgTFFZdl zoq;tY?8=}8!J8)BBHF_%m!{)o)b!){^c&6iA8{#EJz)MIoJ`ylYWWzC^XyYko`wE4 z?T0tvl>5bTkH*K8QD-a7LBQ3%f%m4o(9heuF#TE@c6PCZHTOzO|x+&8|@!H9!%S zN|h#{)tqv*cRN^46E=&r%W$-B@f)bGm4z+VrSx~sMcn1{_6`{o(M7Z=`Pr`5E!UjK z?9}b|)7@`BlJF|VqV)bp#NDAYok$*~>cd)8E9l;E;gLq(Y6N4@-;+mtXm3RT zXarPiaipQf$Zp*>RqHB%_6W!0b4_3vG(J+GLX253;Ix1f-BV2y&1cLaU7bwgm)7Ap zVQ`#S%Y@Z17voKaRdC0Qc2QweCnX+cr%IKYjh<%g?XS;^UZvI-?Hwko)bdNTA#PMS zzTd8xHs)s;C%BlgT1Mq2OUyZFgWivL^~&I9|5T0%hcZUVjn!|6x4@b0IAU+tM@=;0B{_sQsjXX2+2CpA zstjBa;ysz196t&(uhd;x{$rqWvnj}h-!5myBsLgMI#y?D_~7Cm_|Et!so)gug8~WIzJl}EexY_COAI!nCbM1#1;xBMc5l(9!QB#( zu_X^|3wyW{f+<6x%Rs?JwU_Ms*8^tt$f8{bZ7~pH5{LL?C!yos?1&7bVJI@38%bpI zmfQVA<{D($-mUv;zX=4J59NZXddB3zr=mKe?WG5}ma}AC3V34E^lz)lUDQW<%z*rO z(Ld_(#($pUV_q)-IR;;gsA^qi%?jnbrl3l7IOqZv8v|#Rcz(3YQ zpaN_dMX{n4(a$cZO*I8>f%j2JcYu<7IfbDu1g7o23VuCyW`3g#aA3|hBTfj6r9|_# z0Zh_-g8u24Z~8`*b7@KybG?K1h+SH{VE_VKp{<+wj_C}4Tj2taVRK-V=5Pz`LD&;U z37U41Ha$-$2K%gCYUXifIyer%7Tc6L7~Yz9A;OpE@Znkd=y;u?iBe!rJqdE`Y6moq zZits2T6l}duzd~;1I@h_j{68?tYo($Bm;Fza|ol2o1xgICerk~CdY?yh*twXle!_$ zsvLn8ew#P#4j-tY*2&(MB2q;$1%31>7J%`K_QeJMjXUjwo1rxauI*Xymd$AUZ2_PO z<6y_X40u0N?A{0;V{re7-~Rpl$_w*=KI$cc?;0}wP6VpyXacXXjf^C~zym;iCiqv0 znRxKycm)Ll>i_lcN(}G+ti)K@nEco5B~i|9Nf0q~F9L-J%2r!?@=b$C5E%@Dh=^2? z0ZmepN>zI0b99Frz7}soXW2dwCAer$xZe z=LZMj%57VJ>9 zOWb{aT5&+x=p=#V9H~^Y2Q_=H(nb|rPlm_DdazVAF~j_WWXbMisi99t3b$6Cf&=IX!)wbGU%f?@b%iUeE3Yo5Y(&WD5Xs%ee(m+?m zA}H)<2#O9@PrKL)7_K(0Y~imnS)t$5=dJNIp!#~1&**5LZx~a%W*eMSlOZm)+*RJ` zFS%L--$UHL>*@E61|A=?d(M4BUOWg)WD@vKG2QA4`Vr~n@aPHfzgbP%gQq;Y2k|5H z4+ZG5LH#qNkYg;mDry9dcKAb=v5d&pAo8J}-R*FW->bvwOKLHciiu&h49DjnmDWKhvw8G`K1Xvnd=K(L$NbsbL_4*tcnI57>uxek?GDGDz^M z^s}d&!?_uK7Q;$J3JR6SyqfJg>H~1~nt*|zJS9sS%B?e5p5cwbk~3(9+?a))WFHW# z(|H98pHW2i;=S23xkW!D8k~WylAr`+j!0>-HR1=64>X|KL!o#_SgA2favs=W&+PFT z)w8n`*u=eVlB2{?4baLccaXN2cF!b_Mwn7pOtg|M#{Y%Tu#-hxuf>r$N$=}I+M{YL z*;b$YsElD%3V>3XYgk}S;$QIBJtI=Iir5H;PSh~zT&Cj<{_Z~ryZ8^U3I}?ZLo#ps zJ>0tCbX{_mju zpVKpue?{8GEv%jYFTP7OEWB||uzu!j8(DVXZ93&dq9m3;H%KO06@@_PSPKN5lU7UF zOP7rsc{XIZGql{yM6^#7^Zg4!99yV~^O4{r5^N6Ceke^yY z_QTr{0P?MpgB>DGgTj!HQP1yq$Usj2AnaaRT0mrI9mE4+Z|MQiz)xpnm>Ms#C<;Ct zaj4y|YCCA=$^#uEAKCgox^Xww=n8k;5ftoP*h6AovRMSE5e0@}w}8O&f$C}>*1$dlQ9(a*{jKP^110- z#t&;pVJHrZb)J}NvRs*IY`cCVS^5k=LUXry$&yyopnLM-!>63kh6KSyl%bGV*Gi-)muk zn~_;0o5_2r62vAG+1}u=CKZy))X7QEo0BG)(zm(pbhCN}7 zJwV9LNza<=Ff`lnpxCTD-QZ{m6dijPkgngHHc2HZ6Hl4^q5}~elYc&6CmUC5*B(>9 zRXZ1ot}ucsos}x&PG0mTfvbV zh@{5T=0!pJi69hMd0@d^dvN*+wCNHbV-V`|Bh&0dx3D{RG{aiDJIz^E* zmJnRRB2gkEr(Z2v+Ujq^ZDA*nZqY+XT42(f6&6g-HR z;QFAZ+Bqy5O0vSxHm^&6g69;>c#a39gLnPRbpdc}{BSHHO3q`!lV6qJ9-4o)_SU3j(=} zov54OZ6AJns_gR~==Ln_6___V=)5#E)X2OZlmO&dJ~)0!LbqT9$OxeZ(QZUc(8%SX zGZkcaB`dwZ%jcX(A#Ye3qLHb&(tm~N8(LGPe-q0L{b3HWyfOEl&b?XI!CTt_mq5Hu zwY#d)=;s18&r5s6SoMn&P2j-~^{JLN1=cF75|~4~-%I!m4N-nLmV`((lo-68*Qh7S z{4yq{$OD&7ugKh*ns8UD@m6S=BV-lB{u>xfyg)1~ed`p2{``jbVfAX;8q)*~D*p}H z95o6#slA-zxz|D0`x$Wslt2O*BNcaj_!xAqv5?|?8Yw&GpO&0!Mul5)Fs2F3P#ClJ zqIPU^6tfeAlI)l9i$V*cPBM&R9;;K?k#l_57mT2!lyi1u?7TBBd?uM0vl?~olX502 z*{caPRw(yZvKBTEf?I51iv>s;HLJC;Mzz)`p>A%O+%(cg zl}MTzX|-vzQ5p>=U{$7Ym=x2vv%v$px`n*8P<&8q@_-#h=-%w;eJoSPeQff#T~>*6 zr)=#o@}kDYr`7xeMCyTD^(evn&yzy*ZXVW3AEUe-Gqaxw?$7Ax2hY|W0p^In1Ez<< z(eLP&K9gn+B0^#|J^iwIhAAtuT+z&%3zB~_hmT+N5tQz}#G_<&p?2_%w^1(yX&(E> zMHmBRr8989N)MARpATWkT%d`npcu00sx^>S8HY0;W{_UzkV|774K<+6ikqdc)4@MgxCw>2-)P8+C z;xPZ{HGHIh$2-LTW6k#8cn7J9iSz%XA1eP_z4;`yGfBy5gTT!L$q|*jn_u?BffI?6 z7E2cpmRthM36lo9Rjh0X-_V1ED1X9cd0f-{79he^ zOY6Um9q~GlZzgEZVfP4`HB%3ECP;-vCYyLPM^E3t9CCcS)UX_U_(O(b{p~|3uQ@I5l8N$*&Do3lafQ($ zB3bEYjzPE4eq8b&sa0q9x4O?u+qGZjb=ueqCzbU|-!8!uODR)LzO= z9K7VinAu#b0X7isfq~XpgOWKzANk1I z`k5@%P2tqZ^Vi*@E0V|V;&j^+*eVtd zmNJ@tqfTh+O`!nSjK)+&^fE-4WU_6eKvY|b7PtBVt5_2DyivA!yMLka&iv0X_CG8* z{@)6`|C|NOS{OMRI(n$sn;8Gs5J3~hTgL?L$IoUntF-N|#R}471A6U3}bbIc95*7e|m5Xn?}fYZoIew-z2JAY4yK+SYG+II4Z$6=eyz||L( z#oLb6*6vW{ZHMC66<>^VWa~W_!*hI`gKwFI=$IDT#itqV6?~26SQmQdr5KkWydCz* z#=Ad&%u9L9&4G`sg)sGs@aH6<`)`GSEiZMI!1KO@yR5hX{qT{y@E_ib0fl=NI02_4 zDu?s4!Z!-EHg-fxq%#|HGbKCYRyY1Te;%xbx6#( zVE$HQ7%}KJ!>`a7QDRd9&3U941hD9IBwSWdbyjS7u;_5Kde`A;QNcg=-nZ;Qmd(A2 z-8`lcTz4of__eL#luNRd$=sfyHHw_`NYa`{V)l&KPSxYTks^buO+!k0FIs!69y?>8 zYg5hhDijxyBGdJTM0!lUgBEbuM>@>sf7!7f5@nR3>#Z7uL*Pj^CvX@}QV-=9Fc5>JbyM+fe_qg{ri~3ou<1&+^4if*~`2ki%+Sp`Kq@q}()n&!MWDYgI z%$~TpIG9jaS4O4wX=<|({17>;5bs)BCsDY1&9~r5wqL*Lj`!#Kq@Ti#bRe=NFFa^! zx39kA8+0nfDv)Y@%?_}Emgp1k&CF~ZaAjPh068-?AT`^```&Jxlr9vU{?PK16hJIl`9eaN?&6?yoF|M(77jHh&~kp&gT8hYf~iT5aSUC4z@ znCD&ThE)Ldq?|eD(dKH=VN!4~R&67Cuzmj>?!_Ao!pEU=TYG(Y0Y%sqFq08ADlaHI-uql=f5FT5@%qLe`RwPki1Rb67 z(B2@ztQ!`fk0-?S%xuJ6XmCk43H?)fpxDk36CvD`L@u|BPSUvkNNz4$?ibd^VF~96 z4qk_J@x)x!QxLv(U;B%Pox@A6Om@W7sKV@{?x@aPj9p)?JTqdr^5EMy3D+gb6EB9S zX)Y`A#)R3OL}_Y^0I$?GFLv#o`=M&7USVvnUT0{}-Vp z87Qp{z=G;Lr%ZS|t$5!W8wUkQGr(A)uyP2O=yLhO_;ksZ@S1=Ae$r8q*5IOcL(bQl zsG-5oKM2{~=rlAj)>*^WJ6VuRiEFQAJvnvRK#tLb6*LaR!m5XGQOoDeq+X_Klhhcg z6jdqdq*giUtn8=B%q*&seyAaqF`=Bciy13&xZ4OqerUINW>U_0$iJ(Up5K^hQX#T| z37Y{X*|xz-%^<3At(?v*&hIV=j%gzrXP95nRjV;Z=q<0pM$%k1B&{BuHP6E%>tqL? zu!LS+;XOeIg-519Fs&Z>S#18XmMm1)Xp!~FNP3<{TwJ+|fDOW-7zMHE6Vy^pQ-^>a^mT;ASpl)XtHR zsD5H)HRWj|?_{la$IfEjlVlun{hTx-ZMfS!)*mgd&SK-_30gJXDfEEk)Rp2}AnPYv z9DP)v7??5Vpt_kxVTk2G6B0eteiVmj={Y#Qx1k)LoLJvxKx zA7Q~+W-Cy}aSA@P0M_?Si~pzE8Z%mAE|sI)MouW5z1o4>?H^}N{f?fvHv>-dENMHl z(^Z!rXJ-h$oY{wDV^bRJT7TC`bciZrXWN0rH|d| z-R31uw4HPy2>Ci=ZcKq-5J* z2tBF+o?lCi1h~97o4gQRX9FV6ykuGG;`#~evpaT2rYU^l)D>nhVk_{gB(D0ar0&cC z05(_!9xVU5hxf7Id-uJh4QL`K*GaJ1R1SJlimq`@?bN2 zd6SrnFF+cixYFAec_muJ#RED!-vG?La0yY$`*KD60R5qLL|YS92Hp?^@8EC0pVS8TUxI zRGfKKKHJRJ$2KYa|7z{4qUzeVEdvC1*|@vAdvJFN65K7gOK{%=cXxN#XmEG;pusgb zgcP~2s!mRBz1P~gt>*q&>u2}2=9+5`nSJ!Zh^QOfGGQ^QPF?0UTykhdU&&N0zx^3? zxghfU_s6&kSE(ZaFtPfP=c@pGt zFI%miSXkyKB3{HZPa1TiDtW^W)WiZVdsnjB&hTMBc=Vr)8K1nSL!QEJe#q^Ih98p2 zl@DIP@=uiTcT4eyA75hdWDTslLm_W!wTJEBulW&H??Cb~`a-We>)zIeku&nR1I$+) za7<@x_o{%hxS|*cSXdzu4DnIcp${W-Qr*R?aLmMkEN^hUz_NJnjP!%D{R}Jt@m4uDN z6}97%;$gpg!B&XfLY05uc-WJqtew-du5y^)rMd)L=Cx|kC>-yu6Cj(i*4-N|Zy*O+ zy2JEyR6+4L+n~v-EOJ!w+3O-H6vhGI@}HQB9+82$1-%O0J2(gEw;MK^hlH4-PH=&h zDYB1H$9M8j=M0Z8oK`hN-bOPE{Pe20KxJN_rnrSF#etIgfreA9v^rVIH8;WoHu_R0OKBXz@EA4%#}XH;i5>dnaN})n zU12#qt(@$*4y;yK%}YUra1kBh8C|FECiEhMgv?nC-?+HpuYgvyBcF8E{RK z0cF5XIs~Z>hJb=7G?F$|u*$+b{;Cf$Z#1K2V4JTKstF~J^%yG-$7~5jD{@_FxFnT! z_rp8~YG?@daFgrZ^WILGX6uf2HOl973}$7r1A*kc6e+jukzji!nF3nbeZ#3+{6d=Z zoNG?wFQY}eTEQx5%Wvf?g;T905_!R(OVzp6DkVCs`q5lSlgb+{lx?}C8+X{}ySZJA zLXUbN`P+0Vn@PAw9N04Vldr0sb7(Q=Oz-g<)xVcL@ppEC=*>f&w&TczoK)Q1>?N(l ztM*i7niHVn4vH>=+rxE>scH=+s+H2&j>>r}o71qIW7bU>nU$MY5w~;xo1?91X^(D-?c0R7?A#_VTM(v1K4h3yAl3yjn!BW83k%86 zgrT(Du*zvVoMaN1v*}Nowdu3zg9!kjfJf{cAUg~Q-D~=l?R*@x&Un{-_oI#{%`P?; z=XUk{$Huuw_TAcbI$O3c(2<=upZM(uiR=K@13*fMFC;|R_f=vcPEP~srJx`YyxBmK z4V!QBGl2;Rk$0Am^GiKFt2pM?6ir(|nYHj!gfD@nZkIcDN$dHWdM9f3L}!{c@W|9l z(a<;sp;4hr_r0uymz&CvvzIB*UheD1-8+Q*Tix-5{QclFKH34Y^%V(0!_)6y4C8;Md z7NXjdp9pK^W~Ia%f&jG*a|3!EOaPY93Q+1)l+>3jgCkpQN39+iyJPGbudtq|E{h2= zOIEf*{pJ-%Jq4IKzK7C+2xlt-1Dp~YW|G{9Dc&*XI?2g_LQ3fOVRGtL1ta%e7Ah+G zm}Q?4U|o2q_tn%bJ39K0R3VJ5;n8uGfjYZRv3li&R2-vnXnGG4KIv~c)Wt-4#Gt?G zi2I_Ga_;G{2j=Kxa;yfebME(IIxKB_FVQ2VQ;gxV6$kj$n=ON#6fPwbN`aimjz#KN zIZN)|gXgS8Ap$ErpFYdze9c&hQj@0GJ*%SFDS8`YYIaF8QkoJZosXcOe0Ds`Gcyuq zMa{U)<1N|GKSuSbwyu;k3JPbk#2s_8G$k-BVw5@g`6DZOQdSKvs2`+i!e6}&Nw0Pd>eV22kS2 zAkW0EYONz#y*_=eN-1$|Heb4!q`g|$L0*;Z)e~MRc)&Zqr;+)W^dy&xC|5@7Tj)Uc zy9Brhzx-^+(U`OuT5OV$ME{JrP=rcJXJys+oebqjXfS!%tT(j@mP%=wmJ4qZvq)4Z zifC_Qt(4N>v>YMDLR(6m_?#oDWH0-OQDeeU!OLbr>sNB+8y*<<60$8jHR+L%is@XiAvTeI)v;oaBome+h> z*O71eVY_}>z=S1!dm~_dAT*951^A-!K&7q6Cq$M*%2jLSNS6!M?8&~$-5uLLQwVKT zbc7#Tm>wavq;PTWJKPD=I%JqD6%Rp^1CTLYKFlAd7=i-D8DmBv7<-JKuFz)8xZRk= zd@&DbMbh&4l7v%I=10gthh|AL9H0gNFkKmwHnP~l1W{;GN1E6V&ino>VeHCc*o`=& z_+cQK@Dm4O(p<>J)#N*s2;GJWcwC~j)2Cj0Da4XkkW};EcMv9=3FIY6V)OmstUA~>+mr$^L0v- zpAK>k!h&K$p_32~lz1)1?E9&M%KM+Z13nA!A@9y}UXADTKm(1-Ag(lnys!8_^V#k) z;&~*FFOQ}oDGTY3g)@o^1=D)#1f7lNdwoC9J9@Cg^ErKgfSx(^h3xwLC%O;8LVQ!- z7i^nr4p*vqWiYy2CKg+wfkdLsEtE&_S&QK=zc*x5yD=qwiYDj#d4%PotL6C*4SLse zA!~E7OAx>xXXq&#r)|5~W)(0`W@gOgbodyY2mM^>SXof|`Lri(32wb5W{_G@3JwP& zH}9XAzXkb#IW&?iTCY#jtN9?U0MKl?%FDa$+Eg*R401>DhAB3&;I^yhJijQ@y(kbR z388dl&-waI=kUmHJ*7t+5FKzsK$ss+&dn~-NQ+mtth(yucX#N%)#*lB=^q+#Ov_#? zc)mPDb@loQ`72+g#yqRI0`=yNH1hvsNBRB}JNj3yDp6@vp_ds2zd6@`N(9nd%7x2o7YmJnYp>d)8)f0kVJuHoMqS{huM;Q z{h-E~1{(+`S?{rY6kX)Pnk0ihD?);d-?3iP(_e9&OR|3W7*6Byk<2og`tbg_D<;-E zIm&HCrMGI{ziKJ`*~qt8iC*7&qska4sth>R`9WGnB~+x*7D5}oja$n=`vr+ZAD1qp zc1Vq2(Of`z>)DT`GX_xPCs_WJbX;KH9voGCJpZu=HHTSPzgKQ}lvVf}l0?9MfElYB zhIsZmX@;*{kN;lgP2e9->%WrHe@$$r<_{-4NgUsWI_ElX zhYu7AM-Aj7&GZzQ((w(_q83JjrGYyQ4uUnJDho3*f|@O=X9^^C{wmak3_<5Jyu!Ie zb5lqNnWd&ODGdk^Ld<*33ZY;jKi{sHpPLNU?)mOQub-WEKAnB%KW{tFdU^RNq$m1z z^+Fr`1GeQmT=VN4c&G`=DSTkYYzMT4)*-Q12-HjHO3BgqY*-qZ&dR(NdjU< zOfRrknm9ayu+^YT-T)wR4X1eb%w5*nQDWO!u&Bq%9NX3FPVUdNL$`8K4$B`=jpI%a ztfs0#{VHX^Ac4Sdx0w!=g<{dP=RxrqA3GV#w2J;z#cs%rq0Je2Um~#qvM)^7NVhfA zMuA!}M}WQRQG=eF!f#H92c#0I@_cm|(NkFQJ$tdw zDmQwfU%k46uaNZvSuWD#9Xh~?d5(TEE$=>hP~OY>oWJ*`Ek1cqb*Ay!%co|Us2;{l z(fCW}H6d+DUa}Ky=iX9X1|3a0hLeElp@%3Sk*d{{Vvi_Qk}vg4TW&39+JJZ~V)+nH zoV{eETX+z7z3m|{r;A}xSF#x4arWf1C-c?en6QW8ZHL$ZN0LwWXU)t(vCOnB+DRJy zf>8lk>MsSwy(2~SZ`@Tabq(&uE2&Ut(3U`bSK$#?jy`f_B!#ASw4bk&2AVpOM)4h_ z7@2H!EWb_@6F??qTVkLYUM@}+2s+bLgS%(>e`MM8(3IA{2`+7C&Ks0C)=I~AEVC12 zMz=^GO|i4JGNQ_2kL}lp=g3WmsS3URbUXr^$w}#W-_Ybe(&xIq>d)t)4J?Q2&9l+B zlAo_0^z`EeWiKG&Zp^`1om{-omp z0o8)1C%QS<1CL>(u2_ygFgxYd*MY6jr9SGmsx8q)j|UlT7vJN=xVjPCOxEhF`fT(u zUC8?15E0lxFr~PoGU2!{_f=m+gwyD9URa}l=`IJy!}a8T-#8N7;tuDW-QYqZ^`Ibw z30LYVr@AV_qS|^?Xp(x#C|AIpOQN#gW>=?8A56PDPv_$iAmINYo0~HCjyaN^YSGVz za8Qc3B=XRAy&+b-`s%7mJ=<0vn;fYWUVWhMiwR%VQEZlW9(C4Q7-3AvsZA2= z#^|E+i;E4xfWtE4%aHz+(It$yaH?ARmYr?YiK6Xx zmF%~%o-ZQ5*%{X{HPk}qepYVY9juB;7fd2S58-}1^={Et3ht~aa4l%V#hmBnxK+@3 zN$HwMZ$W);_fYj+?1q+r*>XS7+Rejrt8&!^?)V;O9PS~1ETipq%A5u6w_O~v8kLhiA}bpuLcgYJ^eB4y3bl`I{MGhzMT?FS+Z%%xJe)U zQ*CB5r5nuBN{29(p+``b(m~>z9w%^5r{I#sHLl0$AoWdK^7v66!c;N-WzJv=M{z`g z3>_N#y&pdr)lOl3>ji`7L1X9B=Ga~4;f`ojOIdq~xv0e=YEEUDy2zQ;Pjy3^JQFFxd_~!4*y#!I0>g1t~QbthOytsL`>uI`KHe?#| z5$=oZTh}fP<)x$-a0?e`BUj)n{)=Jz&D=p>$3>U9LhEAKa7~lwp6t1WJ%}EctrC8c z$plvRc5K&!BaOp>TgzebLG}oE^eNh>&=TUYrj|0_PD> zp5Agr?pN$?&@&b7lqpD@?qcu~Vyao|z};dAmHxFgq7h*)39M3svmmu2YlH3(&pqbg zC0yr{iLk@(JJE#SH-eUNm$A&<^2)~c8~ zeROd&{y)@OO=~wiO~CqM97i3$D?Wwm&Vqa*_l<1Rd;xi%f=*hWbj{8RrNR#H7Dv*+ z759~tg+wR+wj)i0sR5}(4ES+nK+|$ zmF$*62NRiIlLF@1J`)1w&U=1L`k#;q?mxh+G8c9sDc>03I;$2xHAXEZ0Dl%D}-FIc|dGJ3{#T9K;*&haCD1(qSHK3c8fYt62&{hHKalAY5cQ!)5_)#q5?={}#h zcj}I`e@$D)%$SKy%HtS$Gq@Xek>OX0n$o>-{|3U`myW}?H`Rw+az4ad97X2H*2XhN zAR&T970HGQlCK@&Ujz(M<|bx5ZO3`DrUonD^BUt>s>=4qwMxGh={eJ7jt)@ReKC>F zp)FnY5hoAQ(bk;J`5f>A_++h<^u!b!=^&#t{)tmO{3a4E$%#ZA1fEMM5I?_@R!Mc= zvq!?ssmOW8K zWrX3~2xs+fLBUTgm2BR3>mM|wL&~8}KiRR0AfcN|hu93R@=eY{Ex*^t*wE9Nv~pcp z#w(`k42s=j@3wTZBolxV9YkmAbr@%N@ldeV)8~IVJev(kn z=v=-O%%|24u?p~3?5FQ!ntDGoD8NIONG(hWaBf@%T7-)gXkq$)lalN!heu`TpOYIC zb4<$m3`wbJkF>YJChJ`70sj5}4wt2Q zNtU5{8Ss|Ih*qyJ2J%RzzF?bTtTd(TUC|?&+If@vdwAhYyxNBqED>?S@8DJrX64*b zr~skvcYyuNI^jDVS+VMgF$D8#Pz2<&8tW|^0P7+mKvTUvWD%gN_DTX1>8H@uxR3>W z=>b^Y>Crq{1HQz%ic9hS(9sKS)_keL^how69A7|kqOAGB@I>`NXH2?of$x2SWG80v zMDz8CJCw^l@ei);|f#XVHg}*i~!|oUtU$Wt^&qru7fh&!ywZ3Tv%@2?)`IqCl@K z95r^op%pbdn!OzX?)zg|c*SFoIr5eF+n(1=^R>>BMSk3G6a;8If+hr7Drn*HK}BiD z4Cl>y$;5|LKs~ms5sK>|RWB1I9ogI90RUpX^P(mtyLA@{s6)+Bmm$9R^li$r#(e>^Aa8Z{zg`e9tW0ohD`Rs)gw8h8tPzxR5v`zkz zeH06HC#78!o5+dWZAi)0b$l+7Eq5Z9wLlK+P0e#+7Q|fG{IG3mSEuHQ`51vEcwKY4%iCJvKT_B+gSvqlaD!Mp-qZFW2>6&*WQF?d%?OO`;n1M>X9=$a^&92d&IVmCRtZS)AJfvpmk{K zLl3WRH`H?{OwR&%P-SpJTg=^{G)}AuLcm(v0qgR z<46<#3dQ7&@4hV|0pyPu>O757WL@{8Xdxx9;;uuk3Hm}!uF^LzQYtTf%s*3ukD11H zzHDK?AQAi&#{Joc@Ddz<&qdhpcfp^%-}009h7|Axi0_r>;kg4n~ zG}|lr!RP+Xt9oP(H)Wzls6{grb{Iq2IjOLiwNE3gLeoXulH9O@Fx-Hm?7NcAlwWtBR+N z;mas3wZynrIhB-`r;Z11=u^6cCI(m7h@B&XDDTrDZPznn%C+9n%{;-~3G@>lEZw=s zbC&3h8oQsz@dFBVU9QQ)kOTp@db8T@&YUJZCRXKNUhZOqP=)c^aI-}6;K@>8cMzi| z%*fnTE1hK4l#KLx!R@Tx_f<06mESNRn%hf;Q#AVUMHPhT(U*_Qye&T({eXCGz!2cl z*iyXKzA8=%=nkoK6N+-iM#c`nIfD(UA7<$6MVw`^OHGnucAiLSqA|y@lZ^3|;kE1D z6wg}62ywhrfVP;-aWzsjRtZ~eDx?(4!7Y#Dc776t`$h!j@MfGsNlT-~-cnK^+lolM-Ji(O# zpPJrJz!4nw2qg5e6@rws zVRJ5FN6Oj^*c(Pnjw9|>my%9P${kz#Jj208LBrn@J>S~3?b=OjJIk0M#h1|h(4@a$ zOqp1TSiEGd`_8?xB1Vvn&BMmQ`|s-Is6h!D&$%sY-s?>Tb2KAG#UV+Il&yVj9r+56 zX;IVw@m>1@90{bb2Euzn$VsKuDZNIN-r)EtHGJG;jAohLN9{=Rm+VgB_8mJXk9gTHBC-q|qCBxQ01Fk#11cng>nTJLR;fwqtit&HgG#Y4MPS<1eU!Id zMkj90eFM^RCuE;DL~Df9-tD_XZo!cPoPWqnA?Z@`1fdLL43o33Ng~g}~9MV?4eDA_9ws6L64Qkb4$o$H-*W zp{&yO++#vG*^%SD#p68?{wjJW(;iSCQ_G@YJt^opdns^Dn~@ z52Le(zjFY$GuQe;N2sS<{p`j$(GVqxQBtHmO4A~Gs_>DvkXiWADabF{(ViV$wNdU_ z&b+MWyG={hr{05|o35CUo86R=G3o*f50g7Cy5Aor z=kM=Oo@jqcEG+sU&;A7a6&#tjbEm(5Weh&T{{P*@lnk9M{@BIT{#6|I5`x)OQAwPa z7cl1qzYVA7@7b*)Hltz;8?I=wq>$WyYFE#vzPWgR7MTnqfu=A2tUSObOH+6h$iAG) z<22#Df9&?!3q${FCs_as{ehH0JD!7F$yQScAIsXj1M;1V*ro8`Ydx;q@qvUyaabrW zp{4tnJ-v>u?g#^84Tat2I=c)EALU_eKR9u^5DxV8#FM7y(-=c;F z73zrr=H8r+j8R~W;+k0)cUHeW)A>VExAmGXddM|D74ty@iN{4NJ+1%Ne8FkU8Vmha zmDATDuOtfZk%Gh)3@7zrW{{*<1fpfaiPbo2{m9;fIwMUDopD=}cOP>quXfcnt0?<% zBBZF|hHJl3M=R|!MduMrVSu@#dFoSB9bUR}iy1UR>d&#efYHWe(u)i(ub)$2b=VSK zk{`hDoT{$5WH}5Kitny#1$J6=%4N$EH`RLU0$_;evN5f5d(Nt~!6DhC<%l^r|zl zlOnEQIAQ6IImoYqFo-;>xv}q{t65c=pPc2)gOUSkWt7`ql*N*T(*&0Ks+u2G?RgLn z!s#tnEd|W+FVm~nYAyRzd^TPxD@e41<)Nc(KSQziqs{z?Owv*Pvk>PcO11x7=yRj` zRF#Y|YGDMUWc>AK@;A;#Zw99!NF_9&6+W*o8fBBWed#|dpbD3fvi_hRYZl#E&n=0% zZ)qC$CGgu;EJ01|G}FO-&=B(VDd}Tb^>?~f*Bl>-U1JzdsU6FJxAeSC637|yrFuKy z8%EUE!Ad5`5L+^ZX3E6jkqH-Qr{b2K_-DG7t1S`Mm3-_fRO}XJFj|I#Ve1E+Mnq}e zt<>P|i*Z5=V??63WV@PD`G^HL+eEoMEOn7*n!S8e%?=d=XFl+{d9cY{h@L+Wk+#gF z)VmRbFV(OXl)@XkDyA%grkZ^retnt`OV1GuuO-1kuepo=K9m2?3B9jGgTH^7g&JR- z(A6p0w9K2LFCmtJ$is8SPL1%Z>$uDCnz+k!I$Qqnlr8jA1b!P-nTMQT zxfI31gRf>UC64m(HI#Fp@shNo0?3tE^qXL-UU`TiN$%Op^|J%Z-Lcqc6*dJt+%+Z? zLM}Vm9cB8VxhuCtGhlm_Vtb@%6lDA5=m=5_O$uW(0(c;kgqf}PRG{7MV=6Z5n#}MS^GWCFpwgtQrffY~ZN+G#{6Ed{!8=U4d8XuSs~vP)H>4 zesOyZv~6b68hCVvphjU)Bi$UK zM-vVXGVGC+5dLBXzCELrYgt#94^YjcPPk256(LwP;=PgX;U>5X4Qiuw@23oUrZxzsrtZk}MSXB1sE51fl?L(xoNy<00s$h8I2KouQ_mtCUA1DS0N3T9&W4zFgn z37wGZ3+3>Rh6ne`ZrOwk(Qv&D+nNv%Y;Q^`EY)5|pOCR`I~%ChTHxEw()iMAniZQ` zazna7j~Sch5&<8=X9b!&00Qs0PLw2a4OaNsmfCl-hCGc@AJwcj0cI_oB+N}bvwIT-Zr@j)gQ zuI#{fFU=$oZytHtdK>2mq7gSj!@|8_hH@!5iOrEwG6+C1pVz_=)oRMX_Xv9ek*9dQ zh^z`K!J}7C0nTbu574J?QFcI(hQMN!9&$CgT=e4U{+M{djBa9ts0#?wPq;pvAJ6&) zg7OK?w#Jv1ElvT*wO!@U^)VXC=le2NE(2$Idn9TPB?dRfZwRus_%S%Ik#3z1<-W(L zWzOSmgfIH^=0$!6$k>I$TE?#}sG+1r3H9+zEFwWJLX&++cuRXwm9sC_9^ld@Dv-Ny zewGtixA9&vj3qAb>(rs`n)rK0=#tjX0K^&k;z{WzymE)@b&FVeeD57!v23E}jsXj- z6&}6qBUYRb?}C^j^>mGCtK%mT9YNtNo#?d7XK3Z$(=zxp3`@cH#bXW^djOM8ZBI z&?BnteYoXgs**1WNM8Vs<*RzMNB~F8e11){Ol~x@58RDCvOA%~%fF7M`=vci5MPUl zBjEr4?!4NT_J8iYA6_FnY+nVbRzs~Ed4H7(fKVC9=PIL9=+gNl5eBG7FqQ5VS9DH8 z%cbaa#9LX}N?`s@Auu0EaNa(jie`2amE3V5$s=gag@_76b`$U_JaWGHU2tFEw*k!7ft((Y6eRX5phYTQ9D%NrAOScLumBaYj9y?#c}OKwPd4 zP*8@4V#;}x7VZQVIGUwC<&iU~-CriydN8KNGn#=O1}OpAJ8>MJWxeci0w`9UN!3ly!zvc1dOmvoG(}ZGY1cKbdY<_AyN< zG?PcfR#B&kM_F$y_J*yW;tg`F!C%O(XdFqMHXHw_LgElVC2qIw-X8xEn0T)cxe(9LXI2tdra89^3 z%Moo8Viw9NtglPT(Qr|_HlmNE@e0f1Y5Bff=M`=JfisG|V7narp?V9oma?*u9*C1W zkdR3skh+UXU3no`mvO?2p8UllEg6S0#qYlP##xtW1|mxOM&Jqtl;;$5i)!^+W2@B^ zY#C1-L)lq@f>*_OPnNPQy`-0Wu8`Q*V(qdm?lIf|^wyxzWAIAL{9TH{@{bC==Adea zD17oC!|1qzl3JTfs&1`aQ@7recpJ}Y-_KU&Gs-#mwrrYnkFy@DTtr2)EN`eV;7v^0 zIfL#qmUyft9Iz128@8tN*aR1v4=q?D#Ak~pg?9v3?dMBfzU{NZn=P!4@E`GXhYL%3 zs}hH1g%Am1#${caOYNIM%6)v|Jl5yq8A~TrqBWy#BEWl?S%3;vlKa`0Vx}96KQurS0TJr@K(8tYY zZHmA?#ECN$yZSi3Fb4&<9mZ+K_&hH$Um~)5G^`6i_)FT#$7d_W`#Gk?k`DuT++*6J z4cr0wx~rSp*daq|T#iy(+z(Q$6Y1B{6+8Qtw3lCJwQuFi24WNy>fX8NBx5=cA}9EY z!TWB{Kg&@g!yI9K2@?BUrXd3NvEIQHcCd`10Yy(qL|`r6H$?U4zhoHvFfS#BUqi{~ zS90us@2~!c%oBBIOPl{)ex_k*gYn9W?L4(BZX=M~Ti~hVtQ0@La-A5F`a#_6n zG|sqs_)c!k#Qd8_%)UV9-ILkG52qvctEo@s-qigXV5{EtTdqV&n@C`*J{Ow`NaRFN z#PDHHo54L4fzLFtZa|o~2ms-m+%P`RTG1{Aury?N=CRrdrjSns$N+4{z7JBDRPjPu z&vA@>jBDdeWZ)7w-x^)T^J`bMev@D&+8*EXB3FZPF84+m* z4?u_S)^W#tPl z2p$}b9SCO{WK~L@hU%K6hg)D2fcO$j8Dg8ExCOdGHDJ9+Ge2DJtRtu=;f`@8Ye?VpVkr7we;p;9IIQn zBa3vHLGB(e8SDjAvzU-d1%@gbi?2sTfz5U!b@FuuKWmV5XHp_$RJ@u;CA6EV`FMtF z49fHGtNaNf_~$e2js(YVPf+9vAvJf%cF!&*fP#nLDYP`?t^f~ktH*%t1;O+r#n|C= zQJw^zAyIa}I8%FY{W+;(6N-9%@fJw^{%G-vUe7$&+qBW!Fc9G>0ze@+-`lP}c{S+m z8rim{eyA$jVN-ut0f3o$wM26}m>TFVH>g_Nrn4j+=Vh5FgDUMMt2ADQ24mEGE(& zTrt8OPLZwRM|S$5k+wH}8@|iK{)%G5ysX(-s2)|X8XXRQomol)5T1$UVwZYKmA@}E zZUU9pM3(z(TAm}9?(*PsENOHbWuJUxW|@3`Qryj1xXTpx{M)J-#-()xapQs(zPeL~*`TsJiV5 z`D;Lz%Qm^LkJx87@UtB@ZYTnLQtg`Xv&!dnnK;h$N`cX)&?SE7YO^hAcW!-O(AnFd z31jLCAwnJ~#okmiw{zU})J46U5W|ZAR)_{>RV2#2pmb{Z~Cx zX^fl1iEQCZ9JEB6;SZb|ouM-m()0)A7!9xq;%o9}EbZd7KaXqts;^>j_(&~ySnt1` z2&MfPz5c5BvlsZ&>Bg69Ize)>(n)|Q-M)ya?1=lMqXk!e>R>WEaz!@|KF@Yn7* z(MGSU3r@j^MRZ~6`0nw0jFGQ!$(8Gq&YM!H-KM`RBH!NC(~qDtnaT1LmxPzWr_% z3eyExC;2JYJ&fWzU|< z`_Zfza(U89{&8si+(H|ip4+MlGuf$d81J=2N^U}Go^E(@l4e16jBc7vntnn- zTAprUl!&g(zJ z{5MsDKf!*drwjk>0hh)9DcHZ4cKwO=J8SzF<^8WgBL5$t{l(M%6Xun zPl5iRg#QWk`vLjizpx1Pe+u<0ZSoh_?oXuO53z**dZO2>tq=bc>EDmv|3v#eFaLM6 zVxxbG_V34Jf1>@Ki~9Sb7cu*%X#bY^{}b%@5bp0_?biPk?B9}Ie}esPnE$>Muk8OR u*uR$IUoqmJP`?}EUzYx_5$^W?u=`4~kgpEIn>XmMzlv9i$6NPbcmE4;GY6Xh literal 0 HcmV?d00001 -- 2.39.5