summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Thursfield <sam.thursfield@codethink.co.uk>2012-12-13 15:48:22 +0000
committerSam Thursfield <sam.thursfield@codethink.co.uk>2012-12-13 15:48:22 +0000
commitf2658d3bfeea50a69753ec4eb23181930cc4d88c (patch)
treeea1fd8a17174db93d00c02ced3e0c8665f995d2c
parentd63c97a0bef1cd2f03ca266acda67cad065632df (diff)
parent51e4bbb4dffde9574404df9c5e947f518dc49a41 (diff)
downloadmorph-f2658d3bfeea50a69753ec4eb23181930cc4d88c.tar.gz
Merge remote-tracking branch 'origin/jannispohlmann/morph-tag-v2'
-rw-r--r--morphlib/git.py13
-rw-r--r--morphlib/plugins/branch_and_merge_plugin.py203
-rwxr-xr-xtests.branching/morph-tag-creates-commit-and-tag.script39
-rw-r--r--tests.branching/morph-tag-creates-commit-and-tag.stdout55
-rw-r--r--tests.branching/morph-tag-fails-if-tag-exists.exit1
-rwxr-xr-xtests.branching/morph-tag-fails-if-tag-exists.script33
-rw-r--r--tests.branching/morph-tag-fails-if-tag-exists.stderr3
-rwxr-xr-xtests.branching/morph-tag-tag-works-as-expected.script46
-rw-r--r--tests.branching/morph-tag-tag-works-as-expected.stdout58
-rwxr-xr-xtests.branching/morph-tag-works-with-multiple-morphs-repos.script123
-rw-r--r--tests.branching/morph-tag-works-with-multiple-morphs-repos.stdout172
11 files changed, 746 insertions, 0 deletions
diff --git a/morphlib/git.py b/morphlib/git.py
index 7985b815..a37b6675 100644
--- a/morphlib/git.py
+++ b/morphlib/git.py
@@ -170,6 +170,19 @@ def get_user_name(runcmd):
' git config --global user.email "me@example.com"\n')
+def get_user_email(runcmd):
+ '''Get user.email configuration setting. Complain if none was found.'''
+ if 'GIT_AUTHOR_EMAIL' in os.environ:
+ return os.environ['GIT_AUTHOR_EMAIL'].strip()
+ try:
+ return runcmd(['git', 'config', 'user.email']).strip()
+ except cliapp.AppException:
+ raise cliapp.AppException(
+ 'No git user info found. Please set your identity, using: \n'
+ ' git config --global user.email "My Name"\n'
+ ' git config --global user.email "me@example.com"\n')
+
+
def set_remote(runcmd, gitdir, name, url):
'''Set remote with name 'name' use a given url at gitdir'''
return runcmd(['git', 'remote', 'set-url', name, url], cwd=gitdir)
diff --git a/morphlib/plugins/branch_and_merge_plugin.py b/morphlib/plugins/branch_and_merge_plugin.py
index efaa8dfe..b848eb81 100644
--- a/morphlib/plugins/branch_and_merge_plugin.py
+++ b/morphlib/plugins/branch_and_merge_plugin.py
@@ -48,6 +48,7 @@ class BranchAndMergePlugin(cliapp.Plugin):
arg_synopsis='SYSTEM STRATUM [CHUNK]')
self.app.add_subcommand('petrify', self.petrify)
self.app.add_subcommand('unpetrify', self.unpetrify)
+ self.app.add_subcommand('tag', self.tag)
self.app.add_subcommand('build', self.build,
arg_synopsis='SYSTEM')
self.app.add_subcommand('status', self.status)
@@ -825,6 +826,208 @@ class BranchAndMergePlugin(cliapp.Plugin):
self.print_changelog('The following changes were made but have not '
'been committed')
+ def tag(self, args):
+ if len(args) < 1:
+ raise cliapp.AppException('morph tag expects a tag name')
+
+ tagname = args[0]
+
+ # Deduce workspace, system branch and branch root repository.
+ workspace = self.deduce_workspace()
+ branch, branch_dir = self.deduce_system_branch()
+ branch_root = self.get_branch_config(branch_dir, 'branch.root')
+ branch_root_dir = self.find_repository(branch_dir, branch_root)
+
+ # Define the committer.
+ committer_name = morphlib.git.get_user_name(self.app.runcmd)
+ committer_email = morphlib.git.get_user_email(self.app.runcmd)
+
+ # Prepare an environment for our internal index file.
+ # This index file allows us to commit changes to a tree without
+ # git noticing any change in the working tree or its own index.
+ env = dict(os.environ)
+ env['GIT_INDEX_FILE'] = os.path.join(
+ branch_root_dir, '.git', 'morph-tag-index')
+ env['GIT_COMMITTER_NAME'] = committer_name
+ env['GIT_COMMITTER_EMAIL'] = committer_email
+
+ # Extract git arguments that deal with the commit message.
+ # This is so that we can use them for creating the tag commit.
+ msg_args = []
+ for i in xrange(0, len(args)):
+ if args[i] == '-m' or args[i] == '-F':
+ if i < len(args)-1:
+ msg_args.append(args[i])
+ msg_args.append(args[i+1])
+ elif args[i].startswith('--message='):
+ msg_args.append(args[i])
+
+ # Fail if no commit message was provided.
+ if not msg_args:
+ raise cliapp.AppException(
+ 'Commit message expected. Please run one of '
+ 'the following commands to provide one:\n'
+ ' morph tag NAME -- -m "Message"\n'
+ ' morph tag NAME -- --message="Message"\n'
+ ' morph tag NAME -- -F <message file>')
+
+ # Abort if the tag already exists.
+ try:
+ morphlib.git.rev_parse(self.app.runcmd, branch_root_dir,
+ 'refs/tags/%s' % tagname)
+ raise cliapp.AppException('%s: Tag \"%s\" already exists' %
+ (branch_root, tagname))
+ except:
+ pass
+
+ self.app.status(msg='%(repo)s: Preparing tag commit',
+ repo=branch_root)
+
+ # Read current tree into the internal index.
+ parent_sha1 = self.resolve_ref(branch_root_dir, branch)
+ self.app.runcmd(['git', 'read-tree', parent_sha1],
+ cwd=branch_root_dir, env=env)
+
+ self.app.status(msg='%(repo)s: Petrifying everything',
+ repo=branch_root)
+
+ # Petrify everything.
+ self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app)
+ self.petrify_everything(branch, branch_dir,
+ branch_root, branch_root_dir,
+ tagname, env)
+
+ self.app.status(msg='%(repo)s: Creating tag commit',
+ repo=branch_root)
+
+ # Create a dangling commit.
+ commit = self.create_tag_commit(
+ branch_root_dir, tagname, msg_args, env)
+
+ self.app.status(msg='%(repo)s: Creating annotated tag "%(tag)s"',
+ repo=branch_root, tag=tagname)
+
+ # Create an annotated tag for this commit.
+ self.create_annotated_tag(branch_root_dir, commit, env, args)
+
+ def petrify_everything(self, branch, branch_dir,
+ branch_root, branch_root_dir, tagref, env):
+ petrified_morphologies = set()
+ resolved_refs = {}
+ for f in sorted(glob.iglob(os.path.join(branch_root_dir, '*.morph'))):
+ name = os.path.basename(f)[:-len('.morph')]
+ morphology = self.load_morphology(branch_root_dir, name)
+ self.petrify_morphology(branch, branch_dir,
+ branch_root, branch_root_dir,
+ branch_root, branch_root_dir,
+ tagref, name, morphology,
+ petrified_morphologies, resolved_refs, env)
+
+ def petrify_morphology(self, branch, branch_dir,
+ branch_root, branch_root_dir, repo, repo_dir,
+ tagref, name, morphology,
+ petrified_morphologies, resolved_refs, env):
+ self.app.status(msg='%(repo)s: Petrifying morphology \"%(morph)s\"',
+ repo=repo, morph=name)
+
+ # Mark morphology as petrified_morphologies so we don't petrify it twice.
+ petrified_morphologies.add(morphology)
+
+ # Resolve the refs of all build dependencies (strata) and strata
+ # in the morphology into commit SHA1s.
+ strata = []
+ if 'build-depends' in morphology and morphology['build-depends']:
+ strata += morphology['build-depends']
+ if 'strata' in morphology and morphology['strata']:
+ strata += morphology['strata']
+ for info in strata:
+ # Obtain the commit SHA1 this stratum would be built from.
+ commit, tree = self.resolve_info(info, resolved_refs)
+ stratum_repo_dir = self.edit_stratum(
+ branch, branch_dir, repo, repo_dir, info)
+
+ # Load the stratum morphology and petrify it recursively if
+ # that hasn't happened yet.
+ stratum = self.load_morphology(stratum_repo_dir, info['morph'])
+ if not stratum in petrified_morphologies:
+ self.petrify_morphology(branch, branch_dir,
+ branch_root, branch_root_dir,
+ info['repo'], stratum_repo_dir,
+ tagref, info['morph'], stratum,
+ petrified_morphologies,
+ resolved_refs, env)
+
+ # Change the ref for this morphology to the tag we're creating.
+ if info['ref'] != tagref:
+ info['unpetrify-ref'] = info['ref']
+ info['ref'] = tagref
+
+ # We'll be copying all systems/strata into the tag commit
+ # in the branch root repo, so make sure to note what repos
+ # they all came from
+ if info['repo'] != branch_root:
+ info['unpetrify-repo'] = info['repo']
+ info['repo'] = branch_root
+
+ # If this morphology is a stratum, resolve the refs of all its
+ # chunks into SHA1s.
+ if morphology['kind'] == 'stratum':
+ for info in morphology['chunks']:
+ commit, tree = self.resolve_info(info, resolved_refs)
+ if info['ref'] != commit:
+ info['unpetrify-ref'] = info['ref']
+ info['ref'] = commit
+
+ # Write the petrified morphology to a temporary file in the
+ # branch root repository for inclusion in the tag commit.
+ handle, tmpfile = tempfile.mkstemp(suffix='.morph')
+ self.save_morphology(branch_root_dir, tmpfile, morphology)
+
+ # Hash the petrified morphology and add it to the index
+ # for the tag commit.
+ sha1 = self.app.runcmd(
+ ['git', 'hash-object', '-t', 'blob', '-w', tmpfile],
+ cwd=branch_root_dir, env=env)
+ self.app.runcmd(
+ ['git', 'update-index', '--add', '--cacheinfo',
+ '100644', sha1, '%s.morph' % name],
+ cwd=branch_root_dir, env=env)
+
+ # Delete the temporary file again.
+ os.remove(tmpfile)
+
+ def resolve_info(self, info, resolved_refs):
+ '''Takes a morphology info and resolves its ref with cache support.'''
+
+ key = (info['repo'], info['ref'])
+ if not key in resolved_refs:
+ commit_sha1, tree_sha1 = self.app.resolve_ref(
+ self.lrc, self.rrc, info['repo'], info['ref'],
+ update=not self.app.settings['no-git-update'])
+ resolved_refs[key] = (commit_sha1, tree_sha1)
+ return resolved_refs[key]
+
+ def create_tag_commit(self, repo_dir, tagname, args, env):
+ self.app.status(msg='%(repo)s: Creating commit for the tag',
+ repo=repo_dir)
+
+ # Write and commit the tree.
+ tree = self.app.runcmd(
+ ['git', 'write-tree'], cwd=repo_dir, env=env).strip()
+ commit = self.app.runcmd(
+ ['git', 'commit-tree', tree, '-p', 'HEAD'] + args,
+ cwd=repo_dir, env=env).strip()
+ return commit
+
+ def create_annotated_tag(self, repo_dir, commit, env, args=[]):
+ self.app.status(msg='%(repo)s: Creating annotated tag for '
+ 'commit %(commit)s',
+ repo=repo_dir, commit=commit)
+
+ # Create an annotated tag for the commit
+ self.app.runcmd(['git', 'tag', '-a'] + args + [commit],
+ cwd=repo_dir, env=env)
+
# When 'merge' is unset, git doesn't try to resolve conflicts itself in
# those files.
MERGE_ATTRIBUTE = '*.morph\t-merge\n'
diff --git a/tests.branching/morph-tag-creates-commit-and-tag.script b/tests.branching/morph-tag-creates-commit-and-tag.script
new file mode 100755
index 00000000..4de3e4b9
--- /dev/null
+++ b/tests.branching/morph-tag-creates-commit-and-tag.script
@@ -0,0 +1,39 @@
+#!/bin/sh
+#
+# Copyright (C) 2012 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+## Check that 'morph tag example-tag' successfully creates a dangling
+## commit and an annotated tag pointing to this commit.
+
+set -eu
+
+# Make sure the commits always have the same SHA1s.
+source "$SRCDIR/scripts/fix-committer-info"
+
+# Create a workspace and branch.
+cd "$DATADIR/workspace"
+"$SRCDIR/scripts/test-morph" init
+"$SRCDIR/scripts/test-morph" checkout test:morphs master
+
+# Tag the system branch.
+"$SRCDIR/scripts/test-morph" tag example-tag -- -m Message
+
+# Show the tag itself and its log. This allows to verify a couple of things,
+# including that the commit and tag are created, that the commit message is
+# set correctly and that all references are petrified.
+"$SRCDIR/scripts/test-morph" foreach -- git show example-tag
+"$SRCDIR/scripts/test-morph" foreach -- git log example-tag
diff --git a/tests.branching/morph-tag-creates-commit-and-tag.stdout b/tests.branching/morph-tag-creates-commit-and-tag.stdout
new file mode 100644
index 00000000..38bca41b
--- /dev/null
+++ b/tests.branching/morph-tag-creates-commit-and-tag.stdout
@@ -0,0 +1,55 @@
+test:morphs
+tag example-tag
+Tagger: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+Message
+
+commit 7cab6b5f121518b3ecd585c4af3c5bc267947bae
+Author: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+ Message
+
+diff --git a/hello-stratum.morph b/hello-stratum.morph
+index 3b7be17..87561c1 100644
+--- a/hello-stratum.morph
++++ b/hello-stratum.morph
+@@ -5,8 +5,9 @@
+ {
+ "name": "hello",
+ "repo": "test:hello",
+- "ref": "master",
+- "build-depends": []
++ "ref": "f4d032b42c0134e67bdf19a43fa99072493667d7",
++ "build-depends": [],
++ "unpetrify-ref": "master"
+ }
+ ]
+ }
+diff --git a/hello-system.morph b/hello-system.morph
+index f3f64b4..d26675d 100644
+--- a/hello-system.morph
++++ b/hello-system.morph
+@@ -8,7 +8,8 @@
+ {
+ "morph": "hello-stratum",
+ "repo": "test:morphs",
+- "ref": "master"
++ "ref": "example-tag",
++ "unpetrify-ref": "master"
+ }
+ ]
+ }
+test:morphs
+commit 7cab6b5f121518b3ecd585c4af3c5bc267947bae
+Author: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+ Message
+
+commit 48d38ef3f39857d7dba4ed1ffc51653c6bed4906
+Author: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+ initial
diff --git a/tests.branching/morph-tag-fails-if-tag-exists.exit b/tests.branching/morph-tag-fails-if-tag-exists.exit
new file mode 100644
index 00000000..d00491fd
--- /dev/null
+++ b/tests.branching/morph-tag-fails-if-tag-exists.exit
@@ -0,0 +1 @@
+1
diff --git a/tests.branching/morph-tag-fails-if-tag-exists.script b/tests.branching/morph-tag-fails-if-tag-exists.script
new file mode 100755
index 00000000..a30bb774
--- /dev/null
+++ b/tests.branching/morph-tag-fails-if-tag-exists.script
@@ -0,0 +1,33 @@
+#!/bin/sh
+#
+# Copyright (C) 2012 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+## Check that 'morph tag example-tag' fails if 'example-tag' already exists.
+
+set -eu
+
+# Make sure the commits always have the same SHA1s.
+source "$SRCDIR/scripts/fix-committer-info"
+
+# Create a workspace and branch.
+cd "$DATADIR/workspace"
+"$SRCDIR/scripts/test-morph" init
+"$SRCDIR/scripts/test-morph" checkout test:morphs master
+
+# Tag the system branch twice.
+"$SRCDIR/scripts/test-morph" tag example-tag -- -m First
+"$SRCDIR/scripts/test-morph" tag example-tag -- -m Second
diff --git a/tests.branching/morph-tag-fails-if-tag-exists.stderr b/tests.branching/morph-tag-fails-if-tag-exists.stderr
new file mode 100644
index 00000000..66c71252
--- /dev/null
+++ b/tests.branching/morph-tag-fails-if-tag-exists.stderr
@@ -0,0 +1,3 @@
+ERROR: Command failed: git tag -a example-tag -m Second 70d7adfe89033309e0494efbe6df3e4a5604e558
+fatal: tag 'example-tag' already exists
+
diff --git a/tests.branching/morph-tag-tag-works-as-expected.script b/tests.branching/morph-tag-tag-works-as-expected.script
new file mode 100755
index 00000000..25ba4a51
--- /dev/null
+++ b/tests.branching/morph-tag-tag-works-as-expected.script
@@ -0,0 +1,46 @@
+#!/bin/sh
+#
+# Copyright (C) 2012 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+## Check that tagging an existing tag commit with 'morph tag' preserves
+## the unpetrify-ref and does not "double-petrify" apart from updating
+## references to "example-tag" to "tagged-tag".
+
+set -eu
+
+# Make sure the commits always have the same SHA1s.
+source "$SRCDIR/scripts/fix-committer-info"
+
+# Create a workspace and branch.
+cd "$DATADIR/workspace"
+"$SRCDIR/scripts/test-morph" init
+"$SRCDIR/scripts/test-morph" checkout test:morphs master
+
+# Tag the system branch.
+"$SRCDIR/scripts/test-morph" tag example-tag -- -m First
+
+# Check out the tag.
+"$SRCDIR/scripts/run-git-in" master/test:morphs checkout -b example-tag \
+ 2>/dev/null
+
+# Tag the tag.
+"$SRCDIR/scripts/test-morph" tag tagged-tag -- -m Second
+
+# List all tags and show the second one.
+"$SRCDIR/scripts/test-morph" foreach -- git tag -l
+"$SRCDIR/scripts/test-morph" foreach -- git show tagged-tag
+"$SRCDIR/scripts/test-morph" foreach -- git log tagged-tag
diff --git a/tests.branching/morph-tag-tag-works-as-expected.stdout b/tests.branching/morph-tag-tag-works-as-expected.stdout
new file mode 100644
index 00000000..58d77714
--- /dev/null
+++ b/tests.branching/morph-tag-tag-works-as-expected.stdout
@@ -0,0 +1,58 @@
+test:morphs
+example-tag
+tagged-tag
+test:morphs
+tag tagged-tag
+Tagger: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+Second
+
+commit f7f04d63ec5e494cf5b4fd2e94ad5f2f26265c3f
+Author: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+ Second
+
+diff --git a/hello-stratum.morph b/hello-stratum.morph
+index 3b7be17..87561c1 100644
+--- a/hello-stratum.morph
++++ b/hello-stratum.morph
+@@ -5,8 +5,9 @@
+ {
+ "name": "hello",
+ "repo": "test:hello",
+- "ref": "master",
+- "build-depends": []
++ "ref": "f4d032b42c0134e67bdf19a43fa99072493667d7",
++ "build-depends": [],
++ "unpetrify-ref": "master"
+ }
+ ]
+ }
+diff --git a/hello-system.morph b/hello-system.morph
+index f3f64b4..2981e9b 100644
+--- a/hello-system.morph
++++ b/hello-system.morph
+@@ -8,7 +8,8 @@
+ {
+ "morph": "hello-stratum",
+ "repo": "test:morphs",
+- "ref": "master"
++ "ref": "tagged-tag",
++ "unpetrify-ref": "master"
+ }
+ ]
+ }
+test:morphs
+commit f7f04d63ec5e494cf5b4fd2e94ad5f2f26265c3f
+Author: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+ Second
+
+commit 48d38ef3f39857d7dba4ed1ffc51653c6bed4906
+Author: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+ initial
diff --git a/tests.branching/morph-tag-works-with-multiple-morphs-repos.script b/tests.branching/morph-tag-works-with-multiple-morphs-repos.script
new file mode 100755
index 00000000..374c9ff5
--- /dev/null
+++ b/tests.branching/morph-tag-works-with-multiple-morphs-repos.script
@@ -0,0 +1,123 @@
+#!/bin/sh
+#
+# Copyright (C) 2012 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+## Check that "morph tag" works if morphologies are spread across multiple
+## repositories. In this case, it should copy all petrified morphologies
+## into the branch root repository and only create the tag there.
+
+set -eu
+
+# Make sure the commits always have the same SHA1s.
+source "$SRCDIR/scripts/fix-committer-info"
+
+# Create new directory for repos used in this test.
+mkdir -p "$DATADIR/repos"
+
+# Create the first morphs repository.
+mkdir "$DATADIR/repos/morphs1"
+
+# Create system morphology in first morphs repository.
+cat <<EOF > "$DATADIR/repos/morphs1/test-system.morph"
+{
+ "name": "test-system",
+ "kind": "system",
+ "system-kind": "syslinux-disk",
+ "arch": "$(uname -m)",
+ "disk-size": "1G",
+ "strata": [
+ {
+ "morph": "stratum1",
+ "repo": "repos:morphs1",
+ "ref": "master"
+ },
+ {
+ "morph": "stratum2",
+ "repo": "repos:morphs2",
+ "ref": "master"
+ }
+ ]
+}
+EOF
+
+# Create stratum that depends on another stratum.
+cat <<EOF > "$DATADIR/repos/morphs1/stratum1.morph"
+{
+ "name": "stratum1",
+ "kind": "stratum",
+ "build-depends": [
+ {
+ "morph": "stratum3",
+ "repo": "repos:morphs2",
+ "ref": "master"
+ }
+ ]
+}
+EOF
+
+# Commit all files to the first repository.
+scripts/run-git-in "$DATADIR/repos/morphs1" init
+scripts/run-git-in "$DATADIR/repos/morphs1" add .
+scripts/run-git-in "$DATADIR/repos/morphs1" commit -m initial
+
+# Create second morphs repository.
+mkdir "$DATADIR/repos/morphs2"
+
+# Create two strata in the second repository.
+cat <<EOF > "$DATADIR/repos/morphs2/stratum2.morph"
+{
+ "name": "stratum2",
+ "kind": "stratum",
+ "build-depends": [
+ {
+ "morph": "stratum3",
+ "repo": "repos:morphs2",
+ "ref": "master"
+ }
+ ]
+}
+EOF
+cat <<EOF > "$DATADIR/repos/morphs2/stratum3.morph"
+{
+ "name": "stratum3",
+ "kind": "stratum"
+}
+EOF
+
+# Commit all files to the second repository.
+"$SRCDIR/scripts/run-git-in" "$DATADIR/repos/morphs2" init
+"$SRCDIR/scripts/run-git-in" "$DATADIR/repos/morphs2" add .
+"$SRCDIR/scripts/run-git-in" "$DATADIR/repos/morphs2" commit -m initial
+
+cd "$DATADIR/workspace"
+"$SRCDIR/scripts/test-morph" init
+
+# Check out the master system branch.
+"$SRCDIR/scripts/test-morph" \
+ --repo-alias=repos="file://$DATADIR/repos/%s#file://$DATADIR/%s" \
+ checkout repos:morphs1 master
+
+# Tag the master system branch.
+"$SRCDIR/scripts/test-morph" \
+ --repo-alias=repos="file://$DATADIR/repos/%s#file://$DATADIR/%s" \
+ tag tag-across-multiple-repos -- -m "create tag"
+
+# Show the tag.
+GIT_DIR="$DATADIR/workspace/master/repos:morphs1/.git" \
+ git show tag-across-multiple-repos
+GIT_DIR="$DATADIR/workspace/master/repos:morphs1/.git" \
+ git log -n1 -p --stat tag-across-multiple-repos
diff --git a/tests.branching/morph-tag-works-with-multiple-morphs-repos.stdout b/tests.branching/morph-tag-works-with-multiple-morphs-repos.stdout
new file mode 100644
index 00000000..91b36f99
--- /dev/null
+++ b/tests.branching/morph-tag-works-with-multiple-morphs-repos.stdout
@@ -0,0 +1,172 @@
+Initialized empty Git repository in TMP/repos/morphs1/.git/
+[master (root-commit) 0bfadac] initial
+ 2 files changed, 30 insertions(+), 0 deletions(-)
+ create mode 100644 stratum1.morph
+ create mode 100644 test-system.morph
+Initialized empty Git repository in TMP/repos/morphs2/.git/
+[master (root-commit) 06b1b9c] initial
+ 2 files changed, 15 insertions(+), 0 deletions(-)
+ create mode 100644 stratum2.morph
+ create mode 100644 stratum3.morph
+tag tag-across-multiple-repos
+Tagger: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+create tag
+
+commit 8fe8196c4eb87e5967d7b08eb8da9d7d0764c25d
+Author: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+ create tag
+
+diff --git a/stratum1.morph b/stratum1.morph
+index 93a2d04..bf622db 100644
+--- a/stratum1.morph
++++ b/stratum1.morph
+@@ -4,8 +4,10 @@
+ "build-depends": [
+ {
+ "morph": "stratum3",
+- "repo": "repos:morphs2",
+- "ref": "master"
++ "repo": "repos:morphs1",
++ "ref": "tag-across-multiple-repos",
++ "unpetrify-ref": "master",
++ "unpetrify-repo": "repos:morphs2"
+ }
+ ]
+ }
+diff --git a/stratum2.morph b/stratum2.morph
+new file mode 100644
+index 0000000..d27599c
+--- /dev/null
++++ b/stratum2.morph
+@@ -0,0 +1,13 @@
++{
++ "name": "stratum2",
++ "kind": "stratum",
++ "build-depends": [
++ {
++ "morph": "stratum3",
++ "repo": "repos:morphs1",
++ "ref": "tag-across-multiple-repos",
++ "unpetrify-ref": "master",
++ "unpetrify-repo": "repos:morphs2"
++ }
++ ]
++}
+diff --git a/stratum3.morph b/stratum3.morph
+new file mode 100644
+index 0000000..a735127
+--- /dev/null
++++ b/stratum3.morph
+@@ -0,0 +1,4 @@
++{
++ "name": "stratum3",
++ "kind": "stratum"
++}
+diff --git a/test-system.morph b/test-system.morph
+index 27806c1..c5e4d98 100644
+--- a/test-system.morph
++++ b/test-system.morph
+@@ -8,12 +8,15 @@
+ {
+ "morph": "stratum1",
+ "repo": "repos:morphs1",
+- "ref": "master"
++ "ref": "tag-across-multiple-repos",
++ "unpetrify-ref": "master"
+ },
+ {
+ "morph": "stratum2",
+- "repo": "repos:morphs2",
+- "ref": "master"
++ "repo": "repos:morphs1",
++ "ref": "tag-across-multiple-repos",
++ "unpetrify-ref": "master",
++ "unpetrify-repo": "repos:morphs2"
+ }
+ ]
+ }
+commit 8fe8196c4eb87e5967d7b08eb8da9d7d0764c25d
+Author: developer <developer@example.com>
+Date: Tue Jul 31 16:51:54 2012 +0000
+
+ create tag
+---
+ stratum1.morph | 6 ++++--
+ stratum2.morph | 13 +++++++++++++
+ stratum3.morph | 4 ++++
+ test-system.morph | 9 ++++++---
+ 4 files changed, 27 insertions(+), 5 deletions(-)
+
+diff --git a/stratum1.morph b/stratum1.morph
+index 93a2d04..bf622db 100644
+--- a/stratum1.morph
++++ b/stratum1.morph
+@@ -4,8 +4,10 @@
+ "build-depends": [
+ {
+ "morph": "stratum3",
+- "repo": "repos:morphs2",
+- "ref": "master"
++ "repo": "repos:morphs1",
++ "ref": "tag-across-multiple-repos",
++ "unpetrify-ref": "master",
++ "unpetrify-repo": "repos:morphs2"
+ }
+ ]
+ }
+diff --git a/stratum2.morph b/stratum2.morph
+new file mode 100644
+index 0000000..d27599c
+--- /dev/null
++++ b/stratum2.morph
+@@ -0,0 +1,13 @@
++{
++ "name": "stratum2",
++ "kind": "stratum",
++ "build-depends": [
++ {
++ "morph": "stratum3",
++ "repo": "repos:morphs1",
++ "ref": "tag-across-multiple-repos",
++ "unpetrify-ref": "master",
++ "unpetrify-repo": "repos:morphs2"
++ }
++ ]
++}
+diff --git a/stratum3.morph b/stratum3.morph
+new file mode 100644
+index 0000000..a735127
+--- /dev/null
++++ b/stratum3.morph
+@@ -0,0 +1,4 @@
++{
++ "name": "stratum3",
++ "kind": "stratum"
++}
+diff --git a/test-system.morph b/test-system.morph
+index 27806c1..c5e4d98 100644
+--- a/test-system.morph
++++ b/test-system.morph
+@@ -8,12 +8,15 @@
+ {
+ "morph": "stratum1",
+ "repo": "repos:morphs1",
+- "ref": "master"
++ "ref": "tag-across-multiple-repos",
++ "unpetrify-ref": "master"
+ },
+ {
+ "morph": "stratum2",
+- "repo": "repos:morphs2",
+- "ref": "master"
++ "repo": "repos:morphs1",
++ "ref": "tag-across-multiple-repos",
++ "unpetrify-ref": "master",
++ "unpetrify-repo": "repos:morphs2"
+ }
+ ]
+ }