- Introduction
lock
commandlock
command in action- Example Project Structure
- Take control of the source
- Lock the projects
- Load the Example
- Appendix
- Example Project
- Alternate Project
- External Project
- Sample Project
- Converting configurations to baselines
- Package download script for complex projects
- Example project unload script
- Metacello project
load
details
When you put a project into production you should be managing the references to external projects very carefully. You cannot afford to have an innocent update by the external project maintainers introduce a regression into your production application.
In the past, if you wanted to take total control of your project's source code, you needed to resort to using repository overrides or editting configuration files directly. Neither of these options are ideal.
The lock
command was created to provide developers with
explicit control over which version of a project is to be used.
When you lock
a configuration-based project:
Metacello new
configuration: 'External';
version: '1.0.0';
repository: 'filetree:///opt/git/external';
lock.
you are telling Metacello to always use the given version and to always load the configuration from the given repository.
If you specify a locally owned, disk-based repository as in the example above, you have effectively taken control of the configuration and are able to insulate yourself from any changes made to the configuration.
Note that the packages specified in the configuration will continue to be loaded from the repositories as specified in the configration.
When you lock
a baseline-based project:
Metacello new
baseline: 'External';
repository: 'filetree:///opt/git/external';
lock.
you are telling Metacello to always load the baseline from the given repository and, given the way that baseline-based projects work, to always load the packages referenced by the baseline specification from the given repository.
By using a baseline-based project you are able to take control of the specification and the packages.
If you want to execute the examples in your image, you need to make sure that you've installed the Metacello Preview.
In order to give you a better feel for how the lock
command works,
I've created a collection of projects that can be used for hands on
experiments with various aspects of the lock
command:
The Example project has been created to represent your production
application. For the purposes of this example, you own the baseline and the packages associated with
this project. So let's clone the github repository to your local disk
and checkout the otto
branch:
cd /opt/git
git clone [email protected]:dalehenrich/example.git
cd example
git checkout otto
Now, let's load the BaselineOfExample
into our image, so we can see
the structure of your project:
Metacello new
baseline: 'Example';
repository: 'filetree:///opt/git/example/repository';
get.
With the baseline loaded, let's navigate to the baseline spec
(BaselineOfExample>>baseline:
) and see what we have:
baseline: spec
<baseline>
spec
for: #'common'
do: [
spec
baseline: 'Alternate'
with: [ spec repository: 'github://dalehenrich/alternate:otto/repository' ];
import: 'Alternate' provides: #('Alternate Core' 'Alternate Tests');
yourself.
spec
package: 'Example-Core' with: [ spec requires: #('Alternate Core') ];
package: 'Example-Tests'
with: [ spec requires: #('Example-Core' 'Alternate Tests') ];
yourself.
spec
group: 'default' with: #('Core');
group: 'Core' with: #('Example-Core');
group: 'Tests' with: #('Example-Tests');
group: 'Example Core' with: #('Core');
group: 'Example Tests' with: #('Tests');
yourself ]
From the baseline we see that there is a single external project referenced:
For the purposes of this example, the Alternate project is the moral equivalent of Seaside (a project that itself is composed of a number projects).
Let's load the BaselineOfAlternate
baseline:
Metacello new
baseline: 'Alternate';
repository: 'github:/dalehenrich/alternate:otto/repository';
get.
navigate to the baseline spec
(BaselineOfAlternate>>baseline:
) and see what we have:
baseline: spec
<baseline>
spec
for: #'common'
do: [
spec
configuration: 'External'
with: [
spec
version: '1.0.0';
loads: 'External Core';
repository: 'http://ss3.gemtalksystems.com/ss/external' ];
baseline: 'Sample'
with: [ spec repository: 'github://dalehenrich/sample:otto/repository' ];
import: 'Sample' provides: #('Sample Core' 'Sample Tests');
yourself.
spec
package: 'Alternate-Core'
with: [ spec requires: #('Sample Core' 'External') ];
package: 'Alternate-Tests'
with: [ spec requires: #('Alternate-Core' 'Sample Tests') ];
yourself.
spec
group: 'default' with: #('Core');
group: 'Core' with: #('Alternate-Core');
group: 'Tests' with: #('Alternate-Tests');
group: 'Alternate Core' with: #('Core');
group: 'Alternate Tests' with: #('Tests');
yourself ]
The Alternate project is composed of two more external projects a configuration-based project External Project and a baseline-based project Sample Project.
At the end of the day, you will need to take control control of two baseline-based projects and a configuration-based project.
As discussed earlier, once you've cloned the repositories for the baseline-based projects (Alternate and Sample(, the projects are completely under your control:
cd /opt/git
git clone [email protected]:dalehenrich/sample.git
git clone [email protected]:dalehenrich/alternate.git
To take control of the configuration-based project, you can either copy the configuration to a local repository or you can convert the project into a baseline-based project.
If you decide to copy the configuration, you should plan on editting the specs for the version that you are using to reference your own package repository for the project.
If you decide to convert the project into a baseline-based project, you'll need to extract the baseline for version that you are using.
- Create a local filetree repository.
- Copy the packages from the http repository into the local filetree repository.
- Create a BaselineOfExternal to replace the ConfigurationOfExternal.
First create a local directory for the repository (ans optionally turn the directory into a git repository):
cd /opt/git
mkdir externalDir
cd externalDir
mkdir repository
git init
Use the fetch
command to download the project packages into the
filetree repository:
Metacello new
configuration: 'External';
version: '1.0.0';
repository: 'http://ss3.gemtalksystems.com/ss/external';
cacheRepository: 'filetree:///opt/git/externalDir/repository';
ignoreImage;
get; "get will download the ConfigurationOfExternal to the cache repository"
fetch: 'ALL'.
If you project is more complex (i.e., has dependent projects) then you will want to isolate each project in it's own git repository. In that case you will want to use the [complex project download script][#package-download-script-for-complex-projects].
Start by creating the BaselineOfExternal as a subclass of
BaselineOf. The class should be in a category of the same name and
create a package for that category as well. Then copy the appropriate baseline
method from CofigurationOfExternal in this
case ConfigurationOfExternal>>baseline1000:
:
baseline1000: spec
<baseline: '1.0.0-baseline'>
spec
for: #'common'
do: [
spec blessing: #'baseline'.
spec repository: 'http://ss3.gemstone.com/ss/external'.
spec
package: 'External-Core';
package: 'External-Tests' with: [ spec requires: 'External-Core' ];
yourself.
spec
group: 'default' with: #('Core');
group: 'Core' with: #('External-Core');
group: 'Tests' with: #('External-Tests');
group: 'External Core' with: #('Core');
group: 'External Tests' with: #('Tests');
yourself ]
into BaselineOfExternal>>baseline:
and edit out the unnecessary
statements. In this case we only needed to change the pragma to <baseline>
and
remove the #blessing: and #repository: statements:
baseline: spec
<baseline>
spec
for: #'common'
do: [
spec
package: 'External-Core';
package: 'External-Tests' with: [ spec requires: 'External-Core' ];
yourself.
spec
group: 'default' with: #('Core');
group: 'Core' with: #('External-Core');
group: 'Tests' with: #('External-Tests');
group: 'External Core' with: #('Core');
group: 'External Tests' with: #('Tests');
yourself ]
For more complicated conversion problems, see the section on Converting configurations to baselines in the Appendix.
Now that the repositories have been cloned to your local disk, the
lock
command can be used to associate the local repository with each
of the projects:
Metacello new
baseline: 'Alternate';
repository: 'filetree:///opt/git/alternate/repository';
lock.
Metacello new
baseline: 'External';
repository: 'filetree:///opt/git/externalDir/repository';
lock.
Metacello new
baseline: 'Sample';
repository: 'filetree:///opt/git/sample/repository';
lock.
Use the following set of expressions to load the Example project:
Metacello new
baseline: 'Example';
repository: 'github://dalehenrich/example:otto/repository';
onLock: [ :ex | ex honor ];
get;
load: 'Tests'.
The onLock:
block gets triggered every time a locked project is loaded,
because you should always be informed when a locked project is
referenced, since you always run the risk of introducing an
incompatibility when you aren't using the official repository.
- Example Project
- Alternate Project
- External Project
- Sample Project
- Converting configurations to baselines
- Package download script for complex projects
- Example project unload script
- Metacello project
load
details
Baseline:
baseline: spec
<baseline>
spec
for: #'common'
do: [
spec
configuration: 'External'
with: [
spec
version: '1.0.0';
loads: 'External Core';
repository: 'http://ss3.gemtalksystems.com/ss/external' ];
baseline: 'Sample'
with: [ spec repository: 'github://dalehenrich/sample:otto/repository' ];
import: 'Sample' provides: #('Sample Core' 'Sample Tests');
yourself.
spec
package: 'Alternate-Core'
with: [ spec requires: #('Sample Core' 'External') ];
package: 'Alternate-Tests'
with: [ spec requires: #('Alternate-Core' 'Sample Tests') ];
yourself.
spec
group: 'default' with: #('Core');
group: 'Core' with: #('Alternate-Core');
group: 'Tests' with: #('Alternate-Tests');
group: 'Alternate Core' with: #('Core');
group: 'Alternate Tests' with: #('Tests');
yourself ]
Configuration:
baseline1000: spec
<version: '1.0.0-baseline'>
spec
for: #'common'
do: [
spec blessing: #'baseline'.
spec repository: 'http://ss3.gemstone.com/ss/external'.
spec
package: 'External-Core';
package: 'External-Tests' with: [ spec requires: 'External-Core' ];
yourself.
spec
group: 'default' with: #('Core');
group: 'Core' with: #('External-Core');
group: 'Tests' with: #('External-Tests');
group: 'External Core' with: #('Core');
group: 'External Tests' with: #('Tests');
yourself ]
Baseline:
baseline: spec
<baseline>
spec
for: #'common'
do: [
spec
package: 'External-Core';
package: 'External-Tests' with: [ spec requires: 'External-Core' ];
yourself.
spec
group: 'default' with: #('Core');
group: 'Core' with: #('External-Core');
group: 'Tests' with: #('External-Tests');
group: 'External Core' with: #('Core');
group: 'External Tests' with: #('Tests');
yourself ]
Baseline:
baseline: spec
<baseline>
spec
for: #'common'
do: [
spec package: 'Sample-Core'.
spec package: 'Sample-Tests' with: [ spec requires: 'Sample-Core' ].
spec
group: 'default' with: #('Sample-Core');
group: 'Core' with: #('Sample-Core');
group: 'Tests' with: #('Sample-Tests');
group: 'Sample Core' with: #('Core');
group: 'Sample Tests' with: #('Tests');
yourself ]
| configClass cacheRepository version |
configClass := ConfigurationOfExternal.
cacheRepository := MCDirectoryRepository
directory: (FileDirectory on:'/opt/git/externalDir/repository').
cacheRepository := MCRepositoryGroup default repositories
detect: [ :each | each = cacheRepository ]
ifNone: [ cacheRepository ].
version := configClass project version: '1.0.0'.
version ignoreImage: true.
(version record: 'ALL') loadDirective
versionDirectivesDo: [ :versionDirective |
| p pClass |
versionDirective spec ~~ nil
ifTrue: [
p := versionDirective spec project.
pClass := p configuration class. "save packages for configClass only"
(pClass == configClass)
ifTrue: [
| policy |
policy := (Smalltalk at: #'MetacelloLoaderPolicy') new
cacheRepository: cacheRepository;
ignoreImage: true;
yourself.
p fetchProject: policy.
versionDirective
packagesDo: [ :packageDirective |
"skip nested configurations"
(packageDirective spec name beginsWith: 'ConfigurationOf')
ifFalse: [
"fetch mcz file"
packageDirective spec fetchPackage: policy ] ] ] ] ]
| gofer |
gofer := Gofer new.
#('BaselineOfExample' 'ConfigurationOfExternal' 'BaselineOfExternal'
'External-Core' 'External-Tests' 'BaselineOfSample' 'Example-Core'
'Example-Tests' 'Sample-Core' 'Sample-Tests') do: [:packageName |
(MCWorkingCopy allManagers
detect: [:wc | wc packageName = packageName ]
ifAbsent: [])
ifNotNil: [ gofer package: packageName ] ].
gofer unload.
During a load
, when a project reference is encountered, Metacello
routinely looks up the project in the project registry to see which
version of the project is present in the image.
If the project is present in the image, the details for the existing project are compared to the details for the incoming project. If there are differences (older version or different repository location), a project resolution exception is signalled and the developer must choose whether to continue the load with the existing project or the incoming project. Project resolution exceptions come in three flavors:
- Upgrade. The incoming project spec is newer than existing version project spec
- Downgrade. The incoming project spec is older than existing project spec
- Conflict. The incoming repository is different than the existing repository or the project is switching from configuration-based to baseline-based, etc.
The default action for the upgrade exception is to use the incoming
project. The downgrade exception and conflict exception must be
handled. The onUpgrade:
, onDowngrade:
and onConflict:
directives
are provided to simplify the mechanics of exception handling during a
load
.
Once the developer has chosen which project spec to load, the registration is checked a final time. If the project spec is locked a locked project change exception is signalled and the developer must choose to honor the lock or to break the lock
In this example script where the External
project (referenced by
the Alternate project is locked
:
Metacello new
baseline: 'External';
repository: 'filetree:///opt/git/external';
lock.
Metacello new
baseline: 'Alternate';
repository: 'github://dalehenrich/alternate:otto/repository';
onLock: [ :ex | ex honor ];
load: 'Tests'.
The External project baseline and packages will be loaded from the filetree:///opt/git/external repository instead of the http://ss3.gemtalksystems.com/ss/external repository as specified by the Alternate project.